1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-21 19:33:03 +04:00

Merge pull request #28453 from abhishek-gola:roialign_layer_add

Added RoiAlign layer support in new DNN engine #28453

Fixes `Unsupported Operation: RoiAlign` issue in https://github.com/opencv/opencv/issues/20258 and https://github.com/opencv/opencv/issues/22099, model parsing is successful now.

Merge with: https://github.com/opencv/opencv_extra/pull/1310

### 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-02-10 17:15:56 +05:30
committed by GitHub
parent ca0d47c8e6
commit 1719aa1339
8 changed files with 436 additions and 6 deletions
@@ -1420,6 +1420,12 @@ CV__DNN_INLINE_NS_BEGIN
static Ptr<Layer> create(const LayerParams& params);
};
class CV_EXPORTS RoiAlignLayer : public Layer
{
public:
static Ptr<RoiAlignLayer> create(const LayerParams& params);
};
class CV_EXPORTS CumSumLayer : public Layer
{
public:
+1
View File
@@ -90,6 +90,7 @@ void initializeLayerFactory()
CV_DNN_REGISTER_LAYER_CLASS(ConstantOfShape, ConstantOfShapeLayer);
CV_DNN_REGISTER_LAYER_CLASS(RandomNormalLike, RandomNormalLikeLayer);
CV_DNN_REGISTER_LAYER_CLASS(CropAndResize, CropAndResizeLayer);
CV_DNN_REGISTER_LAYER_CLASS(RoiAlign, RoiAlignLayer);
CV_DNN_REGISTER_LAYER_CLASS(DequantizeLinear, DequantizeLinearLayer);
CV_DNN_REGISTER_LAYER_CLASS(Expand2, Expand2Layer);
CV_DNN_REGISTER_LAYER_CLASS(Flatten, FlattenLayer);
+406
View File
@@ -0,0 +1,406 @@
// 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) 2025, BigVision LLC, all rights reserved.
// Third party copyrights are property of their respective owners.
#include "../precomp.hpp"
#include "layers_common.hpp"
#include <opencv2/dnn/shape_utils.hpp>
namespace cv {
namespace dnn {
// ONNX RoiAlign operator
// Spec: https://onnx.ai/onnx/operators/onnx__RoiAlign.html
// Supported opsets: 10-22
namespace {
enum class RoiAlignMode
{
AVG = 0,
MAX = 1
};
enum class CoordTransformMode
{
HALF_PIXEL = 0,
OUTPUT_HALF_PIXEL = 1
};
template<typename T, bool MaxMode>
class RoiAlignForwardInvoker CV_FINAL : public ParallelLoopBody
{
public:
RoiAlignForwardInvoker(const Mat& X_, Mat& Y_,
const Mat& rois_, const Mat& batch_indices_,
const int* Xsize,
int pooled_h_, int pooled_w_, int sampling_ratio_,
float spatial_scale_, float offset_,
bool clampMalformedRoi_)
: X(X_), Y(Y_), rois(rois_), batch_indices(batch_indices_),
output_height(pooled_h_), output_width(pooled_w_), sampling_ratio(sampling_ratio_),
spatial_scale(spatial_scale_), offset(offset_),
clampMalformedRoi(clampMalformedRoi_)
{
CV_Assert(X.isContinuous() && Y.isContinuous());
CV_Assert(rois.isContinuous() && batch_indices.isContinuous());
CV_Assert(Xsize);
N = Xsize[0]; C = Xsize[1]; H = Xsize[2]; W = Xsize[3];
Xdata = X.ptr<T>();
Ydata = Y.ptr<T>();
spatialSize = static_cast<size_t>(H) * W;
channelStride = spatialSize;
batchStride = static_cast<size_t>(C) * spatialSize;
outSpatial = output_height * output_width;
Rdepth = rois.depth();
const int bdepth = batch_indices.depth();
if (bdepth == CV_32S)
{
batch32 = batch_indices.ptr<int>();
}
else if (bdepth == CV_64S)
{
batch64 = batch_indices.ptr<int64>();
}
else
{
CV_Error(Error::StsUnsupportedFormat, "Unsupported batch_indices depth (expected int32 or int64)");
}
}
void operator()(const Range& range) const CV_OVERRIDE
{
for (int r = range.start; r < range.end; ++r)
{
const int b = batch64 ? static_cast<int>(batch64[r]) : batch32[r];
CV_Assert(0 <= b && b < N);
float x1, y1, x2, y2;
if (Rdepth == CV_32F)
readScaledRoiCoords<float>(rois, r, spatial_scale, offset, x1, y1, x2, y2);
else if (Rdepth == CV_64F)
readScaledRoiCoords<double>(rois, r, spatial_scale, offset, x1, y1, x2, y2);
else if (Rdepth == CV_16F)
readScaledRoiCoords<hfloat>(rois, r, spatial_scale, offset, x1, y1, x2, y2);
else if (Rdepth == CV_16BF)
readScaledRoiCoords<bfloat>(rois, r, spatial_scale, offset, x1, y1, x2, y2);
else
CV_Error(Error::StsUnsupportedFormat, "Unsupported rois depth");
float roi_width = x2 - x1;
float roi_height = y2 - y1;
if (clampMalformedRoi)
{
roi_width = std::max(roi_width, 1.f);
roi_height = std::max(roi_height, 1.f);
}
const float bin_size_w = roi_width / output_width;
const float bin_size_h = roi_height / output_height;
const int gw = std::max((sampling_ratio > 0) ? sampling_ratio
: cvCeil(roi_width / output_width), 1);
const int gh = std::max((sampling_ratio > 0) ? sampling_ratio
: cvCeil(roi_height / output_height), 1);
const int sample_count_i = gw * gh;
const float inv_sample_count = 1.f / static_cast<float>(sample_count_i);
const float step_y = bin_size_h / gh;
const float step_x = bin_size_w / gw;
AutoBuffer<BilinearSample> samples(static_cast<size_t>(outSpatial) * sample_count_i);
for (int outIdx = 0; outIdx < outSpatial; ++outIdx)
{
const int ph = outIdx / output_width;
const int pw = outIdx - ph * output_width;
const float ph_off = ph * bin_size_h;
const float pw_off = pw * bin_size_w;
BilinearSample* dst = samples.data() + static_cast<size_t>(outIdx) * sample_count_i;
for (int s = 0; s < sample_count_i; ++s)
{
const int iy = s / gw;
const int ix = s - iy * gw;
const float yy = y1 + ph_off + (iy + 0.5f) * step_y;
const float xx = x1 + pw_off + (ix + 0.5f) * step_x;
dst[s] = precomputeBilinearSample(H, W, yy, xx);
}
}
const size_t y_roi_base = static_cast<size_t>(r) * static_cast<size_t>(C) * outSpatial;
const size_t x_batch_base = static_cast<size_t>(b) * batchStride;
for (int outPlane = 0; outPlane < C * outSpatial; ++outPlane)
{
const int c = outPlane / outSpatial;
const int outIdx = outPlane - c * outSpatial;
const T* img = Xdata + x_batch_base + static_cast<size_t>(c) * channelStride;
const BilinearSample* src = samples.data() + static_cast<size_t>(outIdx) * sample_count_i;
float outv = MaxMode ? -FLT_MAX : 0.f;
for (int s = 0; s < sample_count_i; ++s)
{
const BilinearSample& bs = src[s];
const float v00 = static_cast<float>(img[bs.idx00]);
const float v01 = static_cast<float>(img[bs.idx01]);
const float v10 = static_cast<float>(img[bs.idx10]);
const float v11 = static_cast<float>(img[bs.idx11]);
const float v = MaxMode
? std::max(std::max(bs.w1 * v00, bs.w2 * v01), std::max(bs.w3 * v10, bs.w4 * v11))
: (bs.w1 * v00 + bs.w2 * v01 + bs.w3 * v10 + bs.w4 * v11);
outv = MaxMode ? std::max(outv, v) : (outv + v);
}
outv = MaxMode ? ((outv == -FLT_MAX) ? 0.f : outv) : (outv * inv_sample_count);
Ydata[y_roi_base + static_cast<size_t>(outPlane)] = saturate_cast<T>(outv);
}
}
}
private:
struct BilinearSample
{
size_t idx00 = 0, idx01 = 0, idx10 = 0, idx11 = 0;
float w1 = 0.f, w2 = 0.f, w3 = 0.f, w4 = 0.f;
};
template<typename TRoi>
static inline void readScaledRoiCoords(const Mat& rois, int r, float spatial_scale, float offset,
float& x1, float& y1, float& x2, float& y2)
{
const TRoi* p = rois.ptr<TRoi>(r);
x1 = static_cast<float>(p[0]) * spatial_scale - offset;
y1 = static_cast<float>(p[1]) * spatial_scale - offset;
x2 = static_cast<float>(p[2]) * spatial_scale - offset;
y2 = static_cast<float>(p[3]) * spatial_scale - offset;
}
static inline BilinearSample precomputeBilinearSample(int height, int width, float y, float x)
{
BilinearSample s;
if (y < -1.f || y > height || x < -1.f || x > width)
return s;
y = std::min(std::max(y, 0.f), static_cast<float>(height - 1));
x = std::min(std::max(x, 0.f), static_cast<float>(width - 1));
const int y_low = static_cast<int>(y);
const int x_low = static_cast<int>(x);
const int y_high = std::min(y_low + 1, height - 1);
const int x_high = std::min(x_low + 1, width - 1);
const float ly = y - y_low;
const float lx = x - x_low;
const float hy = 1.f - ly;
const float hx = 1.f - lx;
s.w1 = hy * hx;
s.w2 = hy * lx;
s.w3 = ly * hx;
s.w4 = ly * lx;
s.idx00 = static_cast<size_t>(y_low) * width + x_low;
s.idx01 = static_cast<size_t>(y_low) * width + x_high;
s.idx10 = static_cast<size_t>(y_high) * width + x_low;
s.idx11 = static_cast<size_t>(y_high) * width + x_high;
return s;
}
const Mat& X;
Mat& Y;
const Mat& rois;
const Mat& batch_indices;
int N = 0, C = 0, H = 0, W = 0;
int output_height, output_width, sampling_ratio;
float spatial_scale, offset;
bool clampMalformedRoi;
const T* Xdata = nullptr;
T* Ydata = nullptr;
int Rdepth = -1;
int outSpatial = 0;
size_t spatialSize = 0;
size_t channelStride = 0;
size_t batchStride = 0;
const int* batch32 = nullptr;
const int64* batch64 = nullptr;
};
}
class RoiAlignLayerImpl CV_FINAL : public RoiAlignLayer
{
public:
RoiAlignLayerImpl(const LayerParams& params)
{
setParamsFrom(params);
output_height = params.get<int>("output_height", 1);
output_width = params.get<int>("output_width", 1);
sampling_ratio = params.get<int>("sampling_ratio", 0);
spatial_scale = params.get<float>("spatial_scale", 1.f);
const String modeStr = params.get<String>("mode", "avg");
CV_Assert(modeStr == "avg" || modeStr == "max");
mode_ = (modeStr == "max") ? RoiAlignMode::MAX : RoiAlignMode::AVG;
const String coordStr = params.get<String>("coordinate_transformation_mode", "half_pixel");
CV_Assert(coordStr == "half_pixel" || coordStr == "output_half_pixel");
coord_mode_ = (coordStr == "half_pixel") ? CoordTransformMode::HALF_PIXEL
: CoordTransformMode::OUTPUT_HALF_PIXEL;
CV_Assert(output_height > 0 && output_width > 0);
CV_Assert(sampling_ratio >= 0);
}
bool supportBackend(int backendId) CV_OVERRIDE
{
return backendId == DNN_BACKEND_OPENCV;
}
bool getMemoryShapes(const std::vector<MatShape>& inputs,
const int /*requiredOutputs*/,
std::vector<MatShape>& outputs,
std::vector<MatShape>& /*internals*/) const CV_OVERRIDE
{
CV_Assert(inputs.size() == 3);
const MatShape& X = inputs[0];
const MatShape& rois = inputs[1];
const MatShape& batch_indices = inputs[2];
CV_Assert(X.size() == 4);
CV_Assert(rois.size() == 2);
CV_Assert(rois[1] == 4);
CV_Assert(batch_indices.size() == 1 || (batch_indices.size() == 2 && batch_indices[1] == 1));
MatShape out(4);
out[0] = rois[0];
out[1] = X[1];
out[2] = output_height;
out[3] = output_width;
outputs.assign(1, out);
return false;
}
void getTypes(const std::vector<MatType>& inputs,
const int requiredOutputs,
const int requiredInternals,
std::vector<MatType>& outputs,
std::vector<MatType>& internals) const CV_OVERRIDE
{
CV_Assert(inputs.size() == 3);
CV_Assert(CV_MAT_DEPTH(inputs[0]) == CV_32F || CV_MAT_DEPTH(inputs[0]) == CV_64F ||
CV_MAT_DEPTH(inputs[0]) == CV_16F || CV_MAT_DEPTH(inputs[0]) == CV_16BF);
CV_Assert(CV_MAT_DEPTH(inputs[1]) == CV_32F || CV_MAT_DEPTH(inputs[1]) == CV_64F ||
CV_MAT_DEPTH(inputs[1]) == CV_16F || CV_MAT_DEPTH(inputs[1]) == CV_16BF);
CV_Assert(CV_MAT_DEPTH(inputs[2]) == CV_32S || CV_MAT_DEPTH(inputs[2]) == CV_64S);
outputs.assign(requiredOutputs, MatType(inputs[0]));
internals.assign(requiredInternals, MatType(inputs[0]));
}
void forward(InputArrayOfArrays inputs_arr,
OutputArrayOfArrays outputs_arr,
OutputArrayOfArrays /*internals_arr*/) CV_OVERRIDE
{
CV_TRACE_FUNCTION();
CV_TRACE_ARG_VALUE(name, "name", name.c_str());
std::vector<Mat> inputs, outputs;
inputs_arr.getMatVector(inputs);
outputs_arr.getMatVector(outputs);
CV_Assert(inputs.size() == 3);
CV_Assert(outputs.size() == 1);
const Mat& X = inputs[0];
Mat rois = inputs[1];
Mat batch_indices = inputs[2];
Mat& Y = outputs[0];
CV_Assert(X.dims == 4);
const int C = X.size[1];
CV_Assert(X.depth() == CV_32F || X.depth() == CV_64F || X.depth() == CV_16F || X.depth() == CV_16BF);
CV_Assert(rois.depth() == CV_32F || rois.depth() == CV_64F || rois.depth() == CV_16F || rois.depth() == CV_16BF);
CV_Assert(batch_indices.depth() == CV_32S || batch_indices.depth() == CV_64S);
CV_Assert(rois.total() % 4 == 0);
const int num_rois = static_cast<int>(rois.total() / 4);
rois = rois.reshape(1, num_rois);
batch_indices = batch_indices.reshape(1, static_cast<int>(batch_indices.total()));
CV_Assert(rois.rows == num_rois && rois.cols == 4);
CV_Assert(static_cast<int>(batch_indices.total()) == num_rois);
CV_Assert(Y.dims == 4);
CV_Assert(Y.size[0] == num_rois && Y.size[1] == C && Y.size[2] == output_height && Y.size[3] == output_width);
const bool isMaxMode = (mode_ == RoiAlignMode::MAX);
const float offset = (coord_mode_ == CoordTransformMode::HALF_PIXEL) ? 0.5f : 0.0f;
const bool clampMalformedRoi = (coord_mode_ != CoordTransformMode::HALF_PIXEL);
const int* Xsize = X.size.p;
const int Xdepth = X.depth();
if (Xdepth == CV_32F)
{
if (isMaxMode)
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<float, true>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
else
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<float, false>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
}
else if (Xdepth == CV_64F)
{
if (isMaxMode)
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<double, true>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
else
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<double, false>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
}
else if (Xdepth == CV_16F)
{
if (isMaxMode)
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<hfloat, true>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
else
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<hfloat, false>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
}
else if (Xdepth == CV_16BF)
{
if (isMaxMode)
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<bfloat, true>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
else
parallel_for_(Range(0, num_rois), RoiAlignForwardInvoker<bfloat, false>(X, Y, rois, batch_indices, Xsize, output_height, output_width, sampling_ratio, spatial_scale, offset, clampMalformedRoi));
}
else
CV_Error(Error::StsUnsupportedFormat, "Unsupported X depth");
}
private:
int output_height = 1;
int output_width = 1;
int sampling_ratio = 0;
float spatial_scale = 1.f;
RoiAlignMode mode_ = RoiAlignMode::AVG;
CoordTransformMode coord_mode_ = CoordTransformMode::HALF_PIXEL;
};
Ptr<RoiAlignLayer> RoiAlignLayer::create(const LayerParams& params)
{
return Ptr<RoiAlignLayer>(new RoiAlignLayerImpl(params));
}
}} // namespace cv::dnn
+8
View File
@@ -253,6 +253,7 @@ protected:
void parseRMSNormalization (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseRotaryEmbedding (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseRandomNormalLike (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
void parseRoiAlign (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
// Domain: com.microsoft
// URL: https://github.com/microsoft/onnxruntime/blob/master/docs/ContribOperators.md
@@ -2682,6 +2683,12 @@ void ONNXImporter2::parseAttentionOnnxAi(LayerParams& params, const opencv_onnx:
addLayer(params, node_proto, n_inputs);
}
void ONNXImporter2::parseRoiAlign(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
{
layerParams.type = "RoiAlign";
addLayer(layerParams, node_proto, 3);
}
// Domain: ai.onnx (default)
// URL: https://github.com/onnx/onnx/blob/master/docs/Operators.md
void ONNXImporter2::buildDispatchMap_ONNX_AI()
@@ -2748,6 +2755,7 @@ void ONNXImporter2::buildDispatchMap_ONNX_AI()
dispatch["HammingWindow"] = &ONNXImporter2::parseHammingWindow;
dispatch["GridSample"] = &ONNXImporter2::parseGridSample;
dispatch["AffineGrid"] = &ONNXImporter2::parseAffineGrid;
dispatch["RoiAlign"] = &ONNXImporter2::parseRoiAlign;
dispatch["Upsample"] = &ONNXImporter2::parseUpsample;
dispatch["BitShift"] = &ONNXImporter2::parseBitShift;
dispatch["BitwiseAnd"] = &ONNXImporter2::parseBitwise;
+8 -1
View File
@@ -1580,7 +1580,7 @@ static const TestCase testConformanceConfig[] = {
{"test_rms_normalization_4d_axis_negative_4_expanded", 0, 0},
{"test_rms_normalization_default_axis", 0, 0},
{"test_rms_normalization_default_axis_expanded", 0, 0},
{"test_roialign_mode_max", 0, 0},
{"test_roialign_mode_max", 3, 1},
{"test_rotary_embedding", 0, 0},
{"test_rotary_embedding_3d_input", 0, 0},
{"test_rotary_embedding_3d_input_expanded", 0, 0},
@@ -1892,6 +1892,10 @@ TEST_P(Test_ONNX_conformance, Layer_Test)
{
applyTestTag(CV_TEST_TAG_DNN_SKIP_CPU, CV_TEST_TAG_DNN_SKIP_OPENCV_BACKEND, CV_TEST_TAG_DNN_SKIP_ONNX_CONFORMANCE);
}
if (name == "test_roialign_aligned_false" || name == "test_roialign_aligned_true")
{
default_l1 = std::max(default_l1, 3e-5);
}
if (name == "test_gelu_tanh_1") {
default_l1 = 0.00011; // Expected: (normL1) <= (l1), actual: 0.000101805 vs 1e-05
default_lInf = 0.00016; // Expected: (normInf) <= (lInf), actual: 0.000152707 vs 0.0001
@@ -1964,6 +1968,9 @@ TEST_P(Test_ONNX_conformance, Layer_Test)
default_lInf = 0.0005; // Expected: (normInf) <= (lInf), actual: 0.000455445 vs 0.0001
}
}
if (name == "test_roialign_aligned_false" || name == "test_roialign_aligned_true") {
default_l1 = 3e-5;
}
}
#endif
else
@@ -2268,9 +2268,11 @@ CASE(test_reversesequence_time)
CASE(test_rnn_seq_length)
// no filter
CASE(test_roialign_aligned_false)
// no filter
SKIP;
CASE(test_roialign_aligned_true)
// no filter
SKIP;
CASE(test_roialign_mode_max)
SKIP;
CASE(test_round)
// no filter
CASE(test_scan9_sum)
@@ -709,3 +709,6 @@
"test_layer_normalization_default_axis",
"test_layer_normalization_default_axis_expanded",
"test_layer_normalization_default_axis_expanded_ver18",
"test_roialign_aligned_false",
"test_roialign_aligned_true",
"test_roialign_mode_max",
@@ -459,9 +459,6 @@
"test_rms_normalization_4d_axis_negative_4_expanded",
"test_rms_normalization_default_axis_expanded",
"test_rnn_seq_length", // Issue:: Parser: Can't create layer "onnx_node_output_1!Y_h" of type "RNN" in function 'getLayerInstance'
"test_roialign_aligned_false", // Issue:: Parser: Layer does not exist (RoiAlign)
"test_roialign_aligned_true", // ---- same as above ---
"test_roialign_mode_max",
"test_scan9_sum", // Issue:: Parser: 'Graph' is not supported in function 'getLayerParams'
"test_scan_sum", // ---- same as above ---
"test_sequence_insert_at_back", // Issue:: Parser: typeProto.has_tensor_type() in function 'populateNet'