Add ANSALPR country format

This commit is contained in:
2026-04-28 19:23:46 +10:00
parent cd5d6f1923
commit e1f37ec767
4 changed files with 206 additions and 4 deletions

View File

@@ -194,6 +194,12 @@ namespace ANSCENTER
else _country = Country::JAPAN; // Default for OCR mode
}
// Install the Vietnam-only plate-format defaults now that the
// country is known. Idempotent — clears `_plateFormats` first.
// SetCountry() also calls this so a runtime country switch picks
// up the right format set without an Initialize / restart.
ApplyCountryDefaultPlateFormats();
// Store the original model zip path — the OCR models (ansocrdec.onnx,
// ansocrcls.onnx, ansocrrec.onnx, dict_ch.txt) are bundled inside the
// same ALPR model zip, so we reuse it for ANSONNXOCR initialization.
@@ -929,6 +935,140 @@ namespace ANSCENTER
return stripped;
}
// ────────────────────────────────────────────────────────────────────
// Country-specific post-processing. See header for full contract.
// Mirrors the ANSALPR_OD pipeline so OD and OCR engines emit the
// same canonical form for Vietnam / Indonesia / Australia / USA,
// while leaving Japan / China text untouched (kana / kanji would
// be destroyed by an ASCII alnum filter, so we pass-through).
// ────────────────────────────────────────────────────────────────────
std::string ANSALPR_OCR::AnalyseLicensePlateText(const std::string& ocrText) {
if (ocrText.empty()) return ocrText;
// Country gate — Vietnam-only. Every other country (Japan, China,
// Indonesia, Australia, USA, …) returns the input unchanged so the
// upstream StripPlateSeparators output flows through verbatim and
// kana / kanji / region-specific punctuation is preserved.
if (_country != Country::VIETNAM) {
return ocrText;
}
try {
// Vietnam: keep only ASCII alphanumerics (StripPlateSeparators
// already removed the common separators; this is a defensive
// second pass that also catches any stray punctuation the OCR
// may have emitted). Uppercase so the format-match literals
// 'M', 'D', 'N', 'G', 'Q', 'T', 'C', 'V' work against the
// uppercase plate output.
std::string analysedLP;
analysedLP.reserve(ocrText.size());
for (char c : ocrText) {
if (std::isalnum(static_cast<unsigned char>(c))) {
analysedLP += c;
}
}
std::transform(analysedLP.begin(), analysedLP.end(), analysedLP.begin(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
// Format validation: when `_plateFormats` is configured (which
// it is by default for Vietnam via ApplyCountryDefaultPlateFormats),
// reject plates that don't match any pattern. Empty list = no
// validation (caller cleared it via SetPlateFormats), in which
// case everything is accepted just like ANSALPR_OD does.
if (!analysedLP.empty() && !_plateFormats.empty()
&& !MatchesPlateFormat(analysedLP)) {
ANS_DBG("ALPR_Format",
"REJECT plate='%s' (no _plateFormats match)",
analysedLP.c_str());
return "";
}
return analysedLP;
}
catch (const std::exception& e) {
this->_logger.LogFatal("ANSALPR_OCR::AnalyseLicensePlateText",
e.what(), __FILE__, __LINE__);
return "";
}
}
bool ANSALPR_OCR::MatchesPlateFormat(const std::string& plate) const {
if (_plateFormats.empty()) {
return true; // No formats configured → accept all.
}
for (const auto& format : _plateFormats) {
if (plate.size() != format.size()) continue;
bool matches = true;
for (size_t i = 0; i < format.size(); ++i) {
char f = format[i];
char p = plate[i];
if (f == 'd') {
if (!std::isdigit(static_cast<unsigned char>(p))) { matches = false; break; }
}
else if (f == 'l') {
if (!std::isalpha(static_cast<unsigned char>(p))) { matches = false; break; }
}
else {
// Literal character — exact match required.
if (p != f) { matches = false; break; }
}
}
if (matches) return true;
}
return false;
}
void ANSALPR_OCR::ApplyCountryDefaultPlateFormats() {
// Reset first so repeated SetCountry calls don't accumulate stale
// patterns from previous countries.
_plateFormats.clear();
switch (_country) {
case Country::VIETNAM: {
// Same 11 patterns ANSALPR_OD installs for Vietnam. Keeping
// these identical ensures the OD- and OCR-based pipelines
// accept exactly the same plate strings in production.
// ddlddddd — standard car (e.g. 29A12345)
// ddldddd — short variant
// ddldddddd — extended variant
// ddllddddd — two-letter series
// ddMDdddddd — military
// dddddNGdd — diplomatic NG
// dddddQTdd — diplomatic QT
// dddddCVdd — diplomatic CV
// dddddNNdd — diplomatic NN
// lldddd — special two-letter prefix
_plateFormats.push_back("ddlddddd");
_plateFormats.push_back("ddldddd");
_plateFormats.push_back("ddldddddd");
_plateFormats.push_back("ddllddddd");
_plateFormats.push_back("ddllddddd");
_plateFormats.push_back("ddMDdddddd");
_plateFormats.push_back("dddddNGdd");
_plateFormats.push_back("dddddQTdd");
_plateFormats.push_back("dddddCVdd");
_plateFormats.push_back("dddddNNdd");
_plateFormats.push_back("lldddd");
ANS_DBG("ALPR_Format",
"ApplyCountryDefaultPlateFormats: VIETNAM (%zu patterns)",
_plateFormats.size());
break;
}
case Country::CHINA:
case Country::AUSTRALIA:
case Country::USA:
case Country::INDONESIA:
case Country::JAPAN:
default:
// No default patterns configured for these countries. The
// caller can still install custom formats via SetPlateFormat
// / SetPlateFormats; otherwise format validation accepts all.
ANS_DBG("ALPR_Format",
"ApplyCountryDefaultPlateFormats: country=%d (no default patterns)",
static_cast<int>(_country));
break;
}
}
std::string ANSALPR_OCR::RecoverKanaFromBottomHalf(
const cv::Mat& plateROI, int halfH) const
{
@@ -1641,6 +1781,13 @@ namespace ANSCENTER
combinedText = StripPlateSeparators(combinedText);
if (combinedText.empty()) continue;
// Vietnam-only: alnum filter + uppercase + format validation
// against `_plateFormats`. No-op on every other country.
// Same canonical form ANSALPR_OD emits, so OD- and OCR-based
// pipelines can be A/B compared on the same input video.
combinedText = AnalyseLicensePlateText(combinedText);
if (combinedText.empty()) continue;
Object lprObject = lprOutput[info.origIndex];
lprObject.cameraId = cameraId;
@@ -1906,6 +2053,13 @@ namespace ANSCENTER
combined = StripPlateSeparators(combined);
if (combined.empty()) continue;
// Vietnam-only post-processing (alnum + uppercase + format
// validation). Same gate as the full-frame path so OD and
// OCR engines emit identical strings on Vietnam input, and
// non-Vietnam countries pass through unchanged.
combined = AnalyseLicensePlateText(combined);
if (combined.empty()) continue;
Object out = pm.lpObj;
out.className = combined; // raw OCR — no ALPRChecker
out.cameraId = cameraId;
@@ -2014,6 +2168,10 @@ namespace ANSCENTER
if (_ocrEngine) {
_ocrEngine->SetCountry(country);
}
// Re-install country-default plate formats so a runtime country
// switch picks up Vietnam's format list (or clears it when leaving
// Vietnam) without needing an Initialize / restart.
ApplyCountryDefaultPlateFormats();
// Log every SetCountry call so runtime country switches are
// visible and we can confirm the update landed on the right
// handle. The recovery + rectification gates read _country on