5.x: convertTo() drops saturation for CV_64U/CV_64S/CV_32U sources once the array reaches the vector width
System Information
OpenCV version: 5.1.0-dev (5.x @ d28b360004) Platform: Windows 11 x64, MinGW-w64 g++ 15.2.0, Release, AVX2 The defect is in the vectorized conversion helpers, so it should reproduce on any SIMD backend.
Detailed description
Mat::convertTo() gives a different answer for the same value depending on how many elements the
array has. Narrowing a CV_64U, CV_64S or CV_32U source to a smaller integer type saturates
correctly on short arrays and wraps once the array is long enough to enter the vector loop.
Mat src(1, n, CV_64U, Scalar(4294967295));
src.convertTo(dst, CV_8U); // n < 16 -> 255 n >= 16 -> 0
src.convertTo(dst, CV_16U); // n < 16 -> 65535 n >= 16 -> 0
Root cause is in modules/core/src/convert.hpp. The 64-bit source helpers narrow through a 32-bit
intermediate with v_pack():
static inline void vx_load_pair_as(const int64_t* ptr, v_int32& a, v_int32& b)
{
const int int64_nlanes = VTraits<v_uint64>::vlanes();
a = v_pack(vx_load(ptr), vx_load(ptr + int64_nlanes));
...
At the 8/16/32-bit widths v_pack saturates, but the 64->32 forms are defined with static_cast
rather than saturate_cast (OPENCV_HAL_IMPL_C_PACK in intrin_cpp.hpp), because no backend
offers a saturating 64->32 pack. So the value wraps first, and the saturating narrow that follows
then clamps the wrapped value - landing at the wrong end of the range (4294967295 -> -1 -> 0 for
CV_8U, instead of 255).
vx_load_pair_as(const unsigned*, v_int32&, v_int32&) has the same problem one width down: it
reinterprets a CV_32U source as signed without clamping, so anything above INT32_MAX goes
negative before the narrow.
The unsigned-destination helpers already clamp negatives (v_and(ia, v_gt(ia, z))), which
suggests the magnitude case was simply overlooked rather than intended.
Steps to reproduce
#include <opencv2/core.hpp>
#include <iostream>
int main()
{
for (int n : { 1, 8, 15, 16, 17, 64 })
{
cv::Mat src(1, n, CV_64U, cv::Scalar(4294967295.0));
cv::Mat d8, d16;
src.convertTo(d8, CV_8U);
src.convertTo(d16, CV_16U);
cv::Mat a, b; d8.convertTo(a, CV_64F); d16.convertTo(b, CV_64F);
std::cout << "n=" << n
<< " -> CV_8U: " << a.at<double>(0,0)
<< " -> CV_16U: " << b.at<double>(0,0) << "\n";
}
return 0;
}
Output:
n=1 -> CV_8U: 255 -> CV_16U: 65535
n=8 -> CV_8U: 255 -> CV_16U: 65535
n=15 -> CV_8U: 255 -> CV_16U: 65535
n=16 -> CV_8U: 0 -> CV_16U: 0
n=17 -> CV_8U: 0 -> CV_16U: 0
n=64 -> CV_8U: 0 -> CV_16U: 0
Expected: 255 and 65535 for every length.
Affected pairs found by comparing the vector path against the scalar path across every depth
combination: CV_64U and CV_64S to CV_8U/CV_8S/CV_16U/CV_16S/CV_32S, and CV_32U to
CV_8S/CV_16S - 12 combinations.
A separate observation
The same vector-vs-scalar comparison also reports divergences for float sources converted to
CV_32U/CV_64U/CV_64S, and for conversions into CV_32U generally. Those have a different
root cause (float-to-integer saturation for the new wide and unsigned destination types) and are
not covered by this report; I have not investigated them far enough to say what the correct
behaviour should be in each case.
Issue submission checklist
- I report the issue, it's not a question
- I checked the problem with documentation, FAQ, open issues, forum.opencv.org, Stack Overflow, etc and have not found any solution
- I updated to the latest OpenCV version and the issue is still there
- There is reproducer code and related data files (videos, images, onnx, etc)
Source: opencv/opencv