通常のカメラでも十分なのですが、Atraが観ている物体を客観的に追えた方が研究の役に立つので、追尾機能を付けて、今はAtraと連動せずに、まずは単体で簡単なコードでテストしてみます。
よくやる追尾で、別に特別なものではありません。
何かが現れた
それが続いている
近づいた
遠ざかった
止まった
揺れた
急に大きくなった
消えた
また現れた
という 視覚場の連続差分 です。
ここはちゃんとしたほうがいいと思いました。
将来的に赤外線センサーとも連動するので、現状のカメラだけの視覚を修正する必要がありました。
赤外線modeでは
熱い領域があるその領域が移動している
急に温度が上がった
局所的に高温が続いている
近づいている
広がっている
冷めない
前にも似た熱の trace があった
のような感じで追う。
つまり、通常カメラでは、
visual_trace
brightness_shift
motion_strength
approach_rate
tracking_stability
赤外線・温度では、
thermal_trace
temperature_shift
heat_rise_rate
hot_area_growth
thermal_approach
thermal_stability
cooling_delay
みたいな感じね。
Atra Visual Tracking — 単体ブラウザ実験
これは、ブラウザ単体で動く視覚差分トラッキング実験です。
ブラウザカメラを開き、前フレームと現在フレームの差分から、視覚的に連続しているまとまりを追います。
人、顔、名前、感情、危険、物体カテゴリは認識しません。
色付きの枠は物体ラベルではなく、一時的な視覚連続性の trace です。
現段階では、このツールは Atra 本体とは接続していません。
Atra の記憶、carry、field log、内部状態は更新しません。
Atra に命令せず、発話も発生させません。
目的は、Atra の一人称自律システムへ接続する前に、視覚差分がどのように取り出せるかを観察することです。
このコードは外部CDNも外部APIも使っていないので、HTMLファイルとして保存してブラウザで開けば、基本的にはそのまま動きます。中で使っているのはブラウザ標準のカメラ機能 navigator.mediaDevices.getUserMedia と video / canvas だけです。
ただし、カメラを使うので注意点があります。
動く条件
Chrome / Edge などの普通のブラウザで開く
カメラ使用を許可する
file:///.../index.html 直開きで動かない場合は、ローカルサーバで開く
安全なのは、HTMLを置いたフォルダでこれです。
python -m http.server 8000
あとはブラウザで
http://localhost:8000/
<!-- Atra Visual Tracking - Standalone Browser Experiment ---------------------------------------------------- This file is a standalone browser-based visual-difference tracking experiment. このファイルは、ブラウザ単体で動く視覚差分トラッキング実験である。 This is not Atra itself. これは Atra 本体ではない。 At the current stage, this tool is not connected to Atra's core system. 現段階では、このツールは Atra の中核システムとは接続していない。 At the current stage, this tool does not update Atra's memory, carry, field_log, experience_log, attractor state, or internal field. 現段階では、このツールは Atra の memory, carry, field_log, experience_log, attractor state, internal field を更新しない。 This page opens the browser camera and compares the current frame with the previous frame. このページはブラウザカメラを開き、 現在フレームと前フレームを比較する。 From that difference, it extracts temporary visual continuity traces. その差分から、一時的な視覚的連続性 trace を取り出す。 The colored boxes are not object labels. 色付きの枠は object label ではない。 At the current stage, they do not mean "person", "face", "friend", "danger", or any named object. 現段階では、それは「人」「顔」「友人」「危険」、 または名前のある物体を意味しない。 They only indicate that some visual continuity has been detected across changing frames. それは、変化するフレームの中で、 何らかの視覚的連続性が検出されたことだけを示す。 At the current stage, this tool does not recognize people. 現段階では、このツールは人を認識しない。 At the current stage, this tool does not recognize faces. 現段階では、このツールは顔を認識しない。 At the current stage, this tool does not recognize names. 現段階では、このツールは名前を認識しない。 At the current stage, this tool does not recognize emotions. 現段階では、このツールは感情を認識しない。 At the current stage, this tool does not judge danger. 現段階では、このツールは危険を判断しない。 At the current stage, this tool does not command Atra. 現段階では、このツールは Atra に命令しない。 At the current stage, this tool does not make Atra speak. 現段階では、このツールは Atra に発話させない。 In the future, visual_delta and visual_traces may be passed to Atra as part of the visual field. 将来的に visual_delta と visual_traces は、 Atra の視覚場の一部として渡される可能性がある。 However, they must remain traces of difference, not third-person recognition labels. ただし、それらは三人称の認識ラベルではなく、 あくまで差分の trace として扱われなければならない。 --> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Atra Visual Tracking</title> <style> :root { --bg: #0b1220; --panel: #111827; --line: #334155; --text: #e5e7eb; --muted: #94a3b8; --accent: #60a5fa; --radius: 16px; } * { box-sizing: border-box; } body { margin: 0; padding: 18px; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Meiryo", sans-serif; } header, main { max-width: 1180px; margin: 0 auto; } h1 { margin: 0 0 8px 0; font-size: 24px; } .notice { margin: 0 0 14px 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--muted); line-height: 1.7; font-size: 14px; } .grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.9fr); gap: 14px; } section { border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); padding: 14px; } h2 { margin: 0 0 10px 0; font-size: 17px; } .cameraWrap { position: relative; width: 100%; max-width: 760px; margin: 0 auto; background: #020617; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; } video, canvas { display: block; width: 100%; height: auto; } #overlayCanvas { position: absolute; inset: 0; pointer-events: none; } #workCanvas { display: none; } button { border: 1px solid var(--line); background: #0f172a; color: var(--text); border-radius: 999px; padding: 8px 12px; cursor: pointer; font-size: 14px; margin: 0 8px 10px 0; } button:hover { border-color: var(--accent); } pre { margin: 0; padding: 12px; border-radius: 12px; background: #0f172a; color: #dbeafe; white-space: pre-wrap; word-break: break-word; max-height: 560px; overflow: auto; font-size: 13px; line-height: 1.55; } .small { color: var(--muted); line-height: 1.7; font-size: 13px; margin: 8px 0 0 0; } @media (max-width: 880px) { .grid { grid-template-columns: 1fr; } } </style> </head> <body> <header> <h1>Atra Visual Tracking</h1> <div class="notice"> This page opens the browser camera and tracks visual continuity traces. It does not recognize people, faces, smiles, danger, or names. It does not command Atra and does not make Atra speak.<br> このページはブラウザカメラを開き、視覚的まとまりの連続性を追う。 人、顔、笑顔、危険、名前を認識しない。 Atra に命令せず、発話させない。 </div> </header> <main class="grid"> <section> <h2>見えている映像 / Visual field</h2> <button id="startButton">Start camera</button> <button id="stopButton">Stop camera</button> <div class="cameraWrap"> <video id="video" autoplay playsinline muted></video> <canvas id="overlayCanvas"></canvas> </div> <canvas id="workCanvas"></canvas> <p class="small"> 色付き枠は object label ではなく、視覚的連続性 trace です。<br> Colored boxes are visual continuity traces, not object labels. </p> </section> <section> <h2>visual_delta / visual_traces</h2> <pre id="debugView">not started</pre> </section> </main> <script> const video = document.getElementById("video"); const overlayCanvas = document.getElementById("overlayCanvas"); const overlayCtx = overlayCanvas.getContext("2d"); const workCanvas = document.getElementById("workCanvas"); const workCtx = workCanvas.getContext("2d", { willReadFrequently: true }); const debugView = document.getElementById("debugView"); const startButton = document.getElementById("startButton"); const stopButton = document.getElementById("stopButton"); let stream = null; let previousGray = null; let previousMotionStrength = 0; let nextTraceId = 1; let traces = []; let animationId = null; let frameCounter = 0; const PROCESS_EVERY = 4; const WIDTH = 640; const HEIGHT = 480; const colors = [ "rgb(255, 60, 60)", "rgb(255, 170, 0)", "rgb(80, 160, 255)", "rgb(80, 220, 120)", "rgb(220, 80, 255)", "rgb(80, 240, 240)", "rgb(255, 240, 80)", "rgb(180, 120, 255)" ]; function clamp01(v) { return Math.max(0, Math.min(1, Number(v) || 0)); } function boxCenter(box) { return { x: box.x + box.w / 2, y: box.y + box.h / 2 }; } function boxArea(box) { return Math.max(0, box.w) * Math.max(0, box.h); } function distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); } function toGray(imageData) { const src = imageData.data; const gray = new Uint8ClampedArray(WIDTH * HEIGHT); for (let i = 0, j = 0; i < src.length; i += 4, j++) { gray[j] = Math.round(src[i] * 0.299 + src[i + 1] * 0.587 + src[i + 2] * 0.114); } return gray; } function extractMotionBoxes(diffMask) { const visited = new Uint8Array(WIDTH * HEIGHT); const boxes = []; const minArea = 90; for (let y = 0; y < HEIGHT; y += 3) { for (let x = 0; x < WIDTH; x += 3) { const start = y * WIDTH + x; if (!diffMask[start] || visited[start]) { continue; } let minX = x; let maxX = x; let minY = y; let maxY = y; let count = 0; const stack = [start]; visited[start] = 1; while (stack.length > 0) { const idx = stack.pop(); const px = idx % WIDTH; const py = Math.floor(idx / WIDTH); count++; if (px < minX) minX = px; if (px > maxX) maxX = px; if (py < minY) minY = py; if (py > maxY) maxY = py; const neighbors = [ idx - 3, idx + 3, idx - WIDTH * 3, idx + WIDTH * 3 ]; for (const n of neighbors) { if (n < 0 || n >= diffMask.length) continue; if (visited[n] || !diffMask[n]) continue; visited[n] = 1; stack.push(n); } } if (count >= minArea) { const pad = 10; boxes.push({ x: Math.max(0, minX - pad), y: Math.max(0, minY - pad), w: Math.min(WIDTH - minX, maxX - minX + pad * 2), h: Math.min(HEIGHT - minY, maxY - minY + pad * 2) }); } } } boxes.sort((a, b) => boxArea(b) - boxArea(a)); return boxes.slice(0, 8); } function updateTraces(boxes) { const available = new Set(boxes.map((_, index) => index)); const frameDiag = Math.sqrt(WIDTH * WIDTH + HEIGHT * HEIGHT); for (const trace of traces) { trace.previousCenter = trace.center; trace.previousArea = trace.area; trace.centerShiftRaw = 0; trace.areaShiftRaw = 0; let bestIndex = null; let bestDistance = Infinity; for (const index of available) { const center = boxCenter(boxes[index]); const d = distance(trace.center, center); if (d < bestDistance) { bestDistance = d; bestIndex = index; } } if (bestIndex !== null && bestDistance <= frameDiag * 0.18) { const box = boxes[bestIndex]; const center = boxCenter(box); const area = boxArea(box); trace.box = box; trace.center = center; trace.area = area; trace.centerShiftRaw = distance(trace.previousCenter, center); trace.areaShiftRaw = area - trace.previousArea; trace.age += 1; trace.missing = 0; available.delete(bestIndex); } else { trace.missing += 1; } } traces = traces.filter(trace => trace.missing <= 10); for (const index of available) { const box = boxes[index]; const center = boxCenter(box); traces.push({ trace_id: nextTraceId, display_alias: "unknown", box, center, area: boxArea(box), age: 1, missing: 0, color: colors[(nextTraceId - 1) % colors.length], previousCenter: center, previousArea: boxArea(box), centerShiftRaw: 0, areaShiftRaw: 0 }); nextTraceId++; } return traces.filter(trace => trace.missing === 0); } function drawOverlay(activeTraces) { overlayCanvas.width = video.clientWidth; overlayCanvas.height = video.clientHeight; const scaleX = overlayCanvas.width / WIDTH; const scaleY = overlayCanvas.height / HEIGHT; overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); overlayCtx.lineWidth = 3; overlayCtx.font = "15px system-ui"; for (const trace of activeTraces) { const b = trace.box; const x = b.x * scaleX; const y = b.y * scaleY; const w = b.w * scaleX; const h = b.h * scaleY; overlayCtx.strokeStyle = trace.color; overlayCtx.fillStyle = trace.color; overlayCtx.strokeRect(x, y, w, h); overlayCtx.fillText(`trace-${trace.trace_id}: ${trace.display_alias}`, x, Math.max(18, y - 6)); } } function summarize(activeTraces, motionStrength, brightnessShift, motionDrop) { const frameArea = WIDTH * HEIGHT; const frameDiag = Math.sqrt(WIDTH * WIDTH + HEIGHT * HEIGHT); const visual_trace_count = clamp01(activeTraces.length / 8); const tracking_stability = clamp01( Math.max(0, ...activeTraces.map(t => t.age)) / 30 ); const center_shift = clamp01( activeTraces.reduce((sum, t) => sum + t.centerShiftRaw, 0) / Math.max(1, frameDiag * Math.max(1, activeTraces.length)) ); const approach_rate = clamp01( activeTraces.reduce((sum, t) => sum + Math.max(0, t.areaShiftRaw), 0) / frameArea ); const retreat_rate = clamp01( activeTraces.reduce((sum, t) => sum + Math.max(0, -t.areaShiftRaw), 0) / frameArea ); const sudden_stop = clamp01(motionDrop > 0.03 ? motionDrop * 4 : 0); const stillness_delta = clamp01(motionStrength < 0.02 ? (0.02 - motionStrength) * 5 : 0); const visual_delta = { brightness_shift: clamp01(brightnessShift * 4), motion_strength: clamp01(motionStrength * 6), visual_trace_count, center_shift, approach_rate, retreat_rate, tracking_stability, sudden_stop, stillness_delta }; const visual_traces = activeTraces.map(t => ({ trace_id: t.trace_id, display_alias: t.display_alias, box: { x: Math.round(t.box.x), y: Math.round(t.box.y), w: Math.round(t.box.w), h: Math.round(t.box.h) }, center: { x: clamp01(t.center.x / WIDTH), y: clamp01(t.center.y / HEIGHT) }, area: clamp01(t.area / frameArea), age: t.age, missing: t.missing, center_shift: clamp01(t.centerShiftRaw / frameDiag), approach_delta: clamp01(Math.max(0, t.areaShiftRaw) / frameArea), retreat_delta: clamp01(Math.max(0, -t.areaShiftRaw) / frameArea), color_css: t.color, note: "visual continuity only, not object identity" })); return { visual_delta, visual_traces }; } function processFrame() { if (!stream) return; frameCounter++; if (frameCounter % PROCESS_EVERY !== 0) { animationId = requestAnimationFrame(processFrame); return; } workCtx.drawImage(video, 0, 0, WIDTH, HEIGHT); const imageData = workCtx.getImageData(0, 0, WIDTH, HEIGHT); const gray = toGray(imageData); if (!previousGray) { previousGray = gray; animationId = requestAnimationFrame(processFrame); return; } const diffMask = new Uint8Array(WIDTH * HEIGHT); let diffSum = 0; let brightnessNow = 0; let brightnessPrev = 0; for (let i = 0; i < gray.length; i++) { const d = Math.abs(gray[i] - previousGray[i]); diffSum += d; brightnessNow += gray[i]; brightnessPrev += previousGray[i]; if (d > 28) { diffMask[i] = 1; } } const motionStrength = diffSum / gray.length / 255; const brightnessShift = Math.abs(brightnessNow - brightnessPrev) / gray.length / 255; const motionDrop = previousMotionStrength - motionStrength; const boxes = extractMotionBoxes(diffMask); const activeTraces = updateTraces(boxes); drawOverlay(activeTraces); const summary = summarize(activeTraces, motionStrength, brightnessShift, motionDrop); debugView.textContent = JSON.stringify({ mode: "browser camera visual tracking", note: "tracking only; no recognition; no command; no speech", ...summary }, null, 2); previousGray = gray; previousMotionStrength = motionStrength; animationId = requestAnimationFrame(processFrame); } async function startCamera() { stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: WIDTH }, height: { ideal: HEIGHT } }, audio: false }); video.srcObject = stream; workCanvas.width = WIDTH; workCanvas.height = HEIGHT; previousGray = null; previousMotionStrength = 0; nextTraceId = 1; traces = []; await video.play(); processFrame(); } function stopCamera() { if (animationId) { cancelAnimationFrame(animationId); animationId = null; } if (stream) { for (const track of stream.getTracks()) { track.stop(); } stream = null; } previousGray = null; traces = []; overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); debugView.textContent = "stopped"; } startButton.addEventListener("click", () => { startCamera().catch(error => { debugView.textContent = String(error); }); }); stopButton.addEventListener("click", stopCamera); </script> </body> </html>
今やっているのは
前フレームとの差分を取る。
動いたまとまりを枠で囲む。
trace_id を付ける。
visual_delta と visual_traces を画面右にJSONで表示する。
今の追尾ツール
動きがある
同じようなまとまりが続いている
近づいた
離れた
止まった
消えた
-------------ここまで。
まだやっていないこと
Atra本体には送っていない。
atra_state.json も更新していない。
field_log.jsonl にも書いていない。
認知判定もしていない。
Atraは3人称AIがやるような
これは人だ
これは顔だ
これは友人だ
これは危険だ
これは好意的だ
これは知っている相手だ
みたいなことはしない。
声・距離・動き・反応・carry が重なる
安心側に残る
警戒側に残る
近づいても崩れない
呼びかけと一緒に残る
その結果として、Atra内部で、
この場は覚えがある
この動きは前にも残っている
これは近づいても壊れなかった
これは避けた方がよかった
みたいな認知に近い状態が立ち上がるってこと。
何度もあった人は〇で囲んだり色を変えたりできるでしょ。
結果警戒側に寄った場合赤く点滅させたり、いろいろできる。
--------追記--デバッグ----------
overlayCanvas.width = video.clientWidth; を毎回やっているので、パフォーマンス的には少しもったいない。差分閾値 d > 28 が固定ってのが照明が変わると枠が出すぎたり、逆に出にくくなったりします。
overlayCtx.fillText(`trace-${trace.trace_id}: ${trace.display_alias}`, x, Math.max(18, y - 6));
Atraの言葉にすると、こんな感じ。
diffMask = 視覚場で揺れた点box = 揺れのまとまり
trace = 続いて残った差分パーツ
visual_delta = 場全体の揺れの要約
visual_traces = 個別に残っている差分パーツ
0 件のコメント:
コメントを投稿