最近有无线传输图像的需求,因为原图数据量很大,因此需要使用JPEG或其他压缩算法对原图进行压缩后再传输

图像文件和压缩标准

​ 常见的图像文件后缀有.arw .bmp .jpg .png .gif .tif .svg

.arw:arw是索尼相机的RAW图像格式。RAW格式可以称为是原始图片数据格式,可以进行更多的后期调整,图像文件中包括光圈、快门、ISO、GPS等相机信息。RAW是广大摄影爱好者的常用格式

.bmp:BMP格式是微软公司制定的图形标准,最大的优点就是在PC上兼容度一流,几乎能被所有的图形软件“接受”,可称为通用格式,就算不装任何看图软件,用Windows的“画笔”一样可以看。其结构简单,未经过压缩,储存为bmp格式的图形不会失真,但文件比较大,而且不支持Alpha(透明背景)通道

.jpg:JPG格式是目前网络上最流行的图形格式,它可以把文件容量压缩到最小的格式。JPG支持不同程度的压缩比,您可以视情况调整压缩倍率,压缩比越大,品质就越低;相反地,压缩比越小,品质就越好。不过要注意的一点是,这种压缩法属于有损压缩,文件的压缩会使得图形品质下降。JPEG(Joint Photographic Experts Group,联合图形专家组)是由CCITT(国际电报电话咨询委员会)和ISO(国际标准化组织)联合组成的一个图像专家组

.png:PNG(Portable Network Graphics,可移植的网络图形格式)是一种新兴的网络图形格式,结合了GIF和JPEG的优点,具有存储形式丰富的特点。PNG最大色深为48bit,采用无损压缩方案存储,是一种位图文件。著名的Macromedia公司的Fireworks的默认格式就是PNG。比起JPG等其他格式多了Alpha通道

.gif:CompuServe公司在1987年开发的图像文件格式。GIF采用LZW压缩算法来存储图象数据,并采用了可变长度等压缩算法。GIF的图像深度从1 bit到8 bit,也即GIF最多支持256种颜色的图像。GIF格式的另一个特点是其在一个GIF文件中可以存多幅彩色图像,如果把存于一个文件中的多幅图像数据逐幅读出并显示到屏幕上,就可构成一种最简单的动画

.tif:pTIF格式可说是做平面设计上最常使用到的一种图形格式,因为是属于跨平台的格式,而且支持cmyk色,所以经常被用于印刷输出的场合。此外还有一个特色就是支持lzw压缩,属于无损压缩。TIFF(Tag Image File Format,Tag Image File Format)文件是由Aldus和Microsoft公司为扫描仪和桌上出版系统研制开发的一种较为通用的图像文件格式。TIFF支持多种编码方法,其中包括RGB无压缩、RLE压缩及JPEG压缩等

.svg:SVG全称为Scalable Vector Graphics,意思为可缩放的矢量图形。它是基于XML(Extensible Markup Language),由World Wide Web Consortium(W3C)联盟进行开发的。严格来说应该是一种开放标准的矢量图形语言,可让你设计高分辨率的Web图形页面。用户可以直接用代码来描绘图像,可以用任何文字处理工具打开SVG图像,通过改变部分代码来使图像具有互交功能,并可以随时插入到HTML中通过浏览器来观看

JPEG

本文主要内容关于将JPEG用于无线通信中,所以仅简单介绍JPEG的基本原理

​ JPEG(Joint Photographic Experts Group)算法会分析图片的各个部分,找到并删除人眼不易察觉的元素。当使用JPEG算法压缩图片时,可以选一个叫“质量”(Quality)的可变数值来决定压缩的程度,当图像的质量从100%下降到0%时,图片文件的压缩程度也随之越来越高,从而减小了图片文件所占的空间。但是JPEG并不会改变图片的分辨率、像素数量。当图片压缩得越来越小时,可以看到图片的分辨率或像素的数量保持不变。但最终我们会得到这些有缺陷的方块,技术上叫“膺像”(artifacts)

