Crop thumbnail to vehicle area instead of full frame

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 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-03 11:14:25 +02:00
co-authored by Claude Sonnet 4.6
parent 00368fba0d
commit 31bb5d86c6
3 changed files with 39 additions and 4 deletions
+5
View File
@@ -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,
+2 -2
View File
@@ -117,9 +117,9 @@
<div class="md:col-span-2 card overflow-hidden">
<div class="bg-black relative cursor-zoom-in" style="aspect-ratio:16/9;"
onclick="openLightbox('/{{ ev.snapshot_path }}', 'Meilleure frame ★')">
onclick="openLightbox('/{{ best_frame_path }}', 'Meilleure frame ★')">
<img id="main-img"
src="{% if ev.snapshot_path %}/{{ ev.snapshot_path }}{% endif %}"
src="{% if best_frame_path %}/{{ best_frame_path }}{% endif %}"
class="w-full h-full object-contain">
<span class="absolute bottom-2 right-2 text-xs text-slate-500">🔍 cliquer pour agrandir</span>
</div>
+32 -2
View File
@@ -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):