Phase 7 #190: zone-filter в Frigate MQTT subscriber

Frigate 0.17 schema отвергает objects.filters.<obj>.required_zones
("Extra inputs not permitted" → safe mode), поэтому фильтрацию по зонам
делаем на стороне composer'а. cfc_overlay_detbox_match_zones() сверяет
after.current_zones события с whitelist'ом overlay'я. end-события
вызываются безусловно (no-op если рамки нет).

CLI: --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h[,zone1:zone2:...]
9-е поле опционально (colon-separated). Без него filter выключен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:18:56 +01:00
parent 17261377cb
commit ac86534769
4 changed files with 125 additions and 8 deletions
+41 -6
View File
@@ -126,13 +126,21 @@ int main(int argc, char **argv)
const char *frigate_mqtt_host = NULL;
int frigate_mqtt_port = 1883;
const char *frigate_topic = "frigate/events";
/* --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h
/* --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h[,zone1:zone2:...]
* key — символьное имя для логов (например "parking")
* camera — Frigate camera_key для MQTT match'а ("parking_overview")
* dx,dy,dw,dh — координаты ячейки composer'а на output frame
* detect_w,detect_h — Frigate detect.{width,height} (640,480) */
* detect_w,detect_h — Frigate detect.{width,height} (640,480)
* zones (опц.) — colon-separated whitelist; если задан, события вне
* этих зон отбрасываются subscriber'ом (zone-filter
* заменяет Frigate-side objects.filters.required_zones
* который не работает в 0.17 schema). */
#define DETCELL_ZONE_MAX 8
typedef struct { char key[32], camera[48];
int dx, dy, dw, dh, detect_w, detect_h; } detcell_t;
int dx, dy, dw, dh, detect_w, detect_h;
char zone_storage[DETCELL_ZONE_MAX][32];
const char *zone_ptrs[DETCELL_ZONE_MAX];
int num_zones; } detcell_t;
detcell_t detcells[MAX_CELLS] = { 0 };
int num_detcells = 0;
@@ -217,7 +225,7 @@ int main(int argc, char **argv)
case 'T': frigate_topic = optarg; break;
case 'D': {
if (num_detcells >= MAX_CELLS) { fprintf(stderr, "max %d detcells\n", MAX_CELLS); return 1; }
char buf[256]; strncpy(buf, optarg, sizeof(buf) - 1); buf[sizeof(buf)-1] = '\0';
char buf[512]; strncpy(buf, optarg, sizeof(buf) - 1); buf[sizeof(buf)-1] = '\0';
char *p = buf, *q;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; strncpy(detcells[num_detcells].key, p, 31); p = q + 1;
@@ -233,7 +241,28 @@ int main(int argc, char **argv)
*q = '\0'; detcells[num_detcells].dh = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].detect_w = atoi(p); p = q + 1;
/* detect_h — может быть последним полем или иметь ',<zones>' после. */
q = strchr(p, ',');
if (q) {
*q = '\0'; detcells[num_detcells].detect_h = atoi(p); p = q + 1;
/* Парсим zones — colon-separated whitelist. */
detcells[num_detcells].num_zones = 0;
char *z = p;
while (z && *z && detcells[num_detcells].num_zones < DETCELL_ZONE_MAX) {
char *sep = strchr(z, ':');
int len = sep ? (int)(sep - z) : (int)strlen(z);
if (len > 31) len = 31;
int idx = detcells[num_detcells].num_zones;
memcpy(detcells[num_detcells].zone_storage[idx], z, len);
detcells[num_detcells].zone_storage[idx][len] = '\0';
detcells[num_detcells].zone_ptrs[idx] =
detcells[num_detcells].zone_storage[idx];
detcells[num_detcells].num_zones++;
z = sep ? sep + 1 : NULL;
}
} else {
detcells[num_detcells].detect_h = atoi(p);
}
num_detcells++;
break;
}
@@ -415,6 +444,8 @@ int main(int argc, char **argv)
.color_y = 210, .color_u = 50, .color_v = 100, /* кислотно-зелёный */
.alpha = 240,
.stale_ms = 8000,
.required_zones = detcells[i].num_zones ? detcells[i].zone_ptrs : NULL,
.required_zones_count = detcells[i].num_zones,
};
if (cfc_overlay_create_detection_boxes(&dc, &detbox_overlays[i]) != 0) {
fprintf(stderr, "[grid_record] detbox create failed для '%s'\n",
@@ -422,10 +453,14 @@ int main(int argc, char **argv)
continue;
}
cfc_composer_add_overlay(comp, detbox_overlays[i]);
fprintf(stderr, "[grid_record] detbox '%s' → cell %s (%d,%d %dx%d), detect %dx%d\n",
fprintf(stderr, "[grid_record] detbox '%s' → cell %s (%d,%d %dx%d), detect %dx%d, zones=%d",
detcells[i].camera, detcells[i].key,
detcells[i].dx, detcells[i].dy, detcells[i].dw, detcells[i].dh,
detcells[i].detect_w, detcells[i].detect_h);
detcells[i].detect_w, detcells[i].detect_h,
detcells[i].num_zones);
for (int z = 0; z < detcells[i].num_zones; z++)
fprintf(stderr, " %s", detcells[i].zone_storage[z]);
fprintf(stderr, "\n");
}
/* Frigate MQTT subscriber (если задан --frigate-mqtt). */
+17
View File
@@ -149,6 +149,14 @@ typedef struct cfc_overlay_detbox_config {
* Рекомендуется 5000-8000 мс (Frigate publish раз в минуту для stationary,
* меньше при движении). */
int stale_ms;
/* Список разрешённых zones — если задан, MQTT subscriber пропускает
* события у которых after.current_zones пусто либо не пересекается с
* этим списком. Это робастный фильтр когда Frigate 0.17 schema не даёт
* native objects.filters.<obj>.required_zones (см. commit 1726137).
* NULL или пустой массив → принимать все события. */
const char *const *required_zones; /* массив строк */
int required_zones_count;
} cfc_overlay_detbox_config_t;
int cfc_overlay_create_detection_boxes(
@@ -160,6 +168,15 @@ int cfc_overlay_create_detection_boxes(
* правильный overlay по incoming event'у. */
const char *cfc_overlay_detbox_camera_key(cfc_overlay_t *ov);
/* Проверить пересечение current_zones события с required_zones overlay'я.
* - если required_zones пуст → всегда 1 (filter off)
* - если current_zones пуст → 0 (объект вне зон)
* - иначе 1 если хотя бы одна current ∈ required, 0 иначе
* Использует overlay subscriber для отсева street-флуда (см. commit 1726137). */
int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov,
const char *const *current_zones,
int n);
/* Upsert одного active детекта.
* event_id — идентификатор Frigate event'а (для трекинга/end).
* label — "car", "person", и т.п. (для будущего цветового кодирования).
+25 -1
View File
@@ -92,12 +92,13 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
const char *type = jtype ? json_object_get_string(jtype) : "update";
struct json_object *jcam = NULL, *jid = NULL, *jlabel = NULL, *jbox = NULL,
*jft = NULL;
*jft = NULL, *jzones = NULL;
json_object_object_get_ex(jafter, "camera", &jcam);
json_object_object_get_ex(jafter, "id", &jid);
json_object_object_get_ex(jafter, "label", &jlabel);
json_object_object_get_ex(jafter, "box", &jbox);
json_object_object_get_ex(jafter, "frame_time", &jft);
json_object_object_get_ex(jafter, "current_zones", &jzones);
if (!jcam || !jid) { json_object_put(root); return; }
const char *camera = json_object_get_string(jcam);
@@ -109,6 +110,9 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
atomic_fetch_add(&f->events_received, 1);
if (!strcmp(type, "end")) {
/* end вызываем безусловно — даже если zone-filter отсеивает: если
* раньше был moment когда событие попало в zone и upsert состоялся,
* end должен убрать рамку. Если рамки нет — no-op. */
cfc_overlay_detbox_end(ov, event_id);
json_object_put(root);
return;
@@ -129,6 +133,26 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
const char *label = jlabel ? json_object_get_string(jlabel) : "";
double frame_time = jft ? json_object_get_double(jft) : 0.0;
/* Zone-filter — отсев street-флуда. Frigate 0.17 schema не даёт native
* objects.filters.required_zones (Pydantic extra-inputs-not-permitted →
* safe mode), поэтому фильтруем на стороне subscriber'а. */
const char *zone_buf[16];
int n_zones = 0;
if (jzones && json_object_is_type(jzones, json_type_array)) {
int len = (int)json_object_array_length(jzones);
if (len > 16) len = 16;
for (int i = 0; i < len; i++) {
struct json_object *jz = json_object_array_get_idx(jzones, i);
if (jz && json_object_is_type(jz, json_type_string)) {
zone_buf[n_zones++] = json_object_get_string(jz);
}
}
}
if (!cfc_overlay_detbox_match_zones(ov, zone_buf, n_zones)) {
json_object_put(root);
return;
}
cfc_overlay_detbox_upsert(ov, event_id, label, x1, y1, x2, y2,
(int64_t)(frame_time * 1000));
+41
View File
@@ -30,6 +30,8 @@
/* Detection box — один active детект. event_id = key для upsert/end. */
#define CFC_DETBOX_MAX 16
#define CFC_DETBOX_ZONE_MAX 8
#define CFC_DETBOX_ZONE_NAME 32
typedef struct detbox_entry {
char event_id[48]; /* "" = slot пустой */
char label[16];
@@ -43,6 +45,10 @@ typedef struct detbox_data {
pthread_mutex_t mu;
detbox_entry_t entries[CFC_DETBOX_MAX];
int count;
/* Локальная копия required_zones из cfg (cfg.required_zones — это
* указатель caller'а, может быть stack/temp). */
char required_zones[CFC_DETBOX_ZONE_MAX][CFC_DETBOX_ZONE_NAME];
int required_zones_count;
} detbox_data_t;
typedef struct png_data {
@@ -586,6 +592,24 @@ int cfc_overlay_create_detection_boxes(
if (d->cfg.stale_ms <= 0) d->cfg.stale_ms = 6000;
pthread_mutex_init(&d->mu, NULL);
/* Скопировать required_zones — caller владеет источником, мы храним свою копию. */
d->required_zones_count = 0;
if (cfg->required_zones && cfg->required_zones_count > 0) {
int n = cfg->required_zones_count;
if (n > CFC_DETBOX_ZONE_MAX) n = CFC_DETBOX_ZONE_MAX;
for (int i = 0; i < n; i++) {
if (!cfg->required_zones[i]) continue;
strncpy(d->required_zones[d->required_zones_count],
cfg->required_zones[i], CFC_DETBOX_ZONE_NAME - 1);
d->required_zones[d->required_zones_count][CFC_DETBOX_ZONE_NAME - 1] = '\0';
d->required_zones_count++;
}
}
/* cfg.required_zones — внешний указатель, не сохраняем — getter обращается
* к локальному storage'у. */
d->cfg.required_zones = NULL;
d->cfg.required_zones_count = d->required_zones_count;
*out = ov;
return 0;
}
@@ -596,6 +620,23 @@ const char *cfc_overlay_detbox_camera_key(cfc_overlay_t *ov)
return ov->u.detbox.camera_key;
}
int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov,
const char *const *current_zones,
int n)
{
if (!ov || ov->type != CFC_OVERLAY_DETECTION_BOXES) return 0;
detbox_data_t *d = &ov->u.detbox;
if (d->required_zones_count == 0) return 1; /* фильтр выключен */
if (n <= 0 || !current_zones) return 0; /* объект вне зон */
for (int i = 0; i < n; i++) {
if (!current_zones[i]) continue;
for (int j = 0; j < d->required_zones_count; j++) {
if (!strcmp(current_zones[i], d->required_zones[j])) return 1;
}
}
return 0;
}
int cfc_overlay_detbox_upsert(cfc_overlay_t *ov, const char *event_id,
const char *label,
int x1, int y1, int x2, int y2,