Add unit testes
This commit is contained in:
@@ -56,7 +56,109 @@ void ANSCustomFS::ResetDetectionState() {
|
||||
ResetDetectedArea();
|
||||
_retainDetectedArea = 0;
|
||||
_isFireNSmokeDetected = false;
|
||||
_trackHistory.clear();
|
||||
}
|
||||
// --- Tracker-based voting functions ---
|
||||
|
||||
void ANSCustomFS::UpdateTrackHistory(int trackId, int classId, const cv::Rect& bbox) {
|
||||
auto it = _trackHistory.find(trackId);
|
||||
if (it == _trackHistory.end()) {
|
||||
TrackRecord record;
|
||||
record.trackId = trackId;
|
||||
record.classId = classId;
|
||||
record.bboxHistory.push_back(bbox);
|
||||
record.detectedCount = 1;
|
||||
record.totalFrames = 1;
|
||||
record.confirmed = false;
|
||||
_trackHistory[trackId] = std::move(record);
|
||||
}
|
||||
else {
|
||||
auto& record = it->second;
|
||||
record.bboxHistory.push_back(bbox);
|
||||
record.detectedCount++;
|
||||
record.totalFrames++;
|
||||
|
||||
// Slide the window: keep only VOTE_WINDOW entries in bbox history
|
||||
while (static_cast<int>(record.bboxHistory.size()) > VOTE_WINDOW) {
|
||||
record.bboxHistory.pop_front();
|
||||
record.detectedCount = std::max(0, record.detectedCount - 1);
|
||||
}
|
||||
|
||||
// Update confirmed status
|
||||
if (record.detectedCount >= VOTE_THRESHOLD) {
|
||||
record.confirmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ANSCustomFS::IsTrackConfirmed(int trackId) const {
|
||||
auto it = _trackHistory.find(trackId);
|
||||
if (it == _trackHistory.end()) return false;
|
||||
return it->second.detectedCount >= VOTE_THRESHOLD;
|
||||
}
|
||||
|
||||
bool ANSCustomFS::HasBboxMovement(int trackId) const {
|
||||
auto it = _trackHistory.find(trackId);
|
||||
if (it == _trackHistory.end()) return false;
|
||||
|
||||
const auto& history = it->second.bboxHistory;
|
||||
if (history.size() < 2) return false;
|
||||
|
||||
const cv::Rect& earliest = history.front();
|
||||
const cv::Rect& latest = history.back();
|
||||
|
||||
// Calculate center positions
|
||||
float cx1 = earliest.x + earliest.width / 2.0f;
|
||||
float cy1 = earliest.y + earliest.height / 2.0f;
|
||||
float cx2 = latest.x + latest.width / 2.0f;
|
||||
float cy2 = latest.y + latest.height / 2.0f;
|
||||
|
||||
// Average size for normalization
|
||||
float avgWidth = (earliest.width + latest.width) / 2.0f;
|
||||
float avgHeight = (earliest.height + latest.height) / 2.0f;
|
||||
if (avgWidth < 1.0f || avgHeight < 1.0f) return false;
|
||||
|
||||
// Position change relative to average size
|
||||
float posChange = std::sqrt(
|
||||
std::pow((cx2 - cx1) / avgWidth, 2) +
|
||||
std::pow((cy2 - cy1) / avgHeight, 2)
|
||||
);
|
||||
|
||||
// Size change relative to average area
|
||||
float area1 = static_cast<float>(earliest.area());
|
||||
float area2 = static_cast<float>(latest.area());
|
||||
float avgArea = (area1 + area2) / 2.0f;
|
||||
float sizeChange = (avgArea > 0) ? std::abs(area2 - area1) / avgArea : 0.0f;
|
||||
|
||||
return (posChange > BBOX_CHANGE_THRESHOLD) || (sizeChange > BBOX_CHANGE_THRESHOLD);
|
||||
}
|
||||
|
||||
void ANSCustomFS::AgeTracks(const std::unordered_set<int>& detectedTrackIds) {
|
||||
auto it = _trackHistory.begin();
|
||||
while (it != _trackHistory.end()) {
|
||||
if (detectedTrackIds.find(it->first) == detectedTrackIds.end()) {
|
||||
// Track was NOT detected this frame
|
||||
it->second.totalFrames++;
|
||||
|
||||
// Remove stale tracks that haven't been seen recently
|
||||
if (it->second.totalFrames > VOTE_WINDOW &&
|
||||
it->second.detectedCount == 0) {
|
||||
it = _trackHistory.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Age out the sliding window (add a "miss" frame)
|
||||
if (static_cast<int>(it->second.bboxHistory.size()) >= VOTE_WINDOW) {
|
||||
it->second.bboxHistory.pop_front();
|
||||
it->second.detectedCount = std::max(0, it->second.detectedCount - 1);
|
||||
}
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// --- End voting functions ---
|
||||
|
||||
void ANSCustomFS::UpdateNoDetectionCondition()
|
||||
{
|
||||
_isRealFireFrame = false;
|
||||
@@ -120,152 +222,6 @@ void ANSCustomFS::GetModelParameters() {
|
||||
_readROIs = true;
|
||||
}
|
||||
}
|
||||
std::vector<ANSCENTER::Object> ANSCustomFS::ProcessExistingDetectedArea(
|
||||
const cv::Mat& frame,
|
||||
const std::string& camera_id,
|
||||
const std::vector<cv::Rect>& fireNSmokeRects,
|
||||
cv::Mat& draw)
|
||||
{
|
||||
#ifdef FNS_DEBUG
|
||||
cv::rectangle(draw, _detectedArea, cv::Scalar(255, 255, 0), 2); // Cyan
|
||||
#endif
|
||||
|
||||
// Run detection on ROI (no clone - just a view into frame)
|
||||
cv::Mat activeROI = frame(_detectedArea);
|
||||
std::vector<ANSCENTER::Object> detectedObjects;
|
||||
_detector->RunInference(activeROI, camera_id.c_str(), detectedObjects);
|
||||
|
||||
if (detectedObjects.empty()) {
|
||||
UpdateNoDetectionCondition();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> output;
|
||||
output.reserve(detectedObjects.size());
|
||||
|
||||
for (auto& detectedObj : detectedObjects) {
|
||||
ProcessDetectedObject(frame, detectedObj, camera_id, fireNSmokeRects, output, draw);
|
||||
}
|
||||
|
||||
if (output.empty()) {
|
||||
UpdateNoDetectionCondition();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
bool ANSCustomFS::ProcessDetectedObject(
|
||||
const cv::Mat& frame,
|
||||
ANSCENTER::Object& detectedObj,
|
||||
const std::string& camera_id,
|
||||
const std::vector<cv::Rect>& fireNSmokeRects,
|
||||
std::vector<ANSCENTER::Object>& output, cv::Mat& draw)
|
||||
{
|
||||
// Adjust coordinates to frame space
|
||||
detectedObj.box.x += _detectedArea.x;
|
||||
detectedObj.box.y += _detectedArea.y;
|
||||
detectedObj.cameraId = camera_id;
|
||||
|
||||
// Check exclusive ROI overlap
|
||||
if (IsROIOverlapping(detectedObj.box, _exclusiveROIs, INCLUSIVE_IOU_THRESHOLD)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check confidence threshold
|
||||
if (detectedObj.confidence <= _detectionScoreThreshold) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if fire or smoke
|
||||
if (!IsFireOrSmoke(detectedObj.classId, detectedObj.confidence)) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for reflection
|
||||
cv::Mat objectMask = frame(detectedObj.box);
|
||||
if (detectReflection(objectMask)) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check area overlap
|
||||
float areaOverlap = calculateIoU(_detectedArea, detectedObj.box);
|
||||
if (areaOverlap >= MAX_AREA_OVERLAP) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef FNS_DEBUG
|
||||
cv::Scalar color = (detectedObj.classId == 0) ?
|
||||
cv::Scalar(0, 255, 255) : cv::Scalar(255, 0, 255); // Yellow/Purple
|
||||
cv::rectangle(draw, detectedObj.box, color, 2);
|
||||
#endif
|
||||
|
||||
// Check motion correlation
|
||||
if (!ValidateMotionCorrelation(fireNSmokeRects)) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsOverlapping(detectedObj, fireNSmokeRects, 0)) {
|
||||
UpdateNoDetectionCondition();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter validation
|
||||
if (!ValidateWithFilter(frame, detectedObj, camera_id, output, draw))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
bool ANSCustomFS::ValidateWithFilter(
|
||||
const cv::Mat& frame,
|
||||
const ANSCENTER::Object& detectedObj,
|
||||
const std::string& camera_id,
|
||||
std::vector<ANSCENTER::Object>& output, cv::Mat& draw)
|
||||
{
|
||||
// Skip filter check after sufficient confirmation frames
|
||||
if (_realFireCheck > FILTER_VERIFICATION_FRAMES) {
|
||||
output.push_back(detectedObj);
|
||||
UpdatePositiveDetection();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run filter inference
|
||||
std::vector<ANSCENTER::Object> filteredObjects;
|
||||
_filter->RunInference(frame, camera_id.c_str(), filteredObjects);
|
||||
std::vector<ANSCENTER::Object> excludedObjects;
|
||||
|
||||
for (const auto& filteredObj : filteredObjects) {
|
||||
if (EXCLUDED_FILTER_CLASSES.find(filteredObj.classId) == EXCLUDED_FILTER_CLASSES.end()) {
|
||||
excludedObjects.push_back(filteredObj);
|
||||
|
||||
#ifdef FNS_DEBUG
|
||||
cv::rectangle(draw, filteredObj.box, cv::Scalar(0, 255, 0), 2);
|
||||
cv::putText(draw, filteredObj.className,
|
||||
cv::Point(filteredObj.box.x, filteredObj.box.y - 10),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 2);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Check if detection overlaps with excluded objects
|
||||
if (excludedObjects.empty() || !IsOverlapping(detectedObj, excludedObjects, 0)) {
|
||||
output.push_back(detectedObj);
|
||||
UpdatePositiveDetection();
|
||||
_realFireCheck++;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// Decrement but don't go negative
|
||||
_realFireCheck = std::max(0, _realFireCheck - 1);
|
||||
_isRealFireFrame = (_realFireCheck > 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> ANSCustomFS::FindNewDetectedArea(
|
||||
const cv::Mat& frame,
|
||||
@@ -576,6 +532,12 @@ bool ANSCustomFS::Initialize(const std::string& modelDirectory, float detectionS
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable ByteTrack tracker on the detector for persistent track IDs
|
||||
int trackerResult = _detector->SetTracker(0 /*BYTETRACK*/, 1 /*enable*/);
|
||||
if (trackerResult != 1) {
|
||||
std::cerr << "ANSCustomFS::Initialize: Warning - Failed to enable ByteTrack tracker." << std::endl;
|
||||
}
|
||||
|
||||
// Load filter model (COCO general object detector for false positive filtering)
|
||||
float filterScoreThreshold = 0.25f;
|
||||
float filterConfThreshold = 0.5f;
|
||||
@@ -1031,7 +993,7 @@ std::vector<CustomObject> ANSCustomFS::RunInference(const cv::Mat& input, const
|
||||
}
|
||||
}
|
||||
|
||||
// New helper function to process detected area
|
||||
// Stage A: Process existing detected area with tracker-based voting
|
||||
std::vector<ANSCENTER::Object> ANSCustomFS::ProcessExistingDetectedArea(
|
||||
const cv::Mat& frame,
|
||||
const std::string& camera_id,
|
||||
@@ -1041,18 +1003,19 @@ std::vector<ANSCENTER::Object> ANSCustomFS::ProcessExistingDetectedArea(
|
||||
|
||||
cv::Mat activeROI = frame(_detectedArea);
|
||||
|
||||
// Detect movement and objects
|
||||
std::vector<ANSCENTER::Object> movementObjects = FindMovementObjects(frame, camera_id, draw);
|
||||
// Run detector on ROI — tracker assigns persistent trackIds
|
||||
std::vector<ANSCENTER::Object> detectedObjects;
|
||||
_detector->RunInference(activeROI, camera_id.c_str(), detectedObjects);
|
||||
|
||||
if (detectedObjects.empty()) {
|
||||
// Age all existing tracks (missed frame for all)
|
||||
AgeTracks({});
|
||||
UpdateNoDetectionCondition();
|
||||
return output;
|
||||
}
|
||||
|
||||
const bool skipMotionCheck = (_motionSpecificity < 0.0f) || (_motionSpecificity >= 1.0f);
|
||||
const bool validMovement = !movementObjects.empty() && movementObjects.size() < MAX_MOTION_TRACKING;
|
||||
// Collect detected track IDs this frame for aging
|
||||
std::unordered_set<int> detectedTrackIds;
|
||||
|
||||
for (auto& detectedObj : detectedObjects) {
|
||||
// Adjust coordinates to full frame
|
||||
@@ -1060,84 +1023,75 @@ std::vector<ANSCENTER::Object> ANSCustomFS::ProcessExistingDetectedArea(
|
||||
detectedObj.box.y += _detectedArea.y;
|
||||
detectedObj.cameraId = camera_id;
|
||||
|
||||
// Skip if overlapping with exclusive ROIs
|
||||
// 1. Exclusive ROI check — skip if overlapping exclusion zones
|
||||
if (IsROIOverlapping(detectedObj.box, _exclusiveROIs, INCLUSIVE_IOU_THRESHOLD)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check confidence thresholds
|
||||
// 2. Confidence check — fire >= threshold, smoke >= smoke threshold
|
||||
const bool isValidFire = (detectedObj.classId == 0) && (detectedObj.confidence >= _detectionScoreThreshold);
|
||||
const bool isValidSmoke = (detectedObj.classId == 2) && (detectedObj.confidence >= _smokeDetetectionThreshold);
|
||||
|
||||
if (!isValidFire && !isValidSmoke) {
|
||||
UpdateNoDetectionCondition();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check area overlap
|
||||
const float area_threshold = calculateIoU(_detectedArea, detectedObj.box);
|
||||
if (area_threshold >= MAX_AREA_OVERLAP) {
|
||||
UpdateNoDetectionCondition();
|
||||
continue;
|
||||
}
|
||||
// 3. Update track history with this detection
|
||||
int trackId = detectedObj.trackId;
|
||||
detectedTrackIds.insert(trackId);
|
||||
UpdateTrackHistory(trackId, detectedObj.classId, detectedObj.box);
|
||||
|
||||
#ifdef FNS_DEBUG
|
||||
// Draw detection with track info
|
||||
cv::Scalar color = (detectedObj.classId == 0) ? cv::Scalar(0, 255, 255) : cv::Scalar(255, 0, 255);
|
||||
cv::rectangle(draw, detectedObj.box, color, 2);
|
||||
auto trackIt = _trackHistory.find(trackId);
|
||||
if (trackIt != _trackHistory.end()) {
|
||||
std::string label = "T" + std::to_string(trackId) + " " +
|
||||
std::to_string(trackIt->second.detectedCount) + "/" +
|
||||
std::to_string(VOTE_THRESHOLD);
|
||||
cv::putText(draw, label,
|
||||
cv::Point(detectedObj.box.x, detectedObj.box.y - 10),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 2);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check motion
|
||||
if (!skipMotionCheck && !validMovement) {
|
||||
UpdateNoDetectionCondition();
|
||||
// 4. Voting check — require consistent detection across frames
|
||||
if (!IsTrackConfirmed(trackId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!skipMotionCheck && !IsOverlapping(detectedObj, movementObjects, 0)) {
|
||||
UpdateNoDetectionCondition();
|
||||
// 5. Movement check — verify bounding box is changing (not static false positive)
|
||||
if (!HasBboxMovement(trackId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process valid detection
|
||||
if (!ProcessValidDetection(frame, camera_id, draw, detectedObj, output)) {
|
||||
// 6. COCO filter — exclude detections that overlap with known non-fire objects
|
||||
std::vector<ANSCENTER::Object> excludedObjects = FindExcludedObjects(frame, camera_id, draw);
|
||||
if (!excludedObjects.empty() && IsOverlapping(detectedObj, excludedObjects, 0)) {
|
||||
// Detection overlaps with a known object — not fire/smoke
|
||||
_realFireCheck = std::max(0, _realFireCheck - 1);
|
||||
if (_realFireCheck <= 0) {
|
||||
_isRealFireFrame = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// All checks passed — confirmed detection
|
||||
AddConfirmedDetection(detectedObj, output);
|
||||
_realFireCheck++;
|
||||
}
|
||||
|
||||
// Age out tracks not seen this frame
|
||||
AgeTracks(detectedTrackIds);
|
||||
|
||||
if (output.empty()) {
|
||||
UpdateNoDetectionCondition();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
bool ANSCustomFS::ProcessValidDetection(
|
||||
const cv::Mat& frame,
|
||||
const std::string& camera_id,
|
||||
cv::Mat& draw,
|
||||
ANSCENTER::Object& detectedObj,
|
||||
std::vector<ANSCENTER::Object>& output)
|
||||
{
|
||||
if (_realFireCheck > FILTERFRAMES) {
|
||||
AddConfirmedDetection(detectedObj, output);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<ANSCENTER::Object> excludedObjects = FindExcludedObjects(frame, camera_id, draw);
|
||||
|
||||
if (excludedObjects.empty()) {
|
||||
AddConfirmedDetection(detectedObj, output);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsOverlapping(detectedObj, excludedObjects, 0)) {
|
||||
AddConfirmedDetection(detectedObj, output);
|
||||
_realFireCheck++;
|
||||
return true;
|
||||
}
|
||||
|
||||
_realFireCheck = std::max(0, _realFireCheck - 1);
|
||||
if (_realFireCheck <= 0) {
|
||||
_isRealFireFrame = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ANSCustomFS::AddConfirmedDetection(ANSCENTER::Object& detectedObj, std::vector<ANSCENTER::Object>& output) {
|
||||
output.push_back(std::move(detectedObj));
|
||||
_isFireNSmokeDetected = true;
|
||||
|
||||
Reference in New Issue
Block a user