前言
发起这个博客的原因是近期有个需求,当用户在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