add face feature extraction pipeline

This commit is contained in:
2026-07-11 21:36:14 +08:00
parent d528379961
commit 4f82e2cdaa
28 changed files with 1270 additions and 95 deletions

View 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()

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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