2056 lines
62 KiB
C++
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;
|
|||
|
|
}
|
|||
|
|
}*/
|