3色電子ペーパーで表示するデータをPythonで作成してみる

#3色電子ペーパーで表示するデータをPythonで作成、ESP32を使って表示してみる

##前の記事

https://qiita.com/ridervance/items/51390f9d6a652bc7fefe

##概要
前の記事で作ったESP32を使った電子ペーパー表示デバイスにおいて、自分で作成したpngファイルをディスプレイ上に表示できるようにします。
画像ファイルからのhexデータ生成については、以下の通りすでに先人がGUIツールを作成されているのを見つけたのですが、自分の環境ではうまく動かなかったため参考にさせていただきつつPythonで実装してみます。

https://github.com/ienaga045/three_color_epaper/

##目的
電子ペーパーの表示領域である250x122のサイズを持つ任意のpngファイルから、黒/白・赤の要素を取り出して表示用RAMに書き込むデータを作成、電子ペーパ上に表示させます。

##実現方式
元々のHATのサンプルプログラムではRaspPi上で画像ファイル読み込み->書き込むデータの生成を行なっていましたが、今回はデータ生成はPC上でPythonを使って行い、生成したデータをESP32側に書き込む方式とします。

##データ生成のためのプログラム(Python)

```python:img2hex.py

import numpy as np
from PIL import Image, ImageChops


# bit->hex変換
def array2hex(bit_list, flag):
    pol = flag
    hex_data = 0x00
    if bit_list[0] == pol: hex_data = hex_data | 0x80
    if bit_list[1] == pol: hex_data = hex_data | 0x40
    if bit_list[2] == pol: hex_data = hex_data | 0x20
    if bit_list[3] == pol: hex_data = hex_data | 0x10
    if bit_list[4] == pol: hex_data = hex_data | 0x08
    if bit_list[5] == pol: hex_data = hex_data | 0x04
    if bit_list[6] == pol: hex_data = hex_data | 0x02
    if bit_list[7] == pol: hex_data = hex_data | 0x01
    return hex_data

# 黒/赤の表示データ作成
def make_red_bw_binary_image(filename, bw_thresh, red_thresh):
    img = Image.open(filename)                  # 元pngファイルオープン
    img_red, img_green, img_blue = img.split()  # RGB抽出

    # 二値化
    img_red_bin = img_red.point(lambda x: 0 if x < bw_thresh else 1, mode='1')      # 白赤->白/黒->黒
    img_blue_bin = img_blue.point(lambda x: 0 if x < red_thresh else 1, mode='1')   # 白緑->白/黒->黒
    img_green_bin = img_green.point(lambda x: 0 if x < red_thresh else 1, mode='1')  # 白青->白/黒->黒
    img_black_bin = img_red_bin                                                     # 黒の抽出(白く塗る部分を'1')
    img_green_and_blue = ImageChops.logical_and(img_green_bin, img_blue_bin)        # AND(G, B)
    img_red_bin = ImageChops.logical_xor(img_red_bin, img_green_and_blue)           # XOR (R, AND(G, B))白部分の除去(赤要素の抽出）
    img_red_bin = ImageChops.invert(img_red_bin)

    # 黒/赤表示画像保存
    img_black_bin.save('black.png')
    img_red_bin.save('red.png')

# 画像を読み込んでhexデータ化
def make_hex_data(img, out_filename, var_name, flag):
    # img = ImageOps.mirror(img) #左右反転が必要なら
    width, height = img.size
    array = np.array(img)
    text_file = open(out_filename, 'w')                         # 書き込みモードで開く
    text_file.write('const uint8_t ' + var_name + '[] = {\n')   # 定数ヘッダ
    rem = height % 8                        # 8で割った時の余り
    for j in range(width):
        for i in range(0, height, 8):
            bit_list = [True] * 8           # 配列初期化
            if int(height / 8) * 8 != i:    # 最終バイト以外の処理
                for k in range(8):
                    bit_list[k] = array[i + k][j]
            else:                           # 最終バイト処理
                if rem != 0:                # 8bitなければ
                    for k in range(rem):
                        bit_list[k] = array[i + k][j]
                    for l in range(rem, 8):
                        bit_list[l] = True
                else:
                    for k in range(rem):    # 8bitあれば
                        bit_list[k] = array[i + k][j]

            hex_data = array2hex(bit_list, flag)
            text_file.write('0x' + format(hex_data, '02X') + ',')  # '0x00, 'の形式で追記
        text_file.write('\n')                                      # 改行
    text_file.write('};')                                          # 定数フッタ


def main():
    filename = 'test.png'
    bw_thresh = 128
    red_thresh = 150
    make_red_bw_binary_image(filename, bw_thresh, red_thresh)
    make_hex_data(Image.open('black.png'), 'predefined_bw.h', 'PREDEFINED_BW', True)
    make_hex_data(Image.open('red.png'), 'predefined_red.h', 'PREDEFINED_RED', False)


if __name__ == '__main__':
    main()

```

* カレントディレクトリの"test.png"を読み込み、同じフォルダに白黒/赤の表示データとして"red.png"/"black.png"(参照用の画像ファイル)と、"predefined_bw.h"、"predefined_red.h"(ESP32書き込み用バイナリ)を書き出す。
* 二値化したpngファイルを読み出す際、最終バイト部分が8bitに満たない場合、配列の存在しない部分の添字を呼ぶとエラーになるため別途処理が必要。("//最終バイト処理"の部分)

##上で生成したHeaderファイルを読み込んで表示するESP32側のプログラム

```C#:epd_test2.ino
#include <SPI.h>
#include "predefined_screen.h"
#include "predefined_bw.h"
#include "predefined_red.h"

#define RST_PIN 16
#define DC_PIN 17
#define CS_PIN 5
#define BUSY_PIN 4

#define BUTTON 0
#define LED 2

void waitBusy()
{
  while (1)
  {
    if (digitalRead(BUSY_PIN) == 0)
    {
      Serial.println("BUSY has been turned low");
      break;
    }
    usleep(100);
  }
}

void sendCommand(unsigned char cmd)
{
  digitalWrite(DC_PIN, 0);
  SPI.transfer(cmd);
}

void sendData(unsigned char data)
{
  digitalWrite(DC_PIN, 1);
  SPI.transfer(data);
}

void sendPredefined(void)
{
  waitBusy();
  sendCommand(0x26);
  for (int i = 0; i < sizeof(PREDEFINED_RED)/sizeof(PREDEFINED1_RED[0]); i++)
  {
    sendData(PREDEFINED1_RED[i]);
  }
  waitBusy();
  sendCommand(0x24);
  for (int i = 0; i < sizeof(PREDEFINED_BW)/sizeof(PREDEFINED1_BW[0]); i++)
  {
    sendData(PREDEFINED1_BW[i]);
  }
}

void updateAndSleep(void)
{
  sendCommand(0x20);
  waitBusy();
  sendCommand(0x10);
  sendData(0x01);
  usleep(100 * 1000);
}

void initScreen()
{
  usleep(10 * 1000);
  digitalWrite(RST_PIN, 0);
  usleep(10 * 1000);
  digitalWrite(RST_PIN, 1);
  usleep(10 * 1000);
  waitBusy();
  sendCommand(0x12);
  waitBusy();
}

void setup()
{
  Serial.begin(115200);
  // put your setup code here, to run once:
  pinMode(RST_PIN, OUTPUT);
  pinMode(DC_PIN, OUTPUT);
  pinMode(CS_PIN, OUTPUT);
  pinMode(BUSY_PIN, INPUT);
  pinMode(BUTTON, INPUT);
  pinMode(LED, OUTPUT);

  SPI.begin();
  Serial.println(digitalRead(BUSY_PIN));
}

void loop()
{
  // predefined screen
  digitalWrite(LED, 0);
  initScreen();
  sendPredefined();
  updateAndSleep();
  digitalWrite(LED, 1);

}

```

* 前項のPythonスクリプトで生成したpredefine_bw[red].hを読み込んで電子ペーパ状のSRAMに書き込み->表示します。

* RAM書き込み中/画面書換中(BUSY_PIN==1)の際にLEDを消灯、書換完了時に点灯するようにしました。

##結果

mspaintで適当に作った以下のテストパターン(test.png)から生成したpredefined_bw(red).hを使って動かしてみます。
![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/b809c09d-c4f7-d8a0-ad9e-bf4415d0b103.png)
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/36c4f620-a763-e4f5-6ff4-582bdc4e48b6.gif)
うまく動きました。
目論見通りPC側で事前にhex化したpngファイルを扱えるようになったのでよかったです。

ちなみにpythonスクリプト実行時に生成された中間ファイルはこんな感じです。

(←black.png / red.png→)

![black.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/e2884a4e-9e0d-4f25-4b7c-3f1daa91bfee.png)    ![red.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540268/8fd0ce5a-99f6-95a3-6909-61a5d8887f99.png)

##今後の課題

* せっかく低消費電力な表示デバイスを扱えるようになったので、次はなんらか表示が必要なIoT機器を作ってみたいと思います。
* また、現状だと事前設定した画像を表示することしかできないので、Webから取ってきた情報をそのまま表示できるよう、Fontを扱えるように継続調査したいと思います。

##参考情報

Pillowによる画像処理関連で参考にした記事

https://qiita.com/pashango2/items/145d858eff3c505c100a