大尺寸图片的加载问题

引言

在面试的过程中,经常会被问到的一个经典问题: 如果给定一个1000 * 20000(宽1000px,高20000px)的大图,怎样正常加载显示并且不发生OOM?

我们可以计算一下如果我们将上面所说的大图加载进来,会占用多少内存呢? 图片加载占用多少内存是由下面三个元素决定的:

  • 图片原始的宽度和高度
  • 图片的色彩空间
  • 图片的缩放比

1、图片原始的宽度和高度:图片的宽度和高度的乘积代表了图片总像素点的数量。 2、图片的色彩空间:每个像素点存储的信息,即每个像素点占用了多少字节,默认的情况下每个像素点用Bitmap.Config.ARGB8888来表示,即每个色彩通道占8个比特,即占用4个字节。我们可以通过在BitmapFactory.Options的inPreferredConfig属性进行调整,例如我们经常使用到的是Bitmap.Config.RGB565(不考虑透明度的情况下)。 3、图片的缩放比:即对原始图片的宽高进行缩放,对应的属性是BitmapFactory.Options的inSampleSize,这是采样率的意思,默认值为1,取值范围必须要大于等于1并且是2的倍数。例如,我们将inSampleSize设置为2,表示图片的原始宽高都变为原始的1/2,那么总像素点就变成了原始的1/4。

所以图片占用内存的公式为: 图片占用内存 =(原始宽 * 缩放比)* (原始高 * 缩放比)* 色彩空间

那么我们计算一下上面的图片占用的内存: 1000 * 20000 * 4 = 80000000 字节 = 80MB。 一张大图就占了80M的内存,如果在一些内存比较小的手机中,就很有可能发生OOM的现象。

那么到底应该如何加载这种大尺寸的图片才不至于让其占用太多内存,从而导致OOM出现呢?

图片采样加载

从上面的分析可以知道,图片的占用内存是由图片原始的宽度和高度、图片的色彩空间、图片的缩放比这三个因素决定的,图片的原始宽度和高度不能被改变了,那么我们可以从图片的色彩空间和图片的缩放比进行入手,这就引入了第一种方案,即图片的采样加载

我们直接来看该方案的代码实现:

private voidloadBigImg() {
        try {
            //从assets中读取图片的输入流
            InputStream inputStream = getAssets().open("test.jpg");
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            //获取图片的宽高
            BitmapFactory.decodeStream(inputStream, null, options);

            DisplayMetrics metrics = new DisplayMetrics();
            getWindowManager().getDefaultDisplay().getMetrics(metrics);
            //在布局中设置了ImageView的宽高位Match_Parent,因此这里就是屏幕的宽高
            int screenWidth = metrics.widthPixels;
            int screenHeight = metrics.heightPixels;

            inputStream.reset(); //不加这一行,在android 7.0以上的手机图片显示不出来
            BitmapFactory.Options newOptions = new BitmapFactory.Options();
            newOptions.inJustDecodeBounds = false;
            newOptions.inSampleSize = calculateInSampleSize(options, screenWidth, screenHeight);
            //图片的色彩空间采用 Bitmap.Config.RGB_565
            newOptions.inPreferredConfig = Bitmap.Config.RGB_565;
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, newOptions);
            mImgView.setImageBitmap(bitmap);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 计算图片缩放比
     * @param options 存储着原始图片的宽度和高度
     * @param requireWidth 需要缩放后的宽度
     * @param requireHeight 需要缩放后的高度
     * @return 图片缩放比
     */
    private int calculateInSampleSize(BitmapFactory.Options options, int requireWidth, int requireHeight) {
        int width = options.outWidth;
        int height = options.outHeight;
        int sampleSize = 1;
        while (width > requireWidth || height > requireHeight) {
            sampleSize *= 2;
            width = width / 2;
            height = height / 2;
        }
        return sampleSize;
    }

上述的代码从图片的色彩空间(采用Bitmap.Config.RGB_565)和图片的缩放比这两方面入手,将大图缩放到屏幕的宽高,最终在手机中图片的显示效果为:

我们可以看到,图片正常显示出来了,图片的高度完整地占据了屏幕高度。我们计算一下采用采样率加载的方式来加载这张大图,需要占用多少内存。 图片的原始宽度为568px,原始高度为16361,色彩空间为2字节,inSampleSize为8,那么所占内存=568 * 16361 * 1/8 * 1/8 * 2 = 290407字节 = 290KB。 可以看到采用该方式的确起到了将大尺寸图片缩放并且不会OOM的效果,但是同时也突出了一个问题,图片显示在屏幕上特别小,导致很多细节看的不清楚。

图片按区域加载

第二种加载方式是图片按区域的加载,这种方式可以做到不对图片进行缩放,只加载原图的局部区域,并通过手势的滑动来更新需要展示的原图区域。 首先我们来认识一下BitmapRegionDecoder这个类。 BitmapRegionDecoder主要用来显示图片的某一块区域,所以它应该提供一个方法来传入图片,并且提供一个方法来显示要展示的区域。

  • BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,支持的参数有文件路径、文件描述符、文件的输入流等。
  • BitmapRegionDecoder提供了decodeRegion方法来显示指定的区域,该方法支持的参数有Rect对象和BitmapFactory.Options。Rect对象表示需要展示的区域,而BitmapFactory.Options表示展示的区域所用到的inSampleSize、inPreferredConfig属性等。

下面来看具体的实例: 首先我们要自定义一个ImageView,叫做LargeImgView

package com.example.runningh.mydemo.test_big_img;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.io.IOException;
import java.io.InputStream;

/**
 * Created by hwldzh on 2018/7/8
 * 类描述:
 */
public class LargeImgView extends View {

    private BitmapRegionDecoder mDecoder;
    private int mImgWidth;
    private int mImgHeight;
    privatefloat mPreX;
    private float mPreY;
    private static BitmapFactory.Options options = new BitmapFactory.Options();
    static {
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }
    private Rect mRect = new Rect();

    public LargeImgView(Context context) {
        super(context);
    }

    public LargeImgView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
  
    //传入原始图片的输入流
    public void setInputStream(InputStream inputStream) {
        try {
            mDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, options);
            mImgWidth = mDecoder.getWidth(); //获取原始图片的宽度
            mImgHeight = mDecoder.getHeight(); //获取原始图片的高度

            requestLayout();
            invalidate();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mDecoder == null) {
            super.onDraw(canvas);
            return;
        }
        Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //默认从顶端开始展示
        mRect.left = 0;
        mRect.right = getMeasuredWidth();
        mRect.top = 0;
        mRect.bottom = getMeasuredHeight();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //这里需要优化的,因为在滑动的过程中不断重绘,肯定会造成卡顿
        int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mPreX = event.getX();
                mPreY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float curX = event.getX();
                float curY = event.getY();
                int deltaX = (int) (curX - mPreX);
                int deltaY = (int) (curY - mPreY);
                if (mImgWidth > getWidth()) {
                    mRect.offset(-deltaX, 0);
                    checkWidth();
                    invalidate();
                }
                if (mImgHeight > getHeight()) {
                    mRect.offset(0, -deltaY);
                    checkHeight();
                    invalidate();
                }
                mPreX = curX;
                mPreY = curY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    private void checkWidth() {
        if (mRect.right > mImgWidth) {
            mRect.right = mImgWidth;
            mRect.left = mImgWidth - getMeasuredWidth();
        }
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = getMeasuredWidth();
        }
    }

    private void checkHeight() {
        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = getMeasuredHeight();
        }
        if (mRect.bottom > mImgHeight) {
            mRect.bottom = mImgHeight;
            mRect.top = mImgHeight - getMeasuredHeight();
        }
    }
}

Activity:

private voidloadBigImg() {
        try {
            InputStream inputStream = getAssets().open("test.jpg");
            mImgView.setInputStream(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.runningh.mydemo.test_big_img.LargeImgView
        android:id="@+id/test_img_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"/>

</RelativeLayout>

最后展示出来的效果如下图所示:

总结: 使用BitmapRegionFactory显示局部区域,并且没有对图片进行压缩,并且解决了OOM的问题。 但是同时也出现了一个问题就是在滑动的时候会很快,因为每一次滑动都在重绘。 网上的解决方法说可以参考下《世界地图》的项目,将绘制放在单独的线程: https://github.com/johnnylambada/WorldMap/blob/master/library/src/com/sigseg/android/map/ImageSurfaceView.java

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO算法是一种实时目标检测算法,它可以在图像中检测和定位多个物体。在使用YOLO加载图片时,通常需要将图片进行预处理,将其缩放到模型能够接受的尺寸。 YOLO模型通常要求输入图片尺寸为正方形,常见的尺寸有416x416、608x608等。图片的大小可以根据具体的应用场景和硬件性能进行调整。 在加载图片之前,你可以使用图像处理库(如OpenCV)来读取图片,并将其调整为模型所需的尺寸。具体的步骤如下: 1. 使用图像处理库读取图片,获取其宽度和高度。 2. 根据模型要求的尺寸,选择一个合适的缩放比例。通常可以通过计算原始图片的宽度和高度与目标尺寸的比值来确定缩放比例。 3. 使用图像处理库将图片缩放到目标尺寸。 下面是一个示例代码片段,演示了如何使用OpenCV来加载图片并将其缩放到416x416的尺寸: ```python import cv2 def load_image(image_path, target_size): # 读取图片 image = cv2.imread(image_path) # 获取原始图片的宽度和高度 image_height, image_width = image.shape[:2] # 计算缩放比例 scale = min(target_size[0] / image_width, target_size[1] / image_height) # 缩放图片 resized_image = cv2.resize(image, None, fx=scale, fy=scale) # 创建一个目标尺寸的空白画布 target_image = np.zeros((target_size[1], target_size[0], 3), dtype=np.uint8) # 将缩放后的图片复制到目标画布中心 target_image[(target_size[1]-resized_image.shape[0])//2:(target_size[1]+resized_image.shape[0])//2, (target_size[0]-resized_image.shape[1])//2:(target_size[0]+resized_image.shape[1])//2] = resized_image return target_image # 加载图片并将其缩放到416x416的尺寸 image_path = 'path/to/your/image.jpg' target_size = (416, 416) resized_image = load_image(image_path, target_size) ``` 请注意,上述代码仅演示了如何将图片缩放到指定尺寸,并未涉及到YOLO模型的具体实现。在实际应用中,你还需要将缩放后的图片输入到YOLO模型中进行目标检测。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值