Remove spaces, ".", and "-" from ALPR

This commit is contained in:
2026-04-28 17:49:37 +10:00
parent 234f2c68a2
commit cd5d6f1923
4 changed files with 251 additions and 12 deletions

View File

@@ -866,6 +866,69 @@ namespace ANSCENTER
return collapsed.substr(first, last - first + 1);
}
// Drop visual separators (hyphens, commas, every flavour of whitespace)
// from a plate string so the emitted className is a contiguous token.
// See header comment for full rationale. UTF-8-aware so it handles
// Japanese OCR output that may produce full-width or CJK variants.
std::string ANSALPR_OCR::StripPlateSeparators(const std::string& text) {
if (text.empty()) return text;
std::string stripped;
stripped.reserve(text.size());
size_t pos = 0;
while (pos < text.size()) {
const size_t before = pos;
uint32_t cp = ANSOCRUtility::NextUTF8Codepoint(text, pos);
if (cp == 0 || pos == before) break;
bool drop = false;
switch (cp) {
// --- Spaces (every flavour the recognizer can emit) ---
case 0x0020: // ASCII space
case 0x0009: // horizontal tab
case 0x00A0: // no-break space
case 0x1680: // ogham space mark
case 0x2000: // en quad
case 0x2001: // em quad
case 0x2002: // en space
case 0x2003: // em space
case 0x2004: // three-per-em space
case 0x2005: // four-per-em space
case 0x2006: // six-per-em space
case 0x2007: // figure space
case 0x2008: // punctuation space
case 0x2009: // thin space
case 0x200A: // hair space
case 0x202F: // narrow no-break space
case 0x205F: // medium mathematical space
case 0x3000: //   ideographic space (CJK)
// --- Periods / full stops ---
case 0x002E: // . ASCII full stop / period
case 0xFF0E: // fullwidth full stop
// --- Hyphens / dashes ---
case 0x002D: // - ASCII hyphen-minus
case 0x2010: // HYPHEN
case 0x2011: // NON-BREAKING HYPHEN
case 0x2012: // FIGURE DASH
case 0x2013: // EN DASH
case 0x2014: // — EM DASH
case 0x2015: // ― HORIZONTAL BAR
case 0x2212: // MINUS SIGN
case 0xFF0D: // fullwidth hyphen-minus
case 0x30FC: // ー katakana-hiragana prolonged sound mark
// (visually similar to a dash and the OCR
// sometimes emits it where a hyphen sits)
drop = true;
break;
default:
break;
}
if (!drop) {
stripped.append(text, before, pos - before);
}
}
return stripped;
}
std::string ANSALPR_OCR::RecoverKanaFromBottomHalf(
const cv::Mat& plateROI, int halfH) const
{
@@ -1338,11 +1401,26 @@ namespace ANSCENTER
// lets SetCountry(nonJapan) take effect on the very next
// frame without a restart.
const bool useRectification = (_country == Country::JAPAN);
// Decide once per frame whether the tracker-based correction
// layer should run. Used by both the OCR-skip optimization
// below and by Step 4's checkPlateByTrackId call further down.
// We auto-detect full-frame vs pipeline mode by watching for
// pixel-identical consecutive frames, exactly the same way
// ANSALPR_OD does it.
const bool useChecker = shouldUseALPRChecker(
cv::Size(frameWidth, frameHeight), cameraId);
struct PlateInfo {
size_t origIndex; // into lprOutput
std::vector<size_t> cropIndices; // into allCrops
cv::Mat plateROI; // full (unsplit) ROI, kept for colour + kana recovery
int halfH = 0; // row-split Y inside plateROI (0 = single row)
// Skip-when-locked: if non-empty, this plate's track is already
// locked in alprChecker and we re-used the locked text instead
// of running OCR. Step 4 emits this directly; cropIndices is
// empty for these entries. See ALPRChecker::tryReuseLockedText.
std::string presetClassName;
};
std::vector<cv::Mat> allCrops;
std::vector<PlateInfo> plateInfos;
@@ -1359,6 +1437,34 @@ namespace ANSCENTER
const int height = std::min(frameHeight - y1, box.height);
if (width <= 0 || height <= 0) continue;
// Skip-when-locked fast path. If alprChecker already has a
// locked text for this plate's trackId, re-use it directly and
// don't push any crops onto allCrops. Saves ~95 ms (AMD DML)
// or ~50 ms (Intel OpenVINO) per locked plate per frame. The
// re-verify period inside tryReuseLockedText lets one OCR run
// through every N frames so a wrong lock can self-correct.
//
// Gated on useChecker so pipeline mode (no tracker, no voting)
// always runs raw OCR exactly like before — the optimization
// only applies in the full-frame ALPR pipeline.
if (useChecker) {
std::string locked = alprChecker.tryReuseLockedText(
cameraId, lprOutput[i].trackId);
if (!locked.empty()) {
// Build a minimal plateROI for the colour-detection
// pass in Step 4 — we still want to run colour even
// on skipped plates because it's cached and very
// cheap once the cache is warm.
const cv::Rect safeBox(x1, y1, width, height);
PlateInfo info;
info.origIndex = i;
info.plateROI = frame(safeBox); // non-owning view
info.presetClassName = std::move(locked);
plateInfos.push_back(std::move(info));
continue;
}
}
// Pad the YOLO LP bbox by 5% on each side. Gives the
// rectifier some background for edge detection and helps
// when the detector cropped a character edge.
@@ -1416,28 +1522,50 @@ namespace ANSCENTER
plateInfos.push_back(std::move(info));
}
if (allCrops.empty()) {
// Nothing to emit: no detected plates AND no skipped-locked plates.
if (plateInfos.empty()) {
return {};
}
// Step 3: Single batched recognizer call for every crop.
// Step 3: Single batched recognizer call for every crop that
// wasn't short-circuited by the skip-when-locked fast path.
// ONNXOCRRecognizer groups crops by bucket width and issues
// one ORT Run per bucket — typically 12 GPU calls for an
// entire frame regardless of plate count.
auto ocrResults = _ocrEngine->RecognizeTextBatch(allCrops);
// entire frame regardless of plate count. When every plate
// in the frame is already locked, allCrops is empty and we
// skip the recognizer call entirely.
std::vector<std::pair<std::string, float>> ocrResults;
if (!allCrops.empty()) {
ocrResults = _ocrEngine->RecognizeTextBatch(allCrops);
}
// Step 4: Assemble per-plate output
// Step 4: Assemble per-plate output.
std::vector<Object> output;
output.reserve(plateInfos.size());
// Decide once per frame whether the tracker-based correction
// layer should run. We auto-detect full-frame vs pipeline mode
// by watching for pixel-identical consecutive frames, exactly
// the same way ANSALPR_OD does it.
const bool useChecker = shouldUseALPRChecker(
cv::Size(frameWidth, frameHeight), cameraId);
for (const auto& info : plateInfos) {
// Skip-when-locked fast path: this plate's track was already
// locked in alprChecker, no OCR was run, just emit the locked
// text directly. Bypasses CTC / artifact stripping / kana
// recovery / re-vote — all of which would no-op anyway since
// the locked text is already the canonical plate string.
if (!info.presetClassName.empty()) {
Object lprObject = lprOutput[info.origIndex];
lprObject.cameraId = cameraId;
lprObject.className = info.presetClassName;
// Colour detection still runs — it's cached per-(camera,
// plate-text) so subsequent frames are O(1) anyway.
std::string colour = DetectLPColourCached(
info.plateROI, cameraId, lprObject.className);
if (!colour.empty()) {
lprObject.extraInfo = "color:" + colour;
}
output.push_back(std::move(lprObject));
continue;
}
// Reassemble row-by-row so we can target the bottom row
// for kana recovery when the fast path silently dropped
// the hiragana on a Japanese 2-row plate.
@@ -1502,6 +1630,17 @@ namespace ANSCENTER
if (combinedText.empty()) continue;
// Final-pass post-processing: drop hyphens, commas, every
// flavour of whitespace so the emitted plate is a single
// contiguous token. Done BEFORE the voter so:
// 1. textHistory + lockedText store the canonical form
// (better vote convergence — "50H 304.07" and
// "50H304.07" no longer compete as separate strings).
// 2. Skip-when-locked emits stripped text automatically
// via ALPRChecker::tryReuseLockedText, no second strip.
combinedText = StripPlateSeparators(combinedText);
if (combinedText.empty()) continue;
Object lprObject = lprOutput[info.origIndex];
lprObject.cameraId = cameraId;
@@ -1759,6 +1898,14 @@ namespace ANSCENTER
if (combined.empty()) continue;
// Final-pass post-processing — same separator strip the
// full-frame path applies before the voter. Pipeline mode
// has no voter, so we strip just before emit. Keeps the
// className consistent with the full-frame output format
// regardless of which inference path the caller chose.
combined = StripPlateSeparators(combined);
if (combined.empty()) continue;
Object out = pm.lpObj;
out.className = combined; // raw OCR — no ALPRChecker
out.cameraId = cameraId;