
M5Stack UnitV (UnitV K210 AI Camera)で物体を認識して、スタックチャンが追従して動くようにしてみました。
目次
(1) UnitVの開発環境を準備する
(2) UnitVにプログラムを登録する
(3) M5Stack側の作成
(4) 認識する色を変更する
(5) 人の顔を検出するには?
(1) UnitVの開発環境を準備する
UnitVとは?
UnitVはKendryte K210を搭載したAIカメラで、ニューラルネットワークプロセッサ(KPU)を使用して高性能な画像処理をすることができます。インターフェースはUSBとUARTの2つがあり、USBは開発に、UARTはデバイスとの通信用に使用します。

私は最初これで何ができるのか、どうやって使うのかを全く理解しないまま買って積んでいました。少し調べてみるといろいろと活用できそうだったので、実用的なアプリケーションを作ってみることにしました。
UnitVのプログラミング環境
UnitVにはMaixPyというK210用のMicroPython環境が使用可能です。カメラの映像をUnitV内で、MicroPythonのプログラムを使って処理を行い、その結果をUARTに送信し、それをM5Stackで受信して処理する、というのが一連の流れになるそうです。なるほど、つまりUnitVとM5Stackはテキストのデータでやりとりをするんですね。
Micro SDカードを用意する
プログラムを保存するためのMicro SDカードを用意します。カードによって相性があるようです。家に適当に転がってたpqiの4GBのカードでも大丈夫でした。SD Card Formatterで初期化したらUnitVに挿入します。
MaixPy IDEをダウンロードする
UnitVの開発にはMaixPy IDEを使用します。M5Stackのドキュメントページのリンクからダウンロードするか、ここからダウンロードしてインストールします。まずはUnitVがちゃんと認識して動作するか試してみましょう。設定方法や接続までの手順はドキュメントを参考にします。
右上のフレームバッファのところにカメラの映像が出るはずですが、環境によっては出ないことがありました。どうもUSBの接続方法によって出たり出なかったりします。
ファームウェアをアップデートする(任意)
これは必須ではないので飛ばしてかまいません。デフォルトでインストールされているファームウェアは v5.1.2 のようですが、これをv6にするとフレームレートが早くなるそうです。
ファームウェアを書き込むソフトのダウンロード
UnitVにファームウェアを書き込むためのソフト、kflash_guiをダウンロードします。M5Stackのドキュメントページのリンクからダウンロードするか、ここ(こちらの方が新しい)からダウンロードします。
ファームウェアのダウンロード
こちらのページにMaixPyのファームウェアがあります。ファイルが表示されない場合は再読込してみてください。最新のフォルダを開くと maixpy_v0.6.3_2_gd8901fd22_m5stickv.bin のように m5stickv という名前が付いたファイルがあります。これがUnitVで使用できるファームウェアのようです。これをダウンロードします。
ファームウェアの書き込み
kflash_guiを起動して、Open Fileでファームウェアを選択。BoardをM5StickV、Portを接続したCOMポートを選択、Speed modeをFast modeにしてDownloadを押すとファームウェアが書き込まれます。

アップロードするのになんでDownloadなの?? って不思議な感じがしますが、デバイス側から見てダウンロードってことなんでしょうね。
今回ダウンロードしたファームウェアは拡張子が .bin でしたが、M5Stackのドキュメントページにあるファームウェアは .kfpkg です。これはファームウェアやモデルデータなどの複数のデータを1つにまとめたファイルになります。実はZIPファイルで、拡張子を変えると中身が見れます。

