私は、小中学生にプログラミングなどを教えるボランティアグループのメンバーになっています。
このグループでは、小学生に電子回路とマイクロビットプログラミングを教える通年制のワークショップを実施しています。
月1回のペースで合計8〜9回程度のワークショップを開催し、「電子回路」や「プログラミング」についてそれぞれ数回の講義を行い、そこで得られた知識を使って、各参加者に自分で考えた作品をつくってもらおうという活動です。
このワークショップに参加している生徒より、「QRコードのような何らかのマーカーをつかって、パソコンのカメラでマーカーの座標を検出し、その情報をつかってマイクロビットに処理をさせたい」というリクエストがありました。
このような用途にピッタリなマーカーに「ArUcoマーカー」というものがあります。
このマーカーをPCのカメラで読み取る処理を行えばよいのですが、「MakeCodeエディタ」や「Scratch」のようなブロックプログラミング環境でそのような処理をするのは難しいようです。
そのため今回は、PCカメラでのマーカー検出と座標計算のみをPythonで行い、その結果をUSBケーブルでPCとつながっているマイクロビットにシリアル通信で送信することにしました。マイクロビット側では、シリアル通信でデータを受信した後は、普通のブロックプログラミングで生徒のすきなプログラムをつくることができます。
ArUcoマーカー
今回は「DICT_4X4_50」という辞書をつかって「ID=1, 2」のふたつのマーカーをつくり、それらを使います。
こちら にアクセスし、「Dictionary」を「4×4」に、「Marker ID」を「0」に、「Marker size」を適当な大きさ(30ミリ程度)に設定してマーカーをつくり、「open」をクリックして印刷します。「Marker ID」を「1」にして、そちらも印刷します。
これでふたつのマーカーが準備できました。
Pythonスクリプト
Pythonはパソコンにインストールされている状態から始めます。
まずはスクリプトで使用するライブラリをインストールします。
最初に、Pythonで画像を扱い、ArUcoマーカーを検知するためのライブラリです。
検索ウィンドウに「cmd」と入力すると「コマンドプロンプト」というのが表示されるので、それをクリックします。すると、コマンドを入力するための黒い画面(コマンドプロンプト)が開きます。
コマンドプロンプトに「py -m pip install opencv-contrib-python」と入力してEnter キーを押します。
メッセージがいろいろ表示されるのでしばらく待ちます。インストールが完了すると画面に「C:\Users\xxx>」のように表示され、カーソルが点滅している状態になります。
同様に、Pythonでシリアル通信を行うためのライブラリもインストールします。「py -m pip install pyserial」と入力してEnter キーを押します。
ふたつのライブラリのインストールが完了したら、画面右上の「×」をクリックしてコマンドプロンプトを閉じます。
以下のような仕様のPythonスクリプトをつくります。
- パソコンのカメラを開きます。
- マイクロビットがつながっているシリアルポートを開きます。
- カメラから画像を取得します。
- 画像からArUcoマーカーを検出します。画像からID=0, 1のマーカーが検知できたら、各マーカーの「X座標」「Y座標」「マーカーの向き」を計算します。
- 1秒ごとに「ID0マーカーのX座標」「ID0マーカーのY座標」「ID0マーカーの向き」「ID1マーカーのX座標」「ID1マーカーのY座標」「ID1マーカーの向き」の4つの数値をカンマ区切りにして、シリアル通信で送信します。マーカーが検知できなかったときは数値を「-1」にします。
スクリプトは以下のとおりです。
先頭の方に記載されている「CAMERA_ID」「PORT」の値は、自分のPC環境やマイクロビットがつながっているUSBポートに応じて変更します。
import cv2
import serial
import time
import math
# ===== 設定 =====
CAMERA_ID = 0 # カメラ番号
PORT = "COM3" # マイクロビットをつないでいるUSBポート名
BAUDRATE = 115200 # マイクロビットとの通信速度
SEND_INTERVAL = 1.0 # データ送信間隔(秒)
ARUCO_DICT = cv2.aruco.DICT_4X4_50 # ArUcoマーカーの辞書を4x4ビット(50種類)にする
# マーカーの四隅の座標から「中心のX座標」「中心のY座標」「向き」を計算する関数
def get_marker_data(corners):
points = corners.reshape(4, 2)
# マーカーの中心座標
x = int(round(points[:, 0].mean()))
y = int(round(points[:, 1].mean()))
# マーカーの上辺の向き(左上相当の角から右上相当の角への方向)
dx = points[1][0] - points[0][0]
dy = points[1][1] - points[0][1]
# マーカーの角度
angle = int(round(math.degrees(math.atan2(dy, dx))))
angle = angle % 360
# 計算結果を返す
return x, y, angle
# メイン関数
def main():
# カメラを開く
cap = cv2.VideoCapture(CAMERA_ID)
if not cap.isOpened():
print("カメラを開けません。CAMERA_IDを確認してください。")
return
print("カメラを開きました。")
# ArUco検出器を準備
dictionary = cv2.aruco.getPredefinedDictionary(ARUCO_DICT)
parameters = cv2.aruco.DetectorParameters()
detector = cv2.aruco.ArucoDetector(dictionary, parameters)
# シリアルポートを開く
try:
ser = serial.Serial(PORT, BAUDRATE, timeout=1)
except serial.SerialException as e:
cap.release()
cv2.destroyAllWindows()
print("シリアルポートを開けません:", e)
return
# USB接続後の初期化を待つ
time.sleep(2)
print("シリアル通信を開始しました。")
print("終了するには、カメラ画面で q キーを押してください。")
# 処理の本体
next_send_time = time.monotonic()
marker_data = { 0: (-1, -1, -1), 1: (-1, -1, -1) } # マーカー座標データの初期化
try:
while True:
# カメラから画像を取得
ret, frame = cap.read()
if not ret:
print("カメラ画像を取得できません。")
break
# ArUcoマーカーを検出
corners, ids, rejected = detector.detectMarkers(frame)
if ids is not None: # マーカーが検知できたら
ids = ids.flatten()
# マーカーのIDが0, 1のとき、その座標情報をmarker_dataに代入する
for i, marker_id in enumerate(ids):
if marker_id in (0, 1):
data = get_marker_data(corners[i])
marker_data[int(marker_id)] = data
# 検出したマーカーを画像上に描画
cv2.aruco.drawDetectedMarkers(frame, corners, ids)
# ID、座標、向きを画像上に表示
for i, marker_id in enumerate(ids):
if marker_id in (0, 1):
x, y, angle = marker_data[int(marker_id)]
text = ( f"ID:{marker_id} " f"X:{x} Y:{y} A:{angle}" )
point = corners[i][0][0]
cv2.putText( frame, text, (int(point[0]), int(point[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2 )
# 1秒ごとにマイクロビットにデータ送信
now = time.monotonic()
if now >= next_send_time:
x0, y0, a0 = marker_data[0]
x1, y1, a1 = marker_data[1]
message = (
f"{x0},{y0},{a0},"
f"{x1},{y1},{a1}\n"
)
ser.write(message.encode("utf-8"))
print("送信:", message.strip())
marker_data = { 0: (-1, -1, -1), 1: (-1, -1, -1) } # マーカー座標データの初期化
next_send_time = now + SEND_INTERVAL
# カメラ画像を表示
cv2.imshow("ArUco to micro:bit", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
ser.close()
cap.release()
cv2.destroyAllWindows()
print("終了しました。")
# メイン関数を実行
if __name__ == "__main__":
main()
「メモ帳」などで新規にファイルを開き、このスクリプトの内容を入力したあと、「xxx.py」という拡張子でファイルを保存します。
保存した「xxx.py」ファイルをダブルクリックすると、カメラ画像表示ウィンドウとメッセージ表示ウィンドウが表示され、メッセージ表示ウィンドウには1秒ごとに送信したデータの内容が表示されます。
カメラ画像表示ウィンドウの上で「q」と入力するとスクリプトが終了できます。
実行例は以下のとおりです、この画像はMacで実行したときのものですが、Windows PCでも同じように実行できます。

マイクロビット用プログラム
今回はMakeCodeエディタで以下のようなプログラムをつくりました。

シリアル通信の「通信速度」は、Pythonスクリプトで定義したのと同じ「115200」を選択します。
Pythonスクリプトでは、末尾が「改行コード」になっている形式でメッセージを送信しているので、ここではシリアル通信で「改行コード」を受信するたびに処理を行うようにしています。
6つの数字をカンマで区切った文字列を受信するので、その文字列をカンマで分割したあと、6つの要素を整数に変換して変数に保存しています。
ここまでが、Pythonスクリプトとの通信に必要な部分です。それ以降は自分のすきなようにプログラムをつくることになります。
この例では、ふたつの座標がどのぐらい離れているかを計算し、その結果に応じてLEDマトリックスを2箇所だけ点灯するようにしました。マーカーどちらかの座標情報が取得できなかったときは「×」を表示します。
これでやりたいことが実現できました。
この方法であれば、生徒がPythonスクリプトの内容を理解する必要はありません。Pythonスクリプトを実行さえすれば、マーカー情報が1秒ごとにマイクロビットに送られてきて、生徒はその情報を使ってブロックプログラミングですきなプログラムをつくることができます。