Files
scene-actor-extraction/src/scrfd_decoder.hpp
T
dtourolle d753062c6c Initial commit: scene-actor-extraction pipeline
Source (KPN++ pipeline nodes, ArcFace embedders, SCRFD/YuNet detectors,
gallery builder), build scripts, and eval artifacts.

- external/KPN as a git submodule (gitea.tourolle.paris/dtourolle/KPN)
- ONNX models tracked via Git LFS (models/*.onnx)
- generated outputs, TensorRT engines, reference repos, and media ignored
2026-06-12 15:29:01 +02:00

231 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include "ort_provider.hpp"
#include "types.hpp"
#include <onnxruntime/onnxruntime_cxx_api.h>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
// ── SCRFDDecoder ──────────────────────────────────────────────────────────────
// ONNX Runtime-based decoder for InsightFace SCRFD face detector with kps.
// Uses ORT instead of cv::dnn because OpenCV 4.x cannot load SCRFD's dynamic
// Shape nodes. ORT handles dynamic shapes natively and is thread-safe for
// concurrent Run() calls.
//
// Model output layout (9 tensors, InsightFace export order):
// [0-2] score_s8 / score_s16 / score_s32 — flat (N,)
// [3-5] bbox_s8 / bbox_s16 / bbox_s32 — flat (N*4,) distance format
// [6-8] kps_s8 / kps_s16 / kps_s32 — flat (N*10,) distance format
//
// Landmark order (same as YuNet/ArcFace convention):
// [0] right-eye [1] left-eye [2] nose [3] right-mouth [4] left-mouth
struct SCRFDDecoder {
static constexpr int kInputW = 640;
static constexpr int kInputH = 640;
static constexpr int kAllStrides[4] = {8, 16, 32, 64};
static constexpr int kAnchors = 2;
SCRFDDecoder(const std::string& model_path,
float conf_threshold, float nms_threshold,
OrtProvider provider = OrtProvider::CPU,
TrtConfig trt_cfg = {})
: conf_threshold_(conf_threshold)
, nms_threshold_(nms_threshold)
{
Ort::SessionOptions opts;
opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
opts.SetIntraOpNumThreads(1);
// SCRFD ONNX has a dynamic H/W input; we letterbox to 640×640 at
// runtime, so pin the TRT profile to that single shape — otherwise
// TRT picks generic shapes and either rebuilds per-call or falls
// back to CUDA EP.
if (provider == OrtProvider::TensorRT) {
if (trt_cfg.input_name.empty()) {
Ort::SessionOptions probe_opts;
probe_opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_DISABLE_ALL);
Ort::Session probe(env_, model_path.c_str(), probe_opts);
Ort::AllocatorWithDefaultOptions alloc;
trt_cfg.input_name = probe.GetInputNameAllocated(0, alloc).get();
}
if (trt_cfg.profile_min.empty()) {
const std::string shape =
"1x3x" + std::to_string(kInputH) + "x" + std::to_string(kInputW);
trt_cfg.profile_min = shape;
trt_cfg.profile_opt = shape;
trt_cfg.profile_max = shape;
}
}
apply_ort_provider(opts, provider, "SCRFDDecoder", trt_cfg);
session_ = std::make_unique<Ort::Session>(env_, model_path.c_str(), opts);
Ort::AllocatorWithDefaultOptions alloc;
auto in_name = session_->GetInputNameAllocated(0, alloc);
input_name_ = in_name.get();
const size_t n_out = session_->GetOutputCount();
if (n_out % 3 != 0 || n_out < 9 || n_out > 12)
throw std::runtime_error(
"[SCRFDDecoder] expected 9 or 12 outputs (kps-variant model), got "
+ std::to_string(n_out));
fmc_ = static_cast<int>(n_out / 3);
for (size_t i = 0; i < n_out; ++i) {
auto name = session_->GetOutputNameAllocated(i, alloc);
out_name_storage_.emplace_back(name.get());
}
for (auto& s : out_name_storage_)
out_name_ptrs_.push_back(s.c_str());
// Reject non-SCRFD models (e.g. YuNet, which also has 12 outputs and so
// passes the count check above, but is encoded entirely differently).
// Cross-check by output channel count: SCRFD's three groups of fmc_
// outputs encode scores (1ch), bboxes (4ch) and 5-point kps (10ch).
// YuNet exports loc/conf/iou with 14/2/1 channels, so this trips.
const int expected_last[3] = {1, 4, 10};
for (size_t gi = 0; gi < 3; ++gi) {
for (int si = 0; si < fmc_; ++si) {
const size_t oi = gi * fmc_ + si;
auto shape = session_->GetOutputTypeInfo(oi)
.GetTensorTypeAndShapeInfo().GetShape();
if (shape.empty() || shape.back() != expected_last[gi]) {
throw std::runtime_error(
"[SCRFDDecoder] model does not look like InsightFace SCRFD: "
"output '" + out_name_storage_[oi] + "' last-dim is "
+ std::to_string(shape.empty() ? -1 : shape.back())
+ ", expected " + std::to_string(expected_last[gi])
+ ". Hint: pass scrfd_500m_bnkps.onnx, not yunet/*.onnx.");
}
}
}
std::cerr << "[SCRFDDecoder] loaded: " << model_path << "\n";
}
// Thread-safe: ORT Run() is safe for concurrent calls on the same Session.
std::vector<DetectedFace> detect(const cv::Mat& img) const {
// Letterbox to 640×640: uniform scale (preserves aspect ratio) + pad
// shorter side with constant grey. Stretching to 640×640 (the prior
// behaviour) distorts faces non-uniformly and degrades landmark
// localisation — matters most for portrait gallery images and 16:9
// video frames alike. Coordinates are mapped back via inverse scale +
// pad-offset below.
const float scale = std::min(static_cast<float>(kInputW) / img.cols,
static_cast<float>(kInputH) / img.rows);
const int new_w = static_cast<int>(std::round(img.cols * scale));
const int new_h = static_cast<int>(std::round(img.rows * scale));
const int pad_x = (kInputW - new_w) / 2;
const int pad_y = (kInputH - new_h) / 2;
cv::Mat resized;
cv::resize(img, resized, {new_w, new_h}, 0, 0, cv::INTER_LINEAR);
cv::Mat letterboxed(kInputH, kInputW, img.type(),
cv::Scalar(114, 114, 114));
resized.copyTo(letterboxed(cv::Rect(pad_x, pad_y, new_w, new_h)));
// BGR→RGB swap + normalize to [-1,1] → NCHW float32 blob
cv::Mat blob = cv::dnn::blobFromImage(
letterboxed, 1.0 / 128.0, {kInputW, kInputH},
cv::Scalar(127.5f, 127.5f, 127.5f),
/*swapRB=*/true, /*crop=*/false, CV_32F);
const std::array<int64_t, 4> in_shape = {1, 3, kInputH, kInputW};
auto mem = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
auto in_tensor = Ort::Value::CreateTensor<float>(
mem, blob.ptr<float>(), blob.total(),
in_shape.data(), in_shape.size());
const char* in_name_c = input_name_.c_str();
auto outs = session_->Run(
Ort::RunOptions{nullptr},
&in_name_c, &in_tensor, 1,
out_name_ptrs_.data(), out_name_ptrs_.size());
std::vector<cv::Rect2d> raw_boxes;
std::vector<float> raw_scores;
std::vector<std::array<cv::Point2f, 5>> raw_kps;
for (int si = 0; si < fmc_; ++si) {
const int stride = kAllStrides[si];
const int fh = kInputH / stride;
const int fw = kInputW / stride;
const float* s = outs[si].GetTensorData<float>();
const float* b = outs[fmc_ + si].GetTensorData<float>();
const float* k = outs[fmc_ * 2 + si].GetTensorData<float>();
for (int r = 0; r < fh; ++r) {
for (int c = 0; c < fw; ++c) {
for (int a = 0; a < kAnchors; ++a) {
const int idx = (r * fw + c) * kAnchors + a;
const float score = s[idx];
if (score < conf_threshold_) continue;
const float cx = static_cast<float>(c * stride);
const float cy = static_cast<float>(r * stride);
// Decode in letterboxed network space, then un-pad +
// un-scale to original image coordinates.
const auto to_img_x = [&](float v) { return (v - pad_x) / scale; };
const auto to_img_y = [&](float v) { return (v - pad_y) / scale; };
const float x1 = to_img_x(cx - b[idx*4+0] * stride);
const float y1 = to_img_y(cy - b[idx*4+1] * stride);
const float x2 = to_img_x(cx + b[idx*4+2] * stride);
const float y2 = to_img_y(cy + b[idx*4+3] * stride);
raw_boxes.push_back({(double)x1, (double)y1,
(double)(x2-x1), (double)(y2-y1)});
raw_scores.push_back(score);
std::array<cv::Point2f, 5> lms;
for (int p = 0; p < 5; ++p)
lms[p] = {to_img_x(cx + k[idx*10+p*2 ] * stride),
to_img_y(cy + k[idx*10+p*2+1] * stride)};
raw_kps.push_back(lms);
}
}
}
}
std::vector<int> keep;
cv::dnn::NMSBoxes(raw_boxes, raw_scores, conf_threshold_, nms_threshold_, keep);
const float img_w = static_cast<float>(img.cols);
const float img_h = static_cast<float>(img.rows);
std::vector<DetectedFace> faces;
faces.reserve(keep.size());
for (int i : keep) {
const auto& rb = raw_boxes[i];
DetectedFace f;
const float x = std::max(0.f, (float)rb.x);
const float y = std::max(0.f, (float)rb.y);
f.bbox = {x, y,
std::min((float)rb.width, img_w - x),
std::min((float)rb.height, img_h - y)};
f.confidence = raw_scores[i];
f.landmarks = raw_kps[i];
faces.push_back(f);
}
return faces;
}
private:
float conf_threshold_;
float nms_threshold_;
int fmc_{3};
Ort::Env env_{ORT_LOGGING_LEVEL_WARNING, "scrfd"};
std::unique_ptr<Ort::Session> session_;
std::string input_name_;
std::vector<std::string> out_name_storage_;
std::vector<const char*> out_name_ptrs_;
};