Fix: Zaehlen verhungert nach Stunden (unbegrenzte Zustaende)

track_positions/counted_ids wuchsen im 24/7-Betrieb unbegrenzt:
-> Speicherleck/Slowdown -> Bildrate bricht ein -> Linienueberquerungen
werden nicht mehr erfasst (Einzel-Erkennung lief weiter).

- Track-IDs altern jetzt (last-seen-Frame) und werden nach
  COUNT_FORGET_FRAMES aus track_positions UND counted_ids entfernt
- bounded State -> kein Leck; loest auch Unterdrueckung bei spaeterer
  Track-ID-Wiederverwendung
- Langzeit-Simulation (5000 Frames): 100/100 gezaehlt, State-Peak 4

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:19:33 +02:00
parent ff4fe55e05
commit c6c87be045
2 changed files with 20 additions and 2 deletions

View File

@@ -30,6 +30,9 @@ export COUNT_BAND_PX=45
# Entprellung gegen Doppelzaehlung bei Track-ID-Wechseln (Pixel-Radius / Frames).
export COUNT_DEDUP_PX=60
export COUNT_DEDUP_FRAMES=12
# 24/7-Betrieb: Track-IDs nach so vielen Frames ohne Sichtung vergessen
# (verhindert Speicherleck / "Zaehlen verhungert nach Stunden").
export COUNT_FORGET_FRAMES=150
# --- MQTT (optional) -------------------------------------------------------
# Komplett abschaltbar: "false" -> App laeuft ohne Broker, sendet keine Events.

19
app.py
View File

@@ -77,6 +77,11 @@ COUNT_BAND_PX = int(os.environ.get("COUNT_BAND_PX", "45"))
# letzten COUNT_DEDUP_FRAMES Frames gezaehlten Punkt liegt.
COUNT_DEDUP_PX = int(os.environ.get("COUNT_DEDUP_PX", "60"))
COUNT_DEDUP_FRAMES = int(os.environ.get("COUNT_DEDUP_FRAMES", "12"))
# 24/7-Betrieb: Track-IDs, die so viele Frames nicht mehr gesehen wurden,
# werden aus track_positions UND counted_ids entfernt. Verhindert das
# unbegrenzte Wachsen der Zustaende (Speicherleck) und falsches Unterdruecken
# bei spaeterer Track-ID-Wiederverwendung. Wert > Verweildauer eines Fahrzeugs.
COUNT_FORGET_FRAMES = int(os.environ.get("COUNT_FORGET_FRAMES", "150"))
# --- MQTT: ein Event pro Linienueberquerung (fuer n8n -> NocoDB) -----------
# Komplett abschaltbar: MQTT_ENABLED=false -> App laeuft ohne Broker/Events.
@@ -341,6 +346,16 @@ def process_frame(frame, det_model, det_names, line_start, line_end, state, sour
c for c in recent_counts if frame_idx - c[2] <= COUNT_DEDUP_FRAMES
]
# Garbage Collection: laengst verschwundene Track-IDs vergessen, damit
# track_positions/counted_ids im 24/7-Betrieb nicht unbegrenzt wachsen.
stale = [
tid for tid, pos in track_positions.items()
if frame_idx - pos[2] > COUNT_FORGET_FRAMES
]
for tid in stale:
track_positions.pop(tid, None)
counted_ids.discard(tid)
if results and results[0].boxes is not None and results[0].boxes.id is not None:
boxes = results[0].boxes.xyxy.int().cpu().tolist()
class_ids = results[0].boxes.cls.int().cpu().tolist()
@@ -358,7 +373,7 @@ def process_frame(frame, det_model, det_names, line_start, line_end, state, sour
# (a) Klassischer Segment-Schnitt: Punkt davor UND dahinter.
if track_id in track_positions:
prev_x, prev_y = track_positions[track_id]
prev_x, prev_y, _ = track_positions[track_id]
cv2.line(frame, (prev_x, prev_y), (center_x, center_y), (255, 100, 0), 2)
if crossed_line((prev_x, prev_y), (center_x, center_y), line_start, line_end):
crossed = True
@@ -381,7 +396,7 @@ def process_frame(frame, det_model, det_names, line_start, line_end, state, sour
publish_crossing(label_name, track_id, source)
cv2.circle(frame, (center_x, center_y), 25, (0, 255, 0), 5)
track_positions[track_id] = (center_x, center_y)
track_positions[track_id] = (center_x, center_y, frame_idx)
box_color = (0, 255, 0) if label_name in VEHICLE_CLASSES else (255, 0, 0)
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)