k_aiのアイコン画像
k_ai 2025年01月29日作成 © MIT
製作品 製作品 閲覧数 77
k_ai 2025年01月29日作成 © MIT 製作品 製作品 閲覧数 77

【spresense】カメラ画像からの数字認識結果をLINE通知

【spresense】カメラ画像からの数字認識結果をLINE通知

まえがき

「駐車場や駐輪場で駐車場所の番号がわからなくて困る!」という思いから、
spresenseのカメラで撮影した映像から数字認識を行い、LINEで共有できるシステムを作ってみようと思いました。

※お詫び
機械工作初心者です。独学でPythonを勉強した程度の知識量で、機械工作に関しては全くの知識0の状態からスタートしています。的外れなことをおこなっているかもしれません。ご了承いただけますと幸いです。
(2024年 SPRESENSE™ 活用コンテスト応募用作品です。)

使用部材

品名 入手先
Spresense メインボード モニター提供品
Spresense 拡張ボード モニター提供品
Spresense HDRカメラボード モニター提供品
ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807 モニター提供品
MEEQ SIM データ通信プラン(100MB/月)_キャリア:DoCoMo回線 モニター提供品
ジャンパー線 amazon
WAGOコネクタ 所持品

準備

spresenseのメインボードにLTE拡張ボードを取り付け、メインボードにカメラと液晶を取り付けました。

キャプションを入力できます

なお、メインボードから液晶表示を行う方法については、公式YouTubeを参考にさせていただきました。
まずはこの構成でカメラ撮影と液晶表示ができるか確認してみます。

#include <Camera.h> #include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_ILI9341.h> #define TFT_CS 24 #define TFT_RST 25 #define TFT_DC 18 #define TFT_WIDTH 320 #define TFT_HEIGHT 240 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI5, TFT_DC, TFT_CS, TFT_RST); #define BAUDRATE (115200) void setup() { Serial.begin(BAUDRATE); tft.begin(40000000); tft.setRotation(3); tft.fillScreen(ILI9341_WHITE); theCamera.begin(); theCamera.setStillPictureImageFormat( TFT_WIDTH, TFT_HEIGHT, CAM_IMAGE_PIX_FMT_RGB565); } void loop() { Serial.println("画像をキャプチャしています..."); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), TFT_WIDTH, TFT_HEIGHT); Serial.println("画像の表示が完了しました。"); } else { Serial.println("画像のキャプチャに失敗しました。"); } delay(2000); // 2秒間隔で画像を更新 }

キャプションを入力できます
※写っているものは段ボールです。

プログラムの検討

本題に入りたいと思います。まずはLINEとの連携をおこなっていきたいと思います。
調べてみるとLINEnotifyを使用するのがメジャーのようですが、サービス終了する模様。
LINE Messaging APIが代替とのことで調べてみますが、spresenseを使用している例はなかなか出てきません。

まずは、Notifyを用いて、カメラ撮影画像の数字認識結果をLINEに送信してみます。

Notifi

