add face feature extraction pipeline
This commit is contained in:
25
face_pipeline/CMakeLists.txt
Normal file
25
face_pipeline/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
add_library(face_pipeline STATIC
|
||||
src/motion_detector.cpp
|
||||
src/face_detector.cpp
|
||||
src/face_aligner.cpp
|
||||
src/face_embedder.cpp
|
||||
src/iou_tracker.cpp
|
||||
src/result_sink.cpp
|
||||
src/face_pipeline.cpp
|
||||
)
|
||||
|
||||
target_include_directories(face_pipeline PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(face_pipeline PUBLIC
|
||||
facedetection
|
||||
${OpenCV_LIBS}
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(face_pipeline PRIVATE -O3)
|
||||
elseif(MSVC)
|
||||
target_compile_options(face_pipeline PRIVATE /O2)
|
||||
endif()
|
||||
15
face_pipeline/include/face_pipeline/face_aligner.h
Normal file
15
face_pipeline/include/face_pipeline/face_aligner.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class FaceAligner {
|
||||
public:
|
||||
static cv::Size outputSize();
|
||||
bool align(const cv::Mat& frame, const FaceObservation& face, cv::Mat& aligned) const;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
21
face_pipeline/include/face_pipeline/face_detector.h
Normal file
21
face_pipeline/include/face_pipeline/face_detector.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class FaceDetector {
|
||||
public:
|
||||
explicit FaceDetector(float minimum_score = 0.60f);
|
||||
std::vector<FaceObservation> detect(const cv::Mat& frame);
|
||||
|
||||
private:
|
||||
float minimum_score_;
|
||||
std::vector<unsigned char> result_buffer_;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
30
face_pipeline/include/face_pipeline/face_embedder.h
Normal file
30
face_pipeline/include/face_pipeline/face_embedder.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class FaceEmbedder {
|
||||
public:
|
||||
virtual ~FaceEmbedder() = default;
|
||||
virtual std::vector<float> extract(const cv::Mat& aligned_face) = 0;
|
||||
virtual std::string modelName() const = 0;
|
||||
};
|
||||
|
||||
class SFaceEmbedder : public FaceEmbedder {
|
||||
public:
|
||||
explicit SFaceEmbedder(const std::string& model_path);
|
||||
~SFaceEmbedder() override;
|
||||
std::vector<float> extract(const cv::Mat& aligned_face) override;
|
||||
std::string modelName() const override;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
62
face_pipeline/include/face_pipeline/face_pipeline.h
Normal file
62
face_pipeline/include/face_pipeline/face_pipeline.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "face_pipeline/face_aligner.h"
|
||||
#include "face_pipeline/face_detector.h"
|
||||
#include "face_pipeline/face_embedder.h"
|
||||
#include "face_pipeline/iou_tracker.h"
|
||||
#include "face_pipeline/motion_detector.h"
|
||||
#include "face_pipeline/result_sink.h"
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class FacePipeline {
|
||||
public:
|
||||
FacePipeline(std::unique_ptr<FaceEmbedder> embedder,
|
||||
std::unique_ptr<ResultSink> sink);
|
||||
~FacePipeline();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool submit(const FramePacket& packet);
|
||||
std::vector<TrackedFace> latestTracks() const;
|
||||
|
||||
private:
|
||||
void workerLoop();
|
||||
void process(const FramePacket& packet);
|
||||
|
||||
MotionDetector motion_detector_;
|
||||
FaceDetector face_detector_;
|
||||
FaceAligner face_aligner_;
|
||||
IouTracker tracker_;
|
||||
std::unique_ptr<FaceEmbedder> embedder_;
|
||||
std::unique_ptr<ResultSink> sink_;
|
||||
|
||||
mutable std::mutex state_mutex_;
|
||||
std::vector<TrackedFace> latest_tracks_;
|
||||
std::set<std::uint64_t> published_tracks_;
|
||||
|
||||
std::mutex mailbox_mutex_;
|
||||
std::condition_variable mailbox_cv_;
|
||||
FramePacket mailbox_;
|
||||
bool has_mail_ = false;
|
||||
bool running_ = false;
|
||||
std::atomic<bool> has_active_tracks_{false};
|
||||
std::atomic<std::uint64_t> submitted_frames_{0};
|
||||
std::atomic<std::uint64_t> overwritten_frames_{0};
|
||||
std::uint64_t processed_frames_ = 0;
|
||||
std::uint64_t alignment_failures_ = 0;
|
||||
std::uint64_t embedding_failures_ = 0;
|
||||
std::uint64_t publish_failures_ = 0;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
40
face_pipeline/include/face_pipeline/iou_tracker.h
Normal file
40
face_pipeline/include/face_pipeline/iou_tracker.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
struct IouTrackerConfig {
|
||||
float match_threshold = 0.30f;
|
||||
std::chrono::milliseconds track_ttl{2000};
|
||||
};
|
||||
|
||||
class IouTracker {
|
||||
public:
|
||||
explicit IouTracker(const IouTrackerConfig& config = IouTrackerConfig());
|
||||
std::vector<TrackedFace> update(const std::vector<FaceObservation>& observations,
|
||||
SteadyTime now);
|
||||
bool hasActiveTracks(SteadyTime now) const;
|
||||
std::vector<std::uint64_t> activeTrackIds() const;
|
||||
std::size_t size() const;
|
||||
|
||||
private:
|
||||
struct Track {
|
||||
std::uint64_t id;
|
||||
FaceObservation observation;
|
||||
SteadyTime last_seen;
|
||||
};
|
||||
|
||||
static float intersectionOverUnion(const cv::Rect& a, const cv::Rect& b);
|
||||
void expire(SteadyTime now);
|
||||
|
||||
IouTrackerConfig config_;
|
||||
std::vector<Track> tracks_;
|
||||
std::uint64_t next_track_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
34
face_pipeline/include/face_pipeline/motion_detector.h
Normal file
34
face_pipeline/include/face_pipeline/motion_detector.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
struct MotionDetectorConfig {
|
||||
cv::Size analysis_size{320, 180};
|
||||
int pixel_threshold = 20;
|
||||
double changed_ratio_threshold = 0.01;
|
||||
std::chrono::milliseconds active_hold{500};
|
||||
std::chrono::milliseconds tracked_scan_interval{1000};
|
||||
std::chrono::milliseconds idle_scan_interval{5000};
|
||||
};
|
||||
|
||||
class MotionDetector {
|
||||
public:
|
||||
explicit MotionDetector(const MotionDetectorConfig& config = MotionDetectorConfig());
|
||||
bool shouldProcess(const cv::Mat& frame, SteadyTime now, bool has_active_tracks);
|
||||
void reset();
|
||||
|
||||
private:
|
||||
MotionDetectorConfig config_;
|
||||
cv::Mat previous_gray_;
|
||||
SteadyTime last_motion_;
|
||||
SteadyTime last_request_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
24
face_pipeline/include/face_pipeline/result_sink.h
Normal file
24
face_pipeline/include/face_pipeline/result_sink.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <ostream>
|
||||
|
||||
#include "face_pipeline/types.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class ResultSink {
|
||||
public:
|
||||
virtual ~ResultSink() = default;
|
||||
virtual bool publish(const FaceFeatureEvent& event) = 0;
|
||||
};
|
||||
|
||||
class LoggingResultSink : public ResultSink {
|
||||
public:
|
||||
explicit LoggingResultSink(std::ostream& output);
|
||||
bool publish(const FaceFeatureEvent& event) override;
|
||||
|
||||
private:
|
||||
std::ostream& output_;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
43
face_pipeline/include/face_pipeline/types.h
Normal file
43
face_pipeline/include/face_pipeline/types.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
using SteadyTime = std::chrono::steady_clock::time_point;
|
||||
|
||||
struct FramePacket {
|
||||
cv::Mat frame;
|
||||
std::uint64_t frame_id = 0;
|
||||
SteadyTime captured_at;
|
||||
std::int64_t captured_at_unix_ms = 0;
|
||||
};
|
||||
|
||||
struct FaceObservation {
|
||||
cv::Rect bbox;
|
||||
float score = 0.0f;
|
||||
std::array<cv::Point2f, 5> landmarks;
|
||||
};
|
||||
|
||||
struct TrackedFace {
|
||||
std::uint64_t track_id = 0;
|
||||
FaceObservation observation;
|
||||
bool is_new = false;
|
||||
};
|
||||
|
||||
struct FaceFeatureEvent {
|
||||
std::uint64_t track_id = 0;
|
||||
std::uint64_t frame_id = 0;
|
||||
std::int64_t captured_at_unix_ms = 0;
|
||||
FaceObservation observation;
|
||||
std::string embedding_model;
|
||||
std::vector<float> embedding;
|
||||
};
|
||||
|
||||
} // namespace face_pipeline
|
||||
41
face_pipeline/src/face_aligner.cpp
Normal file
41
face_pipeline/src/face_aligner.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "face_pipeline/face_aligner.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/calib3d.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
namespace face_pipeline {
|
||||
namespace {
|
||||
|
||||
const std::vector<cv::Point2f> kSFaceTemplate = {
|
||||
{38.2946f, 51.6963f}, {73.5318f, 51.5014f}, {56.0252f, 71.7366f},
|
||||
{41.5493f, 92.3655f}, {70.7299f, 92.2041f}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
cv::Size FaceAligner::outputSize() {
|
||||
return cv::Size(112, 112);
|
||||
}
|
||||
|
||||
bool FaceAligner::align(const cv::Mat& frame, const FaceObservation& face,
|
||||
cv::Mat& aligned) const {
|
||||
if (frame.empty() || face.bbox.width < 12 || face.bbox.height < 12) return false;
|
||||
|
||||
std::vector<cv::Point2f> source(face.landmarks.begin(), face.landmarks.end());
|
||||
for (const cv::Point2f& point : source) {
|
||||
if (!std::isfinite(point.x) || !std::isfinite(point.y)) return false;
|
||||
}
|
||||
|
||||
cv::Mat inliers;
|
||||
cv::Mat transform = cv::estimateAffinePartial2D(source, kSFaceTemplate, inliers, cv::LMEDS);
|
||||
if (transform.empty() || transform.rows != 2 || transform.cols != 3) return false;
|
||||
|
||||
cv::warpAffine(frame, aligned, transform, outputSize(), cv::INTER_LINEAR,
|
||||
cv::BORDER_CONSTANT, cv::Scalar());
|
||||
return !aligned.empty();
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
42
face_pipeline/src/face_detector.cpp
Normal file
42
face_pipeline/src/face_detector.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "face_pipeline/face_detector.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "facedetectcnn.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
FaceDetector::FaceDetector(float minimum_score)
|
||||
: minimum_score_(minimum_score), result_buffer_(FACEDETECTION_RESULT_BUFFER_SIZE) {}
|
||||
|
||||
std::vector<FaceObservation> FaceDetector::detect(const cv::Mat& frame) {
|
||||
std::vector<FaceObservation> observations;
|
||||
if (frame.empty() || frame.type() != CV_8UC3) return observations;
|
||||
|
||||
int* results = facedetect_cnn(result_buffer_.data(), frame.data, frame.cols, frame.rows,
|
||||
static_cast<int>(frame.step));
|
||||
if (!results) return observations;
|
||||
|
||||
observations.reserve(static_cast<std::size_t>(std::max(0, results[0])));
|
||||
for (int i = 0; i < results[0]; ++i) {
|
||||
short* values = reinterpret_cast<short*>(results + 1) +
|
||||
FACEDETECTION_RESULT_STRIDE_SHORTS * i;
|
||||
const float score = static_cast<float>(values[0]) / 100.0f;
|
||||
cv::Rect bbox(values[1], values[2], values[3], values[4]);
|
||||
bbox &= cv::Rect(0, 0, frame.cols, frame.rows);
|
||||
if (score <= minimum_score_ || bbox.width <= 0 || bbox.height <= 0) continue;
|
||||
|
||||
FaceObservation observation;
|
||||
observation.bbox = bbox;
|
||||
observation.score = score;
|
||||
for (std::size_t landmark = 0; landmark < observation.landmarks.size(); ++landmark) {
|
||||
observation.landmarks[landmark] = cv::Point2f(
|
||||
static_cast<float>(values[5 + landmark * 2]),
|
||||
static_cast<float>(values[6 + landmark * 2]));
|
||||
}
|
||||
observations.push_back(observation);
|
||||
}
|
||||
return observations;
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
59
face_pipeline/src/face_embedder.cpp
Normal file
59
face_pipeline/src/face_embedder.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "face_pipeline/face_embedder.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <opencv2/objdetect/face.hpp>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
class SFaceEmbedder::Impl {
|
||||
public:
|
||||
explicit Impl(const std::string& path)
|
||||
: recognizer(cv::FaceRecognizerSF::create(path, "")) {
|
||||
if (recognizer.empty()) throw std::runtime_error("failed to load SFace model: " + path);
|
||||
}
|
||||
|
||||
cv::Ptr<cv::FaceRecognizerSF> recognizer;
|
||||
};
|
||||
|
||||
SFaceEmbedder::SFaceEmbedder(const std::string& model_path)
|
||||
: impl_(new Impl(model_path)) {
|
||||
cv::Mat validation_input(112, 112, CV_8UC3, cv::Scalar(127, 127, 127));
|
||||
extract(validation_input);
|
||||
}
|
||||
|
||||
SFaceEmbedder::~SFaceEmbedder() = default;
|
||||
|
||||
std::vector<float> SFaceEmbedder::extract(const cv::Mat& aligned_face) {
|
||||
if (aligned_face.empty() || aligned_face.size() != cv::Size(112, 112) ||
|
||||
aligned_face.type() != CV_8UC3) {
|
||||
throw std::runtime_error("SFace input must be a 112x112 BGR image");
|
||||
}
|
||||
|
||||
cv::Mat feature;
|
||||
impl_->recognizer->feature(aligned_face, feature);
|
||||
cv::Mat flattened = feature.reshape(1, 1);
|
||||
if (flattened.type() != CV_32F || flattened.total() != 128) {
|
||||
throw std::runtime_error("SFace output must contain 128 float values");
|
||||
}
|
||||
|
||||
const float norm = static_cast<float>(cv::norm(flattened, cv::NORM_L2));
|
||||
if (!std::isfinite(norm) || norm <= 1e-12f) {
|
||||
throw std::runtime_error("SFace produced an invalid feature vector");
|
||||
}
|
||||
flattened /= norm;
|
||||
|
||||
const float* begin = flattened.ptr<float>();
|
||||
std::vector<float> embedding(begin, begin + flattened.total());
|
||||
for (float value : embedding) {
|
||||
if (!std::isfinite(value)) throw std::runtime_error("SFace produced non-finite values");
|
||||
}
|
||||
return embedding;
|
||||
}
|
||||
|
||||
std::string SFaceEmbedder::modelName() const {
|
||||
return "opencv_sface_2021dec";
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
144
face_pipeline/src/face_pipeline.cpp
Normal file
144
face_pipeline/src/face_pipeline.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "face_pipeline/face_pipeline.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
FacePipeline::FacePipeline(std::unique_ptr<FaceEmbedder> embedder,
|
||||
std::unique_ptr<ResultSink> sink)
|
||||
: embedder_(std::move(embedder)), sink_(std::move(sink)) {
|
||||
if (!embedder_ || !sink_) throw std::invalid_argument("pipeline dependencies are required");
|
||||
}
|
||||
|
||||
FacePipeline::~FacePipeline() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void FacePipeline::start() {
|
||||
std::lock_guard<std::mutex> lock(mailbox_mutex_);
|
||||
if (running_) return;
|
||||
running_ = true;
|
||||
worker_ = std::thread(&FacePipeline::workerLoop, this);
|
||||
}
|
||||
|
||||
void FacePipeline::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mailbox_mutex_);
|
||||
if (!running_) return;
|
||||
running_ = false;
|
||||
has_mail_ = false;
|
||||
}
|
||||
mailbox_cv_.notify_all();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
|
||||
bool FacePipeline::submit(const FramePacket& packet) {
|
||||
if (!motion_detector_.shouldProcess(packet.frame, packet.captured_at,
|
||||
has_active_tracks_.load())) return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mailbox_mutex_);
|
||||
if (!running_) return false;
|
||||
submitted_frames_.fetch_add(1);
|
||||
if (has_mail_) overwritten_frames_.fetch_add(1);
|
||||
mailbox_ = packet;
|
||||
mailbox_.frame = packet.frame.clone();
|
||||
has_mail_ = true;
|
||||
mailbox_cv_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<TrackedFace> FacePipeline::latestTracks() const {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
return latest_tracks_;
|
||||
}
|
||||
|
||||
void FacePipeline::workerLoop() {
|
||||
while (true) {
|
||||
FramePacket packet;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mailbox_mutex_);
|
||||
mailbox_cv_.wait(lock, [&] { return has_mail_ || !running_; });
|
||||
if (!running_) break;
|
||||
packet = mailbox_;
|
||||
has_mail_ = false;
|
||||
}
|
||||
process(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void FacePipeline::process(const FramePacket& packet) {
|
||||
try {
|
||||
++processed_frames_;
|
||||
const std::vector<FaceObservation> observations = face_detector_.detect(packet.frame);
|
||||
const std::vector<TrackedFace> tracked = tracker_.update(observations, packet.captured_at);
|
||||
has_active_tracks_.store(tracker_.hasActiveTracks(packet.captured_at));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
latest_tracks_ = tracked;
|
||||
const std::vector<std::uint64_t> active_ids = tracker_.activeTrackIds();
|
||||
const std::set<std::uint64_t> active(active_ids.begin(), active_ids.end());
|
||||
for (std::set<std::uint64_t>::iterator it = published_tracks_.begin();
|
||||
it != published_tracks_.end();) {
|
||||
if (active.count(*it) == 0) {
|
||||
it = published_tracks_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const TrackedFace& face : tracked) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
if (published_tracks_.count(face.track_id) != 0) continue;
|
||||
}
|
||||
|
||||
try {
|
||||
cv::Mat aligned;
|
||||
if (!face_aligner_.align(packet.frame, face.observation, aligned)) {
|
||||
++alignment_failures_;
|
||||
continue;
|
||||
}
|
||||
|
||||
FaceFeatureEvent event;
|
||||
event.track_id = face.track_id;
|
||||
event.frame_id = packet.frame_id;
|
||||
event.captured_at_unix_ms = packet.captured_at_unix_ms;
|
||||
event.observation = face.observation;
|
||||
event.embedding_model = embedder_->modelName();
|
||||
event.embedding = embedder_->extract(aligned);
|
||||
if (sink_->publish(event)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
published_tracks_.insert(face.track_id);
|
||||
} else {
|
||||
++publish_failures_;
|
||||
}
|
||||
} catch (const cv::Exception& error) {
|
||||
++embedding_failures_;
|
||||
std::cerr << "feature extraction failed for track " << face.track_id
|
||||
<< ": " << error.what() << std::endl;
|
||||
} catch (const std::exception& error) {
|
||||
++embedding_failures_;
|
||||
std::cerr << "feature extraction failed for track " << face.track_id
|
||||
<< ": " << error.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
if (processed_frames_ % 100 == 0) {
|
||||
std::clog << "face_pipeline_stats submitted=" << submitted_frames_.load()
|
||||
<< " overwritten=" << overwritten_frames_.load()
|
||||
<< " processed=" << processed_frames_
|
||||
<< " align_failed=" << alignment_failures_
|
||||
<< " embed_failed=" << embedding_failures_
|
||||
<< " publish_failed=" << publish_failures_ << std::endl;
|
||||
}
|
||||
} catch (const cv::Exception& error) {
|
||||
std::cerr << "face pipeline OpenCV error: " << error.what() << std::endl;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "face pipeline error: " << error.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
92
face_pipeline/src/iou_tracker.cpp
Normal file
92
face_pipeline/src/iou_tracker.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "face_pipeline/iou_tracker.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
IouTracker::IouTracker(const IouTrackerConfig& config) : config_(config) {}
|
||||
|
||||
float IouTracker::intersectionOverUnion(const cv::Rect& a, const cv::Rect& b) {
|
||||
const cv::Rect intersection = a & b;
|
||||
if (intersection.area() <= 0) return 0.0f;
|
||||
const float union_area = static_cast<float>(a.area() + b.area() - intersection.area());
|
||||
return union_area > 0.0f ? static_cast<float>(intersection.area()) / union_area : 0.0f;
|
||||
}
|
||||
|
||||
void IouTracker::expire(SteadyTime now) {
|
||||
tracks_.erase(std::remove_if(tracks_.begin(), tracks_.end(), [&](const Track& track) {
|
||||
return now - track.last_seen >= config_.track_ttl;
|
||||
}), tracks_.end());
|
||||
}
|
||||
|
||||
std::vector<TrackedFace> IouTracker::update(
|
||||
const std::vector<FaceObservation>& observations, SteadyTime now) {
|
||||
expire(now);
|
||||
|
||||
struct Candidate {
|
||||
std::size_t track;
|
||||
std::size_t observation;
|
||||
float iou;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
for (std::size_t track = 0; track < tracks_.size(); ++track) {
|
||||
for (std::size_t observation = 0; observation < observations.size(); ++observation) {
|
||||
const float iou = intersectionOverUnion(tracks_[track].observation.bbox,
|
||||
observations[observation].bbox);
|
||||
if (iou >= config_.match_threshold) candidates.push_back({track, observation, iou});
|
||||
}
|
||||
}
|
||||
std::sort(candidates.begin(), candidates.end(), [](const Candidate& left,
|
||||
const Candidate& right) {
|
||||
return left.iou > right.iou;
|
||||
});
|
||||
|
||||
std::vector<bool> track_used(tracks_.size(), false);
|
||||
std::vector<bool> observation_used(observations.size(), false);
|
||||
std::vector<TrackedFace> result;
|
||||
for (const Candidate& candidate : candidates) {
|
||||
if (track_used[candidate.track] || observation_used[candidate.observation]) continue;
|
||||
Track& track = tracks_[candidate.track];
|
||||
track.observation = observations[candidate.observation];
|
||||
track.last_seen = now;
|
||||
track_used[candidate.track] = true;
|
||||
observation_used[candidate.observation] = true;
|
||||
TrackedFace tracked_face;
|
||||
tracked_face.track_id = track.id;
|
||||
tracked_face.observation = track.observation;
|
||||
tracked_face.is_new = false;
|
||||
result.push_back(tracked_face);
|
||||
}
|
||||
|
||||
for (std::size_t observation = 0; observation < observations.size(); ++observation) {
|
||||
if (observation_used[observation]) continue;
|
||||
Track track{next_track_id_++, observations[observation], now};
|
||||
tracks_.push_back(track);
|
||||
TrackedFace tracked_face;
|
||||
tracked_face.track_id = track.id;
|
||||
tracked_face.observation = track.observation;
|
||||
tracked_face.is_new = true;
|
||||
result.push_back(tracked_face);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IouTracker::hasActiveTracks(SteadyTime now) const {
|
||||
for (const Track& track : tracks_) {
|
||||
if (now - track.last_seen < config_.track_ttl) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint64_t> IouTracker::activeTrackIds() const {
|
||||
std::vector<std::uint64_t> ids;
|
||||
ids.reserve(tracks_.size());
|
||||
for (const Track& track : tracks_) ids.push_back(track.id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::size_t IouTracker::size() const {
|
||||
return tracks_.size();
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
54
face_pipeline/src/motion_detector.cpp
Normal file
54
face_pipeline/src/motion_detector.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "face_pipeline/motion_detector.h"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
MotionDetector::MotionDetector(const MotionDetectorConfig& config) : config_(config) {}
|
||||
|
||||
bool MotionDetector::shouldProcess(const cv::Mat& frame, SteadyTime now,
|
||||
bool has_active_tracks) {
|
||||
if (frame.empty()) return false;
|
||||
|
||||
cv::Mat resized;
|
||||
cv::Mat gray;
|
||||
cv::resize(frame, resized, config_.analysis_size);
|
||||
cv::cvtColor(resized, gray, cv::COLOR_BGR2GRAY);
|
||||
cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0.0);
|
||||
|
||||
if (!initialized_) {
|
||||
gray.copyTo(previous_gray_);
|
||||
last_motion_ = now - config_.active_hold;
|
||||
last_request_ = now;
|
||||
initialized_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
cv::Mat difference;
|
||||
cv::Mat changed;
|
||||
cv::absdiff(previous_gray_, gray, difference);
|
||||
cv::threshold(difference, changed, config_.pixel_threshold, 255, cv::THRESH_BINARY);
|
||||
cv::morphologyEx(changed, changed, cv::MORPH_OPEN,
|
||||
cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)));
|
||||
gray.copyTo(previous_gray_);
|
||||
|
||||
const double changed_ratio = static_cast<double>(cv::countNonZero(changed)) /
|
||||
static_cast<double>(changed.total());
|
||||
if (changed_ratio >= config_.changed_ratio_threshold) last_motion_ = now;
|
||||
|
||||
const std::chrono::milliseconds scan_interval = has_active_tracks
|
||||
? config_.tracked_scan_interval : config_.idle_scan_interval;
|
||||
const bool motion_active = now - last_motion_ < config_.active_hold;
|
||||
const bool scan_due = now - last_request_ >= scan_interval;
|
||||
if (!motion_active && !scan_due) return false;
|
||||
|
||||
last_request_ = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MotionDetector::reset() {
|
||||
previous_gray_.release();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
19
face_pipeline/src/result_sink.cpp
Normal file
19
face_pipeline/src/result_sink.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "face_pipeline/result_sink.h"
|
||||
|
||||
namespace face_pipeline {
|
||||
|
||||
LoggingResultSink::LoggingResultSink(std::ostream& output) : output_(output) {}
|
||||
|
||||
bool LoggingResultSink::publish(const FaceFeatureEvent& event) {
|
||||
output_ << "face_feature track_id=" << event.track_id
|
||||
<< " frame_id=" << event.frame_id
|
||||
<< " captured_at_ms=" << event.captured_at_unix_ms
|
||||
<< " bbox=" << event.observation.bbox.x << ',' << event.observation.bbox.y
|
||||
<< ',' << event.observation.bbox.width << ',' << event.observation.bbox.height
|
||||
<< " score=" << event.observation.score
|
||||
<< " model=" << event.embedding_model
|
||||
<< " embedding_dim=" << event.embedding.size() << std::endl;
|
||||
return static_cast<bool>(output_);
|
||||
}
|
||||
|
||||
} // namespace face_pipeline
|
||||
Reference in New Issue
Block a user