diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a4c723e..f878953 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -145,7 +145,11 @@ "Bash(grep -nE \"^\\\\}$|warmupModel\\\\\\(|#if 0|#endif|ANSONNXCL_legacy_Init\" C:/Projects/CLionProjects/ANSCORE/modules/ANSODEngine/ANSONNXCL.cpp)", "Bash(git -C C:/Projects/CLionProjects/ANSCORE restore modules/ANSODEngine/ANSONNXCL.h modules/ANSODEngine/ANSONNXCL.cpp)", "Bash(git -C C:/Projects/CLionProjects/ANSCORE status --short modules/)", - "Bash(git -C C:/Projects/CLionProjects/ANSCORE diff --stat HEAD modules/ANSODEngine/ANSONNXCL.cpp modules/ANSODEngine/ANSONNXOBB.cpp modules/ANSODEngine/ANSONNXPOSE.cpp modules/ANSODEngine/ANSONNXSEG.cpp modules/ANSODEngine/ANSYOLO12OD.cpp engines/ONNXEngine/ONNXEngine.cpp engines/ONNXEngine/ONNXSAM3.cpp)" + "Bash(git -C C:/Projects/CLionProjects/ANSCORE diff --stat HEAD modules/ANSODEngine/ANSONNXCL.cpp modules/ANSODEngine/ANSONNXOBB.cpp modules/ANSODEngine/ANSONNXPOSE.cpp modules/ANSODEngine/ANSONNXSEG.cpp modules/ANSODEngine/ANSYOLO12OD.cpp engines/ONNXEngine/ONNXEngine.cpp engines/ONNXEngine/ONNXSAM3.cpp)", + "Bash(awk -F'[:\\(\\)]' '{gsub\\(/^ +| +$/,\"\",$2\\); split\\($2,a,\" \"\\); ms=a[1]; if \\(ms+0 >= 500\\) print $0}')", + "Bash(awk -F'[:\\(\\)]' '{split\\($2,a,\" \"\\); ms=a[1]; if \\(ms+0 >= 100\\) print $0}')", + "Bash(grep -cE \"^\\\\s*Plate: [^ ]+$\" \"C:/Users/nghia/Downloads/Log4.txt\")", + "Bash(grep -oE \"Plate: [^ ]+$\" \"C:/Users/nghia/Downloads/Log5.txt\")" ] } } diff --git a/modules/ANSLPR/ANSLPR_OCR.cpp b/modules/ANSLPR/ANSLPR_OCR.cpp index 5c0baf5..0853bfc 100644 --- a/modules/ANSLPR/ANSLPR_OCR.cpp +++ b/modules/ANSLPR/ANSLPR_OCR.cpp @@ -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(c))) { + analysedLP += c; + } + } + std::transform(analysedLP.begin(), analysedLP.end(), analysedLP.begin(), + [](unsigned char ch) { return static_cast(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(p))) { matches = false; break; } + } + else if (f == 'l') { + if (!std::isalpha(static_cast(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(_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 diff --git a/modules/ANSLPR/ANSLPR_OCR.h b/modules/ANSLPR/ANSLPR_OCR.h index 9169551..b856c5b 100644 --- a/modules/ANSLPR/ANSLPR_OCR.h +++ b/modules/ANSLPR/ANSLPR_OCR.h @@ -109,6 +109,41 @@ namespace ANSCENTER // --- Original model zip path (reused for ANSONNXOCR initialization) --- std::string _modelZipFilePath; + // ──────────────────────────────────────────────────────────────── + // Country-specific plate-format processing — Vietnam-only. + // + // When `_country == VIETNAM`, `AnalyseLicensePlateText` strips + // any remaining non-alphanumeric characters, uppercases the + // result, and validates it against `_plateFormats` (the same + // 11-pattern list ANSALPR_OD installs for Vietnam). Plates + // that don't match any configured format are dropped — the + // return value is "" and the caller skips the plate. + // + // Every other country (Japan, China, Indonesia, Australia, USA, + // …) returns the input unchanged so the upstream + // StripPlateSeparators output flows through verbatim, preserving + // kana / kanji / region-specific punctuation. + // + // Hooked in after StripPlateSeparators and before the ALPRChecker + // voter, so locked text + skip-when-locked output are emitted in + // the canonical Vietnam form just like ANSALPR_OD's path. + // ──────────────────────────────────────────────────────────────── + [[nodiscard]] std::string AnalyseLicensePlateText(const std::string& ocrText); + + // Format match helper — patterns: 'd' = any digit, 'l' = any + // letter, every other character is a literal that must match + // exactly. Returns true when `_plateFormats` is empty (i.e. + // no formats configured → no validation, accept all). + [[nodiscard]] bool MatchesPlateFormat(const std::string& plate) const; + + // Reset `_plateFormats` to the default list for the current + // `_country`. Called at the end of Initialize (after country.txt + // is read) and from SetCountry so a runtime country switch picks + // up the right format set on the very next frame. Currently + // populates Vietnam's 11 standard patterns; other countries leave + // the list empty (= no validation). + void ApplyCountryDefaultPlateFormats(); + // --- Colour detection helpers --- [[nodiscard]] std::string DetectLPColourDetector(const cv::Mat& lprROI, const std::string& cameraId); [[nodiscard]] std::string DetectLPColourCached(const cv::Mat& lprROI, const std::string& cameraId, const std::string& plateText); diff --git a/tests/ANSLPR-UnitTest/ANSLPR-UnitTest.cpp b/tests/ANSLPR-UnitTest/ANSLPR-UnitTest.cpp index 9f7d896..47e7454 100644 --- a/tests/ANSLPR-UnitTest/ANSLPR-UnitTest.cpp +++ b/tests/ANSLPR-UnitTest/ANSLPR-UnitTest.cpp @@ -3953,9 +3953,14 @@ int ALPR_OCR_VideoTest() { return -1; } - // Step 2: Set country (JAPAN = 5 — adjust to match the dataset if needed) - ANSALPR_SetCountry(&infHandle, 1); - std::cout << "Country set to JAPAN" << std::endl; + // Step 2: Set country (VIETNAM = 0 — adjust to match the dataset if needed). + // Country codes match country.txt parsing in ANSALPR_OCR::Initialize: + // 0 = VIETNAM, 1 = CHINA, 2 = AUSTRALIA, 3 = USA, 4 = INDONESIA, 5 = JAPAN. + // Setting VIETNAM here also activates ANSALPR_OCR's Vietnam-only + // post-processing (alnum filter + uppercase + plate-format validation + // against the 11 standard Vietnam patterns). + ANSALPR_SetCountry(&infHandle, 0); + std::cout << "Country set to VIETNAM" << std::endl; // Step 3: Load engine auto engineStart = std::chrono::high_resolution_clock::now();