From 31bb5d86c61ebca604db415a4cba8824647b50e9 Mon Sep 17 00:00:00 2001 From: perco Date: Wed, 3 Jun 2026 11:14:25 +0200 Subject: [PATCH] Crop thumbnail to vehicle area instead of full frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a plate bbox is detected, the snapshot thumbnail is now cropped around the vehicle (plate bbox + generous margins) and resized to ≤1280px wide. This makes the grid on the index page much more useful. The full best frame is saved separately as best_frame.jpg and used as the main image in the event detail view. Without a plate bbox, fallback crops the center 80% of the frame. Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 5 +++++ app/templates/event_detail.html | 4 ++-- app/watcher.py | 34 +++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index b38d4cf..1edd0b4 100644 --- a/app/main.py +++ b/app/main.py @@ -113,6 +113,10 @@ async def event_detail(request: Request, event_id: str): plate_ocr_abs = os.path.join(event_dir, "plate_ocr.jpg") plate_ocr = f"events/{event_id}/plate_ocr.jpg" if os.path.exists(plate_ocr_abs) else None + # Full best frame (vehicle crop is only the thumbnail; full frame for detail view) + best_frame_abs = os.path.join(event_dir, "best_frame.jpg") + best_frame_path = f"events/{event_id}/best_frame.jpg" if os.path.exists(best_frame_abs) else ev.get("snapshot_path") + is_wl = database.is_whitelisted(ev.get("plate") or "") return templates.TemplateResponse("event_detail.html", { "request": request, @@ -122,6 +126,7 @@ async def event_detail(request: Request, event_id: str): "clip_size": clip_size, "plate_crop": plate_crop, "plate_ocr": plate_ocr, + "best_frame_path": best_frame_path, "capture_duration": CAPTURE_DURATION, "capture_fps": CAPTURE_FPS, "capture_total": CAPTURE_DURATION * CAPTURE_FPS, diff --git a/app/templates/event_detail.html b/app/templates/event_detail.html index 45e0671..babb2b9 100644 --- a/app/templates/event_detail.html +++ b/app/templates/event_detail.html @@ -117,9 +117,9 @@
+ onclick="openLightbox('/{{ best_frame_path }}', 'Meilleure frame ★')"> 🔍 cliquer pour agrandir
diff --git a/app/watcher.py b/app/watcher.py index 3b58484..2bfda44 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -94,6 +94,29 @@ def _get_ai_state() -> dict | None: return None +def _vehicle_crop(frame: np.ndarray, plate_bbox: tuple | None) -> np.ndarray: + """Crop around the vehicle using the plate bbox as anchor. + Expands generously above/around the plate where the vehicle body is.""" + h, w = frame.shape[:2] + if plate_bbox: + px1, py1, px2, py2 = plate_bbox + pw, ph = px2 - px1, py2 - py1 + pad_x = max(int(pw * 3.5), 300) + pad_up = max(int(ph * 9), 400) # vehicle extends above the plate + pad_dn = max(int(ph * 2.5), 100) + cx1 = max(0, px1 - pad_x) + cx2 = min(w, px2 + pad_x) + cy1 = max(0, py1 - pad_up) + cy2 = min(h, py2 + pad_dn) + crop = frame[cy1:cy2, cx1:cx2] + if crop.shape[0] >= 80 and crop.shape[1] >= 80: + return crop + # Fallback: center 60% of frame + cy1, cy2 = int(h * 0.1), int(h * 0.9) + cx1, cx2 = int(w * 0.1), int(w * 0.9) + return frame[cy1:cy2, cx1:cx2] + + 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] @@ -269,10 +292,17 @@ def _process_clip(event_id: str, event_dir: str, clip_path: str, camera_name: st enhanced = cv2.resize(enhanced, (300, int(300 * ch / cw)), interpolation=cv2.INTER_CUBIC) cv2.imwrite(os.path.join(event_dir, "plate_ocr.jpg"), enhanced, [cv2.IMWRITE_JPEG_QUALITY, 95]) - # Save thumbnail + # Save thumbnail — vehicle crop if plate found, full frame otherwise 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]) + thumb = _vehicle_crop(best_frame, plate_bbox) + # Cap thumbnail width to 1280px to keep file size reasonable + th, tw = thumb.shape[:2] + if tw > 1280: + thumb = cv2.resize(thumb, (1280, int(th * 1280 / tw)), interpolation=cv2.INTER_AREA) + cv2.imwrite(snapshot_path, thumb, [cv2.IMWRITE_JPEG_QUALITY, 88]) + # Also save the full best frame so event detail can display it + cv2.imwrite(os.path.join(event_dir, "best_frame.jpg"), best_frame, [cv2.IMWRITE_JPEG_QUALITY, 88]) from database import is_whitelisted if plate and is_whitelisted(plate):