Fix ALPR pipeline. Ready for production
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user