mirror of
https://github.com/opencv/opencv.git
synced 2026-07-25 13:23:02 +04:00
1b483ffea6
Block layout-based convolution in DNN #28585 merge together with https://github.com/opencv/opencv_extra/pull/1321 Some core parts of the new engine in DNN module have been revised substantially: 1. all tests seem to pass, except for `Test_Graph_Simplifier.ResizeSubgraph`, which has been disabled because it does not take the newly added `TransformLayoutLayer` into account. The test should be reworked perhaps. 1. convolution and related operations (maxpool/avgpool) now use so-called block layout (`DATA_LAYOUT_BLOCK`), where `NxCxHxW` tensors are represented as `NxC1xHxWxC0`, where `C1=(C + C0-1)/C0` and `C0` is a power-of-two (usually 4, 8, 16 or 32). 1. graph is now pre-processed and `TransformLayoutLayer` is inserted to convert data from NCHW or NHWC layout to the block layout or vice versa. The transformations are done in a lazy way only when they are really needed. For example, in the whole Resnet only 2 transformations are performed. 1. transformer-based models and other models that do not use convolutions will run as usual, without going to block layout. 1. there is yet another graph preprocessing stage added that embeds constant weights/scale and bias into convolution and batch norm layers. 1. 'batchnorm', 'activation' and 'adding a residual' are now fused with convolution, just like in the old engine. That brings some noticeable acceleration. 1. optimized convolution kernels have been added. * depthwise convolution, as well as maxpool and avgpool support C0=4, 8, 16 etc. _as long as_ C0 is divisible by the number of fp32 lanes in a SIMD register of the target platform (e.g. on ARM with NEON there must be `C0 % 4 == 0`, on x64 with AVX2 `C0 % 8 == 0`). * non-depthwise convolution only supports C0=8 for now. C0=8 seems to be a sweetspot for ARM with NEON, x64 with AVX2 or RISC-V with RVV (with 128- or 256-bit registers). For some platforms with dedicated matrix accelerators C0=16 or even C0=32 might be more efficient, but we could add the respective kernels later. * only fp32 kernels have been added. fp16/bf16 kernels might be added a little later. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable - [x] Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake
219 lines
7.5 KiB
C++
219 lines
7.5 KiB
C++
// This file is part of OpenCV project.
|
|
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
|
// of this distribution and at http://opencv.org/license.html.
|
|
|
|
#include "test_precomp.hpp"
|
|
|
|
namespace opencv_test { namespace {
|
|
|
|
// label format:
|
|
// image_name
|
|
// num_face
|
|
// face_1
|
|
// face_..
|
|
// face_num
|
|
std::map<std::string, Mat> blobFromTXT(const std::string& path, int numCoords)
|
|
{
|
|
std::ifstream ifs(path.c_str());
|
|
CV_Assert(ifs.is_open());
|
|
|
|
std::map<std::string, Mat> gt;
|
|
|
|
Mat faces;
|
|
int faceNum = -1;
|
|
int faceCount = 0;
|
|
for (std::string line, key; getline(ifs, line); )
|
|
{
|
|
std::istringstream iss(line);
|
|
if (line.find(".png") != std::string::npos)
|
|
{
|
|
// Get filename
|
|
iss >> key;
|
|
}
|
|
else if (line.find(" ") == std::string::npos)
|
|
{
|
|
// Get the number of faces
|
|
iss >> faceNum;
|
|
}
|
|
else
|
|
{
|
|
// Get faces
|
|
Mat face(1, numCoords, CV_32FC1);
|
|
for (int j = 0; j < numCoords; j++)
|
|
{
|
|
iss >> face.at<float>(0, j);
|
|
}
|
|
faces.push_back(face);
|
|
faceCount++;
|
|
}
|
|
|
|
if (faceCount == faceNum)
|
|
{
|
|
// Store faces
|
|
gt[key] = faces;
|
|
|
|
faces.release();
|
|
faceNum = -1;
|
|
faceCount = 0;
|
|
}
|
|
}
|
|
|
|
return gt;
|
|
}
|
|
|
|
TEST(Objdetect_face_detection, regression)
|
|
{
|
|
// Pre-set params
|
|
float scoreThreshold = 0.7f;
|
|
float matchThreshold = 0.7f;
|
|
float l2disThreshold = 15.0f;
|
|
int numLM = 5;
|
|
int numCoords = 4 + 2 * numLM;
|
|
|
|
// Load ground truth labels
|
|
std::map<std::string, Mat> gt = blobFromTXT(findDataFile("dnn_face/detection/cascades_labels.txt"), numCoords);
|
|
|
|
// Initialize detector
|
|
std::string model = findDataFile("dnn/onnx/models/yunet-202303.onnx", false);
|
|
Ptr<FaceDetectorYN> faceDetector = FaceDetectorYN::create(model, "", Size(300, 300));
|
|
faceDetector->setScoreThreshold(0.7f);
|
|
|
|
// Detect and match
|
|
for (auto item: gt)
|
|
{
|
|
std::string imagePath = findDataFile("cascadeandhog/images/" + item.first);
|
|
Mat image = imread(imagePath);
|
|
|
|
// Set input size
|
|
faceDetector->setInputSize(image.size());
|
|
|
|
// Run detection
|
|
Mat faces;
|
|
faceDetector->detect(image, faces);
|
|
// std::cout << item.first << " " << item.second.rows << " " << faces.rows << std::endl;
|
|
|
|
// Match bboxes and landmarks
|
|
std::vector<bool> matchedItem(item.second.rows, false);
|
|
for (int i = 0; i < faces.rows; i++)
|
|
{
|
|
if (faces.at<float>(i, numCoords) < scoreThreshold)
|
|
continue;
|
|
|
|
bool boxMatched = false;
|
|
std::vector<bool> lmMatched(numLM, false);
|
|
cv::Rect2f resBox(faces.at<float>(i, 0), faces.at<float>(i, 1), faces.at<float>(i, 2), faces.at<float>(i, 3));
|
|
for (int j = 0; j < item.second.rows && !boxMatched; j++)
|
|
{
|
|
if (matchedItem[j])
|
|
continue;
|
|
|
|
// Retrieve bbox and compare IoU
|
|
cv::Rect2f gtBox(item.second.at<float>(j, 0), item.second.at<float>(j, 1), item.second.at<float>(j, 2), item.second.at<float>(j, 3));
|
|
double interArea = (resBox & gtBox).area();
|
|
double iou = interArea / (resBox.area() + gtBox.area() - interArea);
|
|
if (iou >= matchThreshold)
|
|
{
|
|
boxMatched = true;
|
|
matchedItem[j] = true;
|
|
}
|
|
|
|
// Match landmarks if bbox is matched
|
|
if (!boxMatched)
|
|
continue;
|
|
for (int lmIdx = 0; lmIdx < numLM; lmIdx++)
|
|
{
|
|
float gtX = item.second.at<float>(j, 4 + 2 * lmIdx);
|
|
float gtY = item.second.at<float>(j, 4 + 2 * lmIdx + 1);
|
|
float resX = faces.at<float>(i, 4 + 2 * lmIdx);
|
|
float resY = faces.at<float>(i, 4 + 2 * lmIdx + 1);
|
|
float l2dis = cv::sqrt((gtX - resX) * (gtX - resX) + (gtY - resY) * (gtY - resY));
|
|
|
|
if (l2dis <= l2disThreshold)
|
|
{
|
|
lmMatched[lmIdx] = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
EXPECT_TRUE(boxMatched) << "In image " << item.first << ", cannot match resBox " << resBox << " with any ground truth.";
|
|
if (boxMatched)
|
|
{
|
|
EXPECT_TRUE(std::all_of(lmMatched.begin(), lmMatched.end(), [](bool v) { return v; })) << "In image " << item.first << ", resBox " << resBox << " matched but its landmarks failed to match.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(Objdetect_face_recognition, regression)
|
|
{
|
|
// Pre-set params
|
|
float score_thresh = 0.9f;
|
|
float nms_thresh = 0.3f;
|
|
double cosine_similar_thresh = 0.363;
|
|
double l2norm_similar_thresh = 1.128;
|
|
|
|
// Load ground truth labels
|
|
std::ifstream ifs(findDataFile("dnn_face/recognition/cascades_label.txt").c_str());
|
|
CV_Assert(ifs.is_open());
|
|
|
|
std::set<std::string> fSet;
|
|
std::map<std::string, Mat> featureMap;
|
|
std::map<std::pair<std::string, std::string>, int> gtMap;
|
|
|
|
|
|
for (std::string line, key; getline(ifs, line);)
|
|
{
|
|
std::string fname1, fname2;
|
|
int label;
|
|
std::istringstream iss(line);
|
|
iss>>fname1>>fname2>>label;
|
|
// std::cout<<fname1<<" "<<fname2<<" "<<label<<std::endl;
|
|
|
|
fSet.insert(fname1);
|
|
fSet.insert(fname2);
|
|
gtMap[std::make_pair(fname1, fname2)] = label;
|
|
}
|
|
|
|
// Initialize detector
|
|
std::string detect_model = findDataFile("dnn/onnx/models/yunet-202303.onnx", false);
|
|
Ptr<FaceDetectorYN> faceDetector = FaceDetectorYN::create(detect_model, "", Size(150, 150), score_thresh, nms_thresh);
|
|
|
|
std::string recog_model = findDataFile("dnn/onnx/models/face_recognizer_fast.onnx", false);
|
|
Ptr<FaceRecognizerSF> faceRecognizer = FaceRecognizerSF::create(recog_model, "");
|
|
|
|
// Detect and match
|
|
for (auto fname: fSet)
|
|
{
|
|
std::string imagePath = findDataFile("dnn_face/recognition/" + fname);
|
|
Mat image = imread(imagePath);
|
|
|
|
Mat faces;
|
|
faceDetector->detect(image, faces);
|
|
|
|
ASSERT_EQ(faces.rows, 1);
|
|
|
|
Mat aligned_face;
|
|
faceRecognizer->alignCrop(image, faces.row(0), aligned_face);
|
|
|
|
Mat feature;
|
|
faceRecognizer->feature(aligned_face, feature);
|
|
|
|
featureMap[fname] = feature.clone();
|
|
}
|
|
|
|
for (auto item: gtMap)
|
|
{
|
|
Mat feature1 = featureMap[item.first.first];
|
|
Mat feature2 = featureMap[item.first.second];
|
|
int label = item.second;
|
|
|
|
double cos_score = faceRecognizer->match(feature1, feature2, FaceRecognizerSF::DisType::FR_COSINE);
|
|
double L2_score = faceRecognizer->match(feature1, feature2, FaceRecognizerSF::DisType::FR_NORM_L2);
|
|
|
|
EXPECT_TRUE(label == 0 ? cos_score <= cosine_similar_thresh : cos_score > cosine_similar_thresh) << "Cosine match result of images " << item.first.first << " and " << item.first.second << " is different from ground truth (score: "<< cos_score <<";Thresh: "<< cosine_similar_thresh <<").";
|
|
EXPECT_TRUE(label == 0 ? L2_score > l2norm_similar_thresh : L2_score <= l2norm_similar_thresh) << "L2norm match result of images " << item.first.first << " and " << item.first.second << " is different from ground truth (score: "<< L2_score <<";Thresh: "<< l2norm_similar_thresh <<").";
|
|
}
|
|
}
|
|
|
|
}} // namespace
|