diff --git a/ANSLIB/ANSLIB.cpp b/ANSLIB/ANSLIB.cpp index b192a49..a49656b 100644 --- a/ANSLIB/ANSLIB.cpp +++ b/ANSLIB/ANSLIB.cpp @@ -7,10 +7,27 @@ namespace ANSCENTER { ANSLIB::ANSLIB() { + // Pre-load all ANSCORE DLLs from the Shared folder using full paths. + // This ensures the correct versions are loaded regardless of PATH order + // (e.g., DLHUB_Runtime_Engine may contain older copies on some machines). + // Once loaded, Windows reuses them for all subsequent implicit dependencies. + const char* sharedDir = "C:\\ProgramData\\ANSCENTER\\Shared\\"; + const char* preloadDlls[] = { + "ANSLicensingSystem.dll", + "anslicensing.dll", + "ANSMOT.dll", + "ANSODEngine.dll", + nullptr + }; + for (int i = 0; preloadDlls[i] != nullptr; i++) { + std::string fullPath = std::string(sharedDir) + preloadDlls[i]; + LoadLibraryA(fullPath.c_str()); + } + // Ensure all shared DLLs (OpenCV, OpenVINO, TRT, ORT) are pre-loaded ANSCENTER::ANSLibsLoader::Initialize(); - const char* dllPath = "C:\\ProgramData\\ANSCENTER\\Shared\\ANSODEngine.dll"; // Hardcoded path + const char* dllPath = "C:\\ProgramData\\ANSCENTER\\Shared\\ANSODEngine.dll"; dllHandle = LoadLibraryA(dllPath); if (!dllHandle) { return; diff --git a/docs/PLAN_ALPRChecker_Hybrid_TrackId.md b/docs/PLAN_ALPRChecker_Hybrid_TrackId.md new file mode 100644 index 0000000..f8b9dc2 --- /dev/null +++ b/docs/PLAN_ALPRChecker_Hybrid_TrackId.md @@ -0,0 +1,253 @@ +# Plan: Hybrid trackId ALPRChecker with Auto-Detection of Pipeline vs Full-Frame Mode + +**Date**: 2026-04-05 +**Status**: Approved for implementation +**Affects**: ANSALPR_OD (Layer 2: ALPRChecker, Layer 3: ensureUniquePlateText) + +--- + +## Problem + +When ANSALPR is used in a **pipeline** (vehicle detector crops each vehicle → ALPR runs on each crop independently), ALPRChecker (Layer 2) merges plates from different vehicles because: + +1. LP bounding boxes are **crop-relative** — plates from different vehicles end up at similar (x, y) positions within their respective crops +2. ALPRChecker matches by **IoU on bounding boxes** → high IoU between crop-relative boxes → falsely merges +3. **Levenshtein fallback** can also merge similar plates (e.g., "29BA-1234" and "29BA-1235", distance=1) +4. **Proximity guard** catches remaining cases → all vehicles get the same locked plate text + +**Result**: All detected vehicles return the same license plate. + +--- + +## Solution + +Three-part fix: + +1. **Auto-detect** full-frame vs pipeline mode by tracking image size consistency per camera +2. **Full-frame mode**: Enable Layer 2 + Layer 3, with **hybrid trackId matching** (trackId primary, Levenshtein fallback for lost tracks) +3. **Pipeline/crop mode**: Disable both layers — pass raw OCR through immediately + +--- + +## Design: Tri-State Mode Flag + +``` +_alprCheckerMode: + -1 = auto-detect (default) + 0 = explicitly disabled (raw OCR always) + 1 = explicitly enabled (ALPRChecker + dedup always) +``` + +| `_alprCheckerMode` | Image size | Layer 2 (ALPRChecker) | Layer 3 (ensureUnique) | +|---|---|---|---| +| `0` (explicit off) | Any | **OFF** — raw OCR pass-through | **OFF** | +| `1` (explicit on) | Any | **ON** — hybrid trackId + Levenshtein | **ON** | +| `-1` (auto, default) + size varies | Pipeline detected | **OFF** — raw OCR pass-through | **OFF** | +| `-1` (auto, default) + size constant 5+ frames | Full-frame detected | **ON** — hybrid trackId + Levenshtein | **ON** | + +--- + +## Design: Hybrid trackId ALPRChecker + +### Why trackId is better than IoU for full-frame mode + +| Scenario | Current (IoU) | Hybrid (trackId) | +|---|---|---| +| Two vehicles side-by-side, similar plates | **False merge** (IoU overlap) | **Correct** (different trackIds) | +| Fast-moving vehicle | **May lose history** (IoU=0) | **Correct** (ByteTrack tracks motion) | +| Identical plates in frame (fleet) | **Merges into one** (Levenshtein=0) | **Correct** (separate trackIds) | +| Plate occluded, reappears with new trackId | **Recovers** (text similarity) | **Recovers** (Levenshtein fallback migrates history) | + +### Algorithm: `checkPlateByTrackId(cameraId, ocrText, trackId)` + +``` +Step 1: Age all plates for this camera (framesSinceLastSeen++) +Step 2: Periodic pruning (every 30 calls, remove stale entries >180 frames) + +Step 3 — Primary: hash lookup plates[trackId] + If found: + → Append raw OCR to textHistory (not corrected — avoids feedback loop) + → majorityVote() on history + → Lock logic: + - Not locked + 3 consistent votes → LOCK + - Locked + exact match → return locked text (fast path) + - Locked + vote drifted (Levenshtein > 1) + 3 new votes → RE-LOCK + - Locked + noise → return locked text (resist) + → Return result immediately + +Step 4 — Fallback: Levenshtein scan for lost tracks + For each existing plate entry: + If Levenshtein(detectedPlate, lockedText) ≤ 1: + → MIGRATE: move history from old trackId to new trackId + → Return locked text + If not locked, check last 3 history entries: + → Same migration logic + +Step 5 — No match: create new entry + plates[trackId] = { textHistory=[detectedPlate] } + Return raw OCR text immediately +``` + +### Frame-by-Frame Behavior (what LabVIEW sees) + +| Frame | OCR Read | Returned to LabVIEW | Internal State | +|-------|----------|---------------------|----------------| +| 1 | "29BA-12345" | **"29BA-12345"** (instant) | New entry, history=[1 read] | +| 2 | "29BA-12345" | **"29BA-12345"** (voted) | history=[2 reads], not locked (need 3) | +| 3 | "29B4-12345" | **"29BA-12345"** (voted, corrected OCR error) | history=[3 reads], not locked | +| 4 | "29BA-12345" | **"29BA-12345"** | **LOCKED** (3 consistent votes) | +| 5+ | "29B4-12345" | **"29BA-12345"** (locked, resists noise) | Lock held | +| 50+ | consistently "30CD-567" | **"30CD-567"** | **RE-LOCKED** to new plate | + +**Key**: Every frame gets an immediate response. No waiting, no buffering. Frame 1 returns raw OCR. Subsequent frames return increasingly stable text. + +--- + +## Design: Auto-Detection (`shouldUseALPRChecker`) + +Tracks image size per camera. If size is constant for 5+ consecutive frames → full-frame mode. If size changes → pipeline mode. + +```cpp +bool shouldUseALPRChecker(const cv::Size& imageSize, const std::string& cameraId) { + if (_alprCheckerMode == 0) return false; // explicit off + if (_alprCheckerMode == 1) return true; // explicit on + + // Auto-detect: check image size consistency + auto& tracker = _imageSizeTrackers[cameraId]; + if (imageSize == tracker.lastSize) { + tracker.consistentCount++; + if (tracker.consistentCount >= 5) tracker.detectedFullFrame = true; + } else { + tracker.lastSize = imageSize; + tracker.consistentCount = 1; + tracker.detectedFullFrame = false; + } + return tracker.detectedFullFrame; +} +``` + +--- + +## Files to Modify + +| File | Change | +|------|--------| +| `modules/ANSLPR/ANSLPR.h` | Add `TrackedPlateById` struct, `trackedPlatesById` map, `checkPlateByTrackId()` declaration to ALPRChecker class | +| `modules/ANSLPR/ANSLPR.cpp` | Implement `checkPlateByTrackId()` (after existing `checkPlate()` at line 288) | +| `modules/ANSLPR/ANSLPR_OD.h` | Add `_alprCheckerMode`, `ImageSizeTracker`, `shouldUseALPRChecker()`, public `SetALPRCheckerMode()`/`GetALPRCheckerMode()` | +| `modules/ANSLPR/ANSLPR_OD.cpp` | Implement `shouldUseALPRChecker()`; guard 5 `checkPlate` + 3 `ensureUniquePlateText` call sites | +| `modules/ANSLPR/dllmain.cpp` | Add `ANSALPR_SetALPRCheckerMode` DLL export | + +--- + +## Call Sites to Guard + +### 5 checkPlate call sites (replace with conditional): + +| Line | Function | Image size source | +|------|----------|-------------------| +| 975 | `RunInferenceSingleFrame` | `frameWidth`, `frameHeight` | +| 1476 | `Inference` (no-bbox path) | `input.cols`, `input.rows` | +| 1655 | `Inference` (bbox path) | `input.cols`, `input.rows` | +| 1707 | `Inference` (full-frame fallback) | `input.cols`, `input.rows` | +| 2312 | `RunInference` (batch) | `input.cols`, `input.rows` | + +Pattern at each site: +```cpp +// Before: +lprObject.className = alprChecker.checkPlate(cameraId, ocrText, lprObject.box); + +// After: +if (shouldUseALPRChecker(cv::Size(frameWidth, frameHeight), cameraId)) { + lprObject.className = alprChecker.checkPlateByTrackId(cameraId, ocrText, lprObject.trackId); +} else { + lprObject.className = ocrText; // raw OCR pass-through +} +``` + +### 3 ensureUniquePlateText call sites (wrap with conditional): + +| Line | Function | Image size source | +|------|----------|-------------------| +| 997 | `RunInferenceSingleFrame` | `frameWidth`, `frameHeight` | +| 1726 | `Inference` | `input.cols`, `input.rows` | +| 2330 | `RunInference` (batch) | `input.cols`, `input.rows` | + +Pattern at each site: +```cpp +// Before: +ensureUniquePlateText(output, cameraId); + +// After: +if (shouldUseALPRChecker(cv::Size(frameWidth, frameHeight), cameraId)) { + ensureUniquePlateText(output, cameraId); +} +``` + +--- + +## DLL Export API + +```cpp +// Declaration (ANSLPR.h): +extern "C" ANSLPR_API int ANSALPR_SetALPRCheckerMode(ANSCENTER::ANSALPR** Handle, int mode); + +// Implementation (dllmain.cpp): +extern "C" ANSLPR_API int ANSALPR_SetALPRCheckerMode(ANSCENTER::ANSALPR** Handle, int mode) { + if (!Handle || !*Handle) return -1; + auto* od = dynamic_cast(*Handle); + if (!od) return -2; + od->SetALPRCheckerMode(mode); + return 1; +} +``` + +**LabVIEW usage**: +- `ANSALPR_SetALPRCheckerMode(handle, -1)` → auto-detect (default, no call needed) +- `ANSALPR_SetALPRCheckerMode(handle, 0)` → force disable (guaranteed raw OCR) +- `ANSALPR_SetALPRCheckerMode(handle, 1)` → force enable (guaranteed stabilization) + +--- + +## Backward Compatibility + +- Default `_alprCheckerMode = -1` (auto) + pipeline (varying sizes) = both layers disabled = raw OCR = **same as if ALPRChecker never existed** +- Default auto + full-frame (constant sizes) = auto-enables after 5 frames = **improved accuracy** over current IoU-based approach +- Explicit `mode = 0` = **guaranteed off** regardless of image size — raw OCR always +- Explicit `mode = 1` = **guaranteed on** regardless of image size +- Existing `checkPlate()` methods are **not modified** — remain available for other code +- New `checkPlateByTrackId()` is additive — no existing API changes + +--- + +## Verification + +1. **Pipeline mode**: Call ALPR with different-sized vehicle crops → each returns independent OCR, no cross-contamination +2. **Full-frame mode**: Call ALPR with same-sized frames → after 5 frames, Layer 2+3 auto-enable, trackId-based stabilization active +3. **Track recovery**: Occlude a plate → ByteTrack assigns new trackId → Levenshtein fallback migrates history, lock preserved +4. **Explicit disable**: `ANSALPR_SetALPRCheckerMode(handle, 0)` → raw OCR always, no stabilization +5. **Explicit enable**: `ANSALPR_SetALPRCheckerMode(handle, 1)` → both layers always active +6. **Build**: Compile DLL, verify no linker errors + +--- + +## Performance Comparison: Current vs Hybrid + +### Matching step (per plate, per frame) + +| | Current (IoU + Levenshtein) | Hybrid (trackId + Levenshtein fallback) | +|---|---|---| +| Primary lookup | O(n) linear scan + IoU | O(1) hash map | +| Fallback | O(n) Levenshtein scan | O(n) Levenshtein scan (only on miss) | +| Memory | vector (contiguous) | unordered_map (heap nodes) | +| False merges | Possible (IoU overlap or Levenshtein ≤ 1) | **Impossible** via primary path | +| False splits | Rare (IoU + text recovers) | Possible (new trackId after occlusion), **recovered by fallback** | + +### Accuracy + +| Scenario | Current | Hybrid | +|---|---|---| +| Dense traffic, similar plates | Degrades (false merges) | **Better** (trackId separation) | +| Fast-moving vehicles | May lose history | **Better** (ByteTrack tracks motion) | +| Frequent occlusions | Good recovery (text similarity) | Good recovery (Levenshtein fallback migrates) | +| Fleet vehicles (identical plates) | Merges | **Better** (separate trackIds) | diff --git a/modules/ANSCV/CMakeLists.txt b/modules/ANSCV/CMakeLists.txt index cbb953a..a46cd4d 100644 --- a/modules/ANSCV/CMakeLists.txt +++ b/modules/ANSCV/CMakeLists.txt @@ -66,10 +66,10 @@ target_include_directories(ANSCV PRIVATE ) target_link_libraries(ANSCV - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: SPDLogger/FileExist symbols must be re-exported PRIVATE labview PRIVATE spdlog_dep - PRIVATE ANSLibsLoader + PUBLIC ANSLibsLoader # PUBLIC: loader symbols used in public headers PRIVATE opencv PRIVATE ffmpeg PRIVATE turbojpeg diff --git a/modules/ANSFR/CMakeLists.txt b/modules/ANSFR/CMakeLists.txt index 129509c..5920839 100644 --- a/modules/ANSFR/CMakeLists.txt +++ b/modules/ANSFR/CMakeLists.txt @@ -44,7 +44,7 @@ target_include_directories(ANSFR PRIVATE target_link_libraries(ANSFR PUBLIC ANSLibsLoader PRIVATE ANSODEngine - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: SPDLogger/FileExist symbols must be re-exported PRIVATE labview PRIVATE spdlog_dep PRIVATE opencv diff --git a/modules/ANSLLM/CMakeLists.txt b/modules/ANSLLM/CMakeLists.txt index 8deac32..5e0b74e 100644 --- a/modules/ANSLLM/CMakeLists.txt +++ b/modules/ANSLLM/CMakeLists.txt @@ -14,7 +14,7 @@ target_include_directories(ANSLLM PUBLIC ) target_link_libraries(ANSLLM - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: SPDLogger used in ANSLLM.h public header PRIVATE anslicensing PRIVATE labview PRIVATE opencv diff --git a/modules/ANSLPR/CMakeLists.txt b/modules/ANSLPR/CMakeLists.txt index 3fe33ae..7f65c54 100644 --- a/modules/ANSLPR/CMakeLists.txt +++ b/modules/ANSLPR/CMakeLists.txt @@ -54,7 +54,7 @@ target_include_directories(ANSLPR PRIVATE target_link_libraries(ANSLPR PUBLIC ANSLibsLoader PRIVATE ANSODEngine - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: Utility.h/SPDLogger symbols must be re-exported PRIVATE ANSMOT PRIVATE labview PRIVATE spdlog_dep diff --git a/modules/ANSMOT/CMakeLists.txt b/modules/ANSMOT/CMakeLists.txt index 5cd1e5a..ea6fa1b 100644 --- a/modules/ANSMOT/CMakeLists.txt +++ b/modules/ANSMOT/CMakeLists.txt @@ -19,8 +19,10 @@ target_include_directories(ANSMOT PRIVATE ) # Match original vcxproj: labview, anslicensing, ANSLicensingSystem +# ANSLicensingSystem must be PUBLIC so SPDLogger symbols (LogFatal, etc.) +# are re-exported from ANSMOT.dll for consumers like ANSLIB.dll target_link_libraries(ANSMOT - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem PRIVATE anslicensing PRIVATE labview PRIVATE spdlog_dep diff --git a/modules/ANSOCR/CMakeLists.txt b/modules/ANSOCR/CMakeLists.txt index 66af6d8..524e4ef 100644 --- a/modules/ANSOCR/CMakeLists.txt +++ b/modules/ANSOCR/CMakeLists.txt @@ -40,7 +40,7 @@ target_include_directories(ANSOCR PRIVATE target_link_libraries(ANSOCR PUBLIC ANSLibsLoader PRIVATE ANSODEngine - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: FileExist/SPDLogger symbols must be re-exported PRIVATE labview PRIVATE spdlog_dep PRIVATE opencv diff --git a/modules/ANSODEngine/CMakeLists.txt b/modules/ANSODEngine/CMakeLists.txt index 328d1f4..2fbf4dd 100644 --- a/modules/ANSODEngine/CMakeLists.txt +++ b/modules/ANSODEngine/CMakeLists.txt @@ -111,7 +111,7 @@ target_include_directories(ANSODEngine PRIVATE target_link_libraries(ANSODEngine PUBLIC ANSLibsLoader - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: FileExist/SPDLogger symbols must be re-exported PUBLIC opencv PRIVATE ANSMOT PRIVATE onnxruntime @@ -133,9 +133,9 @@ target_link_libraries(ANSODEngine PRIVATE odhublib.lib odhublib_cpu.lib nvinfer_10.lib nvonnxparser_10.lib labview - ANSLicensingSystem spdlog_dep ) +# Note: ANSLicensingSystem already linked as PUBLIC above — no need to repeat target_compile_definitions(ANSODEngine PRIVATE UNICODE _UNICODE NOMINMAX ANSODENGINE_EXPORTS diff --git a/modules/ANSUtilities/CMakeLists.txt b/modules/ANSUtilities/CMakeLists.txt index 3f341a5..c13192b 100644 --- a/modules/ANSUtilities/CMakeLists.txt +++ b/modules/ANSUtilities/CMakeLists.txt @@ -14,7 +14,7 @@ target_include_directories(ANSUtilities PUBLIC ) target_link_libraries(ANSUtilities - PRIVATE ANSLicensingSystem + PUBLIC ANSLicensingSystem # PUBLIC: SPDLogger used in ANSUtilities.h public header PRIVATE labview PRIVATE opencv PRIVATE chilkat