Fix ALPR pipeline. Ready for production

This commit is contained in:
2026-04-05 11:55:37 +10:00
parent db089c3697
commit f7cef5015a
14 changed files with 331 additions and 22 deletions

View File

@@ -76,7 +76,8 @@
"Bash(python -c \":*)", "Bash(python -c \":*)",
"Bash(find /c/Projects/CLionProjects/ANSCORE -type d -name *ANSODEngine*)", "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\\\\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)"
] ]
} }
} }

View File

@@ -8,7 +8,7 @@
// Set to 0 for production builds to eliminate all debug output overhead. // Set to 0 for production builds to eliminate all debug output overhead.
// ============================================================================ // ============================================================================
#ifndef ANSCORE_DEBUGVIEW #ifndef ANSCORE_DEBUGVIEW
#define ANSCORE_DEBUGVIEW 1 // 1 = enabled (debug), 0 = disabled (production) #define ANSCORE_DEBUGVIEW 0 // 1 = enabled (debug), 0 = disabled (production)
#endif #endif
// ANS_DBG: Debug logging macro for DebugView (OutputDebugStringA on Windows). // ANS_DBG: Debug logging macro for DebugView (OutputDebugStringA on Windows).

View File

@@ -286,6 +286,163 @@ namespace ANSCENTER {
return detectedPlate; 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<std::recursive_mutex> 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<int>(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<int>(tp.textHistory.size()), maxFrames);
for (int i = static_cast<int>(tp.textHistory.size()) - 1;
i >= static_cast<int>(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<int>(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<int>(tp.textHistory.size()),
minVotesToStabilize * 2);
for (int i = static_cast<int>(tp.textHistory.size()) - 1;
i >= static_cast<int>(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<int>(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<int>(p.textHistory.size()), 3);
for (int j = static_cast<int>(p.textHistory.size()) - 1;
j >= static_cast<int>(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<int>(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) { static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) {
try { try {

View File

@@ -39,6 +39,18 @@ namespace ANSCENTER
[[nodiscard]] int levenshteinDistance(const std::string& s1, const std::string& s2); [[nodiscard]] int levenshteinDistance(const std::string& s1, const std::string& s2);
[[nodiscard]] float computeIoU(const cv::Rect& a, const cv::Rect& b); [[nodiscard]] float computeIoU(const cv::Rect& a, const cv::Rect& b);
[[nodiscard]] std::string majorityVote(const std::deque<std::string>& history); [[nodiscard]] std::string majorityVote(const std::deque<std::string>& history);
// --- TrackId-based plate tracking (hybrid: trackId primary, Levenshtein fallback) ---
struct TrackedPlateById {
int trackId = 0;
std::deque<std::string> textHistory;
std::string lockedText;
int lockCount = 0;
int framesSinceLastSeen = 0;
};
// cameraId → (trackId → tracked plate)
std::unordered_map<std::string, std::unordered_map<int, TrackedPlateById>> trackedPlatesById;
public: public:
void Init(int framesToStore = MAX_ALPR_FRAME); void Init(int framesToStore = MAX_ALPR_FRAME);
ALPRChecker(int framesToStore = MAX_ALPR_FRAME) : maxFrames(framesToStore) {} 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); [[nodiscard]] std::string checkPlate(const std::string& cameraId, const std::string& detectedPlate);
// Enhanced API with bounding box for spatial plate tracking // 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); [[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 { class ANSLPR_API ANSALPR {
@@ -72,6 +86,12 @@ namespace ANSCENTER
cv::Rect _detectedArea;// Area where license plate are detected cv::Rect _detectedArea;// Area where license plate are detected
Country _country; Country _country;
std::recursive_mutex _mutex; 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: 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 Initialize(const std::string& licenseKey, const std::string& modelZipFilePath, const std::string& modelZipPassword, double detectorThreshold, double ocrThreshold, double colourTheshold=0);
[[nodiscard]] virtual bool LoadEngine(); [[nodiscard]] virtual bool LoadEngine();
@@ -92,6 +112,13 @@ namespace ANSCENTER
/// (LP detection, OCR, color classification, validation, serialization). /// (LP detection, OCR, color classification, validation, serialization).
/// Also propagates the flag to sub-detectors (_lpDetector, _ocrDetector, etc.). /// Also propagates the flag to sub-detectors (_lpDetector, _ocrDetector, etc.).
virtual void ActivateDebugger(bool debugFlag) { _debugFlag = debugFlag; } 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]] virtual bool Destroy() = 0;
[[nodiscard]] static std::vector<cv::Rect> GetBoundingBoxes(const std::string& strBBoxes); [[nodiscard]] static std::vector<cv::Rect> GetBoundingBoxes(const std::string& strBBoxes);
[[nodiscard]] static std::string PolygonToString(const std::vector<cv::Point2f>& polygon); [[nodiscard]] static std::string PolygonToString(const std::vector<cv::Point2f>& polygon);
@@ -142,6 +169,9 @@ extern "C" ANSLPR_API int ANSALPR_SetFormat(ANSCENTER::ANSALPR** Handle, con
extern "C" ANSLPR_API int ANSALPR_SetFormats(ANSCENTER::ANSALPR** Handle, const char* formats);// comma separated formats 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 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 // 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_ConvertUTF8ToUTF16LE(const char* utf8Str, LStrHandle result, int includeBOM = 1);
extern "C" ANSLPR_API int ANSLPR_ConvertUTF16LEToUTF8(const unsigned char* utf16leBytes, int byteLen, LStrHandle result); extern "C" ANSLPR_API int ANSLPR_ConvertUTF16LEToUTF8(const unsigned char* utf16leBytes, int byteLen, LStrHandle result);

View File

@@ -862,6 +862,39 @@ namespace ANSCENTER {
return false; 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<Object> ANSALPR_OD::RunInferenceSingleFrame(const cv::Mat& input, const std::string& cameraId) { std::vector<Object> ANSALPR_OD::RunInferenceSingleFrame(const cv::Mat& input, const std::string& cameraId) {
// No coarse _mutex here — sub-components (detectors, alprChecker) have their own locks. // No coarse _mutex here — sub-components (detectors, alprChecker) have their own locks.
// LabVIEW semaphore controls concurrency at the caller level. // LabVIEW semaphore controls concurrency at the caller level.
@@ -931,6 +964,13 @@ namespace ANSCENTER {
// Run license plate detection // Run license plate detection
cv::Mat activeFrame = frame(detectedArea); cv::Mat activeFrame = frame(detectedArea);
std::vector<Object> lprOutput = _lpDetector->RunInference(activeFrame, cameraId); std::vector<Object> 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()) { if (lprOutput.empty()) {
#ifdef FNS_DEBUG #ifdef FNS_DEBUG
@@ -972,7 +1012,11 @@ namespace ANSCENTER {
} }
lprObject.cameraId = cameraId; 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()) { if (lprObject.className.empty()) {
continue; continue;
@@ -994,7 +1038,9 @@ namespace ANSCENTER {
// Deduplicate: if two trackIds claim the same plate text, keep the one // Deduplicate: if two trackIds claim the same plate text, keep the one
// with the higher accumulated score to prevent plate flickering // with the higher accumulated score to prevent plate flickering
if (shouldUseALPRChecker(cv::Size(frameWidth, frameHeight), cameraId)) {
ensureUniquePlateText(output, cameraId); ensureUniquePlateText(output, cameraId);
}
return output; return output;
@@ -1473,7 +1519,11 @@ namespace ANSCENTER {
auto tValidate = dbg ? Clock::now() : Clock::time_point{}; auto tValidate = dbg ? Clock::now() : Clock::time_point{};
lprObject.cameraId = cameraId; lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows); 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<double, std::milli>(Clock::now() - tValidate).count(); } if (dbg) { totalValidateMs += std::chrono::duration<double, std::milli>(Clock::now() - tValidate).count(); }
if (lprObject.className.empty()) { if (lprObject.className.empty()) {
@@ -1610,6 +1660,13 @@ namespace ANSCENTER {
cv::Mat croppedObject = frame(objectPos); cv::Mat croppedObject = frame(objectPos);
std::vector<Object> lprOutput = _lpDetector->RunInference(croppedObject, cameraId); std::vector<Object> 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) { for (auto& lprObject : lprOutput) {
const cv::Rect& box = lprObject.box; const cv::Rect& box = lprObject.box;
@@ -1652,7 +1709,11 @@ namespace ANSCENTER {
continue; 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()) { if (lprObject.className.empty()) {
continue; continue;
@@ -1670,6 +1731,13 @@ namespace ANSCENTER {
else { else {
// No bounding boxes - run on full frame // No bounding boxes - run on full frame
std::vector<Object> lprOutput = _lpDetector->RunInference(frame, cameraId); std::vector<Object> 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()); detectedObjects.reserve(lprOutput.size());
for (auto& lprObject : lprOutput) { for (auto& lprObject : lprOutput) {
@@ -1704,7 +1772,11 @@ namespace ANSCENTER {
std::string rawText = DetectLicensePlateString(alignedLPR, cameraId); 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()) { if (lprObject.className.empty()) {
continue; continue;
@@ -1723,7 +1795,9 @@ namespace ANSCENTER {
// Note: in Bbox mode, internal LP trackIds overlap across crops, so // Note: in Bbox mode, internal LP trackIds overlap across crops, so
// dedup uses plate bounding box position (via Object::box) to distinguish. // dedup uses plate bounding box position (via Object::box) to distinguish.
// The ensureUniquePlateText method handles this by plate text grouping. // The ensureUniquePlateText method handles this by plate text grouping.
if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) {
ensureUniquePlateText(detectedObjects, cameraId); ensureUniquePlateText(detectedObjects, cameraId);
}
lprResult = VectorDetectionToJsonString(detectedObjects); lprResult = VectorDetectionToJsonString(detectedObjects);
return true; return true;
@@ -2252,6 +2326,13 @@ namespace ANSCENTER {
// Run license plate detection (should be thread-safe internally) // Run license plate detection (should be thread-safe internally)
cv::Mat activeFrame = frame(detectedArea); cv::Mat activeFrame = frame(detectedArea);
std::vector<Object> lprOutput = _lpDetector->RunInference(activeFrame, cameraId); std::vector<Object> 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()) { if (lprOutput.empty()) {
return {}; return {};
@@ -2308,8 +2389,12 @@ namespace ANSCENTER {
Object lprObject = lprOutput[origIdx]; Object lprObject = lprOutput[origIdx];
lprObject.cameraId = cameraId; lprObject.cameraId = cameraId;
// Stabilize OCR text through ALPRChecker (spatial tracking + majority voting) // Stabilize OCR text through ALPRChecker (hybrid trackId + Levenshtein fallback)
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()) { if (lprObject.className.empty()) {
continue; continue;
@@ -2327,7 +2412,9 @@ namespace ANSCENTER {
// Deduplicate: if two trackIds claim the same plate text, keep the one // Deduplicate: if two trackIds claim the same plate text, keep the one
// with the higher accumulated score to prevent plate flickering // with the higher accumulated score to prevent plate flickering
if (shouldUseALPRChecker(cv::Size(input.cols, input.rows), cameraId)) {
ensureUniquePlateText(output, cameraId); ensureUniquePlateText(output, cameraId);
}
return output; return output;
} }

View File

@@ -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" }; "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; 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<std::string, ImageSizeTracker> _imageSizeTrackers;
[[nodiscard]] bool shouldUseALPRChecker(const cv::Size& imageSize, const std::string& cameraId);
// Plate identity persistence: accumulated confidence per spatial plate location. // Plate identity persistence: accumulated confidence per spatial plate location.
// Prevents the same plate string from appearing on multiple vehicles. // Prevents the same plate string from appearing on multiple vehicles.
// Uses bounding box center position (not trackId) as the identity key, // Uses bounding box center position (not trackId) as the identity key,
@@ -204,6 +222,8 @@ namespace ANSCENTER
if (_ocrDetector) _ocrDetector->ActivateDebugger(debugFlag); if (_ocrDetector) _ocrDetector->ActivateDebugger(debugFlag);
if (_lpColourDetector) _lpColourDetector->ActivateDebugger(debugFlag); if (_lpColourDetector) _lpColourDetector->ActivateDebugger(debugFlag);
} }
// SetALPRCheckerEnabled() and IsALPRCheckerEnabled() inherited from ANSALPR base class
}; };
} }
#endif #endif

View File

@@ -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 extern "C" ANSLPR_API int ANSALPR_GetFormats(ANSCENTER::ANSALPR** Handle, LStrHandle Lstrformats)// semi separated formats
{ {
if (!Handle || !*Handle) return -1; if (!Handle || !*Handle) return -1;

View File

@@ -43,7 +43,7 @@ namespace ANSCENTER {
ANSByteTrack::ANSByteTrack() { ANSByteTrack::ANSByteTrack() {
_licenseValid = false; _licenseValid = false;
CheckLicense(); 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() { ANSByteTrack::~ANSByteTrack() {

View File

@@ -50,7 +50,7 @@ namespace ANSCENTER {
_licenseValid = false; _licenseValid = false;
CheckLicense(); CheckLicense();
int frame_rate = 30; int frame_rate = 10;
int track_buffer = 30; int track_buffer = 30;
float track_thresh = 0.25; float track_thresh = 0.25;
float track_highthres = 0.25; float track_highthres = 0.25;

View File

@@ -44,7 +44,7 @@ namespace ANSCENTER{
ANSByteTrackNCNN::ANSByteTrackNCNN() { ANSByteTrackNCNN::ANSByteTrackNCNN() {
_licenseValid = false; _licenseValid = false;
CheckLicense(); 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() { ANSByteTrackNCNN::~ANSByteTrackNCNN() {

View File

@@ -16,14 +16,14 @@ namespace ByteTrack
public: public:
using STrackPtr = std::shared_ptr<STrack>; using STrackPtr = std::shared_ptr<STrack>;
BYTETracker(const int& frame_rate = 30, BYTETracker(const int& frame_rate = 10,
const int& track_buffer = 30, const int& track_buffer = 30,
const float& track_thresh = 0.5, const float& track_thresh = 0.5,
const float& high_thresh = 0.6, const float& high_thresh = 0.6,
const float& match_thresh = 0.8); const float& match_thresh = 0.8);
~BYTETracker(); ~BYTETracker();
std::vector<STrackPtr> update(const std::vector<Object>& objects); std::vector<STrackPtr> update(const std::vector<Object>& 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; float getEstimatedFps() const;
private: private:

View File

@@ -34,8 +34,8 @@ namespace ByteTrackEigen {
* @param match_thresh Threshold for matching detections to tracks. * @param match_thresh Threshold for matching detections to tracks.
* @param frame_rate Frame rate of the video being processed. * @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); BYTETracker(float track_thresh = 0.25, int track_buffer = 30, float match_thresh = 0.8, int frame_rate = 10);
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; float getEstimatedFps() const;
std::vector<KalmanBBoxTrack> update(const Eigen::MatrixXf& output_results, const std::vector<std::string> obj_ids); std::vector<KalmanBBoxTrack> update(const Eigen::MatrixXf& output_results, const std::vector<std::string> obj_ids);

View File

@@ -6,12 +6,12 @@ namespace ByteTrackNCNN {
class BYTETracker class BYTETracker
{ {
public: public:
BYTETracker(int frame_rate = 30, int track_buffer = 30); BYTETracker(int frame_rate = 10, int track_buffer = 30);
~BYTETracker(); ~BYTETracker();
std::vector<STrack> update(const std::vector<ByteTrackNCNN::Object>& objects); std::vector<STrack> update(const std::vector<ByteTrackNCNN::Object>& objects);
//cv::Scalar get_color(int idx); //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; float getEstimatedFps() const;
private: private:
std::vector<STrack*> joint_stracks(std::vector<STrack*>& tlista, std::vector<STrack>& tlistb); std::vector<STrack*> joint_stracks(std::vector<STrack*>& tlista, std::vector<STrack>& tlistb);

View File

@@ -422,7 +422,7 @@ namespace ANSCENTER
// Store config so per-camera trackers can be created lazily // Store config so per-camera trackers can be created lazily
_trackerMotType = 1; // default BYTETRACK _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) { switch (trackerType) {
case TrackerType::BYTETRACK: { case TrackerType::BYTETRACK: {