#include "ANSMotionDetector.h" namespace ANSCENTER { ANSMOTIONDETECTOR::~ANSMOTIONDETECTOR() { std::lock_guard lock(cameras_mutex); for (auto& [key, cameraData] : cameras) { cameraData.clear(); } cameras.clear(); } bool ANSMOTIONDETECTOR::hasCameraData(const std::string& camera_id) const { std::lock_guard lock(cameras_mutex); return cameras.find(camera_id) != cameras.end(); } void ANSMOTIONDETECTOR::removeCamera(const std::string& camera_id) { std::lock_guard lock(cameras_mutex); auto it = cameras.find(camera_id); if (it != cameras.end()) { it->second.clear(); cameras.erase(it); } } std::vector ANSMOTIONDETECTOR::getCameraIds() const { std::lock_guard lock(cameras_mutex); std::vector 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 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 lock(cameras_mutex); auto it = cameras.find(camera_id); if (it != cameras.end()) { it->second.clear(); } } void ANSMOTIONDETECTOR::clearAll() { std::lock_guard 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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( now - it->second.movement_last_detected ); } size_t ANSMOTIONDETECTOR::getControlFrameCount(const std::string& camera_id) const { std::lock_guard 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 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 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 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 lock(cameras_mutex); auto it = cameras.find(camera_id); if (it != cameras.end()) { return it->second.mask; } return cv::Mat(); } std::vector> ANSMOTIONDETECTOR::getContours(const std::string& camera_id) const { std::lock_guard lock(cameras_mutex); auto it = cameras.find(camera_id); if (it != cameras.end()) { return it->second.contours; } return std::vector>(); } ANSMOTIONDETECTOR::CameraStats ANSMOTIONDETECTOR::getStats(const std::string& camera_id) const { std::lock_guard 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 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(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 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(cvFloor(static_cast(flat.cols) * half_percent)); const int hi = flat.at(cvCeil(static_cast(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 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(i) += hist.at(i - 1); } // Find low and high percentile values const float total_pixels = hist.at(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(i) >= low_threshold) { lo = i; break; } } for (int i = 255; i >= 0; i--) { if (hist.at(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& 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 ANSMOTIONDETECTOR::extractObjectsFromMask(const cv::Mat& mask, // const cv::Mat& image, // CameraData& camera, // const std::string& camera_id) //{ // std::vector outputObjects; // if (mask.empty()) // { // return outputObjects; // } // std::vector 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(boundingRect.x), static_cast(boundingRect.y)), // cv::Point2f(static_cast(boundingRect.x + boundingRect.width), static_cast(boundingRect.y)), // cv::Point2f(static_cast(boundingRect.x + boundingRect.width), static_cast(boundingRect.y + boundingRect.height)), // cv::Point2f(static_cast(boundingRect.x), static_cast(boundingRect.y + boundingRect.height)) // }; // if ((boundingRect.width >= 25) && // (boundingRect.height >= 25))outputObjects.push_back(obj); // } // return outputObjects; //} std::vector ANSMOTIONDETECTOR::extractObjectsFromMask( const cv::Mat& mask, const cv::Mat& image, CameraData& camera, const std::string& camera_id) { std::vector outputObjects; if (mask.empty()) { return outputObjects; } // Use local contours to avoid overwriting camera.contours std::vector> contours; std::vector 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(boundingRect.x), static_cast(boundingRect.y)), cv::Point2f(static_cast(boundingRect.x + boundingRect.width), static_cast(boundingRect.y)), cv::Point2f(static_cast(boundingRect.x + boundingRect.width), static_cast(boundingRect.y + boundingRect.height)), cv::Point2f(static_cast(boundingRect.x), static_cast(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(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 ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id, const cv::Mat& next_image) { std::lock_guard lock(cameras_mutex); CameraData& camera = getCameraData(camera_id); return MovementDetect(camera_id, camera.next_frame_index, next_image); } //std::vector ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id, const size_t frame_index, const cv::Mat& image) //{ // std::lock_guard lock(cameras_mutex); // if (image.empty()) // { // return std::vector(); // } // // 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(); // } // // 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 ANSMOTIONDETECTOR::MovementDetect(const std::string& camera_id, const size_t frame_index, const cv::Mat& image) { std::lock_guard 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 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 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 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 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 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 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 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 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(intersection_pixels) / static_cast(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(m.m10 / m.m00); centroid.y = static_cast(m.m01 / m.m00); return centroid; } double ANSMOTIONDETECTOR::calculateLocationStability(const std::deque& 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 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 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(original_width) / static_cast(original_height); // if (original_width > original_height) // { // working_width = target_size; // working_height = static_cast(target_size / aspect_ratio); // } // else // { // working_height = target_size; // working_width = static_cast(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(original_width) / static_cast(working_width); // scale_y = static_cast(original_height) / static_cast(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(working_width * camera.thumbnail_ratio); // camera.thumbnail_size.height = static_cast(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( // 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::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( // 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( // 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(obj.box.x * scale_x); // obj.box.y = static_cast(obj.box.y * scale_y); // obj.box.width = static_cast(obj.box.width * scale_x); // obj.box.height = static_cast(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(point.x * scale_x); // point.y = static_cast(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( // 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 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 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(original_width) / original_height; if (original_width > original_height) { working_width = target_size; working_height = static_cast(target_size / aspect_ratio); } else { working_height = target_size; working_width = static_cast(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(original_width) / working_width; scale_y = static_cast(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(working_width * camera.thumbnail_ratio); camera.thumbnail_size.height = static_cast(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::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::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::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( 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(obj.box.x * scale_x); obj.box.y = static_cast(obj.box.y * scale_y); obj.box.width = static_cast(obj.box.width * scale_x); obj.box.height = static_cast(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(scale_x), 0.0f, static_cast(original_width - 1)); point.y = std::clamp(point.y * static_cast(scale_y), 0.0f, static_cast(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::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 ANSMOTIONDETECTOR::RunInference(const cv::Mat& input) { return RunInference(input, "MotionCam"); } std::vector ANSMOTIONDETECTOR::RunInference(const cv::Mat& input,const std::string& camera_id) { std::lock_guard lock(_mutex); try { // Consolidated validation if (!_licenseValid || input.empty() || input.cols < 5 || input.rows < 5) { return {}; } // Direct call - no clone needed! std::vector 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 ANSMOTIONDETECTOR::RunInference(const cv::Mat& input, const std::string& camera_id) { std::lock_guard lock(_mutex); std::vector 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 ANSMOTIONDETECTOR::RunInference(const cv::Mat& input, const std::string& camera_id) { std::lock_guard lock(_mutex); std::vector 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; } }*/