kinakonokoのアイコン画像
kinakonoko 2022年09月14日作成 (2022年09月21日更新)
製作品 製作品 閲覧数 4332
kinakonoko 2022年09月14日作成 (2022年09月21日更新) 製作品 製作品 閲覧数 4332

【SPRESENSE】ナンバープレートのリアルタイムAI画像認識による駐車料金管理システム

【SPRESENSE】ナンバープレートのリアルタイムAI画像認識による駐車料金管理システム

概要

SPRESENSEを用いて駐車料金管理システムに応用できるであろうものの原型を作ってみました。カメラでナンバープレートの情報を読み取り、GPSからの時間の情報と車のナンバーを記録するシステムです。Excelを用いてデータをcsv形式で記録していきます。

使用した技術

このシステムを作るにあたって、まずソニーセミコンダクタソリューションズのYouTubeチャンネルを見てSPRESENSEについて勉強しました。無料でいいのか!?と疑ってしまうほど内容がしっかりしているのでおすすめです。

NNC

NNCとは、ソニーセミコンダクタソリューションズが開発しているGUIベースでAIのアルゴリズムを作成できるソフトの事です。SPRESENSEはここで作成したアルゴリズム(モデル)を利用して画像認識をすることができます。これを利用してナンバープレートを認識させたいと思い、下記動画を参考にNNCのモデルを作りました。

ここに動画が表示されます

GNSS

次に、駐車料金を管理するためには正確な時間が必要であると考えたとき、これはSPRESENSEの特徴であるGPSを利用できるのではないかと考え、下記動画を参考にGPSを使ってみることにしました。

ここに動画が表示されます

使用したもの

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

  • SPRESENSEメインボード
  • SPRESENSE拡張ボード
  • SPRESENSEカメラボード
  • LCDディスプレイ
  • SDカード
  • ワイヤー

SPRESENSEメインボード・拡張ボード

キャプションを入力できます
今回使用するマイコンです。このマイコンはSONYが開発しており、この小ささでGPSやハイレゾを搭載する超すぐれもの。マルチコアなんだって…(すごい)。

SPRESENSEカメラボード

キャプションを入力できます
SPRESENSE用のカメラです。最近HDRのカメラも発売されたみたいです。そちらも使ってみたいですね。

LCDディスプレイ

キャプションを入力できます
カメラの画像を表示したり、文字を表示したりすることができるディスプレイです。今回はこちらから購入したものを使用しました。

2.2インチ TFT 液晶ディスプレイ 240x320 ILI9341

SDカード

SDカードはNNCで作成したモデルを入れるために使用します。スロットの形状がmicro SDになっているので、microSDを準備します。

ワイヤー

一般的なジャンパワイヤを使用します。LCDに接続する際、ケーブルの本数が多いのでまとまったものを使用しています。

コーディング

今回は主にArduino IDEでコーディングしました。SPRESENSEのサンプルプログラムや、下記Gitに公開されているプログラムを利用しています。

GitHub - Spresense-Tech-Seminar-Basic

NNCを用いたナンバーの取得

NNCで作成したモデルを利用した画像認識

SPRESENSEはNNCで作成したモデルを利用して画像認識を行う事ができます。OpenCVなどを用いることで画像認識する事は可能ですが、SPRESENSEのような非常に小さなマイコン、また、非常に少ない消費電力で画像認識が利用できることには感動です。
上記で示した動画を参考にモデルのファイルを作成します。

作成したモデル

このファイルをSDカードにコピーして、サンプルコードを実行してみます。

認識中の画面

十分の精度で認識してくれています!これを応用してナンバープレートを認識させます。

正確な現在時刻の取得

SPRESENSEには、GNSSアンテナを搭載しており、高い精度で位置を測位できます(こんな小さいマイコンの中なのにすごい…!)。まず、利用方法を把握するため、下記のサイトを参考にサンプルコードを理解しました。

Spresense Documents - Spresense Arduino チュートリアル

サンプルコードは、SPRESENSEをArduino IDEで利用できるように環境設定などをすると追加されるスケッチ例のgnss.inoを参考にしました。環境設定については下記サイトを参照

Spresense Arduino スタートガイド

LCDに情報を表示

LCDを利用するのが今回初めてだったため、サンプルプログラムを読み漁り、理解していきました。まずは映像を表示させるところから理解し、文字を表示させたり、枠を表示させたりのやり方について試行錯誤を重ねていきました。下記に公開されているGitにあるSpresense_number_recognition.inoやSpresense_camera_preview.inoなどのサンプルプログラムを読みました。

GitHub - Spresense-Tech-Seminar-Basic

私はこのLCD画面に謎にこだわりたかったため、画面内に現在時刻を取得した瞬間をわかるように表示させたいと思い、取得状況が分かる文字と現在時刻をLCDに表示させました。動画では、00:32辺りで取得しています。

ここに動画が表示されます

LCDディスプレイ表示の注意点

今回用いたLCDディスプレイに表示させる際、文字、線などすべてにおいて座標軸の原点が左上であることに注意が必要です。
LCDディスプレイの座標系

Excelにデータを送信

今回は駐車料金を管理したいという事で、データ化できたら面白いと考えたため、シリアル通信で時間と取得したナンバーの情報を出力して、それをExcelで記録できるようにしてみました。これが意外と簡単でした。

まず、Excelのホーム画面からオプションを開きます。
キャプションを入力できます

次にアドインのタブから管理でCOMアドインを選択し、設定を押します。
キャプションを入力できます

そして、Microsoft Data Streamer for Excelにチェックを入れてOKを押します。
キャプションを入力できます

すると、ホーム画面のタブにData Streamerが追加されているので、ここからデバイスの接続を押し、Spresenseが接続されているポートを選択します。
キャプションを入力できます

すると、自動的にデータが入ってきます!!(感動)
キャプションを入力できます

これを応用すると駐車場に合ったシステムを簡単に作れそうです。

ソフト完成

これらを組み合わせて画面上ではそれっぽいシステムが完成しました!数字を読み取りながら時間も取得できています(今回のものはかなり精度が低くなってしまいましたが、NNCのアルゴリズムを工夫するとより精度の高いもの作れそうです)。

ここに動画が表示されます

それっぽいケースの制作

マイコンといえば組み込みだ!ということでケースというか外枠をかっこよく作りたいと思ったので制作してみる事にしました。設計はFusion360を使っています
Fusion 360

いつかアクリル加工をしてみたいと考えていたため、今回はレーザー加工機でアクリル板をカットしてみました。

カットしたパーツたち

組み立てて…完成!

完成!

製品っぽいものが出来て満足満足…アクリルの高級感すごい。

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

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


本体に文字も掘ってみました。いい感じです!

ソースコード

作成したコードです。カメラの画像処理のプログラムとディスプレイのレイアウトについて記述しているプログラムの2つに分かれています。

number_recognition_contest.ino

#include <Camera.h> #include "Adafruit_ILI9341.h" #include <DNNRT.h> #include <SDHCI.h> #include <RTC.h> #include <GNSS.h> #define TFT_DC 9 #define TFT_CS 10 Adafruit_ILI9341 display = Adafruit_ILI9341(TFT_CS, TFT_DC); #define OFFSET_X (73) #define OFFSET_Y (58) #define CLIP_WIDTH (56) #define CLIP_HEIGHT (112) #define OFFSET_X_1 (124) #define OFFSET_Y_1 (58) #define CLIP_WIDTH_1 (56) #define CLIP_HEIGHT_1 (112) #define OFFSET_X_2 (193) #define OFFSET_Y_2 (58) #define CLIP_WIDTH_2 (56) #define CLIP_HEIGHT_2 (112) #define OFFSET_X_3 (246) #define OFFSET_Y_3 (58) #define CLIP_WIDTH_3 (56) #define CLIP_HEIGHT_3 (112) #define DNN_WIDTH (28) #define DNN_HEIGHT (28) #define MY_TIMEZONE_IN_SECONDS (9 * 60 * 60) // JST int SETFINE = 0; int index_0; int index_1; int index_2; int index_3; SpGnss Gnss; SDClass SD; DNNRT dnnrt; DNNVariable input(DNN_WIDTH*DNN_HEIGHT); const char label[11] = {'0','1','2','3','4','5','6','7','8','9',' '}; String CLOCK; void printClock(RtcTime &rtc) { if(SETFINE == 1){ CLOCK = String("OK ") + String(rtc.year()) + String("/") + String(rtc.month()) + String("/") + String(rtc.day()) + String(" ") + String(rtc.hour()) + String(":") + String(rtc.minute()) + String(":") + String(rtc.second()); } else { CLOCK = String("-- ") + String(rtc.year()) + String("/") + String(rtc.month()) + String("/") + String(rtc.day()) + String(" ") + String(rtc.hour()) + String(":") + String(rtc.minute()) + String(":") + String(rtc.second()); } putStringOnLcdClock(CLOCK, ILI9341_YELLOW); Serial.print(CLOCK); } void updateClock() { RtcTime now = RTC.getTime(); printClock(now); } int Number_Re(CamImage img, int O_X, int O_Y) { CamImage small; CamErr err = img.clipAndResizeImageByHW(small , O_X, O_Y , O_X + CLIP_WIDTH -1 , O_Y + CLIP_HEIGHT -1 , DNN_WIDTH, DNN_HEIGHT); if (!small.isAvailable()){ putStringOnLcd("Clip and Reize Error:" + String(err), ILI9341_RED); return; } // 認識用モノクロ画像を設定 uint16_t* imgbuf = (uint16_t*)small.getImgBuff(); float *dnnbuf = input.data(); for (int n = 0; n < DNN_HEIGHT*DNN_WIDTH; ++n) { dnnbuf[n] = (float)(((imgbuf[n] & 0xf000) >> 8) | ((imgbuf[n] & 0x00f0) >> 4))/255.; } // 推論の実行 dnnrt.inputVariable(input, 0); dnnrt.forward(); DNNVariable output = dnnrt.outputVariable(0); int index = output.maxIndex(); return index; } void CamCB(CamImage img) { if (!img.isAvailable()) { return; } // カメラ画像の切り抜きと縮小 index_0 = Number_Re(img, OFFSET_X, OFFSET_Y); index_1 = Number_Re(img, OFFSET_X_1, OFFSET_Y_1); index_2 = Number_Re(img, OFFSET_X_2, OFFSET_Y_2); index_3 = Number_Re(img, OFFSET_X_3, OFFSET_Y_3); SpNavData NavData; // Get the UTC time Gnss.getNavData(&NavData); SpGnssTime *time = &NavData.time; // Check if the acquired UTC time is accurate if (time->year >= 2000) { RtcTime now = RTC.getTime(); // Convert SpGnssTime to RtcTime RtcTime gps(time->year, time->month, time->day, time->hour, time->minute, time->sec, time->usec * 1000); #ifdef MY_TIMEZONE_IN_SECONDS // Set the time difference gps += MY_TIMEZONE_IN_SECONDS; #endif int diff = now - gps; if (abs(diff) >= 1) { RTC.setTime(gps); SETFINE = 1; } } // Display the current time every a second // 推論結果の表示 String gStrResult; if (index_0 < 11) { gStrResult = String(label[index_0]) + String(label[index_1]) + String("-") + String(label[index_2]) + String(label[index_3]); } else { gStrResult = String("ERROR"); } // 推論結果のディスプレイ表示 img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); uint16_t* imgBuf = (uint16_t*)img.getImgBuff(); drawBox(imgBuf, OFFSET_X, OFFSET_Y, CLIP_WIDTH, CLIP_HEIGHT, 5, ILI9341_RED); drawBox(imgBuf, OFFSET_X_1, OFFSET_Y_1, CLIP_WIDTH_1, CLIP_HEIGHT_1, 5, ILI9341_RED); drawBox(imgBuf, OFFSET_X_2, OFFSET_Y_2, CLIP_WIDTH_2, CLIP_HEIGHT_2, 5, ILI9341_RED); drawBox(imgBuf, OFFSET_X_3, OFFSET_Y_3, CLIP_WIDTH_3, CLIP_HEIGHT_3, 5, ILI9341_RED); drawBox_NP(imgBuf, 5, ILI9341_RED); display.drawRGBBitmap(0, 20, (uint16_t*)imgBuf, 320, 204); putStringOnLcd(gStrResult, ILI9341_YELLOW); updateClock(); Serial.print(","); Serial.print(gStrResult); Serial.print("\n"); } void setup() { Serial.begin(115200); // SDカードの挿入待ち while (!SD.begin()) { putStringOnLcd("Insert SD card", ILI9341_RED); } // SDカードにある学習済モデルの読み込み File nnbfile = SD.open("model.nnb"); // 学習済モデルでDNNRTを開始 int ret = dnnrt.begin(nnbfile); if (ret < 0) { putStringOnLcd("dnnrt.begin failed" + String(ret), ILI9341_RED); return; } RTC.begin(); Gnss.begin(); Gnss.start(); display.begin(); display.setRotation(3); theCamera.begin(); theCamera.startStreaming(true, CamCB); } void loop() { }

displayUtil.ino

#define DISPLAY_WIDTH (320) #define DISPLAY_HEIGHT (240) void putStringOnLcd(String str, int color) { int len = str.length(); display.fillRect(0,224, 320, 240, ILI9341_BLACK); display.fillRect(0,0, 320, 20, ILI9341_BLACK); display.setTextSize(2); int sx = 160 - len/2*12; if (sx < 0) sx = 0; display.setCursor(sx, 225); display.setTextColor(color); display.println(str); } // 液晶ディスプレイに四角形を描画する void drawBox(uint16_t* imgBuf, int offset_x, int offset_y, int width, int height, int thickness, int color) { /*ナンバープレート*/ for (int x = 10; x < 310; ++x) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*(27+n) + x) = color; *(imgBuf + DISPLAY_WIDTH*(177-1-n) + x) = color; } } for (int y = 27; y < 177; ++y) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*y + 10+n) = color; *(imgBuf + DISPLAY_WIDTH*y + 310-n) = color; } } /* Draw target line */ for (int x = offset_x; x < offset_x+width; ++x) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*(offset_y+n) + x) = color; *(imgBuf + DISPLAY_WIDTH*(offset_y+height-1-n) + x) = color; } } for (int y = offset_y; y < offset_y+height; ++y) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*y + offset_x+n) = color; *(imgBuf + DISPLAY_WIDTH*y + offset_x + width-1-n) = color; } } } void drawBox_NP(uint16_t* imgBuf, int thickness, int color) { /*ナンバープレート*/ for (int x = 10; x < 310; ++x) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*(27+n) + x) = color; *(imgBuf + DISPLAY_WIDTH*(177-1-n) + x) = color; } } for (int x = 178; x < 193; ++x) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*(114+n) + x) = color; } } for (int y = 27; y < 177; ++y) { for (int n = 0; n < thickness; ++n) { *(imgBuf + DISPLAY_WIDTH*y + 10+n) = color; *(imgBuf + DISPLAY_WIDTH*y + 310-n) = color; } } } void putStringOnLcdClock(String str, int color) { display.fillRect(0,0, 320, 20, ILI9341_BLACK); display.setTextSize(2); display.setCursor(30, 3); display.setTextColor(color); display.println(str); }

まとめ

今回は、Spresenseを活用して数字を認識し、それを現在時刻と共に記録するシステムを作ってみました。今後はよりSpresenseの能力を発揮させるために車が来た際、その音を認識し、データを取得するというような、省電力なシステムを作ってみたいと考えています。

3
ログインしてコメントを投稿する