From f7cef5015a9c933a4712792b74aafa4da7c389e3 Mon Sep 17 00:00:00 2001 From: Tuan Nghia Nguyen Date: Sun, 5 Apr 2026 11:55:37 +1000 Subject: [PATCH] Fix ALPR pipeline. Ready for production --- .claude/settings.local.json | 3 +- core/ANSLicensingSystem/ANSLicense.h | 2 +- modules/ANSLPR/ANSLPR.cpp | 157 ++++++++++++++++++ modules/ANSLPR/ANSLPR.h | 32 +++- modules/ANSLPR/ANSLPR_OD.cpp | 105 +++++++++++- modules/ANSLPR/ANSLPR_OD.h | 20 +++ modules/ANSLPR/dllmain.cpp | 14 ++ modules/ANSMOT/ANSByteTrack.cpp | 2 +- modules/ANSMOT/ANSByteTrackEigen.cpp | 2 +- modules/ANSMOT/ANSByteTrackNCNN.cpp | 2 +- .../ANSMOT/ByteTrack/include/BYTETracker.h | 4 +- .../ByteTrackEigen/include/EigenBYTETracker.h | 4 +- .../ByteTrackNCNN/include/NCNNBYTETracker.h | 4 +- modules/ANSODEngine/ANSODEngine.cpp | 2 +- 14 files changed, 331 insertions(+), 22 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 35748c3..9607b20 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -76,7 +76,8 @@ "Bash(python -c \":*)", "Bash(find /c/Projects/CLionProjects/ANSCORE -type d -name *ANSODEngine*)", "Bash(powershell -Command \"\\(Get-Content ''C:\\\\Users\\\\nghia\\\\Downloads\\\\ANSLEGION41.log''\\).Count\")", - "Bash(powershell -Command \"\\(Get-Content ''C:\\\\Users\\\\nghia\\\\Downloads\\\\ANSLEGION42.log''\\).Count\")" + "Bash(powershell -Command \"\\(Get-Content ''C:\\\\Users\\\\nghia\\\\Downloads\\\\ANSLEGION42.log''\\).Count\")", + "Bash(grep -rn \"ApplyTracking\\\\|_trackerEnabled\" /c/Projects/CLionProjects/ANSCORE/modules/ANSODEngine/*.cpp)" ] } } diff --git a/core/ANSLicensingSystem/ANSLicense.h b/core/ANSLicensingSystem/ANSLicense.h index b55fbf1..7497409 100644 --- a/core/ANSLicensingSystem/ANSLicense.h +++ b/core/ANSLicensingSystem/ANSLicense.h @@ -8,7 +8,7 @@ // Set to 0 for production builds to eliminate all debug output overhead. // ============================================================================ #ifndef ANSCORE_DEBUGVIEW -#define ANSCORE_DEBUGVIEW 1 // 1 = enabled (debug), 0 = disabled (production) +#define ANSCORE_DEBUGVIEW 0 // 1 = enabled (debug), 0 = disabled (production) #endif // ANS_DBG: Debug logging macro for DebugView (OutputDebugStringA on Windows). diff --git a/modules/ANSLPR/ANSLPR.cpp b/modules/ANSLPR/ANSLPR.cpp index d3c8db9..50fa3ba 100644 --- a/modules/ANSLPR/ANSLPR.cpp +++ b/modules/ANSLPR/ANSLPR.cpp @@ -286,6 +286,163 @@ namespace ANSCENTER { return detectedPlate; } } + + // Hybrid trackId-based plate stabilization: + // Primary: O(1) hash lookup by trackId + // Fallback: Levenshtein search for lost tracks (track ID changed after occlusion) + std::string ALPRChecker::checkPlateByTrackId(const std::string& cameraId, + const std::string& detectedPlate, + int trackId) { + std::lock_guard lock(_mutex); + try { + if (detectedPlate.empty()) return detectedPlate; + + auto& plates = trackedPlatesById[cameraId]; + + // Age all plates for this camera + for (auto& [id, p] : plates) { + p.framesSinceLastSeen++; + } + + // Periodic pruning: remove stale entries + static thread_local int pruneCounterById = 0; + pruneCounterById++; + if (pruneCounterById >= 30 && plates.size() > 20) { + pruneCounterById = 0; + int staleThreshold = maxFrames * 3; + for (auto it = plates.begin(); it != plates.end(); ) { + if (it->second.framesSinceLastSeen > staleThreshold) { + it = plates.erase(it); + } else { + ++it; + } + } + } + + // --- Primary: direct trackId lookup (O(1)) --- + auto it = plates.find(trackId); + if (it != plates.end()) { + auto& tp = it->second; + tp.framesSinceLastSeen = 0; + + // Store RAW text (not corrected — avoids feedback loop) + tp.textHistory.push_back(detectedPlate); + if (static_cast(tp.textHistory.size()) > maxFrames) { + tp.textHistory.pop_front(); + } + + // Majority vote + std::string voted = majorityVote(tp.textHistory); + + // Lock logic + if (tp.lockedText.empty()) { + int matchCount = 0; + int lookback = std::min(static_cast(tp.textHistory.size()), maxFrames); + for (int i = static_cast(tp.textHistory.size()) - 1; + i >= static_cast(tp.textHistory.size()) - lookback; i--) { + if (tp.textHistory[i] == voted) matchCount++; + } + if (matchCount >= minVotesToStabilize) { + tp.lockedText = voted; + tp.lockCount = 1; + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d LOCKED '%s' votes=%d", + cameraId.c_str(), trackId, voted.c_str(), matchCount); + } + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d hit: raw='%s' voted='%s' hist=%d lock=%s", + cameraId.c_str(), trackId, detectedPlate.c_str(), voted.c_str(), + static_cast(tp.textHistory.size()), + tp.lockedText.empty() ? "(none)" : tp.lockedText.c_str()); + return voted; + } else { + int dist = levenshteinDistance(detectedPlate, tp.lockedText); + if (dist == 0) { + tp.lockCount++; + return tp.lockedText; + } else { + int newDist = levenshteinDistance(voted, tp.lockedText); + if (newDist > 1) { + int newMatchCount = 0; + int recent = std::min(static_cast(tp.textHistory.size()), + minVotesToStabilize * 2); + for (int i = static_cast(tp.textHistory.size()) - 1; + i >= static_cast(tp.textHistory.size()) - recent; i--) { + if (tp.textHistory[i] == voted) newMatchCount++; + } + if (newMatchCount >= minVotesToStabilize) { + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d RE-LOCK '%s'->'%s'", + cameraId.c_str(), trackId, tp.lockedText.c_str(), voted.c_str()); + tp.lockedText = voted; + tp.lockCount = 1; + return tp.lockedText; + } + } + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d noise: raw='%s' lock='%s' dist=%d", + cameraId.c_str(), trackId, detectedPlate.c_str(), tp.lockedText.c_str(), dist); + return tp.lockedText; + } + } + } + + // --- Fallback: Levenshtein search for lost tracks (track ID changed after occlusion) --- + for (auto& [id, p] : plates) { + if (!p.lockedText.empty()) { + int dist = levenshteinDistance(detectedPlate, p.lockedText); + if (dist <= 1) { + // Found a match — migrate history to new trackId + TrackedPlateById migrated = std::move(p); + migrated.trackId = trackId; + migrated.framesSinceLastSeen = 0; + migrated.textHistory.push_back(detectedPlate); + if (static_cast(migrated.textHistory.size()) > maxFrames) { + migrated.textHistory.pop_front(); + } + std::string result = migrated.lockedText; + plates.erase(id); + plates[trackId] = std::move(migrated); + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d MIGRATED from tid=%d lock='%s'", + cameraId.c_str(), trackId, id, result.c_str()); + return result; + } + } + // Check last 3 history entries if not locked + if (p.lockedText.empty() && !p.textHistory.empty()) { + int checkCount = std::min(static_cast(p.textHistory.size()), 3); + for (int j = static_cast(p.textHistory.size()) - 1; + j >= static_cast(p.textHistory.size()) - checkCount; j--) { + int dist = levenshteinDistance(detectedPlate, p.textHistory[j]); + if (dist <= 1) { + TrackedPlateById migrated = std::move(p); + migrated.trackId = trackId; + migrated.framesSinceLastSeen = 0; + migrated.textHistory.push_back(detectedPlate); + if (static_cast(migrated.textHistory.size()) > maxFrames) { + migrated.textHistory.pop_front(); + } + std::string voted = majorityVote(migrated.textHistory); + plates.erase(id); + plates[trackId] = std::move(migrated); + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d MIGRATED(unlocked) from tid=%d voted='%s'", + cameraId.c_str(), trackId, id, voted.c_str()); + return voted; + } + } + } + } + + // --- No match at all: create new entry --- + TrackedPlateById newPlate; + newPlate.trackId = trackId; + newPlate.textHistory.push_back(detectedPlate); + plates[trackId] = std::move(newPlate); + ANS_DBG("ALPR_TrackId", "cam=%s tid=%d NEW entry raw='%s'", + cameraId.c_str(), trackId, detectedPlate.c_str()); + return detectedPlate; + } + catch (const std::exception& e) { + return detectedPlate; + } + } + // static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) { try { diff --git a/modules/ANSLPR/ANSLPR.h b/modules/ANSLPR/ANSLPR.h index d462cdc..c647702 100644 --- a/modules/ANSLPR/ANSLPR.h +++ b/modules/ANSLPR/ANSLPR.h @@ -39,6 +39,18 @@ namespace ANSCENTER [[nodiscard]] int levenshteinDistance(const std::string& s1, const std::string& s2); [[nodiscard]] float computeIoU(const cv::Rect& a, const cv::Rect& b); [[nodiscard]] std::string majorityVote(const std::deque& history); + + // --- TrackId-based plate tracking (hybrid: trackId primary, Levenshtein fallback) --- + struct TrackedPlateById { + int trackId = 0; + std::deque textHistory; + std::string lockedText; + int lockCount = 0; + int framesSinceLastSeen = 0; + }; + // cameraId → (trackId → tracked plate) + std::unordered_map> trackedPlatesById; + public: void Init(int framesToStore = MAX_ALPR_FRAME); ALPRChecker(int framesToStore = MAX_ALPR_FRAME) : maxFrames(framesToStore) {} @@ -46,6 +58,8 @@ namespace ANSCENTER [[nodiscard]] std::string checkPlate(const std::string& cameraId, const std::string& detectedPlate); // Enhanced API with bounding box for spatial plate tracking [[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 + [[nodiscard]] std::string checkPlateByTrackId(const std::string& cameraId, const std::string& detectedPlate, int trackId); }; class ANSLPR_API ANSALPR { @@ -72,6 +86,12 @@ namespace ANSCENTER cv::Rect _detectedArea;// Area where license plate are detected Country _country; std::recursive_mutex _mutex; + + // ALPRChecker toggle (Layer 2 + Layer 3): + // true = enabled for full-frame, auto-disabled for pipeline/crop + // false = force disabled everywhere (raw OCR pass-through) + bool _enableALPRChecker{ true }; + public: [[nodiscard]] virtual bool Initialize(const std::string& licenseKey, const std::string& modelZipFilePath, const std::string& modelZipPassword, double detectorThreshold, double ocrThreshold, double colourTheshold=0); [[nodiscard]] virtual bool LoadEngine(); @@ -92,6 +112,13 @@ namespace ANSCENTER /// (LP detection, OCR, color classification, validation, serialization). /// Also propagates the flag to sub-detectors (_lpDetector, _ocrDetector, etc.). virtual void ActivateDebugger(bool debugFlag) { _debugFlag = debugFlag; } + + /// Enable/disable ALPRChecker (Layer 2 text stabilization + Layer 3 dedup): + /// true = enabled for full-frame, auto-disabled for pipeline/crop + /// false = force disabled everywhere (raw OCR pass-through) + void SetALPRCheckerEnabled(bool enable) { _enableALPRChecker = enable; } + bool IsALPRCheckerEnabled() const { return _enableALPRChecker; } + [[nodiscard]] virtual bool Destroy() = 0; [[nodiscard]] static std::vector GetBoundingBoxes(const std::string& strBBoxes); [[nodiscard]] static std::string PolygonToString(const std::vector& polygon); @@ -137,11 +164,14 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV(ANSCENTER::ANSALPR extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_CPP(ANSCENTER::ANSALPR** Handle, cv::Mat** cvImage, const char* cameraId, int getJpegString, int jpegImageSize,std::string& detectionResult, std::string& imageStr); extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV(ANSCENTER::ANSALPR** Handle, cv::Mat** cvImage, const char* cameraId, int maxImageSize,const char* strBboxes, LStrHandle detectionResult); -// Get/Set format +// Get/Set format extern "C" ANSLPR_API int ANSALPR_SetFormat(ANSCENTER::ANSALPR** Handle, const char* format); extern "C" ANSLPR_API int ANSALPR_SetFormats(ANSCENTER::ANSALPR** Handle, const char* formats);// comma separated formats extern "C" ANSLPR_API int ANSALPR_GetFormats(ANSCENTER::ANSALPR** Handle, LStrHandle formats);// comma separated formats +// ALPRChecker: 1 = enabled (full-frame auto-detected), 0 = disabled (raw OCR) +extern "C" ANSLPR_API int ANSALPR_SetALPRCheckerEnabled(ANSCENTER::ANSALPR** Handle, int enable); + // Unicode conversion utilities for LabVIEW wrapper classes extern "C" ANSLPR_API int ANSLPR_ConvertUTF8ToUTF16LE(const char* utf8Str, LStrHandle result, int includeBOM = 1); extern "C" ANSLPR_API int ANSLPR_ConvertUTF16LEToUTF8(const unsigned char* utf16leBytes, int byteLen, LStrHandle result); diff --git a/modules/ANSLPR/ANSLPR_OD.cpp b/modules/ANSLPR/ANSLPR_OD.cpp index d450368..7ac2aa5 100644 --- a/modules/ANSLPR/ANSLPR_OD.cpp +++ b/modules/ANSLPR/ANSLPR_OD.cpp @@ -862,6 +862,39 @@ namespace ANSCENTER { return false; } } + + bool ANSALPR_OD::shouldUseALPRChecker(const cv::Size& imageSize, const std::string& cameraId) { + // Force disabled → never use + if (!_enableALPRChecker) return false; + + // Small images are always pipeline crops — skip auto-detection + if (imageSize.width < ImageSizeTracker::MIN_FULLFRAME_WIDTH) return false; + + // Enabled: auto-detect pipeline vs full-frame by exact image size consistency. + // Full-frame: same resolution every frame (e.g., 3840x2160 always). + // Pipeline crops: vary by a few pixels (e.g., 496x453, 497x455) — exact match fails. + auto& tracker = _imageSizeTrackers[cameraId]; + bool wasFullFrame = tracker.detectedFullFrame; + if (imageSize == tracker.lastSize) { + tracker.consistentCount++; + if (tracker.consistentCount >= ImageSizeTracker::CONFIRM_THRESHOLD) { + tracker.detectedFullFrame = true; + } + } else { + tracker.lastSize = imageSize; + tracker.consistentCount = 1; + tracker.detectedFullFrame = false; + } + // Log state transitions + if (tracker.detectedFullFrame != wasFullFrame) { + ANS_DBG("ALPR_Checker", "cam=%s mode auto-detected: %s (img=%dx%d consistent=%d)", + cameraId.c_str(), + tracker.detectedFullFrame ? "FULL-FRAME (Layer2+3 ON)" : "PIPELINE (Layer2+3 OFF)", + imageSize.width, imageSize.height, tracker.consistentCount); + } + return tracker.detectedFullFrame; + } + std::vector ANSALPR_OD::RunInferenceSingleFrame(const cv::Mat& input, const std::string& cameraId) { // No coarse _mutex here — sub-components (detectors, alprChecker) have their own locks. // LabVIEW semaphore controls concurrency at the caller level. @@ -931,6 +964,13 @@ namespace ANSCENTER { // Run license plate detection cv::Mat activeFrame = frame(detectedArea); std::vector lprOutput = _lpDetector->RunInference(activeFrame, cameraId); + for (size_t _di = 0; _di < lprOutput.size(); ++_di) { + ANS_DBG("ALPR_Track", "cam=%s det[%zu] tid=%d box=(%d,%d,%d,%d) conf=%.2f", + cameraId.c_str(), _di, lprOutput[_di].trackId, + lprOutput[_di].box.x, lprOutput[_di].box.y, + lprOutput[_di].box.width, lprOutput[_di].box.height, + lprOutput[_di].confidence); + } if (lprOutput.empty()) { #ifdef FNS_DEBUG @@ -972,7 +1012,11 @@ namespace ANSCENTER { } lprObject.cameraId = cameraId; - lprObject.className = alprChecker.checkPlate(cameraId, ocrText, lprObject.box); + if (shouldUseALPRChecker(cv::Size(frameWidth, frameHeight), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, ocrText, lprObject.trackId); + } else { + lprObject.className = ocrText; + } if (lprObject.className.empty()) { continue; @@ -994,7 +1038,9 @@ namespace ANSCENTER { // Deduplicate: if two trackIds claim the same plate text, keep the one // with the higher accumulated score to prevent plate flickering - ensureUniquePlateText(output, cameraId); + if (shouldUseALPRChecker(cv::Size(frameWidth, frameHeight), cameraId)) { + ensureUniquePlateText(output, cameraId); + } return output; @@ -1473,7 +1519,11 @@ namespace ANSCENTER { auto tValidate = dbg ? Clock::now() : Clock::time_point{}; lprObject.cameraId = cameraId; lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows); - lprObject.className = alprChecker.checkPlate(cameraId, ocrText, lprObject.box); + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, ocrText, lprObject.trackId); + } else { + lprObject.className = ocrText; + } if (dbg) { totalValidateMs += std::chrono::duration(Clock::now() - tValidate).count(); } if (lprObject.className.empty()) { @@ -1610,6 +1660,13 @@ namespace ANSCENTER { cv::Mat croppedObject = frame(objectPos); std::vector lprOutput = _lpDetector->RunInference(croppedObject, cameraId); + for (size_t _di = 0; _di < lprOutput.size(); ++_di) { + ANS_DBG("ALPR_Track", "cam=%s bbox det[%zu] tid=%d box=(%d,%d,%d,%d) conf=%.2f", + cameraId.c_str(), _di, lprOutput[_di].trackId, + lprOutput[_di].box.x, lprOutput[_di].box.y, + lprOutput[_di].box.width, lprOutput[_di].box.height, + lprOutput[_di].confidence); + } for (auto& lprObject : lprOutput) { const cv::Rect& box = lprObject.box; @@ -1652,7 +1709,11 @@ namespace ANSCENTER { continue; } - lprObject.className = alprChecker.checkPlate(cameraId, ocrText, lprObject.box); + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, ocrText, lprObject.trackId); + } else { + lprObject.className = ocrText; + } if (lprObject.className.empty()) { continue; @@ -1670,6 +1731,13 @@ namespace ANSCENTER { else { // No bounding boxes - run on full frame std::vector lprOutput = _lpDetector->RunInference(frame, cameraId); + for (size_t _di = 0; _di < lprOutput.size(); ++_di) { + ANS_DBG("ALPR_Track", "cam=%s full det[%zu] tid=%d box=(%d,%d,%d,%d) conf=%.2f", + cameraId.c_str(), _di, lprOutput[_di].trackId, + lprOutput[_di].box.x, lprOutput[_di].box.y, + lprOutput[_di].box.width, lprOutput[_di].box.height, + lprOutput[_di].confidence); + } detectedObjects.reserve(lprOutput.size()); for (auto& lprObject : lprOutput) { @@ -1704,7 +1772,11 @@ namespace ANSCENTER { std::string rawText = DetectLicensePlateString(alignedLPR, cameraId); - lprObject.className = alprChecker.checkPlate(cameraId, rawText, lprObject.box); + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, rawText, lprObject.trackId); + } else { + lprObject.className = rawText; + } if (lprObject.className.empty()) { continue; @@ -1723,7 +1795,9 @@ namespace ANSCENTER { // Note: in Bbox mode, internal LP trackIds overlap across crops, so // dedup uses plate bounding box position (via Object::box) to distinguish. // The ensureUniquePlateText method handles this by plate text grouping. - ensureUniquePlateText(detectedObjects, cameraId); + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + ensureUniquePlateText(detectedObjects, cameraId); + } lprResult = VectorDetectionToJsonString(detectedObjects); return true; @@ -2252,6 +2326,13 @@ namespace ANSCENTER { // Run license plate detection (should be thread-safe internally) cv::Mat activeFrame = frame(detectedArea); std::vector lprOutput = _lpDetector->RunInference(activeFrame, cameraId); + for (size_t _di = 0; _di < lprOutput.size(); ++_di) { + ANS_DBG("ALPR_Track", "cam=%s batch det[%zu] tid=%d box=(%d,%d,%d,%d) conf=%.2f", + cameraId.c_str(), _di, lprOutput[_di].trackId, + lprOutput[_di].box.x, lprOutput[_di].box.y, + lprOutput[_di].box.width, lprOutput[_di].box.height, + lprOutput[_di].confidence); + } if (lprOutput.empty()) { return {}; @@ -2308,8 +2389,12 @@ namespace ANSCENTER { Object lprObject = lprOutput[origIdx]; lprObject.cameraId = cameraId; - // Stabilize OCR text through ALPRChecker (spatial tracking + majority voting) - lprObject.className = alprChecker.checkPlate(cameraId, ocrText, lprObject.box); + // Stabilize OCR text through ALPRChecker (hybrid trackId + Levenshtein fallback) + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, ocrText, lprObject.trackId); + } else { + lprObject.className = ocrText; + } if (lprObject.className.empty()) { continue; @@ -2327,7 +2412,9 @@ namespace ANSCENTER { // Deduplicate: if two trackIds claim the same plate text, keep the one // with the higher accumulated score to prevent plate flickering - ensureUniquePlateText(output, cameraId); + if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) { + ensureUniquePlateText(output, cameraId); + } return output; } diff --git a/modules/ANSLPR/ANSLPR_OD.h b/modules/ANSLPR/ANSLPR_OD.h index d4db07f..a436ee0 100644 --- a/modules/ANSLPR/ANSLPR_OD.h +++ b/modules/ANSLPR/ANSLPR_OD.h @@ -136,6 +136,24 @@ namespace ANSCENTER "43B1", "68L1", "70G1", "36M1", "81N1", "90K1", "17B1", "64E1", "99D1", "60B2", "74L1", "60C1", "68M1", "63B7", "34B1", "69M1", "24B1", "15M1", "83Y1", "48C1", "95H1", "79X1", "17B6", "36E1", "38K1", "25N1", "25U1", "61B1", "36C1", "36B3", "38F1", "99G1", "69N1", "97D1", "92T1", "92B1", "88B1", "97G1", "14U1", "63A1", "26N1", "19D1", "93C1", "73B1", "84B1", "81K1", "18L1", "64D1", "35M1", "61N1", "83P1", "15S1", "82B1", "92U1", "43D1", "22L1", "63B5", "64G1", "27N1", "14X1", "62C1", "81D1", "38G1", "19F1", "34K1", "49P1", "89H1", "14T1", "19M1", "78D1", "76A1", "66K1", "66C1", "71C1", "37K1", "19G1", "15F1", "85C1", "49B1", "21B1", "89F1", "23M1", "66L1", "90B5", "93M1", "14P1", "77N1", "36B8", "86B1", "12U1", "63B3", "21L1", "36G5", "65G1", "82E1", "61H1", "65H1", "84A1", "23F1", "95C1", "99K1", "49G1", "92D1", "36K3", "92N1", "82X1", "83M1", "11N1", "14K1", "19H1", "93H1", "60A1", "79A1", "20D1", "90D1", "81C1", "66P1", "36K1", "92V1", "18B1", "37P1", "22Y1", "23H1", "26D1", "66G1", "78F1", "49C1", "26H1", "38P1", "47T1", "74H1", "63P1", "47D1", "15D1", "23D1", "68E1", "20B1", "49F1", "43K1", "65K1", "27Z1", "92S1", "79H1", "21E1", "35Y1", "14S1", "75E1", "24Y1", "12T1", "27P1", "77B1", "88H1", "60B3", "23P1", "61F1", "99H1", "23K1", "59A3", "26C1", "81B1", "74E1", "66B1", "22S1", "92P1", "93B1", "69B1", "81P1", "12H1", "62K1", "35A1", "77C1", "27V1", "68N1", "12D1", "64K1", "41A1", "12Z1", "76C1", "38B1", "78G1", "74K1", "69H1", "94A1", "61K1", "86B7", "82G1", "14N1", "82M1", "76E1", "18E1", "61C1", "15N1", "90A1", "77F1", "34D1", "47B1", "62S1", "43E1", "81M1", "92X1", "75B1", "34F1", "70H1", "62B1", "26B1", "60B4", "61A1", "12B1", "90T1", "92E1", "34C1", "47G1", "97B1", "25S1", "70E1", "93Y1", "47S1", "37F1", "28N1", "11K1", "38E1", "78M1", "74C1", "12S1", "75S1", "37A1", "28D1", "65L1", "22B1", "99B1", "74G1", "79K1", "76K1", "76H1", "23B1", "15R1", "36B1", "74D1", "62L1", "37E1", "78E1", "89K1", "26M1", "25F1", "48H1", "79D1", "43H1", "76F1", "36L1", "43L1", "21K1", "88L1", "27S1", "92K1", "77D1", "19N1", "66H1", "36H5", "62N1", "18G1", "75D1", "37L1", "68K1", "28C1", "26E1", "35N1", "85H1", "62D1", "27U1", "19E1", "99E1", "14Y1", "49L1", "66M1", "73F1", "70K1", "36F5", "97H1", "93E1", "68P1", "43F1", "48G1", "75K1", "62U1", "86B9", "65F1", "27L1", "70L1", "63B8", "78L1", "11Z1", "68C1", "18D1", "15L1", "99C1", "49E1", "84E1", "69E1", "38A1", "48D1", "68S1", "81E1", "84K1", "63B6", "24T1", "95A1", "86B4", "34M1", "84L1", "24V1", "14M1", "36H1", "15B1", "69F1", "47E1", "38H1", "88D1", "28E1", "60C2", "63B9", "75Y1", "21D1", "35H1", "68F1", "86B5", "15H1", "36B5", "83X1", "17B7", "12V1", "86B8", "95E1", "63B2", "74F1", "86C1", "48K1", "89M1", "85D1", "71C4", "34E1", "97C1", "88E1", "81F1", "60B5", "84M1", "92H1", "28L1", "34H1", "38X1", "82L1", "61E1", "82F1", "62P1", "93F1", "65B1", "93L1", "95B1", "15P1", "77G1", "28M1", "35B1", "68G1", "36C2", "68D1", "69K1", "14L1", "36M3", "24X1", "24Z1", "86A1", "88C1", "15E1", "77E1", "83E1", "47L1", "25T1", "89C1", "71C3", "49D1", "36L6", "48F1", "36B6", "34P1", "84D1", "15C1", "38M1", "85F1", "77K1", "86B3", "74B1", "78H1", "89G1", "64A2", "15K1", "85B1", "49K1", "21H1", "73C1", "47U1", "65E1", "18C1", "69D1", "63B1", "95G1", "19L1", "20G1", "76D1", "29A1", "68T1", "75L1", "12L1", "89L1", "37C1", "27B1", "19C1", "11H1", "81X1", "70B1", "11V1", "43G1", "22A1", "83C1", "75C1", "79C1", "22F1", "92F1", "81G1", "81T1", "28H1", "66N1", "71B1", "18H1", "76P1", "26F1", "81U1", "34N1", "64F1", "76N1", "24S1", "26P1", "63B4", "35T1", "36N1", "47F1", "81L1", "61G1", "77M1", "34G1", "26G1", "97F1", "62H1", "28F1", "62T1", "93G1", "73D1", "65A1", "47P1", "74P1", "82N1", "20E1", "36D1", "60B1", "49M1", "37H1", "37M1", "38D1", "84F1", "88F1", "36B2", "65C1", "92M1", "86B6", "75H1", "38L1", "20C1", "97E1", "85E1", "38N1", "26K1", "89B1", "99F1", "28B1", "34L1", "86B2", "66F1", "77L1", "27Y1", "68H1", "37D1", "92L1", "82K1", "99A1", "69L1", "76M1", "90B4", "48B1", "95D1", "20H1", "64H1", "79Z1", "92G1", "23G1", "21G1", "37G1", "35K1", "81H1", "83Z1", "76T1", "36F1", "36B4", "14B9", "47K1", "20K1", "62M1", "84H1", "62F1", "74A1", "18A1", "73H1", "37N1", "79N1", "61D1", "11P1", "15G1", "47N1", "19K1", "71C2", "81S1", "11M1", "60B7", "60B8", "62G1", "71A1", "24P1", "69A1", "38C1", "49N1", "21C1", "84G1", "37B1", "72A1", "88K1", "88G1", "83V1", "78C1", "73K1", "78K1", "73E189D1", "67A1", "27X1", "62A1", "18K1", "70F1", "36K5", "19B1", "49H1", "66S1", "12P1" }; ALPRChecker alprChecker; + // --- Full-frame vs pipeline auto-detection --- + // Tri-state: -1 = auto-detect (default), 0 = explicitly disabled, 1 = explicitly enabled + // _enableALPRChecker is inherited from ANSALPR base class + + struct ImageSizeTracker { + cv::Size lastSize{0, 0}; + int consistentCount = 0; + bool detectedFullFrame = false; + static constexpr int CONFIRM_THRESHOLD = 5; + // Full-frame images must be exactly the same size every frame. + // Pipeline crops vary by a few pixels, so exact match filters them out. + // Additionally, full-frame is typically >1000px wide; crops are smaller. + static constexpr int MIN_FULLFRAME_WIDTH = 1000; + }; + std::unordered_map _imageSizeTrackers; + + [[nodiscard]] bool shouldUseALPRChecker(const cv::Size& imageSize, const std::string& cameraId); + // Plate identity persistence: accumulated confidence per spatial plate location. // Prevents the same plate string from appearing on multiple vehicles. // Uses bounding box center position (not trackId) as the identity key, @@ -204,6 +222,8 @@ namespace ANSCENTER if (_ocrDetector) _ocrDetector->ActivateDebugger(debugFlag); if (_lpColourDetector) _lpColourDetector->ActivateDebugger(debugFlag); } + + // SetALPRCheckerEnabled() and IsALPRCheckerEnabled() inherited from ANSALPR base class }; } #endif \ No newline at end of file diff --git a/modules/ANSLPR/dllmain.cpp b/modules/ANSLPR/dllmain.cpp index c5374d8..2274aac 100644 --- a/modules/ANSLPR/dllmain.cpp +++ b/modules/ANSLPR/dllmain.cpp @@ -743,6 +743,20 @@ extern "C" ANSLPR_API int ANSALPR_SetFormats(ANSCENTER::ANSALPR** Handle, co } +// ALPRChecker: 1 = enabled (full-frame auto-detected), 0 = disabled (raw OCR) +extern "C" ANSLPR_API int ANSALPR_SetALPRCheckerEnabled(ANSCENTER::ANSALPR** Handle, int enable) { + if (!Handle || !*Handle) return -1; + try { + (*Handle)->SetALPRCheckerEnabled(enable != 0); + ANS_DBG("ALPR_Checker", "SetALPRCheckerEnabled=%d (%s)", + enable, enable ? "ON" : "OFF"); + return 1; + } + catch (...) { + return 0; + } +} + extern "C" ANSLPR_API int ANSALPR_GetFormats(ANSCENTER::ANSALPR** Handle, LStrHandle Lstrformats)// semi separated formats { if (!Handle || !*Handle) return -1; diff --git a/modules/ANSMOT/ANSByteTrack.cpp b/modules/ANSMOT/ANSByteTrack.cpp index 5511734..53ba151 100644 --- a/modules/ANSMOT/ANSByteTrack.cpp +++ b/modules/ANSMOT/ANSByteTrack.cpp @@ -43,7 +43,7 @@ namespace ANSCENTER { ANSByteTrack::ANSByteTrack() { _licenseValid = false; CheckLicense(); - tracker.update_parameters(30, 30, 0.5, 0.6, 0.8); + tracker.update_parameters(10, 30, 0.5, 0.6, 0.8); } ANSByteTrack::~ANSByteTrack() { diff --git a/modules/ANSMOT/ANSByteTrackEigen.cpp b/modules/ANSMOT/ANSByteTrackEigen.cpp index 0737631..df46a0c 100644 --- a/modules/ANSMOT/ANSByteTrackEigen.cpp +++ b/modules/ANSMOT/ANSByteTrackEigen.cpp @@ -50,7 +50,7 @@ namespace ANSCENTER { _licenseValid = false; CheckLicense(); - int frame_rate = 30; + int frame_rate = 10; int track_buffer = 30; float track_thresh = 0.25; float track_highthres = 0.25; diff --git a/modules/ANSMOT/ANSByteTrackNCNN.cpp b/modules/ANSMOT/ANSByteTrackNCNN.cpp index 4711afc..c1651ea 100644 --- a/modules/ANSMOT/ANSByteTrackNCNN.cpp +++ b/modules/ANSMOT/ANSByteTrackNCNN.cpp @@ -44,7 +44,7 @@ namespace ANSCENTER{ ANSByteTrackNCNN::ANSByteTrackNCNN() { _licenseValid = false; CheckLicense(); - tracker.update_parameters(30, 30, 0.5, 0.6, 0.8); + tracker.update_parameters(10, 30, 0.5, 0.6, 0.8); } ANSByteTrackNCNN::~ANSByteTrackNCNN() { diff --git a/modules/ANSMOT/ByteTrack/include/BYTETracker.h b/modules/ANSMOT/ByteTrack/include/BYTETracker.h index 6bc7662..d376330 100644 --- a/modules/ANSMOT/ByteTrack/include/BYTETracker.h +++ b/modules/ANSMOT/ByteTrack/include/BYTETracker.h @@ -16,14 +16,14 @@ namespace ByteTrack public: using STrackPtr = std::shared_ptr; - BYTETracker(const int& frame_rate = 30, + BYTETracker(const int& frame_rate = 10, const int& track_buffer = 30, const float& track_thresh = 0.5, const float& high_thresh = 0.6, const float& match_thresh = 0.8); ~BYTETracker(); std::vector update(const std::vector& objects); - void update_parameters(int frameRate = 30, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); + void update_parameters(int frameRate = 10, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); float getEstimatedFps() const; private: diff --git a/modules/ANSMOT/ByteTrackEigen/include/EigenBYTETracker.h b/modules/ANSMOT/ByteTrackEigen/include/EigenBYTETracker.h index bd772d8..a6200da 100644 --- a/modules/ANSMOT/ByteTrackEigen/include/EigenBYTETracker.h +++ b/modules/ANSMOT/ByteTrackEigen/include/EigenBYTETracker.h @@ -34,8 +34,8 @@ namespace ByteTrackEigen { * @param match_thresh Threshold for matching detections to tracks. * @param frame_rate Frame rate of the video being processed. */ - BYTETracker(float track_thresh = 0.25, int track_buffer = 30, float match_thresh = 0.8, int frame_rate = 30); - void update_parameters(int frameRate = 30, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); + BYTETracker(float track_thresh = 0.25, int track_buffer = 30, float match_thresh = 0.8, int frame_rate = 10); + void update_parameters(int frameRate = 10, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); float getEstimatedFps() const; std::vector update(const Eigen::MatrixXf& output_results, const std::vector obj_ids); diff --git a/modules/ANSMOT/ByteTrackNCNN/include/NCNNBYTETracker.h b/modules/ANSMOT/ByteTrackNCNN/include/NCNNBYTETracker.h index 54f8ebd..5126c36 100644 --- a/modules/ANSMOT/ByteTrackNCNN/include/NCNNBYTETracker.h +++ b/modules/ANSMOT/ByteTrackNCNN/include/NCNNBYTETracker.h @@ -6,12 +6,12 @@ namespace ByteTrackNCNN { class BYTETracker { public: - BYTETracker(int frame_rate = 30, int track_buffer = 30); + BYTETracker(int frame_rate = 10, int track_buffer = 30); ~BYTETracker(); std::vector update(const std::vector& objects); //cv::Scalar get_color(int idx); - void update_parameters(int frameRate = 30, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); + void update_parameters(int frameRate = 10, int trackBuffer = 30, double trackThreshold = 0.5, double highThreshold = 0.6, double matchThresold = 0.8, bool autoFrameRate = false); float getEstimatedFps() const; private: std::vector joint_stracks(std::vector& tlista, std::vector& tlistb); diff --git a/modules/ANSODEngine/ANSODEngine.cpp b/modules/ANSODEngine/ANSODEngine.cpp index bfe984a..27b28f1 100644 --- a/modules/ANSODEngine/ANSODEngine.cpp +++ b/modules/ANSODEngine/ANSODEngine.cpp @@ -422,7 +422,7 @@ namespace ANSCENTER // Store config so per-camera trackers can be created lazily _trackerMotType = 1; // default BYTETRACK - _trackerParams = R"({"parameters":{"frame_rate":"15","track_buffer":"300","track_threshold":"0.500000","high_threshold":"0.600000","match_thresold":"0.980000"}})"; + _trackerParams = R"({"parameters":{"frame_rate":"10","track_buffer":"300","track_threshold":"0.500000","high_threshold":"0.600000","match_thresold":"0.980000"}})"; switch (trackerType) { case TrackerType::BYTETRACK: {