Operation KiWi

一生使える言語はPythonだと信じてる

Python3で画像をコンソールに表示してみた

f:id:sakage24:20170708022825j:plain

もちろん、生の画像を出力できるわけじゃなくて、ちょっとした加工が必要ですが、なんとかそれっぽくはなったのでメモしておきます。

手順

  1. コンソールのサイズ(高さ、横幅)を取得する
  2. 画像を読み込む
  3. 画像をコンソール用にリサイズする
  4. ドットごとのRGBを取得する
  5. RGBをエスケープシーケンスに変換する

以上がコンソール出力に必要な最低限の機能と手順です。

事前準備

pip install pillow
pip install colorama

coloramaについては以下の記事でまとめています。

www.kiwi-bird.xyz

やってみよう

ソースコード

# images.py
from PIL import Image
import os
import shutil
from colorama import Back


class ConsoleOnImages(object):
    @staticmethod
    def get_terminal_size():
        # shutil.get_terminal_size(fallback=(列, 横幅))
        terminal_size = shutil.get_terminal_size()
        return terminal_size.columns, terminal_size.lines

    @staticmethod
    def get_thumbnail(img, columns=117, lines=63):
        img.thumbnail((lines, columns), Image.ANTIALIAS)
        # img.size[0], img.size[1] = (横幅, 列)
        return img.size[0], img.size[1]

    @staticmethod
    def get_images(path):
        return Image.open(os.path.join(os.path.dirname(__file__), '../img/', path))

    def run(self, file, gray_scale_flag=False):
        path = os.path.join(os.path.dirname(__file__), '../console_imgs/')
        if not os.path.exists(path):
            os.mkdir(path)
        img_path_cut_ext = file[::-1].index(".") + 1
        lists = os.listdir(path)
        if file[:-img_path_cut_ext] + ".txt" in lists:
            with open(os.path.join(path, file[:-img_path_cut_ext] + ".txt"), "rt") as f:
                file = f.read()
                print(file)
            return True
        lines, columns = self.get_terminal_size()
        img = self.get_images(path=file)
        img_w, img_h = self.get_thumbnail(img, lines=lines, columns=columns)
        file_name = file[::-1].split(".")[1]
        self.printer(file_name=file_name[::-1] + ".txt", img=img, img_h=img_h, img_w=img_w, gray_scale_flag=gray_scale_flag)

    def printer(self, file_name, img, img_h, img_w, gray_scale_flag):
        write_flag = False
        file = ""
        if not os.path.exists(os.path.join(os.path.dirname(__file__), '../console_imgs/', file_name)):
            file = open(os.path.join(os.path.dirname(__file__), '../console_imgs/', file_name), 'wt')
            write_flag = True

        def out_put_gray_new_lines(gray_scale):
            print(f"\033[48;5;{gray_scale}m  ")
            if write_flag:
                file.write(f"\033[48;5;{gray_scale}m  \n")

        def out_put_gray_current_lines(gray_scale):
            print(f"\033[48;5;{gray_scale}m  ", end='')
            if write_flag:
                file.write(f"\033[48;5;{gray_scale}m  ")

        def out_put_new_lines(r, g, b):
            print(f"\033[48;2;{r};{g};{b}m  ")
            if write_flag:
                file.write(f"\033[48;2;{r};{g};{b}m  \n")

        def out_put_current_lines(r, g, b):
            print(f"\033[48;2;{r};{g};{b}m  ", end='')
            if write_flag:
                file.write(f"\033[48;2;{r};{g};{b}m  ")

        try:
            for height in range(img_h):
                for width in range(img_w):
                    red, green, blue = img.getpixel((width, height))
                    if width == img_w - 1:
                        if gray_scale_flag:
                            out_put_gray_new_lines(gray_scale=self.gray_pallet(r=red, g=green, b=blue))
                        else:
                            out_put_new_lines(r=red, g=green, b=blue)
                    else:
                        if gray_scale_flag:
                            out_put_gray_current_lines(gray_scale=self.gray_pallet(r=red, g=green, b=blue))
                        else:
                            out_put_current_lines(r=red, g=green, b=blue)
        finally:
            file.close()
            print(Back.RESET)

    @staticmethod
    def gray_pallet(r, g, b):
        gray_scale = 0.299 * r + 0.587 * g + 0.114 * b
        pallets = (0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 132, 143, 154, 165, 176, 187, 198, 209, 220, 231, 242, 253)
        for index, pallet in enumerate(pallets[::-1]):
            if gray_scale >= pallet:
                return 0xFF - index

##### テスト用
if __name__ == '__main__':
    from colorama import init
    if os.name == "nt":
        init()
    ConsoleOnImages().run(file='test.jpg', gray_scale_flag=False)
テスト用
  • ~/img/test.jpgを置いて下さい。
  • ~/sources/images.pyを直接実行して下さい。

実行結果

f:id:sakage24:20170708022825j:plain

何の画像か当ててみて下さい。

解説

get_terminal_size関数でコンソールのサイズを取得します。それを元にPILで開いた画像をコンソールサイズに変換してます。

あとはリサイズした画像の縦横の長さでforループして、1ドットずつ画像のRGB(tuple)を取得します。それをエスケープシーケンスに渡して表示してもらっています。

エスケープシーケンス的に256色が限度だそうなので、モザイクっぽくなってしまいますが、ある意味丁度いいですよねw

それと、windowsだと使える色が少ないっぽいですね。何が何だか分からないことになってしまいました。

f:id:sakage24:20170708023655j:plain

どうにかwindowsでもコンソール表示出来ないか

bash上で256色に対応しているか確かめてみました。

検証コード
    from colorama import init
    init()
    six = 0x0
    for i in range(16):
        for i in range(16):
            print(f"\033[48;5;{six}m  ", end="")
            six += 0x1
        print(f"\033[48;5;{six}m  ")
        six += 0x1

bash

f:id:sakage24:20170708232210j:plain

256色に対応しています。綺麗ですね~

コマンドプロンプト(Windows10 cmd)

f:id:sakage24:20170708232251j:plain

……

結論

Windowsは無視しよう

おまけ

グレースケールにして表示したら何とかなるんじゃないかと思っていましたが錯覚でした!!

f:id:sakage24:20170708233137j:plain

# images.py
    @staticmethod
    def gray_pallet(r, g, b):
        gray_scale = 0.299 * r + 0.587 * g + 0.114 * b
        pallets = (0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121, 132, 143, 154, 165, 176, 187, 198, 209, 220, 231, 242, 253)
        for index, pallet in enumerate(pallets[::-1]):
            if gray_scale >= pallet:
                return 0xFF - index

if __name__ == '__main__':
    from colorama import init
    if os.name == "nt":
        init()
    # gray_scale_flag = False = カラー
    # gray_scale_flag = True = グレースケール
    # デフォルト引数はカラー
    ConsoleOnImages().run(file='test.jpg', gray_scale_flag=True) 

終わり

こちらのスライドが大変参考になりました。

www.slideshare.net

エスケープシーケンスを丁寧に解説してくれています。感謝

www.mm2d.net