持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
声明:此篇文章实现的是基础的霍夫曼编码压缩,不能保证其压缩效果。尤其是对于已压缩过的大分辨率jpg图像。
(一) OpenCV环境安装
在网站www.lfd.uci.edu/~gohlke/pyt…中下载合适的版本,将其放在Anaconda3\Lib\site-packages目录下:
运行Anaconda Prompt,进入到该目录并执行指令:
pip install opencv_python-4.5.5-cp39-cp39-win_amd64.whl
初始下载的版本不合适,提示不兼容:
ERROR: opencv_python-4.5.5+mkl-cp37-cp37m-win_amd64.whl is not a supported wheel on this platform
可键入如下指令:
python -m pip debug –verbose
从而查看对应的兼容tags
下载成功截图如下:
后续通过import cv2 即可导入OpenCV库
(二) . 无损压缩方法
1. 初始数据获取
cv.imread(filename,flag)函数用于读取指定的图像文件,并将读取回来的结果返回。其中filename为读取文件路径,flag表示读取图片的方式,可以设置为读取三通道的BGR 彩色图像或单通道的灰度图像。若读取成功,将返回一个nparray 多维数组。该数组将作为后续压缩算法的初始数据。
对其中一张图片调用该方法并输出结果:
输出结果如下:可知读取后的数据为三维数组,且值分布为0 – 255。
2. 统计数据频率(频数)
通过numpy库中的nditer()方法可以实现直接遍历多维数组,在遍历时使用字典来统计对应像素值的出现频率。若之前未出现则以0代替。
3. 定义节点
自定义树赫夫曼节点类Huffmannode,其中增加父节点与权值:若parent = 0 表示该节点尚未加入到赫夫曼树中。
4. 选择最小权值节点
由于在自底向上的过程中需要不断找到两个最小权值的节点,故编辑方法select_node来寻找parent = 0 中两个具有最小权值节点。
5. 构建Huffman树
思路:若原数据节点为n,则建树过程中新增的节点为n-1,总计有2*n-1节点。通过已收集的字典数据,每次寻找最低权值的两个节点,合并后并将合并的中间节点也放入huffman数组中。
6. Huffman树的数据编码
思路:由于初始节点位于Huffman数组的0 – n-1位,故仅需遍历前n位,向上寻找其父节点并记录其左右方向(左为0,右为1)。由于是自底向上寻找,故最后还需翻转得到的编码字符串。
最终,返回的coding_dict即表示编码对应的字典。
7. 根据字典编码初始数组
根据得到的编码字典,即可在遍历数组时将对应的数据转为二进制的01字符串:
输出结果如下:(由于二进制字符串较长,故仅输出前100位。)
由图可知,该数组的像素值为255的频率最大,故其表示内容为“11”。相应的,出现频率次数越少的像素值对应的编码字符串就越长。
8. 写入二进制文件
注意点:
-
写入文件时需要将该二进制字符串转为bytes数组,若以ASCII码或其他编码方式直接写入文件中,则每一个字符“0”或“1”都将占据1字节的大小,最终反而会生成以MB为单位的文件。
-
由于文件的最小存储单位是1字节(1byte = 8bit),而得到的二进制字符串并不一定是8的整数倍,故我们需要特判并在二进制字符串后补全0,写入时以8位一起写入。
输出的二进制文件大小如下:比原图像增加了100KB左右,大小约为300KB。
分析原因来源:
-
文件读写:可能是二进制文件写入中浪费了部分的数据空间。
-
压缩算法:由于对图中BGR三通道中0 - 255同时做赫夫曼编码,故三者的数值分配不一定能产生较大的频率差(即频率相近),因而会对压缩效率产生一定的影响。
-
图像文件:可能是该图像存储时已经被压缩过,对该图像再次压缩出现数据膨胀的情况。
9. 二进制文件的读取
在读取二进制文件为输出字符串时,需要提前知道实际二进制文件的字符串长度,对于不同程序而言,此处可以通过共享变量,进程通信,或再次将字典写入二进制文件中等方式实现。但本实验中在同一程序下继续编辑下面内容。(即预知二进制文件读取的字符串的有效长度l)
10. 图像的还原
还原图像时需要将原字典翻转key-value,由二进制字符串映射数字。随后在遍历读取的二进制字符串时根据字典查找对应的数值。
由于读取图像时未读取png文件的第四个透明度通道,故还原后png文件丧失透明度,且大小减少了略多于四分之一。336*3/4 = 252KB > 237 KB
至此即完成了基础的赫夫曼编码图像压缩。
运行结果
对于无损编码压缩测试结果如下(不尽人意啊):中间产出的压缩文件略大于原图像大小。
但是对比左右侧图像,左侧原图,右侧解压后生成的图,还是一模一样滴~
程序代码
import cv2
import numpy as np
from math import inf
import datetime
import os
# 初始化哈夫曼结点
class Huffmannode(object):
def __init__(self):
self.parent = 0
self.left = 0
self.right = 0
self.weight = 0
# 选择最小的结点下标
def select_node(huffman):
# 仅有两个直接返回
if len(huffman) == 2:
return 0, 1
min = semin = inf # 无穷大
least1 = least2 = -1 # 两个最小节点
for i in range(len(huffman)):
if huffman[i].parent == 0:
if min > huffman[i].weight:
semin = min
least2 = least1
min = huffman[i].weight
least1 = i
elif semin > huffman[i].weight:
semin = huffman[i].weight
least2 = i
return least1, least2
# 编码
def huffman_code(origin_dict):
# 给结点赋权重
n = len(origin_dict) # 当前节点数
m = 2 * n - 1 # 建树完毕后的节点数
huffman = []
for i in origin_dict:
temp_huffmannode = Huffmannode()
temp_huffmannode.weight = origin_dict[i]
huffman.append(temp_huffmannode)
# 构建Huffman树,选择两个最小权值的结点合并
for i in range(n, m):
least1, least2 = select_node(huffman)
temp_huffmannode = Huffmannode() # 初始化合并的中间节点
temp_huffmannode.weight = huffman[least1].weight + huffman[least2].weight # 权值合并
temp_huffmannode.right = least1 # 略小的放右边
temp_huffmannode.left = least2 # 略大的放左边
huffman[least1].parent = huffman[least2].parent = i
huffman.append(temp_huffmannode)
# 0,1编码
codeing_dict = dict.fromkeys(origin_dict, None)
for i in range(0, n):
codestr = ''
k = i
parent = huffman[i].parent
while parent != 0:
if huffman[parent].left == k:
codestr += '0'
k = parent
parent = huffman[parent].parent
else:
codestr += '1'
k = parent
parent = huffman[parent].parent
codeing_dict[list(origin_dict.keys())[i]] = list(reversed(codestr))
for k in codeing_dict.items():
codeing_dict[k[0]] = ''.join(k[1])
return codeing_dict
if __name__ == '__main__':
basePath = "D:\\PythonProject\\test_image\\"
filename = "green"
filetype = "png"
filepath = basePath + filename + "." + filetype
binary_filename = filename + "_binary.bin"
#读取图像
img = cv2.imread(basePath + filename + "." + filetype,1)
height = len(img)
width = len(img[0])
print("width: "+ str(width) +" height: " + str(height))
start = datetime.datetime.now() # 记录开始时间
# print(img)
# cv2.imshow("img",img)
# key = cv2.waitKey(0) # 等待按键命令
# 创建字典计算频率
dic = {}
for x in np.nditer(img):
# x 本身是numpy.narray 需要做一次类型转化
# get方法,如果有键返回该键对应的值,如果没键,可以设置返回值
dic[x+0] = dic.get(x+0, 0) + 1
# 对256位进行霍夫曼编码
code_dict = huffman_code(dic)
print("Huffman编码计算完成")
# print(code_dict)
# 得到压缩后的二进制字符串
compress_str = ""
for x in np.nditer(img):
compress_str += code_dict[x+0]
# print("得到的二进制字符串(0 - 99位)")
# print(compress_str[0:100])
# 将二进制字符串写入对应的二进制文件中
print("即将写入文件路径:"+basePath + binary_filename)
# with open(basePath + "binary.bin", "wb") as fp:
# fp.write(bytes_content)
# 首先需要保存原字符串长度
l = len(compress_str)
s = compress_str
# 首先将s变成的8的整数倍长的字符串 可以直接在后面填0
if r := l % 8:
s += "0" * (8 - r)
# 将s转成bytes
b = bytes([int(s[i:i + 8], 2) for i in range(0, len(s), 8)])
print("写入二进制文件")
# bytes 可以直接写入以二进制模式打开的文件
with open(basePath + binary_filename, "wb") as f:
f.write(b)
# 为了能够从文件中得到完全一致的二进制字符串,需保留原始长度l
# 从文件中还原
with open(basePath + binary_filename, "rb") as f:
data = f.read()
binstr = "".join(["{:08b}".format(c) for c in data])
binstr = binstr[:l] # 此时的binstr为原来的二进制字符串
if binstr == compress_str:
print("解压字符串相同")
else:
print("解压字符串不相同")
# 逆转字典,用于解压
reverse_dict = dict(zip(code_dict.values(),code_dict.keys()))
# print(reverse_dict)
# 遍历二进制字符串并根据字典翻译成像素值
num_str = ''
num_arr = []
num = 0
for i in range(0,len(binstr)):
num_str += binstr[i]
num = reverse_dict.get(num_str, -1)
if num != -1:
num_arr.append(num)
num_str = ''
# print("压缩数为:")
# print(num_arr)
num_arr = np.array(num_arr).reshape(height, width, 3)
# print(num_arr)
end = datetime.datetime.now()
cv2.imwrite(basePath + filename + "_1." + filetype, num_arr)
# 求压缩比
origin_size = os.path.getsize(filepath)
compress_size = os.path.getsize(basePath + binary_filename)
output_size = os.path.getsize(basePath + filename + "_1." + filetype)
compress_ratio = origin_size/compress_size
# 求均方误差
diff = 0
for i in range(0, height):
for j in range(0, width):
for k in range(0, 3):
diff += (img[i][j][k] - num_arr[i][j][k] )**2
diff /= (height*width*3)
print("图像名称: " + str(filename +"."+ filetype))
print("原图像大小: " + str(origin_size) + " 字节")
print("压缩文件大小: " + str(compress_size) + " 字节")
print("生成图像大小: " + str(output_size) + " 字节")
print("压缩比: "+ str(compress_ratio))
print("均方误差: " + str(diff))
print("程序执行时间: " + str((end-start).seconds) +" 秒")