Add missing fields (event_code, camera_ip, started_at, ended_at, dwell_seconds, is_discarded, alerted_at, etc.) to both batch_event and case_event payloads. Introduce source_id config for payload injection and infer_camera_ip to extract IP from RTSP stream URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
7.5 KiB
Markdown
255 lines
7.5 KiB
Markdown
# 冷藏展示柜食品批次计时报警
|
||
|
||
这是一个独立项目,用于单摄像头监控冷藏展示柜和同画面垃圾桶,记录每个展示区域内食品批次的放置时长,并发现超过自定义报警时间后的异常处理行为。
|
||
|
||
## 已确认业务规则
|
||
|
||
- 摄像头同时看到展示柜和垃圾桶。
|
||
- 展示柜食品区域支持 1 到 10 个自定义区域。
|
||
- 食品区域使用阿拉伯数字标注:`1`、`2`、`3` ...
|
||
- 垃圾桶 ROI 独立标定,不占用食品区域编号。
|
||
- 每个区域可以放多份食品,但这些食品按同一批次计时。
|
||
- 同一区域不允许混批,必须清空后才能放入新批次。
|
||
- 食品放入区域时记录开始时间。
|
||
- 区域清空时记录结束时间。
|
||
- 未达到报警阈值前清空视为正常消耗。
|
||
- 食品在区域内达到 `max_dwell_seconds` 时先产生 `time_alarm`。
|
||
- 已报警食品从区域移出后,必须在确认窗口内看到垃圾桶投放动作。
|
||
- 如果已报警食品移出后没有丢到垃圾桶里,报警事件升级为 `warning_escalated` 警告事件。
|
||
- 已报警食品拿出后又放回展示柜,触发违规事件。
|
||
|
||
## 当前实现范围
|
||
|
||
当前版本先实现纯业务状态机,不依赖摄像头模型。后续视觉模块只需要输出标准观察数据:
|
||
|
||
```json
|
||
{
|
||
"ts": "2026-04-27T10:00:00+08:00",
|
||
"zone_counts": {
|
||
"1": 1,
|
||
"2": 0
|
||
},
|
||
"trash_deposit": false
|
||
}
|
||
```
|
||
|
||
程序会输出 JSONL 事件,例如:
|
||
|
||
- `batch_started`
|
||
- `time_alarm`
|
||
- `batch_consumed`
|
||
- `batch_pending_disposal`
|
||
- `batch_discarded`
|
||
- `warning_escalated`
|
||
- `mixed_batch_violation`
|
||
- `overdue_return_violation`
|
||
|
||
## 配置
|
||
|
||
示例配置在 `config/example.toml`。
|
||
|
||
默认阈值:
|
||
|
||
- 时间报警阈值:`10800` 秒,也就是 3 小时;管理页按分钟输入,例如 20 分钟会保存为 `1200` 秒
|
||
- 垃圾桶投放确认窗口:`120` 秒
|
||
|
||
食品区域配置示例:
|
||
|
||
```toml
|
||
[layout]
|
||
zone_count = 3
|
||
zone_ids = ["1", "2", "3"]
|
||
|
||
[[zones]]
|
||
id = "1"
|
||
label = "区域 1"
|
||
polygon = [[0.1, 0.1], [0.3, 0.1], [0.3, 0.3]]
|
||
|
||
[trash]
|
||
roi = [[0.7, 0.7], [0.9, 0.7], [0.9, 0.9]]
|
||
```
|
||
|
||
## 区域标定
|
||
|
||
项目现在有正式管理页,前端默认 `23000`,后端默认 `19080`。
|
||
|
||
```bash
|
||
scripts/run_manage_api.sh
|
||
```
|
||
|
||
另开一个终端:
|
||
|
||
```bash
|
||
scripts/run_web.sh
|
||
```
|
||
|
||
打开:
|
||
|
||
```text
|
||
http://127.0.0.1:23000
|
||
```
|
||
|
||
管理页支持:
|
||
|
||
- 配置 RTSP 地址和阈值
|
||
- 从 RTSP 拉取一帧截图
|
||
- 设置 1 到 10 个食品区域
|
||
- 标定数字食品区域和垃圾桶 ROI
|
||
- 直接保存标定结果到项目配置文件
|
||
- 查看事件汇总、区域序号、停留时间、报警和警告事件
|
||
- 查看本地处置单状态,并手工标记为已处理
|
||
|
||
项目仍保留 `tools/calibrator` 作为轻量单页标定工具,但正式使用建议走 `23000` 管理页。
|
||
|
||
## 管理 API
|
||
|
||
默认后端:
|
||
|
||
```text
|
||
http://127.0.0.1:19080
|
||
```
|
||
|
||
主要接口:
|
||
|
||
- `GET /api/manage/health`
|
||
- `GET /api/manage/config`
|
||
- `PUT /api/manage/config`
|
||
- `POST /api/manage/snapshot`
|
||
- `PUT /api/manage/calibration`
|
||
- `GET /api/manage/summary`
|
||
- `GET /api/manage/events`
|
||
- `GET /api/manage/cases`
|
||
- `GET /api/manage/cases/summary`
|
||
- `POST /api/manage/cases/{case_id}/handle`
|
||
- `POST /api/manage/webhooks/case-update`
|
||
- `GET /api/manage/webhooks/retries`
|
||
- `POST /api/manage/webhooks/retries/drain`
|
||
|
||
`/api/manage/webhooks/case-update` 需要请求头 `X-Webhook-Token`,并且请求体里的 `status` 目前固定为 `handled`。
|
||
`/api/manage/webhooks/retries` 用于查看最新重试状态,`/api/manage/webhooks/retries/drain` 用于手动触发一次到期重试补偿。
|
||
|
||
## 运行识别计时进程
|
||
|
||
管理页只负责配置和查看数据。要产生数据,还需要启动运行进程:
|
||
|
||
```bash
|
||
scripts/run_runtime.sh
|
||
```
|
||
|
||
运行进程会:
|
||
|
||
1. 按配置读取 RTSP。
|
||
2. 用 `ffmpeg` 周期抓取小尺寸 RGB 帧。
|
||
3. 按标定区域做占用变化检测。
|
||
4. 判断垃圾桶区域是否有明显投放动作。
|
||
5. 调用批次计时状态机。
|
||
6. 将 `time_alarm`、`batch_pending_disposal`、`warning_escalated` 映射到本地处置单状态。
|
||
7. 写入 `logs/events.jsonl`、`logs/cases.jsonl`、`logs/runtime_diagnostics.jsonl`。
|
||
8. 按配置向外部系统推送事件 webhook 和处置单 webhook。
|
||
|
||
当前视觉版本是可运行的启发式版本:
|
||
|
||
- 每个格口输出 `0/1` 占用状态,不识别单份数量。
|
||
- 启动后的前几帧用于建立空柜基线,默认 `3` 帧。
|
||
- 如果启动时格口里已经有食品,系统会把它当作基线,后续要等画面变化后才会产生计时事件。
|
||
- 真实生产精度后续应接食品检测模型。
|
||
|
||
可选运行参数可以放在配置文件的 `[runtime]` 中:
|
||
|
||
```toml
|
||
[runtime]
|
||
sample_interval_seconds = 5.0
|
||
frame_width = 640
|
||
frame_height = 360
|
||
capture_timeout_seconds = 12.0
|
||
baseline_frames = 3
|
||
sample_stride_pixels = 4
|
||
occupancy_mean_delta = 55.0
|
||
occupancy_texture_delta = 18.0
|
||
occupancy_dark_luma_threshold = 80.0
|
||
occupancy_dark_fraction = 0.06
|
||
occupancy_texture_dark_fraction = 0.04
|
||
occupancy_bright_luma_threshold = 220.0
|
||
occupancy_bright_reflection_fraction = 0.18
|
||
occupancy_reflection_dark_fraction = 0.10
|
||
occupancy_reflection_bright_dark_ratio = 2.0
|
||
occupancy_confirm_frames = 2
|
||
empty_confirm_frames = 2
|
||
trash_motion_delta = 18.0
|
||
trash_sustained_motion_delta = 8.0
|
||
trash_sustained_motion_frames = 2
|
||
trash_motion_cooldown_seconds = 3
|
||
diagnostics_path = "logs/runtime_diagnostics.jsonl"
|
||
|
||
[case_sink]
|
||
path = "logs/cases.jsonl"
|
||
|
||
[alarm_snapshot_upload]
|
||
enabled = true
|
||
service_url = "https://ota.zhengxinshipin.com"
|
||
secret = "change-me-in-production"
|
||
object_key_prefix = "cold-display-guard/alarms"
|
||
connect_timeout_seconds = 5
|
||
read_timeout_seconds = 20
|
||
encode_timeout_seconds = 10
|
||
|
||
[webhook_retry_sink]
|
||
path = "logs/webhook_retry.jsonl"
|
||
|
||
[webhooks]
|
||
enabled = true
|
||
event_url = "https://example.com/runtime-events"
|
||
case_url = "https://example.com/case-events"
|
||
source_id = "cold-display-guard"
|
||
callback_token = "shared-secret"
|
||
connect_timeout_seconds = 3
|
||
read_timeout_seconds = 5
|
||
retry_backoff_seconds = 30
|
||
retry_batch_limit = 20
|
||
retry_max_attempts = 5
|
||
retry_max_backoff_seconds = 1800
|
||
```
|
||
|
||
运行时会额外记录:
|
||
|
||
- `logs/cases.jsonl`:本地处置单状态变更
|
||
- `logs/webhook_retry.jsonl`:Webhook 重试队列状态快照
|
||
- `logs/webhook_delivery.jsonl`:Webhook 投递结果审计
|
||
|
||
当某一轮识别结果里出现 `severity=alarm` 或 `severity=warning` 的事件时,运行时会直接复用当前检测帧:
|
||
|
||
1. 用 `ffmpeg` 把当前 RGB 帧编码成 JPEG
|
||
2. 通过 `https://ota.zhengxinshipin.com` 的 chunk-upload API 上传
|
||
3. 把上传返回的 `object_key` 追加到对应 webhook payload
|
||
|
||
相关 webhook 字段:
|
||
|
||
- `event_code`:下游事件列表可直接使用的稳定编码,当前取批次 ID
|
||
- `camera_id` / `camera_ip`:来源设备和摄像头 IP
|
||
- `zone_id` / `zone_label`:所属区域
|
||
- `started_at`:开始计时时间点
|
||
- `ended_at` / `removed_at`:取出时间点
|
||
- `dwell_seconds`:当前批次累计计时时长
|
||
- `is_discarded` / `discarded_at`:是否已丢弃及丢弃时间点
|
||
- `created_at`:该条外部事件记录的创建时间
|
||
- `alerted_at` / `alarm_at`:时长告警时间点
|
||
- `updated_at`:该条外部事件记录的最新更新时间
|
||
- `snapshot_upload_status`:`uploaded` 或 `error`
|
||
- `snapshot_object_key`:上传成功后的 OSS 路径
|
||
- `snapshot_file_name`:上传文件名
|
||
- `snapshot_captured_at`:抓帧时间
|
||
- `snapshot_upload_error`:上传失败原因,仅失败时返回
|
||
|
||
## 本地测试
|
||
|
||
```bash
|
||
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
||
```
|
||
|
||
前端测试和构建:
|
||
|
||
```bash
|
||
node --test web/test/zone-state.test.js
|
||
cd web && pnpm build
|
||
```
|