From 8c2af7dfabdd44377060333982bdb6fdb366a09f Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 2 Jun 2026 16:19:29 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20couleur=20v=C3=A9hicule,=20supprime=20fil?= =?UTF-8?q?tre=20cam=C3=A9ra,=20am=C3=A9liore=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Couleur : crop autour de la plaque (zone carrosserie au-dessus) au lieu du frame entier ; fallback center 50% si pas de plaque - lpr.read_plate() retourne maintenant (text, conf, bbox) pour exposer la position de la plaque - Index : supprime filtre caméra et badge caméra (une seule caméra) - Carte index : badge ▶ clip si clip disponible - Page détail : section debug (nb frames, taille clip, id), gestion propre des anciens événements sans clip/frames Co-Authored-By: Claude Sonnet 4.6 --- app/lpr.py | 11 +-- app/main.py | 19 ++++- app/templates/event_detail.html | 126 ++++++++++++++++---------------- app/templates/index.html | 71 ++++++------------ app/watcher.py | 32 +++++++- 5 files changed, 132 insertions(+), 127 deletions(-) diff --git a/app/lpr.py b/app/lpr.py index 8a871c9..53ce385 100644 --- a/app/lpr.py +++ b/app/lpr.py @@ -111,11 +111,12 @@ class PlateAnalyzer: avg_conf = float(np.mean(confs)) if confs else 0.0 return text, avg_conf - def read_plate(self, frame: np.ndarray) -> tuple[str, float]: - """Detect plate in frame, read text. Returns (plate_text, confidence).""" + def read_plate(self, frame: np.ndarray) -> tuple[str, float, tuple | None]: + """Detect plate, read text. Returns (plate_text, confidence, bbox_or_None). + bbox = (x1, y1, x2, y2) in pixel coords.""" plate_boxes = self.detect_plates(frame) if not plate_boxes: - return "", 0.0 + return "", 0.0, None x1, y1, x2, y2, plate_conf = plate_boxes[0] pad = 8 @@ -130,6 +131,6 @@ class PlateAnalyzer: # Filter noise: plate must have at least 4 alphanumeric chars alnum = "".join(c for c in text if c.isalnum()) if len(alnum) < 4: - return "", 0.0 + return "", 0.0, (int(x1), int(y1), int(x2), int(y2)) - return text, (plate_conf + ocr_conf) / 2.0 + return text, (plate_conf + ocr_conf) / 2.0, (int(x1), int(y1), int(x2), int(y2)) diff --git a/app/main.py b/app/main.py index 938b00f..53ce746 100644 --- a/app/main.py +++ b/app/main.py @@ -78,6 +78,11 @@ async def index( }) +CAPTURE_DURATION = int(os.environ.get("CAPTURE_DURATION", "15")) +CAPTURE_FPS = int(os.environ.get("CAPTURE_FPS", "5")) +TOP_FRAMES = int(os.environ.get("TOP_FRAMES", "10")) + + @app.get("/event/{event_id}", response_class=HTMLResponse) async def event_detail(request: Request, event_id: str): ev = database.get_event(event_id) @@ -86,18 +91,26 @@ async def event_detail(request: Request, event_id: str): ev["time_str"] = ts_to_str(ev["start_time"]) - # List frames for this event event_dir = os.path.join(EVENTS_DIR, event_id) frame_paths = sorted(glob.glob(os.path.join(event_dir, "frame_*.jpg"))) - frames = [f"events/{event_id}/frame_{i+1:04d}.jpg" for i in range(len(frame_paths))] + frames = [f"events/{event_id}/{os.path.basename(p)}" for p in frame_paths] - has_clip = ev.get("clip_path") and os.path.exists(os.path.join("/data", ev["clip_path"])) + clip_abs = os.path.join("/data", ev["clip_path"]) if ev.get("clip_path") else None + has_clip = bool(clip_abs and os.path.exists(clip_abs)) + clip_size = "" + if has_clip: + size = os.path.getsize(clip_abs) + clip_size = f"{size // 1024 // 1024}MB" if size > 1024 * 1024 else f"{size // 1024}KB" return templates.TemplateResponse("event_detail.html", { "request": request, "ev": ev, "frames": frames, "has_clip": has_clip, + "clip_size": clip_size, + "capture_duration": CAPTURE_DURATION, + "capture_total": CAPTURE_DURATION * CAPTURE_FPS, + "top_frames": TOP_FRAMES, }) diff --git a/app/templates/event_detail.html b/app/templates/event_detail.html index 669aac6..be86736 100644 --- a/app/templates/event_detail.html +++ b/app/templates/event_detail.html @@ -3,119 +3,115 @@ - CamWatch — Passage {{ ev.time_str }} + CamWatch — {{ ev.time_str }} - -
-
- ← Retour - 🚗 - Passage — {{ ev.time_str }} -
- +
+ ← Retour + 🚗 + {{ ev.time_str }}
-
+
- +
- -
-

