2026-03-28 16:54:11 +11:00
|
|
|
|
#include "ANSLPR.h"
|
|
|
|
|
|
#include <json.hpp>
|
|
|
|
|
|
#include "ANSLibsLoader.h"
|
|
|
|
|
|
|
|
|
|
|
|
static bool ansalprLicenceValid = false;
|
|
|
|
|
|
// Global once_flag to protect license checking
|
|
|
|
|
|
static std::once_flag ansalprLicenseOnceFlag;
|
|
|
|
|
|
template <typename T>
|
|
|
|
|
|
T GetData(const boost::property_tree::ptree& pt, const std::string& key)
|
|
|
|
|
|
{
|
|
|
|
|
|
T ret;
|
|
|
|
|
|
if (boost::optional<T> data = pt.get_optional<T>(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<int>(s1.size());
|
|
|
|
|
|
int len2 = static_cast<int>(s2.size());
|
|
|
|
|
|
std::vector<std::vector<int>> dp(len1 + 1, std::vector<int>(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<float>(intersection) / unionArea : 0.f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string ALPRChecker::majorityVote(const std::deque<std::string>& history) {
|
|
|
|
|
|
if (history.empty()) return "";
|
|
|
|
|
|
|
|
|
|
|
|
// Count frequency of each plate text, weighted by recency
|
|
|
|
|
|
// (more recent entries get higher weight)
|
|
|
|
|
|
std::map<std::string, float> weightedFreq;
|
|
|
|
|
|
int n = static_cast<int>(history.size());
|
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
|
|
// Recency weight: older=1.0, newest=2.0
|
|
|
|
|
|
float weight = 1.0f + static_cast<float>(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<int>(kv.first.size()) - static_cast<int>(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<int>(s1.size()), len2 = static_cast<int>(s2.size());
|
|
|
|
|
|
if (len1 <= 15 && len2 <= 15) {
|
|
|
|
|
|
std::vector<int> 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<std::recursive_mutex> 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<int>(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<int>(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<int>(plates[i].textHistory.size()), 3);
|
|
|
|
|
|
for (int j = static_cast<int>(plates[i].textHistory.size()) - 1;
|
|
|
|
|
|
j >= static_cast<int>(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<int>(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<float>(plateBox.width), static_cast<float>(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<int>(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<int>(tp.textHistory.size()), maxFrames);
|
|
|
|
|
|
for (int i = static_cast<int>(tp.textHistory.size()) - 1;
|
|
|
|
|
|
i >= static_cast<int>(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<int>(tp.textHistory.size()), minVotesToStabilize * 2);
|
|
|
|
|
|
for (int i = static_cast<int>(tp.textHistory.size()) - 1;
|
|
|
|
|
|
i >= static_cast<int>(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<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);
|
|
|
|
|
|
//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<cv::Rect> ANSCENTER::ANSALPR::GetBoundingBoxes(const std::string& strBBoxes) {
|
|
|
|
|
|
std::vector<cv::Rect> 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<float>(result, "x");
|
|
|
|
|
|
const auto y = GetData<float>(result, "y");
|
|
|
|
|
|
const auto width = GetData<float>(result, "width");
|
|
|
|
|
|
const auto height = GetData<float>(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<cv::Point2f>& 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 14:10:21 +11:00
|
|
|
|
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<unsigned char>(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<unsigned char>(utf8Str[i + 1]) & 0x3F); i += 2;
|
|
|
|
|
|
} else if ((c & 0xF0) == 0xE0 && i + 2 < utf8Str.size()) {
|
|
|
|
|
|
cp = ((c & 0x0F) << 12) | ((static_cast<unsigned char>(utf8Str[i + 1]) & 0x3F) << 6) | (static_cast<unsigned char>(utf8Str[i + 2]) & 0x3F); i += 3;
|
|
|
|
|
|
} else if ((c & 0xF8) == 0xF0 && i + 3 < utf8Str.size()) {
|
|
|
|
|
|
cp = ((c & 0x07) << 18) | ((static_cast<unsigned char>(utf8Str[i + 1]) & 0x3F) << 12) | ((static_cast<unsigned char>(utf8Str[i + 2]) & 0x3F) << 6) | (static_cast<unsigned char>(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 16:54:11 +11:00
|
|
|
|
std::string ANSALPR::VectorDetectionToJsonString(const std::vector<Object>& 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)},
|
2026-03-30 20:14:47 +11:00
|
|
|
|
//{"track_id", std::to_string(det.trackId)},
|
|
|
|
|
|
{"track_id", std::to_string(0)},
|
2026-03-31 14:10:21 +11:00
|
|
|
|
{"class_name", DoubleEscapeUnicode(det.className)},
|
2026-03-28 16:54:11 +11:00
|
|
|
|
{"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)},
|
2026-03-31 14:10:21 +11:00
|
|
|
|
{"mask", ""},
|
2026-03-28 16:54:11 +11:00
|
|
|
|
{"extra_info", det.extraInfo},
|
|
|
|
|
|
{"camera_id", det.cameraId},
|
|
|
|
|
|
{"polygon", PolygonToString(det.polygon)},
|
|
|
|
|
|
{"kps", KeypointsToString(det.kps)}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-31 21:52:47 +11:00
|
|
|
|
return root.dump();
|
2026-03-28 16:54:11 +11:00
|
|
|
|
}
|
|
|
|
|
|
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<std::string>& formats) {
|
|
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
|
|
|
|
_plateFormats.clear();
|
|
|
|
|
|
_plateFormats = formats;
|
|
|
|
|
|
}
|
|
|
|
|
|
void ANSALPR::SetPlateFormat(const std::string& format) {
|
|
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
|
|
|
|
_plateFormats.clear();
|
|
|
|
|
|
_plateFormats.push_back(format);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> ANSALPR::GetPlateFormats() const {
|
|
|
|
|
|
return _plateFormats;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string ANSCENTER::ANSALPR::KeypointsToString(const std::vector<float>& 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<cv::Point2f> 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<cv::Point2f> emptyPolygon;
|
|
|
|
|
|
return emptyPolygon;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate normalized points for each corner of the rectangle
|
|
|
|
|
|
std::vector<cv::Point2f> 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::ImageSection> 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<std::pair<double, ImageSection>> 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<double, ImageSection>& a, const std::pair<double, ImageSection>& 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::ImageSection> 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<int>::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<float>(tileWidth) / static_cast<float>(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<Object> ANSALPR::AdjustLicensePlateBoundingBoxes(
|
|
|
|
|
|
const std::vector<Object>& detectionsInROI,
|
|
|
|
|
|
const cv::Rect& roi,
|
|
|
|
|
|
const cv::Size& fullImageSize,
|
|
|
|
|
|
float aspectRatio,
|
|
|
|
|
|
int padding
|
|
|
|
|
|
) {
|
|
|
|
|
|
std::vector<Object> 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<int>(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|