如何获取JPEG图片质量和预测压缩图片大小

前言

发起这个博客的原因是近期有个需求,当用户在APP中发送图片时,APP要显示将图片压缩到指定尺寸的选项,选项中要显示压缩后图片的大小(占空间),出发点是为了控制上传流量。那转换成技术问题实际上就是计算图片压缩后的大小并显示,难点就在计算图片压缩后的大小,但是有个突破点就是显示给用户看的图片压缩后的大小不需要很精确。

最精确的做法就是将图片压缩后获取其大小,但是实际上这样子操作的话整个过程就会比较漫长而且耗内存,所以就有了下面预估压缩图片大小的想法。

大家都知道,图片压缩之后的大小是取决于图像数据和压缩算法的,几乎是无法预测的。我用手机上180张照片做实验,图片格式都是JPEG,图片大小都为4608x3456像素,内存存储使用ARGB8888,输出为JPEG图片540x405像素,图片质量作为变量,结果如下(此处推荐一个挺好用的压缩库Compressor):

图片输出质量 最大压缩比 最小压缩比
100 33.3 20
95 119 47
90 209 69

压缩比是将压缩前大小除以压缩后大小得出的。从以上数据可以看出,图片压缩后的大小几乎无法预测。只有在输出质量为100的时候,最大压缩比和最小压缩比相对接近,我们可以取个平均值比如26作为平均压缩比来大概预测图片压缩后的大小,毕竟一般图片假设为4mb,压缩之后也就120-200k,以160k来做预估值,这个误差还是可以接受的,毕竟控制流量只需要个大概值。

但是,以上结果是在原始图片质量比较高的情况下得出的结果,如果原始图片质量比较低,比如原始图片的质量为80,尺寸从1000x1000压缩到800x800,输出质量为100,那很可能压缩之后的图片比原始图片还要大,这样的结果可能就无法接受了。而且我通过搜索发现原始图片的质量并不能简单的获取到。

如何预测图片大小

在网上搜索资料后发现sof上有个相同的问题,作者Vincent最后自己回答了这个问题:

I have successfully estimated the scaled size based on the DQT - the quality factor.

I conducted some experiments and find out if we use the same quality factor as in the original JPEG image, the scaled image will have size roughly equal to (scale factor * scale factor) proportion of the original image size. The quality factor can be estimate based on the DQT defined in the every JPEG image. Algorithm has be defined to estimate the quality factor based on the standard quantization table shown in Annex K in JPEG spec.

Although other factors like color subsampling, different compression algorithm and the image itself will contribute to error, the estimation is pretty accurate.
P.S. By examining JPEGSnoop and it source code, it helps me a lot :-)

Cheers!

作者认为JPEG图片在压缩前后图片质量不变的情况下,压缩后的图片大小与原始图片大小的比值基本等于他们面积(即宽高乘积)的比值。
那如何获得原始图片的质量呢,作者只是大概地说可以通过JPEG文件内的DQT定义量化表数据来计算得出,却没有给出计算过程。

如何获取JPEG图片质量

后来通过搜索,发现大部分人还是觉得JPEG图片的质量是不可能获取到的。后来找到sof上一个相同的问题,一个叫Arjan的答者更正了自己之前的回答,表示一个叫ImageMagick的软件的identify功能可以准确的打印出JPEG图片的质量,并表示该图片中没有存储任何EXIF数据。

identify -verbose myimage.jpg

Image: myimage.jpg
Format: JPEG (Joint Photographic Experts Group JFIF format)
Class: DirectClass
Geometry: 358x240+0+0
Resolution: 300x300
[…]
Compression: JPEG
Quality: 90
Orientation: Undefined
[…]

然后下面的一个热心答者sleske查看了ImageMagick的源码并将相关的代码和链接贴了出来,源码中有以下代码段:

