目录
前言
一、压缩
1.图片转像素值
2.十进制转二进制
3.RLE编码
4.主程序:得到RLE编码
5.主程序:显示压缩后节省的空间
二、解压
1.二进制转十进制
2.像素转图片
RLE行程编码属于一种压缩方式。这种压缩是一种无损压缩,也就是说数据被压缩之后不会丢失,图片也就不会失真,解压后图片的质量和压缩前是一样的。
根据RLE压缩原理,他一般适用于图片压缩,通常能够节省20-30%的储存空间,而对于由大色块砌成的图片他省下的空间就更多了。其实RLE也可以用于压缩字符串,但是效果没有霍夫曼编码理想,因为RLE对于很少连续出现重复字母的句子无能为力,只有将文章中的字母按字母顺序排列好才行,最后解压时还要将字母按原来顺序重新排列,没有霍夫曼编码方便。
这个代码分两部分,分别是压缩和解压,对象是图片。
压缩的主要思路是:加载出图片的所有像素的RGB颜色,将其转为二进制后开始RLE编码压缩,最后把编码结果保存到json文件中。我在这里使用的照片是bcm.jpg,大家可以按照自己的喜好加载不同的图片,并把要加载图片和代码保存在一个文件夹中。
from PIL import Image
import json,re,time
#第一部分:解压
pic="bcm.jpg"
img=Image.open(pic)
#从图片获取所有像素RGB值
def img_pixel(img):
lst=[]
width,height=img.size
for j in range(height):
for i in range(width):
lst.append(img.getpixel((i,j)))
return lst
这里的核心方法是img.getpixel()得到图片中的所有像素RGB 值,(i, j)代表的是像素块的坐标。我们这里通过嵌套循环将像素RGB值一行一行的获取并按顺序添加到列表lst中。函数最后返回lst。
通过二层嵌套循环得到坐标的方法我们后面还会用到,所以这里介绍一下。假设图片image3像素高,2像素宽,我们用x,y=image.size 取出他的宽和高。接下来我想取出他每一个坐标的像素值,那我就要先获取他的坐标:
for j in range(y):
for i in range(x):
print((i,j)
#返回结果:
(0,0)
(1,0)
(0,1)
(1,1)
(0,2)
(1,2)
这就是他的所有坐标。因为我们的x,y都是以像素为单位的,所以这些坐标就是图片image的所有像素色块位置。
#十进制转二进制
def bytes(num):
binary1=''
result=''
cnt=1
if num<0 or num>int(num):
print("error:not an integer")
while True:
if num%2==0:
binary1+="0"
else:
binary1+="1"
if num//2==0:
break
num//=2
cnt+=1
return int(binary1[::-1])#切片反转字符串
想理解这段代码要先学习如何把一个十进制数字转化成二进制,知道原理后就自然而然的看懂了。这段代码比较取巧,用字符串的计算性质得到二进制字符串,最后返回的时候把他整化。
#RLE压缩代码
def encode_while(lst):
encoded_items=[]
i=0
while i
以上一段是RLE压缩——统计像素颜色值连续重复的次数。让我们看看RLE到底是怎么运作的。
举个简单的例子:这里有一个一位图列表表示为:one=[1,1,1,1,0,0,0,0,0,1,1,1,1,1,1],每个元素为一像素。他可以写成:(4,1), (5,0), (6,1),表示有分别有4个1,5个0,6个1连续出现。我们把4,5,6变成二进制格式然后算一下编码后省下了多少空间...省下三比特位。
虽然看起来很少,但是一旦位图变成了24,他省下的空间就变成了天文数字。因为位图越多,就越能够在省相同数量的像素时省下更多空间。
我们用上面简单的例子分析一下encode_while()函数:
j=i=0, one[0]=1
(one[0]=one[0+1]) = True so:
cnt<- 2(1+1), j<- 1(0+1)
(one[1]=one[1+1]) = True so:
cnt<- 3(2+1), j<- 2(1+1)
............
(one[3]=one[4]) = False so:
break
now cnt=4
bytes(cnt)=100
so encode_items[0]<-(100,1)
i<-j+1, so i=3+1=4。
check: one[4]=0, one[3]=1, 0!=1, the method is right!
......so...on.....
再用文字表述一下上面的伪代码:如果列表one中两个元素不相等就结束内层循环,并给encoded_items列表中添加元素(100,1),这里用的全是二进制。然后令i=j+1,那此时的lst[j]代表的是元素0。接下来重复这个嵌套while循环直到判断到了列表的最后一个元素。
那判断条件j
另外为什么我的函数名时encode_while()而不直接叫encode()呢?因为其实for循环也能解决问题,只是代码又臭又长,bug让我改的怀疑人生,结果换成条件循环十几分钟就搞定了。。。
#主程序
#这部分计算:RGB十进制->RGB二进制->二进制的RLE编码
lst_bin=[]
lst_bin2=[]
for j in img_pixel(img):
lst_color = []
for i in j:
lst_color.append(bytes(i))
lst_bin.append(tuple(lst_color))
lst_bin2=encode_while(lst_bin) #这就是RLE编码的列表
这部分就是调用之前编写的函数。for循环遍历img_pixel(img)——一个由img全体像素RGB值组成的列表。因为此时的RGB都是以10进制记录的,所以我们需要使用内层循环将RGB元组中的三个颜色值取出来,给他二进制化后再重新添加到新元组lst_color中。每次内层循环完都把一个lst_color元组添加到lst_bin中。最后用encode_while(lst_bin)将他进行压缩处理。
#这部分计算:压缩前后占用空间对比,以及压缩后省了多少空间
string=""
string2=""
#数据类型转换,让计算列表长度更加容易
for a in lst_bin:
for b in a:
string=string+str(b)
for m in lst_bin2:
for n in m:
string2=string2+str(n)
#比特位和字节的转化
def cvt(binary):
bytes_=len(binary)//8
bits=len(binary)%8
return f"{bytes_} 字节 {bits} 比特位"
op=lambda d1,d:round(len(d1)/(len(d1)+len(d)),1)*100 #计算节省了多少空间
print( #输出节省的空间
f"压缩前占用 {cvt(string)}, 压缩后占用\
{cvt(string2)}, 节省了 {op(string2,string)}% 空间"
)
with open("bcm_bin_code.json","w") as f:#将编码结果保存
json.dump(lst_bin2,f)
以上计算压缩后省下的空间。其中有几行看起来好像很复杂,其实就是想省行数,把他分解了来看没有什么难度。string和string2分别代表了压缩前和压缩后的二进制字符串,后面由cvt()函数获取这个字符串的长度。
接下来是解码——将二进制RGB值转化为十进制并通过putpixel() 将按顺序排列的十进制RGB像素转化为原图。
#第二部分:解压
#思路:binary_code->decimal_code->original_picture
#二进制转十进制
def to_dec(binary):
decimal=0
R=str(binary)
for i in range(len(R)):
decimal=decimal+int(R[i])*(2**(len(R)-1-i))
return decimal
这里的思路也是:整数-字符串-整数。看起来稍微会有些复杂,但是只要学过如何从二进制转十进制,这段代码就不难理解了,这里不展开介绍
# 主程序
#像素RGB二进制到十进制
lst_dec=[]
for q in lst_bin:
temp=[]
for p in q:
temp.append(to_dec(p))
lst_dec.append(tuple(temp))
#通过图片的全体像素RGB值得到图片
x,y=img.size
img_=Image.new("RGB",(x,y))
for o in range(0,y):
for t in range(0,x):
img_.putpixel((t,o),lst_dec[o*x+t])
img_.show()
至此,整个压缩包括解压的程序已经展现完毕,这一段的解压的代码将RLE 二进制的列表转化为十进制RGB,并用接收两个值(像素要插入的坐标和像素数值)的putpixel()方法结合嵌套循环将像素数值还原成照片。其中那个嵌套for循环可能会令人感到些许迷惑。他其实就是把列表解成数行RGB条,然后每一个内层循环都将一行RGB条中的每一个转换成对应的一行实实在在的图片像素色块。其中o,t是循环变量,一个是循环原图宽度次也就是外层循环,一个是长度次即内层循环。
让我们举个例子(因为虽然用抽象的术语是把事情解释清楚了,但是不直观,很难理解),这里有一张长2像素,宽3像素的图片被getpixel()方法按顺序分解成了一张RGB数值列表,可以理解成是他这个图片在横向被剪成了两个像素宽的纸条然后按顺序拼接成了一条长方形列表,宽度为一像素。
那显而易见的,列表里有2*3也就是6个像素。假设这个列表为:[(139,69,19), (160,82,45),(139,69,19), (160,82,45), (139,69,19), (160,82,45)] (对应的人话就是:深棕,褐色,深棕,褐色...)。为了准确的还原图片,我们需要把它这么排列:
(139,69,19),(160,82,45),(139,69,19)
(160,82,45),(139,69,19),(160,82,45)
这不就还原成一个宽3像素,高2像素的图片了么?我们刚刚做的是把这个“长方形列表”一行一行的粘回图片。现在我们只需要确定嵌套循环次数,然后每次循环用putpixel()方法把一个RGB值转化成对应颜色,就得到原图了。
那么我们开始策划:循环多少次putpixel()方法才能把这六个数值还原成六个色块呢?6次嘛!我们需要使用嵌套循环,外层循环执行2次代表要把列表分为两行(循环变量为o),每一次外层循环时都执行3次内层循环(循环变量是t)。
循环多少次这个问题我们解决了,那我们现在还需要给putpixel()方法两个参数,让人家能找到正确的位置把正确的色块给我们贴上去,贴的位置毫无疑问就是(t,o)。轮到色块了。由于像素们都按顺序排在列表里,所以这个问题转化成了如何在正确的位置找到列表正确的索引。我们看一下如何按顺序在列表中抓取RGB像素值。我们把色块位置转化成列表索引:
0*3+0->0,0*3+1->1,0*3+2->2
1*3+0->3,1*3+1->4,1*3+2->5。
推广一下,取出列表索引是:o*x+t,其中o是行数,x是图片宽,t是列数
from PIL import Image
import json
pic=r"pictures/bcm.jpeg"#这里大家可以写入想要压缩的图片
img=Image.open(pic)
w,h=img.size
def img_pixel(img):
lst=[]
for j in range(h):
for i in range(w):
lst.append(img.getpixel((i,j)))
return lst
def to_bin(num):
bin1=''
R=''
cnt=1
if num<0 or num>int(num):
print("error:not an integer")
while True:
if num%2==0:
bin1+='0'
elif num//2==0:
break
else:
bin1+='1'
num//=2
cnt+=1
bin1=bin1[::-1]
return int(bin1)
def encode(lst):
encoded_lst=[]
i=0
while i
该段完整代码与前面的演示代码在有些变量名上有出入