1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 23:33:05 +04:00

Merge pull request #29073 from abhishek-gola:disk_feature_extractor

Added DISK feature extractor support #29073

closes: https://github.com/opencv/opencv/issues/27083
Merge with: https://github.com/opencv/opencv_extra/pull/1368/

### 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
- [x] 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
This commit is contained in:
Abhishek Gola
2026-06-03 15:41:11 +05:30
committed by GitHub
parent a0a660fcb1
commit b0b77e7b32
15 changed files with 793 additions and 51 deletions
+9
View File
@@ -1303,6 +1303,15 @@
issn = {1042-296X},
url = {https://kmlee.gatech.edu/me6406/handeye.pdf}
}
@inproceedings{Tyszkiewicz2020DISK,
author = {Tyszkiewicz, Micha{\l} and Fua, Pascal and Trulls, Eduard},
title = {{DISK}: Learning local features with policy gradient},
booktitle = {Advances in Neural Information Processing Systems},
volume = {33},
pages = {14254--14265},
year = {2020},
url = {https://proceedings.neurips.cc/paper/2020/hash/a42a596fc71e17828440030074d15e74-Abstract.html}
}
@inproceedings{UES01,
author = {Uyttendaele, Matthew and Eden, Ashley and Skeliski, R},
title = {Eliminating ghosting and exposure artifacts in image mosaics},
+1 -1
View File
@@ -54,7 +54,7 @@ MatShape convInferShape(const MatShape& inpShape, const MatShape& wshape,
const std::vector<int>& pads,
AutoPadding autoPad, bool ceilMode)
{
bool blockLayout = true;
bool blockLayout = (inpShape.layout == DATA_LAYOUT_BLOCK);
int ndims = inpShape.dims;
size_t nspatialdims = (size_t)(ndims - 2 - int(blockLayout));
MatShape outshape = inpShape;
@@ -193,20 +193,23 @@ void fastNormChannel(const Mat &input, const Mat &scale, const Mat &bias, Mat &o
const size_t outStep3 = output.step.p[3] / sizeof(float);
const size_t norm_size = (size_t)H * (size_t)W;
const float inv_norm_size = 1.f / (float)norm_size;
#if CV_SIMD
const int VEC_SZ = VTraits<v_float32>::vlanes();
#endif
// Accumulators are double-precision
const double inv_norm_size_d = 1.0 / (double)norm_size;
parallel_for_(Range(0, N * C1), [&](const Range& r) {
const float* inptr0 = (const float*)input.data;
float* outptr0 = (float*)output.data;
AutoBuffer<float> buf(C0 * 4);
float* sum = buf.data();
float* sqsum = sum + C0;
float* alpha = sqsum + C0;
AutoBuffer<double> sumBuf(C0 * 2);
double* sum = sumBuf.data();
double* sqsum = sum + C0;
AutoBuffer<float> abBuf(C0 * 2);
float* alpha = abBuf.data();
float* beta = alpha + C0;
for (int i = r.start; i < r.end; ++i) {
@@ -219,28 +222,36 @@ void fastNormChannel(const Mat &input, const Mat &scale, const Mat &bias, Mat &o
float* outbase = outptr0 + n * outStep0 + c1 * outStep1;
int c0 = 0;
#if CV_SIMD
#if CV_SIMD && CV_SIMD_64F
const int VEC_SZ_D = VTraits<v_float64>::vlanes();
CV_DbgAssert(VEC_SZ == 2 * VEC_SZ_D);
for (; c0 <= validC0 - VEC_SZ; c0 += VEC_SZ) {
v_float32 vsum = vx_setzero_f32();
v_float32 vsqsum = vx_setzero_f32();
v_float64 vsum_lo = vx_setzero_f64(), vsum_hi = vx_setzero_f64();
v_float64 vsqsum_lo = vx_setzero_f64(), vsqsum_hi = vx_setzero_f64();
for (int h = 0; h < H; ++h) {
const float* inrow = inbase + h * inStep2;
for (int w = 0; w < W; ++w) {
v_float32 v = vx_load(inrow + w * inStep3 + c0);
vsum = v_add(vsum, v);
vsqsum = v_fma(v, v, vsqsum);
v_float64 vlo = v_cvt_f64(v);
v_float64 vhi = v_cvt_f64_high(v);
vsum_lo = v_add(vsum_lo, vlo);
vsum_hi = v_add(vsum_hi, vhi);
vsqsum_lo = v_fma(vlo, vlo, vsqsum_lo);
vsqsum_hi = v_fma(vhi, vhi, vsqsum_hi);
}
}
vx_store(sum + c0, vsum);
vx_store(sqsum + c0, vsqsum);
vx_store(sum + c0, vsum_lo);
vx_store(sum + c0 + VEC_SZ_D, vsum_hi);
vx_store(sqsum + c0, vsqsum_lo);
vx_store(sqsum + c0 + VEC_SZ_D, vsqsum_hi);
}
#endif
for (; c0 < validC0; ++c0) {
float s = 0.f, sq = 0.f;
double s = 0., sq = 0.;
for (int h = 0; h < H; ++h) {
const float* inrow = inbase + h * inStep2;
for (int w = 0; w < W; ++w) {
float v = inrow[w * inStep3 + c0];
double v = (double)inrow[w * inStep3 + c0];
s += v;
sq += v * v;
}
@@ -250,11 +261,11 @@ void fastNormChannel(const Mat &input, const Mat &scale, const Mat &bias, Mat &o
}
for (int c = 0; c < validC0; ++c) {
float mean = sum[c] * inv_norm_size;
float var = std::max(0.f, sqsum[c] * inv_norm_size - mean * mean);
float inv_stdev = 1.f / std::sqrt(var + epsilon);
double mean = sum[c] * inv_norm_size_d;
double var = std::max(0., sqsum[c] * inv_norm_size_d - mean * mean);
float inv_stdev = 1.f / std::sqrt((float)var + epsilon);
alpha[c] = scale_data[cbase + c] * inv_stdev;
beta[c] = bias_data[cbase + c] - alpha[c] * mean;
beta[c] = bias_data[cbase + c] - alpha[c] * (float)mean;
}
c0 = 0;
+125 -26
View File
@@ -376,6 +376,53 @@ static void maxPool16bf(const void* inp_, void* out_, const ConvState& cs)
typedef void (*MaxPoolFunc)(const void* inp, void* out, const ConvState& cs);
// 2-output (values + ONNX-style int64 indices) NCHW scalar implementation.
static void maxPool32f_nchw_with_indices(const float* inp, float* out, int64_t* outIdx,
int N, int C, int Hi, int Wi, int H, int W,
int kH, int kW, int sH, int sW,
int padH, int padW, int dilH, int dilW)
{
int NC = N * C;
int inHW = Hi * Wi;
int outHW = H * W;
parallel_for_(Range(0, NC), [&](const Range& r) {
for (int nc = r.start; nc < r.end; nc++) {
int c = nc % C;
const float* inp_nc = inp + nc * inHW;
float* out_nc = out + nc * outHW;
int64_t* idx_nc = outIdx + nc * outHW;
for (int yo = 0; yo < H; yo++) {
for (int xo = 0; xo < W; xo++) {
float vmax = -FLT_MAX;
int64_t idxmax = -1;
for (int ky = 0; ky < kH; ky++) {
int yi = yo * sH - padH + ky * dilH;
if ((unsigned)yi >= (unsigned)Hi)
continue;
for (int kx = 0; kx < kW; kx++) {
int xi = xo * sW - padW + kx * dilW;
if ((unsigned)xi >= (unsigned)Wi)
continue;
float v = inp_nc[yi * Wi + xi];
if (v > vmax) {
vmax = v;
// ONNX storage_order=0: index = (c*Hi + yi)*Wi + xi
idxmax = (int64_t)(c * Hi + yi) * Wi + xi;
}
}
}
if (idxmax < 0) {
vmax = 0.f;
idxmax = (int64_t)(c * Hi + 0) * Wi + 0;
}
out_nc[yo * W + xo] = vmax;
idx_nc[yo * W + xo] = idxmax;
}
}
}
});
}
class MaxPoolLayerImpl : public MaxPoolLayer
{
public:
@@ -452,7 +499,10 @@ public:
int ninputs = (int)inptypes.size();
CV_Assert(ninputs == 1);
outtypes.assign(1, inferType(inptypes[0]));
outtypes.clear();
outtypes.push_back(inferType(inptypes[0]));
if (outputs.size() == 2u)
outtypes.push_back(CV_64S); // ONNX MaxPool indices
temptypes.clear();
}
@@ -463,13 +513,14 @@ public:
std::vector<MatShape> &outshapes,
std::vector<MatShape> &tempshapes) const CV_OVERRIDE
{
CV_Assert(outputs.size() == 1u);
CV_Assert(outputs.size() == 1u || outputs.size() == 2u);
size_t ninputs = inpshapes.size();
CV_Assert(ninputs == 1);
outshapes.assign(1, convInferShape(inpshapes[0], MatShape(),
MatShape outshape = convInferShape(inpshapes[0], MatShape(),
kernel_shape, 0, strides, dilations,
pads, auto_pad, ceil_mode));
pads, auto_pad, ceil_mode);
outshapes.assign(outputs.size(), outshape);
tempshapes.clear();
return true;
}
@@ -480,9 +531,12 @@ public:
std::vector<DataLayout>& outputs) const CV_OVERRIDE
{
CV_Assert(actualInputs.size() == 1u);
desiredInputs.assign(1, DATA_LAYOUT_BLOCK);
outputs.assign(requiredOutputs, DATA_LAYOUT_BLOCK);
return getNetImpl(this)->defaultC0;
const bool wantsIndices = requiredOutputs == 2;
const DataLayout layout = wantsIndices ? DATA_LAYOUT_NCHW : DATA_LAYOUT_BLOCK;
desiredInputs.assign(1, layout);
outputs.assign(requiredOutputs, layout);
return wantsIndices ? 0 : getNetImpl(this)->defaultC0;
}
void finalize(InputArrayOfArrays, OutputArrayOfArrays outputs_arr) CV_OVERRIDE
@@ -493,40 +547,85 @@ public:
OutputArrayOfArrays outputs_arr,
OutputArrayOfArrays) CV_OVERRIDE
{
size_t ninputs = inputs_arr.total();
CV_Assert(ninputs == 1);
CV_Assert(inputs_arr.total() == 1);
const int inptype = inputs_arr.type(0);
const MatShape inpshape = inputs_arr.shape(0);
int inptype = inputs_arr.type(0);
MatShape inpshape = inputs_arr.shape(0);
MatShape outshape = convInferShape(inpshape, MatShape(),
kernel_shape, 0, strides, dilations,
pads, auto_pad, ceil_mode);
int outKind = outputs_arr.kind();
const int outKind = outputs_arr.kind();
CV_Assert(outKind == _InputArray::STD_VECTOR_MAT ||
outKind == _InputArray::STD_VECTOR_UMAT);
ConvState cs;
cs.initPooling(inpshape, outshape, kernel_shape, strides,
dilations, pads, auto_pad, ceil_mode);
const size_t noutputs = outputs.size();
CV_Assert(noutputs == 1u || noutputs == 2u);
const bool wantsIndices = (noutputs == 2u);
if (wantsIndices) {
CV_Assert(inptype == CV_32F && "MaxPool with indices currently supports CV_32F only");
CV_Assert(inpshape.dims == 4 && "MaxPool with indices: only 4D (N,C,H,W) inputs supported");
CV_Assert(inpshape.layout != DATA_LAYOUT_BLOCK &&
"MaxPool with indices does not run on a BLOCK-layout input");
}
const MatShape outshape = convInferShape(inpshape, MatShape(),
kernel_shape, 0, strides, dilations,
pads, auto_pad, ceil_mode);
Mat inp = inputs_arr.getMat(0);
if (outKind == _InputArray::STD_VECTOR_MAT) {
Mat inp = inputs_arr.getMat(0);
std::vector<Mat>& outs = outputs_arr.getMatVecRef();
outs.resize(1);
outs.resize(noutputs);
outs[0].fit(outshape, inptype);
runOp(inp, outs[0], cs);
if (wantsIndices)
outs[1].fit(outshape, CV_64S);
runForward(inp, outs, inpshape, outshape);
} else {
// [TODO] more efficient OpenCL implementation
Mat inp = inputs_arr.getMat(0);
std::vector<UMat>& outs = outputs_arr.getUMatVecRef();
outs.resize(1);
outs.resize(noutputs);
outs[0].fit(outshape, inptype);
Mat temp(outshape, inptype);
runOp(inp, temp, cs);
temp.copyTo(outs[0]);
if (wantsIndices)
outs[1].fit(outshape, CV_64S);
std::vector<Mat> tmp(noutputs);
tmp[0].create(outshape, inptype);
if (wantsIndices)
tmp[1].create(outshape, CV_64S);
runForward(inp, tmp, inpshape, outshape);
for (size_t i = 0; i < noutputs; ++i)
tmp[i].copyTo(outs[i]);
}
}
void runForward(const Mat& inp, std::vector<Mat>& outs,
const MatShape& inpshape, const MatShape& outshape)
{
if (outs.size() == 1u) {
ConvState cs;
cs.initPooling(inpshape, outshape, kernel_shape, strides,
dilations, pads, auto_pad, ceil_mode);
runOp(inp, outs[0], cs);
} else {
runOpWithIndices(inp, outs[0], outs[1]);
}
}
void runOpWithIndices(const Mat& inp, Mat& outVal, Mat& outIdx)
{
const int N = inp.size[0], C = inp.size[1], Hi = inp.size[2], Wi = inp.size[3];
const int H = outVal.size[2], W = outVal.size[3];
const int kH = (int)kernel_shape[0];
const int kW = (int)kernel_shape[1];
const int sH = strides.size() > 0 ? (int)strides[0] : 1;
const int sW = strides.size() > 1 ? (int)strides[1] : sH;
const int dilH = dilations.size() > 0 ? (int)dilations[0] : 1;
const int dilW = dilations.size() > 1 ? (int)dilations[1] : dilH;
const int padH = pads.size() > 0 ? (int)pads[0] : 0;
const int padW = pads.size() > 1 ? (int)pads[1] : padH;
maxPool32f_nchw_with_indices(
inp.ptr<float>(), outVal.ptr<float>(), outIdx.ptr<int64_t>(),
N, C, Hi, Wi, H, W, kH, kW, sH, sW, padH, padW, dilH, dilW);
}
void runOp(const Mat& inp, Mat& out, const ConvState& cs)
{
int inptype = inp.type();
+1 -1
View File
@@ -1181,7 +1181,7 @@ void ONNXImporter2::parseMaxUnpool(LayerParams& layerParams, const opencv_onnx::
void ONNXImporter2::parseMaxPool(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
{
CV_CheckEQ(node_outputs.size(), 1u, "the new engine does not support MaxPool with 2 outputs yet");
CV_CheckLE(node_outputs.size(), 2u, "MaxPool may have at most 2 outputs (values + indices)");
layerParams.type = "MaxPool";
addLayer(layerParams, node_proto);
}
@@ -1546,6 +1546,12 @@ CASE(test_maxpool_3d_default)
#if SKIP_SET_1
SKIP_NON_CPU;
#endif
CASE(test_maxpool_3d_dilations)
SKIP;
CASE(test_maxpool_3d_dilations_use_ref_impl)
SKIP;
CASE(test_maxpool_3d_dilations_use_ref_impl_large)
SKIP;
CASE(test_maxpool_with_argmax_2d_precomputed_pads)
SKIP;
CASE(test_maxpool_with_argmax_2d_precomputed_strides)
@@ -753,3 +753,6 @@
"test_convtranspose_pad",
"test_convtranspose_pads",
"test_convtranspose_with_kernel",
"test_maxpool_3d_dilations",
"test_maxpool_3d_dilations_use_ref_impl",
"test_maxpool_3d_dilations_use_ref_impl_large",
@@ -329,9 +329,6 @@
"test_lppool_3d_default",
"test_matmulinteger", // Issues::Layer does not exist. Can't create layer "onnx_node_output_0!Y" of type "MatMulInteger" in function 'getLayerInstance'
"test_maxpool_2d_ceil_output_size_reduce_by_one",
"test_maxpool_3d_dilations",
"test_maxpool_3d_dilations_use_ref_impl",
"test_maxpool_3d_dilations_use_ref_impl_large", //shape mismatch
"test_melweightmatrix",
"test_momentum", // Issues::Layer does not exist. Can't create layer "onnx_node_output_0!X1_new" of type "ai.onnx.preview.training.Momentum" in function 'getLayerInstance'
"test_momentum_multiple", // ---- same as above ---
+1 -1
View File
@@ -6,7 +6,7 @@ set(debug_modules "")
if(DEBUG_opencv_features)
list(APPEND debug_modules opencv_highgui)
endif()
ocv_define_module(features opencv_imgproc opencv_geometry ${debug_modules} OPTIONAL opencv_flann WRAP java objc python js)
ocv_define_module(features opencv_imgproc opencv_geometry ${debug_modules} OPTIONAL opencv_flann opencv_dnn WRAP java objc python js)
ocv_install_3rdparty_licenses(mscr "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/mscr/chi_table_LICENSE.txt")
ocv_install_3rdparty_licenses(annoylib "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/annoy/LICENSE")
@@ -669,9 +669,88 @@ public:
CV_WRAP virtual void setK(double k) = 0;
CV_WRAP virtual double getK() const = 0;
CV_WRAP virtual String getDefaultName() const CV_OVERRIDE;
};
#if defined(HAVE_OPENCV_DNN) || defined(CV_DOXYGEN)
/** @brief DISK feature detector and descriptor, based on a DNN model.
DISK (Deep Image Structure and Keypoints) is a learned local-feature pipeline that produces
keypoints and 128-D L2-normalized descriptors via a single forward pass through a fully
convolutional network. This class wraps an ONNX export of the pre-trained DISK model through
cv::dnn::Net and exposes it under the standard cv::Feature2D interface so it can be used as
a drop-in alternative to SIFT/ORB.
The class assumes the ONNX model has a single input named `image` taking an N×3×H×W float32
tensor in [0, 1] (RGB channel order) and three outputs named `keypoints` (N×2), `scores` (N)
and `descriptors` (N×128).
*/
class CV_EXPORTS_W DISK : public Feature2D
{
public:
/** @brief Creates a DISK detector.
@param modelPath Path to the DISK ONNX model.
@param maxKeypoints Maximum number of keypoints to return per image. The strongest
responses (by network score) are kept; -1 keeps all detections.
@param scoreThreshold Discard keypoints with network score strictly below this value.
@param imageSize Target input size (width, height) fed to the network. Use Size()
(the default) to fall back to the network's expected fixed input
shape of 1024x1024. When overriding, both dimensions must be
positive multiples of 16, since DISK downsamples by a factor of 16.
@param backendId DNN backend identifier (see cv::dnn::Backend); 0 = DNN_BACKEND_DEFAULT.
@param targetId DNN target identifier (see cv::dnn::Target); 0 = DNN_TARGET_CPU.
*/
CV_WRAP static Ptr<DISK> create(const String& modelPath,
int maxKeypoints = -1,
float scoreThreshold = 0.0f,
const Size& imageSize = Size(),
int backendId = 0,
int targetId = 0);
/** @brief Creates a DISK detector from an in-memory model buffer.
This overload loads the DISK ONNX model from a buffer instead of a file on disk. It is
intended for cases where the model is read from application resources (for example Android
assets) and is not available as a path on the filesystem.
@param bufferModel A buffer containing the contents of the DISK ONNX model.
@param maxKeypoints Maximum number of keypoints to return per image. The strongest
responses (by network score) are kept; -1 keeps all detections.
@param scoreThreshold Discard keypoints with network score strictly below this value.
@param imageSize Target input size (width, height) fed to the network. Use Size()
(the default) to fall back to the network's expected fixed input
shape of 1024x1024. When overriding, both dimensions must be
positive multiples of 16, since DISK downsamples by a factor of 16.
@param backendId DNN backend identifier (see cv::dnn::Backend); 0 = DNN_BACKEND_DEFAULT.
@param targetId DNN target identifier (see cv::dnn::Target); 0 = DNN_TARGET_CPU.
@note In C++ this is an overload of @ref create. The Python/Java/Objective-C bindings expose
it as `createFromMemory`, because Objective-C selectors are not disambiguated by argument
type and would otherwise clash with the file-path @ref create.
*/
CV_WRAP_AS(createFromMemory) static Ptr<DISK> create(const std::vector<uchar>& bufferModel,
int maxKeypoints = -1,
float scoreThreshold = 0.0f,
const Size& imageSize = Size(),
int backendId = 0,
int targetId = 0);
CV_WRAP virtual void setMaxKeypoints(int maxKeypoints) = 0;
CV_WRAP virtual int getMaxKeypoints() const = 0;
CV_WRAP virtual void setScoreThreshold(float threshold) = 0;
CV_WRAP virtual float getScoreThreshold() const = 0;
CV_WRAP virtual void setImageSize(const Size& size) = 0;
CV_WRAP virtual Size getImageSize() const = 0;
CV_WRAP virtual String getDefaultName() const CV_OVERRIDE;
};
#endif // HAVE_OPENCV_DNN || CV_DOXYGEN
/** @brief Class for extracting blobs from an image. :
The class implements a simple algorithm for extracting blobs from an image:
+234
View File
@@ -0,0 +1,234 @@
// 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.
// Copyright (C) 2026, BigVision LLC, all rights reserved.
// Third party copyrights are property of their respective owners.
#include "precomp.hpp"
#include "opencv2/features.hpp"
#ifdef HAVE_OPENCV_DNN
#include "opencv2/dnn.hpp"
#include <algorithm>
#include <numeric>
namespace cv {
using namespace dnn;
// Default network input size used when the user does not specify one explicitly.
// Matches the fixed-shape standalone DISK ONNX export shipped in opencv_extra.
static const Size kDefaultDiskInputSize = Size(1024, 1024);
// DISK is a fully convolutional network with a 16x downsampling stride, so user-provided
// input sizes must be positive multiples of 16.
static const int kDiskStride = 16;
class DISK_Impl CV_FINAL : public DISK
{
public:
DISK_Impl(const String& modelPath, int maxKeypoints, float scoreThreshold,
const Size& imageSize, int backendId, int targetId)
: maxKeypoints_(maxKeypoints),
scoreThreshold_(scoreThreshold),
imageSize_(imageSize)
{
validateImageSize(imageSize_);
initNet(readNetFromONNX(modelPath), backendId, targetId);
}
DISK_Impl(const std::vector<uchar>& bufferModel, int maxKeypoints, float scoreThreshold,
const Size& imageSize, int backendId, int targetId)
: maxKeypoints_(maxKeypoints),
scoreThreshold_(scoreThreshold),
imageSize_(imageSize)
{
validateImageSize(imageSize_);
initNet(readNetFromONNX(bufferModel), backendId, targetId);
}
void detectAndCompute(InputArray _image, InputArray _mask,
std::vector<KeyPoint>& keypoints,
OutputArray _descriptors,
bool useProvidedKeypoints) CV_OVERRIDE
{
CV_Assert(!useProvidedKeypoints && "DISK does not support providing keypoints externally");
keypoints.clear();
Mat image = _image.getMat();
if (image.empty())
{
if (_descriptors.needed())
_descriptors.release();
return;
}
Mat mask = _mask.getMat();
if (!mask.empty())
{
CV_Assert(mask.type() == CV_8UC1);
CV_Assert(mask.size() == image.size());
}
const Size netSize = (imageSize_.width > 0 && imageSize_.height > 0) ? imageSize_ : kDefaultDiskInputSize;
const float scaleX = static_cast<float>(image.cols) / netSize.width;
const float scaleY = static_cast<float>(image.rows) / netSize.height;
Mat blob;
// 1/255 normalization, swap BGR->RGB, no mean subtraction.
blobFromImage(image, blob, 1.0 / 255.0, netSize, Scalar(), /*swapRB=*/true, /*crop=*/false);
net_.setInput(blob, "image");
const std::vector<String> outNames = {"keypoints", "scores", "descriptors"};
std::vector<Mat> outs;
net_.forward(outs, outNames);
CV_Assert(outs.size() == 3);
// DISK's ONNX export emits keypoints as int64 pixel coordinates, scores and
// descriptors as float32. Reshape each output to (N, *) for row-wise indexing.
Mat kptsBlob = outs[0].reshape(1, outs[0].size[1]); // N x 2, int64
Mat scoresBlob = outs[1].reshape(1, outs[1].size[1]); // N x 1, float32
Mat descBlob = outs[2].reshape(1, outs[2].size[1]); // N x D, float32
CV_Assert(kptsBlob.depth() == CV_64S);
CV_Assert(scoresBlob.depth() == CV_32F);
CV_Assert(descBlob.depth() == CV_32F);
const int numFeatures = kptsBlob.rows;
CV_Assert(scoresBlob.rows == numFeatures);
CV_Assert(descBlob.rows == numFeatures);
const int64_t* kptsData = kptsBlob.ptr<int64_t>();
const float* scoresData = scoresBlob.ptr<float>();
std::vector<int> validIndices;
validIndices.reserve(numFeatures);
keypoints.reserve(numFeatures);
for (int i = 0; i < numFeatures; ++i)
{
const float score = scoresData[i];
if (score <= scoreThreshold_)
continue;
const float x = static_cast<float>(kptsData[i * 2]) * scaleX;
const float y = static_cast<float>(kptsData[i * 2 + 1]) * scaleY;
if (!mask.empty())
{
const int ix = cvFloor(x);
const int iy = cvFloor(y);
if (ix < 0 || iy < 0 || ix >= mask.cols || iy >= mask.rows)
continue;
if (mask.at<uchar>(iy, ix) == 0)
continue;
}
keypoints.emplace_back(x, y, 1.0f, -1.0f, score);
validIndices.push_back(i);
}
if (maxKeypoints_ > 0 && static_cast<int>(keypoints.size()) > maxKeypoints_)
{
std::vector<int> order(keypoints.size());
std::iota(order.begin(), order.end(), 0);
std::partial_sort(order.begin(), order.begin() + maxKeypoints_, order.end(),
[&](int a, int b) { return keypoints[a].response > keypoints[b].response; });
order.resize(maxKeypoints_);
std::vector<KeyPoint> kept;
std::vector<int> keptIdx;
kept.reserve(maxKeypoints_);
keptIdx.reserve(maxKeypoints_);
for (int idx : order)
{
kept.push_back(keypoints[idx]);
keptIdx.push_back(validIndices[idx]);
}
keypoints.swap(kept);
validIndices.swap(keptIdx);
}
if (_descriptors.needed())
{
if (validIndices.empty())
{
_descriptors.release();
return;
}
const int dim = descBlob.cols;
_descriptors.create(static_cast<int>(validIndices.size()), dim, CV_32F);
Mat descriptors = _descriptors.getMat();
for (size_t i = 0; i < validIndices.size(); ++i)
descBlob.row(validIndices[i]).copyTo(descriptors.row(static_cast<int>(i)));
}
}
int descriptorSize() const CV_OVERRIDE { return 128; }
int descriptorType() const CV_OVERRIDE { return CV_32F; }
int defaultNorm() const CV_OVERRIDE { return NORM_L2; }
bool empty() const CV_OVERRIDE { return net_.empty(); }
void setMaxKeypoints(int maxKeypoints) CV_OVERRIDE { maxKeypoints_ = maxKeypoints; }
int getMaxKeypoints() const CV_OVERRIDE { return maxKeypoints_; }
void setScoreThreshold(float threshold) CV_OVERRIDE { scoreThreshold_ = threshold; }
float getScoreThreshold() const CV_OVERRIDE { return scoreThreshold_; }
void setImageSize(const Size& size) CV_OVERRIDE
{
validateImageSize(size);
imageSize_ = size;
}
Size getImageSize() const CV_OVERRIDE { return imageSize_; }
String getDefaultName() const CV_OVERRIDE { return Feature2D::getDefaultName() + ".DISK"; }
private:
void initNet(const Net& net, int backendId, int targetId)
{
net_ = net;
net_.setPreferableBackend(backendId);
net_.setPreferableTarget(targetId);
}
static void validateImageSize(const Size& size)
{
if (size.width == 0 && size.height == 0)
return; // use default
CV_Assert(size.width > 0 && size.height > 0);
CV_Assert(size.width % kDiskStride == 0);
CV_Assert(size.height % kDiskStride == 0);
}
int maxKeypoints_;
float scoreThreshold_;
Size imageSize_;
Net net_;
};
Ptr<DISK> DISK::create(const String& modelPath, int maxKeypoints, float scoreThreshold,
const Size& imageSize, int backendId, int targetId)
{
CV_TRACE_FUNCTION();
return makePtr<DISK_Impl>(modelPath, maxKeypoints, scoreThreshold, imageSize, backendId, targetId);
}
Ptr<DISK> DISK::create(const std::vector<uchar>& bufferModel, int maxKeypoints, float scoreThreshold,
const Size& imageSize, int backendId, int targetId)
{
CV_TRACE_FUNCTION();
return makePtr<DISK_Impl>(bufferModel, maxKeypoints, scoreThreshold, imageSize, backendId, targetId);
}
String DISK::getDefaultName() const
{
return Feature2D::getDefaultName() + ".DISK";
}
} // namespace cv
#endif // HAVE_OPENCV_DNN
+103
View File
@@ -0,0 +1,103 @@
// 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.
//
// Copyright (C) 2017, Intel Corporation, all rights reserved.
// Third party copyrights are property of their respective owners.
#include "test_precomp.hpp"
#include "npy_blob.hpp"
namespace cv
{
static std::string getType(const std::string& header)
{
std::string field = "'descr':";
int idx = header.find(field);
CV_Assert(idx != -1);
int from = header.find('\'', idx + field.size()) + 1;
int to = header.find('\'', from);
return header.substr(from, to - from);
}
static std::string getFortranOrder(const std::string& header)
{
std::string field = "'fortran_order':";
int idx = header.find(field);
CV_Assert(idx != -1);
int from = header.find_last_of(' ', idx + field.size()) + 1;
int to = header.find(',', from);
return header.substr(from, to - from);
}
static std::vector<int> getShape(const std::string& header)
{
std::string field = "'shape':";
int idx = header.find(field);
CV_Assert(idx != -1);
int from = header.find('(', idx + field.size()) + 1;
int to = header.find(')', from);
std::string shapeStr = header.substr(from, to - from);
if (shapeStr.empty())
return std::vector<int>(1, 1);
// Remove all commas.
shapeStr.erase(std::remove(shapeStr.begin(), shapeStr.end(), ','),
shapeStr.end());
std::istringstream ss(shapeStr);
int value;
std::vector<int> shape;
while (ss >> value)
{
shape.push_back(value);
}
return shape;
}
Mat blobFromNPY(const std::string& path)
{
std::ifstream ifs(path.c_str(), std::ios::binary);
CV_Assert(ifs.is_open());
std::string magic(6, '*');
ifs.read(&magic[0], magic.size());
CV_Assert(magic == "\x93NUMPY");
ifs.ignore(1); // Skip major version byte.
ifs.ignore(1); // Skip minor version byte.
unsigned short headerSize;
ifs.read((char*)&headerSize, sizeof(headerSize));
std::string header(headerSize, '*');
ifs.read(&header[0], header.size());
// Extract data type.
int matType;
if (getType(header) == "<f4")
matType = CV_32F;
else if (getType(header) == "<i4")
matType = CV_32S;
else if (getType(header) == "<i8")
matType = CV_64S;
else
CV_Error(Error::BadDepth, "Unsupported numpy type");
CV_Assert(getFortranOrder(header) == "False");
std::vector<int> shape = getShape(header);
Mat blob(shape, matType);
ifs.read((char*)blob.data, blob.total() * blob.elemSize());
CV_Assert((size_t)ifs.gcount() == blob.total() * blob.elemSize());
return blob;
}
} // namespace cv
+20
View File
@@ -0,0 +1,20 @@
// 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.
//
// Copyright (C) 2017, Intel Corporation, all rights reserved.
// Third party copyrights are property of their respective owners.
#ifndef __OPENCV_DNN_TEST_NPY_BLOB_HPP__
#define __OPENCV_DNN_TEST_NPY_BLOB_HPP__
namespace cv
{
// Parse serialized NumPy array by np.save(...)
// Based on specification of .npy data format.
Mat blobFromNPY(const std::string& path);
}
#endif
+174
View File
@@ -0,0 +1,174 @@
// 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.
// Copyright (C) 2026, BigVision LLC, all rights reserved.
// Third party copyrights are property of their respective owners.
#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("DISK ONNX model is not supported by the classic DNN engine");
}
static void testDiskRegression(const Size& imageSize, const std::string& tag)
{
skipIfClassicDnnEngine();
applyTestTag(CV_TEST_TAG_MEMORY_2GB);
Mat refKpts = blobFromNPY(cvtest::findDataFile("features/disk/box_in_scene_" + tag + "_kpts.npy"));
Mat refDesc = blobFromNPY(cvtest::findDataFile("features/disk/box_in_scene_" + tag + "_desc.npy"));
if (refKpts.type() != CV_32F)
refKpts.convertTo(refKpts, CV_32F);
ASSERT_EQ(refKpts.cols, 3);
const int n = refKpts.rows;
const std::string modelPath = cvtest::findDataFile("dnn/disk.onnx", false);
Ptr<DISK> detector;
ASSERT_NO_THROW(detector = DISK::create(modelPath, n, 0.0f, imageSize));
ASSERT_TRUE(detector);
EXPECT_FALSE(detector->empty());
EXPECT_EQ(detector->descriptorSize(), 128);
EXPECT_EQ(detector->descriptorType(), CV_32F);
EXPECT_EQ(detector->defaultNorm(), NORM_L2);
Mat img = imread(cvtest::findDataFile("shared/box_in_scene.png"));
ASSERT_FALSE(img.empty());
std::vector<KeyPoint> keypoints;
Mat descriptors;
detector->detectAndCompute(img, noArray(), keypoints, descriptors);
ASSERT_EQ(static_cast<int>(keypoints.size()), n) << "keypoint count mismatch (" << tag << ")";
ASSERT_EQ(descriptors.rows, n);
ASSERT_EQ(descriptors.cols, refDesc.cols);
ASSERT_EQ(descriptors.type(), CV_32F);
Mat pos(n, 2, CV_32F), resp(n, 1, CV_32F);
for (int i = 0; i < n; ++i)
{
pos.at<float>(i, 0) = keypoints[i].pt.x;
pos.at<float>(i, 1) = keypoints[i].pt.y;
resp.at<float>(i, 0) = keypoints[i].response;
}
EXPECT_LE(cvtest::norm(pos, refKpts.colRange(0, 2), NORM_INF), 1e-3)
<< "keypoint positions differ (" << tag << ")";
EXPECT_LE(cvtest::norm(resp, refKpts.col(2), NORM_INF), 0.1)
<< "keypoint responses differ (" << tag << ")";
EXPECT_LE(cvtest::norm(descriptors, refDesc, NORM_INF), 1e-2)
<< "descriptors differ (" << tag << ")";
}
TEST(Features2d_DISK, regression_default)
{
testDiskRegression(Size(), "default");
}
TEST(Features2d_DISK, regression_512x384)
{
testDiskRegression(Size(512, 384), "512x384");
}
TEST(Features2d_DISK, MaxKeypointsAndThreshold)
{
skipIfClassicDnnEngine();
applyTestTag(CV_TEST_TAG_MEMORY_2GB);
const std::string modelPath = cvtest::findDataFile("dnn/disk.onnx", false);
Ptr<DISK> detector = DISK::create(modelPath);
ASSERT_TRUE(detector);
Mat img = imread(cvtest::findDataFile("shared/lena.png"));
ASSERT_FALSE(img.empty());
std::vector<KeyPoint> baseKpts;
Mat baseDesc;
detector->detectAndCompute(img, noArray(), baseKpts, baseDesc);
ASSERT_GT(baseKpts.size(), 50u);
const int kCap = 50;
detector->setMaxKeypoints(kCap);
EXPECT_EQ(detector->getMaxKeypoints(), kCap);
std::vector<KeyPoint> capKpts;
Mat capDesc;
detector->detectAndCompute(img, noArray(), capKpts, capDesc);
EXPECT_EQ(capKpts.size(), static_cast<size_t>(kCap));
EXPECT_EQ(capDesc.rows, kCap);
float minKept = std::numeric_limits<float>::max();
for (const KeyPoint& kp : capKpts)
minKept = std::min(minKept, kp.response);
detector->setMaxKeypoints(-1);
detector->setScoreThreshold(minKept);
std::vector<KeyPoint> thrKpts;
detector->detectAndCompute(img, noArray(), thrKpts, noArray());
for (const KeyPoint& kp : thrKpts)
EXPECT_GT(kp.response, minKept);
}
TEST(Features2d_DISK, MaskSupport)
{
skipIfClassicDnnEngine();
applyTestTag(CV_TEST_TAG_MEMORY_2GB);
const std::string modelPath = cvtest::findDataFile("dnn/disk.onnx", false);
Ptr<DISK> detector = DISK::create(modelPath);
Mat img = imread(cvtest::findDataFile("shared/lena.png"));
ASSERT_FALSE(img.empty());
Mat mask = Mat::zeros(img.size(), CV_8UC1);
const Rect roi(img.cols / 4, img.rows / 4, img.cols / 2, img.rows / 2);
mask(roi).setTo(255);
std::vector<KeyPoint> keypoints;
Mat descriptors;
detector->detectAndCompute(img, mask, keypoints, descriptors);
ASSERT_FALSE(keypoints.empty());
ASSERT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
for (const KeyPoint& kp : keypoints)
{
EXPECT_TRUE(roi.contains(Point(cvFloor(kp.pt.x), cvFloor(kp.pt.y))))
<< "Keypoint " << kp.pt << " escaped the mask ROI " << roi;
}
}
TEST(Features2d_DISK, InvalidImageSize)
{
skipIfClassicDnnEngine();
const std::string modelPath = cvtest::findDataFile("dnn/disk.onnx", false);
EXPECT_THROW(DISK::create(modelPath, -1, 0.0f, Size(1000, 1024)), cv::Exception);
EXPECT_THROW(DISK::create(modelPath, -1, 0.0f, Size(1024, 1000)), cv::Exception);
EXPECT_THROW(DISK::create(modelPath, -1, 0.0f, Size(-16, 1024)), cv::Exception);
Ptr<DISK> detector;
ASSERT_NO_THROW(detector = DISK::create(modelPath, -1, 0.0f, Size()));
ASSERT_TRUE(detector);
EXPECT_THROW(detector->setImageSize(Size(15, 1024)), cv::Exception);
EXPECT_NO_THROW(detector->setImageSize(Size(512, 512)));
EXPECT_EQ(detector->getImageSize(), Size(512, 512));
}
}} // namespace
#endif // HAVE_OPENCV_DNN
+8 -1
View File
@@ -7,4 +7,11 @@
#include <hpx/hpx_main.hpp>
#endif
CV_TEST_MAIN("cv")
static
void initTests()
{
cvtest::addDataSearchEnv("OPENCV_DNN_TEST_DATA_PATH");
cvtest::addDataSearchSubDirectory(""); // override "cv" prefix below to access without "../dnn" hacks
}
CV_TEST_MAIN("cv", initTests())