Add CPU/GPU gate and support new ANSALPR using OCR

This commit is contained in:
2026-04-12 17:16:16 +10:00
parent 27083a6530
commit 0a8aaed215
30 changed files with 1870 additions and 2166 deletions

View File

@@ -272,6 +272,82 @@ static bool LoadLpcModel_SEH(const LoadLpcParams& p, DWORD* outCode) {
return false;
}
}
// ---------------------------------------------------------------------------
// Generic SEH wrapper for loading an ANSONNXYOLO model (used by the CPU /
// AMD / Intel fallback path where TensorRT is unavailable).
//
// Why SEH is required here
// ------------------------
// DirectML / OpenVINO / CUDA ORT session creation can crash with an
// asynchronous hardware fault (STATUS_ACCESS_VIOLATION 0xC0000005) when
// the underlying provider driver is in a bad state. C++ `try/catch` does
// NOT catch SEH exceptions on MSVC unless the translator is explicitly
// installed. Without this SEH wrapper the AV propagates up through
// ANSALPR_OD::LoadEngine into LoadANSALPREngineHandle, which logs
// "SEH exception 0xC0000005 caught during engine load" and returns 0 —
// the user sees a generic error with no way to tell which detector
// (LPD / OCR / LPC) failed.
//
// Wrapping each detector creation lets us:
// 1. Isolate the failing detector without taking down the whole load.
// 2. Log a precise error message indicating which model crashed.
// 3. Let the caller zero out the unique_ptr so Destroy() won't run a
// half-initialised engine during cleanup.
// ---------------------------------------------------------------------------
struct LoadOnnxParams {
const std::string* licenseKey;
ANSCENTER::ModelConfig* config;
const std::string* modelFolder;
const char* modelName;
const char* classFile;
std::string* labels;
std::unique_ptr<ANSCENTER::ANSODBase>* detector;
bool enableTracker;
bool disableStabilization;
};
static bool LoadOnnxModel_Impl(const LoadOnnxParams& p) {
try {
auto onnxyolo = std::make_unique<ANSCENTER::ANSONNXYOLO>();
bool ok = onnxyolo->LoadModelFromFolder(
*p.licenseKey, *p.config, p.modelName, p.classFile,
*p.modelFolder, *p.labels);
if (!ok) {
return false;
}
if (p.enableTracker) {
onnxyolo->SetTracker(ANSCENTER::TrackerType::BYTETRACK, true);
} else {
onnxyolo->SetTracker(ANSCENTER::TrackerType::BYTETRACK, false);
}
if (p.disableStabilization) {
onnxyolo->SetStabilization(false);
}
*p.detector = std::move(onnxyolo); // upcast ANSONNXYOLO -> ANSODBase
return true;
}
catch (...) {
p.detector->reset();
return false;
}
}
static bool LoadOnnxModel_SEH(const LoadOnnxParams& p, DWORD* outCode) {
// IMPORTANT: a function containing __try/__except must not run C++
// destructors in the handler body — the CRT's SEH unwind can collide
// with C++ unwind and call std::terminate. We therefore defer any
// cleanup (unique_ptr::reset) to the caller, which runs outside the
// SEH context. This mirrors the LoadLpcModel_SEH pattern above.
*outCode = 0;
__try {
return LoadOnnxModel_Impl(p);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
*outCode = GetExceptionCode();
return false;
}
}
//#define FNS_DEBUG
namespace ANSCENTER {
@@ -285,6 +361,12 @@ namespace ANSCENTER {
ANSALPR_OD::ANSALPR_OD() {
valid = false;
// Default to safest engine (CPU). LoadEngine() overrides this after
// CheckHardwareInformation() runs. We must not leave engineType
// uninitialised because vendor predicates (isNvidiaEngine() etc.)
// gate NV12/CUDA paths and could otherwise activate the CUDA runtime
// on AMD/Intel hardware.
engineType = ANSCENTER::EngineType::CPU;
};
ANSALPR_OD::~ANSALPR_OD() {
try {
@@ -485,8 +567,16 @@ namespace ANSCENTER {
WriteEventLog("ANSALPR_OD::LoadEngine: Step 2 - Checking hardware information");
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 2: Checking hardware information", __FILE__, __LINE__);
engineType = ANSCENTER::ANSLicenseHelper::CheckHardwareInformation();//
WriteEventLog(("ANSALPR_OD::LoadEngine: Step 2 complete - Engine type = " + std::to_string(static_cast<int>(engineType))).c_str());
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 2 complete: Engine type = " + std::to_string(static_cast<int>(engineType)), __FILE__, __LINE__);
const char* vendorTag =
isNvidiaEngine() ? "NVIDIA_GPU (TensorRT + NV12/CUDA fast path)" :
isAmdEngine() ? "AMD_GPU (DirectML via ONNX Runtime, NV12/CUDA DISABLED)" :
isIntelEngine() ? "OPENVINO_GPU (OpenVINO via ONNX Runtime, NV12/CUDA DISABLED)" :
"CPU (ONNX Runtime, NV12/CUDA DISABLED)";
WriteEventLog(("ANSALPR_OD::LoadEngine: Step 2 complete - Engine type = " +
std::to_string(static_cast<int>(engineType)) + " [" + vendorTag + "]").c_str());
this->_logger.LogInfo("ANSALPR_OD::LoadEngine",
"Step 2 complete: Engine type = " + std::to_string(static_cast<int>(engineType)) +
" [" + vendorTag + "]", __FILE__, __LINE__);
valid = false;
if (_lpDetector) _lpDetector.reset();
@@ -788,54 +878,132 @@ namespace ANSCENTER {
}
}
}
// ONNX Runtime fallback path (CPU or when TensorRT fails)
// ONNX Runtime fallback path (CPU / AMD / Intel — or NVIDIA when
// TensorRT build failed). Each detector is loaded through a
// dedicated SEH wrapper (LoadOnnxModel_SEH) so that an
// AV / STATUS_ACCESS_VIOLATION raised deep inside the ONNX
// Runtime session creator (e.g. from a misbehaving DirectML
// / OpenVINO / CUDA provider driver) does not tear down the
// whole LoadEngine call. The wrapper logs the exact detector
// that failed and zeros out the corresponding unique_ptr.
if (!valid) {
if (FileExist(lprModel) && (FileExist(ocrModel)))
{
bool lpSuccess = false, ocrSuccess = false;
// ── Step 6: LPD ─────────────────────────────────────
WriteEventLog("ANSALPR_OD::LoadEngine: Step 6 - Loading LP detector with ONNX Runtime");
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 6: Loading LP detector with ONNX Runtime", __FILE__, __LINE__);
_lpdmodelConfig.detectionType = DetectionType::DETECTION;
_lpdmodelConfig.modelType = ModelType::ONNXYOLO;
_lpdmodelConfig.modelType = ModelType::ONNXYOLO;
std::string _lprClasses;
_lpDetector = std::make_unique<ANSCENTER::ANSONNXYOLO>();// Yolo
bool lpSuccess = _lpDetector->LoadModelFromFolder(_licenseKey, _lpdmodelConfig, "lpd", "lpd.names", _modelFolder, _lprClasses);
if (!lpSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine", "Failed to load LP detector (ONNX Runtime).", __FILE__, __LINE__);
_lpDetector.reset();
}
else {
// Enable tracker on LP detector for stable bounding box tracking,
// but disable stabilization (no ghost plates — ALPRChecker handles text stabilization)
_lpDetector->SetTracker(TrackerType::BYTETRACK, true);
_lpDetector->SetStabilization(false);
{
LoadOnnxParams p{};
p.licenseKey = &_licenseKey;
p.config = &_lpdmodelConfig;
p.modelFolder = &_modelFolder;
p.modelName = "lpd";
p.classFile = "lpd.names";
p.labels = &_lprClasses;
p.detector = &_lpDetector;
p.enableTracker = true;
p.disableStabilization = true;
DWORD sehCode = 0;
lpSuccess = LoadOnnxModel_SEH(p, &sehCode);
if (sehCode != 0) {
char buf[256];
snprintf(buf, sizeof(buf),
"ANSALPR_OD::LoadEngine: Step 6 LPD SEH exception 0x%08X — LP detector disabled", sehCode);
WriteEventLog(buf, EVENTLOG_ERROR_TYPE);
this->_logger.LogFatal("ANSALPR_OD::LoadEngine",
"Step 6: LP detector crashed (SEH 0x" + std::to_string(sehCode) + "). LP detector disabled.",
__FILE__, __LINE__);
lpSuccess = false;
// Drop any half-initialised state outside SEH context.
if (_lpDetector) _lpDetector.reset();
}
else if (!lpSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine",
"Failed to load LP detector (ONNX Runtime).", __FILE__, __LINE__);
if (_lpDetector) _lpDetector.reset();
}
}
// ── Step 7: OCR ─────────────────────────────────────
WriteEventLog("ANSALPR_OD::LoadEngine: Step 7 - Loading OCR detector with ONNX Runtime");
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 7: Loading OCR detector with ONNX Runtime", __FILE__, __LINE__);
_ocrModelConfig.detectionType = DetectionType::DETECTION;
_ocrModelConfig.modelType = ModelType::ONNXYOLO;
_ocrDetector = std::make_unique<ANSCENTER::ANSONNXYOLO>();// Yolo
bool ocrSuccess = _ocrDetector->LoadModelFromFolder(_licenseKey, _ocrModelConfig, "ocr", "ocr.names", _modelFolder, _ocrLabels);
if (!ocrSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine", "Failed to load OCR detector (ONNX Runtime).", __FILE__, __LINE__);
_ocrDetector.reset();
}
else {
_ocrDetector->SetTracker(TrackerType::BYTETRACK, false);
_ocrModelConfig.modelType = ModelType::ONNXYOLO;
{
LoadOnnxParams p{};
p.licenseKey = &_licenseKey;
p.config = &_ocrModelConfig;
p.modelFolder = &_modelFolder;
p.modelName = "ocr";
p.classFile = "ocr.names";
p.labels = &_ocrLabels;
p.detector = &_ocrDetector;
p.enableTracker = false;
p.disableStabilization = false;
DWORD sehCode = 0;
ocrSuccess = LoadOnnxModel_SEH(p, &sehCode);
if (sehCode != 0) {
char buf[256];
snprintf(buf, sizeof(buf),
"ANSALPR_OD::LoadEngine: Step 7 OCR SEH exception 0x%08X — OCR detector disabled", sehCode);
WriteEventLog(buf, EVENTLOG_ERROR_TYPE);
this->_logger.LogFatal("ANSALPR_OD::LoadEngine",
"Step 7: OCR detector crashed (SEH 0x" + std::to_string(sehCode) + "). OCR detector disabled.",
__FILE__, __LINE__);
ocrSuccess = false;
// Drop any half-initialised state outside SEH context.
if (_ocrDetector) _ocrDetector.reset();
}
else if (!ocrSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine",
"Failed to load OCR detector (ONNX Runtime).", __FILE__, __LINE__);
if (_ocrDetector) _ocrDetector.reset();
}
}
// Check if we need to load the color model
// ── Step 8: LPC (optional) ──────────────────────────
if (FileExist(colorModel) && (_lpColourModelConfig.detectionScoreThreshold > 0)) {
WriteEventLog("ANSALPR_OD::LoadEngine: Step 8 - Loading colour classifier with ONNX Runtime");
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 8: Loading colour classifier with ONNX Runtime", __FILE__, __LINE__);
_lpColourModelConfig.detectionType = DetectionType::CLASSIFICATION;
_lpColourModelConfig.modelType = ModelType::ONNXYOLO;
_lpColourDetector = std::make_unique<ANSCENTER::ANSONNXYOLO>();// Classification with ONNX
bool colourSuccess = _lpColourDetector->LoadModelFromFolder(_licenseKey, _lpColourModelConfig, "lpc", "lpc.names", _modelFolder, _lpColourLabels);
if (!colourSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine", "Failed to load colour detector (ONNX Runtime). Colour detection disabled.", __FILE__, __LINE__);
_lpColourDetector.reset();
}
else {
_lpColourDetector->SetTracker(TrackerType::BYTETRACK, false);
_lpColourModelConfig.modelType = ModelType::ONNXYOLO;
{
LoadOnnxParams p{};
p.licenseKey = &_licenseKey;
p.config = &_lpColourModelConfig;
p.modelFolder = &_modelFolder;
p.modelName = "lpc";
p.classFile = "lpc.names";
p.labels = &_lpColourLabels;
p.detector = &_lpColourDetector;
p.enableTracker = false;
p.disableStabilization = false;
DWORD sehCode = 0;
bool colourSuccess = LoadOnnxModel_SEH(p, &sehCode);
if (sehCode != 0) {
char buf[256];
snprintf(buf, sizeof(buf),
"ANSALPR_OD::LoadEngine: Step 8 LPC SEH exception 0x%08X — colour detection disabled", sehCode);
WriteEventLog(buf, EVENTLOG_ERROR_TYPE);
this->_logger.LogError("ANSALPR_OD::LoadEngine",
"Step 8: Colour classifier crashed (SEH 0x" + std::to_string(sehCode) + "). Colour detection disabled.",
__FILE__, __LINE__);
// Drop any half-initialised state outside SEH context.
if (_lpColourDetector) _lpColourDetector.reset();
}
else if (!colourSuccess) {
this->_logger.LogError("ANSALPR_OD::LoadEngine",
"Failed to load colour detector (ONNX Runtime). Colour detection disabled.", __FILE__, __LINE__);
if (_lpColourDetector) _lpColourDetector.reset();
}
}
}
@@ -851,8 +1019,8 @@ namespace ANSCENTER {
}
}
_isInitialized = valid;
WriteEventLog(("ANSALPR_OD::LoadEngine: Step 8 - Engine load complete. Valid = " + std::to_string(valid)).c_str());
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 8: Engine load complete. Valid = " + std::to_string(valid), __FILE__, __LINE__);
WriteEventLog(("ANSALPR_OD::LoadEngine: Step 9 - Engine load complete. Valid = " + std::to_string(valid)).c_str());
this->_logger.LogInfo("ANSALPR_OD::LoadEngine", "Step 9: Engine load complete. Valid = " + std::to_string(valid), __FILE__, __LINE__);
return valid;
}
@@ -1467,8 +1635,11 @@ namespace ANSCENTER {
constexpr int padding = 10;
// --- Compute display→full-res scale (once per frame, cheap) ---
// NV12 GPU fast path is NVIDIA-only — cv::cuda::Stream/GpuMat
// touch the CUDA runtime even when the helper would early-return,
// which destabilises AMD/Intel hardware. Gate strictly on NVIDIA.
float scaleX = 1.f, scaleY = 1.f;
{
if (isNvidiaEngine()) {
auto* gpuData = tl_currentGpuFrame();
if (gpuData && gpuData->width > frame.cols && gpuData->height > frame.rows) {
scaleX = static_cast<float>(gpuData->width) / frame.cols;
@@ -1484,8 +1655,9 @@ namespace ANSCENTER {
cv::Mat lprImage;
// Try GPU NV12 crop (NVIDIA decode: NV12 still in GPU VRAM)
if (scaleX > 1.f) {
// Try GPU NV12 crop (NVIDIA decode: NV12 still in GPU VRAM).
// Skipped on AMD/Intel/CPU — see isNvidiaEngine() guard above.
if (isNvidiaEngine() && scaleX > 1.f) {
auto cropResult = _nv12Helper.tryNV12CropToBGR(
frame, 0, box, padding, scaleX, scaleY,
this->_logger, "LPR");
@@ -1635,8 +1807,11 @@ namespace ANSCENTER {
constexpr int padding = 10;
// --- Compute display→full-res scale (once per frame, cheap) ---
// NVIDIA-only NV12 fast path — see isNvidiaEngine() discussion
// above. cv::cuda::* types touch CUDA even inside the "guarded"
// helper, so we must not even read tl_currentGpuFrame() on AMD.
float scaleX2 = 1.f, scaleY2 = 1.f;
{
if (isNvidiaEngine()) {
auto* gpuData = tl_currentGpuFrame();
if (gpuData && gpuData->width > frame.cols && gpuData->height > frame.rows) {
scaleX2 = static_cast<float>(gpuData->width) / frame.cols;
@@ -1694,9 +1869,11 @@ namespace ANSCENTER {
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
// Crop from full-res NV12 on GPU if available, otherwise display-res
// Crop from full-res NV12 on GPU if available, otherwise display-res.
// NV12 helper is NVIDIA-only — isNvidiaEngine() gate keeps
// CUDA runtime inactive on AMD/Intel/CPU hardware.
cv::Mat lprImage;
if (scaleX2 > 1.f) {
if (isNvidiaEngine() && scaleX2 > 1.f) {
auto cropResult = _nv12Helper.tryNV12CropToBGR(
frame, 0, lprObject.box, 0, scaleX2, scaleY2,
this->_logger, "LPR");
@@ -1760,10 +1937,12 @@ namespace ANSCENTER {
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
// Crop from full-res NV12 on GPU if available, otherwise display-res
// Crop from full-res NV12 on GPU if available, otherwise display-res.
// NV12 helper is NVIDIA-only — isNvidiaEngine() gate keeps
// CUDA runtime inactive on AMD/Intel/CPU hardware.
cv::Rect lprPos(x1, y1, width, height);
cv::Mat lprImage;
if (scaleX2 > 1.f) {
if (isNvidiaEngine() && scaleX2 > 1.f) {
auto cropResult = _nv12Helper.tryNV12CropToBGR(
frame, 0, lprPos, 0, scaleX2, scaleY2,
this->_logger, "LPR");