研究了那么久的图片压缩原理之后
虽然没能带回一个自己用JAVA实现的图片压缩软件
但是总算是自己终于对图片压缩有了个清晰的了解
好了,废话不多说
继续上次关于远程监控系统中用UDP广播图片遇到的图片压缩大小瓶颈问题
首先再次讨论上次给出的那组数据(关于对比ImageIO默认参数下写出GIF/JPEG,以及自己设置参数的JPEG)的讨论
GIF采用的是字典LZW算法,该算法是无损压缩,能提供近乎完美的无损压缩比,我们记得压缩后的图片大小大约为90KB
而JPEG默认情况下为有损压缩,压缩后大小大约200KB
在ImageIO中通过自己设置压缩质量来压缩,我们发现当压缩质量小于0.5以后,图片大小的变化是很缓慢的
用import com.sun.image.codec.jpeg.JPEGCodec提供的编码解码类来设置压缩质量,在同等质量的情况下,虽然比ImageIO中图片大小变小了,其实也是很有限的。通过前文,我们了解了这样设置图片质量其实知识改变量化表,在图片质量已经不高的情况下,其改变对图片大小的影响其实是很小的
package cn.mzd.newIM.test; import java.awt.AWTException; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Iterator; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGEncodeParam; import com.sun.image.codec.jpeg.JPEGImageEncoder; public class ImageSizeTest { /** * @param args * @throws AWTException */ public void getImageSize() throws AWTException { java.awt.Robot rb = new java.awt.Robot(); Dimension d = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); Rectangle rt = new Rectangle(0, 0, (int) d.getWidth(), (int) d .getHeight()); for (int i = 0; i < 1000; i++) { BufferedImage image = rb.createScreenCapture(rt); bufferedImageTobytes(image, "gif"); bufferedImageTobytes(image, "jpeg"); bufferedImageTobytes(image, 0.9f); newCompressImage(image, 0.9f); } } /** * 用Format对应格式中ImageIO默认参数把IMAGE打包成BYTE[] * @param image * @return */ private byte[] bufferedImageTobytes(BufferedImage image, String format) { System.out.println(format + "格式开始打包" + getCurrentTime()); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, format, out); } catch (IOException e) { e.printStackTrace(); } System.out.println(format + "格式完成打包-----" + getCurrentTime() + "----lenth------" + out.toByteArray().length); return out.toByteArray(); } /** * * 自己设置压缩质量来把图片压缩成byte[] * * @param image * 压缩源图片 * @param quality * 压缩质量,在0-1之间, * @return 返回的字节数组 */ private byte[] bufferedImageTobytes(BufferedImage image, float quality) { System.out.println("jpeg" + quality + "质量开始打包" + getCurrentTime()); // 如果图片空,返回空 if (image == null) { return null; } // 得到指定Format图片的writer Iterator<ImageWriter> iter = ImageIO .getImageWritersByFormatName("jpeg");// 得到迭代器 ImageWriter writer = (ImageWriter) iter.next(); // 得到writer // 得到指定writer的输出参数设置(ImageWriteParam ) ImageWriteParam iwp = writer.getDefaultWriteParam(); iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // 设置可否压缩 iwp.setCompressionQuality(quality); // 设置压缩质量参数 iwp.setProgressiveMode(ImageWriteParam.MODE_DISABLED); ColorModel colorModel = ColorModel.getRGBdefault(); // 指定压缩时使用的色彩模式 iwp.setDestinationType(new javax.imageio.ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16))); // 开始打包图片,写入byte[] ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 取得内存输出流 IIOImage iIamge = new IIOImage(image, null, null); try { // 此处因为ImageWriter中用来接收write信息的output要求必须是ImageOutput // 通过ImageIo中的静态方法,得到byteArrayOutputStream的ImageOutput writer.setOutput(ImageIO .createImageOutputStream(byteArrayOutputStream)); writer.write(null, iIamge, iwp); } catch (IOException e) { System.out.println("write errro"); e.printStackTrace(); } System.out.println("jpeg" + quality + "质量完成打包-----" + getCurrentTime() + "----lenth----" + byteArrayOutputStream.toByteArray().length); return byteArrayOutputStream.toByteArray(); } /** * 自己定义格式,得到当前系统时间 * * @return */ private String getCurrentTime() { Calendar c = new GregorianCalendar(); int hour = c.get(Calendar.HOUR_OF_DAY); int min = c.get(Calendar.MINUTE); int second = c.get(Calendar.SECOND); int millsecond = c.get(Calendar.MILLISECOND); String time = hour + "点" + min + "分" + second + "秒" + millsecond; return time; } /** * 通过 com.sun.image.codec.jpeg.JPEGCodec提供的编码器来实现图像压缩 * @param image * @param quality * @return */ private byte[] newCompressImage(BufferedImage image, float quality) { // 如果图片空,返回空 if (image == null) { return null; } // 开始开始,写入byte[] ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 取得内存输出流 // 设置压缩参数 JPEGEncodeParam param = JPEGCodec.getDefaultJPEGEncodeParam(image); param.setQuality(quality, false); // 设置编码器 JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( byteArrayOutputStream, param); System.out.println("newCompressive" + quality + "质量开始打包" + getCurrentTime()); try { encoder.encode(image); } catch (Exception ef){ ef.printStackTrace(); } System.out.println("newCompressive" + quality + "质量打包完成" + getCurrentTime() + "----lenth----" + byteArrayOutputStream.toByteArray().length); return byteArrayOutputStream.toByteArray(); } public static void main(String args[]) throws Exception { ImageSizeTest test = new ImageSizeTest(); test.getImageSize(); }
测试结果依然不理想
突发奇想
GIF采用的是LZW编码进行压缩
JPEG后期的熵编码用的是Huffman,那如果先进行JPEG算法,再进行LZW算法,会有什么样的效果呢?
想干就干
咱写代码来测试一下
测试代码如下
package cn.mzd.newIM.test; import java.awt.AWTException; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Iterator; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import sun.awt.image.JPEGImageDecoder; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGEncodeParam; import com.sun.image.codec.jpeg.JPEGImageEncoder; public class ImageSizeTest { /** * @param args * @throws AWTException */ public void getImageSize() throws AWTException { java.awt.Robot rb = new java.awt.Robot(); Dimension d = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); Rectangle rt = new Rectangle(0, 0, (int) d.getWidth(), (int) d .getHeight()); for (int i = 0; i < 1000; i++) { BufferedImage image = rb.createScreenCapture(rt); // bufferedImageTobytes(image, "gif"); giftest(bufferedImageTobytes(image, "jpeg")); giftest(bufferedImageTobytes(image, 0.2f)); giftest(newCompressImage(image, 0.2f)); } } /** * 用Format对应格式中ImageIO默认参数把IMAGE打包成BYTE[] * * @param image * @return */ private byte[] bufferedImageTobytes(BufferedImage image, String format) { System.out.println(format + "格式开始打包" + getCurrentTime()); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, format, out); } catch (IOException e) { e.printStackTrace(); } System.out.println(format + "格式完成打包-----" + getCurrentTime() + "----lenth------" + out.toByteArray().length); return out.toByteArray(); } /** * * 自己设置压缩质量来把图片压缩成byte[] * * @param image * 压缩源图片 * @param quality * 压缩质量,在0-1之间, * @return 返回的字节数组 */ private byte[] bufferedImageTobytes(BufferedImage image, float quality) { System.out.println("jpeg" + quality + "质量开始打包" + getCurrentTime()); // 如果图片空,返回空 if (image == null) { return null; } // 得到指定Format图片的writer Iterator<ImageWriter> iter = ImageIO .getImageWritersByFormatName("jpeg");// 得到迭代器 ImageWriter writer = (ImageWriter) iter.next(); // 得到writer // 得到指定writer的输出参数设置(ImageWriteParam ) ImageWriteParam iwp = writer.getDefaultWriteParam(); iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // 设置可否压缩 iwp.setCompressionQuality(quality); // 设置压缩质量参数 iwp.setProgressiveMode(ImageWriteParam.MODE_DISABLED); ColorModel colorModel = ColorModel.getRGBdefault(); // 指定压缩时使用的色彩模式 iwp.setDestinationType(new javax.imageio.ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16))); // 开始打包图片,写入byte[] ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 取得内存输出流 IIOImage iIamge = new IIOImage(image, null, null); try { // 此处因为ImageWriter中用来接收write信息的output要求必须是ImageOutput // 通过ImageIo中的静态方法,得到byteArrayOutputStream的ImageOutput writer.setOutput(ImageIO .createImageOutputStream(byteArrayOutputStream)); writer.write(null, iIamge, iwp); } catch (IOException e) { System.out.println("write errro"); e.printStackTrace(); } System.out.println("jpeg" + quality + "质量完成打包-----" + getCurrentTime() + "----lenth----" + byteArrayOutputStream.toByteArray().length); return byteArrayOutputStream.toByteArray(); } /** * 自己定义格式,得到当前系统时间 * * @return */ private String getCurrentTime() { Calendar c = new GregorianCalendar(); int hour = c.get(Calendar.HOUR_OF_DAY); int min = c.get(Calendar.MINUTE); int second = c.get(Calendar.SECOND); int millsecond = c.get(Calendar.MILLISECOND); String time = hour + "点" + min + "分" + second + "秒" + millsecond; return time; } /** * 通过 com.sun.image.codec.jpeg.JPEGCodec提供的编码器来实现图像压缩 * * @param image * @param quality * @return */ private byte[] newCompressImage(BufferedImage image, float quality) { // 如果图片空,返回空 if (image == null) { return null; } // 开始开始,写入byte[] ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 取得内存输出流 // 设置压缩参数 JPEGEncodeParam param = JPEGCodec.getDefaultJPEGEncodeParam(image); param.setQuality(quality, false); // 设置编码器 JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( byteArrayOutputStream, param); System.out.println("newCompressive" + quality + "质量开始打包" + getCurrentTime()); try { encoder.encode(image); } catch (Exception ef) { ef.printStackTrace(); } System.out.println("newCompressive" + quality + "质量打包完成" + getCurrentTime() + "----lenth----" + byteArrayOutputStream.toByteArray().length); return byteArrayOutputStream.toByteArray(); } /** * 测试把图片先压缩成JPEG,再用JPEG压缩成GIF */ public byte[] giftest(byte[] imagedata) { System.out.println("giftest开始打包" + getCurrentTime()); BufferedImage image = null; ByteArrayInputStream input = new ByteArrayInputStream(imagedata); // 得到解码器 JPEGImageDecoder decoder = (JPEGImageDecoder) JPEGCodec .createJPEGDecoder(input); // 把JPEG 数据流解压缩 try { image = ((com.sun.image.codec.jpeg.JPEGImageDecoder) decoder) .decodeAsBufferedImage(); } catch (Exception ef) { ef.printStackTrace(); } ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, "gif", out); } catch (IOException e) { e.printStackTrace(); } System.out.println("giftest开始打包" + getCurrentTime() + "----lenth----" + out.toByteArray().length); return out.toByteArray(); } public static void main(String args[]) throws Exception { ImageSizeTest test = new ImageSizeTest(); test.getImageSize(); } }
测试结果就补贴了
发现,对于默认的JPEG参数压缩,GIF能二次压缩到90K左右(类似之间GIF压缩)
而对于自己设定参数的压缩,当质量很高时(高于0.5),GIF效果还是有的
当质量很低时(低于0.1)再进行GIF压缩,大小反而变大-------------------分布均匀
此次试验再次宣告失败
难道我们的监控系统就不能用UDP来实现了吗?
虽然通过压缩图片直接打到保证图片质量和要求大小小于64KB的试验失败了,但是我们还有其他的办法
我们要始终相信灰太狼的那句“我还会再回来的”
具体怎么实现呢?
我想,思路如下----------把图片数据分包,用UDP广播分包后的消息,每个数据包内容里有属于第几个包的第几块
UDP的不可靠性,以及根据概率学知识,大家都知道分的包越多,越危险,于是,我们还是得先对图像进行压缩,再分包,再传送,尽量少分包
在接收方,用一个缓冲区来接收数据包。根本数据包内容中的标识来组装包,根据实时性要求,当接收消息超过3秒还未收到兄弟包,在丢弃。
别以为在这里我要用UDP来实现类型TCP的差错重传和确认机制,事情还没糟糕到需要做那一步。