#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 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(frame.cols / model_input_shape_.width); factor_.y = static_cast(frame.rows / model_input_shape_.height); float* input_data = reinterpret_cast(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 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 ANSOVDetector::LoadClassesFromFile() { std::lock_guard lock(_mutex); std::vector _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::vectorANSOVDetector::PostProcessing() { std::lock_guard lock(_mutex); try { std::vector class_list; std::vector confidence_list; std::vector box_list; float* detections = inference_request_.get_output_tensor().data(); 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(0, i); const float y = detection_outputs.at(1, i); const float w = detection_outputs.at(2, i); const float h = detection_outputs.at(3, i); cv::Rect box; box.x = static_cast(x); box.y = static_cast(y); box.width = static_cast(w); box.height = static_cast(h); box_list.push_back(box); } } std::vector NMS_result; cv::dnn::NMSBoxes(box_list, confidence_list, _modelConfThreshold, _modelMNSThreshold, NMS_result); std::vector 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 result; result.clear(); std::cout << "ANSOVDetector::PostprocessImage. " << e.what() << std::endl; return result; } } std::string ANSOVDetector::GetOpenVINODevice() { std::vector 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 & _classes) { std::lock_guard 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 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> 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> 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 ANSOVDetector::RunInference(const cv::Mat& input) { std::lock_guard lock(_mutex); std::vector 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; } } /// /// ANSLRP_CPU /// 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 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 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 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 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();// 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 ANSALPR_CPU::RunInference(const cv::Mat& input, const std::string &cameraId) { // No coarse _mutex — sub-components have their own fine-grained locks. std::vector 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)) { #ifdef FNS_DEBUG // Corrected preprocessor directive cv::rectangle(draw, detectedArea, cv::Scalar(0, 0, 255), 2); // RED for detectedArea #endif // 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(); //std::vector lprOutputRaw = _lpDetector->RunInference(activeFrame, cameraId); //std::vector lprOutput = AdjustLicensePlateBoundingBoxes(lprOutputRaw, _detectedArea, frame.size(), 3.0); std::vector lprOutputRaw = _lprDetector->RunInference(activeFrame, cameraId); float iouThreshold = 0.4; std::vector 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 res_ocr; { std::lock_guard ocrLock(_ocrMutex); res_ocr = ppocr->ocr(alignedLPR); } 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) 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. std::vector 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 lprOutputRaw = _lprDetector->RunStaticInference(frame, roi, cameraId); //4. Apply Non-Maximum Suppression (NMS) to merge overlapping results float iouThreshold = 0.1; std::vector lprOutput = ANSUtilityHelper::ApplyNMS(lprOutputRaw, iouThreshold); if (lprOutput.size() > 0) { std::vector 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 res_ocr; { std::lock_guard ocrLock(_ocrMutex); res_ocr = ppocr->ocr(lprImage); } int detectionSize = res_ocr.size(); if ((detectionSize > 0) && (detectionSize < 3)) { for (int n = 0; n < res_ocr.size(); n++) { // number of detections 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 & Bbox, std::string& lprResult) { // No coarse _mutex — delegates to Inference(input, Bbox, lprResult, cameraId) 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& Bbox, std::string& lprResult, const std::string& cameraId) { // No coarse _mutex — sub-components have fine-grained locks. // 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; if (input.channels() == 1) { cv::cvtColor(input, localFrame, cv::COLOR_GRAY2BGR); } const cv::Mat& frame = (input.channels() == 1) ? localFrame : input; const int frameWidth = frame.cols; const int frameHeight = frame.rows; constexpr int padding = 10; std::vector 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 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 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 res_ocr; { std::lock_guard ocrLock(_ocrMutex); res_ocr = ppocr->ocr(alignedLPR); } 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 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(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 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 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 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 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 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 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 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 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> 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(roi.cols) / 2, static_cast(roi.rows) / 2); float minDist = std::numeric_limits::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(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_(3, 1) << bestRect.center.x, bestRect.center.y, 1.0); cv::Mat newCenterMat = rotationMatrix * ptMat; newCenter = cv::Point2f(static_cast(newCenterMat.at(0)), static_cast(newCenterMat.at(1))); } // Apply small padding const int padding = 2; cv::Size paddedSize( std::min(rotated.cols, static_cast(bestRect.size.width + 2 * padding)), std::min(rotated.rows, static_cast(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 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; } };