こんにちは!株式会社神戸デジタル・ラボ DataIntelligenceチームの原口です。
今回は社内で取り組んだ「個室スペースの空き検出システム」の構築についてご紹介します!
設置の様子をKDL BLOGでも掲載しています!
今回構築したもの
最初に今回構築した個室スペースの空き検出システムについてご紹介します!
本システムは、個室スペースに人が入室すると緑色の旗が倒れ、個室スペースから人が退出すると旗が再び立ち上がるようになっています。
これによって自席からでも個室スペースの空席状況を確認できるようになりました!
(以前は個室スペースに出向く必要があったため、会議ギリギリで個室スペースに向かうとすべてが利用中で、あたふたするなんてこともしばしばありました……)
構築したシステム
今回構築したシステムの全体像を上の図に示します。
本システムはYOLOv5とArduino(マイコンボード)によって構成されており、YOLOv5は人物検出用に、Arduinoは旗の制御に利用します。
システム動作の大まかな流れは次の通りです。
- パーテーションエリアに人が入室すると、YOLOv5にて人物検出が行われる
- 検出された人の位置が所定の場所であれば、個室スペースを利用していると認識
- 個室スペースを利用していると認識されると、Arduinoに信号が送信される
- 信号を受け取ったArduinoは、信号の内容に応じて旗の制御を行う
このシステムではYOLOv5とArduinoが重要となります。まずはこれらについてご紹介します!
YOLOv5とは?
YOLOv5はUltralytics社が開発した物体検出モデルです。YOLOv5のプログラムはすべてGitHubリポジトリに公開されています。
YOLOv5の最大の特徴は、簡単に実験・利用が可能な点です。
一般的に深層学習モデルを利用する際は、難解なコードとGitHubに表示されるReadmeを解読することがほとんどです。
高精度なモデルでも、使い方が分からない・分かるけど利用が大変すぎる......、ということで渋々利用を諦めることもよくあります。
一方YOLOv5はReadme内のチュートリアルが充実しており、出来ることがステップバイステップで記載されているため簡単にモデルを利用できます。
また推論&結果表示も以下の5行のみで実現できます。
import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s') img = 'path/to/image' results = model(img) results.print()
そういえば、YOLOv5を利用した面白い記事があったような......?
はっ!思い出した!YOLOv5+ByteTrackで物体追跡をした記事だ!
ということで、まだ読まれていない方はぜひご一読ください!読まれた方もこの機会に是非読み返してください!(宣伝でした)
Arduinoとは?
ArduinoはArduino Holding社が開発したマイコンボードのことを指します。誰でも簡単に電子工作が出来ることを目的として作られており、はんだ付けのような難しい技術は必要なく、素子とArduinoをケーブルでつなぐだけで動作させられます。
またArduinoを用いたデバイス開発では動作をCライクな言語で記述できるので、非常に簡単にモノづくりを始められます。(マイコン開発でよく用いられるアセンブリ言語は、考え方が難しく初心者にとって大きな障壁になります。その部分を取り除けたのが大きなポイントだと思います)
動作内容
それでは構築したシステムの動作の詳細について説明します。まずは人検出システムから説明します。
人検出システムの仕組み
今回装置を設置した個室スペースの俯瞰の様子を上の図に示します。
個室スペースは二つの席が隣り合わせに配置されており、今回は一つのWebカメラで二つの席を同時に処理するように構築します。
二つの席を同時に写すために、Webカメラを個室スペースの仕切りに沿って配置しました。
これによって両方の席を同時に写すだけでなく、画像を中央で分割することでどちらの席に人がいるかを区別できるようになります。
検出された人領域の中心が半分より左側に存在する場合は、カメラから見て左側の席を利用していると認識します。 一方右側に存在する場合はカメラから見て右側の席を利用していると認識します。
実際の動作画面を下の図に示します。
この場合は左側で人が検出されているため、「カメラから見て左側の個室スペースが利用されている」という認識になります。
個室スペース利用の検出結果は以下の4パターンに分類されます。
- 誰も利用していない
- 右だけ利用している
- 左だけ利用している
- 両方利用している
検出パターンが変化した際(例:誰も利用していない→右だけ利用している)に席の利用状況が変化します。
変化があったタイミングでArduinoに信号を送り、旗の状態を変化させます。
信号の伝達にはシリアル通信を利用します。
シリアル通信とは?
シリアル通信とは、デジタル信号のやり取りを1本の線で行うことを意味します。一方で複数本で信号のやり取りをする場合はパラレル通信と呼びます。
通信時の電気信号のやり取りを以下の図に示します。
一般的に通信時の電気信号は矩形をしており、矩形が発生している状態を1、発生していない状態を0というふうに定義をして通信を行います。この情報を送信・受信することで、旗揚げの状態を制御することができます。面白いですねぇ......
旗の制御
続いて旗の制御について説明します。
旗の制御にはサーボモータを用います。サーボモータとは信号によって指定した角度に回転し、その角度を維持してくれるすごいモータです。ロボットのアーム制御などに利用されています。
Arduinoとサーボモータと回路図は次の図の通りです。
Arduinoにはアナログポートとデジタルポートの二種類が存在するので注意が必要です。今回はArduinoのデジタルポートの6,7番ピンを利用します。
旗の制御をするには信号を送る必要があります。サーボモータは6,7番ピンと接続されているので、ここから信号を送ります。
ではどのような信号を送るのでしょうか?
サーボモータの制御ではPWM(Pulse Width Modulation)という制御方法を利用して旗を上げたりおろしたりします。
PWM(Pulse Width Modulation)とは
PWMについて説明する前に、まずはパルス(Pulse)について簡単に説明します。
パルスは先ほど紹介した通信時の電気信号のような矩形の電気信号のことを指します。
一般的にONの状態の電圧は、電源の電圧と等しいことが多いです。
ではPWMとは何でしょうか?PWMとは、パルスのON/OFFの比率(この比率をDuty比という)を変更することで対象物を制御する方式です。
幅の変化の例を下の図に示します。
このようにパルス幅を変化させることで、サーボモータの回転角を制御することができます。
例えば角度を0度に近づけたい場合はONの比率を小さくします。一方で角度を180度に近づけたい場合はONの比率を大きくします。
実際に構築する
動作内容が分かったので、実際に構築しましょう!
構築にあたって利用したものを以下の表に示します。
購入物 | 値段 | 備考 |
---|---|---|
広角カメラ | 2,980 | |
サーボモータ(2個入り) | 950 | 旗を動かす装置0~180度で角度を変更することができる |
Arduino Uno | 3,267 | サーボモータを操作するマイコン装置 |
ブレッドボード | 555 | 回路を作成する場所 |
ジャンパー線 | 695 | マイコン、ブレッドボード、サーボモータを接続するケーブル |
割りばし | ||
画用紙 | ||
段ボール | ||
養生テープ |
後半はIoTデバイスを作るとは思えないラインナップですね。
またシステム構築にあたってこちらのページから、OSに合わせてArduino IDEをインストールしてください。
まずは人検出システムから構築します。
人検出システムの構築
モデルを呼び出す関数を作成します。
import torch def getModel(): model = torch.hub.load('ultralytics/yolov5', 'yolov5x6') return model
今回はモデルをCPUで動かすので、処理速度の速いSサイズモデルを利用しました。
続いて実際の処理を作成します。
Arduinoとの通信の際にシリアル通信を用いるので、pythonにシリアル通信用のモジュールをインストールしましょう。
pip install pyserial
では構築していきましょう。まずはモデルの呼び出しとカメラの起動、そしてシリアル通信の開始を宣言をしましょう。
def main(): model = getModel() # モデルを呼び出す cam = cv2.VideoCapture(1) # webカメラを起動する ser = serial.Serial("ポート名", 9600) # シリアル通信を開始する
ポート名の部分はArduino IDEを起動し、ツール→シリアルポートから確認できるポートを記載してください。
ここに表示されるポート名は利用するマシンによって変化します。必ず確認し、その内容をポート名に入力してください。
続いて利用する変数を定義します。
is_use_left = 2 is_use_right = 1 old_use_type = -1
ここでis_use_right
はwebカメラの右側が利用されている状態、is_use_left
は左側が利用されている状態を表しています。
では連続的に画像を取得しYOLOv5で人を検出してみましょう!
def main(): model = getModel() # モデルを呼び出す cam = cv2.VideoCapture(1) #webカメラを起動する ser = serial.Serial("ポート名", 9600) # シリアル通信を開始する is_use_left = 2 is_use_right = 1 old_use_type = -1 while True: # カメラ映像からスクリーンショットの取得 _, img = cam.read() img = Image.fromarray(img[:,:,::-1]) width,height = img.size cx = int(width/2) result = model(img) df = result.pandas().xyxy[0] cv2.imshow("left_area",result.imgs[0][:,:cx,::-1]) cv2.imshow("right_area",result.imgs[0][:,cx:,::-1]) result.render() k = cv2.waitKey(1) & 0xFF if(k == ord('q')): break
上の図のような結果が表示されれば成功です!続いてどちらの席に人がいるか判断できるようにしましょう。
YOLOv5による物体検出結果は次のプログラムで確認することができます。 まずは内容を確認してみましょう。
df = result.pandas().xyxy[0] print(df) # xmin ymin xmax ymax confidence class name # 0 82.771835 9.553299 569.588562 475.739929 0.818769 0 person # 1 373.982422 137.707397 637.560669 450.646240 0.811702 1 bicycle
物体の左上の座標・右下の座標・信頼度・物体のクラス名が保存されていますね。
Webカメラは席の中央に存在しているので、どちらの席に人がいるかを判断したい場合は、「物体の中心座標が中央より右・左のどちらにあるか」の情報があれば判断できそうです。
では中心座標を計算しましょう。
df["cx"] = (df["xmax"] + df["xmin"])/2 df["cy"] = (df["ymax"] + df["ymin"])/2
中心座標が分かったので、どちらの席に人がいるか判断してみましょう。
#Webカメラから見て左の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] < cx)) != 0: use_type += is_use_left #人がいたらuse_typeに2を足す #Webカメラから見て右の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] >= cx) != 0: use_type += is_use_right #人がいたらuse_typeに1を足す
今回のアルゴリズムでは、人がいるかどうかの判断をuse_type
に1または2を足すことで表現しています。これはどういうことでしょうか?
上記で述べた通り、パーテーションエリアの利用状況は以下の4パターンが考えられます。
- 誰も利用していない
- 右だけ利用している
- 左だけ利用している
- 両方利用している
この情報をパターン分けにしてコードを書くこともできますが、コード量が煩雑になります。実際に書いてみると以下のようになります。
is_use_left = False is_use_right = False #Webカメラから見て左の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] < cx)) != 0: is_use_left = True #Webカメラから見て右の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] >= cx) != 0: is_use_right = True ##パターン分けの部分 if is_use_left == False and is_use_right == False: ser.write(b"0") elif is_use_left == False and is_use_right == True: ser.write(b"1") elif is_use_left == True and is_use_right == False: ser.write(b"2") else: ser.write(b"3")
今回のシステムは二席同時の処理のため4行だけですが、席数が増えるごとにパターン分けの部分は2nずつ増えていきます。
そこでビットの考え方を利用することでこの問題を解消します。
ビットとは?
ビットとはコンピュータにおける情報量の最小単位です。1ビットが取れる値は0 or 1の二つの値のみで2進数と言います。私たちがよく使う数字は0 ~ 9 の10種類の数を使うので10進数と言います。
また、ビットは皆さんがよく耳にする○○GBや○○MBとも関連しています。
K(キロ)、M(メガ)やG(ギガ)は補助単位と言われており、その数字の大きさを補助しています。例えばKは103、Mは106のように大きな数字を簡単に表す際に利用されます。
一方で最後についているB(バイト)は単位を表しています。1kmだとメートルが単位なのと同じで、1GBならバイトが単位です。
バイトも情報量の単位でビットの仲間です。バイトとビットの変換は1 Byte = 8bitというようになっています。
情報の世界ではこのビットと呼ばれる情報を組み合わせることで、大きな情報を表現しています。
ビットの説明の図では1010
と表しました。左側が大きな桁を、右側が小さな桁を表しています。私たちが日常的に使っている数字と同じですね。
この組み合わせたビット情報は10進数に変換することができます。どのように変換するのでしょうか?
2進10進変換
まず10進数から考えてみましょう。例として1234を考えてみます。1234を詳細に書くと次のようになります。
この内容を分かりやすく書くと次の図のようになります。
各桁はその桁分だけ10を乗算されているされていることが分かります。
この考え方は2進数にも適応できます。指数の底は進数によって定義されるので、2進数の場合は2が底となります。
つまりこれを計算すると次のようになり、10であることが分かりました。今回のシステムではこの技術を応用します。
ビットの席への応用
ビットの席への応用では、下の図のように各個室スペースにビットを割り当てます。
席に人がいる場合は1、いない場合は0としたとき、各状態は次の表ように表せます。
状態 | 2進数 | 2進10進変換 | 10進数 |
---|---|---|---|
誰も利用していない | 00 | 21 × 0 + 20 × 0 = 0 + 0 | 0 |
右だけ利用している | 01 | 21 × 0 + 20 × 1 = 0 + 1 | 1 |
左だけ利用している | 10 | 21 × 1 + 20 × 0 = 2 + 0 | 2 |
両方利用している | 11 | 21 × 1 + 20 × 1 = 2 + 1 | 3 |
つまり右の席に人がいる場合は1を、左の席に人がいる場合は2を加算するだけで状態を表現することができます。
この手法を利用することで席利用の状態把握のコードは次のように書くことができます。
#Webカメラから見て左の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] < cx)) != 0: use_type += is_use_left #左側に人がいたらuse_typeに2を足す #Webカメラから見て右の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] >= cx) != 0: use_type += is_use_right #右側に人がいたらuse_typeに1を足す
上のコードを反映した結果は次のようになります。
def main(): model = getModel() # モデルを呼び出す cam = cv2.VideoCapture(1) #webカメラを起動する ser = serial.Serial("ポート名", 9600) # シリアル通信を開始する is_use_left = 2 is_use_right = 1 old_use_type = -1 while True: use_type = -1 # カメラ映像からスクリーンショットの取得 _, img = cam.read() img = Image.fromarray(img[:,:,::-1]) width,height = img.size cx = int(width/2) result = model(img) df = result.pandas().xyxy[0] #中心の計算 df["cx"] = (df["xmax"] + df["xmin"])/2 df["cy"] = (df["ymax"] + df["ymin"])/2 #Webカメラから見て左の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] < cx)) != 0: use_type += is_use_left #左側に人がいたらuse_typeに2を足す #Webカメラから見て右の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] >= cx) != 0: use_type += is_use_right #右側に人がいたらuse_typeに1を足す result.render() cv2.imshow("left_area",result.imgs[0][:,:cx,::-1]) cv2.imshow("right_area",result.imgs[0][:,cx:,::-1]) k = cv2.waitKey(1) & 0xFF if(k == ord('q')): break
信号の送信
最後にArduinoに向けて信号を送信します。信号を送る際は必要な情報を必要なタイミングで送ることが重要です。
Arduinoは信号が送られてくると、信号をバッファ(信号一時保管所)に保管します。
Arudinoは処理タイミングがきた際にバッファに保管されている情報を読み出し、それに合わせた処理を行います。
このとき情報送信間隔がArudinoの処理速度を上回ると、バッファ内に処理されていない信号が溜まっていくことになります。
その結果信号を送ってから数十秒後に動作が行われることや、Arduino自体が動作しなくなることがあります。
この問題が発生しないように状態が変化した場合のみ信号を送信するようにコーディングします。
実際にコーディングした結果は次の通りです。
def main(): model = getModel() # モデルを呼び出す cam = cv2.VideoCapture(1) #webカメラを起動する ser = serial.Serial("ポート名", 9600) # シリアル通信を開始する is_use_left = 2 is_use_right = 1 old_use_type = -1 while True: use_type = -1 # カメラ映像からスクリーンショットの取得 _, img = cam.read() img = Image.fromarray(img[:,:,::-1]) width,height = img.size cx = int(width/2) result = model(img) df = result.pandas().xyxy[0] #中心の計算 df["cx"] = (df["xmax"] + df["xmin"])/2 df["cy"] = (df["ymax"] + df["ymin"])/2 #Webカメラから見て左の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] < cx)) != 0: use_type += is_use_left #左側に人がいたらuse_typeに2を足す #Webカメラから見て右の席に人がいるか if len(df[(df["class"] == 0)&(df["cx"] >= cx) != 0: use_type += is_use_right #右側に人がいたらuse_typeに1を足す # 前の状態と一致しない場合は情報を送信する。 if old_use_type != use_type: old_use_type = use_type bin_type = str(use_type).encode() #状態をint→byteに変換する ser.write(bin_type) result.render() cv2.imshow("left_area",result.imgs[0][:,:cx,::-1]) cv2.imshow("right_area",result.imgs[0][:,cx:,::-1]) k = cv2.waitKey(1) & 0xFF if(k == ord('q')): break
以上で人検出システム部分の構築は完了です!
続いてArduinoによる旗制御システムの構築を行いましょう!
旗制御システムの構築
続いてArduino側の制御を構築します。まずは回路図に沿ってブレッドボード上に回路を構築しましょう!
続いてArudinoのライブラリを準備します。
Arudinoに最初から搭載されているServoライブラリは動作の時間を設定することができません。
そのため旗がものすごい勢いで立ち上がります。
その問題を解決するためにVerSpeedServoというライブラリをインストールしましょう!
このライブラリを利用することで、サーボモータの動作速度(回転する速度)を任意に変更できます。
まずはこちらからコードをzip形式でダウンロードします。
その後Arudino IDEから、スケッチ→ライブラリをインクルード→.ZIP形式のライブラリをインクルードを選択し、ダウンロードしたファイルを選択しましょう。
ライブラリをインクルードの中の提供されたライブラリ内に、VerSpeedServoがあれば完了です!
最後に制御コードを記述します。
#include <VarSpeedServo.h> // Servoライブラリの読み込み VerSpeedServo myservo_left; // Servoオブジェクトの宣言 VerSpeedServo myservo_right; // Servoオブジェクトの宣言 const int SV1_PIN = 7; // サーボモータをデジタルピン7に const int SV2_PIN = 6; // サーボモータをデジタルピン7に void setup() { Serial.begin(9600); myservo_left.attach(SV1_PIN, 500, 2400); myservo_right.attach(SV2_PIN, 500, 2400); } void loop() { bool use_left = false; bool use_right = false; int delay_time = 500; byte var; if (Serial.available() > 0) { var = Serial.read(); use_left = boolean bitRead(var, 1); use_right = boolean bitRead(var, 0); myservo_left.write(90 - int(use_left),100,True ); // サーボモータを 0 or 90度の位置まで動かす delay(delay_time); myservo_right.write(90 + int(use_right),100,True); // サーボモータを90 or 180度の位置まで動かす delay(delay_time); } }
myservo1.attach(SV1_PIN, 500, 2400)
では、サーボモータを利用するピンと角度が0度の時のパルス幅、180度の時のパルス幅を指定します。パルス幅は利用するサーボモータによって若干異なります。購入したサーボモータのデータシートを見ながら調整してください。(データシートは購入サイトや型番名で検索すると取得できます。)
if (Serial.available() > 0)
では、人検出システムから信号が送られてきたかを判別します。データの送信が無い場合は現状の状態を維持、データが送信された場合は状態を変更します。
以下の部分ではデータの読み出しを行い、各ビットごとの情報を取得しています。左側の情報は1ビット目に右側の情報は0ビット目に保管されているので、対象のビットの情報を取得します。
var = Serial.read(); use_left = boolean bitRead(var, 1); use_right = boolean bitRead(var, 0);
最後に信号に合わせて角度を制御します。
myservo_left.write(90 - int(use_left)*90 ); // サーボモータを 0 or 90度の位置まで動かす delay(delay_time); myservo_right.write(90 + int(use_right)*90); // サーボモータを90 or 180度の位置まで動かす delay(delay_time);
以上でArduino側の制御は完了です!実際に動かしてみましょう!
実際の動作
現状の課題点
個室ミーティングスペースの検出はおおむね成功しました。が、実際に構築したうえで以下のような問題点を感じました。
- Arduino Unoが高価
- 人の検出が正しく行われない場合がある
Arduino Unoが高価
サーボモータの制御に利用しているArduino Unoですが、一つ3,000円程度します。今回は一つの個室スペースに設置するだけだったので、そこまで費用が掛かりませんでした。
しかし設置する席数が多い場合は、その分の費用が高くなる問題が発生します。
この問題の解決策として、Arduino Unoの構成を自作することが挙げられます。実はArduino Unoが一つあるだけで、同様の装置を作れるようになります。
Arduino UnoにはATMEGA328Pというマイコンが利用されています。実際の動作はこのマイコンが行っているので、この部分を量産できればサーボモータの制御も簡単になります。
ATMEGA328Pは500円程度で購入することができるので1/6のコストに抑えることができます。コストを抑えられる反面、はんだ付けや回路設計を行う必要があるため注意が必要です。
人の検出が正しく行われない場合がある
現状のシステムではAIが人を認識できなくなると、個室スペースを利用している場合でも旗が下がる問題が発生することがあります。
この問題の解決策は、人検出アルゴリズムを改良することです。
現在のアルゴリズムではYOLOv5の出力のみを利用して動作しています。そのため人が検出できなかった・誤検出が発生した場合に旗が動作する問題が発生します。
一般的に個室スペースを使う際は、一定時間そのスペースにいると考えられます。そこで旗をあげる・下げるの動作のタイミングを「YOLOv5が人を検出すれば変化させる」から「一定時間そこに人がいる・いない」のような累積で動作させることで解決することが可能です。
まとめ
今回はYOLOv5とArduinoで個室スペースの空き検出システムを構築しました!
PythonとArduinoがこんなにも簡単にシリアル通信ができるとは思いませんでした......
またAI x IoTを実施することで、AIがイメージしやすいものとなり様々な意見を頂けるようになりました!
一方で実際にシステムを構築することで見えてきた課題点もあります。
アルゴリズムベースでAIモデルを構築するのは容易ですが、実際に運用することの大変さ・難しさ・様々な技術が必要であることが分かりました。
今後も個室システムの改良を行いつつ、新たなAI x IoTシステムを構築していきます!
データインテリジェンスチーム所属
データエンジニアを担当しています。画像認識を得意としており、画像認識・ニューラルネットワーク系の技術記事を発信していきます