mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
Merge pull request #29018 from yyyisyyy11:project4_Lunhan_Yan
dnn: add DynamicQuantizeLinear ONNX layer support #29018 ### Pull Request Readiness Checklist - [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 target platform(s) - [x] There is an accuracy test - [ ] There is a performance test ### Description Implements the ONNX `DynamicQuantizeLinear` operator (opset 11) for the OpenCV DNN module. **What it does:** - Adds `QuantizeDynamicLayer` and `DequantizeDynamicLayer` layer classes - Registers layers and adds ONNX importer dispatch for `DynamicQuantizeLinear` - Computes scale and zero-point at runtime from activation min/max - Quantizes FP32 input to int8 (stored as uint8 - 128, matching OpenCV convention) **Framework limitation & workaround:** Due to the single-dtype-per-layer constraint in `LayerData::dtype` (see #29017), the float32 scale output cannot be passed through the CV_8S blob pipeline directly. As a workaround, the scale is encoded as 4 raw bytes in a CV_8S `{1,4}` blob using `memcpy`, and decoded by the downstream `DequantizeDynamic` layer. **Testing:** - Custom accuracy tests reproduce all 3 ONNX conformance test cases: `test_dynamicquantizelinear`, `test_dynamicquantizelinear_max_adjusted`, `test_dynamicquantizelinear_min_adjusted` - Each test verifies: quantized values, scale, zero point, and round-trip dequantize accuracy - All existing quantization regression tests pass (42/42) **Conformance tests:** The 6 conformance tests for `DynamicQuantizeLinear` remain in the parser denylist (they were already denylisted before this PR) because the framework cannot produce mixed-type outputs. The custom tests provide equivalent coverage. ### Files changed - `modules/dnn/include/opencv2/dnn/all_layers.hpp` — layer class declarations - `modules/dnn/src/init.cpp` — layer registration - `modules/dnn/src/onnx/onnx_importer.cpp` — ONNX import dispatch - `modules/dnn/src/int8layers/quantization_utils.cpp` — layer implementations - `modules/dnn/test/test_onnx_importer.cpp` — custom accuracy tests
This commit is contained in:
@@ -485,6 +485,24 @@ CV__DNN_INLINE_NS_BEGIN
|
||||
static Ptr<RequantizeLayer> create(const LayerParams ¶ms);
|
||||
};
|
||||
|
||||
/** @brief Dynamic quantization layer that computes scale and zero point at runtime
|
||||
* from activation min/max values. Outputs quantized data plus scale/zp tensors.
|
||||
*/
|
||||
class CV_EXPORTS QuantizeDynamicLayer : public QuantizeLayer
|
||||
{
|
||||
public:
|
||||
static Ptr<QuantizeDynamicLayer> create(const LayerParams ¶ms);
|
||||
};
|
||||
|
||||
/** @brief Dynamic dequantization layer that reads scale and zero point from
|
||||
* input tensors (produced by QuantizeDynamicLayer) rather than from fixed parameters.
|
||||
*/
|
||||
class CV_EXPORTS DequantizeDynamicLayer : public DequantizeLayer
|
||||
{
|
||||
public:
|
||||
static Ptr<DequantizeDynamicLayer> create(const LayerParams ¶ms);
|
||||
};
|
||||
|
||||
class CV_EXPORTS ConcatLayer : public Layer
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -205,6 +205,8 @@ void initializeLayerFactory()
|
||||
CV_DNN_REGISTER_LAYER_CLASS(Quantize, QuantizeLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(Dequantize, DequantizeLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(Requantize, RequantizeLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(QuantizeDynamic, QuantizeDynamicLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(DequantizeDynamic, DequantizeDynamicLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(ConvolutionInt8, ConvolutionLayerInt8);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(InnerProductInt8, InnerProductLayerInt8);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(PoolingInt8, PoolingLayerInt8);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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 "../precomp.hpp"
|
||||
#include "layers_common.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
static_assert(sizeof(float) == 4, "float must be 4 bytes for int8 encoding");
|
||||
|
||||
namespace cv
|
||||
{
|
||||
namespace dnn
|
||||
{
|
||||
|
||||
// Encode a float value as 4 raw bytes into a CV_8S Mat of shape (1, 4).
|
||||
// This is used to pass float scale through the blob pipeline that only supports CV_8S dtype.
|
||||
static inline void encodeFloatToInt8Mat(float value, Mat& dst)
|
||||
{
|
||||
CV_Assert(dst.type() == CV_8S && dst.total() == 4);
|
||||
std::memcpy(dst.ptr(), &value, sizeof(float));
|
||||
}
|
||||
|
||||
// Decode a float value from a CV_8S Mat of shape (1, 4).
|
||||
static inline float decodeFloatFromInt8Mat(const Mat& src)
|
||||
{
|
||||
CV_Assert(src.type() == CV_8S && src.total() == 4);
|
||||
float value;
|
||||
std::memcpy(&value, src.ptr(), sizeof(float));
|
||||
return value;
|
||||
}
|
||||
|
||||
// Dynamic Quantize: compute scale/zp at runtime from activation min/max
|
||||
class QuantizeDynamicLayerImpl CV_FINAL : public QuantizeDynamicLayer
|
||||
{
|
||||
public:
|
||||
int axis;
|
||||
|
||||
QuantizeDynamicLayerImpl(const LayerParams& params)
|
||||
{
|
||||
axis = params.get<int>("axis", 1);
|
||||
setParamsFrom(params);
|
||||
}
|
||||
|
||||
virtual 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() == 1);
|
||||
outputs.resize(3);
|
||||
outputs[0] = inputs[0]; // quantized INT8 data (same shape as input)
|
||||
outputs[1] = MatShape({1, 4}); // scale: float encoded as 4 x int8 raw bytes
|
||||
outputs[2] = MatShape({1, 1}); // zeropoint (int8, dtype matches CV_8S)
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// ONNX DynamicQuantizeLinear spec: quantize to uint8 range [0, 255]
|
||||
const int qmin = 0, qmax = 255;
|
||||
|
||||
// Dynamically compute scale and zeropoint from activation range
|
||||
double rmin, rmax;
|
||||
cv::minMaxIdx(inputs[0], &rmin, &rmax);
|
||||
rmin = std::min(rmin, 0.0);
|
||||
rmax = std::max(rmax, 0.0);
|
||||
|
||||
float sc = (float)((rmax == rmin) ? 1.0 : (rmax - rmin) / (qmax - qmin));
|
||||
// zp in uint8 space
|
||||
int zp_uint8 = saturate_cast<uchar>(cvRound(-rmin / sc));
|
||||
|
||||
scales.resize(1); scales[0] = sc;
|
||||
zeropoints.resize(1); zeropoints[0] = zp_uint8;
|
||||
|
||||
// output[0]: quantize using uint8 math, then subtract 128 to store as CV_8S
|
||||
// This matches getMatFromTensor's convention: int8_value = uint8_value - 128
|
||||
const float* inp = inputs[0].ptr<float>();
|
||||
int8_t* out = outputs[0].ptr<int8_t>();
|
||||
size_t total = inputs[0].total();
|
||||
for (size_t i = 0; i < total; i++)
|
||||
{
|
||||
// Quantize to uint8 range with round-to-nearest ties-to-even.
|
||||
int y_uint8 = saturate_cast<uchar>(cvRound(inp[i] / sc) + zp_uint8);
|
||||
// Convert to int8 for CV_8S storage
|
||||
out[i] = (int8_t)(y_uint8 - 128);
|
||||
}
|
||||
|
||||
// output[1]: scale encoded as 4 raw bytes in CV_8S blob (avoids dtype mismatch)
|
||||
// output[2]: zeropoint as int8 (CV_8S dtype matches, no encoding needed)
|
||||
encodeFloatToInt8Mat(sc, outputs[1]);
|
||||
outputs[2].at<int8_t>(0) = static_cast<int8_t>(zp_uint8 - 128);
|
||||
}
|
||||
};
|
||||
|
||||
// Dynamic Dequantize: reads scale/zp from input tensors (produced by QuantizeDynamic)
|
||||
class DequantizeDynamicLayerImpl CV_FINAL : public DequantizeDynamicLayer
|
||||
{
|
||||
public:
|
||||
DequantizeDynamicLayerImpl(const LayerParams& params)
|
||||
{
|
||||
setParamsFrom(params);
|
||||
}
|
||||
|
||||
virtual 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
|
||||
{
|
||||
// inputs[0] = INT8 data, inputs[1] = encoded scale (1x4 bytes in CV_8S), inputs[2] = zeropoint (1x1 int8)
|
||||
CV_Check(inputs.size(), inputs.size() >= 1 && inputs.size() <= 3,
|
||||
"Number of inputs must be between 1 and 3 inclusive.");
|
||||
outputs.assign(1, inputs[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
float sc = 1.0f;
|
||||
int zp_int8 = -128; // default corresponds to zp_uint8=0 in stored int8 space (uint8_value - 128)
|
||||
|
||||
if (inputs.size() > 1)
|
||||
{
|
||||
// Decode scale from 4 raw bytes in CV_8S blob
|
||||
sc = decodeFloatFromInt8Mat(inputs[1]);
|
||||
if (inputs.size() > 2)
|
||||
zp_int8 = static_cast<int>(inputs[2].at<int8_t>(0));
|
||||
}
|
||||
else if (!scales.empty())
|
||||
{
|
||||
sc = scales[0];
|
||||
// zeropoints stores uint8 value; convert to int8 space
|
||||
zp_int8 = zeropoints.empty() ? -128 : (zeropoints[0] - 128);
|
||||
}
|
||||
|
||||
// Dequantize: y_int8 = uint8_value - 128, zp_int8 = zp_uint8 - 128
|
||||
// x = (y_int8 - zp_int8) * sc
|
||||
inputs[0].convertTo(outputs[0], CV_32F, sc, -sc * zp_int8);
|
||||
}
|
||||
};
|
||||
|
||||
Ptr<QuantizeDynamicLayer> QuantizeDynamicLayer::create(const LayerParams& params)
|
||||
{
|
||||
return Ptr<QuantizeDynamicLayer>(new QuantizeDynamicLayerImpl(params));
|
||||
}
|
||||
|
||||
Ptr<DequantizeDynamicLayer> DequantizeDynamicLayer::create(const LayerParams& params)
|
||||
{
|
||||
return Ptr<DequantizeDynamicLayer>(new DequantizeDynamicLayerImpl(params));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
@@ -119,6 +120,8 @@ protected:
|
||||
std::map<std::string, Mat> constBlobs;
|
||||
std::map<std::string, TensorInfo> constBlobsExtraInfo;
|
||||
|
||||
std::set<std::string> dynamicQuantScaleZpOutputs; // Byte-encoded scale/zp tensors produced by DynamicQuantizeLinear.
|
||||
|
||||
std::map<std::string, MatShape> outShapes; // List of internal blobs shapes.
|
||||
bool hasDynamicShapes; // Whether the model has inputs with dynamic shapes
|
||||
typedef std::map<std::string, MatShape>::iterator IterShape_t;
|
||||
@@ -202,6 +205,7 @@ private:
|
||||
// Domain: com.microsoft
|
||||
// URL: https://github.com/microsoft/onnxruntime/blob/master/docs/ContribOperators.md
|
||||
void parseQuantDequant (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseDynamicQuantizeLinear (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseQConv (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseQMatMul (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseQEltwise (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
@@ -727,6 +731,7 @@ static bool ifInt8Output(const String& layerType)
|
||||
// ai.onnx opset 15
|
||||
static std::vector<String> input8output8List = {
|
||||
"QuantizeLinear",
|
||||
"DynamicQuantizeLinear",
|
||||
"QLinearAdd",
|
||||
"QLinearMul",
|
||||
"QLinearAveragePool",
|
||||
@@ -3345,6 +3350,11 @@ void ONNXImporter::parseQuantDequant(LayerParams& layerParams, const opencv_onnx
|
||||
|
||||
// If scale is not defined as a constant blob, it is considered an external input.
|
||||
if(constBlobs.find(node_proto.input(1)) == constBlobs.end()){
|
||||
// Scale produced by DynamicQuantizeLinear is byte-encoded (1x4 CV_8S); route to
|
||||
// DequantizeDynamic which knows how to decode it.
|
||||
if (layerParams.type == "Dequantize" &&
|
||||
dynamicQuantScaleZpOutputs.count(node_proto.input(1)))
|
||||
layerParams.type = "DequantizeDynamic";
|
||||
addLayer(layerParams, node_proto);
|
||||
return;
|
||||
}
|
||||
@@ -3402,6 +3412,21 @@ void ONNXImporter::parseQuantDequant(LayerParams& layerParams, const opencv_onnx
|
||||
addLayer(layerParams, node_proto);
|
||||
}
|
||||
|
||||
void ONNXImporter::parseDynamicQuantizeLinear(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
|
||||
{
|
||||
// DynamicQuantizeLinear (opset 11): 1 input (FP32 data), 3 outputs (UINT8/INT8 data, scale, zeropoint)
|
||||
CV_Assert(node_proto.input_size() == 1);
|
||||
layerParams.type = "QuantizeDynamic";
|
||||
layerParams.set("depth", CV_8S);
|
||||
// Outputs 1 (y_scale) and 2 (y_zero_point) use a byte-encoded convention (see
|
||||
// quantization_utils.cpp); record their names so consumers can be routed properly.
|
||||
if (node_proto.output_size() > 1 && !node_proto.output(1).empty())
|
||||
dynamicQuantScaleZpOutputs.insert(node_proto.output(1));
|
||||
if (node_proto.output_size() > 2 && !node_proto.output(2).empty())
|
||||
dynamicQuantScaleZpOutputs.insert(node_proto.output(2));
|
||||
addLayer(layerParams, node_proto);
|
||||
}
|
||||
|
||||
void ONNXImporter::parseQConv(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto_)
|
||||
{
|
||||
opencv_onnx::NodeProto node_proto = node_proto_;
|
||||
@@ -4054,6 +4079,7 @@ void ONNXImporter::buildDispatchMap_ONNX_AI(int opset_version)
|
||||
|
||||
// ai.onnx: opset 10+
|
||||
dispatch["QuantizeLinear"] = dispatch["DequantizeLinear"] = &ONNXImporter::parseQuantDequant;
|
||||
dispatch["DynamicQuantizeLinear"] = &ONNXImporter::parseDynamicQuantizeLinear;
|
||||
dispatch["QLinearConv"] = &ONNXImporter::parseQConv;
|
||||
dispatch["QLinearMatMul"] = &ONNXImporter::parseQMatMul;
|
||||
|
||||
|
||||
@@ -599,17 +599,17 @@ CASE(test_dropout_default_ratio)
|
||||
CASE(test_dropout_random_old)
|
||||
// no filter
|
||||
CASE(test_dynamicquantizelinear)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_dynamicquantizelinear_expanded)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_dynamicquantizelinear_max_adjusted)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_dynamicquantizelinear_max_adjusted_expanded)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_dynamicquantizelinear_min_adjusted)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_dynamicquantizelinear_min_adjusted_expanded)
|
||||
// no filter
|
||||
SKIP;
|
||||
CASE(test_edge_pad)
|
||||
// no filter
|
||||
CASE(test_einsum_batch_diagonal)
|
||||
|
||||
@@ -2129,6 +2129,189 @@ TEST_P(Test_ONNX_layers, Quantized_Constant)
|
||||
testONNXModels("quantized_constant", npy, 0.002, 0.008);
|
||||
}
|
||||
|
||||
// Custom accuracy tests for DynamicQuantizeLinear.
|
||||
// Conformance tests are denylisted because the framework's single-dtype-per-layer
|
||||
// constraint prevents correct mixed-type (uint8 + float32) output blobs.
|
||||
// These tests reproduce the exact ONNX conformance test cases:
|
||||
// test_dynamicquantizelinear, test_dynamicquantizelinear_max_adjusted,
|
||||
// test_dynamicquantizelinear_min_adjusted
|
||||
// using direct layer invocation to verify quantize + dequantize correctness.
|
||||
|
||||
// Helper: run QuantizeDynamic forward and verify outputs against ONNX spec.
|
||||
static void testDynamicQuantizeLinear(const float* inputData, int rows, int cols,
|
||||
const uint8_t* expected_uint8, int numElements,
|
||||
float expected_scale, uint8_t expected_zp_uint8)
|
||||
{
|
||||
LayerParams qp;
|
||||
qp.name = "test_quantize_dynamic";
|
||||
qp.type = "QuantizeDynamic";
|
||||
qp.set("depth", CV_8S);
|
||||
|
||||
Ptr<Layer> qlayer = LayerFactory::createLayerInstance("QuantizeDynamic", qp);
|
||||
ASSERT_FALSE(qlayer.empty());
|
||||
|
||||
Mat input(rows, cols, CV_32F);
|
||||
memcpy(input.ptr<float>(), inputData, numElements * sizeof(float));
|
||||
|
||||
std::vector<Mat> inputs = {input};
|
||||
std::vector<Mat> outputs = {
|
||||
Mat(rows, cols, CV_8S),
|
||||
Mat(1, 4, CV_8S), // scale: float encoded as 4 x int8 raw bytes
|
||||
Mat(1, 1, CV_8S) // zeropoint
|
||||
};
|
||||
std::vector<Mat> internals;
|
||||
|
||||
qlayer->forward(inputs, outputs, internals);
|
||||
|
||||
// Verify scale (decode from 4 raw bytes)
|
||||
float decoded_sc;
|
||||
memcpy(&decoded_sc, outputs[1].ptr(), sizeof(float));
|
||||
EXPECT_NEAR(decoded_sc, expected_scale, 1e-6f) << "Scale mismatch";
|
||||
|
||||
// Verify zero point (stored as int8 = uint8_value - 128)
|
||||
int8_t stored_zp = outputs[2].at<int8_t>(0);
|
||||
int recovered_zp_uint8 = (int)stored_zp + 128;
|
||||
EXPECT_EQ(recovered_zp_uint8, (int)expected_zp_uint8) << "Zero point mismatch";
|
||||
|
||||
// Verify quantized values (compare in uint8 space)
|
||||
const int8_t* qdata = outputs[0].ptr<int8_t>();
|
||||
for (int i = 0; i < numElements; i++)
|
||||
{
|
||||
int actual_uint8 = (int)qdata[i] + 128;
|
||||
EXPECT_EQ(actual_uint8, (int)expected_uint8[i])
|
||||
<< "Quantized value mismatch at index " << i
|
||||
<< " (expected uint8=" << (int)expected_uint8[i]
|
||||
<< ", got uint8=" << actual_uint8 << ")";
|
||||
}
|
||||
|
||||
// Verify round-trip: QuantizeDynamic -> DequantizeDynamic
|
||||
{
|
||||
LayerParams dp;
|
||||
dp.name = "test_dequantize_dynamic";
|
||||
dp.type = "DequantizeDynamic";
|
||||
|
||||
Ptr<Layer> dlayer = LayerFactory::createLayerInstance("DequantizeDynamic", dp);
|
||||
ASSERT_FALSE(dlayer.empty());
|
||||
|
||||
std::vector<Mat> dq_inputs = {outputs[0], outputs[1], outputs[2]};
|
||||
std::vector<Mat> dq_outputs = {Mat(rows, cols, CV_32F)};
|
||||
std::vector<Mat> dq_internals;
|
||||
|
||||
dlayer->forward(dq_inputs, dq_outputs, dq_internals);
|
||||
|
||||
const float* outData = dq_outputs[0].ptr<float>();
|
||||
for (int i = 0; i < numElements; i++)
|
||||
{
|
||||
EXPECT_NEAR(outData[i], inputData[i], 0.5f * expected_scale + 1e-6f)
|
||||
<< "Round-trip dequantized value mismatch at index " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test_dynamicquantizelinear: mixed positive/negative input
|
||||
// Input: [0, 2, -3, -2.5, 1.34, 0.5]
|
||||
// Expected: scale=0.0196078438, zp=153
|
||||
TEST_P(Test_ONNX_layers, DynamicQuantizeLinear_Basic)
|
||||
{
|
||||
if (backend != DNN_BACKEND_OPENCV || target != DNN_TARGET_CPU)
|
||||
return;
|
||||
|
||||
float X[] = {0.f, 2.f, -3.f, -2.5f, 1.34f, 0.5f};
|
||||
float x_min = -3.f, x_max = 2.f;
|
||||
float scale = (x_max - x_min) / 255.f;
|
||||
uint8_t zp = saturate_cast<uchar>(cvRound(-x_min / scale)); // 153
|
||||
|
||||
uint8_t expected_Y[6];
|
||||
for (int i = 0; i < 6; i++)
|
||||
expected_Y[i] = saturate_cast<uchar>(cvRound(X[i] / scale) + (int)zp);
|
||||
|
||||
SCOPED_TRACE("test_dynamicquantizelinear");
|
||||
testDynamicQuantizeLinear(X, 1, 6, expected_Y, 6, scale, zp);
|
||||
}
|
||||
|
||||
// test_dynamicquantizelinear_max_adjusted: all negative input, max adjusted to 0
|
||||
// Input: [-1.0, -2.1, -1.3, -2.5, -3.34, -4.0]
|
||||
// Expected: scale=0.0156862754, zp=255
|
||||
TEST_P(Test_ONNX_layers, DynamicQuantizeLinear_MaxAdjusted)
|
||||
{
|
||||
if (backend != DNN_BACKEND_OPENCV || target != DNN_TARGET_CPU)
|
||||
return;
|
||||
|
||||
float X[] = {-1.0f, -2.1f, -1.3f, -2.5f, -3.34f, -4.0f};
|
||||
float x_min = -4.f, x_max = 0.f; // max adjusted to 0
|
||||
float scale = (x_max - x_min) / 255.f;
|
||||
uint8_t zp = saturate_cast<uchar>(cvRound(-x_min / scale)); // 255
|
||||
|
||||
uint8_t expected_Y[6];
|
||||
for (int i = 0; i < 6; i++)
|
||||
expected_Y[i] = saturate_cast<uchar>(cvRound(X[i] / scale) + (int)zp);
|
||||
|
||||
SCOPED_TRACE("test_dynamicquantizelinear_max_adjusted");
|
||||
testDynamicQuantizeLinear(X, 1, 6, expected_Y, 6, scale, zp);
|
||||
}
|
||||
|
||||
// test_dynamicquantizelinear_min_adjusted: all positive input (3x4), min adjusted to 0
|
||||
// Input: [[1, 2.1, 1.3, 2.5], [3.34, 4.0, 1.5, 2.6], [3.9, 4.0, 3.0, 2.345]]
|
||||
// Expected: scale=0.0156862754, zp=0
|
||||
TEST_P(Test_ONNX_layers, DynamicQuantizeLinear_MinAdjusted)
|
||||
{
|
||||
if (backend != DNN_BACKEND_OPENCV || target != DNN_TARGET_CPU)
|
||||
return;
|
||||
|
||||
float X[] = {1.f, 2.1f, 1.3f, 2.5f, 3.34f, 4.0f, 1.5f, 2.6f, 3.9f, 4.0f, 3.0f, 2.345f};
|
||||
float x_min = 0.f, x_max = 4.f; // min adjusted to 0
|
||||
float scale = (x_max - x_min) / 255.f;
|
||||
uint8_t zp = saturate_cast<uchar>(cvRound(-x_min / scale)); // 0
|
||||
|
||||
uint8_t expected_Y[12];
|
||||
for (int i = 0; i < 12; i++)
|
||||
expected_Y[i] = saturate_cast<uchar>(cvRound(X[i] / scale) + (int)zp);
|
||||
|
||||
SCOPED_TRACE("test_dynamicquantizelinear_min_adjusted");
|
||||
testDynamicQuantizeLinear(X, 3, 4, expected_Y, 12, scale, zp);
|
||||
}
|
||||
|
||||
// Net-level round trip: QuantizeDynamic -> DequantizeDynamic wired through a Net,
|
||||
// mirroring how the importer routes DynamicQuantizeLinear -> DequantizeLinear
|
||||
// (the dequantize node consumes the byte-encoded scale/zp output tensors).
|
||||
TEST_P(Test_ONNX_layers, DynamicQuantizeLinear_DequantizeRoundTrip)
|
||||
{
|
||||
if (backend != DNN_BACKEND_OPENCV || target != DNN_TARGET_CPU)
|
||||
return;
|
||||
|
||||
Net net;
|
||||
|
||||
LayerParams qp;
|
||||
qp.name = "quant";
|
||||
qp.type = "QuantizeDynamic";
|
||||
qp.set("depth", CV_8S);
|
||||
int qid = net.addLayerToPrev(qp.name, qp.type, CV_8S, qp);
|
||||
|
||||
LayerParams dp;
|
||||
dp.name = "dequant";
|
||||
dp.type = "DequantizeDynamic";
|
||||
dp.set("depth", CV_32F);
|
||||
int did = net.addLayer(dp.name, dp.type, CV_32F, dp);
|
||||
net.connect(qid, 0, did, 0); // INT8 data
|
||||
net.connect(qid, 1, did, 1); // byte-encoded scale
|
||||
net.connect(qid, 2, did, 2); // zeropoint
|
||||
|
||||
float X[] = {0.f, 2.f, -3.f, -2.5f, 1.34f, 0.5f};
|
||||
Mat input(1, 6, CV_32F, X);
|
||||
float scale = (2.f - (-3.f)) / 255.f;
|
||||
|
||||
net.setInput(input);
|
||||
net.setPreferableBackend(DNN_BACKEND_OPENCV);
|
||||
Mat out = net.forward(dp.name);
|
||||
|
||||
ASSERT_EQ(out.total(), (size_t)6);
|
||||
const float* outData = out.ptr<float>();
|
||||
for (int i = 0; i < 6; i++)
|
||||
EXPECT_NEAR(outData[i], X[i], 0.5f * scale + 1e-6f)
|
||||
<< "Round-trip dequantized value mismatch at index " << i;
|
||||
}
|
||||
|
||||
|
||||
TEST_P(Test_ONNX_layers, OutputRegistration)
|
||||
{
|
||||
testONNXModels("output_registration", npy, 0, 0, false, true, 2);
|
||||
|
||||
Reference in New Issue
Block a user