- 这不是一个简单的问题,请仔细阅读!*
我想处理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没有视觉差异,也没有从Bitmap保存的PNG。
我检查了位图的尺寸和配置,它们与源图像和输入选项匹配:它被解码为ARGB_8888
,正如预期的那样。
- 以上控制检查证明内存位图中的像素正确。*
问题
我想有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制作差异图像:底层为原始,中间层以“谷物提取物”模式重新压缩,顶层以“价值”模式全白色以增强不良。
- 下面的图片被上传到Imgur,它也压缩了文件,但由于所有的图片都被压缩了,所以原始的不需要的伪影仍然可见,就像我打开原始文件时看到的那样。
原始[560 k]:x1c 0d1x Imgur与原始图像的差异(与问题无关,只是为了表明上传图像时不会导致任何 * 额外 * 伪影):
IrfanView 100% [728 k](视觉上与原始图像相同):
IrfanView 100%与原始版本的差异(几乎没有任何差异)
Android 100% [942 k]:x1c4d 1x Android 100%的差异,以原来的(着色,带,涂抹)
在IrfanView中,我必须低于50% [50 k]才能看到远程类似的效果。IrfanView中的70% [100 k]没有明显的差异,但大小是Android的9分之一。
后台
我创建了一个应用程序,从相机API拍摄照片,该图像为byte[]
,是一个编码的JPEG斑点。我通过OutputStream.write(byte[])
方法保存了这个文件,这是我的原始源文件。decodeByteArray(data, 0, data.length, options)
解码与从文件阅读相同的像素,使用Bitmap.sameAs
进行测试,因此与问题无关。
我正在使用我的三星Galaxy S4与Android 4.4.2来测试。编辑:在进一步调查的同时,我还尝试了Android 6. 0和N预览模拟器,它们重现了同样的问题。
3条答案
按热度按时间o0lyfsai1#
经过一番调查,我找到了罪魁祸首:Skia的YCbCr转换。复制,调查和解决方案的代码可以在TWiStErRob/AndroidJPEG找到。
发现
在这个问题上没有得到积极的回应(也没有来自http://b.android.com/206128-> https://issuetracker.google.com/issues/37092486)之后,我开始深入挖掘。我发现了许多半真半假的SO答案,这些答案极大地帮助我发现了点点滴滴。一个这样的答案是https://stackoverflow.com/a/13055615/253468,这让我意识到
YuvImage
可以将YUV NV 21字节数组转换为JPEG压缩字节数组:创建YUV数据有很大的自由度,具有不同的常数和精度。从我的问题很明显,Android使用了一个不正确的算法。在玩我在网上找到的算法和常量时,我总是得到一个不好的形象:亮度改变或具有与问题中相同的条带问题。
深入挖掘
调用
Bitmap.compress
时实际上没有使用YuvImage
,下面是Bitmap.compress
的堆栈: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
的堆栈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
期间:通过将Bitmap的ARGB转换为YUV,再转换回RGB,然后将得到的像素斑点作为原始图像转储,条带已经存在。在这样做的时候,我意识到NV 21/YUV 420 SP布局是有损耗的,因为它每4个像素采样一次颜色信息,但它保留了每个像素的值(亮度),这意味着一些颜色信息丢失了,但人们眼睛的大部分信息都在亮度中。看看wikipedia上的example,Cb和Cr通道几乎无法识别图像,因此有损采样并不重要。
解决方案
所以,在这一点上,我知道libjpeg在传递正确的原始数据时会进行正确的转换。这是我设置NDK并集成来自http://www.ijg.org的最新LibJPEG的时候。我可以确认,从位图的像素数组传递RGB数据确实会产生预期的结果。我喜欢避免在非绝对必要的情况下使用本机组件,所以除了使用编码位图的本机库之外,我发现了一个整洁的解决方案。实际上,我从
jcolor.c
中提取了rgb_ycc_convert
函数,并使用https://stackoverflow.com/a/13055615/253468的框架在Java中重写了它。下面的代码并不是为了速度而优化的,但是为了可读性,为了简洁,一些常量被删除了,你可以在libjpeg代码或我的示例项目中找到它们。神奇的钥匙似乎是
ONE_HALF - 1
,其余的看起来很像Skia中的数学。这是未来研究的一个很好的方向,但对我来说,上面的方法足够简单,可以作为解决Android内置怪异问题的好方法,尽管速度较慢。**请注意,此解决方案使用NV 21布局,丢失了3/4的颜色信息(来自Cr/Cb),但这种损失比Skia的数学产生的误差要小得多。**还请注意,YuvImage
不支持奇数大小的图像,有关更多信息,请参阅NV21 format and odd image dimensions。yc0p9oo02#
请使用以下方法:
drnojrws3#
下面是我的代码: