feat(pose): enrichir card dashboard

data_feeds/feeds/pose.py emet maintenant /data/pose/stats
avg_conf avg_size cx_bar cy_bar apres chaque batch :
- avg_conf : moyenne des confidences keypoints (0..1)
- avg_size : moyenne des bbox surface w*h normalisee
  (proxy distance camera ; 1.0 = cadre plein)
- cx_bar, cy_bar : barycentre des centres de bboxes
  (utile pour piloter des FX qui suivent l'occupation)

Dashboard handler 'pose' enrichi (wide card) :
- count en valeur principale
- subline : conf % + occup. % cadre + centre (cx, cy)
- bordure warn si avg_conf < 0.4
- bboxes stockees dans c._persons pour overlay futur
This commit is contained in:
L'électron rare
2026-05-13 20:29:26 +02:00
parent bcb259d045
commit ee86990195
2 changed files with 51 additions and 3 deletions
+27 -1
View File
@@ -9,10 +9,11 @@ Pourquoi YOLOv8-pose plutot qu'OpenPose proper ?
keypoints (x_norm, y_norm, conf) emis sur OSC.
Sortie OSC :
/data/pose/count <n>
/data/pose/count <n>
/data/pose/person <idx> <cx> <cy> <w> <h> <conf>
/data/pose/skel <idx> <conf_avg> <x0 y0 c0 ... x16 y16 c16>
/data/pose/bone <idx> <kp_a> <kp_b> (a la connexion, statique)
/data/pose/stats <avg_conf> <avg_size> <cx_bar> <cy_bar> (par batch)
Toutes les coordonnees sont normalisees 0..1 (origine top-left).
"""
@@ -137,12 +138,24 @@ async def run(ctx) -> None:
n = 0 if kp_xy is None else int(len(kp_xy))
ctx.send("count", float(n))
# Agregats pour le dashboard : confiance moyenne globale,
# taille moyenne (proxy distance cam), centre du barycentre
# des personnes, fraction de l'image occupee.
global_conf_sum = 0.0
global_conf_n = 0
size_sum = 0.0
cx_sum = 0.0
cy_sum = 0.0
for i in range(n):
# bbox normalisee
if boxes is not None and i < len(boxes):
b = boxes.xywhn[i].cpu().numpy().tolist() # cx, cy, w, h
conf_b = float(boxes.conf[i].item())
ctx.send("person", float(i), *b, conf_b)
cx_sum += float(b[0])
cy_sum += float(b[1])
size_sum += float(b[2]) * float(b[3])
if not emit_kp or kp_xy is None:
continue
pts = kp_xy[i].cpu().numpy() # (17, 2) px
@@ -158,6 +171,19 @@ async def run(ctx) -> None:
conf_sum += cc
avg = conf_sum / max(1, len(pts))
ctx.send("skel", float(i), avg, *flat)
global_conf_sum += avg
global_conf_n += 1
# /data/pose/stats avg_conf avg_size cx cy
if n > 0:
avg_conf = global_conf_sum / max(1, global_conf_n)
avg_size = size_sum / n
cx_bar = cx_sum / n
cy_bar = cy_sum / n
ctx.send("stats", float(avg_conf), float(avg_size),
float(cx_bar), float(cy_bar))
else:
ctx.send("stats", 0.0, 0.0, 0.5, 0.5)
# cadence
dt = time.monotonic() - t0
+24 -2
View File
@@ -295,10 +295,32 @@ const handlers = {
},
pose: (m) => {
if (m.sub === "count") {
const c = ensureCard("pose", { title: "Pose YOLO · personnes", order: 3 });
c.valueEl.textContent = String(m.args[0] | 0);
const c = ensureCard("pose", { title: "Pose YOLO · personnes",
order: 3, wide: true });
c._lastCount = m.args[0] | 0;
c.valueEl.textContent = String(c._lastCount);
pushHistory(c, m.args[0]);
drawSpark(c, "#ff8838");
} else if (m.sub === "stats") {
const [avgConf, avgSize, cxBar, cyBar] = m.args;
const c = ensureCard("pose", { title: "Pose YOLO · personnes",
order: 3, wide: true });
const occup = (avgSize * 100).toFixed(1);
const conf = (avgConf * 100).toFixed(0);
c.subEl.textContent =
`conf ${conf}% · occup. ${occup}% cadre · centre (${cxBar.toFixed(2)}, ${cyBar.toFixed(2)})`;
if (avgConf < 0.4) c.el.classList.add("warn");
else c.el.classList.remove("warn");
} else if (m.sub === "person") {
const c = ensureCard("pose", { title: "Pose YOLO · personnes",
order: 3, wide: true });
// Stocke la liste des bboxes pour overlay potentiel
c._persons = c._persons || [];
const idx = m.args[0] | 0;
c._persons[idx] = {
cx: m.args[1], cy: m.args[2],
w: m.args[3], h: m.args[4], conf: m.args[5],
};
}
},
mempool: (m) => {