草庐IT

android - 如何在Android上以最少的质量损失将位图压缩为JPEG?

coder 2023-06-09 原文

这不是一个简单的问题,请通读!

我想操作一个 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 图像进行了比较,没有任何视觉差异。

我还保存了原始 int[]来自 getPixels并将其作为原始 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 与原始的区别(与问题无关,只是为了表明它在上传图像时不会导致任何额外的工件):

IrfanView 100% [728k](视觉上与原版相同):

IrfanView 100% 与原始差异(几乎没有)

安卓 100% [942k]:

Android 100% 与原始差异(着色、 strip 、涂抹)


在 IrfanView 中,我必须低于 50% [50k] 才能看到远程相似的效果。在 IrfanView 中 70% [100k] 没有明显差异,但大小是 Android 的第 9 位。

背景

我创建了一个从 Camera API 拍摄照片的应用程序,该图像来自 byte[]并且是一个编码的 JPEG blob。我通过 OutputStream.write(byte[]) 保存了这个文件方法,那是我的原始源文件。 decodeByteArray(data, 0, data.length, options)解码与从文件中读取相同的像素,使用 Bitmap.sameAs 进行测试所以这与问题无关。

我使用我的三星 Galaxy S4 和 Android 4.4.2 来测试。
编辑:在进一步调查的同时,我还尝试了 Android 6.0 和 N 预览模拟器,它们重现了相同的问题。

最佳答案

经过一番调查,我找到了罪魁祸首:Skia 的 YCbCr 转换。可在 TWiStErRob/AndroidJPEG 中找到 Repro、调查代码和解决方案.

发现

在没有得到对这个问题的积极回应(都不是来自 http://b.android.com/206128 )之后,我开始深入挖掘。我发现了许多半知半解的 SO 答案,这极大地帮助了我发现点点滴滴。一个这样的答案是 https://stackoverflow.com/a/13055615/253468这让我知道 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 使用了不正确的算法。
在玩弄我在网上找到的算法和常量时,我​​总是得到一个糟糕的图像:要么亮度发生了变化,要么出现了与问题中相同的 strip 问题。

深层发掘
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 ()

  • 通过使用 rgb2yuv_32 中的常量来自 Bitmap.compress我能够使用 YuvImage 重新创建相同的 strip 效果,不是成就,只是确认确实是YUV转换搞砸了。我仔细检查了问题不在 YuvImage 期间调用 libjpeg :通过将位图的 ARGB 转换为 YUV 并返回到 RGB,然后将生成的像素 blob 作为原始图像转储, strip 已经存在。

    这样做时,我意识到 NV21/YUV420SP 布局是有损的,因为它每 4 个像素采样一次颜色信息,但是 它保持每个像素的值(亮度),这意味着一些颜色信息丢失了,但人们眼睛的大部分信息无论如何都在亮度中。看看examplewikipedia ,Cb 和 Cr channel 生成几乎无法识别的图像,因此对其进行有损采样并不重要。

    解决方案

    因此,此时我知道 libjpeg 在传递正确的原始数据时会进行正确的转换。这是我设置 NDK 并集成了来自 http://www.ijg.org 的最新 LibJPEG 的时间。 .我能够确认确实从位图的像素数组传递 RGB 数据会产生预期的结果。我喜欢在非绝对必要时避免使用 native 组件,因此除了使用对位图进行编码的 native 库之外,我还找到了一个巧妙的解决方法。我基本上采用了 rgb_ycc_convert函数来自 jcolor.c并使用来自 https://stackoverflow.com/a/13055615/253468 的骨架用 Java 重写它.下面没有针对速度进行优化,而是针对可读性进行了优化,为简洁起见,删除了一些常量,您可以在 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();
    }
    

    魔法 key 似乎是 ONE_HALF - 1其余的看起来很像 Skia 中的数学。这是 future 调查的一个很好的方向,但对我来说,上面的内容足够简单,可以成为解决 Android 内置奇怪问题的一个很好的解决方案,尽管速度较慢。 请注意,此解决方案使用 NV21 布局,该布局丢失了 3/4 的颜色信息(来自 Cr/Cb),但此损失远小于 Skia 数学产生的错误。 另请注意 YuvImage不支持奇数大小的图像,有关更多信息,请参阅 NV21 format and odd image dimensions .

    关于android - 如何在Android上以最少的质量损失将位图压缩为JPEG?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36487971/

    有关android - 如何在Android上以最少的质量损失将位图压缩为JPEG?的更多相关文章

    1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    2. ruby - 如何在 Ruby 中顺序创建 PI - 2

      出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

    3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

      如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

    4. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    5. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

      exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

    6. ruby - 如何在续集中重新加载表模式? - 2

      鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

    7. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

      我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

    8. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

      我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

    9. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

      这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

    10. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

      我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

    随机推荐