​ JPEG算法有5个步骤组成:

  • Color Space Conversion:色彩空间转换。从RGB转为YUV色彩空间。这个过程是可逆的,在转换过程中没有删除任何数据。Y亮度,Cb蓝色色度、Cr红色色度
  • Chrominance Downsampling:色度缩减取样。将蓝色色度和红色色度分量层上的像素按照2×2个像素成一个区块划分,然后计算每个区块的色度平均值,并删掉重复的信息,然后缩小图像,使得含有一个平均值的由4个像素组成的区块只占一个像素的空间。因此那些我们眼睛不易感知的红蓝色度信息的量被缩减到原来的四分之一,而亮度保持不变,这样就导致图像的大小变成原来的一半。但是当我们查看图片时,蓝色和红色色度图层会被放大到跟亮度图层一样的大小,并根据亮度、蓝色色度、红色色度的值重新计算出RGB的值,由于各个像素的亮度可能不同,重新计算的各个像素上的RGB值也可能有变化
  • Discrete Cosine Transform(DCT):离散余弦变换。人眼不擅长感知图像中的高频率元素,遍历图像的各个部分,并找到具有高频率的色度或亮度的像素频繁出现的区域,然后将这些人眼很难感知的元素删除。DCT不能压缩或缩小图像。基本思想是提取原始图像的某个通道如亮度(其余通道也做同样操作),将该通道分成多block,每个block是8×8像素,将像素值减去128,取值范围变为-128~127。敲定一个基础图像,将基础图像乘一个系数相加64次,每次系数不同,原始的64个像素变成64个系数
  • Quantization:量化。经过DCT拥有一个8×8的常数表,对应于每个基础图像的使用情况。将常数表中的各个值除以量化表中的对应值,并将每个结果四舍五入为最接近的整数。量化表右下角数值较高,那里有人眼不擅长感知的高频数据,而数据较小的左上角,是人眼更易区分的样式所在。这样最后结果有很多0,这些是我们扔掉的人眼无法感知的数据。整张图像使用了一组相同的64个基础图像和两个量化表(一个用于亮度,另一个用于色度),以便将各个8×8的像素区块转化为几个数字和一大堆的0。压缩程度Quality就是改变量化表中的数值
  • Run Length and Huffman Encoding:游程编码/哈夫曼编码。列出所有区块中的亮度和色度数值,但是采取之字形顺序,因为这样更有可能找到一连串的非0数。接下来在列出的数字中,使用游程编码算法,不列出所有的0,只是说有多少个0。这个只有十几个数字的列表,比64个像素分别由一个0~255的数字来表示的方法要压缩得多。之后使用霍夫曼编码。

​ JPEG解压缩上述步骤相反

JPEG文件存储格式

​ JPEG本身只有描述如何将一个视频/图片转换为字节的数据流(streaming),但并没有说明这些字节如何在任何特定的存储媒体上被封存起来

​ 这里要对JPEG做一个补充说明,很多人把JPEG标准和JPEG文件格式理解成一个东西。然而实际并不是这样的,JPEG标准主要还是围绕编解码的部分(如DCT变换、量化、哈夫曼树等等),虽然在JPEG标准中也定义了“JPEG Interchange Format (JIF)”的文件存储格式,但是因为Encoder和Decoder完整实现JIF很困难,且JIF标准也存在一些缺陷,因此JIF并没有被推广开来。倒是后来出现的“**JPEG File Interchange Format (JFIF)” 和 “Exchange image file Format(Exif)**” 等新的存储格式成为了主流。Exif 也好 JFIF 也罢,他们都是遵循 JIF标准的,两者只是在JIF的基础上增加了一些各自的Marker

  • JPEG是压缩标准,JPEG/JFIF和JPEG/EXIF是文件格式标准,不是一个概念,需要注意区分
  • JPEG/EXIF文件格式标准是Camera产业联合会发布,主要用于摄像设备上,摄像产业把EXIF作为行业的元数据(metadata)交换格式
  • JPEG/JFIF文件格式标准是为了方便JPEG压缩图像在广泛的平台和应用间以最小的存储空间代价进行交换而设计的,它不包含JPEG/TIFF标准任何高级特性

实际上EXIF作为数码相机独特存储的数据格式,添加在JFIF数据格式的APPn上