#include <SDHCI.h> #include <NetPBM.h> #include <DNNRT.h> #include <ArduinoHttpClient.h> #include <SDHCI.h> #include <LTE.h> #include <Camera.h> #include <Adafruit_GFX.h> #include <Adafruit_ILI9341.h> //液晶定義 #define TFT_CS 24 #define TFT_RST 25 #define TFT_DC 18 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI5, TFT_DC, TFT_CS, TFT_RST); //DNN,CAM定義 #define DNN_IMG_W 28 #define DNN_IMG_H 28 #define CAM_IMG_W 320 #define CAM_IMG_H 240 #define CAM_CLIP_X 104 #define CAM_CLIP_Y 0 #define CAM_CLIP_W 112 #define CAM_CLIP_H 224 #define LINE_THICKNESS 5 // LTEのAPIデータ #define LTE_APN "meeq.io" // replace your APN #define LTE_USER_NAME "meeq" // replace with your username #define LTE_PASSWORD "meeq" // replace with your password // LINEnotifyのToken char notify_token[] = "ここにトークンを貼り付け"; // Personal Access Token // LINEnotifyのサーバーURL char server[] = "notify-api.line.me"; char postPath[] = "/api/notify"; int port = 443; // port 443 is the default for HTTPS // SDカードに保存したルート証明書のパス // https://notify-api.line.meから取得したPEMファイルを配置 #define ROOTCA_FILE "CERTS/GlobalSign.pem" // ライブラリインスタンスを初期化 LTE lteAccess; LTETLSClient tlsClient; HttpClient client = HttpClient(tlsClient, server, port); DNNRT dnnrt; SDClass SD; CamImage small; DNNVariable input(DNN_IMG_W*DNN_IMG_H); static uint8_t const label[11] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 認識した数を入力する変数。初期値を -1 に設定(認識されていない状態) int recognizedNumber = -1; float accuracy = -1; // LINEnofityに送信する関数 void send_line_notify(String message){ Serial.println("sending Notify"); // LTE接続開始 while (true) { if (lteAccess.begin() == LTE_SEARCHING) { if (lteAccess.attach(LTE_APN, LTE_USER_NAME, LTE_PASSWORD) == LTE_READY) { Serial.println("attach succeeded."); break; } Serial.println("An error occurred, shutdown and try again."); lteAccess.shutdown(); return; // 終了 } } // 送信データ準備 String contentType = "application/x-www-form-urlencoded\nAuthorization: Bearer " + String(notify_token); String postData = "message="+String(message); // HTTPS POST送信 client.post(postPath, contentType, postData); // 応答コードを表示 int statusCode = client.responseStatusCode(); String response = client.responseBody(); Serial.print("Status code: "); Serial.println(statusCode); Serial.print("Response: "); Serial.println(response); // LTE接続停止 Serial.println(); Serial.println("disconnecting."); client.stop(); lteAccess.shutdown(); } //液晶に文字を表示する関数 void putStringOnLcd(String str, int color) { int len = str.length(); tft.fillRect(0,224, 320, 240, ILI9341_BLACK); tft.setTextSize(2); int sx = 160 - len/2*12; if (sx < 0) sx = 0; tft.setCursor(sx, 225); tft.setTextColor(color); tft.println(str); } //液晶にターゲット用の矩形を表示する関数 void drawBox(uint16_t* imgBuf) { /* Draw target line */ for (int x = CAM_CLIP_X; x < CAM_CLIP_X+CAM_CLIP_W; ++x) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+n) + x) = ILI9341_RED; *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+CAM_CLIP_H-1-n) + x) = ILI9341_RED; } } for (int y = CAM_CLIP_Y; y < CAM_CLIP_Y+CAM_CLIP_H; ++y) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X+n) = ILI9341_RED; *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X + CAM_CLIP_W-1-n) = ILI9341_RED; } } } //カメラ画像をストリーミングしながら文字認識を行う関数 void CamCB(CamImage img) { if (!img.isAvailable()) { Serial.println("Image is not available. Try again"); return; } CamImage small; CamErr err = img.clipAndResizeImageByHW(small , CAM_CLIP_X, CAM_CLIP_Y , CAM_CLIP_X + CAM_CLIP_W -1 , CAM_CLIP_Y + CAM_CLIP_H -1 , DNN_IMG_W, DNN_IMG_H); if (!small.isAvailable()){ putStringOnLcd("Clip and Resize Error:" + String(err), ILI9341_RED); return; } small.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* tmp = (uint16_t*)small.getImgBuff(); float *dnnbuf = input.data(); float f_max = 0.0; for (int n = 0; n < DNN_IMG_H*DNN_IMG_W; ++n) { dnnbuf[n] = (float)((tmp[n] & 0x07E0) >> 5); if (dnnbuf[n] > f_max) f_max = dnnbuf[n]; } /* normalization */ for (int n = 0; n < DNN_IMG_W*DNN_IMG_H; ++n) { dnnbuf[n] /= f_max; } String gStrResult = "?"; dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); int index = output.maxIndex(); if (index < 10) { gStrResult = String(label[index]) + String(":") + String(output[index]); } else { gStrResult = String("?:") + String(output[index]); } Serial.println(gStrResult); img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* imgBuf = (uint16_t*)img.getImgBuff(); drawBox(imgBuf); tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), 320, 224); putStringOnLcd(gStrResult, ILI9341_YELLOW); // 数字を認識(値が10未満かつ確度が0.8超えの場合)したらPreview画像の取得を停止 if (index < 10 && output[index] > 0.8 ) { recognizedNumber = index; accuracy = output[index]; Serial.println("Number recognized: " + String(recognizedNumber)); putStringOnLcd("Preview Stopped", ILI9341_GREEN); Serial.println("LINE送信処理を実行します"); } } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(3); SD.begin(); //SDカードからルート証明書をセットする File rootCertsFile = SD.open(ROOTCA_FILE, FILE_READ); tlsClient.setCACert(rootCertsFile, rootCertsFile.available()); rootCertsFile.close(); //SDカードからDNNモデルを開く File nnbfile = SD.open("model.nnb"); int ret = dnnrt.begin(nnbfile); if (ret < 0) { putStringOnLcd("dnnrt.begin failed" + String(ret), ILI9341_RED); return; } //CAMCBを実行 theCamera.begin(); theCamera.startStreaming(true, CamCB); // 初期状態でPreviewを開始 } void loop() { if (recognizedNumber != -1) { // 認識された数字が存在する場合CAMCBを停止する。 theCamera.startStreaming(false, CamCB); // ストリーミング停止 String message = "DNN推論結果: ラベル = " + String(recognizedNumber) + ", 信頼度 = " + String(accuracy); send_line_notify(message); //LINE送信 Serial.println(message); while (true) { // 何もしない } } }

※なおこのプログラムは、以下を参考にさせていただきました。
SPRESENSEからLINEに通知を送ろう
SPRESENSE™とNeural Network Consoleで始めるエッジAIプログラミング

キャプションを入力できます
無事送信出来ましたので、このプログラムをベースに、LINE Messaging APIを用いた手法に変更していきたいと思います。

LINE Messaging APIを用いて数字認識結果を送信する。

調べてみると、api.line.meから証明書を取得する必要がありますが、spresenseの公式が出しているブラウザから取得する方法を使うと、LINEのホームページにリダイレクトされてしまいます。

/v2/bot/message/broadcastまで入力すると、リダイレクトされないようだったので、そちらから取得が出来ました。

なお、今回は友達が自分だけですので、broadcastで送信してみました、

LINEMessagingAPI

#include <SDHCI.h> #include <NetPBM.h> #include <DNNRT.h> #include <ArduinoHttpClient.h> #include <SDHCI.h> #include <LTE.h> #include <Camera.h> #include <Adafruit_GFX.h> #include <Adafruit_ILI9341.h> //液晶定義 #define TFT_CS 24 #define TFT_RST 25 #define TFT_DC 18 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI5, TFT_DC, TFT_CS, TFT_RST); //DNN,CAM定義 #define DNN_IMG_W 28 #define DNN_IMG_H 28 #define CAM_IMG_W 320 #define CAM_IMG_H 240 #define CAM_CLIP_X 104 #define CAM_CLIP_Y 0 #define CAM_CLIP_W 112 #define CAM_CLIP_H 224 #define LINE_THICKNESS 5 // LTEのAPIデータ #define LTE_APN "meeq.io" // replace your APN #define LTE_USER_NAME "meeq" // replace with your username #define LTE_PASSWORD "meeq" // replace with your password // LINEAPIのサーバーURLとトークン char access_token[] = "ここにトークンを貼り付け"; // アクセストークン char server[] = "api.line.me"; char postPath[] = "/v2/bot/message/broadcast"; int port = 443; // port 443 is the default for HTTPS // SDカードに保存したルート証明書のパス // https://api.line.meから取得したPEMファイルを配置 #define ROOTCA_FILE "CERTS/GlobalSign.pem" // ライブラリインスタンスを初期化 LTE lteAccess; LTETLSClient tlsClient; HttpClient client = HttpClient(tlsClient, server, port); DNNRT dnnrt; SDClass SD; CamImage small; DNNVariable input(DNN_IMG_W*DNN_IMG_H); static uint8_t const label[11] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 認識した数を入力する変数。初期値を -1 に設定(認識されていない状態) int recognizedNumber = -1; float accuracy = -1; // LINEAPIに送信する関数 void send_line_notify(String message){ Serial.println("sending LINE"); // LTE接続開始 while (true) { if (lteAccess.begin() == LTE_SEARCHING) { if (lteAccess.attach(LTE_APN, LTE_USER_NAME, LTE_PASSWORD) == LTE_READY) { Serial.println("attach succeeded."); break; } Serial.println("An error occurred, shutdown and try again."); lteAccess.shutdown(); return; // 終了 } } //送信するメッセージをJSON形式にする Serial.println("making POST request to send message"); String postData = String("{\"to\":\"YOUR_USER_ID\",\"messages\":[{\"type\":\"text\",\"text\":\"") + message + "\"}]}"; // POSTリクエストの送信 String contentType = "application/json"; client.beginRequest(); client.post(postPath); client.sendHeader("Authorization", String("Bearer ") + access_token); client.sendHeader("Content-Length", postData.length()); client.sendHeader("Content-Type", contentType); client.beginBody(); client.print(postData); client.endRequest(); sleep(5); Serial.print("送信完了"); // LTE接続停止 Serial.println(); Serial.println("disconnecting."); client.stop(); lteAccess.shutdown(); } //液晶に文字を表示する関数 void putStringOnLcd(String str, int color) { int len = str.length(); tft.fillRect(0,224, 320, 240, ILI9341_BLACK); tft.setTextSize(2); int sx = 160 - len/2*12; if (sx < 0) sx = 0; tft.setCursor(sx, 225); tft.setTextColor(color); tft.println(str); } //液晶にターゲット用の矩形を表示する関数 void drawBox(uint16_t* imgBuf) { /* Draw target line */ for (int x = CAM_CLIP_X; x < CAM_CLIP_X+CAM_CLIP_W; ++x) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+n) + x) = ILI9341_RED; *(imgBuf + CAM_IMG_W*(CAM_CLIP_Y+CAM_CLIP_H-1-n) + x) = ILI9341_RED; } } for (int y = CAM_CLIP_Y; y < CAM_CLIP_Y+CAM_CLIP_H; ++y) { for (int n = 0; n < LINE_THICKNESS; ++n) { *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X+n) = ILI9341_RED; *(imgBuf + CAM_IMG_W*y + CAM_CLIP_X + CAM_CLIP_W-1-n) = ILI9341_RED; } } } //カメラ画像をストリーミングしながら文字認識を行う関数 void CamCB(CamImage img) { if (!img.isAvailable()) { Serial.println("Image is not available. Try again"); return; } CamImage small; CamErr err = img.clipAndResizeImageByHW(small , CAM_CLIP_X, CAM_CLIP_Y , CAM_CLIP_X + CAM_CLIP_W -1 , CAM_CLIP_Y + CAM_CLIP_H -1 , DNN_IMG_W, DNN_IMG_H); if (!small.isAvailable()){ putStringOnLcd("Clip and Resize Error:" + String(err), ILI9341_RED); return; } small.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* tmp = (uint16_t*)small.getImgBuff(); float *dnnbuf = input.data(); float f_max = 0.0; for (int n = 0; n < DNN_IMG_H*DNN_IMG_W; ++n) { dnnbuf[n] = (float)((tmp[n] & 0x07E0) >> 5); if (dnnbuf[n] > f_max) f_max = dnnbuf[n]; } /* normalization */ for (int n = 0; n < DNN_IMG_W*DNN_IMG_H; ++n) { dnnbuf[n] /= f_max; } String gStrResult = "?"; dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); int index = output.maxIndex(); if (index < 10) { gStrResult = String(label[index]) + String(":") + String(output[index]); } else { gStrResult = String("?:") + String(output[index]); } Serial.println(gStrResult); img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* imgBuf = (uint16_t*)img.getImgBuff(); drawBox(imgBuf); tft.drawRGBBitmap(0, 0, (uint16_t *)img.getImgBuff(), 320, 224); putStringOnLcd(gStrResult, ILI9341_YELLOW); // 数字を認識(値が10未満かつ確度が0.8超えの場合)したらPreview画像の取得を停止 if (index < 10 && output[index] > 0.8 ) { recognizedNumber = index; accuracy = output[index]; Serial.println("Number recognized: " + String(recognizedNumber)); putStringOnLcd("Preview Stopped", ILI9341_GREEN); Serial.println("LINE送信処理を実行します"); } } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(3); SD.begin(); //SDカードからルート証明書をセットする File rootCertsFile = SD.open(ROOTCA_FILE, FILE_READ); tlsClient.setCACert(rootCertsFile, rootCertsFile.available()); rootCertsFile.close(); //SDカードからDNNモデルを開く File nnbfile = SD.open("model.nnb"); int ret = dnnrt.begin(nnbfile); if (ret < 0) { putStringOnLcd("dnnrt.begin failed" + String(ret), ILI9341_RED); return; } //CAMCBを実行 theCamera.begin(); theCamera.startStreaming(true, CamCB); // 初期状態でPreviewを開始 } void loop() { if (recognizedNumber != -1) {// 認識された数字が存在する場合CAMCBを停止する。 theCamera.startStreaming(false, CamCB); // ストリーミング停止 String message = "DNN推論結果: ラベル = " + String(recognizedNumber) + ", 信頼度 = " + String(accuracy); send_line_notify(message); Serial.println(message); while (true) { // 何もしない } } }

