在做单片机嵌入式开发,程序用的图片文件一般不是普通的jpg、png、gif等常规图片文件,而是像.c、.h这样种形式的文件。为了还原源代码中的这种C语言形式的数组图像,特意写了一个python程序,用来显示和还原保存数组图片文件。
注意:C语言形式的数组图片数据存储类似这样:
不同软件在处理从图片转换成C数组的时候,存储数据的格式会有不同,要查看具体的C或H头文件。几乎所有的C数组数据只有2种:一种是未经压缩的图片位图数据,另一种是压缩后的数据。
首先处理未压缩的位图数据:
常规的图片转C数组头文件软件如image2lcd,转换出的图片都是位图形式的数组。
位图数据,常见的格式类型如bmp,就是图片中的每一个像素对应于数组中的一个或两个、3个字节。这里要区分一下图片的存储大小。
常见的几种图片数据存储类型:1位(bit)黑白图像、16位(bit)真彩色图像、32位(bit)彩色图像。
要解码C数组结构的图片数据,需要了解图像数据的存储规则。放大图片,会发现图片是由一个一个彩色小方块构成的,这个小方块就是像素。那么这个小方块怎么用数字来表示呢?
一个8位的字节,可以表示的数据大小为0~255, 像素类似一个灯泡,需要亮的时候就打开开关,需要熄灭的时候就关闭开关,我们用用数字“1”表示打开,数字“0”表示关闭。为了方便存储,图片数据是以字节为单位的,一个字节有8位,每个位每个位都代表着一个灯泡开关的状态,开关有“开”和“关”两个状态,所以一个位也只有两种状态,“1”和“0”状态。
前面说了,“1”表示开关闭合,点亮灯泡,“0表示”开关打开,熄灭灯泡,那么一个8位的字节就可以控制8盏灯泡。8盏灯泡全部熄灭就用‘000000’表示,8盏灯泡全部点亮就用‘11111111’表示。换成十进制数就是0和255,要控制哪盏灯的点亮和熄灭,就相应的在哪位上填“1”或“0”。
如果不需要色彩,只用黑白来表示像素,那就比较简单,需要显示像素的地方直接用“1”表示,不需要显示的地方直接用“0”表示。最终图像C数组的形式类似0,0,0,0,0,1,1,1,1,1,1这样一大串含有0和1形式的二进制数据。
图片数据存储的最小单位是字节,所以直接在C数组里显示的不是0,0,0,1,1,1这样的二进制数字,那样就太浪费存储空间了,实际存储的是0xff、0x00这样的十六进制字节数据。
如果存储的是0xff,它表示的是8个像素点都打开,如果用“1”表示白色,“0”表示黑色,那么0xff这8个像素点就发白光,显示的就是白色。把一副图像所有的像素都用黑白来表示,1位(bit)表示一个像素,这样就获得了黑白图像的数据。这就是1bit黑白图像数据。
在用python等语言显示图像数据时,读取图像的数据不是按照一位一位来读取的,而是按照字节来读取的,一个像素是用一个字节来显示的。这样原本是显示8个像素的,现在只能显示1个像素。所以在读取1bit黑白图像数据时,需要将每一位扩充成8字节数据,这样就能正常显示了。
解码就很简单了,将字节中每位“1”或“0”换成“255”或“0”的字节表示就可以了,代码如下:
def show_1bits_img(data):
RGB=[]
for i in data:
for j in range(7,-1,-1):
b=eval(i)>>j
if b&1 == 1:
RGB.append(0)
elif b&0==0:
RGB.append(255)
return RGB
那么彩色像素怎么表示呢?
黑白图像用一位就可表示一个像素,彩色图像就不行了,因为含有色彩信息,一位明显不够,按照数据存储的最小单位来,那么一个彩色像素用一个字节来表示是合情合理的。
色彩是由红(R)、绿(G)、蓝(B)三种基本颜色合成的,那么一个彩色像素就可以用一个(R,G,B)数组来表示。每一个分量都用一个字节表示,那么一个彩色像素就需要3个字节24位来表示,可表示 16,777,216种颜色。彩色有16位和、24位、32位等的表示。
实际上,平常用不到那么多种颜色,那么可以踢掉不常用的颜色,就能减少图像存储的数据。于是就有了16位真彩色图像存储格式。
16位彩色图像存储有565和555两种方案,我们以565方案说明。565也就是红色(R)用5位来表示,绿色(G)用6位来表示,蓝色(B)用5位来表示,刚好是两个字节的大小。555就是RGB每个分量都用5位来表示。
对以565方案存储的数据解码,还是类似的方法,把5位数据扩充为8位,左移3位即可,6位数据左移2位即可。这里要注意,一个像素是用2个字节表示的,这2个字节在数据存储中有顺序的变化,一般采用小端表示,即低位在前,高位在后。16位像素数据0xffd8,在数组中存储为0xd8,0xff这样两个字节,解码的时候需要重新合成0xffd8,这样才能表示一个像素。代码如下:
def show_16bits_img(data,w,h):
RGB=[]
data1=[eval(data[2*i+1])<<8|eval(data[2*i]) for i in range(w*h)]
for i in data1:
R=((i&0xf800)>>11)<<3
RGB.append(R)
G=((i&0x7e0)>>5)<<2
RGB.append(G)
B=(i&0x1f)<<3
RGB.append(B)
return RGB
24位真彩色图像数据解码就很简单了,每个像素分量是用一个字节表示的,按照RGB的顺序依次读取3个字节,并表示成一个像素,然后显示即可。代码如下:
def show_24bitsBGR_img(data):
RGB=[]
for i in range(len(data)//3):
B=data[3*i]
G=data[3*i+1]
R=data[3*i+2]
RGB.append(eval(R))
RGB.append(eval(G))
RGB.append(eval(B))
return RGB
以上针对的是位图数据的数组图像,没有对像素值进行压缩处理。接下来我们直接处理压缩过的图像数据。
常见的图片格式jpg、png、gif等格式文件其实就是一种压缩过的像素数据。单片机等嵌入式开发中、二进制数据的图片直接用字符串表示,于是得到了c数组或h数组,这样的C数组图像就是原来的jpg、png等格式的二进制字符串表示。
解码很方便,将字符串图片数组还原成二进制数据,然后调用jpg、png等图片的解码库解码就得到了解码后的数据,然后用matplotlib显示就可。
这里使用openCV来解码,调用cv2.imdecode(imageArray, cv2.IMREAD_COLOR)函数来得到解码后的图像数据。
注意:用解码用位图表示的C数组图像时,需要获得图像的宽高,这些数据通常在数组的第一行,也可能是在/*...*/这样的注释里,源代码案例中宽高等数据都在注释里,具体如下:
第一个字节0x00表示扫描方式:自左向右;
第二个自己0x18表示图像存储的数据位数:0x18换成十进制是24位,也就是图像数据是24位存储的;
第三、四个字节两个字节表示图像的宽:0x28,0x00,表示0x28,换成十进制是28,宽就是28;
第五、六个字节两个字节表示图像的高:0x28,0x00,表示0x28,换成十进制是28,高就是28;
第七个字节表示图像是否是16位真彩色是否是565格式:0x00不是,16位图像下有效。
第八个字节是描述RGB颜色分量的排列顺序。
在源代码里,我已经添加了匹配注释里宽高的代码,在显示位图数据里不需要手动填入,另外制作C数组或H数组时,不要把图像宽高等数据放到数组中,这样反倒不能正确匹配到,显示数据的时候也麻烦。
def match_imgWH(pattern,one):
match=re.findall(pattern,str(one))
print("匹配的行:",match)
mlist=match[0].split(",")
gray_code=eval(mlist[1])
print("匹配的列表:",mlist)
w=eval(mlist[3])+eval(mlist[2])
h=eval(mlist[5])+eval(mlist[4])
print("图像宽和高:",w,h)
return gray_code,w,h
源代码里包含了完整的程序,把需要复显的C数组图片文件直接放到images文件夹内,保存的png格式图片在savedimages。以下是测试用例显示结果:
完整源代码如下所示,代码可识别.c和.h后缀的文件:
import os
import numpy as np
import cv2
import re
#import random
import matplotlib.pyplot as plt
#from PIL import Image
%matplotlib inline
image_list=[]
data_list=[]
jpeg_list=[]
jpeg_data=[]
path="./images"
savePath=path+"/savedImages"
def show_1bits_img(data):
RGB=[]
for i in data:
for j in range(7,-1,-1):
b=eval(i)>>j
if b&1 == 1:
RGB.append(0)
elif b&0==0:
RGB.append(255)
return RGB
def show_16bits_img(data,w,h):
RGB=[]
data1=[eval(data[2*i+1])<<8|eval(data[2*i]) for i in range(w*h)]
for i in data1:
R=((i&0xf800)>>11)<<3
RGB.append(R)
G=((i&0x7e0)>>5)<<2
RGB.append(G)
B=(i&0x1f)<<3
RGB.append(B)
return RGB
def show_24bitsBGR_img(data):
RGB=[]
for i in range(len(data)//3):
B=data[3*i]
G=data[3*i+1]
R=data[3*i+2]
RGB.append(eval(R))
RGB.append(eval(G))
RGB.append(eval(B))
return RGB
def match_imgWH(pattern,one):
match=re.findall(pattern,str(one))
print("匹配的行:",match)
mlist=match[0].split(",")
gray_code=eval(mlist[1])
print("匹配的列表:",mlist)
w=eval(mlist[3])+eval(mlist[2])
h=eval(mlist[5])+eval(mlist[4])
print("图像宽和高:",w,h)
return gray_code,w,h
def match_jpegIMG(pattern,jpegArray):
match=re.findall(pattern,jpegArray)
return match
def get_image_list(path):
image_list=[]
jpeg_list=[]
check_folder_exists(path)
for filename in os.listdir(path):
if os.path.splitext(filename)[1] in ['.c','.C']:
image_list.append(os.path.join(path,filename))
elif os.path.splitext(filename)[1] in ['.h','.H']:
jpeg_list.append(os.path.join(path,filename))
print("C语言图片数组列表:",image_list)
print("Hjpeg图片数组列表:",jpeg_list)
return image_list,jpeg_list
def read_image_file(filename):
img=[]
with open(filename,'r') as f:
file=f.readlines()
oneLine=file[:1]
for line in file[1:]:
for li in line.split(','):
if li.strip()!="" and li.strip()!="};":
img.append(li.strip())
print("图像总像素:",len(img))
return oneLine,img
def read_jpeg_file(filename):
with open(filename,'r') as f:
file=f.read().replace('\n','')
return file
#根据图像色彩编码格式选则相应的显示解码函数
def convent_image_array(img,arg):
RGB=[]
gray_code=arg[0]
w=arg[1]
h=arg[2]
if gray_code==1:
RGB=show_1bits_img(img)
data=np.array(RGB,dtype=np.uint8).reshape(h,w)
elif gray_code==16:
RGB=show_16bits_img(img,w,h)
data=np.array(RGB,dtype=np.uint8).reshape(h,w,3)
elif gray_code==24:
RGB=show_24bitsBGR_img(img)
data=np.array(RGB,dtype=np.uint8).reshape(h,w,3)
return data
def plt_image_show(data,num,dataLea,imgName):
plt.subplots_adjust(left=0.0,bottom=0.0,top=1,right=1,hspace=0.7,wspace=0.25)
plt.subplot(dataLea//2,2,num)
imgName=os.path.basename(imgName)
plt.title(imgName)
plt.axis('off')
plt.imshow(data,cmap="gray")
check_folder_exists(savePath)
plt.imsave(savePath+"/{}.png".format(imgName),data,cmap='gray',format="png")
def check_folder_exists(folder_path):
if not os.path.exists(folder_path):
os.makedirs(folder_path)
def imshow_C_list(data_list,image_list):
dataLea=len(data_list)
for i in range(dataLea):
plt_image_show(data_list[i],i+1,dataLea,image_list[i])
plt.show()
def imshow_H_list(data_list,image_list):
dataLea=len(data_list)
for i in range(dataLea):
plt_image_show(data_list[i],i+1,dataLea,image_list[i])
plt.show()
image_list,jpeg_list=get_image_list(path)
for imglist in image_list:
pattern="/\*.*?\*/"
oneLine,img=read_image_file(imglist)
gray_code,w,h=match_imgWH(pattern,oneLine)
arg=[gray_code,w,h,imglist]
print(arg)
data=convent_image_array(img,arg)
data_list.append(data)
for jpeglist in jpeg_list:
pattern=r"{(.*)}"
jpegfile=read_jpeg_file(jpeglist)
match=match_jpegIMG(pattern,jpegfile)
jpeg_img=[eval(i) for i in match]
jpegimg=np.array(jpeg_img,dtype="uint8")
image = cv2.imdecode(jpegimg, cv2.IMREAD_COLOR)
print("图像(高,宽,位深)为:")
print(np.array(image).shape)
jpeg_data.append(image)
imshow_C_list(data_list,image_list)
imshow_H_list(jpeg_data,jpeg_list)
#cv2.imshow('URL2Image',image)
#cv2.waitKey()
#plt.imshow(image)
#plt.imshow(res,cmap="gray")
#kk=Image.fromarray(res)
#kk.show()