​ 相比于BMP文件结构,JPEG文件结构要复杂得多。由于Exif和JFIF格式的都是遵循JIF的标准,在存储格式上沿袭了统一的 JPEG Marker + Compressed Data 的方式。整个文件根据不同的Marker划分成不同的标记段。每个Marker的长度为固定的 2 Byte

Marker名称 Marker内容 说明
SOI 0xFFD8 Start Of Image
SOF0 0xFFC0 Start Of Frame 0
SOF2 0xFFC2 Start of Frame 2
DHT 0xFFC4 Define Huffman Table(s)
DQT 0xFFDB Define Quantization Table(s)
DRI 0xFFDD Define Restart Interval
SOS 0xFFDA Start of Scan
RST0~RST7 0xFFD0 ~ 0xFFD7 Restart
APP0~APP15 0xFFE0 ~ 0xFFEF Application-sepcific
COM 0xFFFE Comment
EOI 0xFFD9 End of Image

​ 上面这张表列举了JPEG 的主要 Marker。文件按照Marker的划分成不同的标记段,每个标记段结构轮廓一致,如下图所示。Detail Data 部分的结构根据不同的Marker的定义而进行不同的细分

pCHWkGV.jpg

​ SOI (0xFFD8) 和 EOI (0xFFD9) 作为JPEG文件的起止标志,不参照上图的数据划分。所有的JPEG文件的头两个字节一定是 0xFFD8 最后两个字节是 0xFFD9

JPEG文件格式详细介绍:

JPEG文件的存储格式有很多种,但最常用的是JFIF格式,即JPEG File Interchange Format。JPEG文件大体可以分为两个部分:

(1)标记码;由两个字节构成,其中,前一个字节是固定值0XFF代表了一个标记码的开始,后一个字节不同的值代表着不同的含义。需要提醒的是,连续的多个0XFF可以理解为一个0XFF,并表示一个标记码的开始。另外,标记码在文件中一般是以标记代码的形式出现的。例如,SOI的标记代码是0XFFD8,即,如果JPEG文件中出现了0XFFD8,则代表此处是一个SOI标记。

(2)压缩数据;一个完整的两字节标记码的后面,就是该标记码对应的压缩数据了,它记录了关于文件的若干信息。

一些典型的标记码,及其所代表的含义如下所示:

SOI,Start Of Image, 图像开始,标记代码为固定值0XFFD8,用2字节表示;

APP0,Application 0, 应用程序保留标记0,标记代码为固定值0XFFE0,用2字节表示;该标记码之后包含了9个具体的字段:

(1)数据长度:2个字节,用来表示(1)–(9)的9个字段的总长度,即不包含标记代码但包含本字段;

(2)标示符:5个字节,固定值0X4A6494600,表示了字符串“JFIF0”;

(3)版本号:2个字节,一般为0X0102,表示JFIF的版本号为1.2;但也可能为其它数值,从而代表了其它版本号;

(4)X,Y方向的密度单位:1个字节,只有三个值可选,0:无单位;1:点数每英寸;2:点数每厘米;

(5)X方向像素密度:2个字节,取值范围未知;

(6)Y方向像素密度:2个字节,取值范围未知;

(7)缩略图水平像素数目:1个字节,取值范围未知;

(8)缩略图垂直像素数目:1个字节,取值范围未知;

(9)缩略图RGB位图:长度可能是3的倍数,保存了一个24位的RGB位图;如果没有缩略位图(这种情况更常见),则字段(7)(8)的取值均为0;

APPn, Application n, 应用程序保留标记n(n=1—15),标记代码为2个字节,取值为0XFFE1–0XFFFF;包含了两个字段:

(1)数据长度,2个字节,表示(1)(2)两个字段的总长度;即,不包含标记代码,但包含本字段;

(2)详细信息:数据长度-2个字节,内容不定;

DQT,Define Quantization Table, 定义量化表;标记代码为固定值0XFFDB;包含9个具体字段:

(1)数据长度:2个字节,表示(1)和多个(2)字段的总长度;即,不包含标记代码,但包含本字段;

(2)量化表:数据长度-2个字节,其中包括以下内容:

(a)精度及量化表ID,1个字节,高4位表示精度,只有两个可选值,0:8位;1:16位;低4位表示量化表ID,取值范围为0–3;

(b)表项,64(精度取值+1)个字节,例如,8位精度的量化表,其表项长度为64(0+1)=64字节;

本标记段中,(2)可以重复出现,表示多个量化表,但最多只能出现4次;

SOFO,Start Of Frame, 帧图像开始,标记代码为固定值0XFFC0;包含9个具体字段:

(1)数据长度:2个字节,(1)–(6)共6个字段的总长度;即,不包含标记代码,但包含本字段;

(2)精度:1个字节,代表每个数据样本的位数;通常是8位;

(3)图像高度:2个字节,表示以像素为单位的图像高度,如果不支持DNL就必须大于0;

(4)图像宽度:2个字节,表示以像素为单位的图像宽度,如果不支持DNL就必须大于0;

(5)颜色分量个数:1个字节,由于JPEG采用YCrCb颜色空间,这里恒定为3;

(6)颜色分量信息:颜色分量个数*3个字节,这里通常为9个字节;并依此表示如下一些信息:

(a)颜色分量ID: 1个字节;

(b)水平/垂直采样因子:1个字节,高4位代表水平采样因子,低4位代表垂直采样因子;

(c)量化表:1个字节,当前分量使用的量化表ID;

本标记段中,字段(6)应该重复出现3次,因为这里有3个颜色分量;

DHT,Define Huffman Table定义Huffman表,标记码为0XFFC4;包含2个字段:

(1)数据长度,2个字节,表示(1)–(2)的总长度,即,不包含标记代码,但包含本字段;

(2)Huffman表,数据长度-2个字节,包含以下字段:

(a)表ID和表类型,1个字节,高4位表示表的类型,取值只有两个;0:DC直流;1:AC交流;低4位,Huffman表ID;需要提醒的是,DC表和AC表分开进行编码;

(b)不同位数的码字数量,16个字节;

(c)编码内容,16个不同位数的码字数量之和(字节);

本标记段中,字段(2)可以重复出现,一般需要重复4次。

DRI,Define Restart Interval,定义差分编码累计复位的间隔,标记码为固定值0XFFDD;

包含2个具体字段:

(1)数据长度:2个字节,取值为固定值0X0004,表示(1)(2)两个字段的总长度;即,不包含标记代码,但包含本字段;

(2)MCU块的单元中重新开始间隔:2个字节,如果取值为n,就代表每n个MCU块就有一个RSTn标记;第一个标记是RST0,第二个是RST1,RST7之后再从RST0开始重复;如果没有本标记段,或者间隔值为0,就表示不存在重开始间隔和标记RST;

SOS,Start Of Scan,扫描开始;标记码为0XFFDA,包含2个具体字段:

(1)数据长度:2个字节,表示(1)–(4)字段的总长度;

(2)颜色分量数目:1个字节,只有3个可选值,1:灰度图;3:YCrCb或YIQ;4:CMYK;

(3)颜色分量信息:包括以下字段,

(a)颜色分量ID:1个字节;

(b)直流/交流系数表ID,1个字节,高4位表示直流分量的Huffman表的ID;低4位表示交流分量的Huffman表的ID;

(4)压缩图像数据

(a)谱选择开始:1个字节,固定值0X00;

(b)谱选择结束:1个字节,固定值0X3F;

(c)谱选择:1个字节,固定值0X00;

本标记段中,(3)应该重复出现,有多少个颜色分量,就重复出现几次;本段结束之后,就是真正的图像信息了;图像信息直到遇到EOI标记就结束了;

EOI,End Of Image,图像结束;标记代码为0XFFD9;

另外,需要说明的是,在JPEG中0XFF具有标记的意思,所以在压缩数据流(真正的图像信息)中,如果出现了0XFF,就需要做特别处理了。方法是,如果在图像数据流中遇到0XFF,应该检测其紧接着的字符,如果是:

(1)0X00,表示0XFF是图像流的组成部分;需要进行译码;

(2)0XD9,表示与0XFF组成标记EOI,即,代表图像流的结束,同时,图像文件结束;

