カメラ画像から、BLEマウスを使ってiPadを自動制御しよう
以前の記事で紹介したBLEマウスを外部制御できるように拡張しました。
下記のデモは、iPadを撮影した画像から、アイコンを探し起動します。
関連記事
↑以前作成したbleキーボートです。
bleの部分は、ほぼ共通です。
↑bleマウスの作成手順です。
システム概要
- PCは、USBカメラを経由して、iPadの画面を撮影
- 撮影した画像から、アイコンの座標位置を特定する
- 特定は、テンプレートマッチングを使用
- また、撮影した画像から、マウスのカーソル座標位置を特定する
- 撮影した画像から、アイコンの座標位置を特定する
- PCは、アイコンの座標位置とカーソル座標位置から、カーソルのシフト量を算出
- 差分値を使用する
- PCは、HTTPを経由して、Bleマウスにリクエストを投げる
- Bleマウスは、リクエストを受け、bleを経由してiPadを制御
- カーソルの移動を行う
- 1 - 4を繰り返し、カーソルをアイコン位置に移動する
- アイコンをクリックし、アプリケーションを起動する
検討
いろいろとやったことを記載します。
iPad側の設定
- 背景を単色にする
- オレンジ単色の壁紙に変更しました
- 真っ黒だとカメラのゲインが上がるため、比較的明るい色にしました
- カーソルを大きくする
- 設定 - アクセシビリティ - ポインタコントロール より、カラーをホワイト/ポインタサイズを最大に変更
- マウスカーソル
- AssistiveTocuh設定より、ボタンアプリケーションを追加する
- AssistiveTocuh設定のESP32 Bluetooth Mouseにてボタン割り当てを設定しました
カメラの撮影
USBカメラを使用すことしました。
手元にあったLogicool HD Webcam C270を使用しました。
本カメラは、最大1280x960で撮影できます。
また、できるだけ大きく撮影できるように、iPadを横置きしました。
マッチング時は、コード側で回転させることにしました。
テンプレート画像の作成
一旦カメラで被写体を撮影し、テンプレート画像を作成しました。
今回は、Podcastを起動します。
利用した画像は以下となります。
ファイル名 | 画像 |
---|---|
icon_podcast.png | |
pointer.png |
bleマウス側 - HTTP制御
PCからのインターフェイスとなります。
ESP32自体は、プアな環境のため、HTTPのGETメソッドのみで実装としました。
query | value | 説明 | 備考 |
---|---|---|---|
code | click | クリック動作 | 使用 |
- | move | カーソル移動 or ホイール動作 | 使用 |
- | press | マウスボタンダウン動作 | 本記事では未使用 |
- | release | マウスボタンアップ動作 | 本記事では未使用 |
type | left | 左クリック | iPad上では、シングルタップ動作(ボタン1) |
- | right | 右クリック | iPad上では、メニュー動作(ボタン2) |
- | middle | 第3ボタンクリック | iPad上では、HOME動作(ボタン3) |
- | back | 戻る | iPad上では、ロック動作(ボタン4) |
- | forward | 進む | 本記事では未使用(ボタン5) |
linear | 0 | カーソル移動 - 離散動作 | 使用 |
- | 1 | カーソル移動 - 連続動作 | 本記事では未使用 |
x | 整数 | 水平方向の移動量 | 使用 |
y | 整数 | 垂直方向の移動量 | 使用 |
wheel | -128 - 127 | ホイール移動量 | 本記事では未使用 |
step | 整数(>0) | 連続動作時の移動量 | 本記事では未使用 |
delay | 整数(>0) | 連続動作時の遅延量(msec) | 本記事では未使用 |
- 例:シングルタップを実施
GET http://{IP Address}/mouse?code=click&type=left
- 例:水平方向-100, 垂直方向+100移動
http://{IP Address}/mouse?code=move&x=-100&y=100
bleマウスの作成
完成図
筐体は前回と同じものになります。
🔧パーツ一覧
機材名 | 備考 |
---|---|
ESP32評価ボード | ESP32-WROVER 開発ボード/ESP32-WROOM 開発ボードのどちらでも可 |
🕹ボード | ジョイスティックおよびスイッチセット - Amazonで購入 |
タクトスイッチ | 3つ使用 |
接続図
MPU側でプルアップ設定を行うため、抵抗は不要です。
💻環境
ESP32側
VScode - PlatformIOを使用します。
PlatformaIOの環境構築は、↓にまとめておきました。
ライブラリ - ESP32 BLE Mouse
前記事を参照願います。
アプリケーション側
使用したモジュールの抜粋
Package Version
------------------ ---------
matplotlib 3.5.2
matplotlib-inline 0.1.3
numpy 1.22.0
opencv-python 4.5.5.62
Pillow 9.1.0
requests 2.27.1
📝手順
下記について記載します。
- ESP32側のファーム
- 画像アプリケーション
ESP32側のファーム
以前作成したBleキーボードのプロジェクトをベースに作成しました。
FreeRTOSを使用しています。
タスク構成
タスク名 | 役割 |
---|---|
スイッチ監視タイマー | スイッチのDOWN/UPの変化を監視する |
ジョイスティック監視タイマー | ジョイスティックの変化を監視する |
HTTPd管理タスク | Webサーバの起動及びクエリーを処理する(ここが追加) |
メッセージ管理タスク | スイッチ/ジョイスティックのイベントをキーボード管理に通知 |
マウス管理タスク | メッセージ管理から受けたイベントをBleマウス制御を送信 |
コード関連
コード自体は、githubにアップしてあります。
ポイントを記載します。
Web Server - インクルード関連
Wifi接続とWebServer用に下記をインクルードします。
#include <WiFi.h>
#include <WebServer.h>
Web Server - WiFiへの接続
初期化処理にて、WiFiへの接続を行います。
const char *ssid = "xxxxxxxxxxx"; // Enter SSID here
const char *password = "yyyyyyyyyyyyy"; // Enter Password here
---
WiFi.begin(ssid, password);
vTaskDelay(1000 / portTICK_RATE_MS);
while (WiFi.status() != WL_CONNECTED)
{
// WiFiに接続できているか
vTaskDelay(1000 / portTICK_RATE_MS);
Serial.print(".");
}
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Web Server - WebServerの初期化
生成時にポート番号を指定しています。
- server.on(url, handler)
- url : urlを指定
- handler : urlが指定された場合のサーバ処理
- server.onNotFound(handler)
- handler : 存在しないurlが指定された場合のサーバ処理
- server.begin()
- サーバの開始
/* Put IP Address details */
WebServer server(80);
---
server.on("/", vHandleHdOnConnect);
server.on("/mouse", vHandleHdOnConnectMouse);
server.on("/form", vHandleHdOnConnectForm);
server.onNotFound(vHandleHdNotFound);
server.begin();
Serial.println("HTTP server started");
Web Server - サーバ処理
簡単な例で記載します。
説明は、コメントに記載しています。
// vHandleHdOnConnectは、"http://{IP Address}/"アクセス時にコールされる
void vHandleHdOnConnect(void)
{
Serial.println("run:vHandleHdOnConnectMouse");
String code;
// "http://{IP Address}/"でアクセス => code : ""
// "http://{IP Address}/code=move"でアクセス => code : "move"
code = server.arg("code");
// 応答データを送信
server.send(200, "text/html", "ok\n\n");
Serial.println("over:vHandleHdOnConnect");
return;
}
マウス制御/ジョイスティック/スイッチの監視は、前記事を参照願います。
コード全体
画像アプリケーション
動作は、コメントで記載しました。
"システム概要"に記載した内容となります。
import sys
import time
import cv2
import numpy as np
import requests
# camera
WIDTH = 1280
HEIGHT = 960
FPS = 30
# esp32 ip address
TARGET_IP = "192.168.xxx.yyy"
# img_path
PATH_IMG_ICON = "icon_podcast.png"
PATH_IMG_POINTER = "pointer.png"
PATH_IMG_OUT = "out.jpg"
def find_img(img_base, img_search, meth="cv2.TM_CCOEFF_NORMED"):
"""テンプレートマッチング
Args:
img_base (np.arrasy): カメラ画像
img_search (np.array): 探す画像
meth (str, optional): テンプレートマッチングメソッド. Defaults to "cv2.TM_CCOEFF_NORMED".
Returns:
tapple: 探す画像の座標値
"""
img = frame
method = eval(meth)
# Apply template Matching
res = cv2.matchTemplate(img_base, img_search, method)
# print("res", res)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
print(min_val, max_val, min_loc, max_loc)
# If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
top_left = min_loc
else:
top_left = max_loc
w, h = img_search.shape[::-1]
bottom_right = (top_left[0] + w, top_left[1] + h)
cv2.rectangle(img, top_left, bottom_right, 255, 2)
print(top_left, bottom_right)
return ((top_left[0] + bottom_right[0]) // 2, (top_left[1] + bottom_right[1]) // 2)
def calc_shiftcrd(dst_crd, cur_crd):
"""座標値の算出
Args:
dst_crd (taple): 目標画像の座標値
cur_crd (taple): マウスポインタ画像の座標位置
Returns:
taple: マウスポインタの移動量
"""
x = (dst_crd[0] - cur_crd[0]) // 2
y = (dst_crd[1] - cur_crd[1]) // 2
x = 100 if x > 100 else x
x = -100 if x < -100 else x
y = 100 if y > 100 else y
y = -100 if y < -100 else y
return x, y
if __name__ == "__main__":
# キャプチャデバイスの準備
capture = cv2.VideoCapture(0)
# キャプチャデバイス設定
capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc("Y", "U", "Y", "V"))
capture.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
capture.set(cv2.CAP_PROP_FPS, FPS)
# template - icon画像の準備
icon_podcast = cv2.imread(PATH_IMG_ICON, 0)
icon_podcast = cv2.rotate(icon_podcast, cv2.ROTATE_90_CLOCKWISE)
# template - pointer画像の準備
pointer = cv2.imread(PATH_IMG_POINTER, 0)
pointer = cv2.rotate(pointer, cv2.ROTATE_90_CLOCKWISE)
# ->unlock->home
requests.get(f"http://{TARGET_IP}/mouse?code=click&type=middle")
time.sleep(0.3)
requests.get(f"http://{TARGET_IP}/mouse?code=click&type=middle")
time.sleep(1.0)
dst_crd_flg = False
shift_x, shift_y = 1, 1
for _ in range(10):
# カーソルの移動
requests.get(f"http://{TARGET_IP}/mouse?code=move&x={shift_x}&y={shift_y}")
time.sleep(0.3)
# 画像のキャプチャ
ret, frame = capture.read()
cv2.imwrite(PATH_IMG_OUT, frame)
frame = cv2.imread(PATH_IMG_OUT, 0)
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
# アイコン画像(目標画像)の座標取得
if not dst_crd_flg:
crd_icon = find_img(frame, icon_podcast)
print("crd_icon:", crd_icon)
dst_crd_flg = True
# マウスポインタ画像の座標取得
crd_point = find_img(frame, pointer)
print("crd_point:", crd_point)
# マウスポインタの移動量の算出
shift_x, shift_y = calc_shiftcrd(crd_icon, crd_point)
print("shift_x, shift_y:", shift_x, shift_y)
# アイコンとマウスポインタが重なった場合の処理
if abs(shift_x) < 10 and abs(shift_y) < 10:
# タップしてアプリケーションの起動
requests.get(f"http://{TARGET_IP}/mouse?code=click&type=left")
break
気づきメモ
この作業で遭遇したトラブルや気づきについて記載します。
ESP32 - WiFiとADC2のハード制約
ESP32をデバック中、下記のエラーが頻繁上がりました。
[ 81120][E][esp32-hal-adc.c:186] __analogRead(): GPIO25: ESP_ERR_TIMEOUT: ADC2 is in use by Wi-Fi. Please see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html#adc-limitations for more info
[ 81151][E][esp32-hal-adc.c:186] __analogRead(): GPIO27: ESP_ERR_TIMEOUT: ADC2 is in use by Wi-Fi. Please see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html#adc-limitations for more info
Wi-Fiで使われているから、ADC2は使えないよということです。
ハードウェア制約のため、ピン配置を変更しました。
変更前(ADC2 Pin) | 変更後(ADC1 Pin) |
---|---|
PIN25 | PIN32 |
PIN27 | PIN34 |
- 参考URL
ESP32 - 容量問題の回避(OTA)
Web ServerおよびWifi関連のモジュールを入れることで、ビルド時にエラーとなりました。
Error: The program size (xxxxxxxxxx bytes) is greater than maximum allowed (1310720 bytes)
現在使用しているESP32 Moduleは、4MByteのFlash領域があります。
なぜ?1.3MByteほどを超えるとエラーになるのかわからなかったため、調査をしたところ、OTA(over the air)でよう
いろいろと調べたところ、OTA(over the air)というWifi経由のプログラム書き込み機能を無効にすることで、容量を稼ぐことができることが分かりました。
platformio.iniに、以下を追記ました。
board_build.partitions = no_ota.csv
通常は、↑の修正のみでいいようですが、このファイルが見えないと怒れたため、
platformio.iniと同じ位置に、no_ota.csvを作成し、下記としました。
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x200000,
spiffs, data, spiffs, 0x210000,0x1F0000,
テンプレートマッチング
最初、iPad側はすべてデフォルトで動作させました。
ただ、背景に引っ張られる部分が大きかったたため、マウスポインタを大きくしたり、背景を単色するなどして、安定的に座標が取れるようになりました。
また、Opencvでは、6つのメソッドが使用できます。
- cv2.TM_CCOEFF
- cv2.TM_CCOEFF_NORMED
- cv2.TM_CCORR
- cv2.TM_CCORR_NORMED
- cv2.TM_SQDIFF
- cv2.TM_SQDIFF_NORMED
私の環境下では、cv2.TM_CCOEFF_NORMEDが比較的いい精度でとれました。
検出結果に、ボックスを描画しています。
内容 | 検出結果画像 |
---|---|
アイコン | |
カーソル | |
- | |
- |
カーソルがアイコンに近づいているのが分かります。
さいごに
今回は、簡単な自動制御を行いました。
本マウスは、相対値の制御になるため、なかなか面倒だなと思いました。
作りこめば、いろんな可能性があると思います。
参考URL
Discussion