實驗環境
windows 10
Python 3.7.3
pillow 6.2.0
實驗原理
字符畫是一系列字符的組合,我們可以把字符看作是比較大塊的像素,一個字符能表現一種顏色(爲了簡化可以這麼理解),字符的種類越多,可以表現的顏色也越多,圖片也會更有層次感。
如果要將一張彩色的圖片轉換爲單色字符,就要涉及到灰度值和RGB色彩。
灰度值:指黑白圖像中點的顏色深度,範圍一般從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
這樣就好辦了,我們可以創建一個不重複的字符列表,灰度值小(暗)的用列表開頭的符號,灰度值大(亮)的用列表末尾的符號。
步驟分解
使用 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 值轉爲灰度值,然後使用灰度值映射到字符列表中的某個字符。
字符畫所使用的字符集,一共有 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 的時候,這部分代碼不會被執行)。圖片的處理步驟如下:
1、使用 PIL 的 Image.open 打開圖片文件,獲得對象 im
2、使用 PIL 庫的 im.resize() 調整圖片大小對應到輸出的字符畫的寬度和高度,注意這個函數第二個參數使用 Image.NEAREST
,表示輸出低質量的圖片。
3、遍歷提取圖片中每行的像素的 RGB 值,調用 getchar 轉成對應的字符
4、將所有的像素對應的字符拼接在一起成爲一個字符串 txt
5、打印輸出字符串 txt
6、如果執行時配置了輸出文件,將打開文件將 txt 輸出到文件,如果沒有,則默認輸出到 output.txt 文件
這個過程中需要注意的是調用 getchar 時候的參數是通過 PIL 庫的 getpixel 獲取的,見如下代碼:
char = get_char(*im.getpixel((j,i)))
其中 im.getpixel((j,i)) 獲取得到座標 (j,i) 位置的 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)
最後來測試運行一下:
在命令行窗口輸入:python test.py 1.jpg
(這裏需要再命令行跳轉到源碼所在的目錄,源碼和圖片放在同一文件夾內)
爲了方便對比我把命令行窗口字體調小,效果如下:
源碼
# -*- coding=utf-8 -*-
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)
版權聲明:本次實驗在實驗樓完成:https://www.shiyanlou.com