Python下字符畫(ascii art)生成


之前在b站上看到有人用C寫了個腳本把妹抖龍op轉換成字符畫的形式輸出了,感覺比較好玩在下就用python也寫了一遍(主要是因為python比較簡單好用)。這里就這里就不介紹字符畫了,因為能搜到這個的肯定知道自己在干什么 = =。

首先梳理轉換思路:轉換圖片也就是轉換視頻,因為視頻就是有連續的圖片組成的。而為了簡單起見,我們這里先把彩色圖片先轉換成黑白圖片(將RGB三通道轉換成一個),然后讀取圖片的每個像素點,根據像素點的灰度(grayscale)選擇相應的字符(比如@#對應高灰度點,而.,對應低灰度點),將所有當前圖片或幀的像素轉換成字符后輸出就是我們看到的字符畫了。

這里我們可以看到這其實是一個很簡單的事,最重要無非就是要根據灰度選擇字符。這一點已經有好事者統計每個字符所占的像素幫大家做了(參考)。
完整版(灰度從高到低排列,注意最后的空白字符):$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^\'. 而在實際應用中,發現**精簡版**的打印效果更好(其實可以自己隨便挑一些):@%#*+=-:. `

代碼實現:
為了簡單起見,這里只講如何將視屏直接打印到到終端(輸出為黑白,不保留顏色),至於輸出為其他文件形式具體可以參考這里
1.讀取圖片或視頻
這里我們用Pillow或者opencv包進行圖片讀取和處理,后者可以用於視頻操作,但是由於opencv讀取視頻的格式只支持avi,所以進行視頻處理的時候我們需要另一個包:scikit-video;而如果是轉換圖片,那么寫出可以用matplotlib包。

import skvideo.io
import cv2
    
def read_image(image_path, method="opencv"):
    if self.method == "pillow":
        img = PI.open(image_path)
        grey_image = img.convert("L")
    elif self.method == "opencv":
        img = cv2.imread(image_path)
        grey_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    return grey_image

def read_video(video_path):
    videogen = skvideo.io.vreader(video_path)
    for frame in videogen:
        grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        yield grey_frame # 由於視頻包含的圖片很多,為了防止一次處理的內存過大,這里用generator逐幀返回圖片
  1. 將讀取的圖片各像素點轉換成相應的字符
STANDARD_CHAR_LIST = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^'\`. ")
ABBREVIATED_CHAR_LIST = list("@%#*+=-:. ")

def img2ascii(grey_image, char_list=ABBREVIATED_CHAR_LIST, scale=1.0, method='pillow'):
    if method == 'pillow':
        scaled_img_width = int(grey_image.size[0] * scale) # 按比例放大或縮小圖片
        scaled_img_height = int(grey_image.size[1] * scale)
        scaled_grey_img = grey_image.resize((scaled_img_width, scaled_img_height))
    elif method == 'opencv':
        scaled_grey_img = cv2.resize(grey_image, None, fx=scale, fy=scale)
        scaled_img_width = len(scaled_grey_img[0])
        scaled_img_height = len(scaled_grey_img)
    char_list_length = len(char_list)
    ascii_img = [[None for i in range(scaled_img_width)] for j in range(scaled_img_height)]
    ascii_color = [x[:] for x in ascii_img]
    for i in range(scaled_img_height):
        for j in range(scaled_img_width):
            if self.method == "pillow":
                # brightness: the larger, the brighter, and later position in given char list
                brightness = scaled_grey_img.getpixel((j, i))
            elif self.method == "opencv":
                brightness = scaled_grey_img[i, j]
                ascii_img[i][j] = char_list[brightness * char_list_length / 255]
    return ascii_img

3.打印圖片/視頻
打印圖片和視頻稍微有些不同,因為打印視頻是希望能與原視頻播放同步,因此實時讀取轉換打印的時間是無法在0.0278s(默認24幀/1s)內完成的,需要先將轉換結果保存到數組然后再依次打印。
a)圖片

output_str = ""
for line in ascii_img:
    output_str += "".join(line) + "\n"
    os.system(self.clear_console)
sys.stdout.write(output_str)

b)視頻

total_video = []
video_temp_file = video_path + ".temp"
if not os.path.exists(video_temp_file):
    with open(video_temp_file, "wb") as of:
        first_frame = True
        for grey_frame in read_video(video_path):
            ascii_img = self.img2ascii(grey_image=grey_frame, char_list=char_list, scale=scale)
            output_str = ""
            for line in ascii_img:
                output_str += "".join(line) + "\n"
                total_video.append(output_str)
                if first_frame:
                    of.write("{}\n".format(len(ascii_img))) # 再文件開頭保存打印每一幀所需的行數
                    first_frame = False
                of.write(output_str)
            of.close()
else:
    row = 1
    frame_str = ""
    with open(video_temp_file, "rb") as fi:
        frame_row = int(fi.readline().strip())
        for line in fi:
            frame_str += line
            if row % frame_row == 0:
                total_video.append(frame_str)
                frame_str = ""
            row += 1
    fi.close()
for frame in total_video:
    sys.stdout.write(frame)
    sys.stdout.flush() # 清空屏幕
    sys.stdout.write("\x1b[0;0H") # 重新定位打印光標於最開始的為止(0,0)
    time.sleep(0.015) # 設置與下一幀的打印時間間隔

注意:這里要選擇將中間字符保存到文件,然后再讀取打印,這是因為在打印的過程中讀取和打印都需要時間,所以如果設置sleep時間為0.0278,那么將會出現播放時間延遲不同步,因此需要調整sleep的時間。為了避免每次調試重新轉化需要大量時間,這里就進行了保存。

總結:轉換圖片到字符畫並不是什么難的事情,不過在下發現轉換以后的字符畫跟原圖相比就跟打了碼一樣,那么不免想到不妨用DL技術訓練一個模型用於還原萬惡的馬賽克,豈不美哉2333

參考
http://paulbourke.net/dataformats/asciiart/
http://pillow.readthedocs.io/en/3.0.x/handbook/tutorial.html
opencv官方文檔(隨便找找,這邊忘了鏈接就不給了)


免責聲明!

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



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