ESP32のカメラでドラえもんを撮影しAmazon Lookout for Visionで異常箇所を検知する

# はじめに
[安価なカメラ付きESP32デバイス](https://www.amazon.co.jp/Freenove-ESP32-WROVER-%E4%BB%98%E3%81%8D%E3%83%9C%E3%83%BC%E3%83%89%EF%BC%88Arduino-IDE%E3%81%A8%E4%BA%92%E6%8F%9B%E6%80%A7%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%EF%BC%89%E3%80%81C-Python%E3%82%B3%E3%83%BC%E3%83%89%E3%80%81%E8%A9%B3%E7%B4%B0%E3%81%AA%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%E3%80%81%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/dp/B09BC5CNHM)を購入して、[前々回](https://qiita.com/tsugunao/items/e6058df682728a94ef40)は、AWS IoT Coreへの接続、[前回](https://qiita.com/tsugunao/items/8f345e1843b7b46543c4)はOTAによるファームウエアのアップデートを試してみましたが、せっかくカメラがついているので、カメラを使って何かやってみようということで、カメラから静止画を取り込み、その静止画を使って機械学習を行い、異常を検知するということを行ってみました。[Amazon Lookout for vision](https://aws.amazon.com/jp/lookout-for-vision/)というコンピュータービジョンを使用して工場等で製品欠陥を検出し、品質検査を自動化するサービスがあるので、これを使って、ドラえもんの異常箇所を検知してみます。

# 準備
今回使用するハードウエアはESP32にカメラが付いた Freenove ESP32-WROVER CAMボードという基板です。
![board.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/3fc63689-1365-4832-ae63-a9ddaaaf4034.jpeg)
前回、前々回で使用した以下の環境を引き続き使用します。
* [ESP-IDFのツールチェーン](https://docs.espressif.com/projects/esp-idf/en/release-v4.4/esp32/get-started/index.html)
* [esp-aws-iot](https://github.com/espressif/esp-aws-iot/tree/release/beta)上のOTA用サンプルコード
* [以前の手順](https://qiita.com/tsugunao/items/e6058df682728a94ef40)で作成したデバイス証明書、Thing, ポリシー

また、今回Cameraを使用するので、[こちら](https://github.com/espressif/esp32-camera)の esp32 camera driverを使用します。こちらの [Examples](https://github.com/espressif/esp32-camera/tree/master/examples) を参考にカメラの設定や初期化、カメラからの画像の取得を行っていきます。

# カメラから静止画を取得して AWS IoT Core経由でS3にアップロードする

## デバイス側のプログラムの作成
[以前の記事](https://qiita.com/tsugunao/items/e6058df682728a94ef40)でも使用した[esp-aws-iot](https://github.com/espressif/esp-aws-iot/tree/release/beta)のコードをクローンしてきます。```esp-aws-iot/example/mqtt/tls_mutual_auth```以下に、以前の記事でサンプルとしてデバイスからAWS IoT Coreへの接続を行ったソースコードがあるので、今回はそのソースコードをもとに改造していきます。

```
cd esp-aws-iot/examples/mqtt
cp -rf tls_mutual_auth mqtt_cam
```
としてプログラムをまるごとコピーし、もともと``tls_mutual_auth` というプロジェクト名で設定されていた箇所（CmakeList.txtなど)を```mqtt_cam```の名前で置き換えます。次に、mqtt_cam のディレクトリ以下に components というディレクトリを作成し、そこに [esp32-camera](https://github.com/espressif/esp32-camera)をクローンしてきます。
```
cd mqtt_cam
mkdir components
cd components
git clone https://github.com/espressif/esp32-camera.git
```
```esp-aws-iot/examples/mqtt/mqtt_cam/main/CMakeLists.txt``` に以下のように ```components``` ディレクトリ以下をインクルードパスに含まれるように指定します。
```
set(COMPONENT_ADD_INCLUDEDIRS
	"."
+	"./../components/"
	"${CMAKE_CURRENT_LIST_DIR}"
	)
```
```app_main.c```, ```mqtt_demo_mutual_auth.c```をesp32-cameraの例を参考にして以下のように書き換えます。

app_main.c
```c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"

#include "esp_log.h"

#include "esp_camera.h"

int aws_iot_demo_main( int argc, char ** argv );

static const char *TAG = "MQTT_EXAMPLE";

#define BOARD_WROVER_KIT 1

// WROVER-KIT PIN Map
#ifdef BOARD_WROVER_KIT

#define CAM_PIN_PWDN -1  //power down is not used
#define CAM_PIN_RESET -1 //software reset will be performed
#define CAM_PIN_XCLK 21
#define CAM_PIN_SIOD 26
#define CAM_PIN_SIOC 27

#define CAM_PIN_D7 35
#define CAM_PIN_D6 34
#define CAM_PIN_D5 39
#define CAM_PIN_D4 36
#define CAM_PIN_D3 19
#define CAM_PIN_D2 18
#define CAM_PIN_D1 5
#define CAM_PIN_D0 4
#define CAM_PIN_VSYNC 25
#define CAM_PIN_HREF 23
#define CAM_PIN_PCLK 22

#endif

#if ESP_CAMERA_SUPPORTED
static camera_config_t camera_config = {
    .pin_pwdn = CAM_PIN_PWDN,
    .pin_reset = CAM_PIN_RESET,
    .pin_xclk = CAM_PIN_XCLK,
    .pin_sccb_sda = CAM_PIN_SIOD,
    .pin_sccb_scl = CAM_PIN_SIOC,

    .pin_d7 = CAM_PIN_D7,
    .pin_d6 = CAM_PIN_D6,
    .pin_d5 = CAM_PIN_D5,
    .pin_d4 = CAM_PIN_D4,
    .pin_d3 = CAM_PIN_D3,
    .pin_d2 = CAM_PIN_D2,
    .pin_d1 = CAM_PIN_D1,
    .pin_d0 = CAM_PIN_D0,
    .pin_vsync = CAM_PIN_VSYNC,
    .pin_href = CAM_PIN_HREF,
    .pin_pclk = CAM_PIN_PCLK,

    //XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
    .xclk_freq_hz = 20000000,
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_JPEG, //YUV422,GRAYSCALE,RGB565,JPEG
    .frame_size = FRAMESIZE_SVGA,    //QQVGA-UXGA, For ESP32, do not use sizes above QVGA when not JPEG. The performance of the ESP32-S series has improved a lot, but JPEG mode always gives better frame rates.

    .jpeg_quality = 12, //0-63, for OV series camera sensors, lower number means higher quality
    .fb_count = 1,       //When jpeg mode is used, if fb_count more than one, the driver will work in continuous mode.
    .grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};

static esp_err_t init_camera(void)
{
    //initialize the camera
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "Camera Init Failed");
        return err;
    }

    return ESP_OK;
}
#endif

/*
 * Prototypes for the demos that can be started from this project.  Note the
 * MQTT demo is not actually started until the network is already.
 */

void app_main()
{
    ESP_LOGI(TAG, "[APP] Startup..");
    ESP_LOGI(TAG, "[APP] Free memory: %"PRIu32" bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());

    esp_log_level_set("*", ESP_LOG_INFO);
    
    /* Initialize NVS partition */
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        /* NVS partition was truncated
         * and needs to be erased */
        ESP_ERROR_CHECK(nvs_flash_erase());

        /* Retry nvs_flash_init */
        ESP_ERROR_CHECK(nvs_flash_init());
    }
    
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

#if ESP_CAMERA_SUPPORTED
    if(ESP_OK != init_camera()) {
        return;
    }
#endif

    aws_iot_demo_main(0,NULL);
}

```
ボード上のカメラの設定と初期化。ここでは解像度を SVGA に設定し JPEG のフォーマットを指定しています。試してみたところと、このぐらいの解像度で JPEGの場合は1つの静止画のファイルサイズは20-30KB程度になるようで、AWS IoT Coreで送れる上限である 128KB で十分に送れるようです。それ以上のファイルサイズになった場合には、[credential provider経由で認証情報を取得して直接S3に送信する](https://aws.amazon.com/jp/blogs/news/aws-iot-core-credential-provider/)、というような構成にするのが良さそうです。

mqtt_demo_mutual_auth.c では、もともとのは1秒に1回 ```publishToTopic```関数を呼び、その中で "hello World"の文字列をペイロードとして設定してPublishしていましたが、この関数を改造して、文字列のかわりにペイロードに静止画を入れるようにします。このようにすることで、ESP32のデバイスから1秒に1回、静止画が取得されて指定したMQTT TopicにPublishされるようになります。
```c
tatic int publishToTopic( MQTTContext_t * pMqttContext )
{
    int returnStatus = EXIT_SUCCESS;
    MQTTStatus_t mqttStatus = MQTTSuccess;
    uint8_t publishIndex = MAX_OUTGOING_PUBLISHES;

    assert( pMqttContext != NULL );

    /* Get the next free index for the outgoing publish. All QoS1 outgoing
     * publishes are stored until a PUBACK is received. These messages are
     * stored for supporting a resend if a network connection is broken before
     * receiving a PUBACK. */
    returnStatus = getNextFreeIndexForOutgoingPublishes( &publishIndex );

    if( returnStatus == EXIT_FAILURE )
    {
        LogError( ( "Unable to find a free spot for outgoing PUBLISH message.\n\n" ) );
    }
    else
    {

        camera_fb_t *pic = esp_camera_fb_get();
        if(!pic) {
            printf( "Frame buffer could not be acquired");
        return returnStatus;
        }

        LogInfo( ( "publish image data. %d %u \n\n", (int)pic->len, (unsigned int)pic->buf ) );
        

        /* This example publishes to only one topic and uses QOS1. */
        outgoingPublishPackets[ publishIndex ].pubInfo.qos = MQTTQoS1;
        outgoingPublishPackets[ publishIndex ].pubInfo.pTopicName = MQTT_EXAMPLE_TOPIC;
        outgoingPublishPackets[ publishIndex ].pubInfo.topicNameLength = MQTT_EXAMPLE_TOPIC_LENGTH;
        //outgoingPublishPackets[ publishIndex ].pubInfo.pPayload = MQTT_EXAMPLE_MESSAGE;
        //outgoingPublishPackets[ publishIndex ].pubInfo.payloadLength = MQTT_EXAMPLE_MESSAGE_LENGTH;
        outgoingPublishPackets[ publishIndex ].pubInfo.pPayload = pic->buf;
        outgoingPublishPackets[ publishIndex ].pubInfo.payloadLength = pic->len;

        /* Get a new packet id. */
        outgoingPublishPackets[ publishIndex ].packetId = MQTT_GetPacketId( pMqttContext );

        /* Send PUBLISH packet. */
        mqttStatus = MQTT_Publish( pMqttContext,
                                   &outgoingPublishPackets[ publishIndex ].pubInfo,
                                   outgoingPublishPackets[ publishIndex ].packetId );

        if( mqttStatus != MQTTSuccess )
        {
            LogError( ( "Failed to send PUBLISH packet to broker with error = %s.",
                        MQTT_Status_strerror( mqttStatus ) ) );
            cleanupOutgoingPublishAt( publishIndex );
            returnStatus = EXIT_FAILURE;
        }
        else
        {
            LogInfo( ( "PUBLISH sent for topic %.*s to broker with packet ID %u.\n\n",
                       MQTT_EXAMPLE_TOPIC_LENGTH,
                       MQTT_EXAMPLE_TOPIC,
                       outgoingPublishPackets[ publishIndex ].packetId ) );
        }
        esp_camera_fb_return(pic);
    }

    return returnStatus;
}
```
ターミナルに戻り、```idf.py menuconfig```を行い、以前行ったように、Wifiの設定や、MQTT Brokerのエンドポイントが設定されていることを確認します。
![スクリーンショット 2023-06-08 16.38.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/b2e69e4f-f2a4-9ecc-371d-239f2a010205.png)

あとは、 ビルドしてflashしてMonitorします。

```
idf.py -p /dev/cu.usbserial-14240 build flash monitor
```
プログラムが実行され、以下のようなlogが表示されます
```
I (7553) coreMQTT: Short delay before starting the next iteration....

I (12563) coreMQTT: Establishing a TLS session to a3iwv27yeq472m-ats.iot.ap-northeast-1.amazonaws.com:8883.
I (15433) coreMQTT: MQTT connection established with the broker.
I (15433) coreMQTT: MQTT connection successfully established with broker.


I (15433) coreMQTT: An MQTT session with broker is re-established. Resending unacked publishes.
I (15443) coreMQTT: Subscribing to the MQTT topic esp32-thing01/example/topic.
I (15453) coreMQTT: SUBSCRIBE sent for topic esp32-thing01/example/topic to broker.


I (15583) coreMQTT: Subscribed to the topic esp32-thing01/example/topic. with maximum QoS 1.


I (15583) coreMQTT: Sending Publish to the MQTT topic esp32-thing01/example/topic.
I (15593) coreMQTT: publish image data. 35039 1065355456


E (20313) coreMQTT: sendMessageVector: Unable to send packet: Network Error.
E (20313) coreMQTT: MQTT PUBLISH failed with status MQTTSendFailed.
E (20313) coreMQTT: Failed to send PUBLISH packet to broker with error = MQTTSendFailed.
I (26323) coreMQTT: Delay before continuing to next iteration.
```


## AWS IoT Core側の設定

AWS IoT Coreのコンソールで、MQTTテストクライアントにて該当するTopicをサブスクライブすると何らかのデータが送付されてきていることが見て取れます。(うまく表示されません)
![スクリーンショット 2023-06-08 16.46.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/2ea85400-ce0a-d3a5-c492-7458646657b3.png)

次に、AWS IoT Coreのルールエンジンを設定して、送られてきた静止画をそのままS3に転送するように設定します。AWS IoT Coreのコンソール上、左側の```メッセージのルーティング```-> ```ルール``` から```ルールの作成```をクリックします。任意のルール名を設定して、```次へ```をクリックし、SQLステートメントの設定の画面で、以下のように該当するTopicに来たものすべてを転送するように設定します。
![スクリーンショット 2023-06-08 16.55.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/80a41bf2-0206-c9ed-a031-6fc3c4125034.png)
次のページでルールアクションを設定しますが、今回はS3にデータを転送したいので、アクションとして```S3 Bucket```を選択し、S3上に適当なBucketを作成した上で、そのバケットを指定します。キーの部分には、このデータがS3上に保存されるときのキーを設定しますが、ここでは```${timestamp()}.jpg```のようにタイムスタンプがファイル名になるように設定しました。新しいIAMロールを作成して、 ```次へ```をクリックしルールを作成します。
![スクリーンショット 2023-06-08 19.12.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/84acc116-5eec-0b6e-7666-c7e76de08018.png)

ルールが作成されると、ESP32のデバイスからAWS IoT Coreに送られてきた静止画データがルールアクションによってS3に保存されます。S3を見ると以下のようにJPGファイルが格納されています。
![スクリーンショット 2023-06-08 19.16.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/ba33f7cd-084b-a6b3-e117-987fe070f7a0.png)
中身を確認すると、以下のように、カメラから取得された画像を見ることができます。
![sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/57298208-c037-2532-20f5-f8b27ff2c223.jpeg)


# S3にアップロードされたデータをもとにAmazon Lookout for Visionで異常箇所を検知する
これでESP32で1秒おきに撮影した画像をS3に保存することができるようになりました。次に、工場での画像による異常判別を想定して、撮影された静止画をもとに品質チェックを行いたいと思います。ターゲットは何でも良いのですが、今回は、下の写真にあるようなドラえもんを用いて、笑って手を上げている左側のドラえもんを正常のドラえもん、怒った顔をして空気砲を構えているドラえもんを異常ケースのドラえもんとして実験してみます。
![Image from iOS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/26683f59-1904-7c95-64af-7d086e1b7d54.jpeg)

ドラえもんがベルトコンベアに乗って流れてくることを想像して、たまに怒った顔をした異常ケースのドラえもんが流れてくるという想定で、1秒毎にドラえもんを撮影し、データをS3上に貯めておきます。
Amazon Lookout for Vision では、S3から学習のためのデータを取り込むことができますが、S3上のフォルダ名で、normal/ のフォルダに正常ケースのデータ、anomaly/ のフォルダに異常ケースのデータがあると自動で正常・異常のラベル付けをしてくれるので、S3上に取り込んだデータを、笑ったドラえもんは normal/ フォルダに、怒ったドラえもんは anomaly/ フォルダに移動しておきます。後ほどテストを行うので、正常ケースと異常ケースを混ぜたデータを testdata/ というフォルダを作成しそこに移動しておきます。次に、Amazon Lookout for Visionのコンソールを開き、```プロジェクトを作成```をクリックし、適当なプロジェクト名をつけます。
![スクリーンショット 2023-06-08 19.35.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/6b98d01c-4b06-3a0d-d909-757b2535eb6e.png)
```データセットを作成```、をクリックし、設定オプションとして、```1つのデータセットを作成する```を選択し、イメージソースとして、```S3 バケットからイメージをインポートする```を選択し、先程作成して画像データを置いてあるS3バケットを指定します。自動ラベル付けのチェックを付けて、```データセットを作成```をクリックします。
![スクリーンショット 2023-06-09 14.43.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/86803b47-1b0b-6e6a-29a7-7a43d1922f52.png)

しばらくすると、以下のように自動的にラベル付けされた状態になります。もしまだラベル付けされていない画像があれば、ここで手動でNormal/Anomalyのラベルをつけることもできます。
![スクリーンショット 2023-06-09 14.47.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/ee5f0ab3-5c45-9461-b9b9-b30245112b30.png)
画像分類モデルを作成するためには、通常の画像が20枚以上、異常なオブジェクトの画像が少なくとも10枚は必要なようです。モデルは、画像を正常/異常に分類する```画像分類モデル```と画像の異常領域をピクセルマスクで設定して異常の種類を示す```画像セグメンテーションモデル```の2つのモデルを選択することができます。まず、画像の分類モデルでモデルを作成するには、右上にある、```モデルのトレーニング```ボタンをクリックします。トレーニングにはおおよそ25分程度かかりました。モデルの作成が終了すると、以下のように、モデルのパフォーマンスメトリクスと、それぞれの画像がどのように分類されたのかが表示されます。
![スクリーンショット 2023-06-09 15.02.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/1457689f-d0d9-9dd6-7f5c-88ec829792c6.png)

上の方にある、```トライアル検出を実行```をクリックすると、作成されたモデルを用いて正しく画像を分類できるかどうかテストすることができます。```タスク名```を入力し、S3上で先程テスト用に用意しておいたデータを選択し、```異常を検出```をクリックします。

![スクリーンショット 2023-06-09 15.08.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/86a69d72-84bb-f0b4-e294-024b1a4add0a.png)

しばらくすると検証が終了し、以下のように表示されます。
![スクリーンショット 2023-06-09 15.11.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/a653eab5-d209-d800-7de6-457c2d931212.png)
```マシン予測を検証```ボタンをクリックすると、予測結果を検証することができます。図のように、予測としては```正常```となっているが本来は異常であるべき（ここでは怒った空気砲を構えたドラえもんは異常としている）ものがあった場合は、誤りであることを指摘します。
![スクリーンショット 2023-06-09 15.13.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/8bd7123d-01b9-7280-857f-f1d688d84b4b.png)
検証した結果、```検証済みイメージをデータセットに追加する```をクリックすると、更新されたデータセットでモデルを再トレーニングすることができます。

異常データについてはさらに細分化したラベルを独自定義してセグメンテーション（領域検出）が実行できます。セグメンテーションなので画像内の異常エリアを正解づけするアノテーションが必要です。プロジェクトの左のナビゲーションペインで、```データセット```を選択し、Anomalyと判定されている画像を選択し、```Add anomaly labels```をクリックし、どの領域についての異常であるのかを追加します。
![スクリーンショット 2023-06-09 15.34.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80028/dba5ae05-af4b-94c6-8bcb-40b67134efef.png)
その後再度モデルをトレーニングすることで、モデルの品質を向上させることができます。


# まとめ
ESP32のカメラをつかって静止画を撮影し、Amazon Lookout for Visionで学習を行って異常箇所を検知する実験を行いました。このような技術を使って、例えば工場内での基盤のはんだ付けのクラックを検知したりすることができるようになります。今回使用したのは安価なデバイスなので解像度もそこそこですが、ある程度使用できるイメージは湧きました。