Image模塊應用實例之圖像字符畫
說在前面
“圖像字符畫”是浙教版《信息技術(shù)必修一數(shù)據(jù)與計算》第三章的實踐與體驗項目。教材簡要說明了字符畫的算法原理,并提供了完整源代碼。
筆者輸入并運行了教材提供的代碼,發(fā)現(xiàn)程序能正常運行。但是筆者對代碼中表達式int(gray/(255/(count-1)))的含義表示疑惑,認為這個表達式的意義不明確,恐怕是錯誤的。
因為教材是經(jīng)過專家多次審閱的,懷疑教材出錯需要莫大勇氣。更大的可能性是筆者自身水平不夠高,沒有看懂源代碼。
本文包含筆者對“圖像字符畫”項目實踐的算法分析和授課思路,有不當之處,敬請各位老師批評指正。




算法分析:
程序先構(gòu)建長度為count的字符串列表serarr,然后打開圖片"boy.jpg",并將圖片調(diào)整到適當大小。再以追加模式打開文本文件"boy.txt",然后調(diào)用自定義函數(shù)toText(),將圖像中的像素轉(zhuǎn)換成字符,返回字符串a(chǎn)sd,并將asd寫入到文本文件中。
自定義函數(shù)toText()的基本思路是先構(gòu)建一個空字符串a(chǎn)sd,然后逐行遍歷圖像的各像素點,提取像素點顏色值后,計算其對應灰度值(取值范圍[0,255]),然后將灰度值轉(zhuǎn)換成列表serarr的下標,得到對應的字符,將其拼接到asd中。每處理完一行后,在asd中拼接一個'\r\n',表示回車換行(window系統(tǒng)),最后返回asd。
提出疑點:
表達式int(gray/(255/(count-1)))的含義是什么?
從自定義函數(shù)toText()的功能來看,該表達式應該是用來計算灰度值gray對應的字符在列表serarr中的下標。
因為gray的取值范圍是[0,255],而下標的范圍是[0,count-1],則相當于將256個數(shù)平均分成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 Imagedef 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 asdserarr = ['@','#','$','%','&','?','*','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個通道的變化和相互疊加來表現(xiàn)各種顏色?;叶葓D像是每個像素只有灰度值的圖像,它只有一個通道,可以顯示為從暗黑到亮白的灰度。黑白圖像也叫二值圖像,它相當于只取灰度圖像中0和255兩種值,分別代表純黑和純白。
我們可以使用convert()方法來獲取不同模式的圖像,例如下列代碼能夠?qū)⒉噬珗D像boy_RGB.jpg轉(zhuǎn)換成灰度圖像boy_L.jpg和黑白圖像boy_1.jpg(無抖動效果),效果圖如圖2和圖3所示。
#【示例程序1】from PIL import Imageimg = Image.open('boy_RGB.jpg')img2 = img.convert("L") # 轉(zhuǎn)換成“L”模式灰度圖像img2.save("boy_L.jpg")img3 = img.convert("1", dither=0) #參數(shù)dither=0表示無抖動效果img3.save("boy_1.jpg")

除了直接使用convert()方法將彩色圖像轉(zhuǎn)換成黑白圖像,我們也可以利用圖像模式轉(zhuǎn)換原理,設置自定義函rgb_bw()來實現(xiàn)轉(zhuǎn)換功能。
首先根據(jù)從“RGB”轉(zhuǎn)換為“L”模式的公式:L = R*0.299 + G*0. 587 + B*0.114,計算出每個像素點的灰度值gray,再判斷gray與閾值的關(guān)系,若gray小于閾值,設置為黑色,否則設置為白色。閾值通常取值為128,也可以根據(jù)需要設置不同的閾值。
#【示例程序2】from PIL import Image# 將RGB彩色圖像轉(zhuǎn)換為二值黑白圖像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.114if gray < 128: # 小于閾值,設置為黑色img.putpixel((x, y), (0, 0, 0))else:img.putpixel((x, y), (255, 255, 255))return img# 主函數(shù)部分img = Image.open('boy_RGB.jpg')img2 = rgb_bw(img)img2.save('boy_bw_1.jpg')
(二)黑白圖像字符畫
上述方法都是直接修改圖片的像素值,并另存為新的圖片,能否使用字符來表示圖片的像素值,然后輸出為用字符串表示的圖像呢?
我們先來看二值黑白圖像,可以用”*”表示黑色,” “表示白色。模仿將RGB彩色圖像轉(zhuǎn)換為二值黑白圖像的算法,編寫代碼如下:
#【示例代碼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.114if gray < 128: # 小于閾值,設置為黑色(用”*”表示)asd = asd + "*"else:asd = asd + " "print(asd)# 主函數(shù)部分image = Image.open("boy.jpg") # 打開圖片image = image.resize((int(image.width*0.45), int(image.height*0.25))) #調(diào)整圖片大小show_pic(image)
運行程序,輸出效果圖如下(可縮小字體,以便看到完整圖像):