キャプションを入力できます

なお、LINEMessagingAPIの設定の方法は多くの記事がありますので省略させていただきます。

大まかな処理の流れは以下の通りです。

  1. SDカードから証明書を読み込み、設定する。
  2. SDカードからDNNモデルを読み込む
  3. CAMCBを実行する
  4. 数字を認識したらCAMCBから抜ける
  5. 得られた数字情報をLINE送信関数に渡す
    6 JSON形式にして送信

実験

実際に自転車に括りつけて、地面に数字を印字した紙を置き、認識できるか試して見ようと思いました。
しかしながら、プライベートがバタバタしてしまい、タイミングと天候が合わず出来ませんでした。

あとがき

できることなら「任意のタイミングでのLINE通知」「複数桁の数字読み取り」を実装して完成に持っていきたかったのですが、知識不足ゆえなかなか上手く行かず、Spresense活用コンテストの期日となってしまいました。
中途半端な状態で終わってしまい申し訳ありません。

元々機械工作に興味はありましたが、全くのノータッチで初めての状態からスタートでした。今回のコンテストが無ければ、挑戦する機会はなかったと思います。
新たに学ぶことばかりで、非常に面白く、今後は更に深く勉強してみたいと強く感じました!
拙い文章でしたが、ここまでお読みいただきありがとうございました。

