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):