(3)0XD0–0XD7,组成RSTn标记,需要忽视整个RSTn标记,即不对当前0XFF和紧接着的0XDn两个字节进行译码,并按RST标记的规则调整译码变量;

(4)0XFF,忽略当前0XFF,对后一个0XFF进行判断;

(5)其它数值,忽然当前0XFF,并保留紧接着此数值用于译码;

​ 需要说明的是,JPEG文件格式中,一个字(16位)的存储使用的是Motorola格式,而不是Intel格式。也就是说,一个字的高字节(高8位)在数据流的前面,低字节(低8位)在数据流的后面,与平时习惯的Intel格式有所不同。这种字节顺序问题的起因在于早期的硬件发展上。在8位CPU的时代,许多8位CPU都可以处理16位的数据,但它们显然是分两次进行处理的。这个时候就出现了先处理高位字节还是先处理低位字节的问题。以Intel为代表的厂家生产的CPU采用先低字节后高字节的方式;而以Motorola,IBM为代表的厂家生产的CPU则采用了先高字节后低字节的方式。Intel的字节顺序也称为little-endian,而Motorola的字节顺序就叫做big-endian。而JPEG/JFIF文件格式则采用了big-endian格式。下面的函数,实现了从intel格式到motolora格式的转换

JPEG用于无线图像传输

​ 常用的JPEG算法库:opencv、simplejpeg、PyTurboJPEG等。这些算法库提供JPEG压缩和解压缩的API,当需要将原图进行压缩并存储时可以直接使用这些算法库实现。使用案例如下:

import numpy as np
import cv2

img = cv2.imread('source1.jpg')

# cv2.imencode('.jpg', img, [int(cv2.IMWRITE_JPEG_QUALITY), 95]) # 这种写法的95是图像质量,不写这个参数默认95
# 默认图像质量为95
img_encode = cv2.imencode('.jpg', img)[1] # '.jpg'表示把当前图片img按照jpg格式编码,按照不同格式编码的结果不一样 imgg = cv2.imencode('.png', img)
print(img_encode)
str_encode = img_encode.tobytes()
# 缓存数据保存到本地,以txt格式保存
with open('img_encode.txt', 'wb') as f:
f.write(str_encode)
f.flush()

with open('img_encode.txt', 'rb') as f:
str_encode = f.read()

nparr = np.frombuffer(str_encode, np.uint8) # ndarray格式
img_decode = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

cv2.imshow("img_decode", img_decode)
cv2.waitKey()

'''
# 读取图像
img = cv2.imread('source1.jpg')
print(img.shape)
# 2624400
# 将图像编码为JPEG格式二进制数据
retval, buffer = cv2.imencode('.jpg', img, [int(cv2.IMWRITE_JPEG_QUALITY), 95])
#retval, buffer = cv2.imencode('.jpg', img)

print(buffer.shape)

# 将二进制数据写入文件
with open('image_encoded.jpg', 'wb') as f:
f.write(buffer)
'''

​ 直接调用已有的算法库缺点是无法了解底层实现机制。这只是小问题,主要问题是无法直接用于无线通信,因为JPEG算法库提供的API都是将原图直接压缩成.jpg文件,支持在windows等系统上使用图像查看软件直接打开查看,虽然能直接打开查看压缩结果是一种优势,但这也意味着图像文件中包含了很多必要的Marker。如果将其在无线环境中传输,一旦Marker受损,则无法实现JPEG解压缩,理想情况是只有原始图像数据压缩后的数据受损,而Marker部分不受损才能实现解压缩,这在较差的实际环境中几乎不可能实现。而且在物理层传输对象是比特流,在通信系统中还需要使用信道编码降低误码率。因此不得不自己编写适用于无线环境传输的JPEG算法,即便是这样,也需要假设用于解压缩的一些关键信息在接收端无差错接收。因此参考网上已有的使用python实现JPEG算法:

import numpy as np
import os
from PIL import Image

class KJPEG:
def __init__(self):

