#include "ANSLPR.h" #include #include "ANSLibsLoader.h" static bool ansalprLicenceValid = false; // Global once_flag to protect license checking static std::once_flag ansalprLicenseOnceFlag; template T GetData(const boost::property_tree::ptree& pt, const std::string& key) { T ret; if (boost::optional data = pt.get_optional(key)) { ret = data.get(); } return ret; } namespace ANSCENTER { // Checker class void ALPRChecker::Init(int framesToStore) { maxFrames = framesToStore; } int ALPRChecker::levenshteinDistance(const std::string& s1, const std::string& s2) { try { int len1 = static_cast(s1.size()); int len2 = static_cast(s2.size()); std::vector> dp(len1 + 1, std::vector(len2 + 1)); for (int i = 0; i <= len1; i++) dp[i][0] = i; for (int j = 0; j <= len2; j++) dp[0][j] = j; for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { if (s1[i - 1] == s2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = std::min({ dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1 }); } } } return dp[len1][len2]; } catch (const std::exception& e) { return -1; } } float ALPRChecker::computeIoU(const cv::Rect& a, const cv::Rect& b) { int x1 = std::max(a.x, b.x); int y1 = std::max(a.y, b.y); int x2 = std::min(a.x + a.width, b.x + b.width); int y2 = std::min(a.y + a.height, b.y + b.height); int intersection = std::max(0, x2 - x1) * std::max(0, y2 - y1); int unionArea = a.area() + b.area() - intersection; return (unionArea > 0) ? static_cast(intersection) / unionArea : 0.f; } std::string ALPRChecker::majorityVote(const std::deque& history) { if (history.empty()) return ""; // Count frequency of each plate text, weighted by recency // (more recent entries get higher weight) std::map weightedFreq; int n = static_cast(history.size()); for (int i = 0; i < n; i++) { // Recency weight: older=1.0, newest=2.0 float weight = 1.0f + static_cast(i) / std::max(1, n - 1); weightedFreq[history[i]] += weight; } // Also group similar plates (Levenshtein <= 1) under the most frequent variant std::string bestText; float bestScore = 0.f; for (const auto& kv : weightedFreq) { float totalScore = kv.second; // Add scores from similar plates for (const auto& other : weightedFreq) { if (other.first == kv.first) continue; // Use quick length check to skip obvious non-matches int lenDiff = std::abs(static_cast(kv.first.size()) - static_cast(other.first.size())); if (lenDiff <= 1) { // Only compute Levenshtein for candidates within 1 char length difference int dist = 0; // Inline fast Levenshtein for short strings const std::string& s1 = kv.first; const std::string& s2 = other.first; int len1 = static_cast(s1.size()), len2 = static_cast(s2.size()); if (len1 <= 15 && len2 <= 15) { std::vector prev(len2 + 1), curr(len2 + 1); for (int j = 0; j <= len2; j++) prev[j] = j; for (int i = 1; i <= len1; i++) { curr[0] = i; for (int j = 1; j <= len2; j++) { if (s1[i - 1] == s2[j - 1]) curr[j] = prev[j - 1]; else curr[j] = 1 + std::min({ prev[j], curr[j - 1], prev[j - 1] }); } std::swap(prev, curr); } dist = prev[len2]; } if (dist == 1) { totalScore += other.second * 0.5f; // partial credit for 1-edit variants } } } if (totalScore > bestScore) { bestScore = totalScore; bestText = kv.first; } } return bestText; } // Original API — backward compatible, no spatial tracking std::string ALPRChecker::checkPlate(const std::string& cameraId, const std::string& detectedPlate) { // Delegate to spatial version with empty box (disables IoU matching) return checkPlate(cameraId, detectedPlate, cv::Rect()); } // Enhanced API with bounding box for spatial plate tracking std::string ALPRChecker::checkPlate(const std::string& cameraId, const std::string& detectedPlate, const cv::Rect& plateBox) { std::lock_guard lock(_mutex); try { if (detectedPlate.empty()) return detectedPlate; auto& plates = trackedPlates[cameraId]; bool hasSpatial = (plateBox.width > 0 && plateBox.height > 0); // Increment framesSinceLastSeen for all plates (will be reset for matched plate) for (auto& p : plates) { p.framesSinceLastSeen++; } // Periodic pruning: remove plates not seen for a long time _pruneCounter++; if (_pruneCounter >= 30 && plates.size() > 10) { _pruneCounter = 0; int staleThreshold = maxFrames * 3; // 180 frames = ~6 seconds at 30fps plates.erase( std::remove_if(plates.begin(), plates.end(), [staleThreshold](const TrackedPlate& p) { return p.framesSinceLastSeen > staleThreshold; }), plates.end()); } // Step 1: Find matching tracked plate // Strategy: try IoU first, then text similarity as fallback int bestIdx = -1; float bestIoU = 0.f; // 1a. Try spatial IoU matching if (hasSpatial) { for (int i = 0; i < static_cast(plates.size()); i++) { float iou = computeIoU(plateBox, plates[i].lastBox); if (iou > iouMatchThreshold && iou > bestIoU) { bestIoU = iou; bestIdx = i; } } } // 1b. If IoU failed, try text similarity against locked texts if (bestIdx < 0) { for (int i = 0; i < static_cast(plates.size()); i++) { if (!plates[i].lockedText.empty()) { int dist = levenshteinDistance(detectedPlate, plates[i].lockedText); if (dist <= 1) { bestIdx = i; break; } } // Also check against recent history even if not locked if (bestIdx < 0 && !plates[i].textHistory.empty()) { int checkCount = std::min(static_cast(plates[i].textHistory.size()), 3); for (int j = static_cast(plates[i].textHistory.size()) - 1; j >= static_cast(plates[i].textHistory.size()) - checkCount; j--) { int dist = levenshteinDistance(detectedPlate, plates[i].textHistory[j]); if (dist <= 1) { bestIdx = i; break; } } } } } // Step 2: Create new tracked plate if no match found if (bestIdx < 0) { // Before creating a new plate, check if this detection conflicts with // a nearby locked plate — if so, it's likely a bad OCR read and we should // return the locked text instead of creating a garbage entry if (hasSpatial) { float bestProximity = 0.f; int proximityIdx = -1; for (int i = 0; i < static_cast(plates.size()); i++) { if (plates[i].lockedText.empty()) continue; if (plates[i].lastBox.width <= 0 || plates[i].lastBox.height <= 0) continue; float cx1 = plateBox.x + plateBox.width * 0.5f; float cy1 = plateBox.y + plateBox.height * 0.5f; float cx2 = plates[i].lastBox.x + plates[i].lastBox.width * 0.5f; float cy2 = plates[i].lastBox.y + plates[i].lastBox.height * 0.5f; float dx = std::abs(cx1 - cx2); float dy = std::abs(cy1 - cy2); float maxDim = std::max(static_cast(plateBox.width), static_cast(plates[i].lastBox.width)); if (dx < maxDim * 2.0f && dy < maxDim * 1.5f) { float proximity = 1.0f / (1.0f + dx + dy); if (proximity > bestProximity) { bestProximity = proximity; proximityIdx = i; } } } if (proximityIdx >= 0) { auto& tp = plates[proximityIdx]; tp.lastBox = plateBox; tp.framesSinceLastSeen = 0; tp.textHistory.push_back(detectedPlate); return tp.lockedText; } } TrackedPlate newPlate; newPlate.lastBox = plateBox; newPlate.textHistory.push_back(detectedPlate); plates.push_back(newPlate); return detectedPlate; } // Step 3: Update matched tracked plate auto& tp = plates[bestIdx]; if (hasSpatial) tp.lastBox = plateBox; tp.framesSinceLastSeen = 0; // Reset — this plate was just seen // Store RAW detection (not corrected — avoids feedback loop) tp.textHistory.push_back(detectedPlate); if (static_cast(tp.textHistory.size()) > maxFrames) { tp.textHistory.pop_front(); } // Step 4: Majority vote to find stable text std::string voted = majorityVote(tp.textHistory); // Step 5: Lock logic — once we have enough consistent reads, lock the text if (tp.lockedText.empty()) { int matchCount = 0; int lookback = std::min(static_cast(tp.textHistory.size()), maxFrames); for (int i = static_cast(tp.textHistory.size()) - 1; i >= static_cast(tp.textHistory.size()) - lookback; i--) { if (tp.textHistory[i] == voted) matchCount++; } if (matchCount >= minVotesToStabilize) { tp.lockedText = voted; tp.lockCount = 1; } return voted; } else { int dist = levenshteinDistance(detectedPlate, tp.lockedText); if (dist == 0) { tp.lockCount++; return tp.lockedText; } else { int newDist = levenshteinDistance(voted, tp.lockedText); if (newDist > 1) { int newMatchCount = 0; int recent = std::min(static_cast(tp.textHistory.size()), minVotesToStabilize * 2); for (int i = static_cast(tp.textHistory.size()) - 1; i >= static_cast(tp.textHistory.size()) - recent; i--) { if (tp.textHistory[i] == voted) newMatchCount++; } if (newMatchCount >= minVotesToStabilize) { tp.lockedText = voted; tp.lockCount = 1; return tp.lockedText; } } return tp.lockedText; } } } catch (const std::exception& e) { return detectedPlate; } } // static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) { try { ansalprLicenceValid = ANSCENTER::ANSLicenseHelper::LicenseVerification(licenseKey, 1006, "ANSALPR");//Default productId=1006 if (!ansalprLicenceValid) { // we also support ANSTS license ansalprLicenceValid = ANSCENTER::ANSLicenseHelper::LicenseVerification(licenseKey, 1003, "ANSVIS");//Default productId=1003 (ANSVIS) } } catch (std::exception& e) { ansalprLicenceValid = false; } } void ANSALPR::CheckLicense() { try { // Check once globally std::call_once(ansalprLicenseOnceFlag, [this]() { VerifyGlobalANSALPRLicense(_licenseKey); }); // Update this instance's local license flag _licenseValid = ansalprLicenceValid; } catch (const std::exception& e) { this->_logger.LogFatal("ANSODBase::CheckLicense. Error:", e.what(), __FILE__, __LINE__); } } bool ANSALPR::LoadEngine() { return true; } bool ANSALPR::Initialize(const std::string& licenseKey, const std::string& modelZipFilePath, const std::string& modelZipPassword, double detectorThreshold, double ocrThreshold, double colorThreshold) { try { ANSCENTER::ANSLibsLoader::Initialize(); _licenseKey = licenseKey; _licenseValid = false; _detectorThreshold = detectorThreshold; _ocrThreshold = ocrThreshold; _colorThreshold = colorThreshold; ANSALPR::CheckLicense(); if (!_licenseValid) { this->_logger.LogError("ANSALPR::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::Initialize", "Model zip file is not exist", __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); //this->_logger.LogDebug("ANSFDBase::Initialize. Model name", modelName); 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("ANSFDBase::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 } return true; } catch (std::exception& e) { this->_logger.LogFatal("ANSALPR::Initialize", e.what(), __FILE__, __LINE__); return false; } } ANSALPR::~ANSALPR(){}; bool ANSALPR::Destroy() { return true; }; std::vector ANSCENTER::ANSALPR::GetBoundingBoxes(const std::string& strBBoxes) { std::vector bBoxes; bBoxes.clear(); std::stringstream ss; ss << strBBoxes; boost::property_tree::ptree pt; boost::property_tree::read_json(ss, pt); BOOST_FOREACH(const boost::property_tree::ptree::value_type & child, pt.get_child("results")) { const boost::property_tree::ptree& result = child.second; const auto x = GetData(result, "x"); const auto y = GetData(result, "y"); const auto width = GetData(result, "width"); const auto height = GetData(result, "height"); cv::Rect rectTemp; rectTemp.x = x; rectTemp.y = y; rectTemp.width = width; rectTemp.height = height; bBoxes.push_back(rectTemp); } return bBoxes; } std::string ANSCENTER::ANSALPR::PolygonToString(const std::vector& polygon) { if (polygon.empty()) { return ""; } std::string result; result.reserve(polygon.size() * 20); char buffer[64]; for (size_t i = 0; i < polygon.size(); ++i) { if (i > 0) { snprintf(buffer, sizeof(buffer), ";%.3f;%.3f", polygon[i].x, polygon[i].y); } else { snprintf(buffer, sizeof(buffer), "%.3f;%.3f", polygon[i].x, polygon[i].y); } result += buffer; } return result; } static std::string DoubleEscapeUnicode(const std::string& utf8Str) { bool hasNonAscii = false; for (unsigned char c : utf8Str) { if (c >= 0x80) { hasNonAscii = true; break; } } if (!hasNonAscii) return utf8Str; std::string result; result.reserve(utf8Str.size() * 2); size_t i = 0; while (i < utf8Str.size()) { unsigned char c = static_cast(utf8Str[i]); if (c < 0x80) { result += utf8Str[i++]; continue; } uint32_t cp = 0; if ((c & 0xE0) == 0xC0 && i + 1 < utf8Str.size()) { cp = ((c & 0x1F) << 6) | (static_cast(utf8Str[i + 1]) & 0x3F); i += 2; } else if ((c & 0xF0) == 0xE0 && i + 2 < utf8Str.size()) { cp = ((c & 0x0F) << 12) | ((static_cast(utf8Str[i + 1]) & 0x3F) << 6) | (static_cast(utf8Str[i + 2]) & 0x3F); i += 3; } else if ((c & 0xF8) == 0xF0 && i + 3 < utf8Str.size()) { cp = ((c & 0x07) << 18) | ((static_cast(utf8Str[i + 1]) & 0x3F) << 12) | ((static_cast(utf8Str[i + 2]) & 0x3F) << 6) | (static_cast(utf8Str[i + 3]) & 0x3F); i += 4; } else { i++; continue; } if (cp <= 0xFFFF) { char buf[8]; snprintf(buf, sizeof(buf), "\\u%04x", cp); result += buf; } else { cp -= 0x10000; char buf[16]; snprintf(buf, sizeof(buf), "\\u%04x\\u%04x", 0xD800 + (uint16_t)(cp >> 10), 0xDC00 + (uint16_t)(cp & 0x3FF)); result += buf; } } return result; } std::string ANSALPR::VectorDetectionToJsonString(const std::vector& dets) { if (dets.empty()) { return R"({"results":[]})"; } try { nlohmann::json root; auto& results = root["results"] = nlohmann::json::array(); for (const auto& det : dets) { results.push_back({ {"class_id", std::to_string(det.classId)}, //{"track_id", std::to_string(det.trackId)}, {"track_id", std::to_string(0)}, {"class_name", DoubleEscapeUnicode(det.className)}, {"prob", std::to_string(det.confidence)}, {"x", std::to_string(det.box.x)}, {"y", std::to_string(det.box.y)}, {"width", std::to_string(det.box.width)}, {"height", std::to_string(det.box.height)}, {"mask", ""}, {"extra_info", det.extraInfo}, {"camera_id", det.cameraId}, {"polygon", PolygonToString(det.polygon)}, {"kps", KeypointsToString(det.kps)} }); } return root.dump(-1, ' ', true); } catch (const std::exception& e) { this->_logger.LogFatal("ANSALPR::VectorDetectionToJsonString", e.what(), __FILE__, __LINE__); return R"({"results":[],"error":"Serialization failed"})"; } } void ANSALPR::SetPlateFormats(const std::vector& formats) { std::lock_guard lock(_mutex); _plateFormats.clear(); _plateFormats = formats; } void ANSALPR::SetPlateFormat(const std::string& format) { std::lock_guard lock(_mutex); _plateFormats.clear(); _plateFormats.push_back(format); } std::vector ANSALPR::GetPlateFormats() const { return _plateFormats; } std::string ANSCENTER::ANSALPR::KeypointsToString(const std::vector& kps) { if (kps.empty()) { return ""; } std::string result; result.reserve(kps.size() * 10); char buffer[32]; for (size_t i = 0; i < kps.size(); ++i) { if (i > 0) result += ';'; snprintf(buffer, sizeof(buffer), "%.3f", kps[i]); result += buffer; } return result; } std::vector ANSCENTER::ANSALPR::RectToNormalizedPolygon(const cv::Rect& rect, float imageWidth, float imageHeight) { // Ensure imageWidth and imageHeight are non-zero to avoid division by zero if (imageWidth <= 0 || imageHeight <= 0) { std::vector emptyPolygon; return emptyPolygon; } // Calculate normalized points for each corner of the rectangle std::vector polygon = { { rect.x / imageWidth, rect.y / imageHeight }, // Top-left { (rect.x + rect.width) / imageWidth, rect.y / imageHeight }, // Top-right { (rect.x + rect.width) / imageWidth, (rect.y + rect.height) / imageHeight }, // Bottom-right { rect.x / imageWidth, (rect.y + rect.height) / imageHeight } // Bottom-left }; return polygon; } // Functions for screen size division double ANSALPR::calculateDistanceToCenter(const cv::Point& center, const cv::Rect& rect) { cv::Point rectCenter(rect.x + rect.width / 2, rect.y + rect.height / 2); return std::sqrt(std::pow(rectCenter.x - center.x, 2) + std::pow(rectCenter.y - center.y, 2)); } std::vector ANSALPR::divideImage(const cv::Mat& image) { if (image.empty()) { std::cerr << "Error: Empty image!" << std::endl; return cachedSections; } cv::Size currentSize(image.cols, image.rows); // Check if the image size has changed if (currentSize == previousImageSize) { return cachedSections; // Return cached sections if size is the same } // Update previous size previousImageSize = currentSize; cachedSections.clear(); int width = image.cols; int height = image.rows; int maxDimension = std::max(width, height); int numSections = 10;// std::max(1, numSections); // Ensure at least 1 section if (maxDimension <= 2560)numSections = 8; if (maxDimension <= 1280)numSections = 6; if (maxDimension <= 960)numSections = 4; if (maxDimension <= 640)numSections = 2; if (maxDimension <= 320)numSections = 1; int gridRows = std::sqrt(numSections); int gridCols = (numSections + gridRows - 1) / gridRows; // Ensure all sections are covered int sectionWidth = width / gridCols; int sectionHeight = height / gridRows; cv::Point imageCenter(width / 2, height / 2); std::vector> distancePriorityList; // Create sections and store their distance from the center for (int r = 0; r < gridRows; ++r) { for (int c = 0; c < gridCols; ++c) { int x = c * sectionWidth; int y = r * sectionHeight; int w = (c == gridCols - 1) ? width - x : sectionWidth; int h = (r == gridRows - 1) ? height - y : sectionHeight; ImageSection section(cv::Rect(x, y, w, h)); double distance = calculateDistanceToCenter(imageCenter, section.region); distancePriorityList.emplace_back(distance, section); } } // Sort sections based on distance from center, then top-to-bottom, then left-to-right std::sort(distancePriorityList.begin(), distancePriorityList.end(), [](const std::pair& a, const std::pair& b) { if (std::abs(a.first - b.first) > 1e-5) { return a.first < b.first; // Sort by closest distance to center } // If distance is the same, prioritize top to bottom, then left to right return a.second.region.y == b.second.region.y ? a.second.region.x < b.second.region.x : a.second.region.y < b.second.region.y; }); // Assign priority int priority = 1; for (auto& entry : distancePriorityList) { entry.second.priority = priority++; cachedSections.push_back(entry.second); } return cachedSections; } std::vector ANSALPR::createSlideScreens(const cv::Mat& image) { if (image.empty()) { std::cerr << "Error: Empty image!" << std::endl; return cachedSections; } cv::Size currentSize(image.cols, image.rows); if (currentSize == previousImageSize) { return cachedSections; } previousImageSize = currentSize; cachedSections.clear(); int maxSize = std::max(image.cols, image.rows); const int minCellSize = 320; int maxSections = 10; const float minAspectRatio = 0.8f; const float maxAspectRatio = 1.2f; if (maxSize <= 640) maxSections = 1; else if (maxSize <= 960) maxSections = 2; else if (maxSize <= 1280) maxSections = 4; else if (maxSize <= 2560) maxSections = 6; else if (maxSize <= 3840) maxSections = 8; else maxSections = 10; int width = image.cols; int height = image.rows; int bestRows = 1, bestCols = 1; int bestTileSize = std::numeric_limits::max(); for (int rows = 1; rows <= maxSections; ++rows) { for (int cols = 1; cols <= maxSections; ++cols) { if (rows * cols > maxSections) continue; int tileWidth = (width + cols - 1) / cols; int tileHeight = (height + rows - 1) / rows; if (tileWidth < minCellSize || tileHeight < minCellSize) continue; float aspectRatio = static_cast(tileWidth) / static_cast(tileHeight); if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio) continue; int maxTileDim = std::max(tileWidth, tileHeight); if (maxTileDim < bestTileSize) { bestTileSize = maxTileDim; bestRows = rows; bestCols = cols; } } } // Generate tiles using bestRows, bestCols int tileWidth = (width + bestCols - 1) / bestCols; int tileHeight = (height + bestRows - 1) / bestRows; int priority = 1; for (int r = bestRows - 1; r >= 0; --r) { for (int c = 0; c < bestCols; ++c) { int x = c * tileWidth; int y = r * tileHeight; int w = std::min(tileWidth, width - x); int h = std::min(tileHeight, height - y); if (w <= 0 || h <= 0) continue; cv::Rect region(x, y, w, h); ImageSection section(region); section.priority = priority++; cachedSections.push_back(section); } } return cachedSections; } int ANSALPR::getHighestPriorityRegion() { if (!cachedSections.empty()) { return cachedSections.front().priority; // First element has the highest priority } return 0; // Return empty rect if no sections exist } int ANSALPR::getLowestPriorityRegion() { if (!cachedSections.empty()) { return cachedSections.back().priority; // Last element has the lowest priority } return 0; // Return empty rect if no sections exist } cv::Rect ANSALPR::getRegionByPriority(int priority) { for (const auto& section : cachedSections) { if (section.priority == priority) { return section.region; } } return cv::Rect(); // Return empty rect if priority not found } std::vector ANSALPR::AdjustLicensePlateBoundingBoxes( const std::vector& detectionsInROI, const cv::Rect& roi, const cv::Size& fullImageSize, float aspectRatio, int padding ) { std::vector adjustedDetections; try { // Basic input validation if (detectionsInROI.empty()) { return adjustedDetections; } if (roi.width <= 0 || roi.height <= 0 || fullImageSize.width <= 0 || fullImageSize.height <= 0) { return adjustedDetections; } for (const auto& detInROI : detectionsInROI) { try { if (detInROI.box.width <= 0 || detInROI.box.height <= 0) continue; // Skip invalid box // Convert ROI-relative box to full-image coordinates cv::Rect detInFullImg; try { detInFullImg = detInROI.box + roi.tl(); detInFullImg &= cv::Rect(0, 0, fullImageSize.width, fullImageSize.height); } catch (const std::exception& e) { std::cerr << "[AdjustBBox] Failed to calculate full image box: " << e.what() << std::endl; continue; } // Check if it touches ROI border bool touchesLeft = detInROI.box.x <= 0; bool touchesRight = detInROI.box.x + detInROI.box.width >= roi.width - 1; bool touchesTop = detInROI.box.y <= 0; bool touchesBottom = detInROI.box.y + detInROI.box.height >= roi.height - 1; bool touchesBorder = touchesLeft || touchesRight || touchesTop || touchesBottom; // Compute target width based on aspect ratio int targetWidth = 0, expandWidth = 0; try { targetWidth = std::max(detInFullImg.width, static_cast(detInFullImg.height * aspectRatio)); expandWidth = std::max(0, targetWidth - detInFullImg.width); } catch (const std::exception& e) { std::cerr << "[AdjustBBox] Aspect ratio adjustment failed: " << e.what() << std::endl; continue; } int expandLeft = expandWidth / 2; int expandRight = expandWidth - expandLeft; int padX = touchesBorder ? padding : 0; int padY = touchesBorder ? (padding / 2) : 0; // Apply padded and expanded box int newX = std::max(0, detInFullImg.x - expandLeft - padX); int newY = std::max(0, detInFullImg.y - padY); int newWidth = detInFullImg.width + expandWidth + 2 * padX; int newHeight = detInFullImg.height + 2 * padY; // Clamp to image boundaries if (newX + newWidth > fullImageSize.width) { newWidth = fullImageSize.width - newX; } if (newY + newHeight > fullImageSize.height) { newHeight = fullImageSize.height - newY; } if (newWidth <= 0 || newHeight <= 0) continue; // Construct adjusted object Object adjustedDet = detInROI; adjustedDet.box = cv::Rect(newX, newY, newWidth, newHeight); adjustedDetections.push_back(adjustedDet); } catch (const std::exception& e) { std::cerr << "[AdjustBBox] Exception per detection: " << e.what() << std::endl; continue; } catch (...) { std::cerr << "[AdjustBBox] Unknown exception per detection." << std::endl; continue; } } } catch (const std::exception& e) { std::cerr << "[AdjustBBox] Fatal error: " << e.what() << std::endl; } catch (...) { std::cerr << "[AdjustBBox] Unknown fatal error occurred." << std::endl; } return adjustedDetections; } }