Fix ALPR Batch and memory leak

This commit is contained in:
2026-04-15 09:23:05 +10:00
parent 7778f8c214
commit b05c49ad93
9 changed files with 686 additions and 83 deletions

View File

@@ -953,6 +953,163 @@ namespace ANSCENTER
return {};
}
// ── Stateless batched inference for pipeline mode ───────────────────
// Caller supplies a full frame + a list of vehicle ROIs in FRAME
// coordinates. We run ONE LP-detect call across all vehicle crops and
// ONE text-recognizer call across every resulting plate (with the same
// 2-row split heuristic as ANSALPR_OCR::RunInference), and NO tracker,
// voting, spatial dedup, or per-camera accumulating state. This is the
// drop-in replacement for the per-bbox loop inside
// ANSALPR_RunInferencesComplete_LV (pipeline mode) and is exported as
// ANSALPR_RunInferencesBatch_LV / _V2 in dllmain.cpp. Calling this on
// ANSALPR_OCR avoids the ORT/TRT per-shape allocator churn that
// causes unbounded memory growth when the loop version is used.
std::vector<Object> ANSALPR_OCR::RunInferencesBatch(
const cv::Mat& input,
const std::vector<cv::Rect>& vehicleBoxes,
const std::string& cameraId)
{
if (!_licenseValid) {
this->_logger.LogError("ANSALPR_OCR::RunInferencesBatch", "Invalid license", __FILE__, __LINE__);
return {};
}
if (!_isInitialized) {
this->_logger.LogError("ANSALPR_OCR::RunInferencesBatch", "Model is not initialized", __FILE__, __LINE__);
return {};
}
if (input.empty() || input.cols < 5 || input.rows < 5) return {};
if (!_lpDetector) {
this->_logger.LogFatal("ANSALPR_OCR::RunInferencesBatch", "_lpDetector is null", __FILE__, __LINE__);
return {};
}
if (!_ocrEngine) {
this->_logger.LogFatal("ANSALPR_OCR::RunInferencesBatch", "_ocrEngine is null", __FILE__, __LINE__);
return {};
}
if (vehicleBoxes.empty()) return {};
try {
// Promote grayscale input to BGR once (matches RunInference).
cv::Mat localFrame;
if (input.channels() == 1) {
cv::cvtColor(input, localFrame, cv::COLOR_GRAY2BGR);
}
const cv::Mat& frame = (input.channels() == 1) ? localFrame : input;
// ── 1. Clamp and crop vehicle ROIs ────────────────────────
const cv::Rect frameRect(0, 0, frame.cols, frame.rows);
std::vector<cv::Mat> vehicleCrops;
std::vector<cv::Rect> clamped;
vehicleCrops.reserve(vehicleBoxes.size());
clamped.reserve(vehicleBoxes.size());
for (const auto& r : vehicleBoxes) {
cv::Rect c = r & frameRect;
if (c.width <= 5 || c.height <= 5) continue;
vehicleCrops.emplace_back(frame(c));
clamped.push_back(c);
}
if (vehicleCrops.empty()) return {};
// ── 2. ONE batched LP detection call across all vehicles ──
std::vector<std::vector<Object>> lpBatch =
_lpDetector->RunInferencesBatch(vehicleCrops, cameraId);
// ── 3. Flatten plates, splitting 2-row plates into top/bot ─
// Same aspect-ratio heuristic as ANSALPR_OCR::RunInference
// (lines ~820-870): narrow plates (aspect < 2.0) are split
// horizontally into two recognizer crops, wide plates stay as
// one. The recMap lets us stitch the per-crop OCR outputs
// back into per-plate combined strings.
struct PlateMeta {
size_t vehIdx; // index into vehicleCrops / clamped
Object lpObj; // LP detection in VEHICLE-local coords
cv::Mat plateROI; // full plate crop (kept for colour)
std::vector<size_t> cropIndices; // indices into allCrops below
};
std::vector<cv::Mat> allCrops;
std::vector<PlateMeta> metas;
allCrops.reserve(lpBatch.size() * 2);
metas.reserve(lpBatch.size());
for (size_t v = 0; v < lpBatch.size() && v < vehicleCrops.size(); ++v) {
const cv::Mat& veh = vehicleCrops[v];
const cv::Rect vehRect(0, 0, veh.cols, veh.rows);
for (const auto& lp : lpBatch[v]) {
cv::Rect lpBox = lp.box & vehRect;
if (lpBox.width <= 0 || lpBox.height <= 0) continue;
cv::Mat plateROI = veh(lpBox);
PlateMeta pm;
pm.vehIdx = v;
pm.lpObj = lp;
pm.plateROI = plateROI;
const float aspect =
static_cast<float>(plateROI.cols) /
std::max(1, plateROI.rows);
if (aspect < 2.0f && plateROI.rows >= 24) {
const int halfH = plateROI.rows / 2;
pm.cropIndices.push_back(allCrops.size());
allCrops.push_back(plateROI(cv::Rect(0, 0, plateROI.cols, halfH)));
pm.cropIndices.push_back(allCrops.size());
allCrops.push_back(plateROI(cv::Rect(0, halfH, plateROI.cols, plateROI.rows - halfH)));
} else {
pm.cropIndices.push_back(allCrops.size());
allCrops.push_back(plateROI);
}
metas.push_back(std::move(pm));
}
}
if (allCrops.empty()) return {};
// ── 4. ONE batched recognizer call across every plate ────
// ONNXOCRRecognizer buckets by width internally, so this is
// typically 1-2 ORT Runs regardless of plate count.
auto ocrResults = _ocrEngine->RecognizeTextBatch(allCrops);
// ── 5. Assemble — NO tracker, NO voting, NO dedup ────────
std::vector<Object> output;
output.reserve(metas.size());
for (const auto& pm : metas) {
std::string combined;
for (size_t c : pm.cropIndices) {
if (c >= ocrResults.size()) continue;
const std::string& line = ocrResults[c].first;
if (line.empty()) continue;
if (!combined.empty()) combined += " ";
combined += line;
}
if (combined.empty()) continue;
Object out = pm.lpObj;
out.className = combined; // raw OCR — no ALPRChecker
out.cameraId = cameraId;
out.box.x += clamped[pm.vehIdx].x;
out.box.y += clamped[pm.vehIdx].y;
// Colour lookup — text-keyed cache, bounded.
std::string colour = DetectLPColourCached(
pm.plateROI, cameraId, out.className);
if (!colour.empty()) out.extraInfo = "color:" + colour;
output.push_back(std::move(out));
}
return output;
}
catch (const cv::Exception& e) {
this->_logger.LogFatal("ANSALPR_OCR::RunInferencesBatch",
std::string("OpenCV Exception: ") + e.what(), __FILE__, __LINE__);
}
catch (const std::exception& e) {
this->_logger.LogFatal("ANSALPR_OCR::RunInferencesBatch",
e.what(), __FILE__, __LINE__);
}
catch (...) {
this->_logger.LogFatal("ANSALPR_OCR::RunInferencesBatch",
"Unknown exception occurred", __FILE__, __LINE__);
}
return {};
}
// ── Inference wrappers ───────────────────────────────────────────────
bool ANSALPR_OCR::Inference(const cv::Mat& input, std::string& lprResult) {
if (input.empty()) return false;