Remove spaces, ".", and "-" from ALPR
This commit is contained in:
@@ -450,6 +450,53 @@ namespace ANSCENTER {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip-when-locked query — see header for full contract.
|
||||||
|
//
|
||||||
|
// Returns the locked plate text if the caller may skip OCR this frame for
|
||||||
|
// (cameraId, trackId), or "" if the caller must run OCR. Increments the
|
||||||
|
// re-verify counter on skip; resets it when the counter hits reverifyEvery
|
||||||
|
// so the next frame falls through to a real OCR run.
|
||||||
|
std::string ALPRChecker::tryReuseLockedText(const std::string& cameraId,
|
||||||
|
int trackId,
|
||||||
|
int reverifyEvery) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
||||||
|
try {
|
||||||
|
auto camIt = trackedPlatesById.find(cameraId);
|
||||||
|
if (camIt == trackedPlatesById.end()) return "";
|
||||||
|
auto& plates = camIt->second;
|
||||||
|
auto it = plates.find(trackId);
|
||||||
|
if (it == plates.end()) return "";
|
||||||
|
|
||||||
|
auto& tp = it->second;
|
||||||
|
if (tp.lockedText.empty()) return ""; // not locked yet — caller must run OCR
|
||||||
|
|
||||||
|
// Locked. Decide whether to skip this frame's OCR or fall through
|
||||||
|
// for a periodic re-verify. reverifyEvery <= 0 means "always skip"
|
||||||
|
// (no re-verify), reverifyEvery == 1 means "always run OCR".
|
||||||
|
if (reverifyEvery > 0 && tp.framesSinceVerify >= reverifyEvery) {
|
||||||
|
tp.framesSinceVerify = 0;
|
||||||
|
ANS_DBG("ALPR_TrackId",
|
||||||
|
"cam=%s tid=%d REVERIFY (every=%d) — letting OCR through",
|
||||||
|
cameraId.c_str(), trackId, reverifyEvery);
|
||||||
|
return ""; // signal: caller should run OCR this frame
|
||||||
|
}
|
||||||
|
|
||||||
|
++tp.framesSinceVerify;
|
||||||
|
// Keep the entry alive — checkPlateByTrackId is what normally
|
||||||
|
// resets framesSinceLastSeen, but on skipped frames it's never
|
||||||
|
// called, and the prune pass would eventually delete this lock.
|
||||||
|
tp.framesSinceLastSeen = 0;
|
||||||
|
ANS_DBG("ALPR_TrackId",
|
||||||
|
"cam=%s tid=%d SKIP-LOCKED text='%s' reverifyIn=%d",
|
||||||
|
cameraId.c_str(), trackId, tp.lockedText.c_str(),
|
||||||
|
reverifyEvery > 0 ? (reverifyEvery - tp.framesSinceVerify) : -1);
|
||||||
|
return tp.lockedText;
|
||||||
|
}
|
||||||
|
catch (const std::exception&) {
|
||||||
|
return ""; // any failure → fall back to running OCR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) {
|
static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ namespace ANSCENTER
|
|||||||
std::string lockedText;
|
std::string lockedText;
|
||||||
int lockCount = 0;
|
int lockCount = 0;
|
||||||
int framesSinceLastSeen = 0;
|
int framesSinceLastSeen = 0;
|
||||||
|
// Frames since the last full OCR was used to verify the lock. When
|
||||||
|
// skip-when-locked is active in ANSALPR_OCR::RunInference, this
|
||||||
|
// counter increments on every frame whose OCR was skipped; once it
|
||||||
|
// reaches the re-verify period, the next frame falls through to a
|
||||||
|
// real OCR run (resetting the counter) so a stale or wrong lock
|
||||||
|
// can self-correct via majority-vote re-lock. Zero when not in
|
||||||
|
// use (e.g. raw OCR pass-through, pipeline mode).
|
||||||
|
int framesSinceVerify = 0;
|
||||||
};
|
};
|
||||||
// cameraId → (trackId → tracked plate)
|
// cameraId → (trackId → tracked plate)
|
||||||
std::unordered_map<std::string, std::unordered_map<int, TrackedPlateById>> trackedPlatesById;
|
std::unordered_map<std::string, std::unordered_map<int, TrackedPlateById>> trackedPlatesById;
|
||||||
@@ -61,6 +69,26 @@ namespace ANSCENTER
|
|||||||
[[nodiscard]] std::string checkPlate(const std::string& cameraId, const std::string& detectedPlate, const cv::Rect& plateBox);
|
[[nodiscard]] std::string checkPlate(const std::string& cameraId, const std::string& detectedPlate, const cv::Rect& plateBox);
|
||||||
// Hybrid API: trackId as primary identity, Levenshtein fallback for lost tracks
|
// Hybrid API: trackId as primary identity, Levenshtein fallback for lost tracks
|
||||||
[[nodiscard]] std::string checkPlateByTrackId(const std::string& cameraId, const std::string& detectedPlate, int trackId);
|
[[nodiscard]] std::string checkPlateByTrackId(const std::string& cameraId, const std::string& detectedPlate, int trackId);
|
||||||
|
|
||||||
|
// Skip-when-locked query: returns the locked plate text for (cameraId, trackId)
|
||||||
|
// if the caller may safely SKIP running OCR this frame, or an empty string if
|
||||||
|
// the caller must run OCR and feed the result back through checkPlateByTrackId.
|
||||||
|
//
|
||||||
|
// Behaviour:
|
||||||
|
// * Track not found, or found but not locked yet → returns "".
|
||||||
|
// * Locked, and < `reverifyEvery` frames since the last full OCR for this
|
||||||
|
// track → returns the locked text and increments the per-track
|
||||||
|
// re-verify counter. Caller skips the recognizer entirely.
|
||||||
|
// * Locked, and ≥ `reverifyEvery` frames since the last full OCR →
|
||||||
|
// returns "" and resets the counter, so the caller runs OCR this frame
|
||||||
|
// and the regular voting/re-lock logic in checkPlateByTrackId can
|
||||||
|
// self-correct a wrong lock or notice a vehicle swap on the same trackId.
|
||||||
|
//
|
||||||
|
// Side-effect: also resets framesSinceLastSeen so the periodic prune pass
|
||||||
|
// in checkPlateByTrackId does not retire a track just because we skipped
|
||||||
|
// OCR on it. Without this the locked plate would silently disappear from
|
||||||
|
// the registry after a few hundred skipped frames.
|
||||||
|
[[nodiscard]] std::string tryReuseLockedText(const std::string& cameraId, int trackId, int reverifyEvery = 30);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ANSLPR_API ANSALPR {
|
class ANSLPR_API ANSALPR {
|
||||||
|
|||||||
@@ -866,6 +866,69 @@ namespace ANSCENTER
|
|||||||
return collapsed.substr(first, last - first + 1);
|
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(
|
std::string ANSALPR_OCR::RecoverKanaFromBottomHalf(
|
||||||
const cv::Mat& plateROI, int halfH) const
|
const cv::Mat& plateROI, int halfH) const
|
||||||
{
|
{
|
||||||
@@ -1338,11 +1401,26 @@ namespace ANSCENTER
|
|||||||
// lets SetCountry(nonJapan) take effect on the very next
|
// lets SetCountry(nonJapan) take effect on the very next
|
||||||
// frame without a restart.
|
// frame without a restart.
|
||||||
const bool useRectification = (_country == Country::JAPAN);
|
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 {
|
struct PlateInfo {
|
||||||
size_t origIndex; // into lprOutput
|
size_t origIndex; // into lprOutput
|
||||||
std::vector<size_t> cropIndices; // into allCrops
|
std::vector<size_t> cropIndices; // into allCrops
|
||||||
cv::Mat plateROI; // full (unsplit) ROI, kept for colour + kana recovery
|
cv::Mat plateROI; // full (unsplit) ROI, kept for colour + kana recovery
|
||||||
int halfH = 0; // row-split Y inside plateROI (0 = single row)
|
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<cv::Mat> allCrops;
|
||||||
std::vector<PlateInfo> plateInfos;
|
std::vector<PlateInfo> plateInfos;
|
||||||
@@ -1359,6 +1437,34 @@ namespace ANSCENTER
|
|||||||
const int height = std::min(frameHeight - y1, box.height);
|
const int height = std::min(frameHeight - y1, box.height);
|
||||||
if (width <= 0 || height <= 0) continue;
|
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
|
// Pad the YOLO LP bbox by 5% on each side. Gives the
|
||||||
// rectifier some background for edge detection and helps
|
// rectifier some background for edge detection and helps
|
||||||
// when the detector cropped a character edge.
|
// when the detector cropped a character edge.
|
||||||
@@ -1416,28 +1522,50 @@ namespace ANSCENTER
|
|||||||
plateInfos.push_back(std::move(info));
|
plateInfos.push_back(std::move(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allCrops.empty()) {
|
// Nothing to emit: no detected plates AND no skipped-locked plates.
|
||||||
|
if (plateInfos.empty()) {
|
||||||
return {};
|
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
|
// ONNXOCRRecognizer groups crops by bucket width and issues
|
||||||
// one ORT Run per bucket — typically 1–2 GPU calls for an
|
// one ORT Run per bucket — typically 1–2 GPU calls for an
|
||||||
// entire frame regardless of plate count.
|
// entire frame regardless of plate count. When every plate
|
||||||
auto ocrResults = _ocrEngine->RecognizeTextBatch(allCrops);
|
// 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;
|
std::vector<Object> output;
|
||||||
output.reserve(plateInfos.size());
|
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) {
|
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
|
// Reassemble row-by-row so we can target the bottom row
|
||||||
// for kana recovery when the fast path silently dropped
|
// for kana recovery when the fast path silently dropped
|
||||||
// the hiragana on a Japanese 2-row plate.
|
// the hiragana on a Japanese 2-row plate.
|
||||||
@@ -1502,6 +1630,17 @@ namespace ANSCENTER
|
|||||||
|
|
||||||
if (combinedText.empty()) continue;
|
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];
|
Object lprObject = lprOutput[info.origIndex];
|
||||||
lprObject.cameraId = cameraId;
|
lprObject.cameraId = cameraId;
|
||||||
|
|
||||||
@@ -1759,6 +1898,14 @@ namespace ANSCENTER
|
|||||||
|
|
||||||
if (combined.empty()) continue;
|
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;
|
Object out = pm.lpObj;
|
||||||
out.className = combined; // raw OCR — no ALPRChecker
|
out.className = combined; // raw OCR — no ALPRChecker
|
||||||
out.cameraId = cameraId;
|
out.cameraId = cameraId;
|
||||||
|
|||||||
@@ -191,6 +191,23 @@ namespace ANSCENTER
|
|||||||
// characters are collapsed and leading/trailing spaces trimmed.
|
// characters are collapsed and leading/trailing spaces trimmed.
|
||||||
[[nodiscard]] static std::string StripPlateArtifacts(const std::string& text);
|
[[nodiscard]] static std::string StripPlateArtifacts(const std::string& text);
|
||||||
|
|
||||||
|
// Final-pass post-processing: strip the visual separators that some
|
||||||
|
// countries put between plate sections — spaces, periods, hyphens —
|
||||||
|
// so the emitted plate string is a contiguous alphanumeric token.
|
||||||
|
// Examples:
|
||||||
|
// "84-F1 273.36" → "84F127336"
|
||||||
|
// "54-0L 7564" → "540L7564"
|
||||||
|
// "59-N2 363.25" → "59N236325"
|
||||||
|
// "東京 500あ12-34" → "東京500あ1234"
|
||||||
|
// Handles ASCII space/period/hyphen-minus plus the common Unicode
|
||||||
|
// variants (NBSP, ideographic space, em/en dashes, full-width
|
||||||
|
// period and hyphen, JP prolonged-sound mark) so JP-OCR output
|
||||||
|
// is normalised the same way as Latin output.
|
||||||
|
// Applied BEFORE the ALPRChecker voter so voting + locking both
|
||||||
|
// operate on the canonical stripped form, and the skip-when-locked
|
||||||
|
// path inherits stripped output without a second strip site.
|
||||||
|
[[nodiscard]] static std::string StripPlateSeparators(const std::string& text);
|
||||||
|
|
||||||
// Run recognizer-only on a tight crop of the left portion of the
|
// Run recognizer-only on a tight crop of the left portion of the
|
||||||
// bottom half, trying three vertical offsets to absorb row-split
|
// bottom half, trying three vertical offsets to absorb row-split
|
||||||
// inaccuracies. Returns the first non-empty result that contains
|
// inaccuracies. Returns the first non-empty result that contains
|
||||||
|
|||||||
Reference in New Issue
Block a user