1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-25 21:33:04 +04:00
Files
opencv/modules/features/test/test_aliked_lightglue.cpp
Yang Guanyuhan 527f01449d Merge pull request #28986 from YangGuanyuhan:ai-aliked-lightglue-pipeline
[GSOC] feat: Add ALIKED feature extractor and LightGlue matcher with DNN integration #28986

## PR Description

### Summary

Integrate ALIKED and LightGlue into OpenCV's `features` module as native`Feature2D` and `DescriptorMatcher` implementations, enabling end-to-end neural feature matching within OpenCV's ecosystem.

---

### What's included

#### New classes

- **`cv::ALIKED`** extends `Feature2D`
  - CNN-based keypoint detection
  - 128-D descriptor extraction via ONNX Runtime

- **`cv::LightGlueMatcher`** extends `DescriptorMatcher`
  - Deep feature matching with spatial context
  - Uses keypoints and image sizes during matching

---

#### API design

- Standard OpenCV patterns:
  - `detectAndCompute()`
  - `match()`
  - `knnMatch()`

- Multiple factory methods:
  - ONNX model path
  - In-memory model buffer
  - Pre-loaded `dnn::Net`

- `Params` structs use `CV_EXPORTS_W_SIMPLE`
  for Python/Java bindings support

- Optional DNN dependency:
  - `HAVE_OPENCV_DNN` guards
  - Stub implementations throw `StsNotImplemented`

---

### Files added

| File | Description |
|------|-------------|
| `src/feature2d_aliked.cpp` | ALIKED implementation |
| `src/matchers_lightglue.cpp` | LightGlueMatcher implementation |
| `src/aliked_context.hpp` | Shared internal context struct |
| `test/test_aliked_lightglue.cpp` | Unit tests (9 test cases) |
| `samples/cpp/example_features_aliked_lightglue.cpp` | Demo application |

---

### Files modified

- `CMakeLists.txt`
  - Add `opencv_dnn` as optional dependency

- `features.hpp`
  - Add ALIKED and LightGlueMatcher declarations

- `precomp.hpp`
  - Add DNN include guard

---

### Usage

```cpp
// Feature extraction
Ptr<ALIKED> aliked =
    ALIKED::create("aliked-n16rot-top1k-640.onnx");

vector<KeyPoint> kpts;
Mat descs;

aliked->detectAndCompute(image, Mat(), kpts, descs);

// Feature matching
Ptr<LightGlueMatcher> lg =
    LightGlueMatcher::create("aliked_lightglue.onnx");

lg->setPairInfo(
    kpts1Mat,
    kpts2Mat,
    img1.size(),
    img2.size()
);

vector<DMatch> matches;
lg->match(descs1, descs2, matches);
````
please refer to samples/cpp/example_features_aliked_lightglue.cpp

---

### Test plan

* Build with `BUILD_LIST=features,dnn`
* Build without DNN:

  * Verify stubs compile
  * Verify `StsNotImplemented` is thrown
* Run:

  * `ctest -R Features2d_ALIKED`
  * `ctest -R Features2d_LightGlueMatcher`
* Run sample application with:

  * Real images
  * Real ONNX models
* Verify Python/Java bindings compile and work

---

### Related

Phase 1 of the
"End-to-End AI Feature Extraction and LightGlue Matching Pipeline"
GSoC project.

Designed to be extensible to:

* XFeat
* SuperPoint
* Other neural feature extractors

### test dependency

Depends on the opencv_extra PR adding ALIKED and LightGlue test models:

- [opencv_extra PR](https://github.com/opencv/opencv_extra/pull/1366)

This PR adds the following ONNX models to `download_models.py`:

- `aliked-n16rot-top1k-640.onnx`
- `aliked_lightglue.onnx`

These models are required for the `features2d` tests in the main OpenCV repository to validate the ALIKED and LightGlue feature extraction and matching pipeline.

### 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
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
2026-06-05 14:20:53 +03:00

172 lines
6.2 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"
#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::dnn::EngineType>(
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 = 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<KeyPoint> keypoints;
Mat descriptors;
aliked->detectAndCompute(img, noArray(), keypoints, descriptors);
EXPECT_GT(keypoints.size(), 100u);
ASSERT_EQ(descriptors.rows, static_cast<int>(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<float>(img.cols));
EXPECT_LT(kp.pt.y, static_cast<float>(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<int>(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<float>(img.cols);
const float origH = static_cast<float>(img.rows);
for (int i = 0; i < refKpts.rows; i++)
{
float refX = (refKpts.at<float>(i, 0) + 1.0f) * 0.5f * origW;
float refY = (refKpts.at<float>(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 = ALIKED::create(alikedPath);
Ptr<LightGlueMatcher> 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<KeyPoint> kpts1, kpts2;
Mat descs1, descs2;
aliked->detectAndCompute(img1, noArray(), kpts1, descs1);
aliked->detectAndCompute(img2, noArray(), kpts2, descs2);
ASSERT_GT(static_cast<int>(kpts1.size()), 0);
ASSERT_GT(static_cast<int>(kpts2.size()), 0);
// Build keypoint matrices (pixel coordinates)
Mat kpts1Mat(static_cast<int>(kpts1.size()), 2, CV_32F);
Mat kpts2Mat(static_cast<int>(kpts2.size()), 2, CV_32F);
for (size_t i = 0; i < kpts1.size(); i++)
{
kpts1Mat.at<float>(static_cast<int>(i), 0) = kpts1[i].pt.x;
kpts1Mat.at<float>(static_cast<int>(i), 1) = kpts1[i].pt.y;
}
for (size_t i = 0; i < kpts2.size(); i++)
{
kpts2Mat.at<float>(static_cast<int>(i), 0) = kpts2[i].pt.x;
kpts2Mat.at<float>(static_cast<int>(i), 1) = kpts2[i].pt.y;
}
lg->setPairInfo(kpts1Mat, kpts2Mat, img1.size(), img2.size());
std::vector<DMatch> matches;
lg->match(descs1, descs2, matches);
ASSERT_GT(static_cast<int>(matches.size()), 0);
for (const auto& m : matches)
{
EXPECT_GE(m.queryIdx, 0);
EXPECT_LT(m.queryIdx, static_cast<int>(kpts1.size()));
EXPECT_GE(m.trainIdx, 0);
EXPECT_LT(m.trainIdx, static_cast<int>(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<int>(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<int>(refMatches.at<int64_t>(i, 0));
int refTIdx = static_cast<int>(refMatches.at<int64_t>(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