ファイルが4つありますね。1つはJSONファイルで、何をどのアドレスに書き込むのかを指定するデータのようです。MaixPy以外にも、facedetect.kmodelとm5stickv_resources.imgというファイルもあります。facedetect.kmodelは名前から推測される通り、顔認識のモデルのようです。これでしょうかね?もう一つはよくわかりません。
今回はMaixPyのみを書き込みましたが、アドレスが重複していなければ、それ以外のデータは残ったままだと思います。たぶん。(確認してませんが)
(2) UnitVにプログラムを登録する
UnitVの動作が一通り確認できたら、MicroPythonのプログラムをUnitVに登録します。
プログラムの目的
今回は「ずんだもんが、枝豆を欲しくて枝豆の方を見つめる」という設定です。枝豆の画像認識をするとなると枝豆の画像を学習しなくてはいけなくて面倒なので、単純に緑色の物体を認識するようにします。
UnitVのプログラム
UnitVのプログラムは GitHub からもダウンロードできます。
# 緑色の領域を追跡する
# for M5Stack UnitV (K210)
#
# Copyright (c) 2025 Kaz (https://akibabara.com/blog/)
# Released under the MIT license.
# see https://opensource.org/licenses/MIT
import sensor
import KPU as kpu
import image
import lcd
import time
from machine import UART
from fpioa_manager import fm
# from modules import ws2812 # WS2812はデフォルトのファームウェア以外では有効になっていない可能性あり
# 閾値の設定(MaixPy IDEのツール→マシンビジョン→閾値エディタで調整する)
#green_threshold = (0, 80, -70, -10, -0, 30) # 緑色の領域を追跡するための閾値(サンプルのオリジナル値)
green_threshold = (0, 80, -70, -15, -0, 30) # 緑色の領域を追跡するための閾値
# LCD初期化(UnitVはディスプレイが無いから不要)
# lcd.init()
# lcd.rotation(0)
# シリアルポートの設定
fm.register(35, fm.fpioa.UART1_TX, force=True)
fm.register(34, fm.fpioa.UART1_RX, force=True)
uart = UART(UART.UART1, 115200,8,0,0, timeout=1000, read_buf_len=4096)
# カメラモジュールの準備
sensor.reset(dual_buff=True) # デュアルバッファを有効にする
sensor.set_pixformat(sensor.RGB565) # カラーモード RGB565
sensor.set_framesize(sensor.QVGA) # サイズ QVGA 320x240
sensor.set_hmirror(0) #カメラミラーリングの設定
sensor.set_vflip(0) #カメラのフリップを設定する
sensor.skip_frames(time = 2000) # 2秒間フレームをスキップして安定化
sensor.set_auto_exposure(False, 10000) # 蛍光灯のフリッカー対策(50Hz) 固定露出時間10ms(自動露出無効)
sensor.run(1) #カメラを有効にする
# その他の初期化
# class_ws2812 = ws2812(8, 1)
clock = time.clock()
color = (255, 0, 0)
max_area = 25 # 最大面積(ピクセル)
no_hit = 0
# メインループ
while True:
# 緑領域を検出する
clock.tick()
img = sensor.snapshot()
# blobs = img.find_blobs([green_threshold])
# blobs = img.find_blobs([green_threshold], x_stride=2, y_stride=2, pixels_threshold=100, merge=True, margin=20)
# blobs = img.find_blobs([green_threshold], x_stride=2, y_stride=2, pixels_threshold=64)
blobs = img.find_blobs([green_threshold], pixels_threshold=max_area) # 面積36以上の領域を検出
# 最大の緑色領域を検出
if blobs:
# print(blobs)
blob = max(blobs, key=lambda b: b.area()) # 面積が最大の領域を取得
# 認識した緑色領域を表示
img.draw_rectangle(blob[0:4], color=color)
img.draw_cross(blob[5], blob[6], color=color)
# ディスプレイ出力
# lcd.display(img)
fps = clock.fps()
#print("%2.1f fps" % fps)
# シリアルに出力
if blobs:
# hit, x, y, w, h, pixel, cx, cy, fps
data = "1,{},{},{},{},{},{},{},{}".format(blob[0], blob[1], blob[2], blob[3], blob[4], blob[5], blob[6], int(fps))
print(data)
uart.write(data + "\r\n")
else:
# 見つからない場合も死活監視のため出力
no_hit += 1
if no_hit > 30:
data = "0,,,,,,,{}".format(int(fps))
print(data)
uart.write(data + "\r\n")
no_hit = 0
プログラムの仕組み
1フレーム分の画像データを取得し、find_blobs()で緑色の領域を検出しています。普通に行うと何も対象物がなくても背後に映ったわずかな緑色っぽいところまで検出してしまうので、pixels_thresholdで一定の面積以上のもののみに絞っています。今回は25ピクセルに設定してみました。
img = sensor.snapshot()
blobs = img.find_blobs([green_threshold], pixels_threshold=max_area)
検出されるとblobsには複数のデータが入っていることもあります。
[
{
"x": 112,
"y": 13,
"w": 13,
"h": 31,
"pixels": 220,
"cx": 117,
"cy": 26,
"rotation": 1.757753,
"code": 1,
"count": 1
},
{
"x": 140,
"y": 233,
"w": 7,
"h": 6,
"pixels": 40,
"cx": 143,
"cy": 236,
"rotation": 0.759107,
"code": 1,
"count": 1
}
]
今回は最も緑色の領域(area)が大きいものを「枝豆」と認識させるようにします。
blob = max(blobs, key=lambda b: b.area())
これでデータは1つに絞られました。これをM5Stackに渡してあげれば目的達成です。今回はCSV形式で渡すことにしました。先頭が1なら検出あり、0ならなしです。
# hit, x, y, w, h, pixel, cx, cy, fps
data = "1,{},{},{},{},{},{},{},{}".format(blob[0], blob[1], blob[2], blob[3], blob[4], blob[5], blob[6], int(fps))
print(data)
uart.write(data + "\r\n")
print()はMaixPy IDEのシリアルポート(USB)への出力で、これだけではGroveポート側には出力されないようです。uart.write() でGroveポート側に出力します。これでM5Stackで受信できるようになります。
動作チェック
MaixPy IDEのファイルを開くボタンでプログラムを開きます。左下の接続ボタンを押してUnitVと接続し、左下の実行ボタンを押すと右上に映像が表示されます。この状態でカメラに緑色のものを移すと検出されるのがわかります。

