Files
ANSCORE/modules/ANSLPR/ANSLPR_CPU.cpp

1458 lines
64 KiB
C++
Raw Normal View History

2026-03-28 16:54:11 +11:00
#include "ANSLPR_CPU.h"
#include "ANSYOLOV10OVOD.h"
#include "ANSOPENVINOOD.h"
#include "ANSTENSORRTOD.h"
#include "ANSYOLO12OD.h"
//#define FNS_DEBUG
namespace ANSCENTER {
void ANSOVDetector::PreprocessImage(cv::Mat& frame) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
if (frame.empty()) return;
if ((frame.rows <= 5) || (frame.cols <= 5)) return;
cv::resize(frame, resized_frame_, model_input_shape_, 0, 0, cv::INTER_AREA);
factor_.x = static_cast<float>(frame.cols / model_input_shape_.width);
factor_.y = static_cast<float>(frame.rows / model_input_shape_.height);
float* input_data = reinterpret_cast<float*>(resized_frame_.data);
input_tensor_ = ov::Tensor(compiled_model_.input().get_element_type(), compiled_model_.input().get_shape(), input_data);
inference_request_.set_input_tensor(input_tensor_);
}
catch (const std::exception& e) {
std::cout << "ANSOVDetector::PreprocessImage. " << e.what() << std::endl;
}
catch (...) {
std::cout << "ANSOVDetector::PreprocessImage. " << "unknown exception" << std::endl;
}
}
cv::Rect ANSOVDetector::GetBoundingBox(const cv::Rect& src) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
cv::Rect box = src;
box.x = (box.x - 0.5 * box.width) * factor_.x;
box.y = (box.y - 0.5 * box.height) * factor_.y;
box.width *= factor_.x;
box.height *= factor_.y;
return box;
}
catch (const std::exception& e) {
std::cout << "ANSOVDetector::GetBoundingBox. " << e.what() << std::endl;
return src;
}
}
std::vector <std::string> ANSOVDetector::LoadClassesFromFile() {
std::lock_guard<std::recursive_mutex> lock(_mutex);
std::vector <std::string> _classes;
try {
std::ifstream inputFile(_classFilePath);
if (inputFile.is_open())
{
_classes.clear();
std::string classLine;
while (std::getline(inputFile, classLine))
_classes.push_back(classLine);
inputFile.close();
}
return _classes;
}
catch (std::exception& e) {
std::cout << "ANSOVDetector::LoadClassesFromFile. " << e.what() << std::endl;
return _classes;
}
}
std::vector<Object>ANSOVDetector::PostProcessing() {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
std::vector<int> class_list;
std::vector<float> confidence_list;
std::vector<cv::Rect> box_list;
float* detections = inference_request_.get_output_tensor().data<float>();
const cv::Mat detection_outputs(model_output_shape_, CV_32F, detections);
for (int i = 0; i < detection_outputs.cols; ++i) {
const cv::Mat classes_scores = detection_outputs.col(i).rowRange(4, detection_outputs.rows);
cv::Point class_id;
double score;
cv::minMaxLoc(classes_scores, nullptr, &score, nullptr, &class_id);
if (score > _detectionScoreThreshold) {
class_list.push_back(class_id.y);
confidence_list.push_back(score);
const float x = detection_outputs.at<float>(0, i);
const float y = detection_outputs.at<float>(1, i);
const float w = detection_outputs.at<float>(2, i);
const float h = detection_outputs.at<float>(3, i);
cv::Rect box;
box.x = static_cast<int>(x);
box.y = static_cast<int>(y);
box.width = static_cast<int>(w);
box.height = static_cast<int>(h);
box_list.push_back(box);
}
}
std::vector<int> NMS_result;
cv::dnn::NMSBoxes(box_list, confidence_list, _modelConfThreshold, _modelMNSThreshold, NMS_result);
std::vector<Object> output;
for (int i = 0; i < NMS_result.size(); i++)
{
Object result;
int id = NMS_result[i];
result.classId = class_list[id];
result.confidence = confidence_list[id];
result.box = GetBoundingBox(box_list[id]);
output.push_back(result);
}
return output;
}
catch (const std::exception& e) {
std::vector<Object> result;
result.clear();
std::cout << "ANSOVDetector::PostprocessImage. " << e.what() << std::endl;
return result;
}
}
std::string ANSOVDetector::GetOpenVINODevice() {
std::vector<std::string> available_devices = _core.get_available_devices();
bool device_found = false;
std::string deviceName = "CPU";
// Search for NPU
auto it = std::find(available_devices.begin(), available_devices.end(), "NPU");
if (it != available_devices.end()) {
_core.set_property("NPU", ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
_core.set_property("GPU", ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
deviceName = "AUTO:NPU,GPU";
device_found = true;
return deviceName;
}
// If NPU not found, search for GPU
if (!device_found) {
it = std::find(available_devices.begin(), available_devices.end(), "GPU");
if (it != available_devices.end()) {
_core.set_property("GPU", ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
deviceName = "GPU";
device_found = true;
return deviceName;
}
}
// If GPU not found, search for GPU.0
if (!device_found) {
it = std::find(available_devices.begin(), available_devices.end(), "GPU.0");
if (it != available_devices.end()) {
_core.set_property("GPU", ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
deviceName = "GPU";
device_found = true;
return deviceName;
}
}
// If neither NPU nor GPU found, default to CPU
if (!device_found) {
_core.set_property("GPU", ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
deviceName = "CPU";
return deviceName;
}
return deviceName;
}
bool ANSOVDetector::Initialize(const std::string& modelFilePath, const std::string& classFilePath, std::vector <std::string>& _classes)
{
std::lock_guard<std::recursive_mutex> lock(_mutex);
_detectionScoreThreshold = 0.5;
_modelConfThreshold = 0.48;
_modelMNSThreshold = 0.5;
// 1. Load labelMap and engine
_classes.clear();
_modelFilePath = modelFilePath;
_classFilePath = classFilePath;
_classes = LoadClassesFromFile();
try {
std::shared_ptr<ov::Model> model = _core.read_model(_modelFilePath);
ov::preprocess::PrePostProcessor ppp = ov::preprocess::PrePostProcessor(model);
ppp.input().tensor().set_element_type(ov::element::u8).set_layout("NHWC").set_color_format(ov::preprocess::ColorFormat::BGR);
ppp.input().preprocess().convert_element_type(ov::element::f32).convert_color(ov::preprocess::ColorFormat::RGB).scale({ 255, 255, 255 });
ppp.input().model().set_layout("NCHW");
ppp.output().tensor().set_element_type(ov::element::f32);
model = ppp.build();
std::string deviceName = GetOpenVINODevice();
compiled_model_ = _core.compile_model(model, deviceName);
inference_request_ = compiled_model_.create_infer_request();
const std::vector<ov::Output<ov::Node>> inputs = model->inputs();
const ov::Shape input_shape = inputs[0].get_shape();
short height = input_shape[1];
short width = input_shape[2];
model_input_shape_ = cv::Size2f(width, height);
const std::vector<ov::Output<ov::Node>> outputs = model->outputs();
const ov::Shape output_shape = outputs[0].get_shape();
height = output_shape[1];
width = output_shape[2];
model_output_shape_ = cv::Size(width, height);
_isInitialized = true;
return true;
}
catch (std::exception& e) {
std::cout << "ANSOVDetector::InitialModel. " << e.what() << std::endl;
return false;
}
}
std::vector<Object> ANSOVDetector::RunInference(const cv::Mat& input) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
std::vector<Object> output;
output.clear();
if (!_isInitialized) return output;
try {
// Step 0: Prepare input
if (input.empty()) return output;
if ((input.rows <= 5) || (input.cols <= 5)) return output;
cv::Mat frame = input;
this->PreprocessImage(frame);
//Synchronous mode
inference_request_.infer();
//inference_request_.start_async();
//inference_request_.wait();
return PostProcessing();
}
catch (std::exception& e) {
std::cout << "ANSOVDetector::RunInference. " << e.what() << std::endl;
return output;
}
}
/// <summary>
/// ANSLRP_CPU
/// </summary>
ANSALPR_CPU::ANSALPR_CPU() {
valid = false;
};
ANSALPR_CPU::~ANSALPR_CPU() {
try {
Destroy();
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::~ANSALPR_CPU", e.what(), __FILE__, __LINE__);
}
};
bool ANSALPR_CPU::Destroy() {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
if (ppocr) ppocr.reset();
return true;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::Destroy", e.what(), __FILE__, __LINE__);
return false;
}
};
bool ANSALPR_CPU::Initialize(const std::string& licenseKey, const std::string& modelZipFilePath, const std::string& modelZipPassword, double detectorThreshold, double ocrThreshold, double colourThreshold) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
_licenseKey = licenseKey;
_licenseValid = false;
_detectorThreshold = detectorThreshold;
_ocrThreshold = ocrThreshold;
_colorThreshold = colourThreshold;
if (_detectorThreshold < 0.25) _detectorThreshold = 0.25;
if (_detectorThreshold > 0.95) _detectorThreshold = 0.95;
if (_ocrThreshold < 0.25) _ocrThreshold = 0.25;
if (_ocrThreshold > 0.95) _ocrThreshold = 0.95;
_country = Country::VIETNAM;
CheckLicense();
if (!_licenseValid) {
this->_logger.LogError("ANSALPR_CPU::Initialize.", "License is not valid.", __FILE__, __LINE__);
return false;
}
// Extract model folder
// 0. Check if the modelZipFilePath exist?
if (!FileExist(modelZipFilePath)) {
this->_logger.LogFatal("ANSALPR_CPU::Initialize", "Model zip file is not exist", __FILE__, __LINE__);
}
else {
this->_logger.LogTrace("ANSALPR_CPU::Initialize. Model zip file found: ", modelZipFilePath, __FILE__, __LINE__);
}
// 1. Unzip model zip file to a special location with folder name as model file (and version)
std::string outputFolder;
std::vector<std::string> passwordArray;
if (!modelZipPassword.empty()) passwordArray.push_back(modelZipPassword);
passwordArray.push_back("AnsDemoModels20@!");
passwordArray.push_back("Sh7O7nUe7vJ/417W0gWX+dSdfcP9hUqtf/fEqJGqxYL3PedvHubJag==");
passwordArray.push_back("3LHxGrjQ7kKDJBD9MX86H96mtKLJaZcTYXrYRdQgW8BKGt7enZHYMg==");
std::string modelName = GetFileNameWithoutExtension(modelZipFilePath);
size_t vectorSize = passwordArray.size();
for (size_t i = 0; i < vectorSize; i++) {
if (ExtractPasswordProtectedZip(modelZipFilePath, passwordArray[i], modelName, _modelFolder, false))
break; // Break the loop when the condition is met.
}
// 2. Check if the outputFolder exist
if (!FolderExist(_modelFolder)) {
this->_logger.LogError("ANSALPR_CPU::Initialize. Output model folder is not exist", _modelFolder, __FILE__, __LINE__);
return false; // That means the model file is not exist or the password is not correct
}
// 3. Load LD model
alprChecker.Init(MAX_ALPR_FRAME);
return true;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::Initialize", e.what(), __FILE__, __LINE__);
return false;
}
}
bool ANSALPR_CPU::LoadEngine() {
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
// 1. Load license plate detection model (using CPU OpenVINO)
if (_lprDetector) _lprDetector.reset();
std::string lprModel = CreateFilePath(_modelFolder, "lprModel.xml");
std::string lprClassesFile = CreateFilePath(_modelFolder, "lprClasses.names");
if (FileExist(lprModel))
{
ANSCENTER::ModelConfig modelConfig;
modelConfig.inpHeight = 640;
modelConfig.inpHeight = 640;
modelConfig.detectionScoreThreshold = _detectorThreshold;
modelConfig.modelConfThreshold = 0.5;
modelConfig.modelMNSThreshold = 0.5;
modelConfig.detectionType = DetectionType::DETECTION;
modelConfig.modelType = ModelType::OPENVINO;
std::string _lprClasses;
_lprDetector = std::make_unique<ANSCENTER::OPENVINOOD>();// OpenVINO
_lprDetector->LoadModelFromFolder(_licenseKey, modelConfig, "lprModel", "lprClasses.names", _modelFolder, _lprClasses);
// Disable tracker/stabilization — ANSLPR sub-detectors are internal
_lprDetector->SetTracker(TrackerType::BYTETRACK, false);
valid = true;
}
else {
this->_logger.LogFatal("ANSALPR_CPU::Initialize", "Failed to load LPR model", __FILE__, __LINE__);
valid = false;
return false;
}
// 2. Load OCR model
std::string recognizerModelFile = CreateFilePath(_modelFolder, "ENV4_REC.pdmodel");
std::string recognizerModelParam = CreateFilePath(_modelFolder, "ENV4_REC.pdiparams");
std::string recogizerCharDictionaryPath = CreateFilePath(_modelFolder, "dict_en.txt");
std::string detectionModelFile = CreateFilePath(_modelFolder, "EN_DET.pdmodel");
std::string detectionModelParam = CreateFilePath(_modelFolder, "EN_DET.pdiparams");
std::string clsModelFile = CreateFilePath(_modelFolder, "CH_CLS.pdmodel");
std::string clsModelParam = CreateFilePath(_modelFolder, "CH_CLS.pdiparams");
try {
std::string limit_type = "max";
std::string det_db_score_mode = "slow";
bool is_scale = true;
double det_db_thresh = 0.9;
double det_db_box_thresh = _ocrThreshold;
double det_db_unclip_ratio = 2;
bool use_dilation = false;
int cls_batch_num = 1;
double cls_thresh = 0.98;
int rec_batch_num = 1;
_isInitialized = ppocr->Initialize(detectionModelFile, clsModelFile, recognizerModelFile, recogizerCharDictionaryPath);
ppocr->SetParameters(limit_type, det_db_score_mode, is_scale,
det_db_thresh, det_db_box_thresh, det_db_unclip_ratio,
use_dilation, cls_batch_num, cls_thresh, rec_batch_num);
return _isInitialized;
}
catch (...) {
_licenseValid = false;
this->_logger.LogFatal("ANSALPR_CPU::Initialize", "Failed to create OCR objects", __FILE__, __LINE__);
return false;
}
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::Initialize", e.what(), __FILE__, __LINE__);
return false;
}
}
std::vector<Object> ANSALPR_CPU::RunInference(const cv::Mat& input, const std::string &cameraId) {
// No coarse _mutex — sub-components have their own fine-grained locks.
2026-03-28 16:54:11 +11:00
std::vector<Object> output;
output.clear();
// Initial validation
if (!_licenseValid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid license", __FILE__, __LINE__);
return output;
}
if (!valid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid model", __FILE__, __LINE__);
return output;
}
if (!_isInitialized) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Model is not initialized", __FILE__, __LINE__);
return output;
}
try {
if (input.empty()) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image is empty", __FILE__, __LINE__);
return output;
}
if ((input.cols < 5) || (input.rows < 5)) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image size is too small", __FILE__, __LINE__);
return output;
}
// Convert grayscale to 3-channel BGR if needed
cv::Mat frame;
if (input.channels() == 1) {
cv::cvtColor(input, frame, cv::COLOR_GRAY2BGR);
}
else {
frame = input;// input.clone();
}
//int fWidth = frame.cols;
//int fHeight = frame.rows;
#ifdef FNS_DEBUG // Corrected preprocessor directive
cv::Mat draw = input.clone();
#endif
// Use local variable instead of shared _detectedArea for thread safety
cv::Rect detectedArea(0, 0, frame.cols, frame.rows);
if ((detectedArea.width > 50) && (detectedArea.height > 50)) {
2026-03-28 16:54:11 +11:00
#ifdef FNS_DEBUG // Corrected preprocessor directive
cv::rectangle(draw, detectedArea, cv::Scalar(0, 0, 255), 2); // RED for detectedArea
#endif
2026-03-28 16:54:11 +11:00
// Ensure _lprDetector is valid
if (!_lprDetector) {
this->_logger.LogFatal("ANSALPR_CPU::Inference", "_lprDetector is null", __FILE__, __LINE__);
return output;
}
cv::Mat activeFrame = frame(detectedArea).clone();
2026-03-28 16:54:11 +11:00
//std::vector<Object> lprOutputRaw = _lpDetector->RunInference(activeFrame, cameraId);
//std::vector<Object> lprOutput = AdjustLicensePlateBoundingBoxes(lprOutputRaw, _detectedArea, frame.size(), 3.0);
std::vector<Object> lprOutputRaw = _lprDetector->RunInference(activeFrame, cameraId);
float iouThreshold = 0.4;
std::vector<Object> lprOutput = ANSUtilityHelper::ApplyNMS(lprOutputRaw, iouThreshold);
if (!lprOutput.empty()) {
if (!ppocr) {
this->_logger.LogFatal("ANSALPR_CPU::Inference", "PPOCR instance is null", __FILE__, __LINE__);
return output;
}
for (size_t i = 0; i < lprOutput.size(); ++i) {
Object lprObject = lprOutput[i];
cv::Rect box = lprObject.box;
#ifdef FNS_DEBUG // Corrected preprocessor directive
cv::rectangle(draw, box, cv::Scalar(0, 255, 255), 2); // Yellow for fire
#endif
// Crop region with padding
int padding = 0;
int x1 = std::max(box.x - padding, 0);
int y1 = std::max(box.y - padding, 0);
int x2 = std::min(box.x + box.width + padding, frame.cols);
int y2 = std::min(box.y + box.height + padding, frame.rows);
int width = std::max(0, x2 - x1);
int height = std::max(0, y2 - y1);
x1 = std::max(0, x1);
y1 = std::max(0, y1);
width = std::min(frame.cols - x1, width);
height = std::min(frame.rows - y1, height);
if (width > padding && height > padding) {
cv::Rect lprPos(x1, y1, width, height);
cv::Mat alignedLPR = alignPlateForOCR(frame, lprPos);
// Set metadata
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
// OCR inference (ppocr is not thread-safe, use fine-grained lock)
std::vector<PaddleOCR::OCRPredictResult> res_ocr;
{
std::lock_guard<std::mutex> ocrLock(_ocrMutex);
res_ocr = ppocr->ocr(alignedLPR);
}
2026-03-28 16:54:11 +11:00
std::string ocrText;
if (!res_ocr.empty() && res_ocr.size() < 3) {
for (const auto& r : res_ocr) {
ocrText.append(r.text);
}
std::string rawText = AnalyseLicensePlateText(ocrText);
lprObject.className = alprChecker.checkPlate(cameraId, rawText, lprObject.box);
if (!lprObject.className.empty()) {
output.push_back(lprObject);
}
}
alignedLPR.release();
}
}
}
activeFrame.release();
}
frame.release();
#ifdef FNS_DEBUG // Corrected preprocessor directive
//resize the draw image to fit the screen
cv::resize(draw, draw, cv::Size(1920, 1080));
cv::imshow("Detected Areas", draw);
cv::waitKey(1);// Debugging: Diplsay the frame with the combined detected areas
draw.release();// Debugging: Diplsay the frame with the combined detected areas
#endif
return output;
}
catch (const cv::Exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::Inference - OpenCV Exception", e.what(), __FILE__, __LINE__);
}
catch (const std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::Inference - std::exception", e.what(), __FILE__, __LINE__);
}
catch (...) {
this->_logger.LogFatal("ANSALPR_CPU::Inference", "Unknown exception occurred", __FILE__, __LINE__);
}
return output;
}
bool ANSALPR_CPU::Inference(const cv::Mat& input, std::string& lprResult) {
// No coarse _mutex — delegates to Inference(input, lprResult, cameraId)
2026-03-28 16:54:11 +11:00
if (input.empty()) return false;
if ((input.cols < 5) || (input.rows < 5)) return false;
return Inference(input, lprResult, "CustomCam");
}
bool ANSALPR_CPU::Inference(const cv::Mat& input, std::string& lprResult, const std::string & cameraId) {
// No coarse _mutex — sub-components have fine-grained locks.
2026-03-28 16:54:11 +11:00
std::vector<Object> output;
output.clear();
if (!_licenseValid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid license", __FILE__, __LINE__);
return false;
}
if (!valid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid model", __FILE__, __LINE__);
return false;
}
if (!_isInitialized) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Model is not initialized", __FILE__, __LINE__);
return false;
}
try {
if (input.empty()) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image is empty", __FILE__, __LINE__);
return false;
}
if ((input.cols < 5) || (input.rows < 5)) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image size is too small", __FILE__, __LINE__);
return false;
}
// Convert grayscale images to 3-channel BGR if needed
cv::Mat frame;
if (input.channels() == 1) {
cv::cvtColor(input, frame, cv::COLOR_GRAY2BGR);
}
else {
frame = input;// input.clone();
}
int fWidth = frame.cols;
int fHeight = frame.rows;
cv::Rect roi(0,0,0,0);
std::vector<Object> lprOutputRaw = _lprDetector->RunStaticInference(frame, roi, cameraId);
//4. Apply Non-Maximum Suppression (NMS) to merge overlapping results
float iouThreshold = 0.1;
std::vector<Object> lprOutput = ANSUtilityHelper::ApplyNMS(lprOutputRaw, iouThreshold);
if (lprOutput.size() > 0) {
std::vector<Object> ocrOutput;
ocrOutput.clear();
std::string ocrText = "";
for (int i = 0; i < lprOutput.size(); i++) {
ocrText = "";
Object lprObject = lprOutput.at(i);
cv::Rect box = lprObject.box;
//get frame
int padding = 10;
int x1 = std::max(box.x - padding, 0);
int y1 = std::max(box.y - padding, 0);
int x2 = std::min(box.x + box.width + padding, frame.cols);
int y2 = std::min(box.y + box.height + padding, frame.rows);
int width = std::max(0, x2 - x1);
int height = std::max(0, y2 - y1);
x1 = std::max(0, x1);
y1 = std::max(0, y1);
width = std::min(frame.cols - x1, width);
height = std::min(frame.rows - y1, height);
if ((width > padding) && (height > padding)) {
cv::Rect lprPos(x1, y1, width, height);
cv::Mat lprImage = frame(lprPos).clone();
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
// ppocr is not thread-safe, use fine-grained lock
std::vector<PaddleOCR::OCRPredictResult> res_ocr;
{
std::lock_guard<std::mutex> ocrLock(_ocrMutex);
res_ocr = ppocr->ocr(lprImage);
}
2026-03-28 16:54:11 +11:00
int detectionSize = res_ocr.size();
if ((detectionSize > 0) && (detectionSize < 3)) {
for (int n = 0; n < res_ocr.size(); n++) { // number of detections
2026-03-28 16:54:11 +11:00
ocrText.append(res_ocr[n].text);
}
std::string rawText = AnalyseLicensePlateText(ocrText);
lprObject.className = alprChecker.checkPlate(cameraId, rawText, lprObject.box);
if (!lprObject.className.empty() && (lprObject.className != ""))
output.push_back(lprObject);
}
lprImage.release();
}
}
}
frame.release();
lprResult = VectorDetectionToJsonString(output);
return true;
}
catch (std::exception& e) {
lprResult = VectorDetectionToJsonString(output);
this->_logger.LogFatal("ANSALPR_CPU::Inference", e.what(), __FILE__, __LINE__);
return false;
}
}
bool ANSALPR_CPU::Inference(const cv::Mat& input, const std::vector<cv::Rect> & Bbox, std::string& lprResult) {
// No coarse _mutex — delegates to Inference(input, Bbox, lprResult, cameraId)
2026-03-28 16:54:11 +11:00
if (input.empty()) return false;
if ((input.cols < 5) || (input.rows < 5)) return false;
return Inference(input, Bbox, lprResult, "CustomCam");
}
bool ANSALPR_CPU::Inference(const cv::Mat& input, const std::vector<cv::Rect>& Bbox,
std::string& lprResult, const std::string& cameraId)
{
// No coarse _mutex — sub-components have fine-grained locks.
2026-03-28 16:54:11 +11:00
// Early validation
if (!_licenseValid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid license", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (!valid) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Invalid model", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (!_isInitialized) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Model is not initialized", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (input.empty()) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image is empty", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (input.cols < 5 || input.rows < 5) {
this->_logger.LogError("ANSALPR_CPU::Inference", "Input image size is too small", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (!_lprDetector) {
this->_logger.LogFatal("ANSALPR_CPU::Inference", "_lprDetector is null", __FILE__, __LINE__);
lprResult.clear();
return false;
}
if (!ppocr) {
this->_logger.LogFatal("ANSALPR_CPU::Inference", "ppocr is null", __FILE__, __LINE__);
lprResult.clear();
return false;
}
try {
// Convert grayscale to BGR if necessary (use local buffer for thread safety)
cv::Mat localFrame;
2026-03-28 16:54:11 +11:00
if (input.channels() == 1) {
cv::cvtColor(input, localFrame, cv::COLOR_GRAY2BGR);
2026-03-28 16:54:11 +11:00
}
const cv::Mat& frame = (input.channels() == 1) ? localFrame : input;
2026-03-28 16:54:11 +11:00
const int frameWidth = frame.cols;
const int frameHeight = frame.rows;
constexpr int padding = 10;
std::vector<Object> detectedObjects;
if (!Bbox.empty()) {
// Process each bounding box region
detectedObjects.reserve(Bbox.size());
for (const auto& bbox : Bbox) {
const int x1c = std::max(0, bbox.x);
const int y1c = std::max(0, bbox.y);
const int cropWidth = std::min(frameWidth - x1c, bbox.width);
const int cropHeight = std::min(frameHeight - y1c, bbox.height);
if (cropWidth < 5 || cropHeight < 5) {
continue;
}
cv::Mat croppedObject = frame(cv::Rect(x1c, y1c, cropWidth, cropHeight));
std::vector<Object> lprOutput = _lprDetector->RunInference(croppedObject);
for (auto& lprObject : lprOutput) {
const cv::Rect& box = lprObject.box;
// Calculate padded region within cropped image
const int x1 = std::max(0, box.x - padding);
const int y1 = std::max(0, box.y - padding);
const int x2 = std::min(cropWidth, box.x + box.width + padding);
const int y2 = std::min(cropHeight, box.y + box.height + padding);
// Adjust to original frame coordinates
lprObject.box.x = std::max(0, x1c + x1);
lprObject.box.y = std::max(0, y1c + y1);
lprObject.box.width = std::min(frameWidth - lprObject.box.x, x2 - x1);
lprObject.box.height = std::min(frameHeight - lprObject.box.y, y2 - y1);
if (lprObject.box.width <= padding || lprObject.box.height <= padding) {
continue;
}
// Run OCR
std::string ocrText = runOCROnPlate(frame, lprObject.box);
if (ocrText.empty()) {
continue;
}
std::string rawText = AnalyseLicensePlateText(ocrText);
lprObject.className = alprChecker.checkPlate(cameraId, rawText, lprObject.box);
if (lprObject.className.empty()) {
continue;
}
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
detectedObjects.push_back(std::move(lprObject));
}
}
}
else {
// No bounding boxes - run on full frame
std::vector<Object> lprOutput = _lprDetector->RunInference(frame);
detectedObjects.reserve(lprOutput.size());
for (auto& lprObject : lprOutput) {
const cv::Rect& box = lprObject.box;
// Calculate padded region
const int x1 = std::max(0, box.x - padding);
const int y1 = std::max(0, box.y - padding);
const int width = std::min(frameWidth - x1, box.width + 2 * padding);
const int height = std::min(frameHeight - y1, box.height + 2 * padding);
if (width <= padding || height <= padding) {
continue;
}
// Run OCR
cv::Rect lprPos(x1, y1, width, height);
std::string ocrText = runOCROnPlate(frame, lprPos);
if (ocrText.empty()) {
continue;
}
std::string rawText = AnalyseLicensePlateText(ocrText);
lprObject.className = alprChecker.checkPlate(cameraId, rawText, lprObject.box);
if (lprObject.className.empty()) {
continue;
}
lprObject.cameraId = cameraId;
lprObject.polygon = RectToNormalizedPolygon(lprObject.box, input.cols, input.rows);
detectedObjects.push_back(std::move(lprObject));
}
}
lprResult = VectorDetectionToJsonString(detectedObjects);
return true;
}
catch (const std::exception& e) {
lprResult.clear();
this->_logger.LogFatal("ANSALPR_CPU::Inference", e.what(), __FILE__, __LINE__);
return false;
}
}
// Helper function to run OCR on a plate region
std::string ANSALPR_CPU::runOCROnPlate(const cv::Mat& frame, const cv::Rect& plateRect) {
cv::Mat lprImage = frame(plateRect);
cv::Mat alignedLPR = enhanceForOCR(lprImage);
// ppocr is not thread-safe, use fine-grained lock
std::vector<PaddleOCR::OCRPredictResult> res_ocr;
{
std::lock_guard<std::mutex> ocrLock(_ocrMutex);
res_ocr = ppocr->ocr(alignedLPR);
}
2026-03-28 16:54:11 +11:00
const size_t detectionSize = res_ocr.size();
if (detectionSize == 0 || detectionSize >= 3) {
return {};
}
std::string ocrText;
ocrText.reserve(32); // Typical license plate length
for (const auto& ocr : res_ocr) {
ocrText.append(ocr.text);
}
return ocrText;
}
std::string ANSALPR_CPU::AnalyseLicensePlateText(const std::string& ocrText) {
std::string analysedLP = "";
try {
switch (_country) {
case Country::VIETNAM:
std::string cleanOCRText = "";
// int ind = findSubstringIndex(ocrText); // If ind >2, then LicenseType is deplomat
for (size_t i = 0; i < ocrText.size(); ++i) {
char c = ocrText[i];
if (std::isalnum(c))cleanOCRText += c;
}
int ocrSize = cleanOCRText.size();
std::transform(cleanOCRText.begin(), cleanOCRText.end(), cleanOCRText.begin(), ::toupper);
if (ocrSize == 8) {// car
std::string suburbCode = cleanOCRText.substr(0, 2);// from 11 to 99
std::string seriesCode = cleanOCRText.substr(2, 1);//A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
std::string numberCode = cleanOCRText.substr(3, 5);// 00000 to 99999
std::string newSuburbCode = convertStringToDigits(suburbCode);
// Convert the newSuburbCode to an integer
int numericValue = std::stoi(newSuburbCode);
// Check if it is within the range 11 to 99
if (numericValue >= 11 && numericValue <= 99) {
if (numericValue == 13)newSuburbCode = "73";
if (numericValue == 42)newSuburbCode = "62";
if (numericValue == 44)newSuburbCode = "64";
if (numericValue == 45)newSuburbCode = "65";
if (numericValue == 46)newSuburbCode = "66";
if (numericValue == 87)newSuburbCode = "81";
if (numericValue == 91)newSuburbCode = "97";
analysedLP = newSuburbCode + convertStringToLetters(seriesCode) + convertStringToDigits(numberCode);
}
else {
analysedLP = "";
}
}
else if (ocrSize == 9) {// motorbike
analysedLP = cleanOCRText;
//int dIndex = searchDiplomacyLP(cleanOCRText);
//if (dIndex != 5) {
// std::string suburbCode = cleanOCRText.substr(0, 2);// from 11 to 99
// std::string seriesCodeLetter = cleanOCRText.substr(2, 1);//A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
// std::string seriesCodeNumberOrLetter = cleanOCRText.substr(3, 1);//number or A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
// std::string numberCode = cleanOCRText.substr(4, 5);// 00000 to 99999
// std::string newSuburbCode = convertStringToDigits(suburbCode);
// std::string newseriesCodeNumberOrLetter;
// if (std::isdigit(seriesCodeNumberOrLetter[0])) {
// newseriesCodeNumberOrLetter = seriesCodeNumberOrLetter;
// }
// else {
// newseriesCodeNumberOrLetter = convertStringToLetters(seriesCodeNumberOrLetter);
// }
// std::string combineSeriesCode = convertStringToLetters(seriesCodeLetter) + newseriesCodeNumberOrLetter;
// if (combineSeriesCode == "RP")combineSeriesCode = "PR";
// // Convert the newSuburbCode to an integer
// int numericValue = std::stoi(newSuburbCode);
// // Check if it is within the range 11 to 99
// if (numericValue >= 11 && numericValue <= 99) {
// if (numericValue == 13)newSuburbCode = "73";
// if (numericValue == 42)newSuburbCode = "62";
// if (numericValue == 44)newSuburbCode = "64";
// if (numericValue == 45)newSuburbCode = "65";
// if (numericValue == 46)newSuburbCode = "66";
// if (numericValue == 87)newSuburbCode = "81";
// if (numericValue == 91)newSuburbCode = "97";
// analysedLP = newSuburbCode + combineSeriesCode + convertStringToDigits(numberCode);
// }
// else {
// analysedLP = "";
// }
//}
//else {// Diplomacy LP
// std::string suburbCode = cleanOCRText.substr(0, 2);// from 11 to 99
// std::string nationalCode = cleanOCRText.substr(2, 3);//012,123
// std::string specialCode = cleanOCRText.substr(5, 2);//NN, NG, CV, QT
// std::string numberCode = cleanOCRText.substr(7, 2);// 00 to 99
// std::string newSuburbCode = convertStringToDigits(suburbCode);
// // Convert the newSuburbCode to an integer
// int numericValue = std::stoi(newSuburbCode);
// // Check if it is within the range 11 to 99
// if (numericValue >= 11 && numericValue <= 99) {
// if (numericValue == 13)newSuburbCode = "73";
// if (numericValue == 42)newSuburbCode = "62";
// if (numericValue == 44)newSuburbCode = "64";
// if (numericValue == 45)newSuburbCode = "65";
// if (numericValue == 46)newSuburbCode = "66";
// if (numericValue == 87)newSuburbCode = "81";
// if (numericValue == 91)newSuburbCode = "97";
// analysedLP = newSuburbCode + nationalCode + convertStringToDigits(numberCode);
// }
// else {
// analysedLP = "";
// }
//}
}
else if (ocrSize == 10) {// special case for depolmat LP
analysedLP = cleanOCRText;
//int dIndex = searchDiplomacyLP(cleanOCRText);
//if (dIndex != 5) {
// std::string suburbCode = cleanOCRText.substr(0, 2);// from 11 to 99
// std::string seriesCodeLetter = cleanOCRText.substr(2, 1);//A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
// std::string seriesCodeNumberOrLetter = cleanOCRText.substr(3, 1);//number or A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
// std::string numberCode = cleanOCRText.substr(4, 6);// 00000 to 99999
// std::string newSuburbCode = convertStringToDigits(suburbCode);
// std::string newseriesCodeNumberOrLetter;
// if (std::isdigit(seriesCodeNumberOrLetter[0])) {
// newseriesCodeNumberOrLetter = seriesCodeNumberOrLetter;
// }
// else {
// newseriesCodeNumberOrLetter = convertStringToLetters(seriesCodeNumberOrLetter);
// }
// std::string combineSeriesCode = convertStringToLetters(seriesCodeLetter) + newseriesCodeNumberOrLetter;
// if (combineSeriesCode == "RP")combineSeriesCode = "PR";
// // Convert the newSuburbCode to an integer
// int numericValue = std::stoi(newSuburbCode);
// // Check if it is within the range 11 to 99
// if (numericValue >= 11 && numericValue <= 99) {
// if (numericValue == 13)newSuburbCode = "73";
// if (numericValue == 42)newSuburbCode = "62";
// if (numericValue == 44)newSuburbCode = "64";
// if (numericValue == 45)newSuburbCode = "65";
// if (numericValue == 46)newSuburbCode = "66";
// if (numericValue == 87)newSuburbCode = "81";
// if (numericValue == 91)newSuburbCode = "97";
// analysedLP = newSuburbCode + combineSeriesCode + convertStringToDigits(numberCode);
// }
// else {
// analysedLP = "";
// }
//}
//else {// Diplomacy LP
// std::string suburbCode = cleanOCRText.substr(0, 2);// from 11 to 99
// std::string nationalCode = cleanOCRText.substr(2, 3);//012,123
// std::string specialCode = cleanOCRText.substr(5, 2);//NN, NG, CV, QT
// std::string numberCode = cleanOCRText.substr(7, 3);// 00 to 99
// std::string newSuburbCode = convertStringToDigits(suburbCode);
// // Convert the newSuburbCode to an integer
// int numericValue = std::stoi(newSuburbCode);
// // Check if it is within the range 11 to 99
// if (numericValue >= 11 && numericValue <= 99) {
// if (numericValue == 13)newSuburbCode = "73";
// if (numericValue == 42)newSuburbCode = "62";
// if (numericValue == 44)newSuburbCode = "64";
// if (numericValue == 45)newSuburbCode = "65";
// if (numericValue == 46)newSuburbCode = "66";
// if (numericValue == 87)newSuburbCode = "81";
// if (numericValue == 91)newSuburbCode = "97";
// analysedLP = newSuburbCode + nationalCode + convertStringToDigits(numberCode);
// }
// else {
// analysedLP = "";
// }
//}
}
else {
// We will need to analyse the text for special cases
analysedLP = "";
}
break;
//case Country::CHINA:
// break;
//case Country::AUSTRALIA:
// break;
//case Country::USA:
// break;
}
return analysedLP;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_OD::AnalyseLicensePlateText", e.what(), __FILE__, __LINE__);
return "";
}
}
int ANSALPR_CPU::findSubstringIndex(const std::string& str) {
//std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
// List of substrings to search for
std::string substrings[] = { "NN", "CV", "NG", "QT" };
// Iterate through each substring
for (const std::string& sub : substrings) {
// Use std::string::find to search for the substring in the given string
std::size_t pos = str.find(sub);
// If the substring is found, return the index
if (pos != std::string::npos) {
return static_cast<int>(pos); // Cast to int and return the index
}
}
// If none of the substrings is found, return -1
return -1;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::findSubstringIndex", e.what(), __FILE__, __LINE__);
return -1;
}
}
char ANSALPR_CPU::fixLPDigit(char c) {
//std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
switch (c) {
case 'b':
return '6';
case 'c':
return '0';
case 'f':
case 't':
return '4';
case 'j':
case 'i':
case 'l':
return '1';
case 's':
return '5';
case 'g':
case 'q':
case 'y':
return '9';
case 'o':
return '0';
default:
return c; // If the character is not a letter to convert, return it unchanged
}
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::fixLPDigit", e.what(), __FILE__, __LINE__);
return c;
}
}
//only accept these letters: A, B, C, D, E, F, G, H, K, L, M, N, P, S, T, U, V, X, Y, Z
// I, J, O, Q, R, W
char ANSALPR_CPU::convertDigitToLetter(char c) {
//std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
switch (c) {
case '0':
case 'o':
case 'O':
case 'Q':
return 'C'; // '0' is typically mapped to 'O' or 'C', choosing 'O' to match letter set
case '1':
case 'I':
case 'i':
case 'l':
case 'J':
return 'L'; // '1' is commonly confused with 'I'
case '2':
case 'z':
return 'Z'; // '2' resembles 'Z' in some fonts
case '3':
return 'E'; // '3' can resemble 'E' in some cases
case '4':
return 'A'; // '4' can resemble 'A' or 'H', choosing 'A'
case '5':
case 's':
return 'S'; // '5' looks similar to 'S'
case '6':
case 'g':
return 'G'; // '6' resembles 'G'
case '7':
return 'T'; // '7' is often confused with 'T'
case '8':
case 'b':
return 'B'; // '8' resembles 'B'
case '9':
case 'R':
return 'P'; // '9' is close to 'P'
case 'W':
case 'w':
return 'V'; // 'W' is close to 'V'
default:
return c; // If the character is not a digit to convert, return it unchanged
}
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::convertDigitToLetter", e.what(), __FILE__, __LINE__);
return c;
}
}
char ANSALPR_CPU::convertLetterToDigit(char c) {
// std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
switch (c) {
// Convert common letter confusions with digits
case 'B':
case 'b': // Adding lowercase 'b' to match common mistypes
return '8';
case 'I':
case 'i':
case 'J': // Capital 'J' can also resemble '1'
case 'j':
case 'L':
case 'l':
return '1';
case 'S':
case 's':
return '5';
case 'G':
case 'g': // Adding lowercase 'g' for better matching
return '6';
case 'O':
case 'o':
case 'Q': // 'Q' can also be misread as '0'
case 'U':
case 'u': // Adding lowercase 'u' as it resembles '0'
return '0';
case 'T': // Capital 'T' sometimes looks like '7'
return '7';
case 'F':
case 'f':
case 't':
return '4';
case 'Y': // Capital 'Y' may resemble '9'
case 'y':
case 'q':
return '9';
default:
return '0'; // If no conversion, return the character unchanged
}
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::convertLetterToDigit", e.what(), __FILE__, __LINE__);
return c;
}
}
// Function to convert string to digits, skipping conversion if the character is already a digit
std::string ANSALPR_CPU::convertStringToDigits(const std::string& input) {
// std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
std::string result;
for (char c : input) {
if (std::isdigit(c)) {
result += c; // Skip conversion if the character is a digit
}
else {
result += convertLetterToDigit(c); // Convert if it's a letter
}
}
return result;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::convertStringToDigits", e.what(), __FILE__, __LINE__);
return input;
}
}
// Function to convert string to letters, skipping conversion if the character is already a letter
std::string ANSALPR_CPU::convertStringToLetters(const std::string& input) {
//std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
std::string result;
for (char c : input) {
if (std::isalpha(c)) {
result += c; // Skip conversion if the character is already a letter
}
else {
result += convertDigitToLetter(c); // Convert if it's a digit
}
}
return result;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::convertStringToLetters", e.what(), __FILE__, __LINE__);
return input;
}
}
int ANSALPR_CPU::searchDiplomacyLP(const std::string& input) {
//std::lock_guard<std::recursive_mutex> lock(_mutex);
// List of substrings to search for
try {
std::string substrings[] = { "NN", "NG", "CV", "QT" };
// Initialize index to -1 (not found)
int foundIndex = -1;
// Loop through the substrings
for (const auto& sub : substrings) {
// Find the index of the current substring
size_t index = input.find(sub);
// If the substring is found and either no other substrings have been found,
// or this substring occurs at an earlier position, update foundIndex.
if (index != std::string::npos && (foundIndex == -1 || index < foundIndex)) {
foundIndex = index;
}
}
return foundIndex; // If none are found, returns -1
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::searchDiplomacyLP", e.what(), __FILE__, __LINE__);
return -1;
}
}
bool ANSALPR_CPU::ValidateVNMotobikeLP(const std::string& input) {
// std::lock_guard<std::recursive_mutex> lock(_mutex);
// Search for the string in the list
auto it = std::find(ValidVNMotobikeList.begin(), ValidVNMotobikeList.end(), input);
// Check if found
if (it != ValidVNMotobikeList.end()) {
return true;
}
else {
return false;
}
}
bool ANSALPR_CPU::ValidateVNCarLP(const std::string& input) {
// std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
// Search for the string in the list
auto it = std::find(ValidVNCarList.begin(), ValidVNCarList.end(), input);
// Check if found
if (it != ValidVNCarList.end()) {
return true;
}
else {
return false;
}
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSALPR_CPU::ValidateVNCarLP", e.what(), __FILE__, __LINE__);
return false;
}
}
// Align plate
cv::Mat ANSALPR_CPU::alignPlateForOCR(const cv::Mat& fullImage, const cv::Rect& bbox) {
try {
cv::Rect safeBox = bbox & cv::Rect(0, 0, fullImage.cols, fullImage.rows);
if (safeBox.width < 10 || safeBox.height < 10)
return fullImage(safeBox);// fullImage(safeBox).clone();
cv::Mat roi = fullImage(safeBox);// fullImage(safeBox).clone();
cv::Mat gray;
cv::cvtColor(roi, gray, cv::COLOR_BGR2GRAY);
// Enhance contours
cv::Mat binary;
cv::adaptiveThreshold(gray, binary, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, 15, 10);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
if (contours.empty()) {
cv::Mat enhancedROI = enhanceForOCR(roi); // fallback
#ifdef FNS_DEBUG
try {
cv::Mat debugLeft, debugRight;
cv::resize(roi, debugLeft, cv::Size(240, 80));
cv::resize(enhancedROI, debugRight, cv::Size(240, 80));
cv::Mat combined;
cv::hconcat(debugLeft, debugRight, combined);
cv::putText(combined, "Raw", cv::Point(10, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
cv::putText(combined, "Aligned", cv::Point(250, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 0, 0), 1);
cv::imshow("LPR Cropped + Rotated", combined);
cv::waitKey(1);
}
catch (const std::exception& e) {
std::cerr << "LPR Debug Error: " << e.what() << std::endl;
}
#endif
return enhancedROI;
}
cv::Point2f roiCenter(static_cast<float>(roi.cols) / 2, static_cast<float>(roi.rows) / 2);
float minDist = std::numeric_limits<float>::max();
int bestIdx = -1;
const float minWidth = roi.cols * 0.5f;
const float minHeight = roi.rows * 0.5f;
const float minAreaRatio = 0.3f;
for (size_t i = 0; i < contours.size(); ++i) {
cv::RotatedRect rect = cv::minAreaRect(contours[i]);
float width = rect.size.width;
float height = rect.size.height;
float areaRect = width * height;
float areaContour = cv::contourArea(contours[i]);
if (width < minWidth || height < minHeight) continue;
if (areaContour / areaRect < minAreaRatio) continue;
float dist = cv::norm(rect.center - roiCenter);
if (dist < minDist) {
minDist = dist;
bestIdx = static_cast<int>(i);
}
}
if (bestIdx == -1) {
cv::Mat enhancedROI= enhanceForOCR(roi); // fallback
#ifdef FNS_DEBUG
try {
cv::Mat debugLeft, debugRight;
cv::resize(roi, debugLeft, cv::Size(240, 80));
cv::resize(enhancedROI, debugRight, cv::Size(240, 80));
cv::Mat combined;
cv::hconcat(debugLeft, debugRight, combined);
cv::putText(combined, "Raw", cv::Point(10, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
cv::putText(combined, "Aligned", cv::Point(250, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 0, 0), 1);
cv::imshow("LPR Cropped + Rotated", combined);
cv::waitKey(1);
}
catch (const std::exception& e) {
std::cerr << "LPR Debug Error: " << e.what() << std::endl;
}
#endif
return enhancedROI;
}
// Align and crop using best rotated rect
cv::RotatedRect bestRect = cv::minAreaRect(contours[bestIdx]);
float angle = bestRect.angle;
if (bestRect.size.width < bestRect.size.height) {
angle += 90.0f;
std::swap(bestRect.size.width, bestRect.size.height);
}
angle = std::clamp(angle, -45.0f, 45.0f);
// Rotate the image
cv::Mat rotationMatrix = cv::getRotationMatrix2D(cv::Point2f(roi.cols / 2.0f, roi.rows / 2.0f), angle, 1.0);
cv::Mat rotated;
cv::warpAffine(roi, rotated, rotationMatrix, roi.size(), cv::INTER_LINEAR, cv::BORDER_REPLICATE);
// Transform the rect center after rotation
cv::Point2f newCenter;
{
cv::Mat ptMat = (cv::Mat_<double>(3, 1) << bestRect.center.x, bestRect.center.y, 1.0);
cv::Mat newCenterMat = rotationMatrix * ptMat;
newCenter = cv::Point2f(static_cast<float>(newCenterMat.at<double>(0)), static_cast<float>(newCenterMat.at<double>(1)));
}
// Apply small padding
const int padding = 2;
cv::Size paddedSize(
std::min(rotated.cols, static_cast<int>(bestRect.size.width + 2 * padding)),
std::min(rotated.rows, static_cast<int>(bestRect.size.height + 2 * padding))
);
cv::Mat rawCropped;
cv::getRectSubPix(rotated, paddedSize, newCenter, rawCropped);
cv::Mat cropped = enhanceForOCR(rawCropped);
#ifdef FNS_DEBUG
try {
cv::Mat debugRoi = roi.clone();
cv::drawContours(debugRoi, contours, bestIdx, cv::Scalar(0, 255, 0), 1);
cv::Point2f points[4];
bestRect.points(points);
for (int j = 0; j < 4; ++j)
cv::line(debugRoi, points[j], points[(j + 1) % 4], cv::Scalar(255, 0, 0), 1);
cv::Mat debugLeft, debugRight;
cv::resize(debugRoi, debugLeft, cv::Size(240, 80));
cv::resize(cropped, debugRight, cv::Size(240, 80));
cv::Mat combined;
cv::hconcat(debugLeft, debugRight, combined);
cv::putText(combined, "Raw", cv::Point(10, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
cv::putText(combined, "Aligned", cv::Point(250, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255, 0, 0), 1);
cv::imshow("LPR Cropped + Rotated", combined);
cv::waitKey(1);
}
catch (const std::exception& e) {
std::cerr << "LPR Debug Error: " << e.what() << std::endl;
}
#endif
return cropped;
}
catch (const std::exception& e) {
std::cerr << "alignPlateForOCR (rotated crop) exception: " << e.what() << std::endl;
return fullImage(bbox & cv::Rect(0, 0, fullImage.cols, fullImage.rows)).clone(); // fallback
}
}
cv::Mat ANSALPR_CPU::enhanceForOCR(const cv::Mat& plateROIOriginal) {
if (plateROIOriginal.empty()) {
std::cerr << "Error: plateROI is empty!" << std::endl;
return cv::Mat();
}
cv::Mat plateROI;
// Step 1: Upscale for OCR clarity
cv::resize(plateROIOriginal, plateROI, cv::Size(), 2.0, 2.0, cv::INTER_LANCZOS4);
// Step 2: Grayscale
cv::Mat gray;
if (plateROI.channels() == 3) {
cv::cvtColor(plateROI, gray, cv::COLOR_BGR2GRAY);
}
else {
gray = plateROI;// plateROI.clone();
}
// Step 3: Gentle denoise to preserve edges
cv::Mat denoised;
cv::bilateralFilter(gray, denoised, 7, 50, 50); // Less aggressive
// Step 4: First sharpening (Unsharp Masking)
cv::Mat blurred, unsharp;
cv::GaussianBlur(denoised, blurred, cv::Size(0, 0), 1.5);
cv::addWeighted(denoised, 1.8, blurred, -0.8, 0, unsharp);
// Step 5: Enhance contrast locally using CLAHE
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(4.0, cv::Size(8, 8)); // stronger clip
cv::Mat contrastEnhanced;
clahe->apply(unsharp, contrastEnhanced);
// Step 6: Additional edge sharpening using Laplacian
cv::Mat lap, lapAbs, sharpened;
cv::Laplacian(contrastEnhanced, lap, CV_16S, 3);
cv::convertScaleAbs(lap, lapAbs);
cv::addWeighted(contrastEnhanced, 1.2, lapAbs, -0.3, 0, sharpened); // softer laplacian weight
// Optional: Slight threshold to suppress low values (minor noise cleaning)
// cv::threshold(sharpened, sharpened, 10, 255, cv::THRESH_TOZERO);
// Step 7: Convert back to 3-channel RGB for OCR models like PaddleOCR
cv::Mat ocrInput;
cv::cvtColor(sharpened, ocrInput, cv::COLOR_GRAY2BGR);
return ocrInput;
}
};