備忘録:複数桁の認識(途中)

spresenseを用いた数字認識の記事は数多くありますが、複数桁の認識については調べても出てきません。
画像を二値化し、一定のクラスターから数字があると思われる範囲を推定し、その範囲を切り出した上で数字認識ができないかと考え、以下のプログラムを作成しました。

クラスタリング

#include <stdio.h> #include <Camera.h> #include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_ILI9341.h> #include <stack> #define TFT_CS 24 #define TFT_RST 25 #define TFT_DC 18 #define CAMIMAGE_H 96 // カメラ画像の横解像度 #define CAMIMAGE_V 64 // カメラ画像の縦解像度 #define IMAGE_H 96 // カメラ画像の横解像度 #define IMAGE_V 64 // カメラ画像の縦解像度 #define THRESHOLD 80 // 2値化の閾値 #define DISPLAY_WIDTH 96 #define DISPLAY_HEIGHT 64 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI5, TFT_DC, TFT_CS, TFT_RST); int array[DISPLAY_HEIGHT][DISPLAY_WIDTH]; // データ格納用 bool visited[DISPLAY_HEIGHT][DISPLAY_WIDTH] = {false}; // 領域検出用フラグ #define BAUDRATE (115200) void setup() { tft.begin(40000000); tft.setRotation(3); tft.fillScreen(ILI9341_BLACK); theCamera.begin(); theCamera.setStillPictureImageFormat( CAMIMAGE_H, // 横解像度 CAMIMAGE_V, // 縦解像度 CAM_IMAGE_PIX_FMT_RGB565 // フォーマット ); Serial.begin(BAUDRATE); Serial.println("画像をキャプチャしています..."); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { if (img.convertPixFormat(CAM_IMAGE_PIX_FMT_GRAY) != CAM_ERR_SUCCESS) { Serial.println("画像フォーマットのグレースケール変換に失敗しました。"); return; } uint8_t *gray_data = img.getImgBuff(); // グレースケール画像バッファ uint8_t binarized_data[IMAGE_H * IMAGE_V]; // 2値化画像バッファ // 1. 画像を2値化 Serial.println("画像を二値化しています..."); binarize_image(gray_data, binarized_data, IMAGE_H, IMAGE_V, THRESHOLD); // 2. 配列にデータを格納 convertToArray(binarized_data, array, IMAGE_H, IMAGE_V); // 配列の描画 for (int y = 0; y < DISPLAY_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { if (array[y][x] == 1) { tft.drawPixel(x , y , ILI9341_WHITE); // 1なら白 } else { tft.drawPixel(x , y , ILI9341_BLACK); // 0なら黒 } } } // バウンディングボックスの検出と描画 detectAndDrawBoundingBoxes(); } else { Serial.println("画像のキャプチャに失敗しました。"); } } void loop() { // 1回だけ実行する処理 Serial.println("この処理は1回だけ実行されます"); while (true); // 無限ループで停止 } // 画像の2値化 void binarize_image(uint8_t *input, uint8_t *output, int width, int height, uint8_t threshold) { for (int i = 0; i < width * height; i++) { output[i] = (input[i] >= threshold) ? 1 : 0; } } // 2値化データを配列に変換 void convertToArray(uint8_t *binary_data, int output[DISPLAY_HEIGHT][DISPLAY_WIDTH], int width, int height) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { output[y][x] = binary_data[y * width + x]; } } } // Flood Fill void floodFill(int x, int y, int* x_min, int* x_max, int* y_min, int* y_max) { std::stack<std::pair<int, int>> stack; stack.push({x, y}); while (!stack.empty()) { auto [cx, cy] = stack.top(); stack.pop(); // 範囲外や条件外をチェック if (cx < 0 || cx >= DISPLAY_WIDTH || cy < 0 || cy >= DISPLAY_HEIGHT) continue; if (array[cy][cx] != 0) continue; // 領域をマーク(arrayの値を1以外の値に変更) array[cy][cx] = 1; // バウンディングボックスを更新 if (cx < *x_min) *x_min = cx; if (cx > *x_max) *x_max = cx; if (cy < *y_min) *y_min = cy; if (cy > *y_max) *y_max = cy; // 隣接セルをスタックに追加 stack.push({cx + 1, cy}); stack.push({cx - 1, cy}); stack.push({cx, cy + 1}); stack.push({cx, cy - 1}); } } void detectAndDrawBoundingBoxes() { for (int y = 0; y < DISPLAY_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { if (array[y][x] == 0) { // 新しい領域を検出 int x_min = DISPLAY_WIDTH, x_max = 0, y_min = DISPLAY_HEIGHT, y_max = 0; floodFill(x, y, &x_min, &x_max, &y_min, &y_max); // バウンディングボックスを描画 tft.drawRect(x_min, y_min, (x_max - x_min + 1), (y_max - y_min + 1), ILI9341_RED); } } } } }
  • k_ai さんが 2025/01/29 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する