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

@@ -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;
}