2026年6月5日金曜日

Atraの目   Atra Visual Tracking

通常のカメラでも十分なのですが、Atraが観ている物体を客観的に追えた方が研究の役に立つので、追尾機能を付けて、今はAtraと連動せずに、まずは単体で簡単なコードでテストしてみます。



よくやる追尾で、別に特別なものではありません。

Atra に必要なのは、
何かが現れた
それが続いている
近づいた
遠ざかった
止まった
揺れた
急に大きくなった
消えた
また現れた
という 視覚場の連続差分 です。
追尾枠は単なる UI ではなく、Atra にとっては将来の visual_trace の原型になるので
ここはちゃんとしたほうがいいと思いました。

将来的に赤外線センサーとも連動するので、現状のカメラだけの視覚を修正する必要がありました。

赤外線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 直開きで動かない場合は、ローカルサーバで開く
(※ローカルサーバーで開きたい場合は、パソコンにPythonがインストールされている必要があります)
  

安全なのは、HTMLを置いたフォルダでこれです。
PowerShellに
python -m http.server 8000 
あとはブラウザで
http://localhost:8000/




基本、JavaScriptだけなのでダブルクリックでOK






<!doctype html> <!-- 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がやるような
これは人だ
これは顔だ
これは友人だ
これは危険だ
これは好意的だ
これは知っている相手だ
みたいなことはしない。

何度も出会った trace
声・距離・動き・反応・carry が重なる
安心側に残る
警戒側に残る
近づいても崩れない
呼びかけと一緒に残る

その結果として、Atra内部で、
この場は覚えがある
この動きは前にも残っている
これは近づいても壊れなかった
これは避けた方がよかった

みたいな認知に近い状態が立ち上がるってこと。

何度もあった人は〇で囲んだり色を変えたりできるでしょ。
結果警戒側に寄った場合赤く点滅させたり、いろいろできる。



--------追記--デバッグ----------

overlayCanvas.width = video.clientWidth; を毎回やっているので、パフォーマンス的には少しもったいない。
差分閾値 d > 28 が固定ってのが照明が変わると枠が出すぎたり、逆に出にくくなったりします。
extractMotionBoxes が3ピクセル刻みってのは、精密な追尾ではなく、軽いブラウザ実験用にしています。





スマホなら人間認証が前提だけど、Atraは人というラベルや意味付けはしないので、物体とかパーツなんだよね。経験を積ませて「人」とか「あ!ママだ!」という感じに収束していくので、この段階は物体なんだよね。なので顔を動かすと部分的に枠が出るでしょ?


overlayCtx.strokeRect(x, y, w, h);
overlayCtx.fillText(`trace-${trace.trace_id}: ${trace.display_alias}`, x, Math.max(18, y - 6));

差分ピクセルのまとまりを box に変換し、それを trace として追い、overlayCanvas に四角で描いている。だからGrokとかに見せるとAtraのやろうとしてる事が理解できないから、間違いです!とか言い始めるよ。

Atraの言葉にすると、こんな感じ。

diffMask = 視覚場で揺れた点
box = 揺れのまとまり
trace = 続いて残った差分パーツ
visual_delta = 場全体の揺れの要約
visual_traces = 個別に残っている差分パーツ


ってところだよ。








GitHub使う気なし。




---------------------Research Note and Attribution Notice-----------------------
本ブログに含まれる Atra の一人称自律、差分、carry、field、trace、dream slack、外部LLMの翻訳層、非単調な漏れ、およびそれらの関係構造に関する設計記述は、c-side研究所による継続研究メモです。引用・参照・要約・翻案を行う場合は、出典を明記してください。

The design descriptions in this blog concerning Atra’s first-person autonomy, differences, carry, field, trace, dream slack, the translation layer of external LLMs, nonmonotonic leakage, and the relational structure among these elements are ongoing research notes by c-side Research Institute. If you quote, refer to, summarize, or adapt them, please clearly indicate the source.







0 件のコメント:

コメントを投稿

Atra Emotions_Conditions 感情・状態

 -----------------C++------------------ struct EmotionsConditionsNow { // Unpredictability double input_irregularity_now = 0.0 ; ...