"""依頼110 E-4 補足: 閉じたあと, ブロックが爪先から離れた時刻を上カメラで追う. 0.5 秒おきのコマで, 赤いブロックの重心と, 赤にいちばん近い青い印2つ (間が 60〜150px のもの = 爪先の印) の 中点の距離 sep を測る. 閉じた直後 (+1.0〜2.0 秒) の sep の中央値を基準にし, 基準より 40px 以上離れたコマが 2つ続いた最初の時刻を「離れた」とする. HOME に戻る区間と, 次に爪を開く命令のあとは見ない. 真上からの画なので, ブロックが爪といっしょに持ち上がったか (高さ) は分からない. 分かるのは横に離れたことだけ. 出力: e_track.csv (試行ごと) / e_track_series.csv (コマごと) """ import csv import json from pathlib import Path import cv2 import numpy as np ROOT = Path("/Volumes/KIOXIA/r2r2r-data") RUNS = ROOT / "runs" / "pi05_policy" OUT = ROOT / "lab" / "twin" / "20261107_失敗の再生と置き場所" / "E_つかみと持ち上げ" SEP_JUMP_PX = 40 def height_fn(): c = np.array(json.load(open(OUT / "e_fk_fit.json"))["coef"]) def H(q): j1, j2, j3, j4 = [np.radians(q[k]) for k in (1, 2, 3, 4)] b = j1 + j2 f = np.array([1, np.sin(j1), np.cos(j1), np.sin(b), np.cos(b), np.sin(b) * np.cos(j4), np.cos(b) * np.cos(j4), np.sin(b) * np.sin(j4) * np.cos(j3), np.cos(b) * np.sin(j4) * np.cos(j3), np.sin(j4) * np.sin(j3)]) return float(f @ c) return H def find(f): hsv = cv2.cvtColor(f, cv2.COLOR_BGR2HSV) red = cv2.inRange(hsv, (0, 120, 70), (10, 255, 255)) | cv2.inRange(hsv, (170, 120, 70), (180, 255, 255)) blue = cv2.inRange(hsv, (100, 150, 80), (130, 255, 255)) n, _, st, cen = cv2.connectedComponentsWithStats(red) rs = [(st[k][4], cen[k]) for k in range(1, n) if st[k][4] >= 300 and max(st[k][2], st[k][3]) < 3 * min(st[k][2], st[k][3])] n, _, st, cen = cv2.connectedComponentsWithStats(blue) bs = [cen[k] for k in range(1, n) if st[k][4] >= 60] if not rs or len(bs) < 2: return None rc = max(rs, key=lambda x: x[0])[1] best = None for i in range(len(bs)): for j in range(i + 1, len(bs)): g = np.hypot(*(bs[i] - bs[j])) if 60 <= g <= 150: mid = (bs[i] + bs[j]) / 2 d = np.hypot(*(rc - mid)) if best is None or d < best[0]: best = (d, g) return None if best is None else (best[0], best[1], rc) def main(): H = height_fn() sel = json.load(open(OUT.parent / "B" / "selection.json")) res, series = [], [] for run in sel["failures"] + sel["successes"]: rec = RUNS / run / "recording" st = list(csv.DictReader(open(rec / "states.csv"))) gr = [float(r["ts"]) for r in st if r["event_type"] == "gripper_cmd" and r["gripper_state"] == "1"] if not gr: res.append({"run": run, "note": "閉じる命令の記録なし"}); continue tc = gr[0] t_open = min([float(r["ts"]) for r in st if r["event_type"] == "gripper_cmd" and r["gripper_state"] == "0" and float(r["ts"]) > tc] or [np.inf]) t_home = min([float(r["ts"]) for r in st if r["event_type"] == "stage" and r["stage"] == "home" and float(r["ts"]) > tc] or [np.inf]) act = [(float(r["ts"]), [float(r[f"j{k}"]) for k in range(6)]) for r in st if r["event_type"] == "actual" and r["j0"]] at = np.array([a[0] for a in act]) def q_at(t): k = max(0, int(np.searchsorted(at, t, side="right") - 1)); return act[k][1] h0, j00 = H(q_at(tc)), q_at(tc)[0] fl = [(int(r["frame_idx"]), float(r["capture_ts"])) for r in csv.DictReader(open(rec / "top_frame_log.csv"))] ts = np.array([t for _, t in fl]) cap = cv2.VideoCapture(str(rec / "top.mp4")) t_end = min(t_open, t_home, ts[-1]) base, lost, run_hits = [], None, 0 for t in np.arange(tc + 1.0, t_end, 0.5): i = int(np.searchsorted(ts, t)) if i >= len(fl): break cap.set(cv2.CAP_PROP_POS_FRAMES, fl[i][0]); ok, f = cap.read() m = find(f) if ok else None q = q_at(t) row = {"run": run, "t_s": round(t - tc, 1), "h_rise_mm": round(H(q) - h0, 1), "j0_turn_deg": round(q[0] - j00, 1), "sep_px": None if m is None else round(m[0], 1), "marker_gap_px": None if m is None else round(m[1], 1)} series.append(row) if m is None: continue if t - tc <= 2.0: base.append(m[0]); continue b = float(np.median(base)) if base else None if b is not None and lost is None: run_hits = run_hits + 1 if m[0] > b + SEP_JUMP_PX else 0 if run_hits >= 2: lost = row res.append({"run": run, "sep_base_px": None if not base else round(float(np.median(base)), 1), "lost_t_s": None if lost is None else lost["t_s"], "lost_h_rise_mm": None if lost is None else lost["h_rise_mm"], "lost_j0_turn_deg": None if lost is None else lost["j0_turn_deg"], "lost_sep_px": None if lost is None else lost["sep_px"], "max_h_rise_until_lost_or_end_mm": max([s["h_rise_mm"] for s in series if s["run"] == run and (lost is None or s["t_s"] <= lost["t_s"])] or [None]), "watched_until_s": round(t_end - tc, 1), "end_reason": "開く命令" if t_end == t_open else ("HOME" if t_end == t_home else "記録の終わり")}) cap.release() for name, rows in (("e_track.csv", res), ("e_track_series.csv", series)): keys = [] for r in rows: keys += [k for k in r if k not in keys] with open(OUT / name, "w", newline="") as fo: w = csv.DictWriter(fo, fieldnames=keys); w.writeheader(); w.writerows(rows) for r in res: print(r) if __name__ == "__main__": main()