Yuki5130 が 2025年01月31日17時33分41秒 に編集
初版
タイトルの変更
Spresenseによる転倒検出システムの試作
タグの変更
SPRESENSE
メイン画像の変更
記事種類の変更
製作品
本文の変更
# 概要 祖父母が無事に生活できているか安否を確認したいと思い、カメラを使って生活できているのか転倒などしていないか確認するシステムを作ってみました。 ## 使用部品 - Sony Spresense メインボード https://www.sony-semicon.com/ja/products/spresense/index.html - Spresense LTE拡張ボード https://developer.sony.com/ja/spresense/products/spresense-lte-ext-board - Spresense HDR カメラボード https://developer.sony.com/ja/spresense/products/spresense-hdr-camera-board ## 使用ソフト - Arduino IDE - Neural Network Console ## システム構成 まず、Spresenseメインボードに接続したHDRカメラで転倒・直立を判別するための画像データを撮影します。次に、Sony Neural Network Consoleを使用して収集した画像を基に画像分類モデルを構築し、このモデルをSpresense上で動作させるためのnnbファイルとして出力します。最後に、Spresense上にnnbファイルを組み込み、画像分類モデルを用いて転倒・直立の判別が適切に行えるか検証します。  # HDRカメラボードのセットアップ この章では、Spresenseにカメラボードを接続し、動作確認を行います。 1. カメラボードの取り付け カメラボードのフラットケーブルを、Spresenseメインボードのカメラ専用コネクタ(CN5)に正しく挿し込みます。 フラットケーブルの電極の向きを、下図のようにmicroUSBコネクタ側に向けて接続してください。  引用先:https://developer.sony.com/spresense/development-guides/introduction_ja.html 2. カメラの動作確認 Arduino IDEのメニューバーにある「ファイル」→「スケッチ例」→「Camera」→「camera」を選択し、Spresenseに書き込むとHDR Cameraで撮影した画像がSDカードに保存されます。  ## 1. 画像データセットの準備 まずは転倒状態を判別するための画像を準備します。今回は自分のQiita記事に沿って準備を進めます。 `参考サイト`:https://qiita.com/yuki5130/items/531caa08c48a78bb90cc ### 1-1 画像の撮影・収集 カメラを用いて、以下の2種類の画像を撮影し、SDカードに保存します。 - 正常な状態(直立)  - 転倒状態  ### 1-2 フォルダ構成 撮影した画像を以下のように整理します。 ```bash dataset/falling_standing/ ├── 0/ # 正常な立位の画像 │ ├── img1.jpg │ ├── img2.jpg ├── 1/ # 転倒状態の画像 │ ├── img1.jpg │ ├── img2.jpg ``` #### 注意点 - 各フォルダに最低でも100枚以上の画像を用意してください。多いほど精度が上がります. - 画像サイズがバラバラでも大丈夫です。Neural Network Console側で画像サイズをトリミングやリサイズもしてくれます。 ## 2. Neural Network Consoleのインストール 画像判別AIモデルの構築には、Neural Network Console(NNC) を使用します。自分のQiita記事に沿って準備を進めます。 `参考サイト`:https://qiita.com/yuki5130/items/5951c5f9cf4fac44327c ### 2-1 インストール方法 1. 以下のサイトからインストール用のexeファイルをダウンロードします `参考サイト`:https://techhub.developer.sony.com/ja/neural-network-console#NNC%20W%20JA 2. アカウントを作成またはGoogleアカウントでサインインします。 ## 3. データセットの登録 1. まずはNeural Network Console(以降NNCと呼ぶ)を起動します。 2. メニューバー右側にある「プロジェクトに名前を付けて保存」からプロジェクトの名前を保存します。それから 左メニューの「Dataset」タブをクリックします。  3. データセットタブ下にある「データセットを開く」を選択し、 「データセットを作成」→「を選択します。  4. 「画像分類」を選択します。  5. 「Create Dataset」という新しいタブが表示されます。ソースディレクトリに第1章で用意したデータセットの「dataset/falling_standing/」のフォルダを選択します。  6. そのほかの設定は学習させたいモデルに応じて変更することができます。 7.「適用」を押すと、学習用とテスト用データセットが作成されます。 ## 4. Neutal Network Consoleを使った 画像分類モデルの構築 ### 4-1 ネットワークの構築 - 以下の画像に示す構成でネットワークを組みます。 - 今回は画像による分類を行うために、2クラス分類を行う単純な畳み込みニューラルネットワークを構築します。各層の説明は今回省きます。 - このソフトを使ってモデルを構築するにあたって、重要なポイントはInputの形状(Size)とSoftMaxの形状を正しく入力することです。  ### 4-2 モデルの学習と評価 1. 「編集」のタブからInput層~SoftmaxCrossEntropy層を追加します。 2. 「コンフィグ」からバッチサイズを設定します。「編集」のタブに戻り、「実行」をクリックすると学習開始します 3. 「学習」タブの右にある「実行」を押すとテストデータを使って、モデルの評価が行われます。 4. 「評価」のタブでは画像分類モデルによる分類精度の評価が行われます。 - 出力結果のタブでは、テストデータ1つ1つの分類精度が表示されます。 - 混合行列のタブでは、テストデータ全体の評価が行われます。   ## Spresenseへの組み込み NNCで構築したAIモデルをSpresenseに組み込みます。 ### 手順 1. 「評価」タブから「アクション」→「エクスポート」→「NNB」を順番に選択していきます。 NNBファイルはspresenseで読みこむことができるファイル形式である。 2. 先ほど3章データセット登録の説明でもあった出力ディレクトリにmodel.nnbというファイル形式があるので、この推論モデルををSDカードのルートディレクトリに移します。  ## HDRカメラによるリアルタイム画像分類 前章のAIモデルを使ってHDRで撮影した画像を精度良く分類できるか検証したいと思います。 以下のプログラムを実行してカメラ画像で推論できるか検証したいと思います。 ```c++ #include <SDHCI.h> #include <DNNRT.h> #include <Camera.h> #include <RTC.h> #define DNN_WIDTH 28 #define DNN_HEIGHT 28 DNNRT dnnrt; SDClass SD; DNNVariable input(DNN_WIDTH * DNN_HEIGHT); const char* CSV_FILE = "/results.csv"; void saveToCSV(const String& timestamp, int result, const String& imageFileName) { File file = SD.open(CSV_FILE, FILE_WRITE); if (file) { file.println(timestamp + "," + String(result) + "," + imageFileName); file.close(); } else { Serial.println("Failed to open CSV file for writing."); } } String getTimestamp() { RtcTime now = RTC.getTime(); char buffer[20]; sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()); return String(buffer); } int inferImage(CamImage& img) { // 幅と高さを直接取得する int imgWidth = img.getWidth(); int imgHeight = img.getHeight(); // リサイズ処理: 画像をDNN入力サイズに手動でリサイズ uint16_t* imgbuf = (uint16_t*)img.getImgBuff(); uint16_t resizedBuf[DNN_WIDTH * DNN_HEIGHT]; // 元画像の縦横比を保ちながら、DNNサイズにリサイズ for (int y = 0; y < DNN_HEIGHT; y++) { for (int x = 0; x < DNN_WIDTH; x++) { int srcX = x * imgWidth / DNN_WIDTH; int srcY = y * imgHeight / DNN_HEIGHT; resizedBuf[y * DNN_WIDTH + x] = imgbuf[srcY * imgWidth + srcX]; } } // グレースケール化して、DNNに入力 float* dnnbuf = input.data(); for (int i = 0; i < DNN_WIDTH * DNN_HEIGHT; i++) { uint16_t pixel = resizedBuf[i]; float r = ((pixel & 0xf800) >> 8) / 255.0; float g = ((pixel & 0x07e0) >> 3) / 255.0; float b = ((pixel & 0x001f) << 3) / 255.0; dnnbuf[i] = (r + g + b) / 3.0; // グレースケール化 } // 推論実行 dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); return output.maxIndex(); // 最大値のインデックス(推論結果) } void saveImage(const CamImage& img, const String& fileName) { File file = SD.open(fileName.c_str(), FILE_WRITE); if (!file) { Serial.println("Failed to open file for writing."); return; } file.write((uint8_t*)img.getImgBuff(), img.getImgSize()); file.close(); } void CamCB(CamImage img) { if (!img.isAvailable()) { Serial.println("Camera image not available."); return; } String timestamp = getTimestamp(); String imageFileName = "/image_" + timestamp + ".raw"; saveImage(img, imageFileName); int result = inferImage(img); saveToCSV(timestamp, result, imageFileName); Serial.println("Inference result: " + String(result) + " saved to CSV."); } void setup() { Serial.begin(115200); // SDカード初期化 if (!SD.begin()) { Serial.println("SD card initialization failed."); while (1); } // 学習済みモデルの読み込み File modelFile = SD.open("/model.nnb"); if (!modelFile) { Serial.println("Failed to open model file."); while (1); } if (dnnrt.begin(modelFile) < 0) { Serial.println("Failed to initialize DNNRT."); while (1); } p // RTC初期化 RTC.begin(); // カメラ初期化 theCamera.begin(); theCamera.startStreaming(true, CamCB); Serial.println("Setup complete."); } void loop() { // メインループは空 } ``` ### 検証結果 #### 分類精度 - 正常な直立:100 % - 転倒状態:0 % Spresenseに接続したHDRカメラで撮影した画像をSpresense上で推論した結果、分類性能が芳しくないことが確認されました。これは、学習時のデータ撮影環境と異なる条件で推論を行ったことが原因と考えられます。 ## テスト用データセットをSDカードに保存し、画像分類モデルを評価 次に、テスト用データセットとして使用した画像をSDカードに保存し、Spresense上で画像を読み込んで分類精度を検証しました。 - 転倒状態の検証画像:5枚 (直立状態の分類精度は100%であるため、転倒状態のみ検証) ```c++ #include <SDHCI.h> #include <DNNRT.h> #include <RTC.h> #define DNN_WIDTH 28 #define DNN_HEIGHT 28 DNNRT dnnrt; SDClass SD; DNNVariable input(DNN_WIDTH * DNN_HEIGHT); const char* testImages[] = {"/test1.jpg", "/test2.jpg", "/test3.jpg", "/test4.jpg", "/test5.jpg"}; const int numTestImages = 5; String getTimestamp() { RtcTime now = RTC.getTime(); char buffer[20]; sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()); return String(buffer); } int inferImage(uint8_t* imgData, int imgWidth, int imgHeight) { uint16_t resizedBuf[DNN_WIDTH * DNN_HEIGHT]; // 簡易リサイズ(バイリニア補間なし) for (int y = 0; y < DNN_HEIGHT; y++) { for (int x = 0; x < DNN_WIDTH; x++) { int srcX = x * imgWidth / DNN_WIDTH; int srcY = y * imgHeight / DNN_HEIGHT; resizedBuf[y * DNN_WIDTH + x] = imgData[srcY * imgWidth + srcX]; } } // グレースケール化して、DNNに入力 float* dnnbuf = input.data(); for (int i = 0; i < DNN_WIDTH * DNN_HEIGHT; i++) { uint16_t pixel = resizedBuf[i]; float r = ((pixel & 0xf800) >> 8) / 255.0; float g = ((pixel & 0x07e0) >> 3) / 255.0; float b = ((pixel & 0x001f) << 3) / 255.0; dnnbuf[i] = (r + g + b) / 3.0; // グレースケール化 } // 推論実行 dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); return output.maxIndex(); // 最大値のインデックス(推論結果) } void testInference() { for (int i = 0; i < numTestImages; i++) { String fileName = testImages[i]; File imgFile = SD.open(fileName); if (!imgFile) { Serial.println("Failed to open " + fileName); continue; } int fileSize = imgFile.size(); uint8_t* imgData = new uint8_t[fileSize]; imgFile.read(imgData, fileSize); imgFile.close(); Serial.println("Testing " + fileName); int result = inferImage(imgData, 64, 64); // 仮に64x64の画像とする Serial.println("Inference result for " + fileName + ": " + String(result)); delete[] imgData; } } void setup() { Serial.begin(115200); // SDカード初期化 if (!SD.begin()) { Serial.println("SD card initialization failed."); while (1); } // 学習済みモデルの読み込み File modelFile = SD.open("/model.nnb"); if (!modelFile) { Serial.println("Failed to open model file."); while (1); } if (dnnrt.begin(modelFile) < 0) { Serial.println("Failed to initialize DNNRT."); while (1); } // RTC初期化 RTC.begin(); Serial.println("Starting test inference..."); testInference(); } void loop() { // ループ処理なし } ``` ### 検証結果 5枚の検証画像(テスト用画像)すべてにおいて、直立状態と誤って分類されてしまった。 # おわりに SpresenseのHDRカメラを用いて、直立と転倒を判別する画像分類モデルを構築しました。しかし、学習用画像データを収集した際と同じカメラ・撮影環境を使用したにもかかわらず、分類精度が低いという結果になりました。これは推測ですが、モデルの構築方法やデータの前処理に課題があった可能性があります。 なお、Sony Neural Network Consoleを使用することで、コーディングなしで簡単に画像分類モデルを構築できました。