// 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" #include "npy_blob.hpp" #ifdef HAVE_OPENCV_DNN #include "opencv2/dnn.hpp" #include "opencv2/core/utils/configuration.private.hpp" namespace opencv_test { namespace { static void skipIfClassicDnnEngine() { const auto engine = static_cast( cv::utils::getConfigurationParameterSizeT("OPENCV_FORCE_DNN_ENGINE", cv::dnn::ENGINE_AUTO)); if (engine == cv::dnn::ENGINE_CLASSIC) throw SkipTestException("ALIKED/LightGlue reference outputs are generated with the new DNN engine"); } TEST(Features2d_ALIKED, Regression) { skipIfClassicDnnEngine(); const std::string modelPath = cvtest::findDataFile("dnn/onnx/models/aliked-n16rot-top1k-640.onnx", false); Ptr aliked = ALIKED::create(modelPath); ASSERT_FALSE(aliked.empty()); ASSERT_FALSE(aliked->empty()); EXPECT_EQ(aliked->descriptorSize(), 128); EXPECT_EQ(aliked->descriptorType(), CV_32F); EXPECT_EQ(aliked->defaultNorm(), NORM_L2); const std::string imgPath = cvtest::findDataFile("shared/box.png"); Mat img = imread(imgPath); ASSERT_FALSE(img.empty()) << "Could not load test image: " << imgPath; std::vector keypoints; Mat descriptors; aliked->detectAndCompute(img, noArray(), keypoints, descriptors); EXPECT_GT(keypoints.size(), 100u); ASSERT_EQ(descriptors.rows, static_cast(keypoints.size())); EXPECT_EQ(descriptors.cols, 128); EXPECT_EQ(descriptors.type(), CV_32F); for (const KeyPoint& kp : keypoints) { EXPECT_GE(kp.pt.x, 0.f); EXPECT_GE(kp.pt.y, 0.f); EXPECT_LT(kp.pt.x, static_cast(img.cols)); EXPECT_LT(kp.pt.y, static_cast(img.rows)); EXPECT_GT(kp.response, 0.f); } // Load ORT reference outputs (generated with same OpenCV preprocessing) Mat refKpts = blobFromNPY(cvtest::findDataFile("dnn/aliked_keypoints_box.npy")); Mat refDescs = blobFromNPY(cvtest::findDataFile("dnn/aliked_descriptors_box.npy")); // Keypoint count must match exactly ASSERT_EQ(static_cast(keypoints.size()), refKpts.rows) << "Keypoint count mismatch: got " << keypoints.size() << ", expected " << refKpts.rows; // Compare each keypoint (normalized coords -> pixel coords) const float origW = static_cast(img.cols); const float origH = static_cast(img.rows); for (int i = 0; i < refKpts.rows; i++) { float refX = (refKpts.at(i, 0) + 1.0f) * 0.5f * origW; float refY = (refKpts.at(i, 1) + 1.0f) * 0.5f * origH; EXPECT_NEAR(keypoints[i].pt.x, refX, 1e-4) << "Keypoint " << i << " x mismatch"; EXPECT_NEAR(keypoints[i].pt.y, refY, 1e-4) << "Keypoint " << i << " y mismatch"; } // Compare descriptors row by row for (int i = 0; i < refDescs.rows; i++) { Mat diff = descriptors.row(i) - refDescs.row(i); double maxDiff = cv::norm(diff, cv::NORM_INF); EXPECT_LT(maxDiff, 1e-5) << "Descriptor " << i << " mismatch (max diff=" << maxDiff << ")"; } } TEST(Features2d_LightGlue, Regression) { skipIfClassicDnnEngine(); const std::string alikedPath = cvtest::findDataFile("dnn/onnx/models/aliked-n16rot-top1k-640.onnx", false); const std::string lgPath = cvtest::findDataFile("dnn/onnx/models/aliked_lightglue.onnx", false); Ptr aliked = ALIKED::create(alikedPath); Ptr lg = LightGlueMatcher::create(lgPath); ASSERT_FALSE(aliked.empty()); ASSERT_FALSE(lg.empty()); Mat img1 = imread(cvtest::findDataFile("shared/box.png")); Mat img2 = imread(cvtest::findDataFile("shared/box_in_scene.png")); ASSERT_FALSE(img1.empty()); ASSERT_FALSE(img2.empty()); // Detect features on both images std::vector kpts1, kpts2; Mat descs1, descs2; aliked->detectAndCompute(img1, noArray(), kpts1, descs1); aliked->detectAndCompute(img2, noArray(), kpts2, descs2); ASSERT_GT(static_cast(kpts1.size()), 0); ASSERT_GT(static_cast(kpts2.size()), 0); // Build keypoint matrices (pixel coordinates) Mat kpts1Mat(static_cast(kpts1.size()), 2, CV_32F); Mat kpts2Mat(static_cast(kpts2.size()), 2, CV_32F); for (size_t i = 0; i < kpts1.size(); i++) { kpts1Mat.at(static_cast(i), 0) = kpts1[i].pt.x; kpts1Mat.at(static_cast(i), 1) = kpts1[i].pt.y; } for (size_t i = 0; i < kpts2.size(); i++) { kpts2Mat.at(static_cast(i), 0) = kpts2[i].pt.x; kpts2Mat.at(static_cast(i), 1) = kpts2[i].pt.y; } lg->setPairInfo(kpts1Mat, kpts2Mat, img1.size(), img2.size()); std::vector matches; lg->match(descs1, descs2, matches); ASSERT_GT(static_cast(matches.size()), 0); for (const auto& m : matches) { EXPECT_GE(m.queryIdx, 0); EXPECT_LT(m.queryIdx, static_cast(kpts1.size())); EXPECT_GE(m.trainIdx, 0); EXPECT_LT(m.trainIdx, static_cast(kpts2.size())); } // Load ORT reference outputs Mat refMatches = blobFromNPY(cvtest::findDataFile("dnn/lightglue_matches.npy")); // Match count must match exactly (same OpenCV preprocessing in both) ASSERT_EQ(static_cast(matches.size()), refMatches.rows) << "Match count mismatch: got " << matches.size() << ", expected " << refMatches.rows; // Compare each match (index pairs should be identical) for (int i = 0; i < refMatches.rows; i++) { int refQIdx = static_cast(refMatches.at(i, 0)); int refTIdx = static_cast(refMatches.at(i, 1)); EXPECT_EQ(matches[i].queryIdx, refQIdx) << "Match " << i << " queryIdx mismatch"; EXPECT_EQ(matches[i].trainIdx, refTIdx) << "Match " << i << " trainIdx mismatch"; } } #else // !HAVE_OPENCV_DNN TEST(Features2d_ALIKED, not_available) { EXPECT_THROW(ALIKED::create("dummy.onnx"), cv::Exception); } TEST(Features2d_LightGlueMatcher, not_available) { EXPECT_THROW(LightGlueMatcher::create("dummy.onnx"), cv::Exception); } #endif // HAVE_OPENCV_DNN }} // namespace opencv_test