Image模块应用实例之图像字符画
共 7163字,需浏览 15分钟
·
2022-07-07 17:21
说在前面
“图像字符画”是浙教版《信息技术必修一数据与计算》第三章的实践与体验项目。教材简要说明了字符画的算法原理,并提供了完整源代码。
笔者输入并运行了教材提供的代码,发现程序能正常运行。但是笔者对代码中表达式int(gray/(255/(count-1)))的含义表示疑惑,认为这个表达式的意义不明确,恐怕是错误的。
因为教材是经过专家多次审阅的,怀疑教材出错需要莫大勇气。更大的可能性是笔者自身水平不够高,没有看懂源代码。
本文包含笔者对“图像字符画”项目实践的算法分析和授课思路,有不当之处,敬请各位老师批评指正。
算法分析:
程序先构建长度为count的字符串列表serarr,然后打开图片"boy.jpg",并将图片调整到适当大小。再以追加模式打开文本文件"boy.txt",然后调用自定义函数toText(),将图像中的像素转换成字符,返回字符串asd,并将asd写入到文本文件中。
自定义函数toText()的基本思路是先构建一个空字符串asd,然后逐行遍历图像的各像素点,提取像素点颜色值后,计算其对应灰度值(取值范围[0,255]),然后将灰度值转换成列表serarr的下标,得到对应的字符,将其拼接到asd中。每处理完一行后,在asd中拼接一个'\r\n',表示回车换行(window系统),最后返回asd。
提出疑点:
表达式int(gray/(255/(count-1)))的含义是什么?
从自定义函数toText()的功能来看,该表达式应该是用来计算灰度值gray对应的字符在列表serarr中的下标。
因为gray的取值范围是[0,255],而下标的范围是[0,count-1],则相当于将256个数平均分成count段,那缩小的比率应该是256/count,而不是255/(count-1)。
所以,我认为教材源代码中表达式int(gray/(255/(count-1)))是错误的,应该改成int(gray/(256/count))。
另外,教材中列表serarr的最后一个元素似乎有印刷错误,这是一个空格字符串,应该写成' '才对。教材似乎印刷成了'',变成空字符串了——当然更大的可能是视觉差异,被眼睛骗了。
附注:运行教材源代码程序,获得图像如下所示:
把asd = asd + serarr[int(gray/(255/(count-1)))]改成asd = asd + serarr[int(gray/(256/count))]后,运行程序获得如下图像:
附完整源代码如下:
from PIL import Image
def to_text(img):
asd =''
for h in range(0, img.height): # 垂直方向
for w in range(0, img.width): # 水平方向
r,g,b =img.getpixel((w,h))
gray = int(r* 0.299+g* 0.587+b* 0.114)
asd = asd + serarr[int(gray/(255/(count-1)))]
asd = asd + '\r\n'
return asd
serarr = ['@','#','$','%','&','?','*','o','/','{','[','(','|','!','^','~','-','_',':',';',',','.','`',' ']
count = len(serarr)
image = Image.open("boy.jpg")
image = image.resize((int(image.width*0.45), int(image.height*0.25)))
tmp = open('boy.txt','a')
tmp.write(to_text(image))
tmp.close()
授课思路:
本“实践与体验”课的重点有二,一是理解图像字符画的原理,二是学会把字符串写入到文本文件中。
我们可以把课堂分成两个阶段,分别来完成任务。
首先是理解图像字符画的原理。
要理解字符画的原理,首先要搞清楚图片的模式:彩色图片,灰度图片和黑白图片。
彩色图像通常使用RGB色彩模式,图像中的每个像素都分成R、G、B三个基色分量(通道),利用这3个通道的变化和相互叠加来表现各种颜色。灰度图像是每个像素只有灰度值的图像,它只有一个通道,可以显示为从暗黑到亮白的灰度。黑白图像也叫二值图像,它相当于只取灰度图像中0和255两种值,分别代表纯黑和纯白。
我们可以使用convert()方法来获取不同模式的图像,例如下列代码能够将彩色图像boy_RGB.jpg转换成灰度图像boy_L.jpg和黑白图像boy_1.jpg(无抖动效果),效果图如图2和图3所示。
#【示例程序1】
from PIL import Image
img = Image.open('boy_RGB.jpg')
img2 = img.convert("L") # 转换成“L”模式灰度图像
img2.save("boy_L.jpg")
img3 = img.convert("1", dither=0) #参数dither=0表示无抖动效果
img3.save("boy_1.jpg")
除了直接使用convert()方法将彩色图像转换成黑白图像,我们也可以利用图像模式转换原理,设置自定义函rgb_bw()来实现转换功能。
首先根据从“RGB”转换为“L”模式的公式:L = R*0.299 + G*0. 587 + B*0.114,计算出每个像素点的灰度值gray,再判断gray与阈值的关系,若gray小于阈值,设置为黑色,否则设置为白色。阈值通常取值为128,也可以根据需要设置不同的阈值。
#【示例程序2】
from PIL import Image
# 将RGB彩色图像转换为二值黑白图像
def rgb_bw(img):
for y in range(0, img.height): # 垂直方向
for x in range(0, img.width): # 水平方向
r, g, b = img.getpixel((x, y))
# 计算像素点颜色的灰度值
gray = r * 0.299 + g * 0.587 + b * 0.114
if gray < 128: # 小于阈值,设置为黑色
img.putpixel((x, y), (0, 0, 0))
else:
img.putpixel((x, y), (255, 255, 255))
return img
# 主函数部分
img = Image.open('boy_RGB.jpg')
img2 = rgb_bw(img)
img2.save('boy_bw_1.jpg')
(二)黑白图像字符画
上述方法都是直接修改图片的像素值,并另存为新的图片,能否使用字符来表示图片的像素值,然后输出为用字符串表示的图像呢?
我们先来看二值黑白图像,可以用”*”表示黑色,” “表示白色。模仿将RGB彩色图像转换为二值黑白图像的算法,编写代码如下:
#【示例代码3】
from PIL import Image
def show_pic(img):
for y in range(0, img.height): # 垂直方向
asd = ""
for x in range(0, img.width): # 水平方向
r, g, b = img.getpixel((x, y))
gray = r * 0.299 + g * 0.587 + b * 0.114
if gray < 128: # 小于阈值,设置为黑色(用”*”表示)
asd = asd + "*"
else:
asd = asd + " "
print(asd)
# 主函数部分
image = Image.open("boy.jpg") # 打开图片
image = image.resize((int(image.width*0.45), int(image.height*0.25))) #调整图片大小
show_pic(image)
运行程序,输出效果图如下(可缩小字体,以便看到完整图像):
show_pic()函数是采用逐行输出字符串的方法。在处理每一行时,都先设置一个空字符串asd,再根据像素点的灰度值,逐个将"*"或" "拼接到字符串asd后面。每处理完一行就输出asd。
这种方法虽然简单,但效率不高,因为每次执行字符串拼接操作都要生成新的字符串对象。更Pythonic的方法是使用字符串列表。把要拼接的字符串插入到列表中,最后再使用join()方法把字符串列表合并为一个新的字符串。参考代码如下:
#【示例代码4】
def show_pic2(img):
asd = [] # 字符串列表
for y in range(0, img.height): # 垂直方向
for x in range(0, img.width): # 水平方向
r, g, b = img.getpixel((x, y))
gray = r * 0.299 + g * 0.587 + b * 0.114
if gray < 128: # 小于阈值,设置为黑色(用”*”表示)
asd.append("*")
else:
asd.append(" ")
asd.append('\n') # 处理完一行,记得插入换行符
print(''.join(asd))
(三)灰度图像字符画
既然可以使用2种字符来表示黑白图像,那么能否使用更多的字符来表示灰度图像呢?
当然可以。
我们知道灰度图像中像素点灰度值的取值范围是[0,255],可以表示为从暗黑到亮白的灰度,黑白图像只取了灰度图像中0和255两种值,分别代表纯黑和纯白。最简单粗暴的方法是使用256个字符分别表示不同的灰度值,但这样做出来的图像视觉效果不一定好。更常用的方法是选择若干个疏密不一的字符,按照从密到疏(或从深到浅)的顺序依次表示从黑到白的不同灰度值。例如使用长度为24的字符串"@#$%&?*o/{[(|!^~-_:;,.` "就能较好地表现出灰度图像的层次感。
用24个字符只能表示24种层次,相当于把256个灰度值分成24个不同区域,分别编号为0-23,则各区域的编号恰好与字符串元素的下标相对应,即每个区域的灰度值用同一个字符表示,每个区域包含的灰度值数量为256/24,。若某个像素点灰度值为gray,则其所在区域编号为int(gray/(256/24)),此即对应字符串元素的下标。
当然,你也不一定非得选择上述长度的字符串。若你选择的字符串serarr长度为count,则灰度值为gray的像素点可以字符serarr[int(gray/(256/count))]来表示。
搞清楚图像字符画的原理后,我们可以用如下代码来实现相关功能:
#【示例代码5】
from PIL import Image
def show_pic(img):
# 用来表示不同区域灰度值的字符串
serarr = "@#$%&?*o/{[(|!^~-_:;,.` "
count=len(serarr)
asd = [] # 储存字符画字符串
for y in range(0, img.height): # 垂直方向
for x in range(0, img.width): # 水平方向
r, g, b = img.getpixel((x, y))
gray = r * 0.299 + g * 0.587 + b * 0.114
asd.append(serarr[int(gray/(256/count))])
asd.append('\n')
print(''.join(asd))
# 主函数部分
image = Image.open("boy.jpg") # 打开图片
image = image.resize((int(image.width*0.45), int(image.height*0.25))) # 调整图片大小
show_pic(image)
运行程序,输出效果图如下(可缩小字体,以便看到完整图像):
(四)将字符画存储到文本文件
接下来我们学习如何把字符画存储到文本文件中。
前面我们都是直接从IDLE输出字符画,更多的时候我们需要将字符画保存到文本文件中。该如何处理呢?
算法其实很简单,只需要把从show_pic()函数中获得的字符串写入文本文件就行了。
我们先以写模式打开文本文件fp,然后自定义函数to_text()返回字符画字符串,并将其写入到fp中即可。参考代码如下:
#【示例代码6】
from PIL import Image
def to_text(img):
# 用来表示不同区域灰度值的字符串
serarr = "@#$%&?*o/{[(|!^~-_:;,.` "
count=len(serarr)
asd = [] # 储存字符画字符串
for y in range(0, img.height): # 垂直方向
for x in range(0, img.width): # 水平方向
r, g, b = img.getpixel((x, y))
gray = r * 0.299 + g * 0.587 + b * 0.114
asd.append(serarr[int(gray/(256/count))])
asd.append('\n')
return ''.join(asd)
# 主函数部分
image = Image.open("boy.jpg") # 打开图片
image = image.resize((int(image.width*0.45), int(image.height*0.25))) # 调整图片大小
tmp = open('boy.txt','w')
tmp.write(to_text(image))
tmp.close()
运行程序,打开文件boy.txt,效果图如下所示:
总结:
到这里,“图像字符画”项目的介绍就告一段落了。我们首先分析了将RGB彩色图像转换为二值黑白图像的算法,并因此得到使用2种字符来表示黑白图像的算法,从而总结出“图像字符画”的原理:首先设置一个用来表示不同区域灰度值的字符串serarr,然后逐行扫描图片,计算出各像素点的灰度值gray,再根据serarr的长度count,计算出gray所在灰度区域的编号int(gray/(256/count)),则serarr[int(gray/(256/count))]就是对应的字符。
根据循序渐进原则,我们先使用print()函数直接在IDLE中输出字符画,然后将字符画存储到文本文件中。如果屏幕不够大,字符画显示不完整,可以通过缩小字体的方式调节字符画大小,获得最佳视觉效果。
在文章的开头,我质疑教材提供的源代码中有一条语句有问题,并给出了自认为正确的语句,关于这个问题,你是怎么看的呢?
需要本文word文档、源代码和课后思考答案的,可以加入“Python算法之旅”知识星球参与讨论和下载文件,“Python算法之旅”知识星球汇集了数量众多的同好,更多有趣的话题在这里讨论,更多有用的资料在这里分享。
我们专注Python算法,感兴趣就一起来!
相关优秀文章: