fix: harden v1.2 trajectory disposal matching

This commit is contained in:
Yoilun
2026-05-29 16:26:15 +08:00
parent 90aa5dd704
commit 100b949f1f
11 changed files with 248 additions and 36 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ __pycache__/
.DS_Store
*.textClipping
.pytest_cache/
.superpowers/
.venv/
dist/
build/

View File

@@ -20,7 +20,9 @@
## 当前实现范围
当前版本先实现纯业务状态机,不依赖摄像头模型。后续视觉模块只需要输出标准观察数据:
当前版本已经接入可运行的轻量视觉流程:区域占用、垃圾桶动作和 v1.2 的轻量 motion trajectory 都使用启发式图像差分实现,不使用 YOLO。后续训练好的 YOLO 食品检测模型会通过统一的 `disposal_evidence` / backend 合约接入,不改变批次计时状态机的业务输入形态。
视觉或 backend 模块需要输出标准观察数据:
```json
{
@@ -29,7 +31,23 @@
"1": 1,
"2": 0
},
"trash_deposit": false
"trash_deposit": false,
"disposal_evidence": [
{
"source_zone_id": "1",
"target": "trash",
"confidence": 0.9,
"method": "motion",
"track_points": [
{"x": 0.22, "y": 0.30},
{"x": 0.48, "y": 0.58},
{"x": 0.76, "y": 0.78}
],
"item_class": null,
"detector_score": null,
"observed_at": "2026-04-27T10:00:03+08:00"
}
]
}
```
@@ -132,15 +150,17 @@ scripts/run_runtime.sh
2.`ffmpeg` 周期抓取小尺寸 RGB 帧。
3. 按标定区域做占用变化检测。
4. 判断垃圾桶区域是否有明显投放动作。
5. 调用批次计时状态机
6. 写入 `logs/events.jsonl`,管理页会读取这个文件
5. 对刚清空的来源区域运行轻量 motion trajectory生成可选的 `disposal_evidence`
6. 调用批次计时状态机,优先使用匹配 `source_zone_id``disposal_evidence` 确认丢弃,再回退到通用垃圾桶动作
7. 写入 `logs/events.jsonl`,管理页会读取这个文件。
当前视觉版本是可运行的启发式版本:
- 每个格口输出 `0/1` 占用状态,不识别单份数量。
- 启动后的前几帧用于建立空柜基线,默认 `3` 帧。
- 如果启动时格口里已经有食品,系统会把它当作基线,后续要等画面变化后才会产生计时事件。
- 真实生产精度后续应接食品检测模型
- v1.2 轨迹识别是轻量 motion trajectory不加载 YOLO不要求模型文件
- 训练好的 YOLO 模型后续应作为新的 backend 接入,并继续输出统一的 `disposal_evidence`
可选运行参数可以放在配置文件的 `[runtime]` 中:
@@ -167,9 +187,29 @@ trash_motion_delta = 18.0
trash_sustained_motion_delta = 8.0
trash_sustained_motion_frames = 2
trash_motion_cooldown_seconds = 3
trajectory_enabled = true
trajectory_window_seconds = 8
trajectory_sample_interval_seconds = 1.0
trajectory_min_points = 3
trajectory_min_confidence = 0.72
trajectory_motion_delta = 20.0
trajectory_min_blob_area = 12
trajectory_max_blob_area_fraction = 0.35
trajectory_trash_entry_margin = 0.04
trajectory_backend = "motion"
yolo_enabled = false
yolo_model_path = ""
yolo_min_confidence = 0.65
diagnostics_path = "logs/runtime_diagnostics.jsonl"
```
`trajectory_backend = "motion"` 表示当前使用轻量轨迹 backend。`yolo_enabled``yolo_model_path``yolo_min_confidence` 是为后续训练模型预留的配置项;当前版本即使保留这些字段,也不会启用 YOLO 推理。
运行诊断写入 `logs/runtime_diagnostics.jsonl`。每行包含顶层 `disposal_evidence`,以及 `diagnostics.trajectory`
- 顶层 `disposal_evidence`:本帧实际输出给状态机的来源区域到垃圾桶证据。
- `diagnostics.trajectory`:轻量轨迹 backend 的候选、过期、拒绝和已发出证据等调试信息。
## 本地测试
```bash

View File

@@ -17,7 +17,7 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal
- `manage_api.py`: standard-library HTTP management API.
- `main.py`: RTSP runtime loop connecting frame capture, vision, state engine, and JSONL sinks.
- `vision.py`: heuristic ROI occupancy and trash-motion detection.
- v1.2 adds trajectory evidence between vision and engine: `MotionTrajectoryBackend` emits source-zone-to-trash evidence; `BatchEngine` consumes the backend-neutral `disposal_evidence` contract.
- v1.2 adds trajectory evidence between vision and engine: `TrajectoryTracker` emits source-zone-to-trash evidence; `BatchEngine` consumes the backend-neutral `disposal_evidence` contract.
- Frontend package: `web/`
- Vite + vanilla JavaScript management console.
- Default web port `23000`.
@@ -26,6 +26,7 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal
- Runtime data:
- Events JSONL default path `logs/events.jsonl`.
- Diagnostics JSONL default path `logs/runtime_diagnostics.jsonl`.
- v1.2 diagnostics include root-level `disposal_evidence` plus `diagnostics.trajectory`.
- Deployment:
- Root Python Docker image for API/runtime.
- `web/Dockerfile` for static web console.
@@ -52,9 +53,34 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal
- `trajectory_enabled`: enables source-zone trajectory evidence.
- `trajectory_window_seconds`: seconds after a zone clears where movement can confirm disposal.
- `trajectory_sample_interval_seconds`: faster runtime delay while a candidate is active.
- `trajectory_min_points`: minimum sampled motion points required before evidence can emit.
- `trajectory_min_confidence`: minimum confidence before evidence can close pending disposal.
- `trajectory_motion_delta`: frame-difference threshold for trajectory motion points.
- `trajectory_min_blob_area`: minimum connected motion area to keep as a point.
- `trajectory_max_blob_area_fraction`: rejects overly broad frame motion as ambiguous.
- `trajectory_trash_entry_margin`: margin for treating a track point as entering the trash ROI.
- `trajectory_backend`: first valid value is `"motion"`.
- `yolo_enabled`: defaults to `false`; reserved for a future trained model backend.
- `yolo_enabled`, `yolo_model_path`, `yolo_min_confidence`: reserved for a future trained model backend. Current v1.2 keeps YOLO disabled.
## v1.2 Runtime Flow
1. `RTSPFrameSource` captures a resized RGB frame.
2. `ZoneOccupancyDetector` updates per-zone binary occupancy and generic trash-motion count from calibrated ROIs.
3. `TrajectoryTracker` watches zones that just cleared, follows lightweight motion points toward the trash ROI, and emits source-specific `DisposalEvidence` when confidence passes the configured threshold.
4. `BatchEngine` processes `Observation(zone_counts, trash_deposit_count, disposal_evidence)`.
5. For pending disposal, matching `disposal_evidence.source_zone_id` confirms `batch_discarded` before generic FIFO `trash_deposit_count` fallback is used.
6. Runtime writes events to `logs/events.jsonl` and diagnostics to `logs/runtime_diagnostics.jsonl`.
The current tracker is a motion backend only. A later trained YOLO detector should plug in as another backend that enriches or replaces the evidence producer while preserving the same `disposal_evidence` contract consumed by the engine.
## Diagnostics
- Runtime diagnostics JSONL records one item per runtime iteration.
- Root `disposal_evidence` is the exact evidence list passed into the engine for that iteration.
- `diagnostics.zones` contains occupancy metrics used to derive `zone_counts`.
- `diagnostics.trash` contains generic trash-motion metrics and cooldown state.
- `diagnostics.trajectory` contains v1.2 candidate counts, emitted evidence count, motion point count, and per-candidate emitted/rejected/expired records.
- Capture failures still keep the v1.2 schema with root `disposal_evidence: []` and `diagnostics.trajectory.reason = "frame_capture_failed"`.
## Event Model
@@ -87,11 +113,17 @@ In v1.2, `batch_discarded` can be triggered by zone-scoped `disposal_evidence` b
- `scripts/run_runtime.sh`
- One-frame smoke test when camera and `ffmpeg` are available:
- `PYTHONPATH=src python3 -m cold_display_guard.main --config config/example.toml --once`
- v1.2 operating notes:
- Keep `trajectory_backend = "motion"` and `yolo_enabled = false` unless a trained YOLO backend has been explicitly deployed.
- Confirm `logs/runtime_diagnostics.jsonl` contains top-level `disposal_evidence` and `diagnostics.trajectory` before judging trajectory behavior from events alone.
- When `TrajectoryTracker` has active candidates, runtime sampling uses `trajectory_sample_interval_seconds`; this can temporarily be faster than the normal `sample_interval_seconds`.
- On remote deployments, preserve the remote `config/example.toml` calibration and stream settings when syncing code.
## Known Risks
- The current vision detector is heuristic and reports binary occupancy, not item counts.
- v1.2 motion tracking improves disposal matching but can still miss movement if the hand/object path is occluded or sampled too sparsely.
- v1.2 motion tracking improves disposal matching but can still miss movement if the hand/object path is occluded, too broad, too small, or sampled too sparsely.
- YOLO config fields are present for compatibility, but no trained YOLO model is part of the current runtime.
- If food is already present during baseline collection, those regions may be treated as empty baseline until visual changes occur.
- Changing calibration while the runtime process has active batches can create operational ambiguity; v1.1 should document or enforce a pause/restart expectation.
- Historical events must keep the zone index at the time of emission so later region reordering does not reinterpret old logs.

View File

@@ -77,6 +77,12 @@ Use `阶段 x` only as a workflow stage inside the same `v1.1 优化改造` batc
- 轨迹诊断必须记录 active、emitted、rejected、expired方便现场调参。
- 后续 YOLO 模型只作为 evidence 增强输入,不能修改 `BatchEngine` 的业务语义。
## Final Review Findings
- Evidence and generic trash fallback must share the same count budget: one matched `disposal_evidence` consumes one observed trash deposit signal when `trash_deposit_count` is also present, but extra trash deposits must still fall back to pending batches.
- A trajectory candidate must not append source-zone-external motion before source motion has been seen. If outside motion appears first and is later followed by source noise, the candidate is rejected with `motion_started_outside_source` instead of producing evidence.
- Runtime diagnostics on the deployed host should be checked for schema only, not by printing config, because the remote config may contain RTSP credentials and calibration.
## Backend Planning Notes
- `EngineSettings.zone_ids` should remain config driven; numeric zones are preferred for new configs, but old `r1c1` style IDs should continue loading.

View File

@@ -21,4 +21,19 @@
- Preserve a stable `disposal_evidence` contract for a future trained YOLO product detector.
- Keep ROI occupancy timing as the source of zone occupied/empty state.
- Use trajectory evidence before generic trash-motion FIFO fallback.
- Remote deployment target is `xiaozheng@192.168.5.206:/home/xiaozheng/cold_display_guard`; preserve remote `config/example.toml`.
- Current runtime writes top-level `disposal_evidence` and nested `diagnostics.trajectory` into runtime diagnostics JSONL.
## v1.2 Completed Facts
- Stage 1 established the backend contract: `Observation.disposal_evidence` normalizes backend-neutral disposal evidence, and the engine can discard a pending batch only when evidence targets `trash`, meets confidence, and matches the pending `source_zone_id`.
- Stage 2 added the lightweight motion trajectory runtime path: ROI occupancy still drives occupied/empty state, `TrajectoryTracker` emits source-zone-to-trash evidence, and generic trash-motion count remains as a fallback.
- Stage 3 added diagnostics and tests for runtime evidence propagation, trajectory sampling interval behavior, capture-failure schema, and trajectory/yolo runtime config parsing.
- Final review fixes: matched evidence now only subtracts the trash fallback budget by the number of batches it actually closed, and trajectory candidates reject outside-before-source motion with `motion_started_outside_source`.
- Current v1.2 does not use YOLO. `yolo_enabled`, `yolo_model_path`, and `yolo_min_confidence` are reserved for a future trained model backend that should keep emitting the same `disposal_evidence` shape.
## Remote Deployment Notes
- Remote deployment target is `xiaozheng@192.168.5.206:/home/xiaozheng/cold_display_guard`.
- Preserve the remote `config/example.toml`; it may contain camera, calibration, threshold, and deployment-specific runtime settings that must not be overwritten blindly.
- When syncing code remotely, verify that runtime diagnostics still show top-level `disposal_evidence` and `diagnostics.trajectory` before evaluating v1.2 trajectory behavior from `logs/events.jsonl`.
- The latest v1.2 deployment was verified with `cold-display-guard-runtime` and `cold-display-guard-api` up, API health `status=ok`, and diagnostics schema showing `has_disposal_evidence=True` plus `has_trajectory=True`.

View File

@@ -211,6 +211,16 @@
| 2026-05-29 | Phase 3 | Coding Agent | Fixed runtime integration review concerns | Error diagnostics keep trajectory schema; tests no longer use `create=True`; evidence payload helper type narrowed |
| 2026-05-29 | Phase 3 | Testing Agent | Re-tested runtime integration concerns | Verdict pass; no new issues |
| 2026-05-29 | Phase 3 | Main Agent | Ran local verification | `tests.test_main tests.test_vision` passed with 26 tests; full Python suite passed with 68 tests; dependency scan had no model/heavy vision matches |
| 2026-05-29 | Phase 4 | Main Agent | Marked Phase 4 as `in_progress` | Preparing documentation, final verification, remote deployment, and final review |
| 2026-05-29 | Phase 4 | Main Agent | Synced v1.2 code to `xiaozheng@192.168.5.206` | `rsync` completed while excluding remote `config/example.toml` |
| 2026-05-29 | Phase 4 | Main Agent | Tried remote config patch with inline heredoc | Failed with shell quoting `SyntaxError`; switching to scp temporary patch script |
| 2026-05-29 | Phase 4 | Main Agent | Patched remote runtime config via uploaded Python script | Added missing trajectory/yolo runtime keys without printing RTSP config |
| 2026-05-29 | Phase 4 | Main Agent | Rebuilt and restarted remote runtime/API containers | `cold-display-guard-runtime` and `cold-display-guard-api` recreated and started |
| 2026-05-29 | Phase 4 | Final Code Review Agent | Reviewed full v1.2 implementation | Verdict fail: extra trash fallback was suppressed, tracker could seed from outside-source motion, and docs named a non-existent backend |
| 2026-05-29 | Phase 4 | Main Agent | Fixed final review findings | Added regression tests, adjusted trash fallback budgeting, rejected outside-before-source trajectories, and renamed docs to `TrajectoryTracker` |
| 2026-05-29 | Phase 4 | Main Agent | Re-ran local full verification | Python 70 tests passed; frontend 22 tests passed; Vite build passed; no heavy model dependency matches |
| 2026-05-29 | Phase 4 | Main Agent | Re-synced and redeployed fix to `xiaozheng@192.168.5.206` | Runtime/API rebuilt and restarted from the fixed code |
| 2026-05-29 | Phase 4 | Main Agent | Verified remote runtime after redeploy | Containers are up, API health returns `status=ok`, diagnostics contain `disposal_evidence` and `diagnostics.trajectory` |
### Test Results
@@ -224,6 +234,13 @@
| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_main tests.test_vision -v` | pass | 26 runtime/vision tests passed after phase 3 |
| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 68 full Python tests passed after phase 3 |
| 2026-05-29 | `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml` | pass | No matches; command exited 1 because no heavy vision/model dependency was found |
| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 70 full Python tests passed after final review fixes |
| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 22 frontend model tests passed |
| 2026-05-29 | `cd web && pnpm build` | pass | Vite production build passed |
| 2026-05-29 | `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml` | pass | No matches; command exited 1 because no heavy vision/model dependency was found |
| 2026-05-29 | `docker compose ... build cold-display-guard-runtime cold-display-guard-api && docker compose ... up -d --no-deps ...` on remote | pass | Runtime/API rebuilt and restarted after final fixes |
| 2026-05-29 | `curl --max-time 5 http://192.168.5.206:19080/api/manage/health` | pass | `status=ok`, `runtime_status=running` |
| 2026-05-29 | Remote diagnostics schema check script | pass | `has_disposal_evidence=True`, `has_trajectory=True` |
### Bug Loop
@@ -238,6 +255,44 @@
| Phase 3 | Capture-error diagnostics rows lack `disposal_evidence` and `diagnostics.trajectory` schema | Added regression test and wrote empty evidence plus trajectory error diagnostics on capture failure | Resolved; testing agent and local full Python suite passed |
| Phase 3 | Runtime tests patch `TrajectoryTracker` with `create=True`, which can mask missing imports | Removed `create=True` and asserted fake tracker observe calls | Resolved; testing agent and local full Python suite passed |
| Phase 3 | `disposal_evidence_payloads()` accepts `list[object]` but blindly calls `asdict()` | Narrowed helper signature to `list[DisposalEvidence]` | Resolved; testing agent and local full Python suite passed |
| Phase 4 | Remote config patch inline heredoc lost the empty-string value for `yolo_model_path` | Switched to an scp-uploaded Python patch script instead of repeating inline quoting | Resolved; remote config patch reported 13 runtime keys patched |
| Phase 4 | Source-specific evidence disabled all generic fallback trash deposits | Subtract only the count of evidence-discard events from `remaining_trash_deposits`; add regression test for evidence plus `trash_deposit_count=2` | Resolved; targeted regression and full Python suite passed |
| Phase 4 | Tracker could keep an outside-source blob as the first point before source motion | Track outside-before-source contamination, reject later source noise with `motion_started_outside_source`, and never append those outside points | Resolved; targeted regression and full Python suite passed |
| Phase 4 | `docs/project.md` referenced non-existent `MotionTrajectoryBackend` | Changed architecture docs to name the implemented `TrajectoryTracker` | Resolved; `rg` no longer finds the stale name in project docs/README/src/tests |
## 2026-05-29 Phase Completed: Phase 4 - Documentation, Verification, And Deployment
Status: complete
Files Changed:
- `.gitignore`
- `README_zh.md`
- `docs/project.md`
- `findings.md`
- `memories.md`
- `progress.md`
- `task_plan.md`
- `src/cold_display_guard/engine.py`
- `src/cold_display_guard/vision.py`
- `tests/test_engine.py`
- `tests/test_vision.py`
Tests:
- `PYTHONPATH=src python3 -m unittest discover -s tests -v`: pass, 70 tests
- `node --test web/test/zone-state.test.js`: pass, 22 tests
- `cd web && pnpm build`: pass
- `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml`: pass, no matches
- Remote Docker rebuild/restart: pass
- Remote API health: pass
- Remote diagnostics schema check: pass
Notes:
- Final review findings were fixed before redeploy.
- Remote sync continued to exclude `config/example.toml` to preserve camera/calibration settings.
- Remote diagnostics check intentionally printed only schema booleans and safe trajectory keys.
Risks:
- v1.2 is still heuristic motion tracking. Live precision should be tuned from diagnostics, and future YOLO integration should continue using the same `disposal_evidence` contract.
## 2026-05-29 Phase Completed: Phase 3 - Runtime Integration

