Image模块应用实例之图像字符画

Python算法之旅

共 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彩色图像转换为二值黑白图像

首先是理解图像字符画的原理。

要理解字符画的原理,首先要搞清楚图片的模式:彩色图片,灰度图片和黑白图片。

彩色图像通常使用RGB色彩模式,图像中的每个像素都分成R、G、B三个基色分量(通道),利用这3个通道的变化和相互叠加来表现各种颜色。灰度图像是每个像素只有灰度值的图像,它只有一个通道,可以显示为从暗黑到亮白的灰度。黑白图像也叫二值图像,它相当于只取灰度图像中0和255两种值,分别代表纯黑和纯白。

我们可以使用convert()方法来获取不同模式的图像,例如下列代码能够将彩色图像boy_RGB.jpg转换成灰度图像boy_L.jpg和黑白图像boy_1.jpg(无抖动效果),效果图如图2和图3所示。

#【示例程序1】from PIL import Imageimg = 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 Imagedef 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 Imagedef 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 Imagedef 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算法,感兴趣就一起来!

相关优秀文章:

阅读代码和写更好的代码

最有效的学习方式

Python算法之旅文章分类

浏览 103
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报