学习Python基础语法已经有一段时间了,但是对于各种库的使用还是一知半解的,所以找了几个有意思的小项目练手,这是第一个项目:图片转换成字符画。
效果如下
大概思路如下:
根据像素值的不同在对应位置将像素值映射成合适的字符,就是像素值低的地方映射点阵占位少的字符如’.’,在像素值高的地方映射点阵占位多的字符如’$’。
首先,准备好图片转换后的字符:
charset = ['.', '!', '~', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+']
然后,根据字符再点阵中占位多少进行排序(当然,你也可以手工指定顺序),那如何知道字符在点阵字体中占点的多少呢?我们需要下载一些字库文件来解析,之后就可以知道了,这里我已经上传了一些字库文件到CSDN,可以先下载下来看一下。
需要介绍一下的是,点阵字体就是用点阵来显示字符,和TrueType有点不一样,有点类似于位图和矢量图的区别。点阵越大,字体越清晰,比如我们代码中使用的ASC16点阵文件,使用16×8(16行8列)个点来表示一个ASCII字符,一个点在文件中占1bit,读入计算机内存之后也是占1bit,ASCII码有256个,所以文件大小为16×8×256÷8 个byte= 4KB。
下面的代码读入文件并返回每个字符的点阵中为1的点有多少
def numsofone_in_charbytes(text):
# 先获取字符的ascii码值
offset = ord(text)
# 然后打开字库文件找到该字符的位置
with open("./ASC16", "rb") as ASC16:
location = offset*16
ASC16.seek(location)
retbytes = ASC16.read(16)
# 已经获取到该字符的点阵表示字节retbytes,现在要计算这16字节中有多少个1
count = 0
for i in range(len(retbytes)):
# 对于retbytes中的每一个字节
for j in range(len(KEYS)):
if KEYS[j] & retbytes[i]:
count += 1
return count
根据上面的函数对charset中的每个字符进行排序,得到charsetnew
for s in range(len(charset)):
orders[s] = numsofone_in_charbytes(charset[s])
print(orders)
# 依据这个对点进行排序
# numpy.argsort()可以给出排序后各元素在原来数组中的索引
s = numpy.argsort(orders)
print(s)
# 依据上面的索引重新对charset排序
charsetnew = []
for i in range(len(charset)):
charsetnew.append(charset[s[i]])
charsetnew列表中的字符就是按照每个字符点阵的填充程度从小到大排序的了,这里有一些字符的点阵中1的数目是重复的,不过没关系,影响不大。
接下来我们就要对图片进行处理了。这里图片转字符的思路是这样的,将图片看成16×8的像素块组成的,每一个像素块取平均像素值后映射成相应的字符。
首先读入图片
srcimg = matplotlib.pyplot.imread("F:/temp/testKeji.jpg")
然后对图片进行修剪,因为图片的宽高可能不是8和16的倍数,去除右方和下方多余的边。
def trim_pic(img):
shape = numpy.shape(img)
# 如果图片本身的长宽不满足要求就直接返回空
if shape[0] < 16 or shape[1] < 8:
return None
height = shape[0]//16
width = shape[1]//8
print(height)
print(width)
trimed_pic = img[:height*16, :width*8]
return trimed_pic
图片经过修建过后,算出每个16×8(高×宽)的像素块的平均像素值,得到一个矩阵,这里其实跟卷积神经网络里面的池化操作非常像。所以我直接给这个函数取名字叫池化了
def pool16_8(img):
# shape,第一个元素是矩阵行数,所以是图片的高
shape = numpy.shape(img)
row = shape[0] // 16
cow = shape[1] // 8
avgpixel = numpy.zeros((row,cow), dtype=float)
for i in range(row):
for j in range(cow):
# 此处计算各个像素块的平均值
t = 0.0
for t1 in range(16):
for t2 in range(8):
t += img[t1+i*16, t2+j*8]
avgpixel[i, j] = t/(16*8)
return avgpixel
上面的函数返回一个平均像素值矩阵之后,就可以根据这个矩阵进行映射了。下面这个函数就返回一个字符矩阵
def cvt2char(avgpixel, charset):
# avgpixel是计算后的像素平均值,charset是用于制作字符画的字符集
chars = len(charset)
race = 255.0/chars
shape = numpy.shape(avgpixel)
retcharmatrix = []
rowchar = []
for i in range(shape[0]):
for j in range(shape[1]):
# 获取像素的等级
s = avgpixel[i, j] // race
# 得到对应的字符
rowchar.append(charset[int(s)])
retcharmatrix.append(rowchar[:])
rowchar.clear()
return retcharmatrix
至此,所有的步骤都完成了。可以用一张图片测试一下,尽量选大一点的卡通图片,效果比较好,因为线条比较明显。用我最喜欢的飞天小女警来测试一下
# 读入一张图片
srcimg = matplotlib.pyplot.imread("F:/temp/PowerPuffGirls.jpg")
# 转换成灰度图
grayimg = rgb2gray(srcimg)
# 先裁剪一下
trimedimg = trim_pic(grayimg)
# 再进行池化平均
pooledimg = pool16_8(trimedimg)
# 再得到转换后的字符矩阵
charpic = cvt2char(pooledimg, charsetnew)
原图:
字符画:
有点不明显?摘掉你的眼镜试一下。
我最喜欢的花花:
效果图还不错:
当然,这里只转换成了二维的字符列表,要将这列表再转换成图片保存起来,那就是可以继续研究的地方了。
这代码里面还可以改进的地方就是可以使用边缘检测算法先对图片进行处理,然后再转换成字符画,这样对于自然图片就也会有很好的效果。
项目的完整代码我已经上传GitHub和CSDN。包括图片和完整的运行代码