Webcam: Zaehler-Box als verschiebbares HTML-Overlay (per Maus)
Statt ins Bild gebrannt ist der Zaehler jetzt ein HTML-Overlay, das frei mit der Maus positioniert werden kann; Position wird in localStorage gemerkt. Werte kommen ueber /api/counts (Live-Polling). - draw_overlay/render_static/process_frame: draw_counter-Flag (Webcam laesst Box weg, Video-Upload behaelt sie eingebrannt) - Grabber-State auf self.state -> /api/counts liest live mit - Drag-Logik + Counts-Polling im Frontend Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
48
app.py
48
app.py
@@ -297,8 +297,10 @@ def _recently_counted(recent_counts, cx, cy) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def draw_overlay(frame, line_start, line_end, state):
|
||||
"""Zeichnet Zaehllinie (gestrichelt) + Zaehler-Box. Kein YOLO."""
|
||||
def draw_overlay(frame, line_start, line_end, state, draw_counter=True):
|
||||
"""Zeichnet Zaehllinie (gestrichelt) + optional die Zaehler-Box. Kein YOLO.
|
||||
draw_counter=False -> Box wird im Webcam-Pfad als verschiebbares HTML-
|
||||
Overlay angezeigt statt ins Bild gezeichnet."""
|
||||
types = state["types"]
|
||||
|
||||
cv2.line(frame, line_start, line_end, (0, 255, 255), 3, cv2.LINE_AA)
|
||||
@@ -313,23 +315,24 @@ def draw_overlay(frame, line_start, line_end, state):
|
||||
y2_dash = int(line_start[1] + t2 * (line_end[1] - line_start[1]))
|
||||
cv2.line(frame, (x1_dash, y1_dash), (x2_dash, y2_dash), (0, 0, 0), 3)
|
||||
|
||||
cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1)
|
||||
cv2.putText(frame, f"Gesamt: {state['count']}", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
||||
cv2.putText(frame, f"Autos: {types.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"LKW: {types.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"Busse: {types.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"Motorraeder: {types.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||
if draw_counter:
|
||||
cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1)
|
||||
cv2.putText(frame, f"Gesamt: {state['count']}", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
||||
cv2.putText(frame, f"Autos: {types.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"LKW: {types.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"Busse: {types.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||
cv2.putText(frame, f"Motorraeder: {types.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||
return frame
|
||||
|
||||
|
||||
def render_static(img, line_start, line_end, state):
|
||||
def render_static(img, line_start, line_end, state, draw_counter=True):
|
||||
"""Nur Overlay auf den Rohframe -> wird genutzt, wenn nichts in Bewegung
|
||||
ist (Motion-Gate). Laeuft kein YOLO -> GPU bleibt idle."""
|
||||
frame = cv2.resize(img, FRAME_SIZE)
|
||||
return draw_overlay(frame, line_start, line_end, state)
|
||||
return draw_overlay(frame, line_start, line_end, state, draw_counter=draw_counter)
|
||||
|
||||
|
||||
def process_frame(frame, det_model, det_names, line_start, line_end, state, source="video"):
|
||||
def process_frame(frame, det_model, det_names, line_start, line_end, state, source="video", draw_counter=True):
|
||||
"""
|
||||
Skaliert den Frame, fuehrt YOLO-Tracking aus, zaehlt Linienueberquerungen
|
||||
und zeichnet alle Overlays. Mutiert `state` in-place und gibt den
|
||||
@@ -436,7 +439,7 @@ def process_frame(frame, det_model, det_names, line_start, line_end, state, sour
|
||||
flush=True,
|
||||
)
|
||||
|
||||
return draw_overlay(frame, line_start, line_end, state)
|
||||
return draw_overlay(frame, line_start, line_end, state, draw_counter=draw_counter)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -470,6 +473,7 @@ class WebcamGrabber:
|
||||
self.viewers = 0
|
||||
self._last_active_ts = 0.0 # letzter Zeitpunkt mit Zuschauer (Karenzzeit)
|
||||
self.paused = False # manuell freigegeben -> ESP32-Slot freigeben
|
||||
self.state = new_state() # Zaehl-/Tracking-Zustand (auch fuer /api/counts)
|
||||
self.reset_flag = Event()
|
||||
self.line = dict(SAVED_LINE)
|
||||
|
||||
@@ -589,7 +593,7 @@ class WebcamGrabber:
|
||||
|
||||
# -- Hintergrund-Thread (laeuft die ganze Prozess-Lebensdauer) ----------
|
||||
def _run(self):
|
||||
state = new_state()
|
||||
state = self.state # selbe Instanz -> /api/counts liest live mit
|
||||
|
||||
while True:
|
||||
# Ohne Always-on: nur verbinden, wenn jemand zuschaut (mit Karenzzeit).
|
||||
@@ -647,9 +651,9 @@ class WebcamGrabber:
|
||||
|
||||
line_start, line_end = self._current_line()
|
||||
if moving:
|
||||
frame = process_frame(img, self.model, self.names, line_start, line_end, state, source="webcam")
|
||||
frame = process_frame(img, self.model, self.names, line_start, line_end, state, source="webcam", draw_counter=False)
|
||||
else:
|
||||
frame = render_static(img, line_start, line_end, state)
|
||||
frame = render_static(img, line_start, line_end, state, draw_counter=False)
|
||||
|
||||
ok, out = cv2.imencode(".jpg", frame)
|
||||
if ok:
|
||||
@@ -832,6 +836,20 @@ def webcam_status():
|
||||
return jsonify({"online": webcam.is_online(), "paused": webcam.paused})
|
||||
|
||||
|
||||
@app.route("/api/counts", methods=["GET"])
|
||||
def api_counts():
|
||||
"""Aktuelle Zaehlerstaende des Webcam-Grabbers (fuer das HTML-Overlay)."""
|
||||
st = webcam.state
|
||||
types = st["types"]
|
||||
return jsonify({
|
||||
"total": st["count"],
|
||||
"car": types.get("car", 0),
|
||||
"truck": types.get("truck", 0),
|
||||
"bus": types.get("bus", 0),
|
||||
"motorcycle": types.get("motorcycle", 0),
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/grabber_toggle", methods=["POST"])
|
||||
def grabber_toggle():
|
||||
"""Grabber pausieren/aufnehmen: pausiert gibt den ESP32-Slot frei
|
||||
|
||||
Reference in New Issue
Block a user