3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

逆光で見えない物体をAITRIOSで自動検出できるようにしてみた

Last updated at Posted at 2025-09-29

こんにちは!
SSSの山根です。

今回はAITRIOSEdge Applicationを使って、逆光で見えなくなったペットボトルを自動的に見えるようにエッジデバイスに画質調整をさせて検出させてみたので紹介します。

カメラとAIを使ったIoTで想定される問題

AIの推論精度を保つにはカメラの画質調整が重要になります。
しかし、撮影環境は一定ではなく、特に野外であれば日の傾きによって見え方が大きく異なるので、AIの推論精度に大きく影響します。

例えば、逆光で車のナンバープレートが認識できないような状況が想定されます。
逆光でナンバープレートが認識できない
(この画像はMicrosoft Copilotを使用して生成しました。)

また、カメラの画質調整を1台ずつ手作業で実施しないといけないのでスケールアップが難しいということも問題として挙げられます。

Edge Applicationで動的に変わる画質に対処

AITRIOSのエッジデバイス上で動くEdge Application
AIの出力を課題解決に使える形に変換・送信する、エッジデバイス上の処理アプリです。

また、Edge Applicationはエッジデバイスのセンサを制御することができるので、例えばカメラの画質調整ができます。
これによって、Edge ApplicationがAIの推論結果を基にカメラの画質を調整することができます。

つまり、人の手を介さずに能動的にエッジデバイスが画質を補正することができます。

今回は、実際にそんなことが可能なのか、サンプルの物体検出用Edge Applicationを改変して、逆光下の瓶を検出できるのか実験してみました。
その結果、次のように瓶が検出できるようになりました。
瓶で試した結果

用意したもの

  • CSV26 (有線/PoEモデル)
    エッジファームウェアをV2にする必要があります。V2にする手順はこちらを参照してください。
  • PoE スイッチ、インターネットと接続できる回線、LANケーブル
  • ペットボトル
  • Ubuntu 22.04のPC

Edge Applicationの準備

開発環境の準備

AITRIOSEdge ApplicationのサンプルはこちらのGitHubリポジトリで公開されています。

sample_appsというディレクトリにサンプルのEdge Applicationのコードが用意されています。

今回はdetectionという物体検出向けのサンプルを改変して、逆光で見えなくなったペットボトルを自動で検出するEdge Applicationを作ります。

まずは、Edge Application SDKのリポジトリをcloneします。

git clone https://github.com/SonySemiconductorSolutions/aitrios-sdk-edge-app.git

サブモジュールを取り込む為に次のコマンドを実行します。

git submodule update --init --recursive

プロジェクトのトップディレクトリで次のmakeコマンドを実行するとdetectionEdge Applicationedge_app.wasmとしてビルドされます。

make CMAKE_FLAGS="-DAPPS_SELECTION=detection"

トップディレクトリにbinディレクトリが作成されて、その中にedge_app.wasmが格納されます。

サンプルアプリとして、そのまま使いたい場合はこのビルドされたWasmをConsoleへインポートすることで物体検出用のアプリとして使えます。

Edge Applicationの実装方法

コードを修正する前にEdge Applicationの実装方法について簡単に説明します。

Edge Applicationには主に次の2つの状態があります。

  1. stopped : 停止している状態
  2. running : 稼働している状態

そして、この2つの状態を遷移する過程で各種コールバック関数が呼ばれます。
呼ばれるコールバック関数はsm.cppに定義されています。

なので、Edge Applicationを独自に実装する際、主に修正が必要になるのはsm.cppになります。
sm.cppには6つのコールバック関数が定義されており、それらを変更すれば独自のEdge Applicationを実装できます。

6つのコールバック関数の中で特に重要なのが、onIterate()です。

onIterate()は上に書いたrunningの状態の時に繰り返し実行される関数です。
onIterate()の中の処理は次のようになっています。

int onIterate(){
  // センサデータの取得
  SensorGetFrame(s_stream, &frame, SENSOR_GET_FRAME_TIMEOUT);
  //Input Tensorの取得~送信
  sendInputTensor(&frame);
  //Output Tensorの取得~解析~送信
  sendMetadata(&frame);
  // センサデータの解放
  SensorReleaseFrame(s_stream, frame);
}

コードの改変

今回改変したコードの内容はこちらのdetection用サンプルアプリの実装をベースに参照してください。

改変したポイントを説明します。

DataProcessorAnalyze()を画質調整できるように変更

先ほど書いたように、主にsm.cppを変更するので、sm.cppから説明します。
今回は、sm.cpponIterate()の中に次のような変更を加えました。

-  DataProcessorResultCode data_processor_ret = DataProcessorAnalyze(
-      (float *)data.address, data.size, (char **)&metadata, &metadata_size);
+  DataProcessorResultCode data_processor_ret =
+      DataProcessorAnalyze((float *)data.address, data.size, (char **)&metadata,
+                           &metadata_size, s_stream);

sm.cppに対する変更はこれだけです。

この変更は、DataProcessorAnalyze()という関数の引数にEdgeAppLibSensorStreamを追加しています。
detectionのサンプルでは、DataProcessorAnalyze()という関数の中でIMX500から出力されるfloatの1次元配列を解釈して処理しています。
今回は画質調整をこのDataProcessorAnalyze()の中で実行する為にs_streamを引数として追加しました。

元々のサンプルのDataProcessorAnalyze()の中の処理は次のようになっています。

/**
 * @param in_data IMX500が出力するfloatの1次元配列
 * @param in_size IMX500が出力するfloatの1次元配列のサイズ
 * @param out_data JSON又はFlatBuffers形式の処理結果
 * @param out_size 処理結果のサイズ
 */
DataProcessorResultCode DataProcessorAnalyze(float *in_data, uint32_t in_size,
                                             char **out_data,
                                             uint32_t *out_size) {
  // floatの1次元配列を物体検出の結果として解釈し、detections配列とする (検出結果のスコアやクラス等を抽出)
  Detections *detections = CreateDetections(in_data, in_size, analyze_params);

  // Configurationで設定した値 (analyze_params)を基に検出結果のdetectionsをフィルタリング
  // 例えば、検出結果から閾値を超える検出結果のみがdetections配列に格納されるようになる
  FilterByParams(&detections, analyze_params);

  // Configurationで指定した値に基づいてデータをフォーマット
  // 例えば、JSON形式でデータを送る際には次が実行される
  JSON_Value *tensor_output = MakeDetectionJson(detections);
}

AIの推論結果は1次元配列のままでは扱いにくいので、次のような構造体のデータを要素とするDetections配列として解釈し、
FilterByParams()関数で送信するデータの抽出を行っています。

typedef struct {
  uint16_t class_id; // 検出された物体の判定クラス
  float score; // 検出結果のスコア
  BBox bbox; // 検出結果の座標
} DetectionData;

SetAeMeteringByClass()関数の追加

今回はFilterByParams()の後にdetections配列を基に画質調整をする関数SetAeMeteringByClass()を置くことにしました。
これによって、フィルタリングされた検出結果を使って、露光時間の設定ができます。
実際に定義した関数は次です。

void SetAeMeteringByClass(Detections **detections, uint16_t target_class_id,
                          DataProcessorCustomParam detection_param,
                          EdgeAppLibSensorStream s_stream){
  // Search the target class_id in the detections
  DetectionData *target_detection = NULL;
  for (uint16_t i = 0; i < (*detections)->num_detections; i++) {
    if ((*detections)->detection_data[i].class_id == target_class_id) {
      LOG_DBG("Setting AE metering to class_id %u", target_class_id);
      target_detection = &(*detections)->detection_data[i];
      break;
    }
  }

  // Set Auto Exposure metering based on the target detection
  EdgeAppLibSensorCameraAutoExposureMeteringProperty ae_metering = {};
  ae_metering.mode =
      AITRIOS_SENSOR_CAMERA_AUTO_EXPOSURE_METERING_MODE_USER_WINDOW;
  if (target_detection != NULL) {
    metering_cnt = 0;
    keep_cnt = KEEP_CNT;

    // bbox coordinates are in input_width X input_height, convert to sensor
    // coordinates (2028x1520)
    ae_metering.left = static_cast<uint16_t>(target_detection->bbox.left /
                                             detection_param.input_width *
                                             camera_image_size.width);
    ae_metering.top = static_cast<uint16_t>(target_detection->bbox.top /
                                            detection_param.input_height *
                                            camera_image_size.height);
    ae_metering.right = static_cast<uint16_t>(target_detection->bbox.right /
                                              detection_param.input_width *
                                              camera_image_size.width);
    ae_metering.bottom = static_cast<uint16_t>(target_detection->bbox.bottom /
                                               detection_param.input_height *
                                               camera_image_size.height);

  } else {
    if (keep_cnt > 0) keep_cnt--;
    if (metering_cnt < UINT64_MAX) {
      metering_cnt++;
    } else {
      metering_cnt = 0;
    }
    // If the target is not found, the AE metering area will be moved left and
    // right at the default size every frame.
    ae_metering.left =
        (camera_image_size.width - default_metering_size) * (metering_cnt % 2);
    ae_metering.top = (camera_image_size.height - default_metering_size) / 2;
    ae_metering.right =
        (camera_image_size.width - default_metering_size) * (metering_cnt % 2) +
        default_metering_size;
    ae_metering.bottom = (camera_image_size.height + default_metering_size) / 2;
  }
  int32_t res = 3;
  if (keep_cnt == 0) {
    res = EdgeAppLib::SensorStreamSetProperty(
        s_stream, AITRIOS_SENSOR_CAMERA_AUTO_EXPOSURE_METERING_PROPERTY_KEY,
        &ae_metering, sizeof(ae_metering));
  }

  if (res < 0) {
    LOG_ERR("SensorStreamSetProperty(AutoExposureMeteringProperty) failed.");
  } else if (res == 0) {
    EdgeAppLibSensorCameraAutoExposureMeteringProperty ae_metering_state = {};
    int32_t res = EdgeAppLib::SensorStreamGetProperty(
        s_stream, AITRIOS_SENSOR_CAMERA_AUTO_EXPOSURE_METERING_PROPERTY_KEY,
        &ae_metering_state, sizeof(ae_metering_state));
    LOG_INFO(
        "AE metering state: mode=%d, left=%d, top=%d, "
        "right=%d, "
        "bottom=%d\n",
        ae_metering_state.mode, ae_metering_state.left, ae_metering_state.top,
        ae_metering_state.right, ae_metering_state.bottom);
  } else {
    LOG_INFO(
        "SensorStreamSetProperty(AutoExposureMeteringProperty) skipped because "
        "keep_cnt=%d >0.",
        keep_cnt);
  }
}

