Files
ANSCORE/ANSODEngine/ANSMotionDetector.cpp

2056 lines
62 KiB
C++

#include "ANSMotionDetector.h"
namespace ANSCENTER {
ANSMOTIONDETECTOR::~ANSMOTIONDETECTOR()
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
for (auto& [key, cameraData] : cameras)
{
cameraData.clear();
}
cameras.clear();
}
bool ANSMOTIONDETECTOR::hasCameraData(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
return cameras.find(camera_id) != cameras.end();
}
void ANSMOTIONDETECTOR::removeCamera(const std::string& camera_id)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.clear();
cameras.erase(it);
}
}
std::vector<std::string> ANSMOTIONDETECTOR::getCameraIds() const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
std::vector<std::string> ids;
ids.reserve(cameras.size());
for (const auto& [id, _] : cameras)
{
ids.push_back(id);
}
return ids;
}
bool ANSMOTIONDETECTOR::empty(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
return it == cameras.end() || it->second.control_frames.empty();
}
void ANSMOTIONDETECTOR::clear(const std::string& camera_id)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.clear();
}
}
void ANSMOTIONDETECTOR::clearAll()
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
for (auto& [key, cameraData] : cameras)
{
cameraData.clear();
}
cameras.clear();
}
void ANSMOTIONDETECTOR::setThreshold(const std::string& camera_id, double threshold)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.psnr_threshold = std::clamp(threshold, 0.0, 100.0);
}
}
void ANSMOTIONDETECTOR::setKeyFrameFrequency(const std::string& camera_id, size_t frequency)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.key_frame_frequency = std::max(size_t(1), frequency);
}
}
void ANSMOTIONDETECTOR::setNumberOfControlFrames(const std::string& camera_id, size_t count)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.number_of_control_frames = std::clamp(count, size_t(1), size_t(100));
}
}
void ANSMOTIONDETECTOR::setThumbnailRatio(const std::string& camera_id, double ratio)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.thumbnail_ratio = std::clamp(ratio, 0.01, 1.0);
it->second.thumbnail_size = cv::Size(0, 0); // Reset to recalculate
}
}
void ANSMOTIONDETECTOR::setMinObjectArea(const std::string& camera_id, double area)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.min_object_area = std::max(0.0, area);
}
}
void ANSMOTIONDETECTOR::setMinObjectSize(const std::string& camera_id, int size)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.min_object_dimension = std::max(0, size);
}
}
void ANSMOTIONDETECTOR::setMorphologyIterations(const std::string& camera_id, int iterations)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.morphology_iterations = std::clamp(iterations, 1, 50);
}
}
double ANSMOTIONDETECTOR::getThreshold(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.psnr_threshold;
}
return 0.0;
}
size_t ANSMOTIONDETECTOR::getKeyFrameFrequency(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.key_frame_frequency;
}
return 0;
}
size_t ANSMOTIONDETECTOR::getNumberOfControlFrames(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.number_of_control_frames;
}
return 0;
}
double ANSMOTIONDETECTOR::getThumbnailRatio(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.thumbnail_ratio;
}
return 0.0;
}
bool ANSMOTIONDETECTOR::isMovementDetected(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
return it != cameras.end() && it->second.movement_detected;
}
bool ANSMOTIONDETECTOR::wasTransitionDetected(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
return it != cameras.end() && it->second.transition_detected;
}
double ANSMOTIONDETECTOR::getPSNRScore(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.most_recent_psnr_score;
}
return 0.0;
}
size_t ANSMOTIONDETECTOR::getFrameIndexWithMovement(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.frame_index_with_movement;
}
return 0;
}
std::chrono::milliseconds ANSMOTIONDETECTOR::getTimeSinceLastMovement(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it == cameras.end() || it->second.frame_index_with_movement == 0)
{
return std::chrono::milliseconds::max();
}
auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(
now - it->second.movement_last_detected
);
}
size_t ANSMOTIONDETECTOR::getControlFrameCount(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.control_frames.size();
}
return 0;
}
size_t ANSMOTIONDETECTOR::getNextFrameIndex(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.next_frame_index;
}
return 0;
}
//cv::Mat ANSMOTIONDETECTOR::getOutput(const std::string& camera_id) const
//{
// std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
// auto it = cameras.find(camera_id);
// if (it != cameras.end())
// {
// return it->second.output.clone();
// }
// return cv::Mat();
//}
const cv::Mat& ANSMOTIONDETECTOR::getOutput(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.output; // Return reference - zero copy!
}
static const cv::Mat empty_mat; // Static empty Mat to return reference to
return empty_mat;
}
const cv::Mat ANSMOTIONDETECTOR::getMask(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.mask;
}
return cv::Mat();
}
std::vector<std::vector<cv::Point>> ANSMOTIONDETECTOR::getContours(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.contours;
}
return std::vector<std::vector<cv::Point>>();
}
ANSMOTIONDETECTOR::CameraStats ANSMOTIONDETECTOR::getStats(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.stats;
}
return CameraStats();
}
void ANSMOTIONDETECTOR::resetStats(const std::string& camera_id)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.stats.reset();
}
}
bool ANSMOTIONDETECTOR::cameraExists(const std::string& camera_id) const
{
// Note: cameras_mutex should already be locked by caller
return cameras.find(camera_id) != cameras.end();
}
ANSMOTIONDETECTOR::CameraData& ANSMOTIONDETECTOR::getCameraData(const std::string& camera_id)
{
// Note: cameras_mutex should already be locked by caller
return cameras.try_emplace(camera_id, CameraData{}).first->second;
}
const ANSMOTIONDETECTOR::CameraData* ANSMOTIONDETECTOR::getCameraDataConst(const std::string& camera_id) const
{
auto it = cameras.find(camera_id);
if (it == cameras.end())
{
return nullptr;
}
return &it->second;
}
double ANSMOTIONDETECTOR::psnr(const cv::Mat& src, const cv::Mat& dst)
{
if (src.empty() || dst.empty())
{
std::cerr << "PSNR: Empty image(s)" << std::endl;
return 0.0;
}
if (src.type() != dst.type())
{
std::cerr << "PSNR: Type mismatch - src: " << src.type()
<< " dst: " << dst.type() << std::endl;
return 0.0;
}
if (src.size() != dst.size())
{
std::cerr << "PSNR: Size mismatch - src: " << src.size()
<< " dst: " << dst.size() << std::endl;
return 0.0;
}
cv::Mat s1;
cv::absdiff(src, dst, s1);
s1.convertTo(s1, CV_32F);
s1 = s1.mul(s1);
cv::Scalar s = cv::sum(s1);
double sse = s.val[0] + s.val[1] + s.val[2];
if (sse <= 1e-10)
{
// Identical images - return very high PSNR
return 100.0; // Use a high but finite value instead of infinity
}
const double mse = sse / static_cast<double>(src.channels() * src.total());
const double psnr_value = 10.0 * std::log10((255.0 * 255.0) / mse);
// Sanity check
if (std::isnan(psnr_value) || std::isinf(psnr_value))
{
std::cerr << "PSNR: Invalid result (NaN or Inf), MSE=" << mse << std::endl;
return 0.0;
}
return psnr_value;
}
/*cv::Mat ANSMOTIONDETECTOR::simple_colour_balance(const cv::Mat& src)
{
if (src.empty() || src.channels() != 3)
{
return src.clone();
}
const float full_percent = 1.0f;
const float half_percent = full_percent / 200.0f;
std::vector<cv::Mat> tmpsplit;
cv::split(src, tmpsplit);
for (size_t idx = 0; idx < 3; idx++)
{
cv::Mat flat;
tmpsplit[idx].reshape(1, 1).copyTo(flat);
cv::sort(flat, flat, cv::SORT_EVERY_ROW + cv::SORT_ASCENDING);
const int lo = flat.at<uchar>(cvFloor(static_cast<float>(flat.cols) * half_percent));
const int hi = flat.at<uchar>(cvCeil(static_cast<float>(flat.cols) * (1.0f - half_percent)));
tmpsplit[idx].setTo(lo, tmpsplit[idx] < lo);
tmpsplit[idx].setTo(hi, tmpsplit[idx] > hi);
cv::normalize(tmpsplit[idx], tmpsplit[idx], 0, 255, cv::NORM_MINMAX);
}
cv::Mat output;
cv::merge(tmpsplit, output);
return output;
}*/
cv::Mat ANSMOTIONDETECTOR::simple_colour_balance(const cv::Mat& src)
{
if (src.empty() || src.channels() != 3)
{
return cv::Mat(); // Return empty instead of cloning invalid input
}
const float full_percent = 1.0f;
const float half_percent = full_percent / 200.0f;
std::vector<cv::Mat> channels;
cv::split(src, channels);
cv::Mat output;
output.create(src.size(), src.type());
for (int idx = 0; idx < 3; idx++)
{
// Use histogram instead of sorting - much faster!
int histSize = 256;
float range[] = { 0, 256 };
const float* histRange = { range };
cv::Mat hist;
cv::calcHist(&channels[idx], 1, 0, cv::Mat(), hist, 1, &histSize, &histRange);
// Calculate cumulative histogram
for (int i = 1; i < 256; i++)
{
hist.at<float>(i) += hist.at<float>(i - 1);
}
// Find low and high percentile values
const float total_pixels = hist.at<float>(255);
const float low_threshold = total_pixels * half_percent;
const float high_threshold = total_pixels * (1.0f - half_percent);
int lo = 0, hi = 255;
for (int i = 0; i < 256; i++)
{
if (hist.at<float>(i) >= low_threshold)
{
lo = i;
break;
}
}
for (int i = 255; i >= 0; i--)
{
if (hist.at<float>(i) <= high_threshold)
{
hi = i;
break;
}
}
// Apply stretch without creating temp Mats
if (hi > lo)
{
const float scale = 255.0f / (hi - lo);
channels[idx].convertTo(channels[idx], CV_32F);
channels[idx] = (channels[idx] - lo) * scale;
channels[idx].setTo(0, channels[idx] < 0);
channels[idx].setTo(255, channels[idx] > 255);
channels[idx].convertTo(channels[idx], CV_8U);
}
}
cv::merge(channels, output);
return output;
}
cv::Rect ANSMOTIONDETECTOR::BoundingBoxFromContour(const std::vector<cv::Point>& contour)
{
if (contour.empty())
{
return cv::Rect();
}
if (cv::contourArea(contour) < 1000)
{
return cv::Rect();
}
return cv::boundingRect(contour);
}
void ANSMOTIONDETECTOR::setDynamicPSNRThreshold(CameraData& camera, const cv::Mat& working_image)
{
int min_dimension = std::min(working_image.cols, working_image.rows);
double base_threshold;
if (min_dimension >= 1080)
{
base_threshold = 45.0;
camera.max_control_frame_age = 30;
camera.thumbnail_ratio = 0.1;
}
else if (min_dimension >= 720)
{
base_threshold = 40.0;
camera.max_control_frame_age = 25;
camera.thumbnail_ratio = 0.1;
}
else if (min_dimension >= 480)
{
base_threshold = 35.0;
camera.max_control_frame_age = 20;
camera.thumbnail_ratio = 0.15;
}
else
{
base_threshold = 30.0;
camera.max_control_frame_age = 15;
camera.thumbnail_ratio = 0.2;
}
// Apply sensitivity multiplier
if (_modelConfig.detectionScoreThreshold == 0.0) {
camera.psnr_threshold = base_threshold * camera.sensitivity_multiplier;
}
else {
camera.sensitivity_multiplier = (1.5 - _modelConfig.detectionScoreThreshold);
if (camera.sensitivity_multiplier < 0)camera.sensitivity_multiplier = 0.0;
else if (camera.sensitivity_multiplier > 2.0)camera.sensitivity_multiplier = 2.0;
camera.psnr_threshold = base_threshold * camera.sensitivity_multiplier;
}
std::cout << "Base threshold: " << base_threshold
<< ", Sensitivity: " << camera.sensitivity_multiplier
<< ", Final threshold: " << camera.psnr_threshold << std::endl;
}
cv::Mat ANSMOTIONDETECTOR::computeMovementMask(const cv::Mat& control_frame,
const cv::Mat& current_frame,
const cv::Size& output_size,
int morphology_iterations,
const CameraData& camera)
{
// Convert to grayscale
cv::Mat control_gray, current_gray;
cv::cvtColor(control_frame, control_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(current_frame, current_gray, cv::COLOR_BGR2GRAY);
// Apply Gaussian blur to reduce noise sensitivity
cv::GaussianBlur(control_gray, control_gray, cv::Size(5, 5), 0);
cv::GaussianBlur(current_gray, current_gray, cv::Size(5, 5), 0);
// Compute absolute difference
cv::Mat diff;
cv::absdiff(control_gray, current_gray, diff);
cv::Mat mask;
cv::threshold(diff, mask, camera.mask_diff_threshold, 255, cv::THRESH_BINARY);
double frame_percentage = (cv::countNonZero(mask) * 100.0) / mask.total();
std::cout << "Fixed threshold " << camera.mask_diff_threshold
<< ": " << std::fixed << std::setprecision(2)
<< frame_percentage << "% of frame" << std::endl;
// Reject if too much of frame is flagged
if (frame_percentage > camera.mask_max_percentage)
{
std::cout << "Mask too large (" << frame_percentage
<< "% > " << camera.mask_max_percentage << "%), rejecting" << std::endl;
mask = cv::Mat::zeros(mask.size(), CV_8UC1);
std::cout << "Final mask: 0.00% of frame" << std::endl;
return mask;
}
// CHANGED: Don't reject small masks here - let morphology clean them up first
// Small movements on thumbnails will appear tiny
// ADAPTIVE MORPHOLOGY: Only apply if mask is large enough to survive it
if (morphology_iterations > 0 && frame_percentage >= 0.5)
{
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));
// MORPH_OPEN: Remove small noise
cv::morphologyEx(mask, mask, cv::MORPH_OPEN, kernel,
cv::Point(-1, -1), morphology_iterations);
// MORPH_CLOSE: Fill small holes
cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel,
cv::Point(-1, -1), morphology_iterations / 2);
std::cout << "Applied morphology (iterations: " << morphology_iterations << ")" << std::endl;
}
else if (frame_percentage < 0.5)
{
std::cout << "Skipping morphology (mask too small: " << frame_percentage << "%)" << std::endl;
}
// Check final mask percentage AFTER morphology
double final_percentage = (cv::countNonZero(mask) * 100.0) / mask.total();
std::cout << "Final mask: " << std::fixed << std::setprecision(2)
<< final_percentage << "% of frame" << std::endl;
// Reject if final mask is too small (but use lower threshold)
if (final_percentage < camera.mask_min_percentage)
{
std::cout << "Final mask too small (" << final_percentage
<< "% < " << camera.mask_min_percentage << "%), rejecting" << std::endl;
mask = cv::Mat::zeros(mask.size(), CV_8UC1);
std::cout << "Final mask: 0.00% of frame" << std::endl;
return mask;
}
// Resize to output size
cv::resize(mask, mask, output_size, 0, 0, cv::INTER_NEAREST);
return mask;
}
//std::vector<Object> ANSMOTIONDETECTOR::extractObjectsFromMask(const cv::Mat& mask,
// const cv::Mat& image,
// CameraData& camera,
// const std::string& camera_id)
//{
// std::vector<Object> outputObjects;
// if (mask.empty())
// {
// return outputObjects;
// }
// std::vector<cv::Vec4i> hierarchy;
// cv::findContours(mask, camera.contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// for (const auto& contour : camera.contours)
// {
// double area = cv::contourArea(contour);
// if (area < camera.min_object_area)
// {
// continue;
// }
// cv::Rect boundingRect = cv::boundingRect(contour);
// // Clamp to image bounds
// boundingRect &= cv::Rect(0, 0, image.cols, image.rows);
// if (boundingRect.width < camera.min_object_dimension ||
// boundingRect.height < camera.min_object_dimension ||
// boundingRect.area() < camera.min_object_total_size)
// {
// continue;
// }
// // Draw visualizations
// //if (camera.contours_enabled)
// //{
// // cv::polylines(camera.output, contour, true, cv::Scalar(0, 0, 255),
// // camera.contours_size, camera.line_type);
// //}
// //if (camera.bbox_enabled)
// //{
// // cv::rectangle(camera.output, boundingRect, cv::Scalar(0, 255, 0),
// // camera.bbox_size, camera.line_type);
// //}
// Object obj;
// obj.classId = 0;
// obj.trackId = 0;
// obj.className = "movement";
// obj.confidence = 1.0f;
// obj.box = boundingRect;
// obj.mask = image(boundingRect).clone();
// obj.cameraId = camera_id;
// // Create polygon from contour (simplified to bounding box corners)
// obj.polygon = {
// cv::Point2f(static_cast<float>(boundingRect.x), static_cast<float>(boundingRect.y)),
// cv::Point2f(static_cast<float>(boundingRect.x + boundingRect.width), static_cast<float>(boundingRect.y)),
// cv::Point2f(static_cast<float>(boundingRect.x + boundingRect.width), static_cast<float>(boundingRect.y + boundingRect.height)),
// cv::Point2f(static_cast<float>(boundingRect.x), static_cast<float>(boundingRect.y + boundingRect.height))
// };
// if ((boundingRect.width >= 25) &&
// (boundingRect.height >= 25))outputObjects.push_back(obj);
// }
// return outputObjects;
//}
std::vector<Object> ANSMOTIONDETECTOR::extractObjectsFromMask(
const cv::Mat& mask,
const cv::Mat& image,
CameraData& camera,
const std::string& camera_id)
{
std::vector<Object> outputObjects;
if (mask.empty())
{
return outputObjects;
}
// Use local contours to avoid overwriting camera.contours
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(mask, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// Pre-allocate based on expected objects (prevents reallocations)
outputObjects.reserve(contours.size());
// Pre-calculate image bounds rectangle
const cv::Rect imageBounds(0, 0, image.cols, image.rows);
// Consolidated minimum size (use configuration or hardcoded value)
const int min_width = std::max(camera.min_object_dimension, 25);
const int min_height = std::max(camera.min_object_dimension, 25);
const int min_area = camera.min_object_area;
for (const auto& contour : contours)
{
// Early exit: check area first (cheapest check)
const double area = cv::contourArea(contour);
if (area < min_area)
{
continue;
}
// Calculate bounding rect
cv::Rect boundingRect = cv::boundingRect(contour);
// Clamp to image bounds
boundingRect &= imageBounds;
// Consolidated dimension checks
if (boundingRect.width < min_width ||
boundingRect.height < min_height ||
boundingRect.area() < camera.min_object_total_size)
{
continue;
}
// Construct object in-place using emplace_back
outputObjects.emplace_back();
Object& obj = outputObjects.back();
obj.classId = 0;
obj.trackId = 0;
obj.className = "movement";
obj.confidence = 1.0f;
obj.box = boundingRect;
obj.cameraId = camera_id;
// CRITICAL FIX: Use ROI reference instead of clone
// Only clone if caller will modify the mask
obj.mask = image(boundingRect); // Shallow copy - shares data!
// Optional: Create polygon only if needed
// If you need actual contour, use: obj.polygon = contour;
// Otherwise, skip this allocation entirely
obj.polygon.reserve(4);
obj.polygon = {
cv::Point2f(static_cast<float>(boundingRect.x),
static_cast<float>(boundingRect.y)),
cv::Point2f(static_cast<float>(boundingRect.x + boundingRect.width),
static_cast<float>(boundingRect.y)),
cv::Point2f(static_cast<float>(boundingRect.x + boundingRect.width),
static_cast<float>(boundingRect.y + boundingRect.height)),
cv::Point2f(static_cast<float>(boundingRect.x),
static_cast<float>(boundingRect.y + boundingRect.height))
};
}
return outputObjects;
}
//void ANSMOTIONDETECTOR::updateControlFrames(CameraData& camera, size_t frame_index,
// const cv::Mat& thumbnail)
//{
// if (thumbnail.empty())
// {
// std::cerr << "updateControlFrames: Empty thumbnail!" << std::endl;
// return;
// }
// if (frame_index >= camera.next_key_frame ||
// camera.control_frames.size() < camera.number_of_control_frames)
// {
// // Store a deep copy
// cv::Mat thumbnail_copy = thumbnail.clone();
// // Ensure continuous memory
// if (!thumbnail_copy.isContinuous())
// {
// thumbnail_copy = thumbnail_copy.clone();
// }
// camera.control_frames[frame_index] = thumbnail_copy;
// // Remove oldest frames if exceeding limit
// while (camera.control_frames.size() > camera.number_of_control_frames)
// {
// auto oldest = camera.control_frames.begin();
// oldest->second.release(); // Explicitly release memory
// camera.control_frames.erase(oldest);
// }
// camera.next_key_frame = frame_index + camera.key_frame_frequency;
// }
//}
void ANSMOTIONDETECTOR::updateControlFrames(CameraData& camera,
size_t frame_index,
const cv::Mat& thumbnail)
{
if (thumbnail.empty())
{
// Use logger instead of cerr for consistency
// std::cerr << "updateControlFrames: Empty thumbnail!" << std::endl;
return;
}
// Only update at key frame intervals or when buffer not full
if (frame_index < camera.next_key_frame &&
camera.control_frames.size() >= camera.number_of_control_frames)
{
return; // Early exit - nothing to do
}
// Remove oldest frames BEFORE insertion to maintain exact size limit
while (camera.control_frames.size() >= camera.number_of_control_frames)
{
auto oldest = camera.control_frames.begin();
camera.control_frames.erase(oldest); // .release() unnecessary
}
// Clone once, ensuring continuous memory in single operation
cv::Mat thumbnail_copy;
if (thumbnail.isContinuous())
{
thumbnail_copy = thumbnail.clone();
}
else
{
// Force continuous memory during clone
thumbnail_copy = thumbnail.clone();
if (!thumbnail_copy.isContinuous())
{
thumbnail_copy = thumbnail_copy.clone(); // Rare case
}
}
// Use emplace for efficient insertion
camera.control_frames.emplace(frame_index, std::move(thumbnail_copy));
// Update next key frame
camera.next_key_frame = frame_index + camera.key_frame_frequency;
}
void ANSMOTIONDETECTOR::updateStatistics(CameraData& camera, double psnr_value,
bool movement_detected,
std::chrono::milliseconds processing_time)
{
camera.stats.total_frames_processed++;
if (movement_detected)
{
camera.stats.frames_with_movement++;
camera.stats.last_movement_time = std::chrono::high_resolution_clock::now();
}
camera.stats.control_frames_count = camera.control_frames.size();
// Update PSNR statistics (skip infinity values from identical frames)
if (!std::isinf(psnr_value))
{
camera.stats.min_psnr = std::min(camera.stats.min_psnr, psnr_value);
camera.stats.max_psnr = std::max(camera.stats.max_psnr, psnr_value);
// Running average
double n = static_cast<double>(camera.stats.total_frames_processed);
camera.stats.average_psnr =
(camera.stats.average_psnr * (n - 1.0) + psnr_value) / n;
}
camera.stats.total_processing_time += processing_time;
}
// ============================================================================
// Main Detection Methods
// ============================================================================
std::vector<Object> ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id, const cv::Mat& next_image)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
CameraData& camera = getCameraData(camera_id);
return MovementDetect(camera_id, camera.next_frame_index, next_image);
}
//std::vector<Object> ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id, const size_t frame_index, const cv::Mat& image)
//{
// std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
// if (image.empty())
// {
// return std::vector<Object>();
// }
// // CRITICAL: Ensure consistent format
// cv::Mat processed_image;
// // Convert to BGR if needed
// if (image.channels() == 1)
// {
// cv::cvtColor(image, processed_image, cv::COLOR_GRAY2BGR);
// }
// else if (image.channels() == 4)
// {
// cv::cvtColor(image, processed_image, cv::COLOR_BGRA2BGR);
// }
// else if (image.channels() == 3)
// {
// // Already BGR, but ensure it's a continuous clone
// processed_image = image.clone();
// }
// else
// {
// // Unsupported format
// return std::vector<Object>();
// }
// // Ensure continuous memory layout
// if (!processed_image.isContinuous())
// {
// processed_image = processed_image.clone();
// }
// CameraData& camera = getCameraData(camera_id);
// return MovementDetectInternal(camera_id, frame_index, processed_image, camera);
//}
std::vector<Object> ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id,
const size_t frame_index,
const cv::Mat& image)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
if (image.empty())
{
return {};
}
cv::Mat processed_image;
// Convert to BGR if needed
if (image.channels() == 1)
{
cv::cvtColor(image, processed_image, cv::COLOR_GRAY2BGR);
}
else if (image.channels() == 4)
{
cv::cvtColor(image, processed_image, cv::COLOR_BGRA2BGR);
}
else if (image.channels() == 3)
{
// Use shallow copy for continuous BGR, clone only if needed
processed_image = image.isContinuous() ? image : image.clone();
}
else
{
return {};
}
CameraData& camera = getCameraData(camera_id);
return MovementDetectInternal(camera_id, frame_index, processed_image, camera);
}
void ANSMOTIONDETECTOR::setTemporalConsistency(const std::string& camera_id, bool enabled)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.temporal_consistency_enabled = enabled;
}
}
void ANSMOTIONDETECTOR::setMaskOverlapThreshold(const std::string& camera_id, double threshold)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.mask_overlap_threshold = std::clamp(threshold, 0.0, 1.0);
}
}
void ANSMOTIONDETECTOR::setTemporalHistorySize(const std::string& camera_id, size_t size)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.temporal_history_size = std::clamp(size, size_t(1), size_t(20));
}
}
void ANSMOTIONDETECTOR::setMinConsistentFrames(const std::string& camera_id, size_t frames)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.min_consistent_frames = std::max(size_t(1), frames);
}
}
void ANSMOTIONDETECTOR::setLocationStabilityEnabled(const std::string& camera_id, bool enabled)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.location_stability_enabled = enabled;
}
}
void ANSMOTIONDETECTOR::setMaxLocationJitter(const std::string& camera_id, double pixels)
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
it->second.max_location_jitter = std::max(0.0, pixels);
}
}
bool ANSMOTIONDETECTOR::isTemporalConsistencyEnabled(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.temporal_consistency_enabled;
}
return false;
}
double ANSMOTIONDETECTOR::getTemporalConsistencyScore(const std::string& camera_id) const
{
std::lock_guard<std::recursive_mutex> lock(cameras_mutex);
auto it = cameras.find(camera_id);
if (it != cameras.end())
{
return it->second.last_temporal_consistency_score;
}
return 0.0;
}
// ============================================================================
// Temporal Consistency Helper Functions
// ============================================================================
double ANSMOTIONDETECTOR::calculateMaskOverlap(const cv::Mat& mask1, const cv::Mat& mask2)
{
if (mask1.empty() || mask2.empty())
{
return 0.0;
}
if (mask1.size() != mask2.size())
{
return 0.0;
}
// Calculate intersection
cv::Mat intersection;
cv::bitwise_and(mask1, mask2, intersection);
int intersection_pixels = cv::countNonZero(intersection);
// Calculate union
cv::Mat union_mask;
cv::bitwise_or(mask1, mask2, union_mask);
int union_pixels = cv::countNonZero(union_mask);
if (union_pixels == 0)
{
return 0.0;
}
// IoU (Intersection over Union)
double overlap = static_cast<double>(intersection_pixels) / static_cast<double>(union_pixels);
return overlap;
}
cv::Point ANSMOTIONDETECTOR::calculateMaskCentroid(const cv::Mat& mask)
{
if (mask.empty())
{
return cv::Point(-1, -1);
}
cv::Moments m = cv::moments(mask, true);
if (m.m00 == 0)
{
return cv::Point(-1, -1);
}
cv::Point centroid;
centroid.x = static_cast<int>(m.m10 / m.m00);
centroid.y = static_cast<int>(m.m01 / m.m00);
return centroid;
}
double ANSMOTIONDETECTOR::calculateLocationStability(const std::deque<cv::Point>& centroids)
{
if (centroids.size() < 2)
{
return 0.0;
}
// Calculate total movement distance
double total_distance = 0.0;
for (size_t i = 1; i < centroids.size(); i++)
{
if (centroids[i].x < 0 || centroids[i - 1].x < 0)
{
continue; // Skip invalid centroids
}
double dx = centroids[i].x - centroids[i - 1].x;
double dy = centroids[i].y - centroids[i - 1].y;
double distance = std::sqrt(dx * dx + dy * dy);
total_distance += distance;
}
// Average movement per frame
double avg_movement = total_distance / (centroids.size() - 1);
return avg_movement;
}
//void ANSMOTIONDETECTOR::updateTemporalHistory(CameraData& camera, const cv::Mat& mask)
//{
// if (mask.empty())
// {
// return;
// }
// // Add mask to history
// camera.mask_history.push_back(mask.clone());
// // Maintain history size limit
// while (camera.mask_history.size() > camera.temporal_history_size)
// {
// camera.mask_history.front().release();
// camera.mask_history.pop_front();
// }
// // Calculate and store centroid
// cv::Point centroid = calculateMaskCentroid(mask);
// camera.centroid_history.push_back(centroid);
// // Maintain centroid history size
// while (camera.centroid_history.size() > camera.temporal_history_size)
// {
// camera.centroid_history.pop_front();
// }
//}
void ANSMOTIONDETECTOR::updateTemporalHistory(CameraData& camera, const cv::Mat& mask)
{
if (mask.empty())
{
return;
}
// Enforce size limit BEFORE insertion
if (camera.mask_history.size() >= camera.temporal_history_size)
{
camera.mask_history.pop_front(); // Remove oldest
camera.centroid_history.pop_front();
}
// Add new mask (single clone)
camera.mask_history.push_back(mask.clone());
// Calculate and add centroid
camera.centroid_history.push_back(calculateMaskCentroid(mask));
}
bool ANSMOTIONDETECTOR::checkTemporalConsistency(CameraData& camera, const cv::Mat& current_mask)
{
if (!camera.temporal_consistency_enabled)
{
camera.last_temporal_consistency_score = 1.0;
return true; // Always pass if disabled
}
if (current_mask.empty())
{
camera.last_temporal_consistency_score = 0.0;
return false;
}
// If no history yet, accept the first detection
if (camera.mask_history.empty())
{
camera.last_temporal_consistency_score = 1.0;
return true;
}
// Calculate overlap with recent masks
double total_overlap = 0.0;
int valid_comparisons = 0;
for (const auto& historical_mask : camera.mask_history)
{
if (!historical_mask.empty())
{
double overlap = calculateMaskOverlap(current_mask, historical_mask);
total_overlap += overlap;
valid_comparisons++;
}
}
double avg_overlap = (valid_comparisons > 0) ? (total_overlap / valid_comparisons) : 0.0;
camera.last_temporal_consistency_score = avg_overlap;
std::cout << "Temporal consistency check:" << std::endl;
std::cout << " Average overlap with history: " << (avg_overlap * 100) << "%" << std::endl;
std::cout << " Required threshold: " << (camera.mask_overlap_threshold * 100) << "%" << std::endl;
// Check mask overlap criterion
bool overlap_passed = avg_overlap >= camera.mask_overlap_threshold;
// Check location stability if enabled
bool location_stable = true;
if (camera.location_stability_enabled && camera.centroid_history.size() >= 2)
{
cv::Point current_centroid = calculateMaskCentroid(current_mask);
if (current_centroid.x >= 0)
{
// Check jitter with most recent centroid
const cv::Point& last_centroid = camera.centroid_history.back();
if (last_centroid.x >= 0)
{
double dx = current_centroid.x - last_centroid.x;
double dy = current_centroid.y - last_centroid.y;
double distance = std::sqrt(dx * dx + dy * dy);
location_stable = distance <= camera.max_location_jitter;
std::cout << " Location jitter: " << distance << " pixels (max: "
<< camera.max_location_jitter << ")" << std::endl;
std::cout << " Location stable: " << (location_stable ? "YES" : "NO") << std::endl;
}
}
}
// Both criteria must pass
bool passed = overlap_passed && location_stable;
// Update consistent frame counter
if (passed)
{
camera.consistent_frame_count++;
std::cout << " Consistent frames: " << camera.consistent_frame_count
<< " / " << camera.min_consistent_frames << std::endl;
}
else
{
camera.consistent_frame_count = 0;
std::cout << " Consistency check FAILED - resetting counter" << std::endl;
}
// Movement is only confirmed after min consecutive consistent frames
bool confirmed = camera.consistent_frame_count >= camera.min_consistent_frames;
std::cout << " Movement confirmed: " << (confirmed ? "YES" : "NO") << std::endl;
return confirmed;
}
//std::vector<Object> ANSMOTIONDETECTOR::MovementDetectInternal(
// const std::string& camera_id,
// const size_t frame_index,
// const cv::Mat& image,
// CameraData& camera)
//{
// auto start_time = std::chrono::high_resolution_clock::now();
// std::vector<Object> outputObjects;
// // DEBUG: Log input
// std::cout << "\n=== MovementDetect Debug ===" << std::endl;
// std::cout << "Camera ID: " << camera_id << std::endl;
// std::cout << "Frame Index: " << frame_index << std::endl;
// std::cout << "Original image size: " << image.cols << "x" << image.rows << std::endl;
// std::cout << "Image type: " << image.type() << " (CV_8UC3=" << CV_8UC3 << ")" << std::endl;
// std::cout << "Image channels: " << image.channels() << std::endl;
// std::cout << "Image empty: " << (image.empty() ? "YES" : "NO") << std::endl;
// if (image.empty())
// {
// std::cout << "ERROR: Empty image!" << std::endl;
// return outputObjects;
// }
// // Store original dimensions for scaling back
// const int original_width = image.cols;
// const int original_height = image.rows;
// const int max_dimension = std::max(original_width, original_height);
// // Maintain aspect ratio while resizing to max dimension of 1920
// const int target_size = 1920;
// double scale_x = 1.0;
// double scale_y = 1.0;
// int working_width = original_width;
// int working_height = original_height;
// cv::Mat working_image;
// if (max_dimension > target_size)
// {
// // Calculate new dimensions maintaining aspect ratio
// double aspect_ratio = static_cast<double>(original_width) / static_cast<double>(original_height);
// if (original_width > original_height)
// {
// working_width = target_size;
// working_height = static_cast<int>(target_size / aspect_ratio);
// }
// else
// {
// working_height = target_size;
// working_width = static_cast<int>(target_size * aspect_ratio);
// }
// // Ensure dimensions are at least 1
// working_width = std::max(1, working_width);
// working_height = std::max(1, working_height);
// // Calculate scale factors AFTER determining final dimensions
// scale_x = static_cast<double>(original_width) / static_cast<double>(working_width);
// scale_y = static_cast<double>(original_height) / static_cast<double>(working_height);
// std::cout << "Working size: " << working_width << "x" << working_height
// << " (maintains " << std::fixed << std::setprecision(2) << aspect_ratio << ":1 aspect ratio)" << std::endl;
// cv::resize(image, working_image, cv::Size(working_width, working_height), 0, 0, cv::INTER_AREA);
// }
// else
// {
// working_image = image.clone();
// std::cout << "Image size within limits, no resize needed" << std::endl;
// }
// std::cout << "Scale factors - X: " << scale_x << ", Y: " << scale_y << std::endl;
// std::cout << "Working image size: " << working_image.cols << "x" << working_image.rows << std::endl;
// // Ensure image is continuous in memory
// if (!working_image.isContinuous())
// {
// std::cout << "WARNING: Working image is not continuous, cloning..." << std::endl;
// working_image = working_image.clone();
// }
// camera.contours.clear();
// if (camera.thumbnail_size.area() == 0)
// {
// setDynamicPSNRThreshold(camera, working_image); // Use working image!
// }
// // Initialize thumbnail size if needed (based on working image dimensions)
// if (camera.thumbnail_size.area() == 0)
// {
// camera.thumbnail_ratio = std::clamp(camera.thumbnail_ratio, 0.01, 1.0);
// camera.thumbnail_size.width = static_cast<int>(working_width * camera.thumbnail_ratio);
// camera.thumbnail_size.height = static_cast<int>(working_height * camera.thumbnail_ratio);
// camera.thumbnail_size.width = std::max(1, camera.thumbnail_size.width);
// camera.thumbnail_size.height = std::max(1, camera.thumbnail_size.height);
// std::cout << "Initialized thumbnail size: " << camera.thumbnail_size.width
// << "x" << camera.thumbnail_size.height << std::endl;
// }
// // Create thumbnail for comparison
// cv::Mat thumbnail;
// cv::resize(working_image, thumbnail, camera.thumbnail_size, 0, 0, cv::INTER_AREA);
// if (thumbnail.empty())
// {
// std::cout << "ERROR: Failed to create thumbnail!" << std::endl;
// return outputObjects;
// }
// std::cout << "Thumbnail created: " << thumbnail.cols << "x" << thumbnail.rows
// << " type: " << thumbnail.type() << std::endl;
// // DEBUG: Control frames info
// std::cout << "Control frames count: " << camera.control_frames.size() << std::endl;
// std::cout << "Control frame indices: ";
// for (const auto& [idx, frame] : camera.control_frames)
// {
// std::cout << idx << " ";
// }
// std::cout << std::endl;
// if (camera.control_frames.empty())
// {
// std::cout << "First frame - initializing control frame" << std::endl;
// camera.control_frames[frame_index] = thumbnail.clone();
// camera.next_key_frame = frame_index + camera.key_frame_frequency;
// camera.next_frame_index = frame_index + 1;
// camera.output = working_image.clone();
// camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
// auto end_time = std::chrono::high_resolution_clock::now();
// auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
// end_time - start_time
// );
// updateStatistics(camera, 0.0, false, processing_time);
// return outputObjects;
// }
// bool previous_movement_detected = camera.movement_detected;
// camera.movement_detected = false;
// double min_psnr = std::numeric_limits<double>::infinity();
// cv::Mat best_control_frame;
// size_t best_control_index = 0;
// std::cout << "Previous movement: " << (previous_movement_detected ? "YES" : "NO") << std::endl;
// std::cout << "PSNR Threshold: " << camera.psnr_threshold << std::endl;
// std::cout << "Max control frame age: " << camera.max_control_frame_age << " frames" << std::endl;
// // Compare with ALL control frames within max age and find the one with minimum PSNR
// int compared_frames_count = 0;
// for (auto iter = camera.control_frames.rbegin(); iter != camera.control_frames.rend(); ++iter)
// {
// const cv::Mat& control_image = iter->second;
// size_t control_frame_index = iter->first;
// // ADDED: Calculate frame age and skip if too old
// size_t frame_age = frame_index - control_frame_index;
// if (frame_age > camera.max_control_frame_age)
// {
// std::cout << "Skipping control frame " << control_frame_index
// << " (age: " << frame_age << " > max: " << camera.max_control_frame_age << ")" << std::endl;
// continue;
// }
// // DEBUG: Verify control frame
// std::cout << "Comparing with control frame " << control_frame_index
// << " (age: " << frame_age << " frames)"
// << " size: " << control_image.cols << "x" << control_image.rows
// << " type: " << control_image.type() << std::endl;
// if (control_image.empty())
// {
// std::cout << "ERROR: Control frame is empty!" << std::endl;
// continue;
// }
// if (control_image.size() != thumbnail.size())
// {
// std::cout << "ERROR: Size mismatch! Control: " << control_image.size()
// << " Thumbnail: " << thumbnail.size() << std::endl;
// continue;
// }
// if (control_image.type() != thumbnail.type())
// {
// std::cout << "ERROR: Type mismatch! Control: " << control_image.type()
// << " Thumbnail: " << thumbnail.type() << std::endl;
// continue;
// }
// double current_psnr = psnr(control_image, thumbnail);
// std::cout << "PSNR Score: " << std::fixed << std::setprecision(2)
// << current_psnr << std::endl;
// if (std::isinf(current_psnr))
// {
// std::cout << "PSNR is infinity (identical frames), skipping..." << std::endl;
// continue;
// }
// // Track the minimum PSNR and corresponding control frame
// if (current_psnr < min_psnr)
// {
// min_psnr = current_psnr;
// best_control_frame = control_image;
// best_control_index = control_frame_index;
// }
// compared_frames_count++;
// }
// std::cout << "Compared " << compared_frames_count << " control frames within age limit" << std::endl;
// // ADDED: If no valid control frames were compared, treat as no movement
// if (compared_frames_count == 0)
// {
// std::cout << "WARNING: No valid control frames within age limit to compare!" << std::endl;
// camera.most_recent_psnr_score = 0.0;
// camera.movement_detected = false;
// camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
// camera.output = working_image.clone();
// auto end_time = std::chrono::high_resolution_clock::now();
// auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
// end_time - start_time
// );
// updateStatistics(camera, 0.0, false, processing_time);
// std::cout << "Processing time: " << processing_time.count() << "ms" << std::endl;
// std::cout << "=== End Debug ===" << std::endl;
// return outputObjects;
// }
// camera.most_recent_psnr_score = min_psnr;
// // Check if minimum PSNR is below threshold
// if (!std::isinf(min_psnr) && min_psnr < camera.psnr_threshold)
// {
// std::cout << "*** POTENTIAL MOVEMENT *** (Min PSNR: " << min_psnr << " < threshold: "
// << camera.psnr_threshold << ")" << std::endl;
// std::cout << "Best matching control frame: " << best_control_index
// << " (age: " << (frame_index - best_control_index) << " frames)" << std::endl;
// // Compute movement mask using the best control frame
// if (!best_control_frame.empty())
// {
// camera.mask = computeMovementMask(best_control_frame, thumbnail, working_image.size(),
// camera.morphology_iterations, camera);
// int non_zero_pixels = cv::countNonZero(camera.mask);
// double mask_percentage = (non_zero_pixels * 100.0) / (camera.mask.cols * camera.mask.rows);
// std::cout << "Mask created: " << camera.mask.cols << "x" << camera.mask.rows
// << " non-zero pixels: " << non_zero_pixels
// << " (" << std::fixed << std::setprecision(2) << mask_percentage << "%)" << std::endl;
// int min_mask_pixels = static_cast<int>(
// working_image.cols * working_image.rows * camera.min_mask_pixels_percentage / 100.0
// );
// std::cout << "Minimum required pixels: " << min_mask_pixels << std::endl;
// if (non_zero_pixels >= min_mask_pixels && mask_percentage < camera.mask_max_percentage)
// {
// std::cout << "*** MOVEMENT CONFIRMED *** (mask has " << non_zero_pixels
// << " pixels, minimum: " << min_mask_pixels << ")" << std::endl;
// camera.movement_detected = true;
// camera.movement_last_detected = std::chrono::high_resolution_clock::now();
// camera.frame_index_with_movement = frame_index;
// }
// else
// {
// std::cout << "Movement REJECTED - mask too small (" << non_zero_pixels
// << " < " << min_mask_pixels << " pixels)" << std::endl;
// std::cout << "This was likely noise or compression artifacts" << std::endl;
// camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
// }
// }
// }
// else
// {
// std::cout << "No movement (Min PSNR: " << min_psnr
// << " >= threshold: " << camera.psnr_threshold << ")" << std::endl;
// }
// camera.transition_detected = (previous_movement_detected != camera.movement_detected);
// std::cout << "Current movement: " << (camera.movement_detected ? "YES" : "NO") << std::endl;
// std::cout << "Transition detected: " << (camera.transition_detected ? "YES" : "NO") << std::endl;
// if (camera.mask.empty() || (camera.transition_detected && !camera.movement_detected))
// {
// std::cout << "Resetting mask to zeros" << std::endl;
// camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
// }
// camera.output = working_image.clone();
// if (!camera.mask.empty() && camera.movement_detected && cv::countNonZero(camera.mask) > 0)
// {
// // Extract objects from working image
// outputObjects = extractObjectsFromMask(camera.mask, working_image, camera, camera_id);
// std::cout << "Objects extracted (at working resolution): " << outputObjects.size() << std::endl;
// // Scale objects back to original image size
// for (auto& obj : outputObjects)
// {
// // Scale bounding box coordinates
// obj.box.x = static_cast<int>(obj.box.x * scale_x);
// obj.box.y = static_cast<int>(obj.box.y * scale_y);
// obj.box.width = static_cast<int>(obj.box.width * scale_x);
// obj.box.height = static_cast<int>(obj.box.height * scale_y);
// // Clamp to original image boundaries
// obj.box.x = std::max(0, std::min(obj.box.x, original_width - 1));
// obj.box.y = std::max(0, std::min(obj.box.y, original_height - 1));
// obj.box.width = std::min(obj.box.width, original_width - obj.box.x);
// obj.box.height = std::min(obj.box.height, original_height - obj.box.y);
// // Scale polygon points if they exist
// if (!obj.polygon.empty())
// {
// for (auto& point : obj.polygon)
// {
// point.x = static_cast<int>(point.x * scale_x);
// point.y = static_cast<int>(point.y * scale_y);
// // Clamp points to image boundaries
// point.x = MAX(0, MIN(point.x, original_width - 1));
// point.y = MAX(0, MIN(point.y, original_height - 1));
// }
// }
// }
// std::cout << "Objects scaled back to original size (" << original_width
// << "x" << original_height << ")" << std::endl;
// }
// // Update control frames
// if (frame_index >= camera.next_key_frame ||
// camera.control_frames.size() < camera.number_of_control_frames)
// {
// std::cout << "Adding new control frame at index " << frame_index << std::endl;
// updateControlFrames(camera, frame_index, thumbnail);
// std::cout << "Next key frame will be at: " << camera.next_key_frame << std::endl;
// }
// camera.next_frame_index = frame_index + 1;
// auto end_time = std::chrono::high_resolution_clock::now();
// auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
// end_time - start_time
// );
// updateStatistics(camera, camera.most_recent_psnr_score, camera.movement_detected,
// processing_time);
// std::cout << "Processing time: " << processing_time.count() << "ms" << std::endl;
// std::cout << "=== End Debug ===" << std::endl;
// return outputObjects;
//}
std::vector<Object> ANSMOTIONDETECTOR::MovementDetectInternal(
const std::string& camera_id,
const size_t frame_index,
const cv::Mat& image,
CameraData& camera)
{
auto start_time = std::chrono::high_resolution_clock::now();
std::vector<Object> outputObjects;
if (image.empty())
{
return outputObjects;
}
// Store original dimensions for scaling back
const int original_width = image.cols;
const int original_height = image.rows;
const int max_dimension = std::max(original_width, original_height);
// Calculate scaling parameters
constexpr int target_size = 1920;
double scale_x = 1.0;
double scale_y = 1.0;
int working_width = original_width;
int working_height = original_height;
cv::Mat working_image;
if (max_dimension > target_size)
{
// Calculate new dimensions maintaining aspect ratio
const double aspect_ratio = static_cast<double>(original_width) / original_height;
if (original_width > original_height)
{
working_width = target_size;
working_height = static_cast<int>(target_size / aspect_ratio);
}
else
{
working_height = target_size;
working_width = static_cast<int>(target_size * aspect_ratio);
}
// Ensure minimum dimensions
working_width = std::max(1, working_width);
working_height = std::max(1, working_height);
// Calculate scale factors
scale_x = static_cast<double>(original_width) / working_width;
scale_y = static_cast<double>(original_height) / working_height;
// Resize once
cv::resize(image, working_image, cv::Size(working_width, working_height),
0, 0, cv::INTER_AREA);
}
else
{
// Use shallow copy if continuous, clone only if needed
working_image = image.isContinuous() ? image : image.clone();
}
// Clear previous contours
camera.contours.clear();
// Initialize thumbnail size if needed
if (camera.thumbnail_size.area() == 0)
{
setDynamicPSNRThreshold(camera, working_image);
camera.thumbnail_ratio = std::clamp(camera.thumbnail_ratio, 0.01, 1.0);
camera.thumbnail_size.width = static_cast<int>(working_width * camera.thumbnail_ratio);
camera.thumbnail_size.height = static_cast<int>(working_height * camera.thumbnail_ratio);
camera.thumbnail_size.width = std::max(1, camera.thumbnail_size.width);
camera.thumbnail_size.height = std::max(1, camera.thumbnail_size.height);
}
// Create thumbnail
cv::Mat thumbnail;
cv::resize(working_image, thumbnail, camera.thumbnail_size, 0, 0, cv::INTER_AREA);
if (thumbnail.empty())
{
return outputObjects;
}
// First frame initialization
if (camera.control_frames.empty())
{
camera.control_frames[frame_index] = thumbnail.clone();
camera.next_key_frame = frame_index + camera.key_frame_frequency;
camera.next_frame_index = frame_index + 1;
camera.output = working_image.isContinuous() ? working_image : working_image.clone();
camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start_time
);
updateStatistics(camera, 0.0, false, processing_time);
return outputObjects;
}
// Motion detection
const bool previous_movement_detected = camera.movement_detected;
camera.movement_detected = false;
double min_psnr = std::numeric_limits<double>::infinity();
cv::Mat best_control_frame;
size_t best_control_index = 0;
int compared_frames_count = 0;
// Compare with control frames within age limit
for (auto iter = camera.control_frames.rbegin();
iter != camera.control_frames.rend(); ++iter)
{
const size_t control_frame_index = iter->first;
const size_t frame_age = frame_index - control_frame_index;
// Skip frames exceeding max age
if (frame_age > camera.max_control_frame_age)
{
continue;
}
const cv::Mat& control_image = iter->second;
// Validate control frame
if (control_image.empty() ||
control_image.size() != thumbnail.size() ||
control_image.type() != thumbnail.type())
{
continue;
}
const double current_psnr = psnr(control_image, thumbnail);
// Skip infinite PSNR (identical frames)
if (std::isinf(current_psnr))
{
continue;
}
// Track minimum PSNR
if (current_psnr < min_psnr)
{
min_psnr = current_psnr;
best_control_frame = control_image;
best_control_index = control_frame_index;
}
compared_frames_count++;
}
// Handle no valid control frames
if (compared_frames_count == 0)
{
camera.most_recent_psnr_score = 0.0;
camera.movement_detected = false;
camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
camera.output = working_image.isContinuous() ? working_image : working_image.clone();
auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start_time
);
updateStatistics(camera, 0.0, false, processing_time);
return outputObjects;
}
camera.most_recent_psnr_score = min_psnr;
// Check for movement
if (!std::isinf(min_psnr) && min_psnr < camera.psnr_threshold)
{
if (!best_control_frame.empty())
{
camera.mask = computeMovementMask(best_control_frame, thumbnail,
working_image.size(),
camera.morphology_iterations, camera);
const int non_zero_pixels = cv::countNonZero(camera.mask);
const double mask_percentage = (non_zero_pixels * 100.0) / camera.mask.total();
const int min_mask_pixels = static_cast<int>(
working_image.total() * camera.min_mask_pixels_percentage / 100.0
);
if (non_zero_pixels >= min_mask_pixels &&
mask_percentage < camera.mask_max_percentage)
{
camera.movement_detected = true;
camera.movement_last_detected = std::chrono::high_resolution_clock::now();
camera.frame_index_with_movement = frame_index;
}
else
{
// Noise - reset mask
camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
}
}
}
camera.transition_detected = (previous_movement_detected != camera.movement_detected);
// Reset mask on transition to no movement
if (camera.mask.empty() || (camera.transition_detected && !camera.movement_detected))
{
camera.mask = cv::Mat(working_image.size(), CV_8UC1, cv::Scalar(0));
}
// Store output
camera.output = working_image.isContinuous() ? working_image : working_image.clone();
// Extract and scale objects
if (!camera.mask.empty() && camera.movement_detected && cv::countNonZero(camera.mask) > 0)
{
outputObjects = extractObjectsFromMask(camera.mask, working_image, camera, camera_id);
// Scale objects back to original size (only if resized)
if (scale_x != 1.0 || scale_y != 1.0)
{
const cv::Rect imageBounds(0, 0, original_width, original_height);
for (auto& obj : outputObjects)
{
// Scale bounding box
obj.box.x = static_cast<int>(obj.box.x * scale_x);
obj.box.y = static_cast<int>(obj.box.y * scale_y);
obj.box.width = static_cast<int>(obj.box.width * scale_x);
obj.box.height = static_cast<int>(obj.box.height * scale_y);
// Clamp to image boundaries
obj.box &= imageBounds;
// Scale polygon points
if (!obj.polygon.empty())
{
for (auto& point : obj.polygon)
{
point.x = std::clamp(point.x * static_cast<float>(scale_x),
0.0f, static_cast<float>(original_width - 1));
point.y = std::clamp(point.y * static_cast<float>(scale_y),
0.0f, static_cast<float>(original_height - 1));
}
}
}
}
}
// Update control frames
if (frame_index >= camera.next_key_frame ||
camera.control_frames.size() < camera.number_of_control_frames)
{
updateControlFrames(camera, frame_index, thumbnail);
}
camera.next_frame_index = frame_index + 1;
auto processing_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start_time
);
updateStatistics(camera, camera.most_recent_psnr_score, camera.movement_detected,
processing_time);
return outputObjects;
}
// Override functions
bool ANSMOTIONDETECTOR::Initialize(std::string licenseKey, ModelConfig modelConfig, const std::string& modelZipFilePath, const std::string& modelZipPassword, std::string& labelMap) {
bool result = ANSODBase::Initialize(licenseKey, modelConfig, modelZipFilePath, modelZipPassword, labelMap);
if (!result) return false;
_isInitialized = true;
//_modelConfig.detectionScoreThreshold = 0.5;
labelMap = "Movement";
return true;
}
bool ANSMOTIONDETECTOR::LoadModel(const std::string& modelZipFilePath, const std::string& modelZipPassword) {
bool result = ANSODBase::LoadModel(modelZipFilePath, modelZipPassword);
_isInitialized = true;
return true;
}
bool ANSMOTIONDETECTOR::LoadModelFromFolder(std::string licenseKey, ModelConfig modelConfig, std::string modelName, std::string className, const std::string& modelFolder, std::string& labelMap) {
bool result = ANSODBase::LoadModelFromFolder(licenseKey, modelConfig, modelName, className, modelFolder, labelMap);
if (!result) return false;
_isInitialized = true;
labelMap = "Movement";
return true;
}
bool ANSMOTIONDETECTOR::OptimizeModel(bool fp16, std::string& optimizedModelFolder) {
if (!ANSODBase::OptimizeModel(fp16, optimizedModelFolder)) {
return false;
}
return true;
}
std::vector<Object> ANSMOTIONDETECTOR::RunInference(const cv::Mat& input) {
return RunInference(input, "MotionCam");
}
std::vector<Object> ANSMOTIONDETECTOR::RunInference(const cv::Mat& input,const std::string& camera_id)
{
std::lock_guard<std::recursive_mutex> lock(_mutex);
try {
// Consolidated validation
if (!_licenseValid || input.empty() || input.cols < 5 || input.rows < 5)
{
return {};
}
// Direct call - no clone needed!
std::vector<Object> result = MovementDetect(camera_id, input);
if (_trackerEnabled) {
result = ApplyTracking(result, camera_id);
if (_stabilizationEnabled) result = StabilizeDetections(result, camera_id);
}
return result;
}
catch (const std::exception& e) {
_logger.LogFatal("ANSMOTIONDETECTOR::RunInference",
e.what(), __FILE__, __LINE__);
return {};
}
}
bool ANSMOTIONDETECTOR::Destroy() {
return true;
}
}
/*std::vector<Object> ANSMOTIONDETECTOR::RunInference(const cv::Mat& input, const std::string& camera_id) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
std::vector<Object> objects;
objects.clear();
try {
if (!_licenseValid) return objects;
if (input.empty() || !input.data || !input.u) {
return objects;
}
if ((input.cols < 5) || (input.rows < 5)) return objects;
cv::Mat frame = input.clone();
objects = MovementDetect(camera_id, frame);
frame.release();
return objects;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSMOTIONDETECTOR::RunInference", e.what(), __FILE__, __LINE__);
return objects;
}
}*//*std::vector<Object> ANSMOTIONDETECTOR::RunInference(const cv::Mat& input, const std::string& camera_id) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
std::vector<Object> objects;
objects.clear();
try {
if (!_licenseValid) return objects;
if (input.empty() || !input.data || !input.u) {
return objects;
}
if ((input.cols < 5) || (input.rows < 5)) return objects;
cv::Mat frame = input.clone();
objects = MovementDetect(camera_id, frame);
frame.release();
return objects;
}
catch (std::exception& e) {
this->_logger.LogFatal("ANSMOTIONDETECTOR::RunInference", e.what(), __FILE__, __LINE__);
return objects;
}
}*/