# 初始化DCT变换的A矩阵
# 该矩阵用于控制压缩率,离散余弦变换是无损的
self.__dctA = np.zeros(shape=(8, 8)) # 8*8
for i in range(8):
c = 0
if i == 0:
c = np.sqrt(1 / 8)
else:
c = np.sqrt(2 / 8)
for j in range(8):
self.__dctA[i, j] = c * np.cos(np.pi * i * (2 * j + 1) / (2 * 8))
# 亮度量化矩阵
self.__lq = np.array([
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
])
# 色度量化矩阵
self.__cq = np.array([
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
])
# 标记矩阵类型,lt是亮度矩阵,ct是色度矩阵
self.__lt = 0
self.__ct = 1
# Zig编码表
self.__zig = np.array([
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63
])
# Zag编码表
self.__zag = np.array([
0, 1, 5, 6, 14, 15, 27, 28,
2, 4, 7, 13, 16, 26, 29, 42,
3, 8, 12, 17, 25, 30, 41, 43,
9, 11, 18, 24, 31, 40, 44, 53,
10, 19, 23, 32, 39, 45, 52, 54,
20, 22, 33, 38, 46, 41, 55, 60,
21, 34, 37, 47, 50, 56, 59, 61,
35, 36, 48, 49, 57, 58, 62, 63
])

def __Rgb2Yuv(self, r, g, b):
# 从图像获取YUV矩阵
y = 0.299 * r + 0.587 * g + 0.114 * b
u = -0.1687 * r - 0.3313 * g + 0.5 * b + 128
v = 0.5 * r - 0.419 * g - 0.081 * b + 128
return y, u, v

def __Fill(self, matrix):
# 图片的长宽都需要满足是16的倍数(采样长宽会缩小1/2和取块长宽会缩小1/8)
# 图像压缩三种取样方式4:4:4、4:2:2、4:2:0
fh, fw = 0, 0
if self.height % 16 != 0:
fh = 16 - self.height % 16
if self.width % 16 != 0:
fw = 16 - self.width % 16
res = np.pad(matrix, ((0, fh), (0, fw)), 'constant',
constant_values=(0, 0))
return res

