字符畫


今日依舊無事,不想搞畢設。

無聊的人想法多,今日就想到把一只 Super Mario 在終端中輸出。

具體做法十分「老土」,就是玩字符畫那一套,但我這次想把這個字符串輸出成彩色的。

准備工作

第一步當然是把圖片轉換為 24 位的位圖,即 bmp 格式的圖片,使用 Windows 自帶的畫圖工具即可。

Aside
之所以叫 24 位圖,是因為在這種格式的圖片中,一個像素由三個整數 (R, G, B) 表示,每個整數均為 8 bit 的整型。R 是 Red,G 是 Green,B 是 Blue,光學三原色是也。

這樣使用合適的庫打開某個圖片,訪問 image[i][j] 就可以獲得一個三元組 (R, G, B) ,后面的事情就是對這些三元組進行操作輸出到終端。也就是說,一個彩色圖片可以等價於一個三維數組 image[m][n][3]

打開圖片

from PIL import Image
image = Image.open(os.sys.argv[1])
image = image.resize((int(80), int(80)), Image.ANTIALIAS)
# 可以通過 resize 調整高度和寬度

獲取一個像素點

image.getpixel(i,j)

預處理為可操作的 list 類型

rgb_data = parse_image(image)
def parse_image(image: Image):
    rgb_tuple_list = list()
    width, height = image.size
    for j in range(height):
        l = list()
        for i in range(width):
            l.append(image.getpixel((i, j)))
        rgb_tuple_list.append(list(l))
    return rgb_tuple_list

終端帶顏色輸出

參考這篇文章

終端字符顏色實際上是通過轉移字符序列來控制的。也就是說,我們在需要輸出的字符串前面加入特定的 ASCII 序列就能夠改變字體的顏色。

輸出有以下 3 種控制方式:

  • 顯示方式:默認值 (0),高亮 (1),下划線 (4),閃爍 (5),反顯 (7)
  • 前景顏色:即字體顏色。紅色 (31),綠色 (32),黃色 (33),藍色 (34),洋紅色 (35),青色 (36),白色 (37)
  • 背景顏色:黑色 (40),紅色 (41),綠色 (42),黃色 (43),藍色 (44),洋紅色 (45),青色 (46),灰白色 (47)

控制 ASCII 序列為 format_str = '\033[{};{};{}m',例如下面的 python 代碼可以輸出🌈顏色:

format_str = '\033[{};{};{}m'
ctl = [0, 1, 4, 5, 7]
for i in ctl:
    for j in range(31, 37 + 1):
        for k in range(40, 47 + 1):
            print(format_str.format(i, j, k) + 'sinkinben', end=' ')
    print('')

灰度圖字符畫

原本一個有顏色的像素點用 (r,g,b) 三個 8 位整型數值表示,灰度圖就是把 (r,g,b) 轉換為一個代表黑白深淺的數值,這樣 image[m][n] 一個整型二維數組可以表示一個黑白的圖片。

轉換函數是一個固定的公式:

def gray_val(r, g, b):
    return int(0.2126 * r + 0.7152 * g + 0.0722 * b)

但是,這仍然是一個圖片,只不過是黑白的,我們無法在普通的終端輸出。因此需要把灰度值映射為一個字符,這樣就能做出網上常見的字符畫。

table = list("@W#$%0OEXC[(/?=^~_.` ")
# table = list("MNHQ$OC67)oa+>!:+. ")
def get_char(r, g, b):
    step = int(256 / len(table)) + 1
    return table[int(gray_val(r, g, b) / step)]

灰度值的范圍是 \([0,255]\) ,相鄰的灰度值呈現的灰度在視覺上是相近的,因此我們就用一個字符 table[i] 來表示某個區間的灰度。為什么代碼是這么寫?舉個例子說明。假設灰度值的范圍是 \([0,16]\),使用四個字符 table = '#@*O' 來表示。也就是說:

[0, 3]   => table[0]
[4, 7]   => table[1]
[8, 11]  => table[2]
[12, 15] => table[3]

在這里灰度值映射得到的字符為 table[gray_val / 4] ,4 是區間的長度,表示一個字符表示灰度值的個數。

table 可以根據輸出的字體手動修改,這是影響「字符畫」美觀的主要因素之一(另外一個因素是寬度和高度的比值,因為終端的字體都是長方形的,如果不調整,輸出的字符畫也是長不拉幾的)。

輸出純字符畫代碼:

def gray_ascii_picture(rgb_data: list):
    ascii_pic = ''
    for row in rgb_data:
        for t in row:
            ascii_pic += get_char(*t) * 3
        ascii_pic += '\n'
    return ascii_pic

ascii_pic += get_char(*t) * 3 表示用 3 個字符表示一個像素點,這是調整寬度的一個技巧。

上色字符畫

首先我們解決一個問題,獲取終端顏色的 RGB 表示,這里使用的是 webcolors 這個庫:

color_list = ['black', 'red', 'green', 'yellow', 'blue',
              'purple', 'skyblue']
color_dict = dict()
for i in range(len(color_list)):
    t = tuple(webcolors.name_to_rgb(color_list[i]))
    color_dict[t] = int(i)