Informations

- + +
-
Date / Heure
-
{{ ev.time_str }}
+ +
+ {% if ev.plate %} + {{ ev.plate }} + {% else %} + Non lue + {% endif %} +
-
Caméra
-
{{ ev.camera }}
-
- -
-
Plaque détectée
- {% if ev.plate %} - - {{ ev.plate }} - - {% else %} - Non lue - {% endif %} -
- -
-
Couleur véhicule
-
-
+ +
+
{{ ev.color_name or '—' }} - {{ ev.color_hex or '' }} + {{ ev.color_hex or '' }} +
+
+ + +
+ +
+
id: {{ ev.id[:8] }}…
+
frames: {{ frames|length }} / {{ capture_total }}
+
clip: {% if ev.clip_path %}✓ {{ clip_size }}{% else %}—{% endif %}
+
snapshot: {% if ev.snapshot_path %}✓{% else %}—{% endif %}
- -
-
+ +
+
Meilleure frame
-
Meilleure frame (score LPR)
+
Meilleure frame (★)
{% if has_clip %} -
-

Clip vidéo ({{ ev.clip_path.split('/')[-1] }})

-
diff --git a/app/templates/index.html b/app/templates/index.html index 96bd8aa..293f94f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -9,19 +9,17 @@ -
🚗 @@ -36,20 +34,13 @@
- - {% if filter_plate or filter_camera or filter_date %} + {% if filter_plate or filter_date %} ✕ Reset {% endif %}
- {% if not events %}
📷
@@ -58,46 +49,32 @@
{% endif %} -
- - + diff --git a/app/watcher.py b/app/watcher.py index 07cb921..91e607c 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -80,6 +80,29 @@ def _get_ai_state() -> dict | None: return None +def _vehicle_color(frame: np.ndarray, plate_bbox: tuple | None) -> tuple[str, str]: + """Extract dominant color from the vehicle body (above the plate, or center frame).""" + h, w = frame.shape[:2] + if plate_bbox: + px1, py1, px2, py2 = plate_bbox + pw = px2 - px1 + ph = py2 - py1 + # Vehicle body: above plate, same horizontal span expanded ×3 + crop_x1 = max(0, px1 - pw * 2) + crop_x2 = min(w, px2 + pw * 2) + crop_y2 = max(0, py1 - 5) + crop_y1 = max(0, py1 - ph * 10) # 10× plate height above plate + if crop_y2 > crop_y1 and crop_x2 > crop_x1: + crop = frame[int(crop_y1):int(crop_y2), int(crop_x1):int(crop_x2)] + return extract_dominant_color_from_frame(crop) + # Fallback: center 50% of image (excludes sky at top, road at bottom) + cy1 = h // 4 + cy2 = 3 * h // 4 + cx1 = w // 4 + cx2 = 3 * w // 4 + return extract_dominant_color_from_frame(frame[cy1:cy2, cx1:cx2]) + + def _frame_score(frame: np.ndarray, plates: list) -> float: if plates: x1, y1, x2, y2, conf = plates[0] @@ -152,17 +175,18 @@ def _process_event(): return # Step 4: run LPR on best frame - plate, conf = ("", 0.0) + plate, conf, plate_bbox = ("", 0.0, None) if _analyzer: - plate, conf = _analyzer.read_plate(best_frame) - log.info(f"LPR: plate={plate!r} conf={conf:.2f}") + plate, conf, plate_bbox = _analyzer.read_plate(best_frame) + log.info(f"LPR: plate={plate!r} conf={conf:.2f} bbox={plate_bbox}") # Step 5: save thumbnail (best frame) snapshot_file = f"{event_id}.jpg" snapshot_path = os.path.join(SNAPSHOTS_DIR, snapshot_file) cv2.imwrite(snapshot_path, best_frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) - hex_color, color_name = extract_dominant_color_from_frame(best_frame) + # Color: crop vehicle body above plate (avoids sky/road) + hex_color, color_name = _vehicle_color(best_frame, plate_bbox) insert_event( event_id, CAMERA_NAME, int(time.time()), f"snapshots/{snapshot_file}",