python 圖片轉字符畫
項目介紹
本實驗用 50 行 Python 代碼完成圖片轉字符畫小工具。通過本實驗將學習到 Linux 命令行操作,Python 基礎,pillow 庫的使用,argparse 庫的使用。
項目知識點
- linux 命令操作
- python 基礎
- pillow 庫的使用
- argparse 庫的使用 簡易教程:https://blog.ixxoo.me/argparse.html
項目環境
- python 3.5
- pillow 5.1.0
PIL 是一個 Python 圖像處理庫,是本項目的重要工具,使用下面的命令來安裝 Pillow(PIL)庫
sudo pip3 install pillow
適合人群
本項目難度簡單,屬於 python 基礎,適合剛剛學習python基礎的人群。
項目原理
字符畫是一系列字符的組合,我們可以把字符看作是比較大塊的像素,一個字符能表現一種顏色(為了簡化可以這么理解),字符的種類越多,可以表現的顏色也越多,圖片也會更有層次感。
問題來了,我們是要轉換一張彩色的圖片,這么多的顏色,要怎么對應到單色的字符畫上去?這里就要介紹灰度值的概念了。
灰度值:指黑白圖像中點的顏色深度,范圍一般從0到255,白色為255,黑色為0,故黑白圖片也稱灰度圖像。
另外一個概念是 RGB 色彩:
RGB色彩模式是工業界的一種顏色標准,是通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標准幾乎包括了人類視力所能感知的所有顏色,是目前運用最廣的顏色系統之一。
我們可以使用灰度值公式將像素的 RGB 值映射到灰度值(注意這個公式並不是一個真實的算法,而是簡化的 sRGB IEC61966-2.1 公式,真實的公式更復雜一些,不過在我們的這個應用場景下並沒有必要):
gray = 0.2126 * r + 0.7152 * g + 0.0722 * b
這樣就好辦了,我們可以創建一個不重復的字符列表,灰度值小(暗)的用列表開頭的符號,灰度值大(亮)的用列表末尾的符號。
項目步驟
首先安裝python圖像處理庫 pillow(PIL):
sudo pip3 install pillow

然后在 /home/shiyanlou/ 目錄下創建 ascii.py 代碼文件進行編輯:

使用 vim 或者 gidit 打開代碼文件:
cd /home/shiyanlou gedit ascii.py
文件打開后,依次輸入以下代碼內容。
首先導入必要的庫,argparse 庫是用來管理命令行參數輸入的。
from PIL import Image import argparse

我們首先使用 argparse 處理命令行參數,目標是獲取輸入的圖片路徑,輸出字符畫的高度和寬度以及輸出文件路徑:
# 首先,構建命令行輸入參數處理 ArgumentParser 實例
parser = argparse.ArgumentParser()
# 定義輸入文件、輸出文件、輸出字符畫的寬和高
parser.add_argument('file') #輸入文件
parser.add_argument('-o', '--output') #輸出文件
parser.add_argument('--width', type = int, default = 80) #輸出字符畫寬
parser.add_argument('--height', type = int, default = 80) #輸出字符畫高
# 解析並獲取參數
args = parser.parse_args()
# 輸入的圖片文件路徑
IMG = args.file
# 輸出字符畫的寬度
WIDTH = args.width
# 輸出字符畫的高度
HEIGHT = args.height
# 輸出字符畫的路徑
OUTPUT = args.output

實現RGB值轉字符的函數
首先,將RGB值轉為灰度值,然后使用灰度值映射到字符列表中的某個字符。
下面是我們的字符畫所使用的字符集,一共有 70 個字符,為了方便項目操作中,直接粘貼避免錯誤:
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
![]()
下面是 RGB 值轉換字符的函數,注意 alpha 值為 0 的時候表示圖片中該位置為空白:
def get_char(r,g,b,alpha = 256):
# 判斷 alpha 值
if alpha == 0:
return ' '
# 獲取字符集的長度,這里為 70
length = len(ascii_char)
# 將 RGB 值轉為灰度值 gray,灰度值范圍為 0-255
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
# 灰度值范圍為 0-255,而字符集只有 70
# 需要進行如下處理才能將灰度值映射到指定的字符上
unit = (256.0 + 1)/length
# 返回灰度值對應的字符
return ascii_char[int(gray/unit)]

