// Copyright 2025 heabeounMKTO // SPDX-License-Identifier: BSD-3-Clause /* ncnn example using yolo-face and arcface to extract embeddings from a face * * * the arcface model is converted from * https://github.com/onnx/models/tree/main/validated/vision/body_analysis/arcface * 1. first simplify the arcface.onnx using onnxsim * 2. then convert it using ncnn's onnx exporter onnx2ncnn * using pnnx to convert would cause -nan output! * * the yolov8-face model is converted from * https://github.com/derronqi/yolov8-face * * * you can find the models preconverted at * https://drive.google.com/drive/folders/1P0RDzj9V7FHEL8w_-yqls5RHeVpO-2PS?usp=sharing * * */ #if defined(USE_NCNN_SIMPLEOCV) #include "simpleocv.h" #else #include #include #include #endif #include #include #include #include "layer.h" #include "net.h" #include "mat.h" #ifndef ARCFACE_EXAMPLE_YOLO_INFER_SIZE #define ARCFACE_EXAMPLE_YOLO_INFER_SIZE 320 #endif struct Bbox { float x1, y1, x2, y2, confidence; int label; Bbox() : x1(0.0f), y1(0.0f), x2(0.0f), y2(0.0f), confidence(0.0f), label(0) { } Bbox(float x1, float y1, float x2, float y2, float confidence, int label = 0, std::string label_name = "") : x1(x1), y1(y1), x2(x2), y2(y2), confidence(confidence), label(label) { } Bbox apply_image_scale(const cv::Mat& original_image, const float scale_factor, const int pad_w, const int pad_h) { int img_w = original_image.cols; int img_h = original_image.rows; x1 = (x1 - pad_w) / scale_factor; y1 = (y1 - pad_h) / scale_factor; x2 = (x2 - pad_w) / scale_factor; y2 = (y2 - pad_h) / scale_factor; // clamp x1 = std::max(0.0f, std::min(x1, (float)img_w)); y1 = std::max(0.0f, std::min(y1, (float)img_h)); x2 = std::max(0.0f, std::min(x2, (float)img_w)); y2 = std::max(0.0f, std::min(y2, (float)img_h)); return Bbox(x1, y1, x2, y2, confidence, label); } std::string get_label_name(const std::vector& classes) { return classes[this->label]; } /// what more do you need to know vro float area() const { float width = x2 - x1; float height = y2 - y1; return width * height; } cv::Mat crop_bbox(const cv::Mat& originalImage) const { // Calculate width and height int bbox_width = static_cast(x2 - x1); int bbox_height = static_cast(y2 - y1); // Ensure valid dimensions if (bbox_width <= 0 || bbox_height <= 0) { fprintf(stderr, "Invalid bounding box dimensions\n"); return cv::Mat(); } // Ensure coordinates are within image bounds int x1_int = static_cast(x1); int y1_int = static_cast(y1); int x2_int = static_cast(x2); int y2_int = static_cast(y2); // Clamp to image bounds x1_int = std::max(0, x1_int); y1_int = std::max(0, y1_int); x2_int = std::min(originalImage.cols, x2_int); y2_int = std::min(originalImage.rows, y2_int); // Create ROI and return cropped image cv::Rect roi(x1_int, y1_int, x2_int - x1_int, y2_int - y1_int); return originalImage(roi).clone(); } cv::Rect_ get_rect() const { int x1_int = static_cast(x1); int y1_int = static_cast(y1); int width = static_cast(x2 - x1); int height = static_cast(y2 - y1); // Ensure valid dimensions if (width <= 0 || height <= 0) { return cv::Rect(0, 0, 0, 0); // Return invalid rect } return cv::Rect(x1_int, y1_int, width, height); } }; static void print_bbox(Bbox& bbox) { printf("Bbox(x1=%.2f, y1=%.2f, x2=%.2f, y2=%.2f, conf=%.4f, label=%d)\n", bbox.x1, bbox.y1, bbox.x2, bbox.y2, bbox.confidence, bbox.label); } static void qsort_descent_inplace(std::vector& faceobjects, int left, int right) { int i = left; int j = right; float p = faceobjects[(left + right) / 2].confidence; while (i <= j) { while (faceobjects[i].confidence > p) i++; while (faceobjects[j].confidence < p) j--; if (i <= j) { // swap std::swap(faceobjects[i], faceobjects[j]); i++; j--; } } // #pragma omp parallel sections { // #pragma omp section { if (left < j) qsort_descent_inplace(faceobjects, left, j); } // #pragma omp section { if (i < right) qsort_descent_inplace(faceobjects, i, right); } } } static void qsort_descent_inplace(std::vector& faceobjects) { if (faceobjects.empty()) return; qsort_descent_inplace(faceobjects, 0, faceobjects.size() - 1); } float calculate_iou(const Bbox& box1, const Bbox& box2) { float x1 = std::max(box1.x1, box2.x1); float y1 = std::max(box1.y1, box2.y1); float x2 = std::min(box1.x2, box2.x2); float y2 = std::min(box1.y2, box2.y2); if (x2 <= x1 || y2 <= y1) { return 0.0f; // no intersect } float intersection_area = (x2 - x1) * (y2 - y1); float box1_area = (box1.x2 - box1.x1) * (box1.y2 - box1.y1); float box2_area = (box2.x2 - box2.x1) * (box2.y2 - box2.y1); float union_area = box1_area + box2_area - intersection_area; return intersection_area / union_area; } static std::vector non_maximum_supression(const std::vector& bbox, float iou_thresh, bool class_agnostic = false) { std::vector picked; const int n = bbox.size(); if (n == 0) return picked; std::vector areas(n); for (int i = 0; i < n; i++) { areas[i] = bbox[i].area(); } for (int i = 0; i < n; i++) { const Bbox& a = bbox[i]; bool keep = true; for (int j : picked) { const Bbox& b = bbox[j]; // Enhanced class comparison logic using labels if (!class_agnostic) { if (a.label != b.label) { continue; // Different classes, don't suppress } } float iou = calculate_iou(a, b); if (iou > iou_thresh) { keep = false; break; } } if (keep) { picked.push_back(i); } } return picked; } static std::vector scale_wh(float w0, float h0, float w1, float h1) { float r = std::min(w1 / w0, h1 / h0); std::vector _scale_factor(3); _scale_factor[0] = r; _scale_factor[1] = (float)std::round(w0 * r); _scale_factor[2] = (float)std::round(h0 * r); return _scale_factor; } struct ImagePreProcessResults { ncnn::Mat result; float img_scale, pad_w, pad_h; ImagePreProcessResults(ncnn::Mat result, float img_scale, float pad_w, float pad_h) : result(result), img_scale(img_scale), pad_w(pad_w), pad_h(pad_h) { } }; struct DetectionResult { std::vector bboxes; std::vector > keypoints; }; static ImagePreProcessResults preprocess_yolo_kpts(cv::Mat& input_image, int infer_size) noexcept { float mean_vals[] = {0.f, 0.f, 0.f}; float norm_vals[] = {1 / 255.f, 1 / 255.f, 1 / 255.f}; int img_w = input_image.cols; int img_h = input_image.rows; float scale_factor, new_w, new_h; std::vector _scale_factor = scale_wh(img_w, img_h, (float)infer_size, (float)infer_size); scale_factor = _scale_factor[0]; new_w = _scale_factor[1]; new_h = _scale_factor[2]; ncnn::Mat in = ncnn::Mat::from_pixels_resize(input_image.data, ncnn::Mat::PIXEL_BGR2RGB, img_w, img_h, new_w, new_h); // padding calculation int pad_w = (infer_size - new_w) / 2; int pad_h = (infer_size - new_h) / 2; ncnn::Mat in_pad; ncnn::copy_make_border(in, in_pad, pad_h, infer_size - new_h - pad_h, pad_w, infer_size - new_w - pad_w, ncnn::BORDER_CONSTANT, 114.f); in_pad.substract_mean_normalize(mean_vals, norm_vals); return ImagePreProcessResults(in_pad, scale_factor, pad_w, pad_h); } /// parses extra keypoints data for face mmodel /// the format is this: /// [x, y, w, h, conf, class_scores..., kp1_conf, kp1_x, kp1_y, kp2_conf, kp2_x, kp2_y, ...] static DetectionResult parse_yolo_keypoints_results(ncnn::Mat& result, cv::Mat& original_image, ImagePreProcessResults& preproc_img, float confidence_threshold, float iou_threshold, std::vector class_names) { cv::Mat output((int)result.w, (int)result.h, CV_32FC1); for (int i = 0; i < output.cols; i++) { for (int j = 0; j < output.rows; j++) { output.ptr(j)[i] = result.row(i)[j]; } } std::vector detections; std::vector > all_keypoints; int num_classes = class_names.size(); int kp_stride = 3; int num_keypoints = 5; for (int i = 0; i < output.rows; i++) { const float* row_ptr = output.ptr(i); const float* bboxes_ptr = row_ptr; const float* classes_ptr = row_ptr + 4; const float* max_s_ptr = std::max_element(classes_ptr, classes_ptr + num_classes); float score = *max_s_ptr; int class_id = max_s_ptr - classes_ptr; if (score >= confidence_threshold) { float x = bboxes_ptr[0]; float y = bboxes_ptr[1]; float w = bboxes_ptr[2]; float h = bboxes_ptr[3]; float x1 = x - w / 2.0f; float y1 = y - h / 2.0f; float x2 = x + w / 2.0f; float y2 = y + h / 2.0f; if (x2 > x1 && y2 > y1) { Bbox bbox = Bbox(x1, y1, x2, y2, score, class_id) .apply_image_scale(original_image, preproc_img.img_scale, preproc_img.pad_w, preproc_img.pad_h); // Parse exactly 5 keypoints for this face model std::vector face_keypoints; face_keypoints.reserve(15); const float* kp_ptr = row_ptr + 4 + num_classes; float scale = 1.0f / preproc_img.img_scale; for (int k = 0; k < num_keypoints; k++) { float kp_x = kp_ptr[k * kp_stride]; float kp_y = kp_ptr[k * kp_stride + 1]; float kp_conf_raw = kp_ptr[k * kp_stride + 2]; // Apply sigmoid to convert logit to probability float kp_conf = 1.0f / (1.0f + expf(-kp_conf_raw)); // Scale keypoints to original kp_x = (kp_x - preproc_img.pad_w) * scale; kp_y = (kp_y - preproc_img.pad_h) * scale; face_keypoints.push_back(kp_x); face_keypoints.push_back(kp_y); face_keypoints.push_back(kp_conf); } detections.push_back(bbox); all_keypoints.push_back(face_keypoints); } } } // nms qsort_descent_inplace(detections); std::vector picked = non_maximum_supression(detections, iou_threshold, false); DetectionResult res; for (size_t i = 0; i < picked.size(); i++) { int idx = picked[i]; res.bboxes.push_back(detections[idx]); res.keypoints.push_back(all_keypoints[idx]); } return res; } static inline float get_similarity(std::vector f1, std::vector f2) { float sim = 0.0; for (size_t i = 0; i < f1.size(); i++) { sim += f1[i] * f2[i]; } return sim; } // these are converted from here // https://github.com/deepinsight/insightface/blob/master/python-package/insightface/utils/face_align.py static int estimate_norm(float* transform_matrix, const float* lmk, int image_size = 112) { float ARCFACE_DST[] { 38.2946f, 51.6963f, // left eye 73.5318f, 51.5014f, // right eye 56.0252f, 71.7366f, // nose 41.5493f, 92.3655f, // left mouth 70.7299f, 92.2041f // right mouth }; if (image_size % 112 != 0 && image_size % 128 != 0) { return -1; } float ratio, diff_x; if (image_size % 112 == 0) { ratio = static_cast(image_size) / 112.0f; diff_x = 0.0f; } else { ratio = static_cast(image_size) / 128.0f; diff_x = 8.0f * ratio; } float src_points[10]; for (int i = 0; i < 5; i++) { src_points[i * 2] = lmk[i * 3]; src_points[i * 2 + 1] = lmk[i * 3 + 1]; } float dst_points[10]; for (int i = 0; i < 5; i++) { dst_points[i * 2] = ARCFACE_DST[i * 2] * ratio + diff_x; dst_points[i * 2 + 1] = ARCFACE_DST[i * 2 + 1] * ratio; } ncnn::get_affine_transform(dst_points, src_points, 5, transform_matrix); return 0; } static int norm_crop(cv::Mat& output, const cv::Mat& input, const float* lmk, int image_size = 112) { float transform_matrix[6]; int status = estimate_norm(transform_matrix, lmk, image_size); if (status != 0) { return status; } output = cv::Mat(image_size, image_size, CV_8UC3); ncnn::warpaffine_bilinear_c3(input.data, input.cols, input.rows, output.data, image_size, image_size, transform_matrix); return 0; } void normalize_arcface(std::vector& feature) { if (feature.empty()) return; float sum = 0; for (auto it = feature.begin(); it != feature.end(); it++) sum += (float)*it * (float)*it; sum = sqrt(sum); if (sum == 0.0f) return; for (auto it = feature.begin(); it != feature.end(); it++) *it /= sum; } static int get_face(const cv::Mat& rgb, DetectionResult& result) { int status = 0; ncnn::Net yoloface; yoloface.opt.use_vulkan_compute = true; status = yoloface.load_param("yolov8-face.param"); if (status != 0) { fprintf(stderr, "couldn't load params"); return status; } status = yoloface.load_model("yolov8-face.bin"); if (status != 0) { fprintf(stderr, "couldn't load model"); return status; } cv::Mat input_image = rgb.clone(); ImagePreProcessResults preproc_img = preprocess_yolo_kpts(input_image, ARCFACE_EXAMPLE_YOLO_INFER_SIZE); ncnn::Extractor ex = yoloface.create_extractor(); ex.input("in0", preproc_img.result); ncnn::Mat out; ex.extract("out0", out); std::vector class_names = {"face"}; result = parse_yolo_keypoints_results(out, input_image, preproc_img, 0.5, 0.4, class_names); if (result.bboxes.size() < 1) { fprintf(stderr, "no faces are found!"); return -1; } return 0; } static int get_embedding(const cv::Mat& rgb, std::vector& result) { ncnn::Net arcface; arcface.opt.use_vulkan_compute = true; int status = arcface.load_param("arcfaceresnet.param"); if (status != 0) { fprintf(stderr, "couldn't load arcface params"); return status; } status = arcface.load_model("arcfaceresnet.bin"); if (status != 0) { fprintf(stderr, "couldn't load arcface model"); return status; } if (rgb.empty() || rgb.type() != CV_8UC3) { fprintf(stderr, "invalid input image!"); return -1; } /* * the arcface model provided in the link has builtin normalization layers, * no need to run substract_mean_normalize * * reference from .param BinaryOp _minusscalar0 2 1 data scalar_op2 _minusscalar0 0=1 BinaryOp _mulscalar0 2 1 _minusscalar0 scalar_op3 _mulscalar0 0=2 * */ ncnn::Mat in = ncnn::Mat::from_pixels_resize( rgb.data, ncnn::Mat::PIXEL_BGR2RGB, rgb.cols, rgb.rows, 112, 112); ncnn::Extractor ex = arcface.create_extractor(); ex.input("data", in); ncnn::Mat out; ex.extract("fc1", out); const float* ptr = (const float*)out.data; for (int i = 0; i < 512; i++) { result[i] = ptr[i]; } normalize_arcface(result); return 0; } int main(int argc, char** argv) { if (argc != 3) { fprintf(stderr, "Usage: %s \n", argv[0]); return -1; } const char* face1_path = argv[1]; const char* face2_path = argv[2]; int status = 0; cv::Mat face_img1 = cv::imread(face1_path); cv::Mat face_img2 = cv::imread(face2_path); if (face_img1.empty()) { fprintf(stderr, "Failed to load image: %s\n", face1_path); return -1; } if (face_img2.empty()) { fprintf(stderr, "Failed to load image: %s\n", face2_path); return -1; } cv::Mat input_embed1, input_embed2; DetectionResult res1, res2; std::vector embedding1(512), embedding2(512); status = get_face(face_img1, res1); if (status != 0) { fprintf(stderr, "get face failed for %s!\n", face1_path); return -1; } fprintf(stdout, "found faces in face1: %d\n", (int)res1.bboxes.size()); for (size_t i = 0; i < res1.bboxes.size(); i++) { print_bbox(res1.bboxes[i]); } status = get_face(face_img2, res2); if (status != 0) { fprintf(stderr, "get face failed for %s!\n", face2_path); return -1; } fprintf(stdout, "found faces in face2: %d\n", (int)res2.bboxes.size()); for (size_t i = 0; i < res2.bboxes.size(); i++) { print_bbox(res2.bboxes[i]); } status = norm_crop(input_embed1, face_img1, res1.keypoints[0].data()); status = get_embedding(input_embed1, embedding1); if (status != 0) { fprintf(stderr, "get embedding failed for %s!\n", face1_path); return -1; } status = norm_crop(input_embed2, face_img2, res2.keypoints[0].data()); if (status != 0) { fprintf(stderr, "norm_crop failed for face2!\n"); return -1; } status = get_embedding(input_embed2, embedding2); if (status != 0) { fprintf(stderr, "get embedding failed for face2!\n"); return -1; } if (status != 0) { fprintf(stderr, "get embedding failed for %s!\n", face2_path); return -1; } float similarity = get_similarity(embedding1, embedding2); fprintf(stdout, "Similarity: %f\n", similarity); }