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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ea4118ffcd
commit
c43652d187
+81
@@ -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"}
|
||||
|
||||
@@ -102,12 +102,20 @@
|
||||
style="background:#166534;color:#4ade80;border:1px solid #16a34a;">
|
||||
✓ Sauvegarder l'annotation
|
||||
</button>
|
||||
<button type="button" id="btn-test-lpr" onclick="testLPR()"
|
||||
class="px-4 py-2 rounded font-medium"
|
||||
style="background:#1e293b;color:#60a5fa;border:1px solid #2563eb;">
|
||||
🔍 Tester LPR
|
||||
</button>
|
||||
<span id="save-warning" class="text-yellow-400 text-sm hidden">⚠ Dessine d'abord le rectangle autour de la plaque</span>
|
||||
<span id="save-ok" class="text-green-400 text-sm hidden">✓ Rectangle défini — prêt à sauvegarder</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- LPR test results -->
|
||||
<div id="lpr-result" class="card p-4 hidden"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 = '<p class="text-yellow-400 text-sm">Aucune frame sélectionnée.</p>';
|
||||
resultEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ En cours…';
|
||||
resultEl.innerHTML = '<p class="text-slate-500 text-sm">Requête en cours…</p>';
|
||||
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 = `<p class="text-red-400 text-sm">Erreur: ${e.message}</p>`;
|
||||
} 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 = `<p class="text-red-400 text-sm">${data.error}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="space-y-4">';
|
||||
html += '<label class="block">Résultats LPR</label>';
|
||||
|
||||
// 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 += `<div>
|
||||
<div class="text-xs text-slate-500 mb-1 uppercase tracking-wide font-semibold">PlateRecognizer API</div>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class="font-mono text-lg font-bold px-4 py-1 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">${pr.plate || '—'}</span>
|
||||
<span class="text-sm font-mono" style="color:${col};">${pct}%</span>
|
||||
${pr.plate ? `<button type="button" onclick="usePlate('${pr.plate}')" class="btn-ghost text-xs py-1 px-2 text-blue-400">↑ Utiliser</button>` : ''}
|
||||
</div>
|
||||
${pr.raw && pr.raw.replace(/-/g,'') !== pr.plate.replace(/-/g,'') ? `<p class="text-slate-500 text-xs mt-1">Brut API: ${pr.raw}</p>` : ''}
|
||||
</div>`;
|
||||
} else if (data.has_pr) {
|
||||
html += `<div>
|
||||
<div class="text-xs text-slate-500 mb-1 uppercase tracking-wide font-semibold">PlateRecognizer API</div>
|
||||
<p class="text-slate-500 text-sm">Aucune plaque détectée</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 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 += `<div>
|
||||
<div class="text-xs text-slate-500 mb-1 uppercase tracking-wide font-semibold">OCR local — ${src}</div>
|
||||
<div class="flex items-center gap-3 flex-wrap mb-2">
|
||||
<span class="font-mono text-lg font-bold px-4 py-1 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">${loc.plate || '—'}</span>
|
||||
<span class="text-sm font-mono" style="color:${col};">${pct}%</span>
|
||||
${loc.plate ? `<button type="button" onclick="usePlate('${loc.plate}')" class="btn-ghost text-xs py-1 px-2 text-blue-400">↑ Utiliser</button>` : ''}
|
||||
</div>
|
||||
${loc.raw && loc.raw !== loc.plate ? `<p class="text-slate-500 text-xs mb-2">Brut OCR: "${loc.raw}"</p>` : ''}
|
||||
${loc.img_b64 ? `<img src="data:image/jpeg;base64,${loc.img_b64}" class="rounded border border-slate-600" style="image-rendering:pixelated;max-height:56px;" title="Image envoyée à l'OCR">` : ''}
|
||||
</div>`;
|
||||
} else if (data.local_error) {
|
||||
html += `<div>
|
||||
<div class="text-xs text-slate-500 mb-1 uppercase tracking-wide font-semibold">OCR local</div>
|
||||
<p class="text-red-400 text-sm">${data.local_error}</p>
|
||||
</div>`;
|
||||
} else if (document.getElementById('input-bbox').value) {
|
||||
html += `<div>
|
||||
<div class="text-xs text-slate-500 mb-1 uppercase tracking-wide font-semibold">OCR local</div>
|
||||
<p class="text-slate-500 text-sm">Aucun résultat</p>
|
||||
</div>`;
|
||||
} else {
|
||||
html += `<p class="text-slate-500 text-sm italic">Dessine un rectangle autour de la plaque pour tester l'OCR local.</p>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
resultEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Form validation ────────────────────────────────────────────────────
|
||||
document.getElementById('ann-form').addEventListener('submit', function(e) {
|
||||
if (!document.getElementById('input-bbox').value) {
|
||||
|
||||
Reference in New Issue
Block a user