Fix ALPR pipeline. Ready for production
This commit is contained in:
@@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<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) {
|
||||
try {
|
||||
|
||||
@@ -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<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:
|
||||
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<cv::Rect> GetBoundingBoxes(const std::string& strBBoxes);
|
||||
[[nodiscard]] static std::string PolygonToString(const std::vector<cv::Point2f>& 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);
|
||||
|
||||
@@ -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<Object> 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<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()) {
|
||||
#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<double, std::milli>(Clock::now() - tValidate).count(); }
|
||||
|
||||
if (lprObject.className.empty()) {
|
||||
@@ -1610,6 +1660,13 @@ namespace ANSCENTER {
|
||||
cv::Mat croppedObject = frame(objectPos);
|
||||
|
||||
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) {
|
||||
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<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());
|
||||
|
||||
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<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()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<std::string, ImageSizeTracker> _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
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -16,14 +16,14 @@ namespace ByteTrack
|
||||
public:
|
||||
using STrackPtr = std::shared_ptr<STrack>;
|
||||
|
||||
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<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;
|
||||
|
||||
private:
|
||||
|
||||
@@ -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<KalmanBBoxTrack> update(const Eigen::MatrixXf& output_results, const std::vector<std::string> obj_ids);
|
||||
|
||||
|
||||
@@ -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<STrack> update(const std::vector<ByteTrackNCNN::Object>& 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<STrack*> joint_stracks(std::vector<STrack*>& tlista, std::vector<STrack>& tlistb);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user