函数拟合思想的一个应用

需求

对于任意给定的tiff文件,需要对其进行压缩并输出,要求输出后的文件大小为450k-500k。

现有jpeg2000第三方包,可进行jp2图像的处理。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-jpeg2000</artifactId>
<version>1.4.0</version>
</dependency>

BufferedImage转jp2文件输出方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void transformImgToJp2(BufferedImage bufferedImage, OutputStream outputStream, float quality, float encodingRate)
throws IOException {
try (
ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream);
) {
String name = null;
ImageWriter writer = null;
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("JPEG2000");
while (!Objects.equals(name, "com.github.jaiimageio.jpeg2000.impl.J2KImageWriter")) {
writer = writers.next();
name = writer.getClass().getName();
}
writer.setOutput(ios);
J2KImageWriteParam param = (J2KImageWriteParam) writer.getDefaultWriteParam();
IIOImage ioimage = new IIOImage(bufferedImage, null, null);
// param.setSOP(true);
// param.setWriteCodeStreamOnly(true);
// param.setProgressionType("layer");
// param.setLossless(true);
param.setCompressionMode(J2KImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("JPEG2000");
if (quality > 0)
param.setCompressionQuality(quality);
if (encodingRate != 0) {
param.setEncodingRate(encodingRate);
}
writer.write(null, ioimage, param);
writer.dispose();
ios.flush();
}
}

其中:影响jp2输出文件大小及质量的参数为:CompressionQuality大小0-1EncodingRate>0。

设$x$为输入文件大小,$c$为压缩质量,$e$为图像编码率。

其数学关系可简要表示为
$$
g(x,c,e)=s,(450\le s\le 50,x>0,0<c<1,e>0)
$$

再实验性的尝试后,发现$c$的大小是不对文件大小的产生影响的,它影响的是图像所能容纳的信息,压缩质量越高,指定编码率的输出图像的信息越多,相应的压缩和解压缩的时间越长。

思路一:

因为在给定参数后,文件的输出大小并不能直接确定,所以可以预先给定一组参数,在文件输出后,根据输出文件的大小不断的修正编码率。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void test() throws IOException {
int compressLimit = 500;
File dir = new File("C:\\Users\\Gatsby\\datasets/图片处理模板/samples");
List<String> fileNames = List.of(
// "67"//e0.16
// "79",//0.125
"83", //e 0.125
"85", //0.125
"98", //0.111
"103" //e0.11
// "142" ,//e=0.075
// "164" //e0.0625
);
float quality = 0.5f;
float encoding = 0.5f;
for (String fileName : fileNames) {
File oriTifFile = new File(dir, fileName + ".tiff");
File outFile = new File(oriTifFile.getParentFile(), oriTifFile.getName() + ".jp2");
BufferedImage bufferedImageToSave = ImageIO.read(oriTifFile);
float fsize = oriTifFile.length() / (1024f * 1024);
float size = oriTifFile.length() / (1024f * 1024);
encoding = -0.001f * fsize + 0.227f;
while (size > 0.5f || size < 0.4f) {
PicCompressUtils.transformImgToJp2(bufferedImageToSave, new FileOutputStream(outFile), quality, encoding);
size = outFile.length() / (1024 * 1024f);
log.info("输出文件大小{}m,原文件大小{}m", size, oriTifFile.length() / 1024);
System.out.println(encoding);
if (size > 0.5f)
encoding = -encoding / 10 + encoding;
else
encoding = encoding / 10 + encoding;
}
System.out.println("#########################");
}
}

结果分析:这样的方法的确能够将目标文件数位到目标格式和大小要求,但存在下列问题:

  • 压缩次数过多,每次压缩都会在内存中产生输出图像的缓冲信息,这会造成内存占用过多,特别是在次数超过5时,有几率造成OOM,造成程序直接断掉。
  • 因每一次压缩大概需要20s的时间,这种方法必然造成过多的压缩次数,导致图片处理的速度不高。

思路二:

利用函数拟合思想,拟合出上述数学关系图像。

根据前面分析,$c$可作为超参数指定,而$s$其实是一个范围值,完全可以看作一个常数。所以上述函数关系可写作:
$$
h(x)=e
$$
这样就可以简化为一个待拟合的二维函数图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import matplotlib.pyplot as plt
import numpy as np

# #压缩次数,输出文件大小,源文件大小,编码率,压缩时间
lines=[]
with open('../data/03_log.txt', 'r') as f:
temp_lines = f.readlines()
for line in temp_lines:
if line != '':
lines.append(line)

fises=[]
foses=[]
ers=[]
for line in lines:
splits = line.split(',')
if splits[0] != '1':
continue
out_file_Size = float(splits[1])
file_ize = float(splits[2])
encoding_rate = float(splits[3])
ers.append(encoding_rate)
foses.append(out_file_Size)
fises.append(file_ize)

# 使用Python中的ployfit()函数进行拟合
z = np.polyfit(fises, ers, 2) # 用1次多项式拟合,输出系数从高到0
# poly1d()函数可以根据你传入的直线或者曲线的参数生成方程,而且这里的直线或者曲线参数就是由polyfit提供的
p1 = np.poly1d(z) # 生成拟合后的函数方程
# 使用自变量x和预测的函数方程生成预测的y值
y_pre = p1(fises)

# 绘图
plt.plot(fises, ers, '.')
plt.title("曲线拟合")
plt.xlabel('fis')
plt.ylabel('ers')
plt.plot(fises, y_pre)
plt.show()
print("拟合后的函数方程是:\n", p1)

得到的目标一元多次函数为:
$$
f(x)=-6.856e^{-11} x^4 + 3.091e^{-08} x^3 + 7.805e^{-07} x^2 - 0.001876 x + 0.2634
$$

pF7soQA.png

存在的问题:因采样的点较少只有200个点,覆盖不全,再者,对于目前超过90的图时能够一次就得到目标需要的编码率,但也仍然存在不满足要求的情况。

思路三:

综合上述两种思路,第一次压缩的$e$从拟合的函数中获取,后面的参数通过不断的修正获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* tiff定制压缩方法
*/
public static void imageCompress(BufferedImage bufferedImage, File outFile, int limit) throws IOException {
limit=500;
float fsize = bufferedImage.getData().getDataBuffer().getSize() / (1024 * 1024f);
float oriFileSizeM = fsize;
float encoding = (float) (5.842e-6 * Math.pow(fsize, 2) - 2.235e-3 * fsize + 0.2732);
float limitM = limit / 1024f;
if (limitM == 0) {
OutputStream os = Files.newOutputStream(outFile.toPath());
ImageIO.write(bufferedImage, "JPG2000", os);
os.close();
return;
}
int compressTime = 0;
while (fsize > limitM || fsize < limitM * 0.8) {
compressTime += 1;
OutputStream os = Files.newOutputStream(outFile.toPath());
long s = System.currentTimeMillis();
transformImgToJp2(bufferedImage, os, 0.5f, encoding);
os.close();
fsize = outFile.length() / (1024 * 1024f);
log.debug("压缩次数{},输出文件大小{}m,原文件大小{}m,编码率{},耗时{}s,文件名{}",
compressTime,
fsize,
oriFileSizeM,
encoding,
(System.currentTimeMillis() - s) / 1000f,
outFile.getAbsolutePath()
);
if (compressTime < 2)
encoding = (limitM * 0.95f) / fsize * encoding;
else if (compressTime < 5) {
System.gc();
if (fsize > limitM)
encoding *= 0.9;
else if (fsize < limitM * 0.8)
encoding *= 1.1;
else break;
} else
throw new IOException("压缩次数过多,为防爆内存,异常推出");
}
}