Fix couleur véhicule, supprime filtre caméra, améliore debug

- 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 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-02 16:19:29 +02:00
co-authored by Claude Sonnet 4.6
parent 528a158c42
commit 8c2af7dfab
5 changed files with 132 additions and 127 deletions
+6 -5
View File
@@ -111,11 +111,12 @@ class PlateAnalyzer:
avg_conf = float(np.mean(confs)) if confs else 0.0 avg_conf = float(np.mean(confs)) if confs else 0.0
return text, avg_conf return text, avg_conf
def read_plate(self, frame: np.ndarray) -> tuple[str, float]: def read_plate(self, frame: np.ndarray) -> tuple[str, float, tuple | None]:
"""Detect plate in frame, read text. Returns (plate_text, confidence).""" """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) plate_boxes = self.detect_plates(frame)
if not plate_boxes: if not plate_boxes:
return "", 0.0 return "", 0.0, None
x1, y1, x2, y2, plate_conf = plate_boxes[0] x1, y1, x2, y2, plate_conf = plate_boxes[0]
pad = 8 pad = 8
@@ -130,6 +131,6 @@ class PlateAnalyzer:
# Filter noise: plate must have at least 4 alphanumeric chars # Filter noise: plate must have at least 4 alphanumeric chars
alnum = "".join(c for c in text if c.isalnum()) alnum = "".join(c for c in text if c.isalnum())
if len(alnum) < 4: 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))
+16 -3
View File
@@ -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) @app.get("/event/{event_id}", response_class=HTMLResponse)
async def event_detail(request: Request, event_id: str): async def event_detail(request: Request, event_id: str):
ev = database.get_event(event_id) 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"]) ev["time_str"] = ts_to_str(ev["start_time"])
# List frames for this event
event_dir = os.path.join(EVENTS_DIR, event_id) event_dir = os.path.join(EVENTS_DIR, event_id)
frame_paths = sorted(glob.glob(os.path.join(event_dir, "frame_*.jpg"))) 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", { return templates.TemplateResponse("event_detail.html", {
"request": request, "request": request,
"ev": ev, "ev": ev,
"frames": frames, "frames": frames,
"has_clip": has_clip, "has_clip": has_clip,
"clip_size": clip_size,
"capture_duration": CAPTURE_DURATION,
"capture_total": CAPTURE_DURATION * CAPTURE_FPS,
"top_frames": TOP_FRAMES,
}) })
+61 -65
View File
@@ -3,119 +3,115 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CamWatch — Passage {{ ev.time_str }}</title> <title>CamWatch — {{ ev.time_str }}</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script> <script>tailwind.config = { darkMode: 'class' }</script>
<style> <style>
body { background: #0f172a; color: #e2e8f0; } body { background: #0f172a; color: #e2e8f0; }
.card { background: #1e293b; border: 1px solid #334155; } .card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
.plate { font-family: monospace; letter-spacing: 0.15em; } .plate { font-family: monospace; letter-spacing: 0.15em; }
.btn { background: #2563eb; color: #fff; border-radius: 6px; padding: 7px 16px; font-size: 0.85rem; cursor: pointer; text-decoration: none; display: inline-block; } .btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; border-radius: 6px; padding: 6px 14px; font-size: 0.85rem; text-decoration: none; display: inline-block; }
.btn:hover { background: #1d4ed8; }
.btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; }
.btn-ghost:hover { background: #334155; color: #e2e8f0; } .btn-ghost:hover { background: #334155; color: #e2e8f0; }
.frame-thumb { cursor: pointer; transition: transform 0.1s; } .frame-thumb { cursor: pointer; border: 2px solid #334155; border-radius: 6px; overflow: hidden; transition: border-color 0.15s; }
.frame-thumb:hover { transform: scale(1.03); border-color: #3b82f6 !important; } .frame-thumb:hover { border-color: #3b82f6; }
.frame-thumb.active { border-color: #3b82f6 !important; } .frame-thumb.active { border-color: #3b82f6; }
label { color: #64748b; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
</style> </style>
</head> </head>
<body class="min-h-screen"> <body class="min-h-screen">
<!-- Header --> <header class="sticky top-0 z-10 px-4 py-3 flex items-center gap-3" style="background:#0f172a;border-bottom:1px solid #1e293b;">
<header class="sticky top-0 z-10 px-4 py-3 flex items-center justify-between" style="background:#0f172a;border-bottom:1px solid #1e293b;"> <a href="/" class="btn-ghost">← Retour</a>
<div class="flex items-center gap-3"> <span class="text-xl">🚗</span>
<a href="/" class="btn btn-ghost text-sm px-3 py-1.5">← Retour</a> <span class="font-bold">{{ ev.time_str }}</span>
<span class="text-xl">🚗</span>
<span class="font-bold">Passage — {{ ev.time_str }}</span>
</div>
<span class="text-slate-500 text-sm hidden sm:inline">{{ ev.camera }}</span>
</header> </header>
<main class="max-w-5xl mx-auto px-3 py-5 space-y-5"> <main class="max-w-5xl mx-auto px-3 py-5 space-y-4">
<!-- Top row: info + main image --> <!-- Main image + info -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Info card --> <!-- Info -->
<div class="card rounded-xl p-4 space-y-3"> <div class="card p-4 space-y-4">
<h2 class="text-slate-400 text-xs uppercase font-semibold tracking-wide">Informations</h2>
<div> <div>
<div class="text-xs text-slate-500 mb-0.5">Date / Heure</div> <label>Plaque</label>
<div class="font-medium">{{ ev.time_str }}</div> <div class="mt-1">
{% if ev.plate %}
<span class="plate text-xl font-bold px-4 py-2 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">{{ ev.plate }}</span>
{% else %}
<span class="text-slate-500 italic">Non lue</span>
{% endif %}
</div>
</div> </div>
<div> <div>
<div class="text-xs text-slate-500 mb-0.5">Caméra</div> <label>Couleur véhicule</label>
<div class="font-medium">{{ ev.camera }}</div> <div class="flex items-center gap-2 mt-1">
</div> <div class="w-7 h-7 rounded-full border-2 border-slate-600" style="background:{{ ev.color_hex or '#808080' }};"></div>
<div>
<div class="text-xs text-slate-500 mb-1">Plaque détectée</div>
{% if ev.plate %}
<span class="plate text-lg font-bold px-4 py-1.5 rounded"
style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">
{{ ev.plate }}
</span>
{% else %}
<span class="text-slate-500 italic text-sm">Non lue</span>
{% endif %}
</div>
<div>
<div class="text-xs text-slate-500 mb-1">Couleur véhicule</div>
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full border border-slate-600"
style="background:{{ ev.color_hex or '#808080' }};"></div>
<span class="font-medium">{{ ev.color_name or '—' }}</span> <span class="font-medium">{{ ev.color_name or '—' }}</span>
<span class="text-slate-600 text-xs">{{ ev.color_hex or '' }}</span> <span class="text-slate-600 text-xs font-mono">{{ ev.color_hex or '' }}</span>
</div>
</div>
<!-- Debug section -->
<div class="pt-2 border-t border-slate-700">
<label>Debug</label>
<div class="mt-1 space-y-1 text-xs font-mono text-slate-400">
<div>id: {{ ev.id[:8] }}…</div>
<div>frames: {{ frames|length }} / {{ capture_total }}</div>
<div>clip: {% if ev.clip_path %}✓ {{ clip_size }}{% else %}—{% endif %}</div>
<div>snapshot: {% if ev.snapshot_path %}✓{% else %}—{% endif %}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Main image viewer --> <!-- Image viewer -->
<div class="md:col-span-2 card rounded-xl overflow-hidden"> <div class="md:col-span-2 card overflow-hidden">
<div class="relative bg-black" style="aspect-ratio:16/9;"> <div class="bg-black relative" style="aspect-ratio:16/9;">
<img id="main-img" <img id="main-img"
src="{% if ev.snapshot_path %}/{{ ev.snapshot_path }}{% endif %}" src="{% if ev.snapshot_path %}/{{ ev.snapshot_path }}{% endif %}"
alt="Meilleure frame"
class="w-full h-full object-contain"> class="w-full h-full object-contain">
</div> </div>
<div class="px-3 py-2 text-xs text-slate-500 text-center" id="img-label">Meilleure frame (score LPR)</div> <div class="px-3 py-2 text-xs text-slate-500 text-center" id="img-label">Meilleure frame ()</div>
</div> </div>
</div> </div>
<!-- Video clip --> <!-- Video clip -->
{% if has_clip %} {% if has_clip %}
<div class="card rounded-xl p-4"> <div class="card p-4">
<h2 class="text-slate-400 text-xs uppercase font-semibold tracking-wide mb-3">Clip vidéo ({{ ev.clip_path.split('/')[-1] }})</h2> <label class="block mb-3">Clip vidéo {{ capture_duration }}s</label>
<video controls class="w-full rounded-lg" style="max-height:400px;background:#000;" <video controls preload="metadata" class="w-full rounded-lg" style="max-height:380px;background:#000;">
preload="metadata">
<source src="/{{ ev.clip_path }}" type="video/mp4"> <source src="/{{ ev.clip_path }}" type="video/mp4">
Votre navigateur ne supporte pas la vidéo HTML5.
</video> </video>
</div> </div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Clip vidéo</label>
{% if ev.clip_path %}Fichier introuvable{% else %}Événement antérieur à la fonctionnalité clip{% endif %}
</div>
{% endif %} {% endif %}
<!-- Frames gallery --> <!-- Frames gallery -->
{% if frames %} {% if frames %}
<div class="card rounded-xl p-4"> <div class="card p-4">
<h2 class="text-slate-400 text-xs uppercase font-semibold tracking-wide mb-3"> <label class="block mb-3">Frames analysées — {{ frames|length }} meilleures (sur ~{{ capture_total }}), classées par score LPR</label>
Frames extraites ({{ frames|length }} meilleures sur {{ (15 * 5)|int }} capturées)
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2"> <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
{% for frame_path in frames %} {% for frame_path in frames %}
<div class="frame-thumb rounded overflow-hidden border border-slate-700 {% if loop.first %}active{% endif %}" <div class="frame-thumb {% if loop.first %}active{% endif %}"
onclick="selectFrame(this, '/{{ frame_path }}', 'Frame {{ loop.index }} / {{ frames|length }}')"> onclick="selectFrame(this, '/{{ frame_path }}', 'Frame #{{ loop.index }}{% if loop.first %} ★{% endif %}')">
<img src="/{{ frame_path }}" alt="Frame {{ loop.index }}" <img src="/{{ frame_path }}" class="w-full object-cover" style="aspect-ratio:16/9;" loading="lazy">
class="w-full object-cover" style="aspect-ratio:16/9;"> <div class="text-center text-xs text-slate-500 py-0.5 bg-slate-900">
<div class="text-center text-xs text-slate-500 py-0.5">
#{{ loop.index }}{% if loop.first %} ★{% endif %} #{{ loop.index }}{% if loop.first %} ★{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Frames</label>
Événement antérieur à la fonctionnalité frames (pas de dossier /data/events/{{ ev.id[:8] }}…)
</div>
{% endif %} {% endif %}
</main> </main>
+21 -50
View File
@@ -9,19 +9,17 @@
<style> <style>
body { background: #0f172a; color: #e2e8f0; } body { background: #0f172a; color: #e2e8f0; }
.card { background: #1e293b; border: 1px solid #334155; } .card { background: #1e293b; border: 1px solid #334155; }
.badge { display:inline-block; padding:2px 8px; border-radius:999px; font-size:0.7rem; font-weight:600; } .plate { font-family: monospace; letter-spacing: 0.12em; }
.plate { font-family: monospace; letter-spacing:0.1em; } input { background: #0f172a; border: 1px solid #475569; color: #e2e8f0; border-radius: 6px; padding: 6px 10px; }
input, select { background:#0f172a; border:1px solid #475569; color:#e2e8f0; border-radius:6px; padding:6px 10px; } input:focus { outline: none; border-color: #60a5fa; }
input:focus, select:focus { outline:none; border-color:#60a5fa; } .btn { background: #2563eb; color: #fff; border-radius: 6px; padding: 7px 16px; font-size: 0.85rem; cursor: pointer; }
.btn { background:#2563eb; color:#fff; border-radius:6px; padding:7px 16px; font-size:0.85rem; cursor:pointer; } .btn:hover { background: #1d4ed8; }
.btn:hover { background:#1d4ed8; } .btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; }
.btn-ghost { background:#1e293b; border:1px solid #475569; color:#94a3b8; } .btn-ghost:hover { background: #334155; color: #e2e8f0; }
.btn-ghost:hover { background:#334155; color:#e2e8f0; }
</style> </style>
</head> </head>
<body class="min-h-screen"> <body class="min-h-screen">
<!-- Header -->
<header class="sticky top-0 z-10 px-4 py-3 flex items-center justify-between" style="background:#0f172a;border-bottom:1px solid #1e293b;"> <header class="sticky top-0 z-10 px-4 py-3 flex items-center justify-between" style="background:#0f172a;border-bottom:1px solid #1e293b;">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xl">🚗</span> <span class="text-xl">🚗</span>
@@ -36,20 +34,13 @@
<!-- Filters --> <!-- Filters -->
<form method="get" class="flex flex-wrap gap-2 mb-5"> <form method="get" class="flex flex-wrap gap-2 mb-5">
<input type="text" name="plate" placeholder="Plaque…" value="{{ filter_plate }}" class="w-32"> <input type="text" name="plate" placeholder="Plaque…" value="{{ filter_plate }}" class="w-32">
<select name="camera">
<option value="">Toutes caméras</option>
{% for cam in cameras %}
<option value="{{ cam }}" {% if cam == filter_camera %}selected{% endif %}>{{ cam }}</option>
{% endfor %}
</select>
<input type="date" name="date" value="{{ filter_date }}"> <input type="date" name="date" value="{{ filter_date }}">
<button type="submit" class="btn">Filtrer</button> <button type="submit" class="btn">Filtrer</button>
{% if filter_plate or filter_camera or filter_date %} {% if filter_plate or filter_date %}
<a href="/" class="btn btn-ghost">✕ Reset</a> <a href="/" class="btn btn-ghost">✕ Reset</a>
{% endif %} {% endif %}
</form> </form>
<!-- Empty state -->
{% if not events %} {% if not events %}
<div class="text-center py-20 text-slate-500"> <div class="text-center py-20 text-slate-500">
<div class="text-5xl mb-4">📷</div> <div class="text-5xl mb-4">📷</div>
@@ -58,46 +49,32 @@
</div> </div>
{% endif %} {% endif %}
<!-- Events grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{% for ev in events %} {% for ev in events %}
<a href="/event/{{ ev.id }}" class="card rounded-xl overflow-hidden block hover:border-blue-500 transition-colors" style="text-decoration:none;color:inherit;"> <a href="/event/{{ ev.id }}" class="card rounded-xl overflow-hidden block transition-colors hover:border-blue-500" style="text-decoration:none;color:inherit;">
<!-- Snapshot -->
<div class="relative bg-black" style="aspect-ratio:16/9;"> <div class="relative bg-black" style="aspect-ratio:16/9;">
{% if ev.snapshot_path %} {% if ev.snapshot_path %}
<img src="/{{ ev.snapshot_path }}" alt="snapshot" <img src="/{{ ev.snapshot_path }}" alt="snapshot" class="w-full h-full object-cover"
class="w-full h-full object-cover" onerror="this.parentElement.innerHTML='<div class=\'w-full h-full flex items-center justify-center text-slate-600 text-4xl\'>📷</div>'">
onerror="this.parentElement.innerHTML='<div class=\'w-full h-full flex items-center justify-center text-slate-600 text-3xl\'>📷</div>'">
{% else %} {% else %}
<div class="w-full h-full flex items-center justify-center text-slate-600 text-3xl">📷</div> <div class="w-full h-full flex items-center justify-center text-slate-600 text-4xl">📷</div>
{% endif %}
{% if ev.clip_path %}
<span class="absolute top-2 right-2 text-xs px-1.5 py-0.5 rounded" style="background:rgba(0,0,0,0.7);color:#4ade80;">▶ clip</span>
{% endif %} {% endif %}
<!-- Camera badge -->
<span class="absolute top-2 left-2 badge" style="background:rgba(0,0,0,0.6);color:#94a3b8;">
{{ ev.camera }}
</span>
</div> </div>
<!-- Info -->
<div class="p-3 space-y-2"> <div class="p-3 space-y-2">
<!-- Time -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-slate-300 text-sm font-medium">{{ ev.time_str }}</span> <span class="text-slate-300 text-sm font-medium">{{ ev.time_str }}</span>
<!-- Color swatch -->
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="w-4 h-4 rounded-full border border-slate-600" <div class="w-4 h-4 rounded-full border border-slate-600" style="background:{{ ev.color_hex or '#808080' }};" title="{{ ev.color_name }}"></div>
style="background:{{ ev.color_hex or '#808080' }};"
title="{{ ev.color_hex }}"></div>
<span class="text-slate-400 text-xs">{{ ev.color_name or '—' }}</span> <span class="text-slate-400 text-xs">{{ ev.color_name or '—' }}</span>
</div> </div>
</div> </div>
<div>
<!-- Plate -->
<div class="flex items-center gap-2">
{% if ev.plate %} {% if ev.plate %}
<span class="plate text-sm font-bold px-3 py-1 rounded" <span class="plate text-sm font-bold px-3 py-1 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">{{ ev.plate }}</span>
style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">
{{ ev.plate }}
</span>
{% else %} {% else %}
<span class="text-slate-500 text-sm italic">Plaque non lue</span> <span class="text-slate-500 text-sm italic">Plaque non lue</span>
{% endif %} {% endif %}
@@ -107,26 +84,20 @@
{% endfor %} {% endfor %}
</div> </div>
<!-- Pagination -->
{% if total_pages > 1 %} {% if total_pages > 1 %}
<div class="flex items-center justify-center gap-2 mt-6"> <div class="flex items-center justify-center gap-2 mt-6">
{% if page > 1 %} {% if page > 1 %}
<a href="?page={{ page - 1 }}&plate={{ filter_plate }}&camera={{ filter_camera }}&date={{ filter_date }}" <a href="?page={{ page - 1 }}&plate={{ filter_plate }}&date={{ filter_date }}" class="btn btn-ghost text-sm">← Préc.</a>
class="btn btn-ghost text-sm">← Préc.</a>
{% endif %} {% endif %}
<span class="text-slate-400 text-sm">Page {{ page }} / {{ total_pages }}</span> <span class="text-slate-400 text-sm">Page {{ page }} / {{ total_pages }}</span>
{% if page < total_pages %} {% if page < total_pages %}
<a href="?page={{ page + 1 }}&plate={{ filter_plate }}&camera={{ filter_camera }}&date={{ filter_date }}" <a href="?page={{ page + 1 }}&plate={{ filter_plate }}&date={{ filter_date }}" class="btn btn-ghost text-sm">Suiv. →</a>
class="btn btn-ghost text-sm">Suiv. →</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</main> </main>
<!-- Auto-refresh every 60s --> <script>setTimeout(() => location.reload(), 60000);</script>
<script>
setTimeout(() => location.reload(), 60000);
</script>
</body> </body>
</html> </html>
+28 -4
View File
@@ -80,6 +80,29 @@ def _get_ai_state() -> dict | None:
return 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: def _frame_score(frame: np.ndarray, plates: list) -> float:
if plates: if plates:
x1, y1, x2, y2, conf = plates[0] x1, y1, x2, y2, conf = plates[0]
@@ -152,17 +175,18 @@ def _process_event():
return return
# Step 4: run LPR on best frame # Step 4: run LPR on best frame
plate, conf = ("", 0.0) plate, conf, plate_bbox = ("", 0.0, None)
if _analyzer: if _analyzer:
plate, conf = _analyzer.read_plate(best_frame) plate, conf, plate_bbox = _analyzer.read_plate(best_frame)
log.info(f"LPR: plate={plate!r} conf={conf:.2f}") log.info(f"LPR: plate={plate!r} conf={conf:.2f} bbox={plate_bbox}")
# Step 5: save thumbnail (best frame) # Step 5: save thumbnail (best frame)
snapshot_file = f"{event_id}.jpg" snapshot_file = f"{event_id}.jpg"
snapshot_path = os.path.join(SNAPSHOTS_DIR, snapshot_file) snapshot_path = os.path.join(SNAPSHOTS_DIR, snapshot_file)
cv2.imwrite(snapshot_path, best_frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) 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( insert_event(
event_id, CAMERA_NAME, int(time.time()), event_id, CAMERA_NAME, int(time.time()),
f"snapshots/{snapshot_file}", f"snapshots/{snapshot_file}",