白箱検出、3方式の試行。OpenCV HSV失敗からテンプレートマッチング0.994まで
白箱の検出方式を3回変えた日だった。
最初のテンプレートマッチングはスコア0.547でズレが54mm。OpenCV HSV白色マスク方式に切り替えたらロボットアームの筐体を拾って悪化。テンプレートマッチングに戻して新規テンプレートを12枚撮り直したら、スコアが0.994になった。
3回の試行で見えてきたのは、「方式の問題」だと思っていたことが「データの問題」だったということ。そして、白箱をpixel精度で検出できても、lookup tableのカバー範囲外なら動かないという、別レイヤーの壁。
出発点の状態
homography座標系への全面移行は前日に済んでいた。アフィン2点キャリブを廃止し、マット四隅自動検出 → homography.npy でpixel→Unity座標変換する方式が動いている。赤ブロックの検出はpixel精度3mm以内で確認済み。
問題は白箱だった。
白箱検出 Unity座標: (-0.239, 0.238)
実測中心: (-0.185, +0.222)
X方向誤差: 54mm白箱検出にはテンプレートマッチングを使っていた。auto学習で追加された古いテンプレート3枚がある。スコアの最高値は0.547で、青枠が実物より一回り大きい。
「余計ひどいね。ちゃんと白箱を認識しているか、確認した方がいいんじゃない?」
この問いかけから、3回の試行が始まった。
試行1: テンプレートマッチング(古い3枚)
3枚のテンプレート × 5スケールで15通りのスコアを全探索。
テンプレート | 0.70 | 0.85 | 1.00 | 1.15 | 1.30 |
|---|---|---|---|---|---|
white_box.png | 0.503 | 0.456 | 0.446 | 0.442 | 0.486 |
white_box_auto_20260419_083634.png | 0.471 | 0.515 | 0.487 | 0.504 | 0.487 |
white_box_auto_20260419_122937.png | 0.489 | 0.450 | 0.475 | 0.536 | 0.547 |
全体が0.44〜0.55の団子状態。最大スケール(1.30)が選ばれている=テンプレートが実物より小さい側に振れている。
white_box_auto_20260419_083634.png と 122937.png はLOG163-164でマット位置を100mm上にずらし、homography再キャリブをする以前に自動学習で保存されたもの。物理的な設定が変わっていたのに、テンプレートが更新されていなかった。
この段階でやるべきだったこと: 現環境でテンプレートを撮り直す。 そこには気づかず、方式を変える方向に舵を切った。
試行2: OpenCV HSV白色マスク方式
白箱は白い。ならばHSV色空間で白を抽出して最大面積輪郭を取ればいい。テンプレート依存がなくなり、環境変化にも強くなるはず。
バックアップ後、テンプレート関連を全削除した。
削除した定数: TEMPLATE_DIR / TEMPLATE_SCALES / TEMPLATE_SCORE_THRESHOLD
TEMPLATE_AUTO_ADD_THRESHOLD / TEMPLATE_SIMILARITY_THRESHOLD
TEMPLATE_MAX_COUNT / TEMPLATE_PROTECTED / TEMPLATE_LEARNING_LOG
削除したグローバル: ALL_TEMPLATES
削除した関数: load_all_templates() / _log_learning_event() / _try_auto_add_template()914行 → 808行(-106行)。find_white_box() の実装:
def find_white_box(frame, debug_log=False):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 180])
upper = np.array([180, 50, 255])
mask = cv2.inRange(hsv, lower, upper)
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# マット範囲外をゼロに
if _mat_px_range is not None:
min_x, max_x, min_y, max_y = _mat_px_range
mask_clipped = np.zeros_like(mask)
mask_clipped[min_y:max_y+1, min_x:max_x+1] = \
mask[min_y:max_y+1, min_x:max_x+1]
mask = mask_clipped
contours, _ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return None
best_contour = max(contours, key=cv2.contourArea)
if cv2.contourArea(best_contour) < 3000:
return None
x, y, w, h = cv2.boundingRect(best_contour)
return (x + w // 2, y + h // 2)実行したログ:
[Mac] ✓ 白箱検出: HSV白色マスク方式
...
[Mac] ✓ 白箱検出: pixel=(700, 250), area=31190px²
[Mac] → Unity座標: (0.1188, 0.0041)
[Mac] PICK Unity座標: (0.2115, 0.2966)
[Mac] △ PICK lookup失敗: dist=125.4mm > 100mm
[Mac] × PICK lookup失敗 → 固定角度モードにfallback白箱の実物は画面右下にある。しかし検出座標は pixel=(700, 250) ―― マット上部だった。
デバッグ可視化を追加して確認した。_save_white_debug() を実装し、白色マスクを青半透明、全候補の外接矩形を緑、採用領域を赤で重畳して ~/r2r2r-scripts/logs/debug_frames/ に保存する。
def _save_white_debug(frame, mask, candidates, best):
dbg_dir = os.path.join(LOGS_DIR, "debug_frames")
os.makedirs(dbg_dir, exist_ok=True)
cv2.imwrite(os.path.join(dbg_dir, "white_mask_frame.jpg"), mask)
debug_img = frame.copy()
blue_overlay = np.zeros_like(debug_img)
blue_overlay[:, :, 0] = mask
cv2.addWeighted(blue_overlay, 0.5, debug_img, 1.0, 0, debug_img)
if _mat_px_range is not None:
min_x, max_x, min_y, max_y = _mat_px_range
cv2.rectangle(debug_img,
(min_x, min_y), (max_x, max_y), (0, 255, 255), 2)
for (area, cx, cy, x, y, w, h) in candidates:
cv2.rectangle(debug_img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(debug_img, f"{area:.0f}", (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
if best is not None:
_, cx, cy, bx, by, bw, bh = best
cv2.rectangle(debug_img,
(bx, by), (bx + bw, by + bh), (0, 0, 255), 3)
cv2.circle(debug_img, (cx, cy), 10, (0, 0, 255), 2)
cv2.imwrite(os.path.join(dbg_dir, "white_debug_frame.jpg"), debug_img)保存された white_debug_frame.jpg を見ると、マット上部に広がる青い領域がはっきりわかった。ロボットアームの白い筐体がマットに侵入している部分が、そのまま最大白色領域として認識されていた。
「白い」という条件だけでは、白箱と白いアームを区別できない。 マット範囲フィルタで「マット外」は切り落とせても、アームはマット上に存在する。HSV方式の限界はここだった。
「これ前みたいに白箱を画像で覚えさせた方がいいんじゃないの?」
試行3: テンプレートマッチング復帰 + ROIクリップ + 12枚撮影
テンプレート方式に戻す。ただし今回は cv2.matchTemplate をマット範囲ROI内でのみ実行する改良を加えた。アームの白筐体を物理的に候補から外せる。
def find_white_box(frame, debug_log=False):
if _mat_px_range is None:
return None
min_x, max_x, min_y, max_y = _mat_px_range
# マット範囲をROIとして切り出し
roi = frame[min_y:max_y+1, min_x:max_x+1]
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
best_score = -1.0
best_result = None
for template_info in ALL_TEMPLATES:
template_gray = template_info['gray']
template_name = template_info['name']
for scale in TEMPLATE_SCALES:
scaled_w = int(template_gray.shape[1] * scale)
scaled_h = int(template_gray.shape[0] * scale)
if scaled_w >= roi.shape[1] or scaled_h >= roi.shape[0]:
continue
scaled_template = cv2.resize(template_gray, (scaled_w, scaled_h))
result = cv2.matchTemplate(
gray_roi, scaled_template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
if max_val > best_score:
best_score = max_val
mx, my = max_loc
# ROI座標 → 元frame座標に変換
cx = mx + min_x + scaled_w // 2
cy = my + min_y + scaled_h // 2
best_result = (cx, cy, scaled_w, scaled_h, template_name)
if best_score < TEMPLATE_SCORE_THRESHOLD:
return None
cx, cy, w, h, template_name = best_result
return (cx, cy)バックアップ: r2r2r_loop.py.bak_DAY165_pre_template_restore(848行 → 939行、+91行)
古いテンプレート3枚のまま実行。アームの誤検出は消えたが、スコアは依然0.547。ROIクリップはアームを除外できても、「データが古い」という根本は変わらない。
ここで新規テンプレート撮影専用のスクリプトを書いた。capture_white_box_templates.py(143行)。
#!/usr/bin/env python3
"""
capture_white_box_templates.py
白箱テンプレートを複数角度・位置で撮影・切り出し保存する。
SPACE: cv2.selectROIで矩形選択→切り出し保存 / Q: 終了
"""
import os, sys, time, cv2, numpy as np
from datetime import datetime
from mat_corner_detection import (
capture_frames, detect_corners, remove_outliers_and_average)
CAMERA_INDEX = 2
WARMUP_FRAMES = 30
TEMPLATES_DIR = os.path.expanduser("~/r2r2r-scripts/templates")
MIN_W, MIN_H = 30, 30
def detect_mat_range(cap):
frames = capture_frames(cap, 10)
results = [detect_corners(f) for f in frames
if detect_corners(f) is not None]
if len(results) < 3:
return None
avg = remove_outliers_and_average(results)
xs, ys = avg[:, 0], avg[:, 1]
return (int(xs.min()), int(xs.max()), int(ys.min()), int(ys.max()))
def main():
os.makedirs(TEMPLATES_DIR, exist_ok=True)
cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
print("[ERROR] カメラを開けませんでした"); sys.exit(1)
for _ in range(WARMUP_FRAMES): cap.read()
time.sleep(0.3)
mat_px_range = detect_mat_range(cap)
capture_count = 0
while True:
ret, frame = cap.read()
if not ret: continue
disp = frame.copy()
if mat_px_range:
min_x, max_x, min_y, max_y = mat_px_range
cv2.rectangle(disp,
(min_x, min_y), (max_x, max_y), (0, 255, 255), 2)
cv2.putText(disp,
f"Saved: {capture_count} | SPACE: capture Q: quit",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
cv2.imshow("Capture White Box Templates", disp)
key = cv2.waitKey(1) & 0xFF
if key in (ord('q'), ord('Q')): break
if key == ord(' '):
roi = cv2.selectROI("Select Region", frame,
showCrosshair=True, fromCenter=False)
cv2.destroyWindow("Select Region")
x, y, w, h = roi
if w < MIN_W or h < MIN_H:
print(f"[WARN] 小さすぎます ({w}x{h})")
continue
capture_count += 1
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
dst = os.path.join(TEMPLATES_DIR,
f"white_box_{ts}_{capture_count:03d}.png")
cv2.imwrite(dst, frame[y:y+h, x:x+w])
print(f"[SAVED] {os.path.basename(dst)} ({w}x{h}px)"
f" — 累計 {capture_count}枚")
cap.release()
cv2.destroyAllWindows()
print(f"\n[INFO] 終了。テンプレート合計: {capture_count}枚")
if __name__ == "__main__": main()古いテンプレート3枚を templates/archive_pre_DAY165/ に退避してから撮影を開始。
マット上に白箱を置き、位置と向きを変えながら12回。四隅・四辺中央・中央付近の3×4グリッドを念頭に、向きは0°/15°/30°/45°を循環させた。アームに近い位置(影が落ちる)、マット端(照り返しが強い)も含めた。
# | ファイル名 | サイズ |
|---|---|---|
001 | white_box_20260420_192715_001.png | 186×167 |
002 | white_box_20260420_192733_002.png | 180×184 |
003 | white_box_20260420_192746_003.png | 193×171 |
004 | white_box_20260420_192758_004.png | 188×190 |
005 | white_box_20260420_192808_005.png | 174×191 |
006 | white_box_20260420_192822_006.png | 188×183 |
007 | white_box_20260420_192831_007.png | 188×191 |
008 | white_box_20260420_192845_008.png | 183×170 |
009 | white_box_20260420_192854_009.png | 195×185 |
010 | white_box_20260420_192905_010.png | 184×190 |
011 | white_box_20260420_192915_011.png | 185×189 |
012 | white_box_20260420_192926_012.png | 192×187 |
幅174〜195、高さ167〜191px。合計12枚、約400KB。
スコア0.994
12枚を読み込ませて実行した。起動ログ:
=== R2R2R Loop (homography方式) ===
[Mac] FK scan CSV読み込み中...
[Mac] ✓ 213312点ロード, ee_yフィルタ後: 156603点
[Mac] ✓ テンプレート 12枚読み込み完了
- white_box_20260420_192715_001.png: 186x167
...(12枚)...
- white_box_20260420_192926_012.png: 192x187
[Mac] ✓ マット範囲: X=[291, 1543] Y=[45, 956]スコア一覧(scale=1.00 抜粋):
テンプレート | scale=0.85 | scale=1.00 | scale=1.15 |
|---|---|---|---|
001.png | 0.784 | 0.887 | 0.643 |
002.png | 0.746 | 0.937 | 0.640 |
007.png | 0.744 | 0.817 | 0.624 |
008.png | 0.793 | 0.815 | 0.598 |
011.png | 0.802 | 0.910 | 0.690 |
012.png | 0.802 | 0.994 | 0.628 |
最高スコア: 0.994 (threshold=0.5)
✓ 白箱検出: pixel=(1434, 835)
score=0.994, template=white_box_20260420_192926_012.png × scale=1.00
→ Unity座標: (-0.2677, 0.3128)0.547 → 0.994。 Detection画面の青い矩形が白箱の外形とピタリ一致した。複数テンプレートが0.8〜0.9台を出していて、冗長性がある。どれかが外れても他でカバーできる。
lookup結果: PICK成功 / PLACE範囲外
[Mac] PICK Unity座標: (0.0883, 0.3269)
[Mac] ✓ PICK lookup成功: ee=(0.0641, 0.0412, 0.2313) dist=98.6mm
[Mac] PICK angles=[-15.0, 107.7, -63.3, 3.6, -40.1, -30.0]
[Mac] PLACE Unity座標: (-0.2677, 0.3128)
[Mac] △ PLACE lookup失敗: dist=188.6mm > 100mm
[Mac] △ PLACE lookup失敗 → PLACE固定角度 fallbackPICK側は成功(dist=98.6mm、閾値100mmギリギリ)。PLACE側は失敗。白箱のUnity座標(-0.2677, 0.3128) = ロボット座標 X=-268mm, Y=313mm付近。現行 fk_scan_motion_based.csv(LOG151生成)のカバー範囲 Robot X -30〜-90, Y +100〜+180 から完全に外れている。
レイヤー | 状態 | 数値 |
|---|---|---|
白箱pixel検出 | ✅ 成功 | score 0.994 |
赤ブロック→Unity変換 | ✅ 成功 | 誤差3mm以内 |
PICK lookup | ✅ 成功 | dist 98.6mm |
PLACE lookup | ❌ 失敗 | dist 188.6mm(範囲外) |
pixel検出の成功と実機動作の成功は別レイヤーにある。どのレイヤーで止まっているかを切り分けることが、次の手を正しく決める前提になる。
判明したこと
方式を変えるより先に、データを疑うべきだった。朝の段階でテンプレートが古いと分かっていれば、HSV方式への切り替えというタスクは発生しなかった。「テンプレートマッチングのスコアが低い」は「方式が悪い」と「データが古い」の両方を含んでいる。その切り分けをせずに方式を変えたのが遠回りだった。
環境依存のテンプレートは、環境が変わるたびに更新するものだ。LOG163-164でマット位置を変えてhomographyを再キャリブした。そのタイミングでテンプレートも更新すべきだった。auto学習は「閾値を通過した瞬間に保存する」だけで、意図的なバリエーション撮影ではない。
テンプレートマッチングという古典的な手法でも、12枚のバリエーション撮影でスコア0.994まで到達する。問題は方式ではなく、環境に合わせてデータを更新する運用があるかどうかだった。
バックアップ一覧
ファイル名 | 内容 |
|---|---|
r2r2r_loop.py.bak_DAY165_pre_homography | homography移行前 |
r2r2r_loop.py.bak_DAY165_pre_whitehsv | HSV方式切替前 |
r2r2r_loop.py.bak_DAY165_pre_template_restore | テンプレート復元前 |
homography.npy.bak_20260420_181438 | homography再キャリブ前 |
templates/archive_pre_DAY165/ | 古いテンプレート3枚 |
次の課題
PLACE側lookup tableの範囲拡張。現行カバー範囲 Robot X -30〜-90, Y +100〜+180 に対し、白箱の置かれる位置は X=-268mm, Y=313mm 付近。PICK側もdist=98.6mmでギリギリなので、両方をカバーするUnity FK scanの範囲再設計が必要になる。
白箱が見える。次はその位置まで腕を届かせる。
