Fix ALPR pipeline

This commit is contained in:
2026-04-01 17:01:05 +11:00
parent 6c6d1c0e0b
commit 4bedf3a3a2
3 changed files with 102 additions and 47 deletions

View File

@@ -1715,6 +1715,12 @@ namespace ANSCENTER {
}
}
// Deduplicate: same plate text should not appear on multiple vehicles
// 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);
lprResult = VectorDetectionToJsonString(detectedObjects);
return true;
@@ -2712,35 +2718,80 @@ namespace ANSCENTER {
void ANSALPR_OD::ensureUniquePlateText(std::vector<Object>& results, const std::string& cameraId)
{
if (results.empty()) return;
auto& identities = _plateIdentities[cameraId];
auto isEmptyPlate = [](const std::string& plate) {
return plate.empty();
// Option B: Auto-detect mode by counting detections.
// 1 detection → crop/pipeline mode → return instant result, no accumulated scoring
// 2+ detections → full-frame mode → use accumulated scoring for dedup
if (results.size() <= 1) {
// Still prune stale spatial identities from previous full-frame calls
if (!identities.empty()) {
constexpr int MAX_UNSEEN_FRAMES = 30;
for (auto& id : identities) {
id.framesSinceLastSeen++;
}
for (auto it = identities.begin(); it != identities.end(); ) {
if (it->framesSinceLastSeen > MAX_UNSEEN_FRAMES) {
it = identities.erase(it);
} else {
++it;
}
}
}
return;
}
// --- Full-frame mode: 2+ detections, apply accumulated-score dedup ---
// Helper: compute IoU between two rects
auto computeIoU = [](const cv::Rect& a, const cv::Rect& b) -> float {
int x1 = std::max(a.x, b.x);
int y1 = std::max(a.y, b.y);
int x2 = std::min(a.x + a.width, b.x + b.width);
int y2 = std::min(a.y + a.height, b.y + b.height);
if (x2 <= x1 || y2 <= y1) return 0.0f;
float intersection = static_cast<float>((x2 - x1) * (y2 - y1));
float unionArea = static_cast<float>(a.area() + b.area()) - intersection;
return (unionArea > 0.0f) ? intersection / unionArea : 0.0f;
};
auto& identities = _plateIdentities[cameraId];
// Helper: find matching spatial identity by bounding box overlap
auto findSpatialMatch = [&](const cv::Rect& box, const std::string& plateText) -> SpatialPlateIdentity* {
for (auto& id : identities) {
if (id.plateText == plateText) {
// Reconstruct approximate rect from stored center
cv::Rect storedRect(
static_cast<int>(id.center.x - box.width * 0.5f),
static_cast<int>(id.center.y - box.height * 0.5f),
box.width, box.height);
if (computeIoU(box, storedRect) > PLATE_SPATIAL_MATCH_THRESHOLD) {
return &id;
}
}
}
return nullptr;
};
// Step 1: Build map of plateText → candidate indices
std::unordered_map<std::string, std::vector<size_t>> plateCandidates;
for (size_t i = 0; i < results.size(); ++i) {
if (isEmptyPlate(results[i].className)) continue;
if (results[i].className.empty()) continue;
plateCandidates[results[i].className].push_back(i);
}
// Step 2: Resolve duplicates using accumulated scores
// Step 2: Resolve duplicates using spatial accumulated scores
for (auto& [plateText, indices] : plateCandidates) {
if (indices.size() <= 1) continue;
// Find the candidate with the highest accumulated score
// Find which candidate has the best accumulated score at its location
size_t winner = indices[0];
float bestScore = 0.0f;
for (size_t idx : indices) {
int tid = results[idx].trackId;
float score = results[idx].confidence; // fallback for new trackIds
auto it = identities.find(tid);
if (it != identities.end() && it->second.plateText == plateText) {
score = it->second.accumulatedScore + results[idx].confidence;
float score = results[idx].confidence;
auto* match = findSpatialMatch(results[idx].box, plateText);
if (match) {
score = match->accumulatedScore + results[idx].confidence;
}
if (score > bestScore) {
bestScore = score;
@@ -2756,52 +2807,48 @@ namespace ANSCENTER {
}
}
// Step 3: Update accumulated scores — winners accumulate, losers decay
// Step 3: Update spatial identities — winners accumulate, losers decay
constexpr float DECAY_FACTOR = 0.8f;
constexpr float MIN_SCORE = 0.1f;
constexpr int MAX_UNSEEN_FRAMES = 30;
// Age all existing identities
for (auto& id : identities) {
id.framesSinceLastSeen++;
}
for (auto& r : results) {
int tid = r.trackId;
if (r.className.empty()) continue;
if (isEmptyPlate(r.className)) {
// Lost dedup or empty — decay
auto it = identities.find(tid);
if (it != identities.end()) {
it->second.accumulatedScore *= DECAY_FACTOR;
if (it->second.accumulatedScore < MIN_SCORE) {
identities.erase(it);
}
}
continue;
}
cv::Point2f center(
r.box.x + r.box.width * 0.5f,
r.box.y + r.box.height * 0.5f);
auto it = identities.find(tid);
if (it != identities.end()) {
if (it->second.plateText == r.className) {
it->second.accumulatedScore += r.confidence;
} else {
it->second.plateText = r.className;
it->second.accumulatedScore = r.confidence;
}
auto* match = findSpatialMatch(r.box, r.className);
if (match) {
// Same plate at same location — accumulate
match->accumulatedScore += r.confidence;
match->center = center; // update position
match->framesSinceLastSeen = 0;
} else {
identities[tid] = { r.className, r.confidence };
// New plate location — add entry
identities.push_back({ center, r.className, r.confidence, 0 });
}
}
// Step 4: Clean up trackIds no longer in the scene
std::unordered_set<int> activeTrackIds;
for (const auto& r : results) {
activeTrackIds.insert(r.trackId);
}
// Decay unseen identities and remove stale ones
for (auto it = identities.begin(); it != identities.end(); ) {
if (activeTrackIds.find(it->first) == activeTrackIds.end()) {
if (it->framesSinceLastSeen > 0) {
it->accumulatedScore *= DECAY_FACTOR;
}
if (it->accumulatedScore < MIN_SCORE || it->framesSinceLastSeen > MAX_UNSEEN_FRAMES) {
it = identities.erase(it);
} else {
++it;
}
}
// Step 5: Remove entries with cleared plate text from results
// Step 4: Remove entries with cleared plate text
results.erase(
std::remove_if(results.begin(), results.end(),
[](const Object& o) { return o.className.empty(); }),