show_pic()函數(shù)是采用逐行輸出字符串的方法。在處理每一行時,都先設置一個空字符串a(chǎn)sd,再根據(jù)像素點的灰度值,逐個將"*"或" "拼接到字符串a(chǎn)sd后面。每處理完一行就輸出asd。
這種方法雖然簡單,但效率不高,因為每次執(zhí)行字符串拼接操作都要生成新的字符串對象。更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.114if gray < 128: # 小于閾值,設置為黑色(用”*”表示)asd.append("*")else:asd.append(" ")asd.append('\n') # 處理完一行,記得插入換行符print(''.join(asd))
(三)灰度圖像字符畫
既然可以使用2種字符來表示黑白圖像,那么能否使用更多的字符來表示灰度圖像呢?
當然可以。
我們知道灰度圖像中像素點灰度值的取值范圍是[0,255],可以表示為從暗黑到亮白的灰度,黑白圖像只取了灰度圖像中0和255兩種值,分別代表純黑和純白。最簡單粗暴的方法是使用256個字符分別表示不同的灰度值,但這樣做出來的圖像視覺效果不一定好。更常用的方法是選擇若干個疏密不一的字符,按照從密到疏(或從深到淺)的順序依次表示從黑到白的不同灰度值。例如使用長度為24的字符串"@#$%&?*o/{[(|!^~-_:;,.` "就能較好地表現(xiàn)出灰度圖像的層次感。
用24個字符只能表示24種層次,相當于把256個灰度值分成24個不同區(qū)域,分別編號為0-23,則各區(qū)域的編號恰好與字符串元素的下標相對應,即每個區(qū)域的灰度值用同一個字符表示,每個區(qū)域包含的灰度值數(shù)量為256/24,。若某個像素點灰度值為gray,則其所在區(qū)域編號為int(gray/(256/24)),此即對應字符串元素的下標。
當然,你也不一定非得選擇上述長度的字符串。若你選擇的字符串serarr長度為count,則灰度值為gray的像素點可以字符serarr[int(gray/(256/count))]來表示。
搞清楚圖像字符畫的原理后,我們可以用如下代碼來實現(xiàn)相關(guān)功能:
#【示例代碼5】from PIL import Imagedef show_pic(img):# 用來表示不同區(qū)域灰度值的字符串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.114asd.append(serarr[int(gray/(256/count))])asd.append('\n')print(''.join(asd))# 主函數(shù)部分image = Image.open("boy.jpg") # 打開圖片image = image.resize((int(image.width*0.45), int(image.height*0.25))) # 調(diào)整圖片大小show_pic(image)
運行程序,輸出效果圖如下(可縮小字體,以便看到完整圖像):

(四)將字符畫存儲到文本文件
接下來我們學習如何把字符畫存儲到文本文件中。
前面我們都是直接從IDLE輸出字符畫,更多的時候我們需要將字符畫保存到文本文件中。該如何處理呢?
算法其實很簡單,只需要把從show_pic()函數(shù)中獲得的字符串寫入文本文件就行了。
我們先以寫模式打開文本文件fp,然后自定義函數(shù)to_text()返回字符畫字符串,并將其寫入到fp中即可。參考代碼如下:
#【示例代碼6】from PIL import Imagedef to_text(img):# 用來表示不同區(qū)域灰度值的字符串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.114asd.append(serarr[int(gray/(256/count))])asd.append('\n')return ''.join(asd)# 主函數(shù)部分image = Image.open("boy.jpg") # 打開圖片image = image.resize((int(image.width*0.45), int(image.height*0.25))) # 調(diào)整圖片大小tmp = open('boy.txt','w')tmp.write(to_text(image))tmp.close()
運行程序,打開文件boy.txt,效果圖如下所示:

總結(jié):
到這里,“圖像字符畫”項目的介紹就告一段落了。我們首先分析了將RGB彩色圖像轉(zhuǎn)換為二值黑白圖像的算法,并因此得到使用2種字符來表示黑白圖像的算法,從而總結(jié)出“圖像字符畫”的原理:首先設置一個用來表示不同區(qū)域灰度值的字符串serarr,然后逐行掃描圖片,計算出各像素點的灰度值gray,再根據(jù)serarr的長度count,計算出gray所在灰度區(qū)域的編號int(gray/(256/count)),則serarr[int(gray/(256/count))]就是對應的字符。
根據(jù)循序漸進原則,我們先使用print()函數(shù)直接在IDLE中輸出字符畫,然后將字符畫存儲到文本文件中。如果屏幕不夠大,字符畫顯示不完整,可以通過縮小字體的方式調(diào)節(jié)字符畫大小,獲得最佳視覺效果。
在文章的開頭,我質(zhì)疑教材提供的源代碼中有一條語句有問題,并給出了自認為正確的語句,關(guān)于這個問題,你是怎么看的呢?
需要本文word文檔、源代碼和課后思考答案的,可以加入“Python算法之旅”知識星球參與討論和下載文件,“Python算法之旅”知識星球匯集了數(shù)量眾多的同好,更多有趣的話題在這里討論,更多有用的資料在這里分享。
我們專注Python算法,感興趣就一起來!
相關(guān)優(yōu)秀文章:
