M5UnitVで顔検出してM5StickCPlusのLCDに表示する

# はじめに

* 以前、M5UnitVをカメラとして使い、M5StackのLCDに表示するってのをやってみた。
    * [M5UnitVのカメラ映像をシリアル通信でM5Stackの液晶に表示する](https://qiita.com/Nabeshin/items/9268dc88927123319549)
* 今回は発展版として、M5UnitVで顔検出した結果をM5StickCPlusのLCDに表示してみた。

![IMG_4104.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/ed4a8631-4a82-074b-eea0-da7b4d0ee9bb.jpeg)

# セットアップ

写真のようにM5UnitVとM5StickCPlusをGroveケーブルで繋ぐ。

![IMG_4105.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/4042e3b3-bc2e-4b40-28ee-30cd837c927e.jpeg)

前回同様に、以下を参考にさせて頂きました。ありがとうございます。

* HomeMadeGarbage さんのページがとても参考になります。構成が多少異なるので、適宜読み替えます。
    * https://homemadegarbage.com/ai13
* グラフィックス系はLovyanGFXライブラリを使わせてもらっています。フレームレートが向上します。
    * https://github.com/lovyan03/LovyanGFX




# 顔検出
* Sipeedの解説ページはこちら。
    * https://wiki.sipeed.com/soft/maixpy/en/course/ai/image/face_detect.html
* Yolo v2で作ったモデルをK210向けのkmodelに変換した `face_model_at_0x300000.kfpkg` が提供されているので、これを使う。
    * kflash_guiを使ってフラッシュメモリに書き込むか、SDカードに格納して読み込む方法の２つがある。
    * 自分はkflash_guiを使った。
    * フラッシュメモリに書き込んだ場合は、 `task = kpu.load(0x300000)` でメモリ領域のオフセットを指定してモデルをロードする。

![スクリーンショット 2022-08-18 10.43.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/4db8d664-659a-b39a-b3c2-52f4d044bb58.png)

モデルはここから入手可能。

https://github.com/sipeed/MaixPy_scripts/tree/master/machine_vision/models

![スクリーンショット 2022-08-18 10.52.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/59c41d69-80d1-9fc2-4ee7-208da89a97d9.png)


ちなみに、今回ちゃんとドキュメントを読み込んでみたらこんな解説があるのを知った。

![スクリーンショット 2022-08-18 10.59.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/191e66c4-4187-f1ad-63b0-f09ad3632966.png)

要約すると、

* MaixPyには２種類のイメージバッファがあって、使い分けている。
    * １つはLCD用の`RGB565`で、もう一つは`RGB888`というKPU（AI処理）用
* カメラでキャプチャすると、自動的に２箇所のバッファに画像がコピーされるが、それ以外の場合は手動でやる必要がある。
    * 例えば、モデルが要求する画像サイズがLCDと異なる場合には、`img = img.resize((224, 224))`みたいな感じで一旦resizeし、`img.pix_to_ai()` を実行する必要がある。


ふーん。なるほど。
ただ、M5UnitVのカメラはQVGA(320×240)なので、AI処理用にリサイズすると言ってもそんなに大幅なサイズギャップにはならない気がするけど、ちゃんと使い分けできるのは良いと思った。


# M5UnitV側のコード

* [sipeedのリファレンスコード](https://github.com/sipeed/MaixPy_scripts/blob/master/machine_vision/face_find/demo_find_face.py)をベースに、UART転送処理を追加。
    * 顔検出モデルはあらかじめ `model_addr=0x300000` のフラッシュに書き込んでおく
    * 小さいLCDでも見やすいようにバウンディングボックスの色と太さを変更
    * UnitVカメラのQVGA画像をM5StickCPlusのLCDサイズ（128, 240）に中心部分をクロップしてJpegにしたものを転送
    * Jpeg画像のサイズは可変なので、パケットヘッダに画像サイズを仕込んで転送


```python:demo_find_face.py

# https://github.com/sipeed/MaixPy_scripts/blob/master/machine_vision/face_find/demo_find_face.py
import sensor, image, time
import KPU as kpu
import gc, sys
from machine import UART
from fpioa_manager import fm

def lcd_show_except(e):
    import uio
    err_str = uio.StringIO()
    sys.print_exception(e, err_str)
    err_str = err_str.getvalue()
    img = image.Image(size=(224,224))
    img.draw_string(0, 10, err_str, scale=1, color=(0xff,0x00,0x00))

def main(model_addr=0x300000, lcd_rotation=0, sensor_hmirror=False, sensor_vflip=False):

    # UART
    fm.register(35, fm.fpioa.UART1_TX, force=True)
    fm.register(34, fm.fpioa.UART1_RX, force=True)
    uart = UART(UART.UART1, 115200,8,0,0, timeout=1000, read_buf_len=4096)

    try:
        sensor.reset()
    except Exception as e:
        raise Exception("sensor reset fail, please check hardware connection, or hardware damaged! err: {}".format(e))
    sensor.set_pixformat(sensor.RGB565)
    sensor.set_framesize(sensor.QVGA)
    sensor.set_hmirror(1)
    sensor.set_vflip(1)
    sensor.run(1)

    anchors = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
    try:
        task = None
        task = kpu.load(model_addr)
        kpu.init_yolo2(task, 0.5, 0.3, 5, anchors) # threshold:[0,1], nms_value: [0, 1]
        while(True):
            img = sensor.snapshot()
            t = time.ticks_ms()
            objects = kpu.run_yolo2(task, img)
            t = time.ticks_ms() - t
            if objects:
                for obj in objects:
                    img.draw_rectangle(obj.rect(), color=(0,0,255), thickness=3)
            img.draw_string(60, 200, "t:%dms" %(t), scale=2)

            # crop image for M5StickCPlus
            img2 = img.copy((56, 0, 128, 240))
            img_buf = img2.compress(quality=50)

            # Packetヘッダ
            img_size1 = (img2.size()& 0xFF0000)>>16
            img_size2 = (img2.size()& 0x00FF00)>>8
            img_size3 = (img2.size()& 0x0000FF)>>0
            data_packet = bytearray([0xFF,0xD8,0xEA,0x01,img_size1,img_size2,img_size3,0x00,0x00,0x00])
            uart.write(data_packet)

            uart.write(img_buf)

    except Exception as e:
        raise e
    finally:
        if not task is None:
            kpu.deinit(task)


if __name__ == "__main__":
    try:
        main( model_addr=0x300000, lcd_rotation=0, sensor_hmirror=False, sensor_vflip=False)
        # main(model_addr="/sd/m.kmodel")
    except Exception as e:
        sys.print_exception(e)
        lcd_show_except(e)
    finally:
        gc.collect()

```



# M5StickCPlus側のコード

* `#define LGFX_AUTODETECT`のおかげでほぼM5StackのコードのままでM5StickCPlus でも動く。素敵！
    * 一応、`#include <M5StickCPlus.h>`は必要。

```c:
#define LGFX_AUTODETECT
#include <LovyanGFX.hpp>
#include <M5StickCPlus.h>

static LGFX lcd;

typedef struct {
    uint32_t  length;  //Jpegデータサイズ
    uint8_t   *buf;    //Jpegデータバッファ
} jpeg_data_t;

jpeg_data_t jpeg_data;
static const int RX_BUF_SIZE = 100000;
uint8_t rx_buffer[10];

// Packetヘッダ判別用固定バイト列
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };

void setup() {
    M5.begin();
//    Serial.begin(9600);
//    Serial2.begin(115200, SERIAL_8N1, 21, 22); //M5Stack
    Serial2.begin(115200, SERIAL_8N1, 32, 33); //M5StickC
 
    jpeg_data.buf = (uint8_t *) malloc(sizeof(uint8_t) * RX_BUF_SIZE);

    lcd.init();
    lcd.setRotation(0);
    lcd.setBrightness(255);
    lcd.setColorDepth(16);
    lcd.clear();
}

void loop() {
    if (Serial2.available()) {
        jpeg_data.length = 0;

        int rx_size = Serial2.readBytes(rx_buffer, 10);
        if (rx_size == 10) {
          if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
            jpeg_data.length = (uint32_t)(rx_buffer[4] << 16) | (rx_buffer[5] << 8) | rx_buffer[6];
            rx_size = Serial2.readBytes(jpeg_data.buf, jpeg_data.length);
            lcd.drawJpg(jpeg_data.buf, jpeg_data.length);
//            lcd.drawJpg(jpeg_data.buf, jpeg_data.length,0, 0, 128, 240, 0, 0, ::JPEG_DIV_NONE);  //StickCPlus:128*240
          }
        }
    }
}
```

# 動作イメージ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">M5UnitVとM5StickCPlus繋いでYolo v2で顔認識した結果をLCDに出力してみた。<br>25ms前後で推論できてますね。<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://twitter.com/hashtag/M5UnitV?src=hash&amp;ref_src=twsrc%5Etfw">#M5UnitV</a> <a href="https://t.co/nxjaWiTcNH">pic.twitter.com/nxjaWiTcNH</a></p>&mdash; Nabeshin (@desmoquattro996) <a href="https://twitter.com/desmoquattro996/status/1560064309262430208?ref_src=twsrc%5Etfw">August 18, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>