Fix ALPR Batch and memory leak
This commit is contained in:
@@ -304,11 +304,18 @@ namespace ANSCENTER {
|
||||
p.framesSinceLastSeen++;
|
||||
}
|
||||
|
||||
// Periodic pruning: remove stale entries
|
||||
static thread_local int pruneCounterById = 0;
|
||||
pruneCounterById++;
|
||||
if (pruneCounterById >= 30 && plates.size() > 20) {
|
||||
pruneCounterById = 0;
|
||||
// Periodic pruning: remove stale entries.
|
||||
// NOTE: previously used `static thread_local int pruneCounterById`.
|
||||
// LabVIEW dispatches calls across a worker-thread pool, so the
|
||||
// thread-local counter fragmented across threads and pruning
|
||||
// effectively stopped firing — `trackedPlatesById[cameraId]` then
|
||||
// grew unbounded (one entry per frame when trackIds never repeat),
|
||||
// which manifested as a slow LabVIEW-side memory leak. The counter
|
||||
// is now a plain instance member guarded by `_mutex` (taken above),
|
||||
// so every 30th call across the whole engine triggers a prune pass.
|
||||
_pruneCounterById++;
|
||||
if (_pruneCounterById >= 30 && plates.size() > 20) {
|
||||
_pruneCounterById = 0;
|
||||
int staleThreshold = maxFrames * 3;
|
||||
for (auto it = plates.begin(); it != plates.end(); ) {
|
||||
if (it->second.framesSinceLastSeen > staleThreshold) {
|
||||
@@ -521,6 +528,36 @@ namespace ANSCENTER {
|
||||
ANSALPR::~ANSALPR(){};
|
||||
bool ANSALPR::Destroy() { return true; };
|
||||
|
||||
// Default batch implementation — fallback that loops RunInference over
|
||||
// vehicle crops one at a time. Subclasses should override this with a
|
||||
// true batched path (see ANSALPR_OD::RunInferencesBatch and
|
||||
// ANSALPR_OCR::RunInferencesBatch) to issue a single LP-detect and a
|
||||
// single OCR call per frame instead of N. The fallback preserves the
|
||||
// "transform bboxes back to frame coordinates" contract so a subclass
|
||||
// that never overrides still produces valid output.
|
||||
std::vector<Object> ANSALPR::RunInferencesBatch(
|
||||
const cv::Mat& input,
|
||||
const std::vector<cv::Rect>& vehicleBoxes,
|
||||
const std::string& cameraId)
|
||||
{
|
||||
std::vector<Object> out;
|
||||
if (input.empty() || vehicleBoxes.empty()) return out;
|
||||
const cv::Rect frameRect(0, 0, input.cols, input.rows);
|
||||
out.reserve(vehicleBoxes.size());
|
||||
for (const auto& r : vehicleBoxes) {
|
||||
cv::Rect c = r & frameRect;
|
||||
if (c.width <= 0 || c.height <= 0) continue;
|
||||
const cv::Mat crop = input(c);
|
||||
std::vector<Object> perVehicle = RunInference(crop, cameraId);
|
||||
for (auto& obj : perVehicle) {
|
||||
obj.box.x += c.x;
|
||||
obj.box.y += c.y;
|
||||
out.push_back(std::move(obj));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> ANSCENTER::ANSALPR::GetBoundingBoxes(const std::string& strBBoxes) {
|
||||
std::vector<cv::Rect> bBoxes;
|
||||
bBoxes.clear();
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace ANSCENTER
|
||||
};
|
||||
// cameraId → (trackId → tracked plate)
|
||||
std::unordered_map<std::string, std::unordered_map<int, TrackedPlateById>> trackedPlatesById;
|
||||
int _pruneCounterById = 0; // counts checkPlateByTrackId calls for periodic pruning
|
||||
|
||||
public:
|
||||
void Init(int framesToStore = MAX_ALPR_FRAME);
|
||||
@@ -100,6 +101,28 @@ namespace ANSCENTER
|
||||
[[nodiscard]] virtual bool Inference(const cv::Mat& input, const std::vector<cv::Rect> & Bbox, std::string& lprResult) = 0;
|
||||
[[nodiscard]] virtual bool Inference(const cv::Mat& input, const std::vector<cv::Rect> & Bbox, std::string& lprResult,const std::string & cameraId) = 0;
|
||||
[[nodiscard]] virtual std::vector<Object> RunInference(const cv::Mat& input, const std::string &cameraId) = 0;
|
||||
|
||||
/// Stateless batch inference for pipeline mode.
|
||||
/// For each vehicle ROI in `vehicleBoxes` (in FRAME coordinates), crop
|
||||
/// the vehicle, run LP detection and text recognition, and return
|
||||
/// detected plates in FULL-FRAME coordinates.
|
||||
///
|
||||
/// Tracker, voting, spatial dedup, and per-camera accumulating state
|
||||
/// are all bypassed — this is the fast-path for callers that already
|
||||
/// have precise vehicle bboxes and want raw per-frame results with no
|
||||
/// cross-frame memory. The implementation issues a single batched
|
||||
/// `_lpDetector->RunInferencesBatch` call for detection and a single
|
||||
/// batched recognizer call for OCR, so the ORT/TRT allocator sees
|
||||
/// exactly one shape per frame regardless of how many vehicles the
|
||||
/// caller passes.
|
||||
///
|
||||
/// Default implementation falls back to calling `RunInference` in a
|
||||
/// loop per crop so older subclasses keep compiling.
|
||||
[[nodiscard]] virtual std::vector<Object> RunInferencesBatch(
|
||||
const cv::Mat& input,
|
||||
const std::vector<cv::Rect>& vehicleBoxes,
|
||||
const std::string& cameraId);
|
||||
|
||||
[[nodiscard]] std::string VectorDetectionToJsonString(const std::vector<Object>& dets);
|
||||
void SetPlateFormats(const std::vector<std::string>& formats);
|
||||
void SetPlateFormat(const std::string& format);
|
||||
@@ -167,6 +190,17 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV(ANSCENTER::ANSALPR
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_CPP(ANSCENTER::ANSALPR** Handle, cv::Mat** cvImage, const char* cameraId, int getJpegString, int jpegImageSize,std::string& detectionResult, std::string& imageStr);
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV(ANSCENTER::ANSALPR** Handle, cv::Mat** cvImage, const char* cameraId, int maxImageSize,const char* strBboxes, LStrHandle detectionResult);
|
||||
|
||||
// Dedicated pipeline-mode batch inference:
|
||||
// - always runs with tracker OFF, voting OFF, spatial dedup OFF
|
||||
// - issues ONE batched LP-detect call and ONE batched recognizer call
|
||||
// across every vehicle in `strBboxes`, instead of looping per crop
|
||||
// - returns plate bboxes in the caller's resized coordinate space (same
|
||||
// semantics as ANSALPR_RunInferencesComplete_LV)
|
||||
// Use this in LabVIEW whenever the caller already has vehicle bboxes and
|
||||
// wants raw per-frame results. Fixes the cross-frame allocator churn that
|
||||
// causes ANSALPR_OCR memory growth under the per-bbox loop path.
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferencesBatch_LV(ANSCENTER::ANSALPR** Handle, cv::Mat** cvImage, const char* cameraId, int maxImageSize, const char* strBboxes, LStrHandle detectionResult);
|
||||
|
||||
// Get/Set format
|
||||
extern "C" ANSLPR_API int ANSALPR_SetFormat(ANSCENTER::ANSALPR** Handle, const char* format);
|
||||
extern "C" ANSLPR_API int ANSALPR_SetFormats(ANSCENTER::ANSALPR** Handle, const char* formats);// comma separated formats
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -135,6 +135,10 @@ namespace ANSCENTER
|
||||
[[nodiscard]] bool Inference(const cv::Mat& input, const std::vector<cv::Rect>& Bbox, std::string& lprResult) override;
|
||||
[[nodiscard]] bool Inference(const cv::Mat& input, const std::vector<cv::Rect>& Bbox, std::string& lprResult, const std::string& cameraId) override;
|
||||
[[nodiscard]] std::vector<Object> RunInference(const cv::Mat& input, const std::string& cameraId) override;
|
||||
[[nodiscard]] std::vector<Object> RunInferencesBatch(
|
||||
const cv::Mat& input,
|
||||
const std::vector<cv::Rect>& vehicleBoxes,
|
||||
const std::string& cameraId) override;
|
||||
[[nodiscard]] bool Destroy() override;
|
||||
|
||||
/// Propagate country to inner OCR engine so ALPR post-processing
|
||||
|
||||
@@ -2617,6 +2617,124 @@ 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 char-OCR batch across every resulting plate, with 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.
|
||||
std::vector<Object> ANSALPR_OD::RunInferencesBatch(
|
||||
const cv::Mat& input,
|
||||
const std::vector<cv::Rect>& vehicleBoxes,
|
||||
const std::string& cameraId)
|
||||
{
|
||||
if (!_licenseValid || !valid || !_isInitialized) {
|
||||
this->_logger.LogWarn("ANSALPR_OD::RunInferencesBatch",
|
||||
"Invalid state: license=" + std::to_string(_licenseValid) +
|
||||
" valid=" + std::to_string(valid) +
|
||||
" init=" + std::to_string(_isInitialized), __FILE__, __LINE__);
|
||||
return {};
|
||||
}
|
||||
if (input.empty() || input.cols < 5 || input.rows < 5) return {};
|
||||
if (!this->_lpDetector || !this->_ocrDetector) {
|
||||
this->_logger.LogFatal("ANSALPR_OD::RunInferencesBatch",
|
||||
"Detector instances are null", __FILE__, __LINE__);
|
||||
return {};
|
||||
}
|
||||
if (vehicleBoxes.empty()) return {};
|
||||
|
||||
try {
|
||||
// ── 1. Clamp and crop vehicle ROIs ────────────────────────
|
||||
const cv::Rect frameRect(0, 0, input.cols, input.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(input(c));
|
||||
clamped.push_back(c);
|
||||
}
|
||||
if (vehicleCrops.empty()) return {};
|
||||
|
||||
// ── 2. ONE batched LP detection call across all vehicles ──
|
||||
// ANSODBase::RunInferencesBatch is fixed-shape YOLO, so this
|
||||
// is a single ORT/TRT call regardless of how many vehicles
|
||||
// the caller passed.
|
||||
std::vector<std::vector<Object>> lpBatch =
|
||||
_lpDetector->RunInferencesBatch(vehicleCrops, cameraId);
|
||||
|
||||
// ── 3. Flatten detected plates, keeping back-reference ───
|
||||
struct PlateMeta {
|
||||
size_t vehIdx; // index into vehicleCrops / clamped
|
||||
Object lpObj; // LP detection in VEHICLE-local coords
|
||||
};
|
||||
std::vector<cv::Mat> alignedLPRBatch;
|
||||
std::vector<PlateMeta> metas;
|
||||
alignedLPRBatch.reserve(lpBatch.size() * 2);
|
||||
metas.reserve(lpBatch.size() * 2);
|
||||
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;
|
||||
alignedLPRBatch.emplace_back(veh(lpBox));
|
||||
metas.push_back({ v, lp });
|
||||
}
|
||||
}
|
||||
if (alignedLPRBatch.empty()) return {};
|
||||
|
||||
// ── 4. ONE batched char-OCR call across every plate ──────
|
||||
std::vector<std::string> ocrTextBatch =
|
||||
DetectLicensePlateStringBatch(alignedLPRBatch, cameraId);
|
||||
if (ocrTextBatch.size() != alignedLPRBatch.size()) {
|
||||
this->_logger.LogWarn("ANSALPR_OD::RunInferencesBatch",
|
||||
"Char OCR batch size mismatch", __FILE__, __LINE__);
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── 5. Assemble — NO tracker, NO voting, NO dedup ────────
|
||||
std::vector<Object> output;
|
||||
output.reserve(alignedLPRBatch.size());
|
||||
for (size_t i = 0; i < alignedLPRBatch.size(); ++i) {
|
||||
const std::string& text = ocrTextBatch[i];
|
||||
if (text.empty()) continue;
|
||||
|
||||
Object out = metas[i].lpObj;
|
||||
out.className = text; // raw OCR — no ALPRChecker
|
||||
out.cameraId = cameraId;
|
||||
out.box.x += clamped[metas[i].vehIdx].x;
|
||||
out.box.y += clamped[metas[i].vehIdx].y;
|
||||
|
||||
// Colour lookup — uses text-keyed cache, bounded by
|
||||
// COLOUR_CACHE_MAX_SIZE, no per-frame growth.
|
||||
std::string colour = DetectLPColourCached(
|
||||
alignedLPRBatch[i], 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_OD::RunInferencesBatch",
|
||||
std::string("OpenCV Exception: ") + e.what(), __FILE__, __LINE__);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
this->_logger.LogFatal("ANSALPR_OD::RunInferencesBatch",
|
||||
e.what(), __FILE__, __LINE__);
|
||||
}
|
||||
catch (...) {
|
||||
this->_logger.LogFatal("ANSALPR_OD::RunInferencesBatch",
|
||||
"Unknown exception occurred", __FILE__, __LINE__);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> ANSALPR_OD::DetectLPColourDetectorBatch(const std::vector<cv::Mat>& lprROIs, const std::string& cameraId) {
|
||||
// Early validation - no lock needed for immutable config
|
||||
if (_lpColourModelConfig.detectionScoreThreshold <= 0.0f || !_lpColourDetector) {
|
||||
|
||||
@@ -234,6 +234,10 @@ namespace ANSCENTER
|
||||
[[nodiscard]] bool Inference(const cv::Mat& input, const std::vector<cv::Rect> & Bbox, std::string& lprResult, const std::string & cameraId) override;
|
||||
[[nodiscard]] std::vector<Object> RunInferenceSingleFrame(const cv::Mat& input, const std::string& cameraId);
|
||||
[[nodiscard]] std::vector<Object> RunInference(const cv::Mat& input, const std::string& cameraId) override;
|
||||
[[nodiscard]] std::vector<Object> RunInferencesBatch(
|
||||
const cv::Mat& input,
|
||||
const std::vector<cv::Rect>& vehicleBoxes,
|
||||
const std::string& cameraId) override;
|
||||
[[nodiscard]] std::vector<std::string> DetectLicensePlateStringBatch(const std::vector<cv::Mat>& lprROIs, const std::string& cameraId);
|
||||
[[nodiscard]] std::vector<std::string> DetectLPColourDetectorBatch(const std::vector<cv::Mat>& lprROIs, const std::string& cameraId);
|
||||
[[nodiscard]] std::string ProcessSingleOCRResult(const std::vector<Object>& ocrOutput);
|
||||
|
||||
@@ -104,6 +104,19 @@ public:
|
||||
ALPRHandleGuard& operator=(const ALPRHandleGuard&) = delete;
|
||||
};
|
||||
|
||||
// RAII guard — sets the per-thread current GPU frame pointer and always
|
||||
// clears it on scope exit, even if the wrapped inference call throws.
|
||||
// Without this, a throwing RunInference leaves tl_currentGpuFrame pointing
|
||||
// at a GpuFrameData that may be freed before the next call on this thread,
|
||||
// causing use-after-free or stale NV12 data on subsequent frames.
|
||||
class GpuFrameScope {
|
||||
public:
|
||||
explicit GpuFrameScope(GpuFrameData* f) { tl_currentGpuFrame() = f; }
|
||||
~GpuFrameScope() { tl_currentGpuFrame() = nullptr; }
|
||||
GpuFrameScope(const GpuFrameScope&) = delete;
|
||||
GpuFrameScope& operator=(const GpuFrameScope&) = delete;
|
||||
};
|
||||
|
||||
BOOL APIENTRY DllMain( HMODULE hModule,
|
||||
DWORD ul_reason_for_call,
|
||||
LPVOID lpReserved
|
||||
@@ -465,9 +478,6 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV(
|
||||
|
||||
try {
|
||||
const cv::Mat& localImage = **cvImage; // No clone — RunInference takes const ref
|
||||
// Set thread-local NV12 frame data for fast-path inference
|
||||
// Cleared after first RunInference to prevent NV12 mismatch on cropped sub-images (OCR, etc.)
|
||||
tl_currentGpuFrame() = ANSGpuFrameRegistry::instance().lookup(*cvImage);
|
||||
|
||||
int originalWidth = localImage.cols;
|
||||
int originalHeight = localImage.rows;
|
||||
@@ -476,8 +486,13 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV(
|
||||
return -2;
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> outputs = engine->RunInference(localImage, cameraId);
|
||||
tl_currentGpuFrame() = nullptr;
|
||||
std::vector<ANSCENTER::Object> outputs;
|
||||
{
|
||||
// Scoped NV12 fast-path pointer; cleared on any exit path (normal or
|
||||
// throw) so the next call on this thread cannot see a stale frame.
|
||||
GpuFrameScope _gfs(ANSGpuFrameRegistry::instance().lookup(*cvImage));
|
||||
outputs = engine->RunInference(localImage, cameraId);
|
||||
}
|
||||
|
||||
bool getJpeg = (getJpegString == 1);
|
||||
std::string stImage;
|
||||
@@ -567,15 +582,17 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_CPP(ANSCENTER::ANSALP
|
||||
|
||||
try {
|
||||
const cv::Mat& localImage = **cvImage; // No clone — RunInference takes const ref
|
||||
// Set thread-local NV12 frame data for fast-path inference
|
||||
// Cleared after first RunInference to prevent NV12 mismatch on cropped sub-images (OCR, etc.)
|
||||
tl_currentGpuFrame() = ANSGpuFrameRegistry::instance().lookup(*cvImage);
|
||||
|
||||
int originalWidth = localImage.cols;
|
||||
int originalHeight = localImage.rows;
|
||||
int maxImageSize = originalWidth;
|
||||
|
||||
std::vector<ANSCENTER::Object> outputs = engine->RunInference(localImage, cameraId);
|
||||
std::vector<ANSCENTER::Object> outputs;
|
||||
{
|
||||
// Scoped NV12 fast-path pointer; cleared on any exit path (normal or throw).
|
||||
GpuFrameScope _gfs(ANSGpuFrameRegistry::instance().lookup(*cvImage));
|
||||
outputs = engine->RunInference(localImage, cameraId);
|
||||
}
|
||||
bool getJpeg = (getJpegString == 1);
|
||||
std::string stImage;
|
||||
|
||||
@@ -646,9 +663,6 @@ extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV(
|
||||
|
||||
try {
|
||||
const cv::Mat& localImage = **cvImage; // No clone — RunInference takes const ref
|
||||
// Set thread-local NV12 frame data for fast-path inference
|
||||
// Cleared after first RunInference to prevent NV12 mismatch on cropped sub-images (OCR, etc.)
|
||||
tl_currentGpuFrame() = ANSGpuFrameRegistry::instance().lookup(*cvImage);
|
||||
|
||||
std::vector<ANSCENTER::Object> objectDetectionResults;
|
||||
std::vector<cv::Rect> bBox = ANSCENTER::ANSALPR::GetBoundingBoxes(strBboxes);
|
||||
@@ -659,10 +673,14 @@ extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV(
|
||||
const double scaleFactor = (maxImageSize > 0) ? static_cast<double>(originalWidth) / maxImageSize : 1.0;
|
||||
|
||||
if (bBox.empty()) {
|
||||
// Full-frame path: NV12 fast-path is only safe for the full-frame
|
||||
// inference call. Scope the TL pointer so it is cleared on any exit
|
||||
// path (normal or thrown) and is NOT seen by any subsequent
|
||||
// cropped-image inference (which would mismatch the NV12 cache).
|
||||
GpuFrameScope _gfs(ANSGpuFrameRegistry::instance().lookup(*cvImage));
|
||||
objectDetectionResults = engine->RunInference(localImage, cameraId);
|
||||
}
|
||||
tl_currentGpuFrame() = nullptr; // Clear before crop-based inference
|
||||
if (!bBox.empty()) {
|
||||
else {
|
||||
for (const auto& rect : bBox) {
|
||||
cv::Rect scaledRect;
|
||||
scaledRect.x = static_cast<int>(rect.x * scaleFactor);
|
||||
@@ -708,6 +726,103 @@ extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV(
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated pipeline-mode export. Unlike ANSALPR_RunInferencesComplete_LV
|
||||
// this function:
|
||||
// 1. Always runs with tracker OFF, voting OFF, dedup OFF — it targets
|
||||
// callers that already have precise per-vehicle bboxes and want raw,
|
||||
// stateless results.
|
||||
// 2. Issues ONE batched LP-detect call and ONE batched recognizer call
|
||||
// per frame via ANSALPR::RunInferencesBatch, instead of looping
|
||||
// engine->RunInference once per crop. This eliminates the per-shape
|
||||
// ORT/TRT allocator churn that causes ANSALPR_OCR memory growth when
|
||||
// the legacy pipeline-mode loop is used under LabVIEW worker threads.
|
||||
// 3. Does NOT touch tl_currentGpuFrame — the caller is working with
|
||||
// cropped regions, not the NV12-keyed source Mat, so the fast-path
|
||||
// pointer is meaningless here. Eliminates a class of UAF risk.
|
||||
// Output coordinates are rescaled back into the caller's resized space,
|
||||
// matching the convention used by ANSALPR_RunInferencesComplete_LV.
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferencesBatch_LV(
|
||||
ANSCENTER::ANSALPR** Handle,
|
||||
cv::Mat** cvImage,
|
||||
const char* cameraId,
|
||||
int maxImageSize,
|
||||
const char* strBboxes,
|
||||
LStrHandle detectionResult)
|
||||
{
|
||||
if (!Handle || !*Handle) return -1;
|
||||
if (!cvImage || !(*cvImage) || (*cvImage)->empty()) return -2;
|
||||
|
||||
ALPRHandleGuard guard(AcquireALPRHandle(*Handle));
|
||||
if (!guard) return -3;
|
||||
auto* engine = guard.get();
|
||||
|
||||
try {
|
||||
const cv::Mat& frame = **cvImage;
|
||||
const int frameW = frame.cols;
|
||||
const int frameH = frame.rows;
|
||||
if (frameW <= 0 || frameH <= 0) return -2;
|
||||
|
||||
// Same scaling convention as ANSALPR_RunInferencesComplete_LV:
|
||||
// the bboxes in `strBboxes` are in a resized coordinate space;
|
||||
// scale them up to the full frame for the crop, then rescale the
|
||||
// plate outputs back to the caller's space.
|
||||
const double scale =
|
||||
(maxImageSize > 0) ? static_cast<double>(frameW) / maxImageSize : 1.0;
|
||||
|
||||
std::vector<cv::Rect> rawBoxes = ANSCENTER::ANSALPR::GetBoundingBoxes(strBboxes);
|
||||
std::vector<cv::Rect> scaledBoxes;
|
||||
scaledBoxes.reserve(rawBoxes.size());
|
||||
const cv::Rect frameRect(0, 0, frameW, frameH);
|
||||
for (const auto& r : rawBoxes) {
|
||||
cv::Rect s(
|
||||
static_cast<int>(r.x * scale),
|
||||
static_cast<int>(r.y * scale),
|
||||
static_cast<int>(r.width * scale),
|
||||
static_cast<int>(r.height * scale));
|
||||
s &= frameRect;
|
||||
if (s.width > 0 && s.height > 0) scaledBoxes.push_back(s);
|
||||
}
|
||||
|
||||
// Empty bbox list → fall through to full-frame RunInference so
|
||||
// existing LabVIEW code that passes "" still works, matching the
|
||||
// behaviour of ANSALPR_RunInferencesComplete_LV.
|
||||
std::vector<ANSCENTER::Object> results;
|
||||
if (scaledBoxes.empty()) {
|
||||
results = engine->RunInference(frame, cameraId);
|
||||
} else {
|
||||
results = engine->RunInferencesBatch(frame, scaledBoxes, cameraId);
|
||||
}
|
||||
|
||||
// Rescale plate boxes back into the caller's resized space.
|
||||
if (scale != 1.0) {
|
||||
const double inv = 1.0 / scale;
|
||||
for (auto& o : results) {
|
||||
o.box.x = static_cast<int>(o.box.x * inv);
|
||||
o.box.y = static_cast<int>(o.box.y * inv);
|
||||
o.box.width = static_cast<int>(o.box.width * inv);
|
||||
o.box.height = static_cast<int>(o.box.height * inv);
|
||||
}
|
||||
}
|
||||
|
||||
std::string json = engine->VectorDetectionToJsonString(results);
|
||||
if (json.empty()) return 0;
|
||||
|
||||
const int size = static_cast<int>(json.length());
|
||||
MgErr err = DSSetHandleSize(detectionResult, sizeof(int32) + size * sizeof(uChar));
|
||||
if (err != noErr) return 0;
|
||||
|
||||
(*detectionResult)->cnt = size;
|
||||
memcpy((*detectionResult)->str, json.c_str(), size);
|
||||
return 1;
|
||||
}
|
||||
catch (const std::exception& /*ex*/) {
|
||||
return 0;
|
||||
}
|
||||
catch (...) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern "C" ANSLPR_API int ANSALPR_SetFormat(ANSCENTER::ANSALPR** Handle, const char* format) {
|
||||
if (!Handle || !*Handle) return -1;
|
||||
@@ -1042,9 +1157,6 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV_V2(
|
||||
|
||||
try {
|
||||
const cv::Mat& localImage = **cvImage; // No clone — RunInference takes const ref
|
||||
// Set thread-local NV12 frame data for fast-path inference
|
||||
// Cleared after first RunInference to prevent NV12 mismatch on cropped sub-images (OCR, etc.)
|
||||
tl_currentGpuFrame() = ANSGpuFrameRegistry::instance().lookup(*cvImage);
|
||||
|
||||
int originalWidth = localImage.cols;
|
||||
int originalHeight = localImage.rows;
|
||||
@@ -1053,8 +1165,12 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV_V2(
|
||||
return -2;
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> outputs = engine->RunInference(localImage, cameraId);
|
||||
tl_currentGpuFrame() = nullptr;
|
||||
std::vector<ANSCENTER::Object> outputs;
|
||||
{
|
||||
// Scoped NV12 fast-path pointer; cleared on any exit path (normal or throw).
|
||||
GpuFrameScope _gfs(ANSGpuFrameRegistry::instance().lookup(*cvImage));
|
||||
outputs = engine->RunInference(localImage, cameraId);
|
||||
}
|
||||
|
||||
bool getJpeg = (getJpegString == 1);
|
||||
std::string stImage;
|
||||
@@ -1132,6 +1248,85 @@ extern "C" ANSLPR_API int ANSALPR_RunInferenceComplete_LV_V2(
|
||||
}
|
||||
}
|
||||
|
||||
// V2 uint64_t handle variant of ANSALPR_RunInferencesBatch_LV.
|
||||
// See the non-V2 version for semantics — identical behaviour, differs only
|
||||
// in how the caller passes the ALPR engine handle (by value instead of via
|
||||
// a LabVIEW Handle** pointer-to-pointer).
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferencesBatch_LV_V2(
|
||||
uint64_t handleVal,
|
||||
cv::Mat** cvImage,
|
||||
const char* cameraId,
|
||||
int maxImageSize,
|
||||
const char* strBboxes,
|
||||
LStrHandle detectionResult)
|
||||
{
|
||||
ANSCENTER::ANSALPR* _v2Direct = reinterpret_cast<ANSCENTER::ANSALPR*>(handleVal);
|
||||
if (_v2Direct == nullptr) return -1;
|
||||
if (!cvImage || !(*cvImage) || (*cvImage)->empty()) return -2;
|
||||
|
||||
ALPRHandleGuard guard(AcquireALPRHandle(_v2Direct));
|
||||
if (!guard) return -3;
|
||||
auto* engine = guard.get();
|
||||
|
||||
try {
|
||||
const cv::Mat& frame = **cvImage;
|
||||
const int frameW = frame.cols;
|
||||
const int frameH = frame.rows;
|
||||
if (frameW <= 0 || frameH <= 0) return -2;
|
||||
|
||||
const double scale =
|
||||
(maxImageSize > 0) ? static_cast<double>(frameW) / maxImageSize : 1.0;
|
||||
|
||||
std::vector<cv::Rect> rawBoxes = ANSCENTER::ANSALPR::GetBoundingBoxes(strBboxes);
|
||||
std::vector<cv::Rect> scaledBoxes;
|
||||
scaledBoxes.reserve(rawBoxes.size());
|
||||
const cv::Rect frameRect(0, 0, frameW, frameH);
|
||||
for (const auto& r : rawBoxes) {
|
||||
cv::Rect s(
|
||||
static_cast<int>(r.x * scale),
|
||||
static_cast<int>(r.y * scale),
|
||||
static_cast<int>(r.width * scale),
|
||||
static_cast<int>(r.height * scale));
|
||||
s &= frameRect;
|
||||
if (s.width > 0 && s.height > 0) scaledBoxes.push_back(s);
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> results;
|
||||
if (scaledBoxes.empty()) {
|
||||
results = engine->RunInference(frame, cameraId);
|
||||
} else {
|
||||
results = engine->RunInferencesBatch(frame, scaledBoxes, cameraId);
|
||||
}
|
||||
|
||||
if (scale != 1.0) {
|
||||
const double inv = 1.0 / scale;
|
||||
for (auto& o : results) {
|
||||
o.box.x = static_cast<int>(o.box.x * inv);
|
||||
o.box.y = static_cast<int>(o.box.y * inv);
|
||||
o.box.width = static_cast<int>(o.box.width * inv);
|
||||
o.box.height = static_cast<int>(o.box.height * inv);
|
||||
}
|
||||
}
|
||||
|
||||
std::string json = engine->VectorDetectionToJsonString(results);
|
||||
if (json.empty()) return 0;
|
||||
|
||||
const int size = static_cast<int>(json.length());
|
||||
MgErr err = DSSetHandleSize(detectionResult, sizeof(int32) + size * sizeof(uChar));
|
||||
if (err != noErr) return 0;
|
||||
|
||||
(*detectionResult)->cnt = size;
|
||||
memcpy((*detectionResult)->str, json.c_str(), size);
|
||||
return 1;
|
||||
}
|
||||
catch (const std::exception& /*ex*/) {
|
||||
return 0;
|
||||
}
|
||||
catch (...) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV_V2(
|
||||
uint64_t handleVal,
|
||||
cv::Mat** cvImage,
|
||||
@@ -1150,9 +1345,6 @@ extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV_V2(
|
||||
|
||||
try {
|
||||
const cv::Mat& localImage = **cvImage; // No clone — RunInference takes const ref
|
||||
// Set thread-local NV12 frame data for fast-path inference
|
||||
// Cleared after first RunInference to prevent NV12 mismatch on cropped sub-images (OCR, etc.)
|
||||
tl_currentGpuFrame() = ANSGpuFrameRegistry::instance().lookup(*cvImage);
|
||||
|
||||
std::vector<ANSCENTER::Object> objectDetectionResults;
|
||||
std::vector<cv::Rect> bBox = ANSCENTER::ANSALPR::GetBoundingBoxes(strBboxes);
|
||||
@@ -1163,10 +1355,14 @@ extern "C" ANSLPR_API int ANSALPR_RunInferencesComplete_LV_V2(
|
||||
const double scaleFactor = (maxImageSize > 0) ? static_cast<double>(originalWidth) / maxImageSize : 1.0;
|
||||
|
||||
if (bBox.empty()) {
|
||||
// Full-frame path: NV12 fast-path is only safe for the full-frame
|
||||
// inference call. Scope the TL pointer so it is cleared on any exit
|
||||
// path (normal or thrown) and is NOT seen by any subsequent
|
||||
// cropped-image inference (which would mismatch the NV12 cache).
|
||||
GpuFrameScope _gfs(ANSGpuFrameRegistry::instance().lookup(*cvImage));
|
||||
objectDetectionResults = engine->RunInference(localImage, cameraId);
|
||||
}
|
||||
tl_currentGpuFrame() = nullptr; // Clear before crop-based inference
|
||||
if (!bBox.empty()) {
|
||||
else {
|
||||
for (const auto& rect : bBox) {
|
||||
cv::Rect scaledRect;
|
||||
scaledRect.x = static_cast<int>(rect.x * scaleFactor);
|
||||
|
||||
Reference in New Issue
Block a user