#29964·opencv

5.x: convertTo() drops saturation for CV_64U/CV_64S/CV_32U sources once the array reaches the vector width

Author: pranayr710Created Sep 16, 2026Updated Sep 17, 2026
Labelsbugcategory: corepriority: high

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)