完成上面的代碼之后,我們進入到最后一個步驟,對圖片進行處理。
這一個步驟我們放入到 if __name__ == '__main__': 代碼塊中(表示如果 ascii.py 被當作 python 模塊 import 的時候,這部分代碼不會被執行)。圖片的處理步驟如下:
- 首先使用 PIL 的 Image.open 打開圖片文件,獲得對象 im
- 使用 PIL 庫的 im.resize() 調整圖片大小對應到輸出的字符畫的寬度和高度,注意這個函數第二個參數使用 Image.NEAREST,表示輸出低質量的圖片。
- 遍歷提取圖片中每行的像素的 RGB 值,調用 getchar 轉成對應的字符
- 將所有的像素對應的字符拼接在一起成為一個字符串 txt
- 打印輸出字符串 txt
- 如果執行時配置了輸出文件,將打開文件將 txt 輸出到文件,如果沒有,則默認輸出到
output.txt文件
這個過程中需要注意的是調用 getchar 時候的參數是通過 PIL 庫的 getpixel 獲取的,見如下代碼:
其中 im.getpixel((j,i)) 獲取得到坐標 (i,j) 位置的 RGB 像素值(有的時候會包含 alpha 值),返回的結果是一個元組,例如 (1,2,3) 或者 (1,2,3,0)。我們使用 * 可以將元組作為參數傳遞給 get_char,同時元組中的每個元素都對應到 get_char 函數的每個參數。
該部分的代碼實現如下(注意 name 和 main 前后都是兩個下划線):
if __name__ == '__main__':
# 打開並調整圖片的寬和高
im = Image.open(IMG)
im = im.resize((WIDTH,HEIGHT), Image.NEAREST)
# 初始化輸出的字符串
txt = ""
# 遍歷圖片中的每一行
for i in range(HEIGHT):
# 遍歷該行中的每一列
for j in range(WIDTH):
# 將 (j,i) 坐標的 RGB 像素轉為字符后添加到 txt 字符串
txt += get_char(*im.getpixel((j,i)))
# 遍歷完一行后需要增加換行符
txt += '\n'
# 輸出到屏幕
print(txt)
# 字符畫輸出到文件
if OUTPUT:
with open(OUTPUT,'w') as f:
f.write(txt)
else:
with open("output.txt",'w') as f:
f.write(txt)

然后,我們還要從實驗樓下載用來測試的圖片:
wget http://labfile.oss.aliyuncs.com/courses/370/ascii_dora.png


最后,使用剛剛編寫的 ascii.py 來將下載的 ascii_dora.png 轉換成字符畫,此時執行過程沒有指定其他的參數,比如輸出文件、輸出文件的寬和高,這些參數都將使用默認的參數值:
python3 ascii.py ascii_dora.png

然后使用 vim 打開 output.txt 文件:
然后使用 vim 打開 output.txt 文件:
如果使用 vim 可以按 ESC 鍵,然后輸入 :q 進行退出。

注意,不同的環境中顯示的效果可能不盡相同
終端顯示的字體是不是等寬字體,終端顯示的行高和行寬,輸入輸出的圖像寬高等等,這些都會影響顯示效果。
實驗源碼
ascii.py 文件
from PIL import Image
import argparse
#命令行輸入參數處理
parser = argparse.ArgumentParser()
parser.add_argument('file') #輸入文件
parser.add_argument('-o', '--output') #輸出文件
parser.add_argument('--width', type = int, default = 80) #輸出字符畫寬
parser.add_argument('--height', type = int, default = 80) #輸出字符畫高
#獲取參數
args = parser.parse_args()
IMG = args.file
WIDTH = args.width
HEIGHT = args.height
OUTPUT = args.output
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
# 將256灰度映射到70個字符上
def get_char(r,g,b,alpha = 256):
if alpha == 0:
return ' '
length = len(ascii_char)
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
unit = (256.0 + 1)/length
return ascii_char[int(gray/unit)]
if __name__ == '__main__':
im = Image.open(IMG)
im = im.resize((WIDTH,HEIGHT), Image.NEAREST)
txt = ""
for i in range(HEIGHT):
for j in range(WIDTH):
txt += get_char(*im.getpixel((j,i)))
txt += '\n'
print(txt)
#字符畫輸出到文件
if OUTPUT:
with open(OUTPUT,'w') as f:
f.write(txt)
else:
with open("output.txt",'w') as f:
f.write(txt)
