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
|
||||
|
||||
@@ -67,6 +67,24 @@
|
||||
.offline-overlay .icon { font-size: 54px; }
|
||||
.offline-overlay .msg { font-size: 22px; font-weight: 600; }
|
||||
.offline-overlay .sub { font-size: 14px; opacity: 0.8; }
|
||||
.counter-box {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.78);
|
||||
color: #fff;
|
||||
padding: 8px 14px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
z-index: 4;
|
||||
min-width: 150px;
|
||||
}
|
||||
.counter-box .cb-total { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
|
||||
.counter-box .cb-grip { opacity: 0.5; font-size: 11px; margin-top: 4px; }
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 1020px;
|
||||
@@ -143,6 +161,14 @@
|
||||
<div class="video-container">
|
||||
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
||||
<canvas id="lineCanvas"></canvas>
|
||||
<div id="counterBox" class="counter-box" title="Zum Verschieben ziehen">
|
||||
<div class="cb-total">Gesamt: <span id="cTotal">0</span></div>
|
||||
<div>Autos: <span id="cCar">0</span></div>
|
||||
<div>LKW: <span id="cTruck">0</span></div>
|
||||
<div>Busse: <span id="cBus">0</span></div>
|
||||
<div>Motorräder: <span id="cMoto">0</span></div>
|
||||
<div class="cb-grip">⠿ ziehen</div>
|
||||
</div>
|
||||
<div id="offlineOverlay" class="offline-overlay">
|
||||
<div class="icon" id="ovIcon">📷</div>
|
||||
<div class="msg" id="ovMsg">Kamera offline</div>
|
||||
@@ -369,6 +395,57 @@
|
||||
|
||||
checkCamStatus();
|
||||
setInterval(checkCamStatus, 2000);
|
||||
|
||||
// --- Zaehler-Box: Werte pollen ---
|
||||
const cEl = {
|
||||
total: document.getElementById('cTotal'),
|
||||
car: document.getElementById('cCar'),
|
||||
truck: document.getElementById('cTruck'),
|
||||
bus: document.getElementById('cBus'),
|
||||
moto: document.getElementById('cMoto'),
|
||||
};
|
||||
async function updateCounts() {
|
||||
try {
|
||||
const d = await (await fetch('/api/counts', { cache: 'no-store' })).json();
|
||||
cEl.total.textContent = d.total;
|
||||
cEl.car.textContent = d.car;
|
||||
cEl.truck.textContent = d.truck;
|
||||
cEl.bus.textContent = d.bus;
|
||||
cEl.moto.textContent = d.motorcycle;
|
||||
} catch (e) { /* ignorieren */ }
|
||||
}
|
||||
updateCounts();
|
||||
setInterval(updateCounts, 1000);
|
||||
|
||||
// --- Zaehler-Box: per Maus verschiebbar (Position gemerkt) ---
|
||||
const counterBox = document.getElementById('counterBox');
|
||||
const videoContainer = document.querySelector('.video-container');
|
||||
const savedPos = JSON.parse(localStorage.getItem('counterPos') || 'null');
|
||||
if (savedPos) {
|
||||
counterBox.style.left = savedPos.x + 'px';
|
||||
counterBox.style.top = savedPos.y + 'px';
|
||||
}
|
||||
let dragging = false, offX = 0, offY = 0;
|
||||
counterBox.addEventListener('mousedown', (e) => {
|
||||
dragging = true;
|
||||
offX = e.clientX - counterBox.offsetLeft;
|
||||
offY = e.clientY - counterBox.offsetTop;
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
const maxX = videoContainer.clientWidth - counterBox.offsetWidth;
|
||||
const maxY = videoContainer.clientHeight - counterBox.offsetHeight;
|
||||
const x = Math.max(0, Math.min(e.clientX - offX, maxX));
|
||||
const y = Math.max(0, Math.min(e.clientY - offY, maxY));
|
||||
counterBox.style.left = x + 'px';
|
||||
counterBox.style.top = y + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
localStorage.setItem('counterPos', JSON.stringify({ x: counterBox.offsetLeft, y: counterBox.offsetTop }));
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function applyThemeBtn(){var d=document.documentElement.getAttribute('data-theme')==='dark';var b=document.getElementById('themeToggle');if(b)b.textContent=d?'☀️':'🌙';}
|
||||
|
||||
Reference in New Issue
Block a user