def __Encode(self, matrix, tag):
# 先对矩阵进行填充
matrix = self.__Fill(matrix)
# 将图像矩阵切割成8*8小块
height, width = matrix.shape
# 减少for循环语句,利用numpy的自带函数来提升算法效率
# 参考吴恩达的公开课视频,numpy的函数自带并行处理,不用像for循环一样串行处理
shape = (height // 8, width // 8, 8, 8) # (28, 28, 8, 8)
strides = matrix.itemsize * np.array([width * 8, 8, width, 1])
blocks = np.lib.stride_tricks.as_strided(matrix, shape=shape, strides=strides)
res = []
# 对每个8*8的block做处理
for i in range(height // 8):
for j in range(width // 8):
res.append(self.__Quantize(self.__Dct(blocks[i, j]).reshape(64), tag))
return res

def __Dct(self, block):
# DCT变换
res = np.dot(self.__dctA, block)
res = np.dot(res, np.transpose(self.__dctA))
return res

def __Quantize(self, block, tag):
res = block
if tag == self.__lt:
res = np.round(res / self.__lq)
elif tag == self.__ct:
res = np.round(res / self.__cq)
return res

def __Zig(self, blocks):
ty = np.array(blocks) # 784*64
tz = np.zeros(ty.shape)
for i in range(len(self.__zig)):
tz[:, i] = ty[:, self.__zig[i]]
tz = tz.reshape(tz.shape[0] * tz.shape[1])
return tz.tolist()

def __Rle(self, blist):
# blist是50176的list列表,3个,分别为yuv
res = []
cnt = 0
for i in range(len(blist)):
if blist[i] != 0:
res.append(cnt)
res.append(int(blist[i]))
cnt = 0
elif cnt == 15:
res.append(cnt)
res.append(int(blist[i]))
cnt = 0
else:
cnt += 1
# 末尾全是0的情况
if cnt != 0:
res.append(cnt - 1)
res.append(0)
return res

def Compress(self, filename):
# 根据路径image_path读取图片,并存储为RGB矩阵
image = Image.open(filename)
# 获取图片宽度width和高度height
self.width, self.height = image.size
image = image.convert('RGB')
image = np.asarray(image)

r = image[:, :, 0]
g = image[:, :, 1]
b = image[:, :, 2]
# 将图像RGB转YUV
y, u, v = self.__Rgb2Yuv(r, g, b) # (224, 224)
# 对图像矩阵进行编码
y_blocks = self.__Encode(y, self.__lt) # 784长度的list,每个元素是一个block编码后结果
u_blocks = self.__Encode(u, self.__ct)
v_blocks = self.__Encode(v, self.__ct)
# 对图像小块进行Zig编码和RLE编码
y_code = self.__Rle(self.__Zig(y_blocks)) # 29948
u_code = self.__Rle(self.__Zig(u_blocks)) # 8650
v_code = self.__Rle(self.__Zig(v_blocks)) # 8046
# 计算VLI可变字长整数编码并写入文件,未实现Huffman部分
buff = 0
tfile = os.path.splitext(filename)[0] + ".gpj"
if os.path.exists(tfile):
os.remove(tfile)
with open(tfile, 'wb') as o:
o.write(self.height.to_bytes(2, byteorder='big')) # 2字节高度
o.flush()
o.write(self.width.to_bytes(2, byteorder='big')) # 2字节宽度
o.flush()
o.write((len(y_code)).to_bytes(4, byteorder='big')) # 4字节y长度
o.flush()
o.write((len(u_code)).to_bytes(4, byteorder='big')) # 4字节u长度
o.flush()
o.write((len(v_code)).to_bytes(4, byteorder='big')) # 4字节v长度
o.flush()
self.__Write2File(tfile, y_code, u_code, v_code)

def __Write2File(self, filename, y_code, u_code, v_code):
with open(filename, "ab+") as o:
buff = 0
bcnt = 0
data = y_code + u_code + v_code
for i in range(len(data)):
if i % 2 == 0:
td = data[i]
for ti in range(4):
buff = (buff << 1) | ((td & 0x08) >> 3)
td <<= 1
bcnt += 1
if bcnt == 8:
o.write(buff.to_bytes(1, byteorder='big'))
o.flush()
buff = 0
bcnt = 0
else:
td = data[i]
vtl, vts = self.__VLI(td)
for ti in range(4):
buff = (buff << 1) | ((vtl & 0x08) >> 3)
vtl <<= 1
bcnt += 1
if bcnt == 8:
o.write(buff.to_bytes(1, byteorder='big'))
o.flush()
buff = 0
bcnt = 0
for ts in vts:
buff <<= 1
if ts == '1':
buff |= 1
bcnt += 1
if bcnt == 8:
o.write(buff.to_bytes(1, byteorder='big'))
o.flush()
buff = 0
bcnt = 0
if bcnt != 0:
buff <<= (8 - bcnt)
o.write(buff.to_bytes(1, byteorder='big'))
o.flush()
buff = 0
bcnt = 0

def __IDct(self, block):
# IDCT变换
res = np.dot(np.transpose(self.__dctA), block)
res = np.dot(res, self.__dctA)
return res

def __IQuantize(self, block, tag):
res = block
if tag == self.__lt:
res *= self.__lq
elif tag == self.__ct:
res *= self.__cq
return res

def __IFill(self, matrix):
matrix = matrix[:self.height, :self.width]
return matrix

def __Decode(self, blocks, tag):
tlist = []
for b in blocks:
b = np.array(b)
tlist.append(self.__IDct(self.__IQuantize(b, tag).reshape(8 ,8)))
height_fill, width_fill = self.height, self.width
if height_fill % 16 != 0:
height_fill += 16 - height_fill % 16
if width_fill % 16 != 0:
width_fill += 16 - width_fill % 16
rlist = []
for hi in range(height_fill // 8):
start = hi * width_fill // 8
rlist.append(np.hstack(tuple(tlist[start: start + (width_fill // 8)])))
matrix = np.vstack(tuple(rlist))
res = self.__IFill(matrix)
return res

def __ReadFile(self, filename):
with open(filename, "rb") as o:
tb = o.read(2)
self.height = int.from_bytes(tb, byteorder='big') # 1080
tb = o.read(2)
self.width = int.from_bytes(tb, byteorder='big') # 810
tb = o.read(4)
ylen = int.from_bytes(tb, byteorder='big') # 427458
tb = o.read(4)
ulen = int.from_bytes(tb, byteorder='big') # 130636
tb = o.read(4)
vlen = int.from_bytes(tb, byteorder='big') # 125942

buff = 0
bcnt = 0
rlist = []
itag = 0
icnt = 0
vtl, tb, tvtl = None, None, None
while len(rlist) < ylen + ulen + vlen:
if bcnt == 0:
tb = o.read(1)
if not tb:
break
tb = int.from_bytes(tb, byteorder='big')
bcnt = 8
if itag == 0:
buff = (buff << 1) | ((tb & 0x80) >> 7)
tb <<= 1
bcnt -= 1
icnt += 1
if icnt == 4:
rlist.append(buff & 0x0F)
elif icnt == 8:
vtl = buff & 0x0F
tvtl = vtl
itag = 1
buff = 0
else:
buff = (buff << 1) | ((tb & 0x80) >> 7)
tb <<= 1
bcnt -= 1
tvtl -= 1
if tvtl == 0 or tvtl == -1:
rlist.append(self.__IVLI(vtl, bin(buff)[2:].rjust(vtl, '0')))
itag = 0
icnt = 0
y_dcode = rlist[:ylen]
u_dcode = rlist[ylen:ylen+ulen]
v_dcode = rlist[ylen+ulen:ylen+ulen+vlen]
return y_dcode, u_dcode, v_dcode
pass

def __Zag(self, dcode):
dcode = np.array(dcode).reshape((len(dcode) // 64, 64))
tz = np.zeros(dcode.shape)
for i in range(len(self.__zag)):
tz[:, i] = dcode[:, self.__zag[i]]
rlist = tz.tolist()
return rlist

def __IRle(self, dcode):
rlist = []
for i in range(len(dcode)):
if i % 2 == 0:
rlist += [0] * dcode[i]
else:
rlist.append(dcode[i])
return rlist

def Decompress(self, filename):
y_dcode, u_dcode, v_dcode = self.__ReadFile(filename)

y_blocks = self.__Zag(self.__IRle(y_dcode)) # 784*64 list
u_blocks = self.__Zag(self.__IRle(u_dcode))
v_blocks = self.__Zag(self.__IRle(v_dcode))
#############################
# y_blocks、u_blocks和v_blocks都是待传的-127~128整数列表,我们只需要在这里编写将其转换为比特流,然后信道编码、调制、通过信道、解调、信道译码、重新转为y_blocks、u_blocks和v_blocks即可实现传统通信系统
#############################
y = self.__Decode(y_blocks, self.__lt) # 224*224
u = self.__Decode(u_blocks, self.__ct)
v = self.__Decode(v_blocks, self.__ct)
r = (y + 1.402 * (v - 128))
g = (y - 0.34414 * (u - 128) - 0.71414 * (v - 128))
b = (y + 1.772 * (u - 128))
r = Image.fromarray(r).convert('L')
g = Image.fromarray(g).convert('L')
b = Image.fromarray(b).convert('L')
image = Image.merge("RGB", (r, g, b))
image.save("./result.bmp", "bmp")
image.show()

def __VLI(self, n):
# 获取整数n的可变字长整数编码
ts, tl = 0, 0
if n > 0:
ts = bin(n)[2:]
tl = len(ts)
elif n < 0:
tn = (-n) ^ 0xFFFF
tl = len(bin(-n)[2:])
ts = bin(tn)[-tl:]
else:
tl = 0
ts = '0'
return (tl, ts)

def __IVLI(self, tl, ts):
# 获取可变字长整数编码对应的整数n
if tl != 0:
n = int(ts, 2)
if ts[0] == '0':
n = n ^ 0xFFFF
n = int(bin(n)[-tl:], 2)
n = -n
else:
n = 0
return n

if __name__ == '__main__':
kjpeg = KJPEG()
kjpeg.Compress("./source1_224.jpg")
kjpeg.Decompress("./source1_224.gpj")

​ 上述算法仅仅展示了JPEG压缩和解压缩的过程,并且没有使用huffman编码,因为模拟无线环境传输时可以直接使用定长编码替代

​ 通信系统在Decompress函数中实现