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

@@ -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 {