この関数はtarget_class_idで指定した物体と一致する物体が検出された時、その物体の検出領域を自動露光時間の検波枠として設定するものです。

今回はtarget_class_id43を設定することで、bottleを検出したら、その検出領域を自動露光の検波枠として設定します。

uint16_t target_class_id = 43;  // 43 == bottle

今回はグローバル変数として
sample_apps/detection/data_processor/src/detection_data_processor.cpp
に宣言しました。

また、bottle検出時に毎フレームに対してこの検波枠設定をするとあまり安定しなかったので、keep_cntという変数を定義して、1度bottleを検出したら3フレームは露光時間を調整しないようにしました。

#define KEEP_CNT 3
uint8_t keep_cnt = KEEP_CNT;

こちらはsample_apps/detection/data_processor/src/detection_utils.cppにグローバル変数として宣言しました。

また、暗いところに検出したい物体を置くだけでは、相変わらず暗いままで検出できないので、bottleを検出できない場合は左右の領域を交互に検波枠として設定することでbottleを探すような処理を入れました。

  // If the target is not found, the AE metering area will be moved left and
  // right at the default size every frame.
  ae_metering.left =
      (camera_image_size.width - default_metering_size) * (metering_cnt % 2);
  ae_metering.top = (camera_image_size.height - default_metering_size) / 2;
  ae_metering.right =
      (camera_image_size.width - default_metering_size) * (metering_cnt % 2) +
      default_metering_size;
  ae_metering.bottom = (camera_image_size.height + default_metering_size) / 2;

Edge ApplicationConsoleを使ってデプロイ

プロジェクトのトップディレクトリで次のmakeコマンドを実行してedge_app.wasmを生成します。

make CMAKE_FLAGS="-DAPPS_SELECTION=detection"

こちらの手順を元に生成したedge_app.wasmとDTDL、manifest.jsonを1つのフォルダに格納してzip化します。

今回はサンプルのdetectionを改変しており、特にConfigurationに関連する変更はしていないので、こちらdetection用のファイルがそのまま使えます。

もしもインポートがうまくいかない場合は、こちらのAssetsにあるsample_edge_app_detection_wasm_v2_x.x.x.zipを参考にしてください。

zip化したらConsoleを開いて、Edge Device SWからEdge Applicationのタブを開いてImportします。
その後、Consoleから自分のデバイスにEdge Applicationをデプロイします。

前回の記事にImportからデプロイするまでの手順を記載しているので、参照してください。  

AIモデルの準備

AIモデルはCOCOのデータセットで学習させたSSD MobileNetを使いました。今回のEdge Applicationのコードはbottleclass_idをハードコードしているので、使用するAIモデルを間違えないように注意します。

撮影環境の準備

今回は窓際にダンボールとペットボトルを用意して逆光で画面右側のエリアが見えなくなるシチュエーションを作りました。
逆光で見えないシチュエーション

