diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 45c09b0..7ac652c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,10 @@ "Bash(ssh -T git@anscenter.ddns.net -p 2222)", "Bash(ssh-add -l)", "Bash(dotnet build:*)", - "Bash(grep -n \"struct Object\\\\|class Object\" /c/Projects/CLionProjects/ANSCORE/modules/ANSLPR/*.h /c/Projects/CLionProjects/ANSCORE/modules/ANSLPR/include/*.h)" + "Bash(grep -n \"struct Object\\\\|class Object\" /c/Projects/CLionProjects/ANSCORE/modules/ANSLPR/*.h /c/Projects/CLionProjects/ANSCORE/modules/ANSLPR/include/*.h)", + "Bash(grep -n \"cudaStream\\\\|cudaMalloc\\\\|cudaFree\\\\|queue\\\\|Task\\\\|mutex\" /c/Projects/CLionProjects/ANSCORE/engines/TensorRTAPI/include/engine/*.inl)", + "Bash(grep -n \"~Engine\\\\|destructor\\\\|cleanup\\\\|~\" /c/Projects/CLionProjects/ANSCORE/engines/TensorRTAPI/include/engine/*.inl)", + "Bash(grep -n \"for.*cudaFree\\\\|m_buffers\\\\[\" /c/Projects/CLionProjects/ANSCORE/engines/TensorRTAPI/include/engine/*.inl)" ] } } diff --git a/modules/ANSLPR/ANSLPR_OD.cpp b/modules/ANSLPR/ANSLPR_OD.cpp index 1843546..c1327c0 100644 --- a/modules/ANSLPR/ANSLPR_OD.cpp +++ b/modules/ANSLPR/ANSLPR_OD.cpp @@ -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& 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((x2 - x1) * (y2 - y1)); + float unionArea = static_cast(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(id.center.x - box.width * 0.5f), + static_cast(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> 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 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(); }), diff --git a/modules/ANSLPR/ANSLPR_OD.h b/modules/ANSLPR/ANSLPR_OD.h index 5e6ee9a..9bb684b 100644 --- a/modules/ANSLPR/ANSLPR_OD.h +++ b/modules/ANSLPR/ANSLPR_OD.h @@ -136,14 +136,19 @@ namespace ANSCENTER "43B1", "68L1", "70G1", "36M1", "81N1", "90K1", "17B1", "64E1", "99D1", "60B2", "74L1", "60C1", "68M1", "63B7", "34B1", "69M1", "24B1", "15M1", "83Y1", "48C1", "95H1", "79X1", "17B6", "36E1", "38K1", "25N1", "25U1", "61B1", "36C1", "36B3", "38F1", "99G1", "69N1", "97D1", "92T1", "92B1", "88B1", "97G1", "14U1", "63A1", "26N1", "19D1", "93C1", "73B1", "84B1", "81K1", "18L1", "64D1", "35M1", "61N1", "83P1", "15S1", "82B1", "92U1", "43D1", "22L1", "63B5", "64G1", "27N1", "14X1", "62C1", "81D1", "38G1", "19F1", "34K1", "49P1", "89H1", "14T1", "19M1", "78D1", "76A1", "66K1", "66C1", "71C1", "37K1", "19G1", "15F1", "85C1", "49B1", "21B1", "89F1", "23M1", "66L1", "90B5", "93M1", "14P1", "77N1", "36B8", "86B1", "12U1", "63B3", "21L1", "36G5", "65G1", "82E1", "61H1", "65H1", "84A1", "23F1", "95C1", "99K1", "49G1", "92D1", "36K3", "92N1", "82X1", "83M1", "11N1", "14K1", "19H1", "93H1", "60A1", "79A1", "20D1", "90D1", "81C1", "66P1", "36K1", "92V1", "18B1", "37P1", "22Y1", "23H1", "26D1", "66G1", "78F1", "49C1", "26H1", "38P1", "47T1", "74H1", "63P1", "47D1", "15D1", "23D1", "68E1", "20B1", "49F1", "43K1", "65K1", "27Z1", "92S1", "79H1", "21E1", "35Y1", "14S1", "75E1", "24Y1", "12T1", "27P1", "77B1", "88H1", "60B3", "23P1", "61F1", "99H1", "23K1", "59A3", "26C1", "81B1", "74E1", "66B1", "22S1", "92P1", "93B1", "69B1", "81P1", "12H1", "62K1", "35A1", "77C1", "27V1", "68N1", "12D1", "64K1", "41A1", "12Z1", "76C1", "38B1", "78G1", "74K1", "69H1", "94A1", "61K1", "86B7", "82G1", "14N1", "82M1", "76E1", "18E1", "61C1", "15N1", "90A1", "77F1", "34D1", "47B1", "62S1", "43E1", "81M1", "92X1", "75B1", "34F1", "70H1", "62B1", "26B1", "60B4", "61A1", "12B1", "90T1", "92E1", "34C1", "47G1", "97B1", "25S1", "70E1", "93Y1", "47S1", "37F1", "28N1", "11K1", "38E1", "78M1", "74C1", "12S1", "75S1", "37A1", "28D1", "65L1", "22B1", "99B1", "74G1", "79K1", "76K1", "76H1", "23B1", "15R1", "36B1", "74D1", "62L1", "37E1", "78E1", "89K1", "26M1", "25F1", "48H1", "79D1", "43H1", "76F1", "36L1", "43L1", "21K1", "88L1", "27S1", "92K1", "77D1", "19N1", "66H1", "36H5", "62N1", "18G1", "75D1", "37L1", "68K1", "28C1", "26E1", "35N1", "85H1", "62D1", "27U1", "19E1", "99E1", "14Y1", "49L1", "66M1", "73F1", "70K1", "36F5", "97H1", "93E1", "68P1", "43F1", "48G1", "75K1", "62U1", "86B9", "65F1", "27L1", "70L1", "63B8", "78L1", "11Z1", "68C1", "18D1", "15L1", "99C1", "49E1", "84E1", "69E1", "38A1", "48D1", "68S1", "81E1", "84K1", "63B6", "24T1", "95A1", "86B4", "34M1", "84L1", "24V1", "14M1", "36H1", "15B1", "69F1", "47E1", "38H1", "88D1", "28E1", "60C2", "63B9", "75Y1", "21D1", "35H1", "68F1", "86B5", "15H1", "36B5", "83X1", "17B7", "12V1", "86B8", "95E1", "63B2", "74F1", "86C1", "48K1", "89M1", "85D1", "71C4", "34E1", "97C1", "88E1", "81F1", "60B5", "84M1", "92H1", "28L1", "34H1", "38X1", "82L1", "61E1", "82F1", "62P1", "93F1", "65B1", "93L1", "95B1", "15P1", "77G1", "28M1", "35B1", "68G1", "36C2", "68D1", "69K1", "14L1", "36M3", "24X1", "24Z1", "86A1", "88C1", "15E1", "77E1", "83E1", "47L1", "25T1", "89C1", "71C3", "49D1", "36L6", "48F1", "36B6", "34P1", "84D1", "15C1", "38M1", "85F1", "77K1", "86B3", "74B1", "78H1", "89G1", "64A2", "15K1", "85B1", "49K1", "21H1", "73C1", "47U1", "65E1", "18C1", "69D1", "63B1", "95G1", "19L1", "20G1", "76D1", "29A1", "68T1", "75L1", "12L1", "89L1", "37C1", "27B1", "19C1", "11H1", "81X1", "70B1", "11V1", "43G1", "22A1", "83C1", "75C1", "79C1", "22F1", "92F1", "81G1", "81T1", "28H1", "66N1", "71B1", "18H1", "76P1", "26F1", "81U1", "34N1", "64F1", "76N1", "24S1", "26P1", "63B4", "35T1", "36N1", "47F1", "81L1", "61G1", "77M1", "34G1", "26G1", "97F1", "62H1", "28F1", "62T1", "93G1", "73D1", "65A1", "47P1", "74P1", "82N1", "20E1", "36D1", "60B1", "49M1", "37H1", "37M1", "38D1", "84F1", "88F1", "36B2", "65C1", "92M1", "86B6", "75H1", "38L1", "20C1", "97E1", "85E1", "38N1", "26K1", "89B1", "99F1", "28B1", "34L1", "86B2", "66F1", "77L1", "27Y1", "68H1", "37D1", "92L1", "82K1", "99A1", "69L1", "76M1", "90B4", "48B1", "95D1", "20H1", "64H1", "79Z1", "92G1", "23G1", "21G1", "37G1", "35K1", "81H1", "83Z1", "76T1", "36F1", "36B4", "14B9", "47K1", "20K1", "62M1", "84H1", "62F1", "74A1", "18A1", "73H1", "37N1", "79N1", "61D1", "11P1", "15G1", "47N1", "19K1", "71C2", "81S1", "11M1", "60B7", "60B8", "62G1", "71A1", "24P1", "69A1", "38C1", "49N1", "21C1", "84G1", "37B1", "72A1", "88K1", "88G1", "83V1", "78C1", "73K1", "78K1", "73E189D1", "67A1", "27X1", "62A1", "18K1", "70F1", "36K5", "19B1", "49H1", "66S1", "12P1" }; ALPRChecker alprChecker; - // Plate identity persistence: accumulated confidence per (trackId, plateText) pair. - // Prevents the same plate string from flickering between different vehicles. - struct PlateTrackIdentity { + // Plate identity persistence: accumulated confidence per spatial plate location. + // Prevents the same plate string from appearing on multiple vehicles. + // Uses bounding box center position (not trackId) as the identity key, + // since trackIds may overlap when ALPR runs on cropped vehicle images. + struct SpatialPlateIdentity { + cv::Point2f center; // plate center in frame coordinates std::string plateText; float accumulatedScore = 0.0f; + int framesSinceLastSeen = 0; }; - // cameraId → { trackId → PlateTrackIdentity } - std::unordered_map> _plateIdentities; + // cameraId → list of tracked plate identities + std::unordered_map> _plateIdentities; + static constexpr float PLATE_SPATIAL_MATCH_THRESHOLD = 0.3f; // IoU threshold for same plate void ensureUniquePlateText(std::vector& results, const std::string& cameraId); std::vector ValidVNCarList = { "94H", "49F", "93A", "20F", "81H", "95R", "38R", "29F", "81F", "28G", "19A", "85B", "2", "43H", "51L", "28C", "21A", "51D", "50F", "24H", "93R", "92H", "71G", "75H", "86G", "30L", "79A", "82B", "79H", "78C", "61E", "70A", "90C", "72G", "34B", "17E", "18E", "78A", "37F", "51E", "71A", "28F", "47E", "83D", "81B", "84C", "71H", "76G", "92E", "36A", "69R", "30M", "27R", "71D", "19B", "34E", "38K", "88G", "68G", "30E", "68E", "25F", "74D", "98K", "89H", "36R", "84D", "61F", "49G", "25H", "17F", "14R", "36H", "47G", "90A", "68A", "83C", "26B", "15B", "61C", "15K", "47H", "78E", "75D", "15C", "63E", "34C", "36F", "38G", "15E", "93F", "22G", "60B", "94D", "62R", "24D", "11R", "12A", "76A", "94C", "97R", "24E", "26A", "15F", "72A", "49H", "62D", "98C", "71B", "61A", "12C", "27A", "78R", "51M", "69E", "76D", "78F", "49R", "81A", "64F", "29D", "18A", "19F", "21E", "92A", "65G", "86E", "62G", "61K", "47A", "23R", "14F", "95D", "36B", "74R", "11H", "24C", "11G", "66D", "63A", "43R", "70F", "86B", "61G", "47M", "67C", "37D", "43G", "14H", "90F", "51G", "86A", "11E", "29K", "85C", "83F", "24B", "98R", "19E", "61B", "90D", "82G", "14K", "74G", "72D", "85A", "19C", "37G", "98E", "74F", "28H", "90E", "89D", "35R", "97H", "83H", "95A", "20C", "65E", "15R", "73C", "37A", "38E", "77G", "94B", "17A", "75R", "98F", "65R", "76R", "20B", "24G", "25B", "73G", "62F", "29G", "77C", "22H", "14D", "23F", "93C", "19R", "15D", "47R", "79D", "60G", "77A", "82C", "63G", "21H", "81E", "25D", "12D", "37R", "36K", "84F", "98G", "28B", "51N", "18F", "50R", "74C", "35C", "30G", "64A", "95F", "18C", "99G", "99B", "37C", "76H", "60K", "67R", "75A", "83R", "28E", "65F", "17D", "92G", "23C", "60R", "90R", "38A", "43D", "50H", "43C", "77H", "47B", "89F", "82F", "65H", "89E", "62C", "24R", "26G", "84E", "17C", "65B", "34A", "12B", "64R", "29H", "71C", "88D", "79F", "76C", "98A", "69H", "22B", "29A", "72R", "67H", "48C", "22D", "60C", "35H", "38H", "63P", "70D", "49D", "18H", "89A", "72E", "92D", "26H", "73R", "85G", "20E", "98H", "69C", "18B", "73B", "22E", "34G", "30K", "20D", "50A", "34D", "15H", "34H", "71E", "62E", "64C", "51R", "82D", "99E", "70R", "18D", "92F", "94R", "24A", "85H", "11C", "73E", "95E", "86C", "94F", "86R", "37K", "23B", "20H", "73D", "95H", "35A", "89B", "82H", "67F", "70H", "97F", "29E", "97A", "51K", "68D", "37B", "82E", "18R", "86H", "35B", "43E", "35F", "95B", "70E", "21D", "27F", "36E", "63D", "68C", "50E", "36G", "75F", "21G", "29B", "93B", "22A", "18G", "43F", "93G", "62A", "83B", "28D", "75C", "22C", "21R", "25E", "23G", "97C", "75E", "79E", "19H", "47K", "65C", "35E", "20R", "68B", "89R", "67A", "75G", "81R", "78B", "77D", "78G", "20K", "36D", "66C", "38F", "27G", "19D", "67B", "84G", "22F", "61D", "20G", "48A", "76F", "48H", "92B", "85R", "26C", "65A", "70B", "38D", "14C", "66A", "73A", "49C", "74E", "68R", "66B", "74A", "49E", "17B", "69D", "51C", "85F", "21F", "99C", "17G", "72H", "94E", "51F", "92R", "60H", "21B", "93D", "19G", "86F", "51A", "66R", "72B", "26D", "64E", "93H", "12H", "97E", "60E", "82A", "60A", "83E", "27D", "64B", "11B", "11D", "76B", "95G", "14A", "61R", "21C", "30F", "23H", "89C", "97G", "62B", "63R", "88B", "98B", "90B", "67G", "69F", "73H", "20A", "72C", "65D", "68H", "51H", "79G", "70C", "90G", "66G", "83A", "77F", "63B", "64G", "25A", "88E", "68F", "99D", "26E", "94A", "48F", "34R", "61H", "90H", "74B", "14G", "12F", "15A", "27E", "69A", "35D", "12E", "85E", "25C", "29M", "89G", "17R", "78D", "84R", "95C", "15G", "28R", "99A", "69G", "48D", "97D", "27C", "78H", "14E", "79R", "73F", "88A", "48E", "48B", "64H", "99R", "14B", "77R", "75B", "88F", "84B", "11A", "67E", "12R", "50M", "11F", "79C", "49A", "43A", "88R", "77E", "48G", "51B", "81D", "74H", "93E", "37H", "88C", "71F", "94G", "38C", "29C", "43B", "30H", "81G", "28A", "26R", "66H", "66E", "17H", "79B", "49B", "63C", "98D", "81C", "69B", "63H", "85D", "26F", "22R", "83G", "37E", "12G", "77B", "35G", "62H", "60D", "60F", "99H", "70G", "76E", "84A", "72F", "25R", "27B", "30A", "47F", "34F", "97B", "23E", "36C", "66F", "48R", "92C", "71R", "23A", "50G", "47C", "82R", "63F", "84H", "38B", "47D", "67D", "25G", "86D", "88H", "64D", "24F", "23D", "99F" }; [[nodiscard]] std::string AnalyseLicensePlateText(const std::string& ocrText);