/*
Determine the JPEG compression quality from the quantization tables.
*/
sum=0;
for (i=0; i < NUM_QUANT_TBLS; i++)
{
if (jpeg_info.quant_tbl_ptrs[i] != NULL)
for (j=0; j < DCTSIZE2; j++)
sum+=jpeg_info.quant_tbl_ptrs[i]->quantval[j];

sleske表示他不理解这段代码,但也表示identify功能确实计算出了图片的质量,虽然不一定都精确。后面他也附上了一个C#语言实现的算法,不过该链接页面找不到了。只能自己看源码实现该算法,还好算法并不算难。

结合前面的知识可以发现源码中的quant_tbl_ptrs其实就是指向DQT数组的,我通过CSDN上的一篇博客了解了DQT在JPEG文件中的位置并将其取出代入该算法,最后得出与ImageMagick软件identify功能一样的结果。其中核心算法其实很简单,如下:

public int getJPEGImageQuality(File file) {
        if (file == null || !file.exists() || file.isDirectory())
            return QUALITY_UNDEFINED;
        QuantTables qts = new QuantTables();
        qts.getDataFromFile(file);

        if (!qts.hasData())
            return QUALITY_UNDEFINED;

        int sum = getQuantSum(qts);
        int qvalue = getQValue(qts);

        if (qvalue == 0)
            return QUALITY_UNDEFINED;

        int[] realHash = null;
        int[] realSums = null;
        if (qts.getTable(0) != null && qts.getTable(1) != null) {
            realHash = hash;
            realSums = sums;
        } else if (qts.getTable(0) != null) {
            realHash = singlehash;
            realSums = singlesums;
        } else {
            return QUALITY_UNDEFINED;
        }

        int quality = 0;
        for (int i = 0; i < MAX_QUALITY; i++) {
            if ((qvalue < realHash[i]) && (sum < realSums[i]))
                continue;
            if (((qvalue <= realHash[i]) && (sum <= realSums[i])) || (i >= 50))
                quality = i + 1;
            break;
        }
        return quality;
    }

最后得出前面例子中所用的图片的质量为95,即其最大压缩比为119,最小压缩比为47,理论压缩(即面积比)比为73,假设一张图片为4M。其压缩后大小范围为336k-850k,其理论值547k,看来前面的Vincent的结论也不是非常准确,这样的预测结果能不能接受看个人了。如果还想要更精确的值,那你要继续努力了。

下面附上我写的获取JPEG图片质量的Java完整实现,总共有2个类:
测试了微信中保存的100多张图片,只有一张图片读取不到质量值,其他的都读取到比较准确的数值。

public class Magick {
    static final int QUALITY_UNDEFINED = 0;
    static final int MAX_QUALITY = 100;

    static int hash[] = new int[]{1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645, 632, 623, 613, 607, 600, 594,
            589, 585, 581, 571, 555, 542, 529, 514, 494, 474, 457, 439, 424, 410, 397, 386, 373, 364, 351, 341, 334,
            324, 317, 309, 299, 294, 287, 279, 274, 267, 262, 257, 251, 247, 243, 237, 232, 227, 222, 217, 213, 207,
            202, 198, 192, 188, 183, 177, 173, 168, 163, 157, 153, 148, 143, 139, 132, 128, 125, 119, 115, 108, 104, 99,
            94, 90, 84, 79, 74, 70, 64, 59, 55, 49, 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, 0};

    static int sums[] = new int[]{32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, 27670, 27225, 26725,
            26210, 25716, 25240, 24789, 24373, 23946, 23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998,
            16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702, 12423, 12056, 11779, 11513, 11135, 10955,
            10676, 10392, 10208, 9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458, 8270, 8084, 7896, 7710, 7527,
            7347, 7156, 6977, 6788, 6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128, 4945, 4751, 4638, 4442, 4248,
            4065, 3888, 3698, 3509, 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846, 1666, 1483, 1297, 1109, 927,
            735, 554, 375, 201, 128, 0};

    static int singlehash[] = new int[]{510, 505, 422, 380, 355, 338, 326, 318, 311, 305, 300, 297, 293, 291, 288,
            286, 284, 283, 281, 280, 279, 278, 277, 273, 262, 251, 243, 233, 225, 218, 211, 205, 198, 193, 186, 181,
            177, 172, 168, 164, 158, 156, 152, 148, 145, 142, 139, 136, 133, 131, 129, 126, 123, 120, 118, 115, 113,
            110, 107, 105, 102, 100, 97, 94, 92, 89, 87, 83, 81, 79, 76, 74, 70, 68, 66, 63, 61, 57, 55, 52, 50, 48, 44,
            42, 39, 37, 34, 31, 29, 26, 24, 21, 18, 16, 13, 11, 8, 6, 3, 2, 0};

    static int singlesums[] = new int[]{
            16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859,
            12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679,
            9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823,
            6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086,
            4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092,
            3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396,
            3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727,
            2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068,
            1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398,
            1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736,
            667, 592, 518, 441, 369, 292, 221, 151, 86, 64, 0
    };

    public int getJPEGImageQuality(File file) {
        if (file == null || !file.exists() || file.isDirectory())
            return QUALITY_UNDEFINED;

        QuantTables qts = new QuantTables();
        qts.getDataFromFile(file);

        if (!qts.hasData())
            return QUALITY_UNDEFINED;

        int sum = getQuantSum(qts);
        int qvalue = getQValue(qts);

        if (qvalue == 0)
            return QUALITY_UNDEFINED;

        int[] realHash = null;
        int[] realSums = null;
        if (qts.getTable(0) != null && qts.getTable(1) != null) {
            realHash = hash;
            realSums = sums;
        } else if (qts.getTable(0) != null) {
            realHash = singlehash;
            realSums = singlesums;
        } else {
            return QUALITY_UNDEFINED;
        }

        int quality = 0;
        for (int i = 0; i < MAX_QUALITY; i++) {
            if ((qvalue < realHash[i]) && (sum < realSums[i]))
                continue;
            if (((qvalue <= realHash[i]) && (sum <= realSums[i])) || (i >= 50))
                quality = i + 1;
            break;
        }
        return quality;
    }

    private int getQValue(QuantTables qts) {
        if (qts.getTable(0) != null && qts.getTable(1) != null) {
            return qts.getTable(0)[2] + qts.getTable(0)[53] + qts.getTable(1)[0] + qts.getTable(1)[QuantTables.TABLE_LENGTH - 1];
        } else if (qts.getTable(0) != null) {
            return qts.getTable(0)[2] + qts.getTable(0)[53];
        } else {
            return 0;
        }
    }

    private int getQuantSum(QuantTables qts) {
        int sum = 0;
        for (int i = 0; i < QuantTables.MAX_TABLE_COUNT; i++) {
            int[] table = qts.getTable(i);
            if(table != null){
                for (int j = 0; j < table.length; j++) {
                    sum += table[j];
                }
            }
        }
        return sum;
    }
}

public class QuantTables {
    static final int MAX_TABLE_COUNT = 2;
    static final int TABLE_LENGTH = 64;

    private int tables[][] = new int[MAX_TABLE_COUNT][TABLE_LENGTH];
    private byte tablesInitFlag[] = new byte[MAX_TABLE_COUNT];

    private static final byte JPEG_FLAG_START = (byte) 0xff;
    private static final byte JPEG_FLAG_DQT = (byte) 0xdb;// define quantization  table

    private static final byte[] JPEG_HEADER_FLAG = new byte[] { (byte) 0xff, (byte) 0xd8 };

    void getDataFromFile(File file) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            byte[] buf = new byte[6];
            int len = -1;

            len = fis.read(buf, 0, 6);

            if (len < 6)
                return;

            if (buf[0] != JPEG_HEADER_FLAG[0] || buf[1] != JPEG_HEADER_FLAG[1])// it's not a jpeg file so return
                return;

            int index = 2;

            while (len > index) {
                int sectionLength = byteToInt(buf[index + 2]) * 0x100 + byteToInt(buf[index + 3]);

                if (buf[index] != JPEG_FLAG_START) {
                    break;
                }

                if (buf[index + 1] == JPEG_FLAG_DQT) {// it's a begin of DQT
                    buf = new byte[sectionLength - 2];// dqt 长度不超过4个表长,即不超过4*64再加一些标志位
                    len = fis.read(buf, 0, buf.length);

                    if (len < buf.length)
                        break;// file is not complete
                    index = 0;

                    while (index < len) {
                        byte flag = buf[index];
                        byte high_precision = (byte) (flag >> 4);
                        byte low_id = (byte) (flag & 0x0f);

                        if (high_precision != 0) {
                            // don't know how to deal with high precision table
                            return;
                        }

                        if (low_id < 0 || low_id > 1)
                            return;

                        if (tablesInitFlag[low_id] != 0) {
                            // table already got,don't know how to deal with this,just clear and return
                            tablesInitFlag[0] = 0;
                            tablesInitFlag[1] = 0;
                            return;
                        }

                        tablesInitFlag[low_id] = 1;
                        for (int i = 0; i < tables[low_id].length; i++) {
                            tables[low_id][i] = buf[index + 1 + i];
                        }

                        index += 65;
                    }
                } else {
                    fis.skip(sectionLength - 2);
                    len = fis.read(buf, 0, 4);
                    index = 0;
                    continue;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeStream(fis);
        }
    }

    private int byteToInt(byte b) {
        return b & 0xff;
    }

    int[] getTable(int index) {
        if (index >= MAX_TABLE_COUNT || index < 0)
            return null;
        if (tablesInitFlag[index] == 0)
            return null;
        return tables[index];
    }

    boolean hasData() {
        for (int i = 0; i < MAX_TABLE_COUNT; i++) {
            if (tablesInitFlag[i] != 0)
                return true;
        }
        return false;
    }

    private void closeStream(FileInputStream fis) {
        if (fis != null)
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

版权声明:本文为CSDN博主「番茄大圣」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tomatomas/article/details/62235963

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,046评论 0 4
  • 公元:2019年11月28日19时42分农历:二零一九年 十一月 初三日 戌时干支:己亥乙亥己巳甲戌当月节气:立冬...
    石放阅读 6,877评论 0 2
  • 今天上午陪老妈看病,下午健身房跑步,晚上想想今天还没有断舍离,马上做,衣架和旁边的的布衣架,一看乱乱,又想想自己是...
    影子3623253阅读 2,912评论 1 8