# color_dict is {(0, 0, 0): 0, (255, 0, 0): 1, (0, 128, 0): 2, (255, 255, 0): 3, (0, 0, 255): 4, (128, 0, 128): 5, (135, 206, 235): 6}

圖片中的顏色數目是遠多於終端中可輸出的顏色,因此我們需要用 8 種終端顏色來表示所有的 (R, G, B) 顏色,這里采取的策略是,從終端顏色中挑選一個幾何距離最近的顏色:

# rgb = get_closest_rgb(terminal_colors=color_dict.keys(), rgb=tuple(r,g,b))
def get_closest_rgb(terminal_colors, rgb: tuple):
    min_val = 255 * 255 * 3
    result = None
    for t in terminal_colors:
        val = int(rgb[0] - t[0]) ** 2 + int(rgb[1] - t[1]) ** 2 + int(rgb[2] - t[2])
        if val < min_val:
            min_val = val
            result = t
    return result

最后對每一個像素處理,通過格式化字符串輸出一個「色塊」。

def colorful_ascii_picture(rgb_data: list):
    format_str = '\033[{};{};40m{}\033[0m'
    color_list = ['black', 'red', 'green', 'yellow', 'blue',
                  'purple', 'skyblue']
    color_dict = dict()
    for i in range(len(color_list)):
        t = tuple(webcolors.name_to_rgb(color_list[i]))
        color_dict[t] = int(i)

    print('')
    for row in rgb_data:
        line = ''
        for t in row:
            rgb = get_closest_rgb(terminal_colors=color_dict.keys(), rgb=t)
            icolor = color_dict[rgb]
            print(format_str.format(1, icolor+30, get_char(*t)), end='')
        print('')
    return

效果圖

  • 黑白 Doraemon :使用一個字符和一個空格來表示一個像素,在記事本中縮放查看的效果
  • 彩色 Doraemon :使用 2 個字符表示一個像素,很不幸藍色映射為綠色了😅,終端字體調整為 1 的效果
  • 彩色 Sun Xiaochuan:圖片縮放 200 × 200,2 個字符表示 1 個像素
  • 彩色皮卡丘,背景是 Windows Terminal 自帶的 Acrylic 效果,如果在純黑色背景的終端,效果應該更好,下次用 Ubuntu 試試
  • 彩色 Mario

完整代碼

Usage: python xxx.bmp

from PIL import Image
import numpy
import os
import matplotlib.pyplot as pyplot
import webcolors

table = list("@W#$%0OEXC[(/?=^~_.` ")
# table = list("MNHQ$OC67)oa+>!:+. ")

kernel_size = 2
merge_kernel = [[1 for i in range(kernel_size)] for j in range(kernel_size)]


def gray_val(r, g, b):
    return int(0.2126 * r + 0.7152 * g + 0.0722 * b)


def get_char(r, g, b):
    step = int(256 / len(table)) + 1
    return table[int(gray_val(r, g, b) / step)]


def parse_image(image: Image):
    rgb_tuple_list = list()
    width, height = image.size
    for j in range(height):
        l = list()
        for i in range(width):
            l.append(image.getpixel((i, j)))
        rgb_tuple_list.append(list(l))
    return rgb_tuple_list


def show_in_gui(rgb_data: list):
    # 在 pyplot 中顯示圖片
    pyplot.subplot()
    pyplot.imshow(rgb_data)
    pyplot.show()


def gray_ascii_picture(rgb_data: list):
    ascii_pic = ''
    for row in rgb_data:
        for t in row:
            ascii_pic += get_char(*t) + ' '
        ascii_pic += '\n'
    return ascii_pic


def get_closest_rgb(terminal_colors, rgb: tuple):
    min_val = 255 * 255 * 3
    result = None
    for t in terminal_colors:
        val = int(rgb[0] - t[0]) ** 2 + \
            int(rgb[1] - t[1]) ** 2 + int(rgb[2] - t[2])
        if val < min_val:
            min_val = val
            result = t
    return result


def colorful_ascii_picture(rgb_data: list):
    format_str = '\033[{};{};40m{}\033[0m'
    color_list = ['black', 'red', 'green', 'yellow', 'blue',
                  'purple', 'skyblue']
    color_dict = dict()
    for i in range(len(color_list)):
        t = tuple(webcolors.name_to_rgb(color_list[i]))
        color_dict[t] = int(i)

    print('')
    for row in rgb_data:
        line = ''
        for t in row:
            rgb = get_closest_rgb(terminal_colors=color_dict.keys(), rgb=t)
            icolor = color_dict[rgb]
            # print(icolor, end=' ')
            print(format_str.format(1, icolor+30, get_char(*t))*2, end='')
        print('')
    return


if __name__ == '__main__':
    image = Image.open(os.sys.argv[1])
    image = image.resize((int(200), int(150)), Image.ANTIALIAS)
    rgb_data = parse_image(image)

    # preview
    # show_in_gui(rgb_data)

    # print gray picture in terminal
    # print(gray_ascii_picture(rgb_data))

    colorful_ascii_picture(rgb_data)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM