android 图片压缩算法-luban

图片压缩算法-luban

luban 鲁班算法是号称最接近微信朋友圈图片压缩算法的一种图片压缩算法,GitHub 地址:

https://github.com/Curzibn/Luban

根据作者提供的数据,压缩效果如下:

内容 原图 Luban Wechat
截屏 720P 720*1280,390k 720*1280,87k 720*1280,56k
截屏 1080P 1080*1920,2.21M 1080*1920,104k 1080*1920,112k
拍照 13M(4:3) 3096*4128,3.12M 1548*2064,141k 1548*2064,147k
拍照 9.6M(16:9) 4128*2322,4.64M 1032*581,97k 1032*581,74k
滚动截屏 1080*6433,1.56M 1080*6433,351k 1080*6433,482k

压缩原理

这里的压缩,指的是,不影响分辨率的情况下,对图片进行压缩,那么图片压缩的原理是什么呢?假设一张原始,例如小米五相机拍摄的图片,分辨率是 3456 X 4608 px,每个像素使用 32位表示,那么所占内存如下:

3456 X 4608 X 4 = 63700992 字节 =60.75 MB,然而实际上,却不是,我们看到的原图大小,可能是5 MB 左右,那么实际上, jpg 格式采用了特殊的编码格式,所以实际上,我们可以对 jpg 格式进行多次压缩,根据维基百科资料显示,在原始 jpg 图片压缩为 六分一的 质量之后,人眼是无法察觉有损失 差距的。

压缩操作代码实现分析

压缩一个图片的代码如下:

Luban.with(this)
    .load(photos)//设置压缩图片文件路径,全路径
    .ignoreBy(100)//设置忽略压缩的大小上限
    .setTargetDir(getPath())//设置压缩输出文件目录
    .setCompressListener(new OnCompressListener() {
      @Override
      public void onStart() {
      }

      @Override
      public void onSuccess(File file) {
        showResult(photos, file);
      }

      @Override
      public void onError(Throwable e) {
      }
    }).launch();

主要看 launch() 方法,如下:

@UiThread private void launch(final Context context) {
    //检查路径准确性以及是否设置了回调监听器
  if (mPaths == null || mPaths.size() == 0 && mCompressListener != null) {
    mCompressListener.onError(new NullPointerException("image file cannot be null"));
  }

  Iterator iterator = mPaths.iterator();
  while (iterator.hasNext()) {
    final String path = iterator.next();
    if (Checker.isImage(path)) {
        // 使用单线程池执行器,一次只能执行一个线程
      AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
        @Override public void run() {
          try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));//发送开始压缩的信息
            //开始压缩,实际起压缩作用的是 Engine 类的 compress() 方法
            File result = Checker.isNeedCompress(mLeastCompressSize, path) ?
                new Engine(path, getImageCacheFile(context, Checker.checkSuffix(path))).compress() :
                new File(path);

            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
          } catch (IOException e) {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
          }
        }
      });
    } else {
      mCompressListener.onError(new IllegalArgumentException("can not read the path : " + path));
    }
    iterator.remove();
  }
}

然后这里实际起作用的是 Engine 的 compress() 方法,首先会创建一个新的 Engine() 对象,调用方法如下:

Engine(String srcImg, File tagImg) throws IOException {
  if (Checker.isJPG(srcImg)) {//判断格式是否 jpg 或者 jpeg
    this.srcExif = new ExifInterface(srcImg);//创建 ExifInterface 对象,这个对象用于图片读取,旋转,生成缩略图
  }
  this.tagImg = tagImg;
  this.srcImg = srcImg;

  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inJustDecodeBounds = true;//设置读取方法为只读取边界大小
  options.inSampleSize = 1;

  BitmapFactory.decodeFile(srcImg, options);
  this.srcWidth = options.outWidth;//设置为图片原始宽高
  this.srcHeight = options.outHeight;
}

Engine 类里面的值都是通过 Luban.Builder 传递过来的。

具体的压缩比例计算,在 computeSize() 方法里面,computeSize() 方法主要是根据图片的宽高比,对图片进行压缩,这个方法的返回值,设置的是

private int computeSize() {
    //将 srcWidth 和 srcHeight 设置为偶数,方便除法计算
  srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
  srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

  int longSide = Math.max(srcWidth, srcHeight);
  int shortSide = Math.min(srcWidth, srcHeight);

  float scale = ((float) shortSide / longSide);
  if (scale <= 1 && scale > 0.5625) {
    if (longSide < 1664) {
      return 1;
    } else if (longSide >= 1664 && longSide < 4990) {
      return 2;
    } else if (longSide > 4990 && longSide < 10240) {
      return 4;
    } else {
      return longSide / 1280 == 0 ? 1 : longSide / 1280;
    }
  } else if (scale <= 0.5625 && scale > 0.5) {
    return longSide / 1280 == 0 ? 1 : longSide / 1280;
  } else {
    return (int) Math.ceil(longSide / (1280.0 / scale));
  }
}
  1. 判断图片比例值,是否处于以下区间内;

    • [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
    • [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
    • [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
  2. 判断图片最长边是否过边界值;

    • [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
    • [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
    • [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
  3. 计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)

  4. 计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。

size = (newW * newH) / (width * height) * m;

  • [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
  • [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
  • [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值

    1. 判断第4步的size是否过小
  • [1, 0.5625) 则最小 size 对应 60,60,100

  • [0.5625, 0.5) 则最小 size 都为 100
  • [0.5, 0) 则最小 size 都为 100

    1. 将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值

那么为什么会采用这种压缩方式呢?

我们很简单的可以理解,移动设备的分辨率有限,我们只需要保证,能够压缩的图片能够在主流分辨率上能够达到合适的显示效果,并且最大可能的去压缩图片分辨率,这就是图片压缩的初衷。所以,合理之处便是在于设置这个分辨率了,luban 算法就是起了这个效果吧。

最终执行压缩操作的方法 cmopress() 方法

File compress() throws IOException {
  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inSampleSize = computeSize();//根据图片比例,设置压缩比例

  Bitmap tagBitmap = BitmapFactory.decodeFile(srcImg, options);//压缩图片
  ByteArrayOutputStream stream = new ByteArrayOutputStream();

  tagBitmap = rotatingImage(tagBitmap);
  tagBitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
  tagBitmap.recycle();//将 bitmap 写入到输入流

  FileOutputStream fos = new FileOutputStream(tagImg);//将输入流写入文件
  fos.write(stream.toByteArray());
  fos.flush();
  fos.close();
  stream.close();

  return tagImg;
}

这里比较简单,就不多阐述了。

你可能感兴趣的:(android,图片专题,android,Bitmap,压缩,图片压缩,luban,算法解析)