シリアルターミナルを押すとCSV形式で検出した情報が表示されています。
UnitVの起動時に実行されるように登録する
このままではMaixPy IDE上で実行しているだけなので、UnitV単独でプログラムが動くようにします。
ツール→Save open script to board(boot.py) を実行すると、UnitVのSDカードに書き込まれます。このときデータ量を抑えるために不要なコメントなどが取り除かれて保存されます。直接SDカードにアップロードしても大丈夫なはずですが、IDEから行った方が無難です。
シリアルターミナルで動作テスト
最後にUnitV単独で動作するかテストしてみましょう。MaixPy IDEを閉じ、TeraTermなどでシリアルポートを開いてみましょう。

UnitV単独でもちゃんとデータが出力されていますね。
(3) M5Stack側の作成
UnitVの準備が終わったら次はM5Stack側を作ります。スタックチャンの作り方を最初から説明しているととても1ページでは説明できないので割愛します。ヘッダー画像ではずんだもんになっていますが、こちらはまだ開発途中なので、以下のプログラムはずんだもんじゃない普通のスタックチャンで説明していきます。
GPIOの接続
M5Stack Core2のPort AとUnitVを接続します。サーボモーターはGPIO 27, 19に接続します。私のスタックチャンは縦表示のマグネット脱着仕様で、背面からサーボ用の配線を取り出しています。
// GPIO設定
#define GPIO_SERVO_X 27 // サーボ X軸
#define GPIO_SERVO_Y 19 // サーボ Y軸
#define GPIO_UART_RX 32 // UnitV UART RX (Core2 Port A)
#define GPIO_UART_TX 33 // UnitV UART TX (Core2 Port A)
UnitVはM5Stackに固定しない
今回はUnitVはM5Stackに固定しません。固定するとカメラも一緒に動いてしまうので、現在向いている方向からどれくらい動かすか、という計算が必要になってきてしまいます。今回は実験で、なんとなくそれっぽい動きができればいいので、カメラは別の場所に固定しておきました。
M5Stack側のプログラム
プログラムは GitHub からダウンロードできます。
・stackchan_unitv_demo.ino(メインプログラム)
・ServoChan.h(サーボライブラリ)
・ServoChan.cpp(サーボライブラリ)
・tools.h(そのほかのプログラム)
必要なライブラリ
・M5Unified
・ESP32Servo
・ServoEasing
・M5Stack_Avatar
ターゲットの位置情報をサーボの座標に変換
UnitVから送られてくるデータは、320x240pxの映像の中のどの座標に緑色の物体があるかという情報です。これを元にサーボを動かします。
UnitVData unitv = get_unitv_latest_data();
if (unitv.hit == 1 && unitv.pixel > 200) {
float fx = -map(unitv.cx, 0, UNITV_CAM_WIDTH, -1000, 1000) / 1000.0;
float fy = -map(unitv.cy, 0, UNITV_CAM_HEIGHT, -1000, 1000) / 1000.0;
servo.headPosition(fx, fy); // 頭の向きを変更
}
hitは認識したときのみ1が来ます。pixcelは認識した物体の面積でUnitV側のプログラムで25以上の場合のみ出力するようにしていますが、実際に動かしてみると部屋の奥にあるキムワイプに反応してしまったので、より大きなもののみに反応するようにしました。
servo.headPosition(x, y) は角度ではなく -1.0~1.0 の範囲でサーボの角度を変更する関数です。map()関数を使って物体の中央座標(cx/cy)をこのサーボの可動範囲に変換しています。
コンパイル & 実行
Arduino IDEでコンパイルしてM5Stack Core2に書き込んだら完了です。UnitVを接続して、緑色のものをカメラの前で動かすと、カメラに追従して動くはずです。
何も緑色のものがないのに反応してしまう場合は、反応する範囲が広すぎる可能性があります。後述する「認識する色を変更する」のところで調整方法を説明しています。
Serial2のデバッグ表示
Bボタンを押しながら電源を入れると、UnitV側のシリアルポートのデータが画面に出ます。本当にデータが受信できてるかデバッグしたいときにどうぞ。
(4) 認識する色を変更する
別の色を検出させたい場合や、何もないところに反応してしまう場合は調整が必要です。find_blobs()に与える以下のパラメーターを変更します。
green_threshold = (0, 80, -70, -15, -0, 30)
って、これを見てもなんのこっちゃって感じですね。Lab色空間という色の指定方法で与えるようです。左から L (min), L (max), A (min), A (max), B (min), B (max) で、Lは明るさ(100~0)、Aは緑(-100)~赤(+100)、Bは青(-100)~黄(+100) の値を取ります。
MaixPy IDEのツール→マシンビジョン→しきい値エディタを開くと、画像を見ながら範囲を調整することができます。まずは現在プログラムで設定している値を上から順に設定してから調整するといいと思います。

L分、Lマックス、これmin, maxの誤訳ですな。調整が終わったらテキストボックスの値をコピペすればOKです。
ちなみに複数の色を同時に検出することもできるみたいです。
import sensor
import image
import lcd
import time
lcd.init()
clock = time.clock()
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)
threshold = [
(0, 80, -70, -15, -0, 30), # 緑
(0, 81, 14, 72, -2, 46) # 赤
]
while True:
img=sensor.snapshot()
clock.tick()
blobs = img.find_blobs(threshold)
if blobs:
for b in blobs:
col = (0,255,0) if b.code() == 1 else (255,0,0)
img.draw_rectangle(b[0:4], col)
img.draw_cross(b[5], b[6], col)
lcd.display(img)
fps = clock.fps()
print("%2.1f fps" % fps)

緑と赤を区別して識別できました!
find_blobs() 以外にも find_lines() や find_circles() などいろいろあるみたいなので、試してみると面白いかもしれませんね。
(5) 人の顔を検出するには?
まずは簡単なテスト
これまでUnitVのカメラで単純に緑色の物体認識をしましたが、人の顔のような、より高度な検出もできます。デフォルトのファームウェアに含まれている facedetect.kmodel を使用して顔認識をするプログラムです。
# デフォルト状態のファームウェアに含まれているfacedetect.kmodelを使用する
import sensor
import KPU as kpu
import lcd
import time
lcd.init()
clock = time.clock()
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)
# モデルの読み込み
task_fd = kpu.load(0x300000)
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
kpu.init_yolo2(task_fd, 0.5, 0.3, 5, anchor)
while True:
img = sensor.snapshot()
code = kpu.run_yolo2(task_fd, img)
if code:
print(code)
for i in code:
img.draw_rectangle(i.x(), i.y(), i.w(), i.h())
lcd.display(img)
kpu.deinit(task_fd)
実行してカメラに顔が映ると、その範囲が検出されます。

