ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【python】pythonでpngファイルをBitmapテキストデータに変換する

 今回の記事はタイトルにある通りpythonを使ってpngファイルをBitmap形式のテキストデータに変換する方法の備忘録になります。

 では、始めます。


1:RGB565とRGB332について
 画像の色を表現するフォーマットには様々なものがありますが、今回変換に使用したRGB565とRGB332について軽く説明しておきます。

・RGB565
 Rを5bit、Gを6bit、Bを5bitの合計16bitで65536色表現可能な形式。

・RGB332
 Rを3bit、Gを3bit、Bを2bitの合計8bitで256色表現可能な形式。

 それぞれの違いをまとめると以下のようになります。

RGB565 RGB332
R 5bit 3bit
G 6bit 3bit
B 5bit 2bit
合計bit数 16bit 8bit
表現可能色 65536色 256色
0xFFFF 0xFF


2:RGBの値をRGB565やRGB332に変換する方法
 RGBの値(それぞれの値が0~255)をRGB565やRGB332に変換する方法は簡単です。

 RGBそれぞれの値は0~255の範囲なのでそれぞれ8bitの情報を持っています。

 例えばRの値をRGB565に対応させる場合は8bitを右へビットシフトさせて5bitまで減らします。つまり8bit-5bit=3bit分だけ右へビットシフトさせます。同様にしてGは8bit-6bit=2bit分、Bは8bit-5bit=3bit分右へビットシフトさせます。

newR = R>>3
newG = G>>2
newB = B>>3

 各色自体の変換は終わったので、16bitにまとめるために各値を合体させます。

# 桁数を増やしてORで結合させている
newRGB = (newR<<11) | (newG<<5) | newB

 これでRGBの値をRGB565に変換ができました。

 RGBの値をRGB332へ変換するのも同様の方法でできます。

# RGBをRGB332へ変換
newR = R>>5
newG = G>>5
newB = B>>6

newRGB = (newR<<5) | (newG<<2) | newB

 これでRGBをそれぞれのフォーマットに変換する方法がわかりました。
 あとはこの方法を使ってpngファイルをBitmap形式のテキストデータに変換してきます。


3:PillowとNumpyのインストール
 pythonで行う際に画像からpixelの色を取り出すのにPillowというライブラリを使用するのでインストールします。

$ pip install Pillow

# もしくは以下の2コマンドを実行
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade Pillow

 またNumpyも使うのでインストールします。

$ pip install numpy

 これで準備は完了です。


4:pngファイルをBitmapテキストデータに変換するサンプル
 基本的なところは以下のソースコードをほぼ流用させていただきました。

 以下の「pngToBitmapData.py」ファイルと同じ階層に「img」フォルダを作成し、中に連番png画像を入れた状態で実行すると「ImgData.h」という連番画像のBitmapテキストデータを記述したC言語向けヘッダーファイルが作成されます。

・pngToBitmapData.py (※2021/04/26 ソースコードを修正)

# -*- coding:utf-8 -*-
import numpy as np
from PIL import Image
import sys
import glob

IMG_FOLDER_PATH = "./img/*"
SAVE_FILE_PATH  = "./ImgData.h"


def main(outstr):
    img_list = sorted(glob.glob(IMG_FOLDER_PATH)) # 画像リストを取得

    # 画像がない場合は終了
    if len(img_list) == 0:
        return print("No image File!")
    
    f = open(SAVE_FILE_PATH, 'w')

    file_count = 0
    for fn in img_list:
        file_count += 1
        print("loading..." + fn)

        # 画像ファイル読み込み
        image = Image.open(fn)
        width, height = image.size

        # 最初の画像の時に画像サイズなどの情報を書き込む
        if file_count == 1:
            header_str  = "const int IMG_WIDTH  = {};\n".format(width)
            header_str += "const int IMG_HEIGHT = {};\n".format(height)
            header_str += "const int IMG_MAX    = {};\n\n".format(len(img_list))
            header_str += "const unsigned short IMG_DATA[IMG_MAX][IMG_WIDTH*IMG_HEIGHT] = {\n"
            f.write(header_str)

        # 画像の色配列情報の文字列を取得して書き込む
        image_hex = outputColorPixel(width, height, image, outstr)
        f.write("    {\n")
        f.write(image_hex)

        if file_count != len(img_list):
            f.write("    },\n")
        else:
            f.write("    }\n")

    # 最後の括弧とセミコロンを書き込んで閉じる
    f.write("};\n")
    f.close()

    print("saved " + SAVE_FILE_PATH)


# RGB値を16bit(RGB565)のテキストに変換する
def rgb2hexstr(rgb):
    col = ((rgb[0]>>3)<<11) | ((rgb[1]>>2)<<5) | (rgb[2]>>3)
    return "0x{:04X}".format(col)


# RGB値を8bit(RGB332)のテキストに変換する
def rgb8bithexstr(rgb):
    col = ((rgb[0]>>5)<<5) | ((rgb[1]>>5)<<2) | (rgb[2]>>6)
    return "0x{:02X}".format(col)


# 8, 16bpp出力
def outputColorPixel(width, height, image, outstr):
    result_str = ""

    #  パレット読み込み
    if image.mode == 'P':
        palette = np.array(image.getpalette()).reshape(-1, 3)  # n行3列に変換
        getPixel = lambda x,y: palette[image.getpixel((x, y))]
    else:
        getPixel = lambda x,y: image.getpixel((x, y))

    # 書き込み時の改行位置を計算
    line_break = 16
    while True:
        if (width % line_break) == 0:
            break
        line_break = line_break - 1

    for y in range(height):
        x_cnt = 0
        for x in range(width):
            #  行の先頭に空白を入れる
            if x_cnt == 0:
                result_str += "    "
            pixel = getPixel(x, y)
            result_str += outstr(pixel) + ","
            x_cnt = x_cnt + 1
            if x_cnt >= line_break:
                x_cnt = 0
                result_str += "\n"

    return result_str[:-2] + "\n"


if __name__ == '__main__':
    args = sys.argv

    outstr = rgb2hexstr # 16bit(RGB565の場合)
    if len(args) > 1:
        if "8" in args:
            outstr = rgb8bithexstr # 8bit(RGB332の場合)
            print("RGB332(8bit)")
        else:
            print("RGB565(16bit)")
    else:
        print("RGB565(16bit)")

    main(outstr)

 コードの内容について特に説明することはないかと思いますが、連番画像を読み込んでその画像の横幅と縦幅に合わせた色の配列を作ってヘッダーファイルとして出力しているだけです。

 ちなみに実行時はデフォルトでは「RGB565」に変換されますが、以下のように引数「8」を設定すると「RGB332」の変換になります。

# RGB565への変換
$ python pngToBitmapData.py

# RGB332への変換
$ python pngToBitmapData.py 8


※2021/04/26追記:サンプル連番画像を含めたzipファイルも公開しておきます。


 以上がpythonpngファイルをBitmapテキストデータに変換する方法になります。

 画像変換など理解できれば簡単ですが、何も知らなかったので理解するまでに色々調べたりして大変でした。

 また何故C言語向けのヘッダーファイルに出力しているのかというと、M5StickC PlusにはSDカードスロットがないので画像を表示させたい場合は色の配列として持っておくしかないからです(その分ヘッダーファイルが巨大になりますが…)。

 この方法で変換したヘッダーファイルを使えばM5StickC Plusで画像を表示できるようになるので、次の記事でそれをやっていきたいと思います。


・参考資料