如何在 Android 上以质量损失最小的方式将位图压缩为 JPEG?
How to compress Bitmap as JPEG with least quality loss on Android?
这不是一个简单的问题,请仔细阅读!
我想处理 JPEG 文件并将其再次保存为 JPEG。问题是即使不进行操作,也会有显着(可见)的质量损失。
问题:我缺少什么选项或 API 能够在不损失质量的情况下重新压缩 JPEG(我知道这不太可能,但我认为我在下面描述的是不是可接受的伪影水平,尤其是质量=100)。
控制
我从文件中将其作为 Bitmap
加载:
BitmapFactory.Options options = new BitmapFactory.Options();
// explicitly state everything so the configuration is clear
options.inPreferredConfig = Config.ARGB_8888;
options.inDither = false; // shouldn't be used anyway since 8888 can store HQ pixels
options.inScaled = false;
options.inPremultiplied = false; // no alpha, but disable explicitly
options.inSampleSize = 1; // make sure pixels are 1:1
options.inPreferQualityOverSpeed = true; // doesn't make a difference
// I'm loading the highest possible quality without any scaling/sizing/manipulation
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
现在,要有一个对照图像进行比较,让我们将纯位图字节另存为 PNG:
bitmap.compress(PNG, 100/*ignored*/, new FileOutputStream("/sdcard/image.png"));
我将其与计算机上的原始 JPEG 图片进行了比较,没有视觉差异。
我还从 getPixels
中保存了原始 int[]
并将其作为原始 ARGB 文件加载到我的计算机上:与原始 JPEG 和从位图保存的 PNG 没有视觉差异。
我检查了位图的尺寸和配置,它们与源图像和输入选项匹配:它按预期解码为 ARGB_8888
。
以上对照检查证明内存中Bitmap中的像素是正确的
问题
我想要 JPEG 文件作为结果,所以上面的 PNG 和 RAW 方法不起作用,让我们先尝试保存为 JPEG 100%:
// 100% still expected lossy, but not this amount of artifacts
bitmap.compress(JPEG, 100, new FileOutputStream("/sdcard/image.jpg"));
我不确定它的度量是百分比,但是它更容易阅读和讨论,所以我会使用它。
我知道质量为 100% 的 JPEG 仍然有损,但它不应该在视觉上有损到从远处就能注意到的程度。这是同一来源的两个 100% 压缩的比较。
在单独的选项卡中打开它们,然后在它们之间来回单击以了解我的意思。差异图像是使用 Gimp 制作的:原始作为底层,用 "Grain extract" 模式重新压缩中间层,顶层全白用 "Value" 模式来增强坏度。
下面的图片上传到 Imgur,它也压缩了文件,但由于所有图片的压缩方式相同,因此原始不需要的伪像仍然可见,就像我打开原始图片时看到的一样文件。
原始[560k]:
Imgur 与原始版本的区别(与问题无关,只是为了表明它在上传图像时不会导致任何 extra 伪像):
IrfanView 100% [728k](视觉上与原版相同):
IrfanView 与原始版本有 100% 的差异(几乎没有)
Android 100% [942k]:
Android 与原始版本有 100% 的差异(着色、条纹、涂抹)
在 IrfanView 中,我必须低于 50% [50k] 才能看到类似的效果。在 IrfanView 的 70% [100k] 中没有明显差异,但大小是 Android 的第 9。
背景
我创建了一个从相机 API 拍摄照片的应用程序,该图像以 byte[]
形式出现并且是编码的 JPEG blob。我通过 OutputStream.write(byte[])
方法保存了这个文件,这是我的原始源文件。 decodeByteArray(data, 0, data.length, options)
解码与从文件读取相同的像素,已使用 Bitmap.sameAs
进行测试,因此与问题无关。
我正在使用带有 Android 4.4.2 的 Samsung Galaxy S4 进行测试。
编辑:在进一步调查的同时,我还尝试了 Android 6.0 和 N 预览模拟器,它们重现了同样的问题。
经过一些调查,我发现了罪魁祸首:Skia 的 YCbCr 转换。 Repro,调查代码和解决方案可以在TWiStErRob/AndroidJPEG.
找到
发现
在没有得到关于这个问题的积极回应之后(也没有来自 http://b.android.com/206128) I started digging deeper. I found numerous half-informed SO answers which helped me tremendously in discovering bits and pieces. One such answer was 这让我意识到 YuvImage
将 YUV NV21 字节数组转换为 JPEG 压缩字节数组:
YuvImage yuv = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), 100, jpeg);
创建 YUV 数据有很大的自由度,具有不同的常量和精度。根据我的问题,很明显 Android 使用了不正确的算法。
在使用我在网上找到的算法和常量时,我总是得到一个糟糕的图像:亮度改变或与问题中的条带问题相同。
深入挖掘
YuvImage
其实在调用Bitmap.compress
的时候并没有用到,这里是Bitmap.compress
的堆栈:
- libjpeg/
jpeg_write_scanlines
(jcapistd.c:77)
- 斯基亚/
rgb2yuv_32
(SkImageDecoder_libjpeg.cpp:913)
- 斯基亚/
writer(=Write_32_YUV).write
(SkImageDecoder_libjpeg.cpp:961)
[WE_CONVERT_TO_YUV
无条件定义]
SkJPEGImageEncoder::onEncode
(SkImageDecoder_libjpeg.cpp:1046)
SkImageEncoder::encodeStream
(SkImageEncoder.cpp:15)
Bitmap_compress
(Bitmap.cpp:383)
Bitmap.nativeCompress
(Bitmap.java:1573)
Bitmap.compress
(Bitmap.java:984)
app.saveBitmapAsJPEG
()
和使用堆栈 YuvImage
- libjpeg/
jpeg_write_raw_data
(jcapistd.c:120)
YuvToJpegEncoder::compress
(YuvToJpegEncoder.cpp:71)
YuvToJpegEncoder::encode
(YuvToJpegEncoder.cpp:24)
YuvImage_compressToJpeg
(YuvToJpegEncoder.cpp:219)
YuvImage.nativeCompressToJpeg
(YuvImage.java:141)
YuvImage.compressToJpeg
(YuvImage.java:123)
app.saveNV21AsJPEG
()
通过使用 Bitmap.compress
流程中 rgb2yuv_32
中的常量,我能够使用 YuvImage
重新创建相同的条带效果,不是成就,只是确认它确实是乱七八糟的 YUV 转换。我仔细检查了问题不在 YuvImage
调用 libjpeg
期间:通过将位图的 ARGB 转换为 YUV 再转换回 RGB,然后将生成的像素 blob 转储为原始图像,条带已经存在。
在这样做时我意识到 NV21/YUV420SP 布局是有损的,因为它每 4 个像素采样一次颜色信息, 但是 它保持每个像素的值(亮度)像素这意味着一些颜色信息丢失了,但无论如何人们眼睛的大部分信息都在亮度中。看看example on wikipedia,Cb 和 Cr 通道使图像几乎无法识别,因此对其进行有损采样无关紧要。
解决方案
所以,此时我知道 libjpeg 在传递正确的原始数据时会进行正确的转换。这是我设置 NDK 并集成来自 http://www.ijg.org. I was able to confirm that indeed passing the RGB data from the Bitmap's pixels array yields the expected result. I like to avoid using native components when not absolutely necessary, so aside of going for a native library that encodes a Bitmap I found a neat workaround. I've essentially taken the rgb_ycc_convert
function from jcolor.c
and rewrote it in Java using the skeleton from 的最新 LibJPEG 的时候。下面没有针对速度进行优化,但为了可读性,为了简洁起见删除了一些常量,您可以在 libjpeg 代码或我的示例项目中找到它们。
private static final int JSAMPLE_SIZE = 255 + 1;
private static final int CENTERJSAMPLE = 128;
private static final int SCALEBITS = 16;
private static final int CBCR_OFFSET = CENTERJSAMPLE << SCALEBITS;
private static final int ONE_HALF = 1 << (SCALEBITS - 1);
private static final int[] rgb_ycc_tab = new int[TABLE_SIZE];
static { // rgb_ycc_start
for (int i = 0; i <= JSAMPLE_SIZE; i++) {
rgb_ycc_tab[R_Y_OFFSET + i] = FIX(0.299) * i;
rgb_ycc_tab[G_Y_OFFSET + i] = FIX(0.587) * i;
rgb_ycc_tab[B_Y_OFFSET + i] = FIX(0.114) * i + ONE_HALF;
rgb_ycc_tab[R_CB_OFFSET + i] = -FIX(0.168735892) * i;
rgb_ycc_tab[G_CB_OFFSET + i] = -FIX(0.331264108) * i;
rgb_ycc_tab[B_CB_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[R_CR_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[G_CR_OFFSET + i] = -FIX(0.418687589) * i;
rgb_ycc_tab[B_CR_OFFSET + i] = -FIX(0.081312411) * i;
}
}
static void rgb_ycc_convert(int[] argb, int width, int height, byte[] ycc) {
int[] tab = LibJPEG.rgb_ycc_tab;
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = (argb[index] & 0x00ff0000) >> 16;
int g = (argb[index] & 0x0000ff00) >> 8;
int b = (argb[index] & 0x000000ff) >> 0;
byte Y = (byte)((tab[r + R_Y_OFFSET] + tab[g + G_Y_OFFSET] + tab[b + B_Y_OFFSET]) >> SCALEBITS);
byte Cb = (byte)((tab[r + R_CB_OFFSET] + tab[g + G_CB_OFFSET] + tab[b + B_CB_OFFSET]) >> SCALEBITS);
byte Cr = (byte)((tab[r + R_CR_OFFSET] + tab[g + G_CR_OFFSET] + tab[b + B_CR_OFFSET]) >> SCALEBITS);
ycc[yIndex++] = Y;
if (y % 2 == 0 && index % 2 == 0) {
ycc[uvIndex++] = Cr;
ycc[uvIndex++] = Cb;
}
index++;
}
}
}
static byte[] compress(Bitmap bitmap) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] argb = new int[w * h];
bitmap.getPixels(argb, 0, w, 0, 0, w, h);
byte[] ycc = new byte[w * h * 3 / 2];
rgb_ycc_convert(argb, w, h, ycc);
argb = null; // let GC do its job
ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
YuvImage yuvImage = new YuvImage(ycc, ImageFormat.NV21, w, h, null);
yuvImage.compressToJpeg(new Rect(0, 0, w, h), quality, jpeg);
return jpeg.toByteArray();
}
魔法钥匙似乎是 ONE_HALF - 1
其余的看起来非常像 Skia 中的数学。这是未来调查的一个很好的方向,但对我来说,上面的内容足够简单,可以作为解决 Android 的内置怪异问题的一个很好的解决方案,尽管速度较慢。 请注意,此解决方案使用 NV21 布局,丢失了 3/4 的颜色信息(来自 Cr/Cb),但这种损失比 Skia 的数学产生的错误要小得多。另请注意,YuvImage
不支持奇数大小的图像,有关详细信息,请参阅 。
请使用以下方法:
public String convertBitmaptoSmallerSizetoString(String image){
File imageFile = new File(image);
Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath());
int nh = (int) (bitmap.getHeight() * (512.0 / bitmap.getWidth()));
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, 512, nh, true);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaled.compress(Bitmap.CompressFormat.PNG, 90, stream);
byte[] imageByte = stream.toByteArray();
String img_str = Base64.encodeToString(imageByte, Base64.NO_WRAP);
return img_str;
}
下面是我的代码:
public static String compressImage(Context context, String imagePath)
{
final float maxHeight = 1024.0f;
final float maxWidth = 1024.0f;
Bitmap scaledBitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(imagePath, options);
int actualHeight = options.outHeight;
int actualWidth = options.outWidth;
float imgRatio = (float) actualWidth / (float) actualHeight;
float maxRatio = maxWidth / maxHeight;
if (actualHeight > maxHeight || actualWidth > maxWidth) {
if (imgRatio < maxRatio) {
imgRatio = maxHeight / actualHeight;
actualWidth = (int) (imgRatio * actualWidth);
actualHeight = (int) maxHeight;
} else if (imgRatio > maxRatio) {
imgRatio = maxWidth / actualWidth;
actualHeight = (int) (imgRatio * actualHeight);
actualWidth = (int) maxWidth;
} else {
actualHeight = (int) maxHeight;
actualWidth = (int) maxWidth;
}
}
options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPurgeable = true;
options.inInputShareable = true;
options.inTempStorage = new byte[16 * 1024];
try {
bmp = BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
try {
scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.RGB_565);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
float ratioX = actualWidth / (float) options.outWidth;
float ratioY = actualHeight / (float) options.outHeight;
float middleX = actualWidth / 2.0f;
float middleY = actualHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
assert scaledBitmap != null;
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
if (bmp != null) {
bmp.recycle();
}
ExifInterface exif;
try {
exif = new ExifInterface(imagePath);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
Matrix matrix = new Matrix();
if (orientation == 6) {
matrix.postRotate(90);
} else if (orientation == 3) {
matrix.postRotate(180);
} else if (orientation == 8) {
matrix.postRotate(270);
}
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream out = null;
String filepath = getFilename(context);
try {
out = new FileOutputStream(filepath);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return filepath;
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
final float totalPixels = width * height;
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
return inSampleSize;
}
public static String getFilename(Context context) {
File mediaStorageDir = new File(Environment.getExternalStorageDirectory()
+ "/Android/data/"
+ context.getApplicationContext().getPackageName()
+ "/Files/Compressed");
if (!mediaStorageDir.exists()) {
mediaStorageDir.mkdirs();
}
String mImageName = "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg";
return (mediaStorageDir.getAbsolutePath() + "/" + mImageName);
}
这不是一个简单的问题,请仔细阅读!
我想处理 JPEG 文件并将其再次保存为 JPEG。问题是即使不进行操作,也会有显着(可见)的质量损失。 问题:我缺少什么选项或 API 能够在不损失质量的情况下重新压缩 JPEG(我知道这不太可能,但我认为我在下面描述的是不是可接受的伪影水平,尤其是质量=100)。
控制
我从文件中将其作为 Bitmap
加载:
BitmapFactory.Options options = new BitmapFactory.Options();
// explicitly state everything so the configuration is clear
options.inPreferredConfig = Config.ARGB_8888;
options.inDither = false; // shouldn't be used anyway since 8888 can store HQ pixels
options.inScaled = false;
options.inPremultiplied = false; // no alpha, but disable explicitly
options.inSampleSize = 1; // make sure pixels are 1:1
options.inPreferQualityOverSpeed = true; // doesn't make a difference
// I'm loading the highest possible quality without any scaling/sizing/manipulation
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
现在,要有一个对照图像进行比较,让我们将纯位图字节另存为 PNG:
bitmap.compress(PNG, 100/*ignored*/, new FileOutputStream("/sdcard/image.png"));
我将其与计算机上的原始 JPEG 图片进行了比较,没有视觉差异。
我还从 getPixels
中保存了原始 int[]
并将其作为原始 ARGB 文件加载到我的计算机上:与原始 JPEG 和从位图保存的 PNG 没有视觉差异。
我检查了位图的尺寸和配置,它们与源图像和输入选项匹配:它按预期解码为 ARGB_8888
。
以上对照检查证明内存中Bitmap中的像素是正确的
问题
我想要 JPEG 文件作为结果,所以上面的 PNG 和 RAW 方法不起作用,让我们先尝试保存为 JPEG 100%:
// 100% still expected lossy, but not this amount of artifacts
bitmap.compress(JPEG, 100, new FileOutputStream("/sdcard/image.jpg"));
我不确定它的度量是百分比,但是它更容易阅读和讨论,所以我会使用它。
我知道质量为 100% 的 JPEG 仍然有损,但它不应该在视觉上有损到从远处就能注意到的程度。这是同一来源的两个 100% 压缩的比较。
在单独的选项卡中打开它们,然后在它们之间来回单击以了解我的意思。差异图像是使用 Gimp 制作的:原始作为底层,用 "Grain extract" 模式重新压缩中间层,顶层全白用 "Value" 模式来增强坏度。
下面的图片上传到 Imgur,它也压缩了文件,但由于所有图片的压缩方式相同,因此原始不需要的伪像仍然可见,就像我打开原始图片时看到的一样文件。
原始[560k]:
在 IrfanView 中,我必须低于 50% [50k] 才能看到类似的效果。在 IrfanView 的 70% [100k] 中没有明显差异,但大小是 Android 的第 9。
背景
我创建了一个从相机 API 拍摄照片的应用程序,该图像以 byte[]
形式出现并且是编码的 JPEG blob。我通过 OutputStream.write(byte[])
方法保存了这个文件,这是我的原始源文件。 decodeByteArray(data, 0, data.length, options)
解码与从文件读取相同的像素,已使用 Bitmap.sameAs
进行测试,因此与问题无关。
我正在使用带有 Android 4.4.2 的 Samsung Galaxy S4 进行测试。 编辑:在进一步调查的同时,我还尝试了 Android 6.0 和 N 预览模拟器,它们重现了同样的问题。
经过一些调查,我发现了罪魁祸首:Skia 的 YCbCr 转换。 Repro,调查代码和解决方案可以在TWiStErRob/AndroidJPEG.
找到发现
在没有得到关于这个问题的积极回应之后(也没有来自 http://b.android.com/206128) I started digging deeper. I found numerous half-informed SO answers which helped me tremendously in discovering bits and pieces. One such answer was 这让我意识到 YuvImage
将 YUV NV21 字节数组转换为 JPEG 压缩字节数组:
YuvImage yuv = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), 100, jpeg);
创建 YUV 数据有很大的自由度,具有不同的常量和精度。根据我的问题,很明显 Android 使用了不正确的算法。 在使用我在网上找到的算法和常量时,我总是得到一个糟糕的图像:亮度改变或与问题中的条带问题相同。
深入挖掘
YuvImage
其实在调用Bitmap.compress
的时候并没有用到,这里是Bitmap.compress
的堆栈:
- libjpeg/
jpeg_write_scanlines
(jcapistd.c:77) - 斯基亚/
rgb2yuv_32
(SkImageDecoder_libjpeg.cpp:913) - 斯基亚/
writer(=Write_32_YUV).write
(SkImageDecoder_libjpeg.cpp:961)
[WE_CONVERT_TO_YUV
无条件定义] SkJPEGImageEncoder::onEncode
(SkImageDecoder_libjpeg.cpp:1046)SkImageEncoder::encodeStream
(SkImageEncoder.cpp:15)Bitmap_compress
(Bitmap.cpp:383)Bitmap.nativeCompress
(Bitmap.java:1573)Bitmap.compress
(Bitmap.java:984)app.saveBitmapAsJPEG
()
和使用堆栈 YuvImage
- libjpeg/
jpeg_write_raw_data
(jcapistd.c:120) YuvToJpegEncoder::compress
(YuvToJpegEncoder.cpp:71)YuvToJpegEncoder::encode
(YuvToJpegEncoder.cpp:24)YuvImage_compressToJpeg
(YuvToJpegEncoder.cpp:219)YuvImage.nativeCompressToJpeg
(YuvImage.java:141)YuvImage.compressToJpeg
(YuvImage.java:123)app.saveNV21AsJPEG
()
通过使用 Bitmap.compress
流程中 rgb2yuv_32
中的常量,我能够使用 YuvImage
重新创建相同的条带效果,不是成就,只是确认它确实是乱七八糟的 YUV 转换。我仔细检查了问题不在 YuvImage
调用 libjpeg
期间:通过将位图的 ARGB 转换为 YUV 再转换回 RGB,然后将生成的像素 blob 转储为原始图像,条带已经存在。
在这样做时我意识到 NV21/YUV420SP 布局是有损的,因为它每 4 个像素采样一次颜色信息, 但是 它保持每个像素的值(亮度)像素这意味着一些颜色信息丢失了,但无论如何人们眼睛的大部分信息都在亮度中。看看example on wikipedia,Cb 和 Cr 通道使图像几乎无法识别,因此对其进行有损采样无关紧要。
解决方案
所以,此时我知道 libjpeg 在传递正确的原始数据时会进行正确的转换。这是我设置 NDK 并集成来自 http://www.ijg.org. I was able to confirm that indeed passing the RGB data from the Bitmap's pixels array yields the expected result. I like to avoid using native components when not absolutely necessary, so aside of going for a native library that encodes a Bitmap I found a neat workaround. I've essentially taken the rgb_ycc_convert
function from jcolor.c
and rewrote it in Java using the skeleton from 的最新 LibJPEG 的时候。下面没有针对速度进行优化,但为了可读性,为了简洁起见删除了一些常量,您可以在 libjpeg 代码或我的示例项目中找到它们。
private static final int JSAMPLE_SIZE = 255 + 1;
private static final int CENTERJSAMPLE = 128;
private static final int SCALEBITS = 16;
private static final int CBCR_OFFSET = CENTERJSAMPLE << SCALEBITS;
private static final int ONE_HALF = 1 << (SCALEBITS - 1);
private static final int[] rgb_ycc_tab = new int[TABLE_SIZE];
static { // rgb_ycc_start
for (int i = 0; i <= JSAMPLE_SIZE; i++) {
rgb_ycc_tab[R_Y_OFFSET + i] = FIX(0.299) * i;
rgb_ycc_tab[G_Y_OFFSET + i] = FIX(0.587) * i;
rgb_ycc_tab[B_Y_OFFSET + i] = FIX(0.114) * i + ONE_HALF;
rgb_ycc_tab[R_CB_OFFSET + i] = -FIX(0.168735892) * i;
rgb_ycc_tab[G_CB_OFFSET + i] = -FIX(0.331264108) * i;
rgb_ycc_tab[B_CB_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[R_CR_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[G_CR_OFFSET + i] = -FIX(0.418687589) * i;
rgb_ycc_tab[B_CR_OFFSET + i] = -FIX(0.081312411) * i;
}
}
static void rgb_ycc_convert(int[] argb, int width, int height, byte[] ycc) {
int[] tab = LibJPEG.rgb_ycc_tab;
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = (argb[index] & 0x00ff0000) >> 16;
int g = (argb[index] & 0x0000ff00) >> 8;
int b = (argb[index] & 0x000000ff) >> 0;
byte Y = (byte)((tab[r + R_Y_OFFSET] + tab[g + G_Y_OFFSET] + tab[b + B_Y_OFFSET]) >> SCALEBITS);
byte Cb = (byte)((tab[r + R_CB_OFFSET] + tab[g + G_CB_OFFSET] + tab[b + B_CB_OFFSET]) >> SCALEBITS);
byte Cr = (byte)((tab[r + R_CR_OFFSET] + tab[g + G_CR_OFFSET] + tab[b + B_CR_OFFSET]) >> SCALEBITS);
ycc[yIndex++] = Y;
if (y % 2 == 0 && index % 2 == 0) {
ycc[uvIndex++] = Cr;
ycc[uvIndex++] = Cb;
}
index++;
}
}
}
static byte[] compress(Bitmap bitmap) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] argb = new int[w * h];
bitmap.getPixels(argb, 0, w, 0, 0, w, h);
byte[] ycc = new byte[w * h * 3 / 2];
rgb_ycc_convert(argb, w, h, ycc);
argb = null; // let GC do its job
ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
YuvImage yuvImage = new YuvImage(ycc, ImageFormat.NV21, w, h, null);
yuvImage.compressToJpeg(new Rect(0, 0, w, h), quality, jpeg);
return jpeg.toByteArray();
}
魔法钥匙似乎是 ONE_HALF - 1
其余的看起来非常像 Skia 中的数学。这是未来调查的一个很好的方向,但对我来说,上面的内容足够简单,可以作为解决 Android 的内置怪异问题的一个很好的解决方案,尽管速度较慢。 请注意,此解决方案使用 NV21 布局,丢失了 3/4 的颜色信息(来自 Cr/Cb),但这种损失比 Skia 的数学产生的错误要小得多。另请注意,YuvImage
不支持奇数大小的图像,有关详细信息,请参阅
请使用以下方法:
public String convertBitmaptoSmallerSizetoString(String image){
File imageFile = new File(image);
Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath());
int nh = (int) (bitmap.getHeight() * (512.0 / bitmap.getWidth()));
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, 512, nh, true);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaled.compress(Bitmap.CompressFormat.PNG, 90, stream);
byte[] imageByte = stream.toByteArray();
String img_str = Base64.encodeToString(imageByte, Base64.NO_WRAP);
return img_str;
}
下面是我的代码:
public static String compressImage(Context context, String imagePath)
{
final float maxHeight = 1024.0f;
final float maxWidth = 1024.0f;
Bitmap scaledBitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(imagePath, options);
int actualHeight = options.outHeight;
int actualWidth = options.outWidth;
float imgRatio = (float) actualWidth / (float) actualHeight;
float maxRatio = maxWidth / maxHeight;
if (actualHeight > maxHeight || actualWidth > maxWidth) {
if (imgRatio < maxRatio) {
imgRatio = maxHeight / actualHeight;
actualWidth = (int) (imgRatio * actualWidth);
actualHeight = (int) maxHeight;
} else if (imgRatio > maxRatio) {
imgRatio = maxWidth / actualWidth;
actualHeight = (int) (imgRatio * actualHeight);
actualWidth = (int) maxWidth;
} else {
actualHeight = (int) maxHeight;
actualWidth = (int) maxWidth;
}
}
options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPurgeable = true;
options.inInputShareable = true;
options.inTempStorage = new byte[16 * 1024];
try {
bmp = BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
try {
scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.RGB_565);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
float ratioX = actualWidth / (float) options.outWidth;
float ratioY = actualHeight / (float) options.outHeight;
float middleX = actualWidth / 2.0f;
float middleY = actualHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
assert scaledBitmap != null;
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
if (bmp != null) {
bmp.recycle();
}
ExifInterface exif;
try {
exif = new ExifInterface(imagePath);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
Matrix matrix = new Matrix();
if (orientation == 6) {
matrix.postRotate(90);
} else if (orientation == 3) {
matrix.postRotate(180);
} else if (orientation == 8) {
matrix.postRotate(270);
}
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream out = null;
String filepath = getFilename(context);
try {
out = new FileOutputStream(filepath);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return filepath;
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
final float totalPixels = width * height;
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
return inSampleSize;
}
public static String getFilename(Context context) {
File mediaStorageDir = new File(Environment.getExternalStorageDirectory()
+ "/Android/data/"
+ context.getApplicationContext().getPackageName()
+ "/Files/Compressed");
if (!mediaStorageDir.exists()) {
mediaStorageDir.mkdirs();
}
String mImageName = "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg";
return (mediaStorageDir.getAbsolutePath() + "/" + mImageName);
}