需求 对于任意给定的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.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-1
,EncodingRate
>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( "83" , "85" , "98" , "103" ); 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 pltimport numpy as nplines=[] 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) z = np.polyfit(fises, ers, 2 ) p1 = np.poly1d(z) 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 $$
存在的问题:因采样的点较少只有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 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 ("压缩次数过多,为防爆内存,异常推出" ); } }