最初はペットボトルを置かないことで、Edge Applicationが露光時間を調整してペットボトルを探す様子を確認したいと思います。

推論開始

推論の開始方法は前回の記事と同様なので、前回の記事を参照ください。

結果の確認

まずペットボトルを置かない時間の画像は次のような画像が交互に撮影される期待通りの結果になりました。

黒い画像 白い画像

ペットボトルが見つからないので、Edge Applicationが露光時間調整の為の検波枠を左右の領域に振ることで、露光時間を変更していることが分かります。
検波枠は次の画像の赤い斜線部分に設定されています。

ae_area image

左側のような画像で暗く見える領域を検波枠として露光時間を調整するので、次回撮影されるフレームで右側のような明るい画像が撮影されます。

しかし、今度は画面左側の明るく見える領域を検波枠として露光時間を調整するので、再び暗く見える左側のような画像が撮影されます。

Consoleで推論結果を画像に重畳した結果を見ると次のような結果が交互に確認できました。

黒い画像に対する結果重畳 白い画像に対する結果重畳

暗い見た目の画像では誤検出がありますが、ペットボトル(class_id==43)は検出されていないことが分かります。

ペットボトルを置いてみたところ、意外にも暗いエリアで検出されてしまいました。
ペットボトルの検出

しかし、これを契機に、Edge Applicationはこの検出された領域を自動露光の検波枠に設定します。次のフレームでは露光時間が調整されて、よりペットボトルがはっきりと見えるようになっていました。
ペットボトルの検出

Consoleの描画ではclass_id==85の花瓶が一番上に記載されていますが、73 %の確率でclass_id==43bottleが検出されていました。

よって、さきほどよりも検出確率が大幅に向上していることが分かります。

その後も、次のように誤検出があるものの、73 %程度の高い検出精度でペットボトルが検出されるようになりました。
ペットボトルの検出後のフレーム

Consoleで描画された画像をダウンロードしてGIFにしてみました。
推論結果をGIFにした結果

初めの方はペットボトルが見つからず、露光時間を調整しているので、白くなったり黒くなったりしています。
その後、ペットボトルが置かれた後は、安定的にペットボトルが検出されていました。

別の日に瓶を置いて実験した際の結果も紹介します。
瓶で試した結果

こちらはkeep_cntを5にしています。
また、Consoleでの描画結果は使わずに自分でpythonのコードを書いてbottleのクラスだけ枠を描画するようにしました。

1度暗い状態で検出されて、keep_cntによって画質が保持されるものの、連続して検出できなかった為に、また露光時間を調整してbottleを探します。
探し始めてすぐにbottleが見つかったので以降、安定して検出されるようになったようです。

まとめと所感

今回はサンプルのEdge Applicationの実装方法を紹介しながら実際にコードを改変して、暗くて見えなくなった物体を検出できるようにしてみました。
サンプルコードを少し変えるだけで、意外と面白いものができたなと感じました。

本題とは逸れるのですが、IMX500の性能が良い為か、暗くて人の目では物体が見えないように思われても、意外にも物体を検出できてしまうことに驚きました。
ただし、検出精度は低く安定はしなかったので、今回の改変によって安定的に目的の物体を検出できるようになりました。今後センサやAI自体の精度が改善されても、補助的に今回のような実装を行うことで、よりロバストなシステムを構築できると思います。

気になった点として、AIは物体を正しく検出しているものの、私の感覚ではもう少し明るく露光時間を変更できるのではないかと思えたり、露光時間の変更によって画像の色味 (今回の場合は画像の右下部分) がおかしく見えたりしていることがありました。
しかし、あくまでもAIが検出した領域を基に露光時間のみを機械的に調整しているので、このようなことが起きると考えられます。
一方で、AIの推論精度は露光時間の調整によって改善することが確かめられました。これによってAIの推論精度を画質の変化に対してロバストに保つことができます。エッジAIソリューションを構築する上で、必ずしも人にとってキレイと思える画像を撮ることが目的ではないということは留意しておきたいポイントです。

エッジデバイスで状況に応じて能動的に画質調整することでAIの精度を改善できるのはAITRIOSEdge Applicationの大きな特徴で、実際に実験してみると楽しく色々とカスタマイズしたくなるので、是非皆さんにも試して頂きたいと思います。

困った時は

もし、記事の途中でうまくいかなかった場合は、気軽にこの記事にコメントをください。以下のサポートサイトもご覧ください。
コメントのお返事にはお時間を頂く可能性がありますが、ご了承ください。

また、記事の内容以外で AITRIOS についてお困りごとなどあれば以下よりお問い合わせください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?