View File

@@ -25,18 +25,15 @@ class BatchEngine:
previous_zone_counts = dict(self._zone_counts)
remaining_trash_deposits = observation.trash_deposit_count
used_disposal_evidence: set[int] = set()
has_source_specific_disposal = self._has_confirming_disposal_evidence(observation.disposal_evidence)
if has_source_specific_disposal:
remaining_trash_deposits = 0
events.extend(self._expire_pending_disposal(observation.ts))
events.extend(
self._apply_disposal_evidence(
observation.ts,
observation.disposal_evidence,
used_disposal_evidence,
)
evidence_events = self._apply_disposal_evidence(
observation.ts,
observation.disposal_evidence,
used_disposal_evidence,
)
remaining_trash_deposits = max(0, remaining_trash_deposits - len(evidence_events))
events.extend(evidence_events)
trash_events = self._apply_trash_deposits(observation.ts, remaining_trash_deposits)
remaining_trash_deposits = max(0, remaining_trash_deposits - len(trash_events))
events.extend(trash_events)
@@ -82,13 +79,13 @@ class BatchEngine:
self._zone_counts[zone_id] = new_count
newly_pending_count = max(0, len(self.pending_disposal) - pending_count_before_zone_transitions)
events.extend(
self._apply_disposal_evidence(
observation.ts,
observation.disposal_evidence,
used_disposal_evidence,
)
evidence_events = self._apply_disposal_evidence(
observation.ts,
observation.disposal_evidence,
used_disposal_evidence,
)
remaining_trash_deposits = max(0, remaining_trash_deposits - len(evidence_events))
events.extend(evidence_events)
trash_deposits_to_apply = remaining_trash_deposits
if remaining_trash_deposits > 0 and newly_pending_count > 1:
trash_deposits_to_apply = max(remaining_trash_deposits, newly_pending_count)
@@ -283,9 +280,6 @@ class BatchEngine:
events.append(self._event("batch_discarded", when, batch, severity="info"))
return events
def _has_confirming_disposal_evidence(self, disposal_evidence: list[DisposalEvidence]) -> bool:
return any(self._is_confirming_disposal_evidence(evidence) for evidence in disposal_evidence)
def _is_confirming_disposal_evidence(self, evidence: DisposalEvidence) -> bool:
if evidence.confidence < DISPOSAL_EVIDENCE_CONFIDENCE_THRESHOLD:
return False

View File

@@ -83,6 +83,7 @@ class _TrajectoryCandidate:
last_sample_at: datetime | None = None
points: list[_MotionPoint] | None = None
source_motion_seen: bool = False
pre_source_motion_seen: bool = False
def __post_init__(self) -> None:
if self.points is None:
@@ -456,13 +457,22 @@ class TrajectoryTracker:
available_source_blobs = [blob for blob in source_blobs if blob.blob_id not in consumed_blob_ids]
if not available_source_blobs:
return "ambiguous_motion_track"
if candidate.pre_source_motion_seen:
return "motion_started_outside_source"
candidate.source_motion_seen = True
point = _nearest_point(region_center(candidate.source_region), available_source_blobs)
else:
available_blobs = [blob for blob in blobs if blob.blob_id not in consumed_blob_ids]
if not available_blobs:
return None
point = _nearest_point(region_center(candidate.source_region), available_blobs)
candidate.pre_source_motion_seen = True
if any(
region_contains(self.trash_region, blob.x, blob.y, margin=self.settings.trajectory_trash_entry_margin)
for blob in available_blobs
if self.trash_region is not None
):
return "motion_started_outside_source"
return None
else:
previous = candidate.points[-1] if candidate.points else None
available_blobs = [blob for blob in blobs if blob.blob_id not in consumed_blob_ids]

View File

@@ -85,12 +85,12 @@ Create and evolve an independent git project under `~/Code` for monitoring food
### Stop Conditions
- [ ] v1.2 所有阶段完成。
- [ ] 必要 Python 测试通过。
- [ ] 前端测试或构建在受影响时通过。
- [ ] `docs/project.md` 记录 v1.2 架构、配置、运行方式和关键决策。
- [ ] 没有 blocking bug 或未处理的高风险问题。
- [ ] 如果同一问题连续 3 次修复仍失败,暂停并报告原因、已尝试方案和建议下一步。
- [x] v1.2 所有阶段完成。
- [x] 必要 Python 测试通过。
- [x] 前端测试或构建在受影响时通过。
- [x] `docs/project.md` 记录 v1.2 架构、配置、运行方式和关键决策。
- [x] 没有 blocking bug 或未处理的高风险问题。
- [x] 如果同一问题连续 3 次修复仍失败,暂停并报告原因、已尝试方案和建议下一步。
### Phases
@@ -99,7 +99,7 @@ Create and evolve an independent git project under `~/Code` for monitoring food
| 1 | complete | 建立 `disposal_evidence` 数据契约并让状态机优先按来源区域丢弃 | `Observation` 支持 evidenceengine 能按 `source_zone_id` 精确关闭 pending batch同帧移除+evidence 有回归测试;旧 `trash_deposit_count` 仍可兜底 |
| 2 | complete | 实现无 YOLO 依赖的轻量轨迹检测 | synthetic frame 测试覆盖源区域到垃圾桶、非源区域运动、未到垃圾桶、单帧反光、多候选互不串扰;不引入模型依赖 |
| 3 | complete | 集成 runtime 配置、诊断和候选窗口加速采样 | `main.py` 写入 `disposal_evidence` 与 trajectory diagnostics配置默认 `trajectory_enabled=true``yolo_enabled=false`;候选活跃时使用更短采样间隔 |
| 4 | pending | 文档、全量验证和部署准备 | README/project/progress 更新Python 全量测试通过;前端测试/构建按影响范围验证;远端部署命令和风险记录清楚 |
| 4 | complete | 文档、全量验证和部署准备 | README/project/progress 更新Python 全量测试通过;前端测试/构建按影响范围验证;远端部署命令和风险记录清楚 |
### v1.2 Decisions

View File

@@ -250,6 +250,33 @@ class BatchEngineTests(unittest.TestCase):
self.assertEqual([(event["event"], event["zone_id"]) for event in events], [("batch_discarded", "4")])
self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"])
def test_extra_trash_deposits_still_fallback_after_matching_disposal_evidence(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
max_dwell_seconds=1200,
trash_confirmation_seconds=120,
zone_ids=("1", "2"),
)
engine = BatchEngine(settings)
engine.process(obs(self.t0, {"1": 1, "2": 1}))
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1, "2": 1}))
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0, "2": 0}))
events = engine.process(
obs(
self.t0 + timedelta(seconds=1310),
{"1": 0, "2": 0},
trash=2,
disposal_evidence=[disposal_evidence("1")],
)
)
self.assertEqual(
[(event["event"], event["zone_id"]) for event in events],
[("batch_discarded", "1"), ("batch_discarded", "2")],
)
self.assertEqual(engine.pending_disposal, [])
def test_same_observation_removal_and_disposal_evidence_discards_newly_pending_batch(self) -> None:
settings = EngineSettings(
camera_id="test_cam",

View File

@@ -474,7 +474,39 @@ class VisionTests(unittest.TestCase):
rejected.extend(diagnostics["rejected"])
self.assertEqual(all_evidence, [])
self.assertTrue(any(item["reason"] == "missing_source_motion" for item in rejected))
self.assertTrue(any(item["reason"] == "motion_started_outside_source" for item in rejected))
def test_motion_before_source_motion_cannot_seed_later_trash_evidence(self) -> None:
source = Region("source", ((0.05, 0.35), (0.25, 0.35), (0.25, 0.65), (0.05, 0.65)))
trash = Region("trash", ((0.72, 0.35), (0.95, 0.35), (0.95, 0.65), (0.72, 0.65)))
tracker = TrajectoryTracker(
[source],
trash,
RuntimeVisionSettings(trajectory_sample_interval_seconds=0.0, trajectory_min_points=3),
)
now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc)
tracker.observe(solid_frame(80, 80, 40), now, {"source": 1})
all_evidence = []
rejected = []
frames = [
frame_with_motion_patch(80, 80, (34, 36)),
frame_with_motion_patch(80, 80, (16, 36)),
frame_with_motion_patch(80, 80, (34, 36)),
frame_with_motion_patch(80, 80, (50, 36)),
frame_with_motion_patch(80, 80, (66, 36)),
]
for index, frame in enumerate(frames):
evidence, diagnostics = tracker.observe(
frame,
now + timedelta(seconds=index + 1),
{"source": 0},
)
all_evidence.extend(evidence)
rejected.extend(diagnostics["rejected"])
self.assertEqual(all_evidence, [])
self.assertTrue(any(item["reason"] == "motion_started_outside_source" for item in rejected))
def test_trajectory_diagnostics_include_per_candidate_events(self) -> None:
source = Region("source", ((0.05, 0.35), (0.25, 0.35), (0.25, 0.65), (0.05, 0.65)))