Add unit testes

This commit is contained in:
2026-04-05 14:30:43 +10:00
parent fed40b0c90
commit f57ed78763
12 changed files with 1013 additions and 216 deletions

View File

@@ -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;

View File

@@ -1,6 +1,7 @@
#include "ANSLIB.h"
#include <deque>
#include <unordered_set>
#include <unordered_map>
#define RETAINFRAMES 80
#define FILTERFRAMES 10
@@ -13,6 +14,22 @@ class CUSTOM_API ANSCustomFS : public IANSCustomClass
int priority;
ImageSection(const cv::Rect& r) : region(r), priority(0) {}
};
// Track record for voting-based detection confirmation
struct TrackRecord {
int trackId{ 0 };
int classId{ 0 }; // fire=0, smoke=2
std::deque<cv::Rect> bboxHistory; // bounding box history within window
int detectedCount{ 0 }; // frames detected in sliding window
int totalFrames{ 0 }; // total frames since track appeared
bool confirmed{ false }; // passed voting threshold
};
// Voting mechanism constants
static constexpr int VOTE_WINDOW = 15;
static constexpr int VOTE_THRESHOLD = 8;
static constexpr float BBOX_CHANGE_THRESHOLD = 0.05f;
private:
using ANSLIBPtr = std::unique_ptr<ANSCENTER::ANSLIB, decltype(&ANSCENTER::ANSLIB::Destroy)>;
@@ -74,6 +91,9 @@ private:
float _smokeDetetectionThreshold{ 0 };
float _motionSpecificity{ 0 };
// Tracker-based voting state
std::unordered_map<int, TrackRecord> _trackHistory;
cv::Rect GenerateMinimumSquareBoundingBox(const std::vector<ANSCENTER::Object>& detectedObjects, int minSize = 640);
void UpdateNoDetectionCondition();
bool detectStaticFire(std::deque<cv::Mat>& frameQueue);
@@ -101,7 +121,6 @@ private:
void ResetDetectionState();
void GetModelParameters();
std::vector<ANSCENTER::Object> ProcessExistingDetectedArea(const cv::Mat& frame, const std::string& camera_id, cv::Mat& draw);
bool ProcessValidDetection(const cv::Mat& frame, const std::string& camera_id, cv::Mat& draw, ANSCENTER::Object& detectedObj, std::vector<ANSCENTER::Object>& output);
void AddConfirmedDetection(ANSCENTER::Object& detectedObj, std::vector<ANSCENTER::Object>& output);
#ifdef FNS_DEBUG
void DisplayDebugFrame(cv::Mat& draw) {
@@ -117,21 +136,11 @@ private:
int getLowestPriorityRegion();
cv::Rect getRegionByPriority(int priority);
std::vector<ANSCENTER::Object> ProcessExistingDetectedArea(
const cv::Mat& frame,
const std::string& camera_id,
const std::vector<cv::Rect>& fireNSmokeRects, cv::Mat& draw);
bool 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);
bool ValidateWithFilter(
const cv::Mat& frame,
const ANSCENTER::Object& detectedObj,
const std::string& camera_id,
std::vector<ANSCENTER::Object>& output, cv::Mat& draw);
// Tracker-based voting methods
void UpdateTrackHistory(int trackId, int classId, const cv::Rect& bbox);
bool IsTrackConfirmed(int trackId) const;
bool HasBboxMovement(int trackId) const;
void AgeTracks(const std::unordered_set<int>& detectedTrackIds);
std::vector<ANSCENTER::Object> FindNewDetectedArea(
const cv::Mat& frame,
const std::string& camera_id, cv::Mat& draw);