利用opencv与python3 JPEG压缩与解压实现
由于内容是从写好的word文件中复制过来,可能排版等会有各种问题,建议直接看github中的pdf
另外由于我写这份作业的时候还不熟悉py3,因此实际上由很多可以优化的地方,比如数组强烈建议使用numpy而不是此处的列表
github地址:https://github.com/c980129/JPEG
JPEG压缩实现(Python3)
- RGB转YUV
JPEG会将彩色图像执行YUV或YIQ的颜色空间转换,二次采样JPEG采用4:2:0,所以这里使用YUV420的颜色空间。在JPEG中使用的颜色模型是YCbCr(由YUV调整而来)。对于一个2*2的块,我们会保存4个Y值,1个Cb值(取0行0列的Cb)与1个Cr值(取1行0列的Cr),6个值保存信息,因此Cb与Cr有一定损失。
其中我们用函数rgb2yuv(在RGB2YUV.py)中来实现颜色模型转换与二次采样。并分别用三个二维数组保存采取的Y、U、V值。于是我们能得到三张分别用Y、U、V生成的灰度图(由于U和V损失为原来的1/4,因此其图像的长宽也分别为原来的1/2)(为了减少绝对值将Y值减去128):
- 图像边长填充为8的倍数并等分
用DCT.fill(img)函数对二次采样得到的Y、U、V图像分别用0填充知道其矩阵的height和width都是8的倍数,因为DCT函数的参数是一个8*8的矩阵。同样用DCT.split(img)函数将图像以左到右,上到下的顺序分成多个8*8矩阵,并返回这些矩阵连成的数组。
- 离散余弦变换
用DCT.FDCT(block)函数对一个8*8矩阵进行二维离散余弦变换,保存得到的矩阵。
- 量化
在类Quantization中保存成员变量table0与table1作为亮度和色度的量化表,调用Quantization.quanY(img)与Quantization.quanUV(img)分别用于对Y图像与U、V图像量化。
- AC系数
用AC类中的ZScan(img)对一个图像进行Z型扫描,得到一个长度为63的数组(图像第一个像素并不需要,它将在DC系数中保存)。之后通过RLC(array)对上面得到的数组array进行RLC得到他们的游长编码。
- DC系数
用DC类中的DPCM(blocks)函数对所有图像的DC系数提取并返回它们的DPCM编码数组。
- 熵编码
压缩部分是无损压缩,对DC系数(一个数组)采用可变字长整数编码。将一个DC系数分成size和amplitude两部分,配合VLI(num)和toB(num)函数将一个DC系数转换为一个[size(num), s(string(B))]。
这里的s已经是二进制串了(暂且用字符串存储方便操作),size需要用哈夫曼编码压缩。这里使用JPEG推荐的哈夫曼编码(亮度与色度两个表)。
对于AC系数,我们知道AC系数采用有偿编码,由两个数runlength与value组成。先将value如同DC系数一样采用可变字长整数编码拆分成size与amplitude,然后runlength与size合并为symbol1,smplitude独立为symbol2。对于runlength大于15的数情况,在symbol1添加(15,0)表示(为了解码时能识别,应在前端添加)。然后对symbol1采用哈夫曼编码,对于symbol2则直接用上文可变字长整数编码得到的二进制码。Symbol1采用的哈夫曼编码同样用JPEG推荐的哈夫曼编码表,由于过长不在报告中贴出。最终可用函数AllCompressY与AllcompressUV(未贴出)将一个表格的数据转换为二进制字符串。参数为DC系数(一个)与AC系数(数组)。
在Test.py中将对所有函数测试(实际上每个类的python文件的下方注释部分都是对这个类中函数的单元测试,由于不方便在报告中列出因此采用这种方式)。
这是Y图像的第一个8*8矩阵:
这是该矩阵的DCT变换结果(取整):
量化结果(取整):
Z字形扫描,DC系数,AC系数(太长未列全),二进制字符串,串长:
可以看到此处二进制长217位,意思是已经将一个8*8*8(512)bit的图像压缩为217bit。
对于译码,我们需要事先保存图片的长宽(正如图片位流里会保存一样),以此计算出Y,U,V图像的矩阵数,才能对整个二进制流正确分割(在译码过程中分割)。
在Compress类的encoding函数中,参数是位流(字符串形式)与宽,高(整型)。我们根据宽高得到Y、U、V图像的8*8矩阵的数量(我们知道U和V是一样多的),然后从位流头部开始移动两个指针。我们需要先得到上面使用的四个哈夫曼编码表的反向映射(这里用字典)。我们知道两个指针之间的二进制码的含义必定在几个状态之间转换:读取DC系数的size(通过不断比较两个指针之间的位流是否为字典的key,是的话得到其value(这里指字典的value),即size,不是则移动尾指针);通过size得到新的头尾指针,得到amplitude;然后开始读取AC系数的(runlength,size),如同上面得到DC系数的size一样,获得size后以此得到amplitude;循环读取AC系数直到读取翻译到的(runlength, value)为(0,0)或得到63个AC系数为止,将DC系数与AC系数都加进各自的列表中(这两列表将存储全部Y矩阵的DC系数与AC系数)。U与V同理,最终我们得到Y、U、V的DC系数与AC系数的数据(由于这段代码比较长不贴出,在Compress.py中)
然后通过DC类中的DPCM2(DC)函数,传入各自的DC系数,得到三个其元素是一个矩阵的列表,每个矩阵的[0][0]都通过DC系数还原为编码前
然后对每个矩阵分别通过对应的DC系数还原为63个数(AC.RLE(array))
再Z形填进矩阵中(AC.Z2Tab函数)(这段较长不贴出)
分别逆量化(Quantization.reY(img)与Quantization.reUV(img))(因为亮度与色度量化表不一样因此要分别操作)
再二维逆离散余弦变换(DCT.IDCT(img)。
这段操作每个函数的对象都是单个8*8矩阵
通过对所有矩阵进行同样的操作我们能得到所有当初刚分割完的8*8矩阵(不算损失的话)。
同样通过对这些矩阵进行合拼并将当初填充的0割掉,得到Y、U、V图像(这段代码较长不贴出,在DCT.py的DCT.merge()中,注意需要图像的长宽作为参数)
经此我们能得到YUV420的原图像。
关于测试代码,可以直接运行Test.py,会打印得到对每个函数的测试,或者运行Main.py,将执行对图像从压缩到解压的全部操作。Main.py会打印压缩得到的位流长,并在代码的上一层目录中创建一个txt.txt文件用字节形式保存每个位的数据(主要是用于测试解压代码时可以免去压缩过程迅速开始测试)。
运行Main.py会打印位流的长度,图片的高度与宽度(因此推荐在终端窗口中运行而不是直接运行py文件),并显示解压后的图片
我们直到682*1024个像素,如果直接用RGB的24位保存将要16760832bit
而这里压缩后得到的位流是1388439bit(虽然这里是字符串形式而非位流形式,但为了方便省去了以位流写文件的步骤,我们直接通过字符串的长度来对比)。压缩率是8.3%。
然而压缩的代价也十分明显(如图)。其中黑色变为其他颜色的点主要是YUV420中大量色度损失导致的(直接将图片从RGB转为YUV420再直接转为RGB就会有这些点的损失,因此如此推断),颜色也同样有不少损失,远处的云能看到明显的格子化。
解压后:
原图:
③ 结果对比:
压缩率约为8.3%的JPEG图(忽略文件信息等真正JPEG文件可能会造成的误差)
体积大约为170KB。与原文件接近,或许是我二度采样哪里理解错了或者写错了导致失真严重。
用PS使压缩的GIF也接近170KB(调整损失),得到的图像明显颜色(只有256)不足导致颜色接近的区块容易模糊融在一起。但整体色彩比起jpeg更接近原图,但是缺少细节。因此像照片这种细节多的图片,在体积相同的情况下还是选择JPEG更合适。或许是我的代码或者算法不够好导致的损失,也可能是我的二度采样是在原JPEG已经压缩过的二度采样的基础上的再次二度采样导致的损失,实际上原图jpg也是170KB体积,但表现效果十分好,比起我的压缩解压以及PS的转换为GIF都是如此。
GIF与JPEG文件保存在根目录下。代码保存在py文件夹中。