顔を検出すると以下のようなデータが返ってきます。
[
{
"x": 130,
"y": 130,
"w": 82,
"h": 111,
"value": 0.663561,
"classid": 0,
"index": 0,
"objnum": 1
}
]
辞書型のデータが配列に入って出てくるので、この座標をUARTで出力させてあげれば、顔認識するスタックチャンが作れますね。ただし、この方法では複数の人を同時に検出してしまうので、2人以上いるとスタックチャンはどちらを向いたらいいかわからなくなってしまいます。
スタックチャンが顔に反応するようにするプログラム
認識した顔の座標データをUARTで送信してあげれば、そのままスタックチャンで使用することができます。UnitVのプログラムは GitHub からもダウンロードできます。
# 人の顔を追跡する
# for M5Stack UnitV (K210)
#
# Copyright (c) 2025 Kaz (https://akibabara.com/blog/)
# Released under the MIT license.
# see https://opensource.org/licenses/MIT
import sensor
import KPU as kpu
import image
import lcd
import time
from machine import UART
from fpioa_manager import fm
# モデルの読み込み
task_fd = kpu.load(0x300000) # デフォルトのファームウェアの0x300000にあるfacedetect.kmodelを読み込む
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
kpu.init_yolo2(task_fd, 0.5, 0.3, 5, anchor)
# シリアルポートの設定
fm.register(35, fm.fpioa.UART1_TX, force=True)
fm.register(34, fm.fpioa.UART1_RX, force=True)
uart = UART(UART.UART1, 115200,8,0,0, timeout=1000, read_buf_len=4096)
# カメラモジュールの準備
sensor.reset(dual_buff=True) # デュアルバッファを有効にする
sensor.set_pixformat(sensor.RGB565) # カラーモード RGB565
sensor.set_framesize(sensor.QVGA) # サイズ QVGA 320x240
sensor.set_hmirror(0) #カメラミラーリングの設定
sensor.set_vflip(0) #カメラのフリップを設定する
sensor.skip_frames(time = 2000) # 2秒間フレームをスキップして安定化
sensor.set_auto_exposure(False, 10000) # 蛍光灯のフリッカー対策(50Hz) 固定露出時間10ms(自動露出無効)
sensor.run(1) #カメラを有効にする
# その他の初期化
clock = time.clock()
no_hit = 0
# メインループ
while True:
# 顔を検出する
clock.tick()
img = sensor.snapshot()
code = kpu.run_yolo2(task_fd, img)
if code:
#print(code)
for i in code:
# 認識した顔の領域を表示
img.draw_rectangle(i.x(), i.y(), i.w(), i.h())
# ディスプレイ出力
fps = clock.fps()
# シリアルに出力
if code:
# hit, x, y, w, h, pixel, cx, cy, fps のフォーマットに合わせる
x = code[0].x()
y = code[0].y()
w = code[0].w()
h = code[0].h()
cx = x + w / 2
cy = y + h / 2
area = w * h /2
data = "1,{},{},{},{},{},{},{},{}".format(x, y, w, h, area, cx, cy, int(fps))
print(data)
uart.write(data + "\r\n")
else:
# 見つからない場合も死活監視のため出力
no_hit += 1
if no_hit > 30:
data = "0,,,,,,,{}".format(int(fps))
print(data)
uart.write(data + "\r\n")
no_hit = 0
同じフォーマットでCSVで出力しているので、スタックチャン側のプログラムはそのままで動きます。
複数の人の中から人を区別して追跡するには
今まで説明してきた方法では複数の人がいても、どちらが誰なのか区別できませんでした。こちら のモデルを使うと人を区別して検出することができます。このモデルではまず顔を検出して、その顔から目鼻口の座標を抽出、その特徴から人を区別しているようです。このモデルを使うには以下のページが詳しいので、詳しくはこちらを参考にしてみてください。
#M5StickV でお顔認証する手順
この記事の中でファームウェアの再ビルドの説明がありますが、現在はこの方法ではできなくなっています。最近私が試した方法が こちら にありますのでよろしければ参考にしてみてください。
公開されているモデルを利用したり、自分でモデルの学習をすることにも興味があるので、気が向いたら(難しそうだ…)やってみたいと思います。
