detbox: label + score pill над bbox (FreeType)
Раньше detbox рисовал только рамку. Теперь на каждое active событие отображает "label NN%" в полупрозрачной pill цвета рамки над верхним краем bbox. include/cuframes_composer/overlay.h: - cfc_overlay_detbox_config_t: + font_path, font_size, label_bg_alpha - cfc_overlay_detbox_upsert: + float score parameter src/overlay.c: - detbox_entry_t: + score, rendered_text (кеш), text_atlas/w/h/pitch - detbox_data_t: + ft_library, ft_face, font_path_copy, font_size_px - cfc_overlay_create_detection_boxes: opens FT face если font_path задан - cfc_overlay_destroy: free cached atlas'ы + FT face/library - detbox_rebuild_label_atlas: format "label NN%" → FT render → VRAM upload (reuse text_measure / text_render из CFC_OVERLAY_TEXT) - upsert: вызывает rebuild при изменении label/score (кеш по rendered_text) - draw_detection_boxes: snap расширен под text_atlas; после border рисует pill bg (fill_nv12 цветом рамки) + текст (blit_rgba_nv12, белый) - mutex hold всю draw — atlas чтение должно быть atomic против upsert src/frigate_mqtt.c: - parse after.score → передаём в upsert (Frigate & yoloworld envelope совместимы: оба содержат score) examples/grid_record.c: - 4 frigate detbox: font_path=/fonts/DejaVuSans-Bold.ttf, font_size=16 - 4 yw detbox: то же — magenta pill с белым текстом Live: cfc-grid healthy 25 fps, image gx/cuframes-composer:0.11b-step4 deployed. Видно на TV при первом detection event. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -570,6 +570,11 @@ int main(int argc, char **argv)
|
||||
.stale_ms = 8000,
|
||||
.required_zones = detcells[i].num_zones ? detcells[i].zone_ptrs : NULL,
|
||||
.required_zones_count = detcells[i].num_zones,
|
||||
/* Label+score pill — белым текстом на полупрозрачном зелёном
|
||||
* фоне (color_y/u/v). Шрифт DejaVu mounted из /fonts (см. compose). */
|
||||
.font_path = "/fonts/DejaVuSans-Bold.ttf",
|
||||
.font_size = 16,
|
||||
.label_bg_alpha = 200,
|
||||
};
|
||||
if (cfc_overlay_create_detection_boxes(&dc, &detbox_overlays[i]) != 0) {
|
||||
fprintf(stderr, "[grid_record] detbox create failed для '%s'\n",
|
||||
@@ -608,6 +613,9 @@ int main(int argc, char **argv)
|
||||
.stale_ms = 8000,
|
||||
.required_zones = detcells[i].num_zones ? detcells[i].zone_ptrs : NULL,
|
||||
.required_zones_count = detcells[i].num_zones,
|
||||
.font_path = "/fonts/DejaVuSans-Bold.ttf",
|
||||
.font_size = 16,
|
||||
.label_bg_alpha = 200,
|
||||
};
|
||||
if (cfc_overlay_create_detection_boxes(&yc, &yw_detbox_overlays[i]) != 0) {
|
||||
fprintf(stderr, "[grid_record] yw detbox create failed для '%s'\n",
|
||||
|
||||
@@ -165,6 +165,13 @@ typedef struct cfc_overlay_detbox_config {
|
||||
* NULL или пустой массив → принимать все события. */
|
||||
const char *const *required_zones; /* массив строк */
|
||||
int required_zones_count;
|
||||
|
||||
/* Label + confidence text над bbox.
|
||||
* NULL font_path → рисовать только рамки (legacy behavior).
|
||||
* Текст формата "<label> <pct>%" в pill. Color = тот же что у рамки. */
|
||||
const char *font_path; /* напр. "/fonts/DejaVuSans-Bold.ttf" */
|
||||
int font_size; /* px, рекомендуется 16-20 */
|
||||
int label_bg_alpha; /* 0..255 (default 200 если 0) */
|
||||
} cfc_overlay_detbox_config_t;
|
||||
|
||||
int cfc_overlay_create_detection_boxes(
|
||||
@@ -195,6 +202,8 @@ int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov,
|
||||
/* Upsert одного active детекта.
|
||||
* event_id — идентификатор Frigate event'а (для трекинга/end).
|
||||
* label — "car", "person", и т.п. (для будущего цветового кодирования).
|
||||
* score — confidence ∈ [0..1] (используется для подписи "label NN%").
|
||||
* Если < 0 — score не рисуется.
|
||||
* x1/y1/x2/y2 — bbox в detect-разрешении (raw Frigate coords).
|
||||
* frame_time_ms — Frigate frame_time для TTL.
|
||||
* Thread-safe (mutex внутри). */
|
||||
@@ -202,6 +211,7 @@ int cfc_overlay_detbox_upsert(
|
||||
cfc_overlay_t *ov,
|
||||
const char *event_id,
|
||||
const char *label,
|
||||
float score,
|
||||
int x1, int y1, int x2, int y2,
|
||||
int64_t frame_time_ms
|
||||
);
|
||||
|
||||
+4
-2
@@ -92,13 +92,14 @@ 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, *jzones = NULL;
|
||||
*jft = NULL, *jzones = NULL, *jscore = 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);
|
||||
json_object_object_get_ex(jafter, "score", &jscore);
|
||||
|
||||
if (!jcam || !jid) { json_object_put(root); return; }
|
||||
const char *camera = json_object_get_string(jcam);
|
||||
@@ -153,6 +154,7 @@ 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;
|
||||
float score = jscore ? (float)json_object_get_double(jscore) : -1.0f;
|
||||
|
||||
/* Zone-filter overlay'я (current_zones уже распарсены выше). Отсев
|
||||
* street-флуда — Frigate 0.17 не даёт native objects.filters.required_zones. */
|
||||
@@ -161,7 +163,7 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
|
||||
return;
|
||||
}
|
||||
|
||||
cfc_overlay_detbox_upsert(ov, event_id, label, x1, y1, x2, y2,
|
||||
cfc_overlay_detbox_upsert(ov, event_id, label, score, x1, y1, x2, y2,
|
||||
(int64_t)(frame_time * 1000));
|
||||
|
||||
json_object_put(root);
|
||||
|
||||
+171
-9
@@ -35,13 +35,20 @@
|
||||
typedef struct detbox_entry {
|
||||
char event_id[48]; /* "" = slot пустой */
|
||||
char label[16];
|
||||
float score; /* 0..1; <0 → не рисовать score */
|
||||
int x1, y1, x2, y2; /* raw detect coords */
|
||||
int64_t last_update_ms; /* для TTL */
|
||||
/* Cached label atlas. Rebuild только при изменении label_with_score_txt.
|
||||
* Atlas — RGBA на VRAM (того же color что и border, но через FT bitmap). */
|
||||
char rendered_text[32]; /* что сейчас в atlas'е ("car 87%") */
|
||||
CUdeviceptr text_atlas; /* 0 = нет cached */
|
||||
int text_w, text_h, text_pitch; /* RGBA buffer size */
|
||||
} detbox_entry_t;
|
||||
|
||||
typedef struct detbox_data {
|
||||
cfc_overlay_detbox_config_t cfg;
|
||||
char camera_key[64]; /* копия cfg.camera_key */
|
||||
char font_path_copy[128]; /* копия cfg.font_path */
|
||||
pthread_mutex_t mu;
|
||||
detbox_entry_t entries[CFC_DETBOX_MAX];
|
||||
int count;
|
||||
@@ -49,6 +56,11 @@ typedef struct detbox_data {
|
||||
* указатель caller'а, может быть stack/temp). */
|
||||
char required_zones[CFC_DETBOX_ZONE_MAX][CFC_DETBOX_ZONE_NAME];
|
||||
int required_zones_count;
|
||||
/* FreeType — opaque (FT_Library, FT_Face) — нужен <ft2build.h>.
|
||||
* Hold как void* чтобы не тянуть FT API в overlay.h. */
|
||||
void *ft_library; /* FT_Library */
|
||||
void *ft_face; /* FT_Face — NULL если нет font_path */
|
||||
int font_size_px;
|
||||
} detbox_data_t;
|
||||
|
||||
typedef struct png_data {
|
||||
@@ -600,6 +612,59 @@ static int draw_text(cfc_overlay_t *ov, CUstream stream,
|
||||
|
||||
/* ── DETECTION_BOXES (Phase 7) ────────────────────────────────────────── */
|
||||
|
||||
/* Re-render label atlas в VRAM для entry. Called с mutex'ом удерживаемым
|
||||
* caller'ом (mu локает entries access). Если label/score не изменились —
|
||||
* no-op. Возвращает 0 даже если FT нет (атлас просто не создаётся).
|
||||
*
|
||||
* Алгоритм: format → measure → render RGBA → upload в CUDA. Использует
|
||||
* существующие text_measure/text_render (общие с CFC_OVERLAY_TEXT).
|
||||
*
|
||||
* Color RGBA пишется белым с background pill цвета overlay (фон даст
|
||||
* контраст border-цвета, белый текст всегда читаемо). */
|
||||
static void detbox_rebuild_label_atlas(detbox_data_t *d, int slot)
|
||||
{
|
||||
if (!d->ft_face) return;
|
||||
FT_Face face = (FT_Face)d->ft_face;
|
||||
detbox_entry_t *e = &d->entries[slot];
|
||||
|
||||
/* Формат: "label NN%" (если score >= 0) или просто "label". */
|
||||
char txt[32];
|
||||
if (e->score >= 0.0f) {
|
||||
int pct = (int)(e->score * 100.0f + 0.5f);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
snprintf(txt, sizeof(txt), "%s %d%%", e->label, pct);
|
||||
} else {
|
||||
snprintf(txt, sizeof(txt), "%s", e->label);
|
||||
}
|
||||
if (!strcmp(txt, e->rendered_text)) return; /* кеш свеж */
|
||||
|
||||
/* Measure */
|
||||
int w = 0, h = 0, ascent = 0;
|
||||
if (text_measure(face, txt, &w, &h, &ascent) != 0) return;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
unsigned char *cpu = calloc((size_t)w * h, 4);
|
||||
if (!cpu) return;
|
||||
/* Белый текст для контраста с pill background. */
|
||||
text_render(face, txt, cpu, w, h, ascent, 255, 255, 255);
|
||||
|
||||
/* Free old + allocate new — каждый раз. Размер может меняться. */
|
||||
if (e->text_atlas) { cuMemFree(e->text_atlas); e->text_atlas = 0; }
|
||||
if (cuMemAlloc(&e->text_atlas, (size_t)w * h * 4) != CUDA_SUCCESS) {
|
||||
free(cpu); return;
|
||||
}
|
||||
if (cuMemcpyHtoD(e->text_atlas, cpu, (size_t)w * h * 4) != CUDA_SUCCESS) {
|
||||
cuMemFree(e->text_atlas); e->text_atlas = 0;
|
||||
free(cpu); return;
|
||||
}
|
||||
free(cpu);
|
||||
e->text_w = w; e->text_h = h; e->text_pitch = w * 4;
|
||||
strncpy(e->rendered_text, txt, sizeof(e->rendered_text) - 1);
|
||||
e->rendered_text[sizeof(e->rendered_text) - 1] = '\0';
|
||||
}
|
||||
|
||||
|
||||
int cfc_overlay_create_detection_boxes(
|
||||
const cfc_overlay_detbox_config_t *cfg, cfc_overlay_t **out)
|
||||
{
|
||||
@@ -637,6 +702,30 @@ int cfc_overlay_create_detection_boxes(
|
||||
d->cfg.required_zones = NULL;
|
||||
d->cfg.required_zones_count = d->required_zones_count;
|
||||
|
||||
/* FreeType — опционально, для рендеринга label+score над bbox.
|
||||
* Если font_path не задан → text не рисуется (legacy behavior). */
|
||||
d->ft_library = NULL;
|
||||
d->ft_face = NULL;
|
||||
d->font_size_px = cfg->font_size > 0 ? cfg->font_size : 16;
|
||||
if (cfg->font_path) {
|
||||
strncpy(d->font_path_copy, cfg->font_path, sizeof(d->font_path_copy) - 1);
|
||||
d->cfg.font_path = d->font_path_copy;
|
||||
FT_Library lib = NULL;
|
||||
if (FT_Init_FreeType(&lib) == 0) {
|
||||
FT_Face face = NULL;
|
||||
if (FT_New_Face(lib, d->font_path_copy, 0, &face) == 0) {
|
||||
FT_Set_Pixel_Sizes(face, 0, (FT_UInt)d->font_size_px);
|
||||
d->ft_library = lib;
|
||||
d->ft_face = face;
|
||||
} else {
|
||||
FT_Done_FreeType(lib);
|
||||
fprintf(stderr, "[overlay] detbox: font %s не открыт\n",
|
||||
d->font_path_copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (d->cfg.label_bg_alpha <= 0) d->cfg.label_bg_alpha = 200;
|
||||
|
||||
*out = ov;
|
||||
return 0;
|
||||
}
|
||||
@@ -681,6 +770,7 @@ int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov,
|
||||
|
||||
int cfc_overlay_detbox_upsert(cfc_overlay_t *ov, const char *event_id,
|
||||
const char *label,
|
||||
float score,
|
||||
int x1, int y1, int x2, int y2,
|
||||
int64_t frame_time_ms)
|
||||
{
|
||||
@@ -714,12 +804,19 @@ int cfc_overlay_detbox_upsert(cfc_overlay_t *ov, const char *event_id,
|
||||
strncpy(d->entries[slot].label, label,
|
||||
sizeof(d->entries[slot].label) - 1);
|
||||
}
|
||||
d->entries[slot].score = score;
|
||||
d->entries[slot].x1 = x1;
|
||||
d->entries[slot].y1 = y1;
|
||||
d->entries[slot].x2 = x2;
|
||||
d->entries[slot].y2 = y2;
|
||||
d->entries[slot].last_update_ms = now_ms();
|
||||
|
||||
/* Rebuild label atlas (если label/score изменились). FT render + CUDA
|
||||
* upload под mutex'ом — это slow (~few ms), но upsert редкий (cooldown
|
||||
* детектора 3s × 4 камеры × 2-3 labels ~ 3-4/sec total). Если text
|
||||
* не изменился — no-op. */
|
||||
detbox_rebuild_label_atlas(d, slot);
|
||||
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
return 0;
|
||||
}
|
||||
@@ -751,22 +848,45 @@ static int draw_detection_boxes(cfc_overlay_t *ov,
|
||||
detbox_data_t *d = &ov->u.detbox;
|
||||
int64_t cutoff = now_ms() - d->cfg.stale_ms;
|
||||
|
||||
/* Snapshot active boxes под mutex'ом — короткая критическая секция. */
|
||||
typedef struct { int x1, y1, x2, y2; } box_t;
|
||||
/* Snapshot active boxes под mutex'ом — короткая критическая секция.
|
||||
* Также захватываем text_atlas pointer + size (read-only — упомянуть в
|
||||
* thread-safety: atlas остаётся валидным пока entry не disposed,
|
||||
* а entry disposed только в upsert/end/destroy под mutex'ом — и мы тут
|
||||
* не держим mutex после snapshot. Если в этот промежуток какой-то
|
||||
* upsert rebuild'ит атлас (cuMemFree + alloc) — мы будем читать
|
||||
* освобождённую память. Делаем под mutex'ом). */
|
||||
typedef struct {
|
||||
int x1, y1, x2, y2;
|
||||
CUdeviceptr text_atlas;
|
||||
int text_w, text_h, text_pitch;
|
||||
} box_t;
|
||||
box_t snap[CFC_DETBOX_MAX];
|
||||
int snap_n = 0;
|
||||
pthread_mutex_lock(&d->mu);
|
||||
for (int i = 0; i < CFC_DETBOX_MAX; i++) {
|
||||
if (!d->entries[i].event_id[0]) continue;
|
||||
if (d->entries[i].last_update_ms < cutoff) continue; /* TTL expired */
|
||||
snap[snap_n++] = (box_t){
|
||||
.x1 = d->entries[i].x1, .y1 = d->entries[i].y1,
|
||||
.x2 = d->entries[i].x2, .y2 = d->entries[i].y2,
|
||||
};
|
||||
snap[snap_n].x1 = d->entries[i].x1;
|
||||
snap[snap_n].y1 = d->entries[i].y1;
|
||||
snap[snap_n].x2 = d->entries[i].x2;
|
||||
snap[snap_n].y2 = d->entries[i].y2;
|
||||
snap[snap_n].text_atlas = d->entries[i].text_atlas;
|
||||
snap[snap_n].text_w = d->entries[i].text_w;
|
||||
snap[snap_n].text_h = d->entries[i].text_h;
|
||||
snap[snap_n].text_pitch = d->entries[i].text_pitch;
|
||||
snap_n++;
|
||||
}
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
/* Удерживаем mutex до конца draw — atlas чтение в blit_rgba_nv12
|
||||
* issue'ит CUDA копирование, после launch host может отпустить.
|
||||
* Но launch синхронный wrt host — поэтому safe отпустить после
|
||||
* последнего launch. Делаем так: snapshot done → unlock. blit launches
|
||||
* (после) идут через CUstream — async wrt host, но CUDA hold'ает
|
||||
* input atlas pointer до completion. Между launch и atlas free есть race.
|
||||
* Решение: НЕ освобождать atlas в upsert, только аллоцировать новый
|
||||
* (старый утечёт). Альтернатива — synchronize stream перед free.
|
||||
* На MVP оставляем mutex до конца — простота важнее. */
|
||||
|
||||
if (snap_n == 0) return 0;
|
||||
if (snap_n == 0) { pthread_mutex_unlock(&d->mu); return 0; }
|
||||
|
||||
/* Coordinate mapping: detect → cell. Линейный scale + offset.
|
||||
* detect_w/h не кэшированы — берутся из cfg в момент draw'а, layout
|
||||
@@ -813,7 +933,39 @@ static int draw_detection_boxes(cfc_overlay_t *ov,
|
||||
x + w - tt, y + tt, tt, h - 2 * tt,
|
||||
d->cfg.color_y, d->cfg.color_u, d->cfg.color_v,
|
||||
d->cfg.alpha);
|
||||
|
||||
/* Label + score pill — над bbox (или внутри, если bbox у верха frame'а). */
|
||||
if (snap[i].text_atlas && snap[i].text_w > 0 && snap[i].text_h > 0) {
|
||||
int pad = 4;
|
||||
int pill_w = snap[i].text_w + 2 * pad;
|
||||
int pill_h = snap[i].text_h + 2 * pad;
|
||||
int pill_x = x; /* выравниваем по левому краю bbox */
|
||||
int pill_y = y - pill_h; /* над верхним краем */
|
||||
if (pill_y < 0) pill_y = y + tt; /* fallback внутрь top */
|
||||
pill_x &= ~1; pill_y &= ~1;
|
||||
int eff_w = pill_w & ~1;
|
||||
int eff_h = pill_h & ~1;
|
||||
if (pill_x + eff_w > frame_w) eff_w = (frame_w - pill_x) & ~1;
|
||||
if (pill_y + eff_h > frame_h) eff_h = (frame_h - pill_y) & ~1;
|
||||
if (eff_w > 0 && eff_h > 0) {
|
||||
/* Pill background — цвет border'а (зелёный/magenta) полу-непрозрачный. */
|
||||
cfc_cugrid_fill_nv12(stream, dst_y, pitch_y, dst_uv, pitch_uv,
|
||||
pill_x, pill_y, eff_w, eff_h,
|
||||
d->cfg.color_y, d->cfg.color_u, d->cfg.color_v,
|
||||
d->cfg.label_bg_alpha);
|
||||
/* Text — белый RGBA → blend в NV12. */
|
||||
int text_x = (pill_x + pad) & ~1;
|
||||
int text_y = (pill_y + pad) & ~1;
|
||||
cfc_cugrid_blit_rgba_nv12(stream, dst_y, pitch_y, dst_uv, pitch_uv,
|
||||
text_x, text_y,
|
||||
snap[i].text_atlas,
|
||||
snap[i].text_w, snap[i].text_h,
|
||||
snap[i].text_pitch,
|
||||
255);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&d->mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -858,7 +1010,17 @@ int cfc_overlay_destroy(cfc_overlay_t *ov)
|
||||
free(td->font_path_owned);
|
||||
}
|
||||
if (ov->type == CFC_OVERLAY_DETECTION_BOXES) {
|
||||
pthread_mutex_destroy(&ov->u.detbox.mu);
|
||||
detbox_data_t *d = &ov->u.detbox;
|
||||
/* Освободить cached text atlas'ы. */
|
||||
for (int i = 0; i < CFC_DETBOX_MAX; i++) {
|
||||
if (d->entries[i].text_atlas) {
|
||||
cuMemFree(d->entries[i].text_atlas);
|
||||
d->entries[i].text_atlas = 0;
|
||||
}
|
||||
}
|
||||
if (d->ft_face) FT_Done_Face((FT_Face)d->ft_face);
|
||||
if (d->ft_library) FT_Done_FreeType((FT_Library)d->ft_library);
|
||||
pthread_mutex_destroy(&d->mu);
|
||||
}
|
||||
free(ov);
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user