第八章 图像压缩和水印
1.前言
图像压缩是我们比较熟悉的应用技术,它能减少图像所需的数据量,从而降低存储空间,在传输上也有很好的效果,与之对应的解压技术也是重要的一环。本章主要讲述了压缩的技术,能应用在静止图像和视频,最后介绍了图像水印的技术
2.基础
具有无关或者重复信息的数据叫做冗余数据,令 b b b和 b ′ b^{'} b′为相同信息的两个比特数(信息携带单元),则 b b b比特表示的相对数据冗余 R R R是 R = 1 − 1 C R=1-\frac{1}{C} R=1−C1其中 C C C称为压缩率 C = b b ′ C=\frac{b}{b^{'}} C=b′b比如 C C C=10(或写为10:1)表示小表示为 1 b i t 1bit 1bit数据,大表示为 10 b i t 10bit 10bit,对应的数据冗余为 R = 0.9 R=0.9 R=0.9,表示有 90 % 90\% 90%的数据量是冗余的
在图像压缩中, b b b是将图像表示为而为二维灰度值阵列所需的比特数,而二维灰度值阵列受三种数据冗余的影响:
- 编码冗余:不同的编码类型对于同一数据的表示会有差异,比如用相同位数的二进制编码和最优的哈夫曼编码表示使用频率不一的英文,二进制编码相对哈夫曼就会有冗余
- 空间和时间冗余:二维灰度值阵列的像素空间相关导致的信息重复,比如同种颜色
- 无关信息:即人眼能观察看到的无关图像,比如基本全黑的图像里有用信息被遮住了
以上的三种冗余都能通过直方图观察出来,并通过映射、量化等方式进行数据量的减少,而信源的熵 H H H用来度量最少的平均图像信息。用来量化图像信息损失的标准有客观保真和主观保真两个准则
3.针对编码冗余
霍夫曼编码
这个和数据结构中的哈夫曼编码一样。它的思想是对出现频率越高的值,分配越短的编码长度,相应地对出现频率越低的值则分配较长的编码长度,它是一种无损编码方法,
Golomb编码
Golomb编码是一种可变长度编码方法,常用于数据压缩和无损压缩领域,它是一种分组编码,需要一个正整数参数m,然后以m为单位对待编码的数字进行分组,如下图:
对于任一非负整数N,通过除法
N
÷
m
N \div m
N÷m得到商
q
q
q和余数
r
r
r,其中商是组号,使用一元编码(q个1后跟个0),余下部分r则使用固定长度的二进制编码,下面详细说明
二进制编码根据编码数字大小的不同有不同的处理方法:
- 如果参数m是2的次幂(这也是下面将要介绍的Golomb-Rice编码),则使用取r的二进制表示的低 log 2 ( m ) log 2 ( m ) \log2(m)\log2(m) log2(m)log2(m)位,作为r的码字
- 如果参数m不是2的次幂,如果m不是2的次幂,设
k
=
⌈
log
2
m
⌉
k=⌈\log_2m⌉
k=⌈log2m⌉
- 如果 r < 2 b − m r < 2 b − m r<2b−mr<2b−m r<2b−mr<2b−m,则使用 b − 1 b-1 b−1位的二进制编码r。
- 如果 r ≥ 2 b − m r ≥ 2 b − m r\geq2b−mr\geq2b−m r≥2b−mr≥2b−m,则使用b位二进制对 r + 2 b − m r+2b−m r+2b−m进行编码
其中 ⌈ a ⌉ ⌈a⌉ ⌈a⌉表示大于a的最小整数, ⌊ a ⌋ ⌊a⌋ ⌊a⌋表示小于a的最大整数
Golomb编码是适合小的数字比大的数字出现概率比较高的编码,它具有简单性、无损压缩、适用性广泛、自适应性和随机访问的优点,但同时,对于数据分布不均匀或包含大量极端值的情况,Golomb编码可能无法获得较好的压缩效果,解码的速度可能相对较慢
算术编码
算术编码也是一种无损压缩方法,通过将整个数据序列映射到一个区间内的实数来实现压缩。它的基本思想是根据每个符号的概率分布来分配它在区间中的长度,从而实现更高效的编码,具体过程如下:
- 确定符号集合:首先确定要编码的符号集合,可以是字母、数字、像素值等。每个符号都有对应的概率分布。
- 计算累积概率:根据符号的概率分布,计算每个符号的累积概率。累积概率是指该符号及其之前的所有符号的概率之和。累积概率通常被表示为区间的上下界。
- 初始化编码区间:将编码区间初始化为0到1之间的范围。初始区间表示整个数据序列的范围。
- 编码过程,对于每个要编码的符号:
- 根据符号的累积概率和当前编码区间的范围,确定符号在当前区间内的子区间。
- 更新当前编码区间为该子区间。
- 重复上述步骤。
- 输出编码:当所有符号都被编码后,输出位于当前编码区间中的任意一位数。这个数将作为压缩后的编码结果。
举个例子,假设要编码的符号集合为{A(0.4), B(0.3), C(0.2), D(0.1)},括号内是对应的概率,先计算累计概率[A(0.4), B(0.7), C(0.9), D(1)],再初始化编码区间为从0到1的范围,假设要编码的符号序列为"AACBD",则有
符号 | 子区间 | 更新编码区间 | 对应符号序列 |
---|---|---|---|
A | [0, 0.4) | [0, 0.4) | A |
A | [0, 0.4) | [0, 0.16) | AA |
C | [0.7, 0.9) | [0.112, 0.144) | AAC |
B | [0.4, 0.7) | [0.1248, 0.1344) | AACB |
D | [0.9, 1) | [0.13344, 0.13440) | AACBD |
最后得到的D编码区间内的任意数字都能表示‘AACBD’这个符号集合消息
解码过程中,算术编码使用相同的编码表和概率分布进行反向计算。根据解码器收到的编码值,它将该值映射到原始的符号,并更新区间范围。通过不断缩小区间范围并逐个解码符号,最终可以还原原始的数据序列。
4.针对空间冗余
行程编码
对于行列中的重复元素,采用相同灰度的行程对来进行压缩,每个行程对规定了新灰度的起点和连续像素数,这就是行程编码,它的基本思想是将连续出现的相同数据值替换为一个标记符号和重复次数的组合,从而减少数据的存储空间
行程编码有算法简单、无损压缩、运行速度快、消耗资源少等优点,但同时又有适用性有限、在相隔像素不重复时压缩率受限和解压缩开销的缺点
# 读取图像
image = cv.imread('./img/cat1.jpeg',0)
rows, cols = image.shape
#把灰度化后的二维图像降维为一维列表
image1 = image.flatten()
#二值化操作
for i in range(len(image1)):
if image1[i] >= 127:
image1[i] = 255
if image1[i] < 127:
image1[i] = 0
data = []
image3 = []
count = 1
#行程压缩编码
for i in range(len(image1)-1):
if (count == 1):
image3.append(image1[i])
if image1[i] == image1[i+1]:
count = count + 1
if i == len(image1) - 2:
image3.append(image1[i])
data.append(count)
else:
data.append(count)
count = 1
if(image1[len(image1)-1] != image1[-1]):
image3.append(image1[len(image1)-1])
data.append(1)
#压缩率
ys_rate = len(image3)/len(image1)*100
print('压缩率为' + str(ys_rate) + '%')
#行程编码解码
rec_image = []
for i in range(len(data)):
for j in range(data[i]):
rec_image.append(image3[i])
rec_image = np.reshape(rec_image,(rows,cols))
# 显示原图和结果图
cv.imshow('Original Image', image)
cv.imshow('Decoded Image', rec_image)
cv.waitKey(0)
cv.destroyAllWindows()
能够看到与原图相比,压缩后的图像去除了很多的多余数据,保留了较多的图像的信息,但似乎损失一些细节,比如猫的眼睛和垃圾桶以及右上角道路的细节,形态被勾勒得不完全
基于符号的编码
所谓符号编码,就是用子图像(符号)组合来表示图片,子图像存储在字典中,且用编码为三元组集合 { ( x 1 , y 1 , t 1 ) , ( x 2 , y 2 , t 2 ) , . . . } \{(x_1,y_1,t_1),(x_2,y_2,t_2),...\} {(x1,y1,t1),(x2,y2,t2),...},前两位表示符号在图像的位置,后一个表示符号在字典中的地址。这样只存储一次重复的符号,减少了数据量,举例如下:
按照上图,记符号b、a、n在字典中位置为0、1、2,则第一个b的三元组(0,2,0)表示b在文档中第0行第2列,其他同理
5.比特平面编码
同之前提到过的比特平面分层,比特平面编码也是将图像的像素值分解为多个比特平面进行表示。每个比特平面都包含了图像的一部分信息,从最高有效位到最低有效位。其基本过程如下:
- 图像分解:将原始图像的每个像素值转换为二进制形式。对于一个8位灰度图像,每个像素值可以表示为8个比特(位)
- 比特平面分解:将每个像素的二进制表示分解为8个比特平面。第一个比特平面(最高有效位)包含最高位上的比特,而最后一个比特平面(最低有效位)包含最低位上的比特
- 编码:对每个比特平面进行编码,以减小数据量或提高数据传输效率。这可以通过应用不同的压缩算法来实现,比如行程长度编码或哈夫曼编码
- 解码和重建:对编码后的比特平面进行解码,恢复为原始的像素值。通过将解码的比特平面重新组合,可以重建原始的图像
# 定义比特平面编码函数
def bit_plane_encoding(image):
height, width = image.shape[:2]
planes = []
for i in range(8):
plane = (image >> i) & 1 # 提取比特位
planes.append(plane)
return np.stack(planes, axis=2)
# 读取原始图像
image = cv2.imread('./img/cat1.jpeg', cv2.IMREAD_GRAYSCALE)
# 进行比特平面编码
encoded_image = bit_plane_encoding(image)
# 显示原始图像
plt.subplot(2, 5, 1)
plt.imshow(image, cmap='gray')
plt.title('Original Image')
plt.axis('off')
# 显示比特平面编码后的图像
for i in range(8):
plt.subplot(2, 5, i + 2)
plt.imshow(encoded_image[:, :, i], cmap='gray')
plt.title('Bit Plane {}'.format(i))
plt.axis('off')
plt.tight_layout()
plt.show()
如上图所示,较高位的比特平面包含了图像的全局信息,而较低位的比特平面则包含图像的细节和噪声,在比特值为2、3、4中能看到猫躺着的白色区域,噪声很多,并没有显示白色的明显结构。而比特值为6、7中则较完整显示了图像的整体轮廓
6.预测编码
预测编码基于一个基本原理:通过消除紧邻像素的时空冗余来实现,即提取像素的新信息(像素实际值与预测值之差)。由于是预测性质的,所以很明显是针对视频压缩而言的。预测编码的基本步骤如下:
- 基于图像的特性和编码的需求选择合适的预测模型,比如无损预测编码模型,根据周围像素的值来估计当前像素的值
- 计算预测误差:将预测值与实际像素值进行比较,得到预测误差 e ( n ) = f ( n ) − f ^ ( n ) e(n)=f(n)-\widehat f(n) e(n)=f(n)−f (n)
- 编码预测误差:对预测误差进行编码和压缩。编码过程会根据预测误差的统计特性来选择合适的编码方式,以减少数据的冗余性
- 存储编码后的数据:将编码后的数据存储起来,通常会使用比特流或字节流的形式。
其中无损预测编码模型如下,编码器和解码器都有相同的预测器
运动补偿预测残差
由于在视频压缩时,通常对静止的部分压缩效果好,运动的部分由于不相似性大,容易导致数据扩展。因此图像更适合的基于时间预测编码对视频就不太适合,数据扩展问题有以下两种方法解决:1. 运动补偿 2.预测编码咩有优势时,用另一种编码方式
运动补偿预测编码就是根据前后视频帧的宏块的运动估计每个宏块的位置达到压缩的效果,解压反之,其中运动估计这一关键部分常用平均绝对失真(MAD)作为误差测度
7.小波编码
小波编码的基本思想是:对图像像素去相关变换系数编码,要比对图像像素本身编码更有效,也就是通过计算图像像素的小波系数进行去相关变换,使得图像压缩至更小且保留高频细节。通过使用小波函数将大多数重要的可视信息分组至少量系数中,其他的信息被量化或归零,以此图像几乎没有失真
下图显示了一个小波编码系统,对图像进行小波变换将原图中大部分转换为水平、垂直和对角分解系数,由于很多计算的系数的视觉信息很少,通过量化器进行量化和编码这些系数,最后用一种或多种无损编码方法实现压缩,解码过程则相反
影响小波编码计算的因素主要有:作为变换的基的小波、变换分解级数、系数量化
- 变换的基的小波:也就是选择什么小波进行编码,不同类型的小波有不同的特性,如哈尔小波是最简单且不连续的小波、Daubechies小波是最常用的小波、对称小波是增强了对称性的 Daubechies小波等等
- 变换分解级数:在小波变换中,正反变换计算的次数会随着变换分解级数的增加而增加,且量化增大的低尺度系数会产生更多的分解级数,并影响重建图像的增大区域
- 系数量化:一般的小波变换需要均匀量化的量化器进行量化,但是量化效果还可进一步改进,比如在零附近引入更大的量化区间(死区)
8.数字图像水印
为了保证图像所有者的权益,水印诞生了,它主要有版权识别、真实性判定、自动监视、复制保护等功能。令 f w f_w fw表示水印图像,它可由原图 f f f和水印 w w w的线性组合表示 f w = ( 1 − α ) f + α w f_w=(1-\alpha)f+\alpha w fw=(1−α)f+αw其中常数 α \alpha α控制水印和图像的相对可见度,当 α = 1 \alpha=1 α=1时,水印不透明,反之同理
import cv2
def add_watermark(input_image, watermark_image, watermark_x, watermark_y, watermark_scale=1.0, bg_color=(0, 0, 0)):
# 将水印图片转换为 RGB 格式
watermark_rgb = cv2.cvtColor(watermark_image, cv2.COLOR_BGR2RGB)
# 计算水印图片的缩放比例
watermark_scale_factor = watermark_scale * (input_image.shape[1] / watermark_rgb.shape[1])
# 根据缩放比例调整水印图片位置
watermark_x = watermark_x * watermark_scale_factor
watermark_y = watermark_y * watermark_scale_factor
# 对水印图片进行缩放并调整位置
watermark = cv2.resize(watermark_rgb, None, fx=watermark_scale_factor, fy=watermark_scale_factor, interpolation=cv2.INTER_LINEAR)
watermark_img = cv2.resize(watermark, (input_image.shape[1], input_image.shape[0]), interpolation=cv2.INTER_LINEAR)
# 在输入图片上绘制水印
cv2.putText(watermark_img, f"Watermark Image", (watermark_x, watermark_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (bg_color, bg_color, bg_color), 2)
# 保存添加水印后的图片
output_image = np.copy(input_image)
cv2.imwrite("output_image.jpg", output_image)
return output_image
# 读取图片
input_image = cv2.imread("./img/cat1.jpeg", 0)
watermark_image = cv2.imread("./img/pinghua.jpeg", 0)
# 设置水印位置
watermark_x = int(input_image.shape[1] * 0.8)
watermark_y = int(input_image.shape[0] * 0.5)
# 添加水印图片
# 添加水印文字
cv2.putText(input_image, "Watermark Image", (watermark_x, watermark_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
# 显示添加水印后的图片
cv2.imshow("Watermarked Image", input_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
上述代码就是给一张图片添加了字母水印,可以看到水印被添加了,实际上通过调整参数,可以控制水印的位置,大小,透明度等等
章总结
在本章学习了图像压缩的相关知识,比如压缩的各类编码,有针对编码冗余的霍夫曼编码、Golomb编码、算数编码,也有针对空间冗余的行程编码和基于符号的编码,以及其他编码内容,最后简要分析了数字图像水印的生成和相关的知识