Remove spaces, ".", and "-" from ALPR
This commit is contained in:
@@ -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 1–2 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;
|
||||
|
||||
Reference in New Issue
Block a user