From c43652d18776ac432393c42fbae6b4874f52d15c Mon Sep 17 00:00:00 2001 From: perco Date: Wed, 3 Jun 2026 15:20:14 +0200 Subject: [PATCH] Add LPR test button in annotation UI New POST /event/{id}/test-lpr endpoint runs PlateRecognizer on the full frame and local OCR on the drawn bbox region (with perspective correction when possible). Annotation UI shows results inline with a one-click "Utiliser" button to copy the best plate into the input field. Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 81 ++++++++++++++++++++++++++ app/templates/annotate.html | 111 ++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/app/main.py b/app/main.py index 0f46e00..7099280 100644 --- a/app/main.py +++ b/app/main.py @@ -316,6 +316,87 @@ async def whitelist_remove(plate: str, back: str = Form("")): return RedirectResponse(back or "/whitelist", status_code=303) +@app.post("/event/{event_id}/test-lpr") +async def test_lpr(event_id: str, frame_path: str = Form(""), bbox: str = Form("")): + import json, base64, cv2, numpy as np + + frame_abs = os.path.join("/data", frame_path) if frame_path else None + if not frame_abs or not os.path.exists(frame_abs): + raise HTTPException(404, "Frame introuvable") + + def _run(): + frame = cv2.imread(frame_abs) + if frame is None: + return {"error": "Impossible de lire l'image"} + + result: dict = {"has_pr": bool(PLATERECOGNIZER_KEY)} + + if PLATERECOGNIZER_KEY: + from lpr import call_platerecognizer + from watcher import _normalize_plate + p, c = call_platerecognizer(frame, PLATERECOGNIZER_KEY) + if p: + result["platerecognizer"] = { + "plate": _normalize_plate(p) or p, + "raw": p, + "conf": round(c, 3), + } + + analyzer = watcher._analyzer + if bbox and analyzer: + try: + box = json.loads(bbox) + h, w = frame.shape[:2] + cx2, cy2 = box["cx"] * w, box["cy"] * h + bw2, bh2 = box["w"] * w, box["h"] * h + x1 = max(0, int(cx2 - bw2 / 2)) + y1 = max(0, int(cy2 - bh2 / 2)) + x2 = min(w, int(cx2 + bw2 / 2)) + y2 = min(h, int(cy2 + bh2 / 2)) + + source = "raw_crop" + ocr_img = None + quad = analyzer._find_plate_quad(frame, x1, y1, x2, y2) + if quad is not None: + corrected = analyzer._perspective_correct(frame, quad) + if corrected is not None and corrected.size > 0: + ocr_img = analyzer._enhance_crop(corrected) + source = "perspective_corrected" + + if ocr_img is None: + crop = frame[y1:y2, x1:x2] + ocr_img = analyzer._enhance_crop(crop) if crop.size > 0 else None + + if ocr_img is not None: + text, conf = analyzer._ocr_paddle(ocr_img) + if not text: + text, conf = analyzer._ocr_tesseract(ocr_img) + + oh, ow = ocr_img.shape[:2] + if ow > 0 and ow < 300: + scale = 300 / ow + ocr_display = cv2.resize(ocr_img, (300, int(oh * scale)), interpolation=cv2.INTER_CUBIC) + else: + ocr_display = ocr_img + _, buf = cv2.imencode(".jpg", ocr_display, [cv2.IMWRITE_JPEG_QUALITY, 90]) + + from watcher import _normalize_plate + result["local"] = { + "plate": _normalize_plate(text) or text, + "raw": text, + "conf": round(conf, 3), + "img_b64": base64.b64encode(buf).decode(), + "source": source, + } + except Exception as e: + result["local_error"] = str(e) + + return result + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _run) + + @app.get("/health") async def health(): return {"status": "ok"} diff --git a/app/templates/annotate.html b/app/templates/annotate.html index b308850..723cb77 100644 --- a/app/templates/annotate.html +++ b/app/templates/annotate.html @@ -102,12 +102,20 @@ style="background:#166534;color:#4ade80;border:1px solid #16a34a;"> βœ“ Sauvegarder l'annotation + + + + @@ -442,6 +450,109 @@ redraw(); }; + // ── LPR test ─────────────────────────────────────────────────────────── + async function testLPR() { + const framePathEl = document.getElementById('input-frame'); + const bboxEl = document.getElementById('input-bbox'); + const resultEl = document.getElementById('lpr-result'); + const btn = document.getElementById('btn-test-lpr'); + + if (!framePathEl.value) { + resultEl.innerHTML = '

Aucune frame sΓ©lectionnΓ©e.

'; + resultEl.classList.remove('hidden'); + return; + } + + btn.disabled = true; + btn.textContent = '⏳ En cours…'; + resultEl.innerHTML = '

RequΓͺte en cours…

'; + resultEl.classList.remove('hidden'); + + const fd = new FormData(); + fd.append('frame_path', framePathEl.value); + fd.append('bbox', bboxEl.value); + + try { + const resp = await fetch('/event/{{ ev.id }}/test-lpr', { method: 'POST', body: fd }); + const data = await resp.json(); + renderLPRResult(data); + } catch(e) { + resultEl.innerHTML = `

Erreur: ${e.message}

`; + } finally { + btn.disabled = false; + btn.textContent = 'πŸ” Tester LPR'; + } + } + + function usePlate(plate) { + document.getElementById('input-plate').value = plate; + } + + function renderLPRResult(data) { + const resultEl = document.getElementById('lpr-result'); + if (data.error) { + resultEl.innerHTML = `

${data.error}

`; + return; + } + + let html = '
'; + html += ''; + + // PlateRecognizer + if (data.platerecognizer) { + const pr = data.platerecognizer; + const pct = Math.round(pr.conf * 100); + const col = pr.conf >= 0.7 ? '#4ade80' : pr.conf >= 0.4 ? '#fbbf24' : '#f87171'; + html += `
+
PlateRecognizer API
+
+ ${pr.plate || 'β€”'} + ${pct}% + ${pr.plate ? `` : ''} +
+ ${pr.raw && pr.raw.replace(/-/g,'') !== pr.plate.replace(/-/g,'') ? `

Brut API: ${pr.raw}

` : ''} +
`; + } else if (data.has_pr) { + html += `
+
PlateRecognizer API
+

Aucune plaque dΓ©tectΓ©e

+
`; + } + + // Local OCR + if (data.local) { + const loc = data.local; + const pct = Math.round(loc.conf * 100); + const col = loc.conf >= 0.7 ? '#4ade80' : loc.conf >= 0.4 ? '#fbbf24' : '#f87171'; + const src = loc.source === 'perspective_corrected' ? 'correction perspective βœ“' : 'recadrage direct'; + html += `
+
OCR local β€” ${src}
+
+ ${loc.plate || 'β€”'} + ${pct}% + ${loc.plate ? `` : ''} +
+ ${loc.raw && loc.raw !== loc.plate ? `

Brut OCR: "${loc.raw}"

` : ''} + ${loc.img_b64 ? `` : ''} +
`; + } else if (data.local_error) { + html += `
+
OCR local
+

${data.local_error}

+
`; + } else if (document.getElementById('input-bbox').value) { + html += `
+
OCR local
+

Aucun rΓ©sultat

+
`; + } else { + html += `

Dessine un rectangle autour de la plaque pour tester l'OCR local.

`; + } + + html += '
'; + resultEl.innerHTML = html; + } + // ── Form validation ──────────────────────────────────────────────────── document.getElementById('ann-form').addEventListener('submit', function(e) { if (!document.getElementById('input-bbox').value) {