From 25ed5d4c0d1eec67bdc94add882e5b6a93554b1a Mon Sep 17 00:00:00 2001 From: vrooomy Date: Mon, 3 Aug 2026 12:26:19 +0530 Subject: [PATCH] map 64bit int anf bfloat16 --- .../misc/python/test/test_onnx_conformance.py | 44 +------------- .../python/test/test_feature_homography.py | 8 ++- .../misc/python/test/test_objdetect_aruco.py | 3 +- modules/python/src2/cv2_convert.cpp | 45 +++++++-------- modules/python/src2/cv2_numpy.cpp | 8 +++ modules/python/src2/cv2_util.cpp | 57 +++++++++++++++---- modules/python/test/test_houghcircles.py | 2 +- modules/python/test/test_misc.py | 35 +++++++++++- modules/python/test/test_squares.py | 8 ++- modules/python/test/tests_common.py | 5 +- modules/python/test/tst_scene_render.py | 2 +- 11 files changed, 129 insertions(+), 88 deletions(-) diff --git a/modules/dnn/misc/python/test/test_onnx_conformance.py b/modules/dnn/misc/python/test/test_onnx_conformance.py index ff87815703..d69ad6e167 100644 --- a/modules/dnn/misc/python/test/test_onnx_conformance.py +++ b/modules/dnn/misc/python/test/test_onnx_conformance.py @@ -632,48 +632,10 @@ TOLERANCE_OVERRIDES = { "test_roialign_aligned_true": (3e-05, 0.0001), } -UINT64_SETINPUT_REASON = ( - "cv2.dnn.Net.setInput() does not accept a uint64 ndarray (\"Overload " - "resolution failed: blob data type\"); readTensorFromONNX decodes the " - "input correctly (verified byte-for-byte against the onnx package), so " - "this is a Python-binding overload gap, not a data or C++ Mat/Net issue." -) -BFLOAT16_READ_REASON = ( - "cv2.dnn.readTensorFromONNX does not support the ONNX BFLOAT16 element " - "type: it returns a uint64-dtyped array of nonsensical values (raw bytes " - "misinterpreted) instead of the actual tensor content, for both BFLOAT16 " - "inputs and outputs." -) - +# Cases the C++ suite passes but that fail only through the Python bindings. +# Not a port of the C++ denylist; empty is the expected state. KNOWN_SKIPS = { - "test_add_uint64": UINT64_SETINPUT_REASON, - "test_bitshift_left_uint64": UINT64_SETINPUT_REASON, - "test_bitshift_right_uint64": UINT64_SETINPUT_REASON, - "test_bitwise_and_ui64_bcast_3v1d": UINT64_SETINPUT_REASON, - "test_bitwise_or_ui64_bcast_3v1d": UINT64_SETINPUT_REASON, - "test_bitwise_xor_ui64_bcast_3v1d": UINT64_SETINPUT_REASON, - "test_cast_BFLOAT16_to_FLOAT": BFLOAT16_READ_REASON, - "test_cast_FLOAT_to_BFLOAT16": BFLOAT16_READ_REASON, - "test_castlike_BFLOAT16_to_FLOAT": BFLOAT16_READ_REASON, - "test_castlike_BFLOAT16_to_FLOAT_expanded": BFLOAT16_READ_REASON, - "test_castlike_FLOAT_to_BFLOAT16": BFLOAT16_READ_REASON, - "test_castlike_FLOAT_to_BFLOAT16_expanded": BFLOAT16_READ_REASON, - "test_div_uint64": UINT64_SETINPUT_REASON, - "test_equal_uint64": UINT64_SETINPUT_REASON, - "test_greater_equal_uint64": UINT64_SETINPUT_REASON, - "test_greater_equal_uint64_expanded": UINT64_SETINPUT_REASON, - "test_greater_uint64": UINT64_SETINPUT_REASON, - "test_less_equal_uint64": UINT64_SETINPUT_REASON, - "test_less_equal_uint64_expanded": UINT64_SETINPUT_REASON, - "test_less_uint64": UINT64_SETINPUT_REASON, - "test_max_uint64": UINT64_SETINPUT_REASON, - "test_min_uint64": UINT64_SETINPUT_REASON, - "test_mod_uint64": UINT64_SETINPUT_REASON, - "test_mul_uint64": UINT64_SETINPUT_REASON, - "test_pow_types_float32_uint64": UINT64_SETINPUT_REASON, - "test_range_bfloat16_type_positive_delta": BFLOAT16_READ_REASON, - "test_sub_uint64": UINT64_SETINPUT_REASON, - "test_top_k_uint64": UINT64_SETINPUT_REASON, + # "test_name": "reason", } L1_DEFAULT = 1e-5 diff --git a/modules/features/misc/python/test/test_feature_homography.py b/modules/features/misc/python/test/test_feature_homography.py index 4308da75b4..22dd3e08f0 100755 --- a/modules/features/misc/python/test/test_feature_homography.py +++ b/modules/features/misc/python/test/test_feature_homography.py @@ -26,10 +26,12 @@ from tst_scene_render import TestSceneRender def intersectionRate(s1, s2): x1, y1, x2, y2 = s1 - s1 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]]) + # dtype is explicit: the geometry functions accept only CV_32S/CV_32F points. + s1 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]], dtype=np.int32) + s2 = np.array(s2, dtype=np.int32) - area, _intersection = cv.intersectConvexConvex(s1, np.array(s2)) - return 2 * area / (cv.contourArea(s1) + cv.contourArea(np.array(s2))) + area, _intersection = cv.intersectConvexConvex(s1, s2) + return 2 * area / (cv.contourArea(s1) + cv.contourArea(s2)) from tests_common import NewOpenCVTests diff --git a/modules/objdetect/misc/python/test/test_objdetect_aruco.py b/modules/objdetect/misc/python/test/test_objdetect_aruco.py index d2aa8b87a8..6490d620c7 100644 --- a/modules/objdetect/misc/python/test/test_objdetect_aruco.py +++ b/modules/objdetect/misc/python/test/test_objdetect_aruco.py @@ -74,7 +74,8 @@ def projectChessboard(squaresX, squaresY, squareSize, imageSize, cameraMatrix, r squareCorners[0] + [0, squareSize, 0])) projectedCorners, _ = cv.projectPoints(squareCorners, rvec, tvec, cameraMatrix, distCoeffs) - projectedCorners = projectedCorners.astype(np.int64) + # fillPoly() requires CV_32S point coordinates + projectedCorners = projectedCorners.astype(np.int32) projectedCorners = projectedCorners.reshape(1, 4, 2) img = cv.fillPoly(img, [projectedCorners], 0) diff --git a/modules/python/src2/cv2_convert.cpp b/modules/python/src2/cv2_convert.cpp index 8d41c2c2a4..04087fb3cb 100644 --- a/modules/python/src2/cv2_convert.cpp +++ b/modules/python/src2/cv2_convert.cpp @@ -127,25 +127,20 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) return false; } - bool needcopy = false, needcast = false; - int typenum = PyArray_TYPE(oarr), new_typenum = typenum; + bool needcopy = false; + int typenum = PyArray_TYPE(oarr); int type = numpyTypeToCvDepth(typenum); if( type < 0 ) { - if( typenum == NPY_INT64 || typenum == NPY_UINT64 || typenum == NPY_LONG ) - { - needcopy = needcast = true; - new_typenum = NPY_INT; - type = CV_32S; - } - else - { - const std::string dtype_name = getArrayTypeName(oarr); - failmsg("%s data type = %s is not supported", info.name, - dtype_name.c_str()); - return false; - } + // 64-bit integers used to be force-cast to CV_32S here, which silently + // truncated any value outside the int32 range. They now map to + // CV_64S/CV_64U in numpyTypeToCvDepth(), so reaching this point means + // the dtype genuinely has no cv::Mat equivalent. + const std::string dtype_name = getArrayTypeName(oarr); + failmsg("%s data type = %s is not supported", info.name, + dtype_name.c_str()); + return false; } #ifndef CV_MAX_DIM @@ -220,14 +215,8 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) return false; } - if( needcast ) { - o = PyArray_Cast(oarr, new_typenum); - oarr = (PyArrayObject*) o; - } - else { - oarr = PyArray_GETCONTIGUOUS(oarr); - o = (PyObject*) oarr; - } + oarr = PyArray_GETCONTIGUOUS(oarr); + o = (PyObject*) oarr; _strides = PyArray_STRIDES(oarr); } @@ -315,6 +304,16 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) template<> PyObject* pyopencv_from(const cv::Mat& m) { + // NumPy has no bfloat16 dtype, so CV_16BF is widened to float32 (lossless). + // The values must actually be converted, not just relabelled: cvDepthToNumpyType() + // reports NPY_FLOAT for CV_16BF, and handing a 2-byte-per-element buffer to the + // NumPy allocator under a 4-byte dtype would misinterpret the payload. + if( m.depth() == CV_16BF ) + { + cv::Mat m32f; + ERRWRAP2(m.convertTo(m32f, CV_32F)); + return pyopencv_from(m32f); // m32f is CV_32F, so this recurses at most once + } if( m.empty() ) { // empty() also catches a live buffer with a zero-length dim: return an empty array, not None. diff --git a/modules/python/src2/cv2_numpy.cpp b/modules/python/src2/cv2_numpy.cpp index dfd94a3f92..4ba944c374 100644 --- a/modules/python/src2/cv2_numpy.cpp +++ b/modules/python/src2/cv2_numpy.cpp @@ -34,6 +34,14 @@ UMatData* NumpyAllocator::allocate(int dims0, const int* sizes, int type, void* int depth = CV_MAT_DEPTH(type); int cn = CV_MAT_CN(type); + // cvDepthToNumpyType() widens CV_16BF to NPY_FLOAT for export, which is only + // valid when the values are converted (see pyopencv_from). Backing a CV_16BF + // Mat with a float32 buffer here would instead pair a 2-byte element step with + // 4-byte NumPy strides and silently corrupt the data, so refuse it outright. + if( depth == CV_16BF ) + CV_Error(Error::StsNotImplemented, + "CV_16BF (bfloat16) arrays cannot be allocated through the NumPy allocator: " + "NumPy has no bfloat16 dtype"); int typenum = cvDepthToNumpyType(depth); int i, dims = dims0; cv::AutoBuffer _sizes(dims + 1); diff --git a/modules/python/src2/cv2_util.cpp b/modules/python/src2/cv2_util.cpp index 625d071612..9ad875e104 100644 --- a/modules/python/src2/cv2_util.cpp +++ b/modules/python/src2/cv2_util.cpp @@ -8,21 +8,56 @@ cv::TLSData > conversionErrorsTLS; int cvDepthToNumpyType(int depth) { - const int f = (int)(sizeof(size_t)/8); - return depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE : - depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT : - depth == CV_32U ? NPY_UINT32 : depth == CV_32S ? NPY_INT32 : depth == CV_64S ? NPY_INT64 : - depth == CV_32F ? NPY_FLOAT : depth == CV_64F ? NPY_DOUBLE : depth == CV_16F ? NPY_HALF : - depth == CV_Bool ? NPY_BOOL : f*NPY_ULONGLONG + (f^1)*NPY_UINT; + switch (depth) + { + case CV_8U: return NPY_UBYTE; + case CV_8S: return NPY_BYTE; + case CV_16U: return NPY_USHORT; + case CV_16S: return NPY_SHORT; + case CV_32U: return NPY_UINT32; + case CV_32S: return NPY_INT32; + case CV_64U: return NPY_UINT64; + case CV_64S: return NPY_INT64; + case CV_32F: return NPY_FLOAT; + case CV_64F: return NPY_DOUBLE; + case CV_16F: return NPY_HALF; + // NumPy has no bfloat16 dtype, so CV_16BF is exported as float32 (a lossless + // widening). pyopencv_from() performs the value conversion; without it the + // 2-byte payload would be reinterpreted as 4-byte elements. + case CV_16BF: return NPY_FLOAT; + case CV_Bool: return NPY_BOOL; + default: + // Deliberately an error rather than a fallback: silently mapping an + // unknown depth to some default dtype mislabels the payload. + CV_Error(cv::Error::StsNotImplemented, + cv::format("Mat depth %d has no corresponding NumPy dtype", depth)); + } } int numpyTypeToCvDepth(int typenum) { - return typenum == NPY_UBYTE ? CV_8U : typenum == NPY_BYTE ? CV_8S : - typenum == NPY_USHORT ? CV_16U : typenum == NPY_SHORT ? CV_16S : - typenum == NPY_INT ? CV_32S : typenum == NPY_UINT32 ? CV_32U : typenum == NPY_INT32 ? CV_32S : - typenum == NPY_HALF ? CV_16F : typenum == NPY_FLOAT ? CV_32F : typenum == NPY_DOUBLE ? CV_64F : - typenum == NPY_BOOL ? CV_Bool : -1; + // Only canonical NPY_* values may appear as case labels: the fixed-width + // aliases (NPY_INT32, NPY_INT64, ...) expand to these and would collide. + switch (typenum) + { + case NPY_UBYTE: return CV_8U; + case NPY_BYTE: return CV_8S; + case NPY_USHORT: return CV_16U; + case NPY_SHORT: return CV_16S; + case NPY_UINT: return CV_32U; + case NPY_INT: return CV_32S; + case NPY_ULONGLONG: return CV_64U; + case NPY_LONGLONG: return CV_64S; + // 'long' is 64-bit on LP64 (Linux/macOS) but 32-bit on LLP64 (Windows), + // so this must be decided by size rather than by name. + case NPY_ULONG: return NPY_SIZEOF_LONG == 8 ? CV_64U : CV_32U; + case NPY_LONG: return NPY_SIZEOF_LONG == 8 ? CV_64S : CV_32S; + case NPY_HALF: return CV_16F; + case NPY_FLOAT: return CV_32F; + case NPY_DOUBLE: return CV_64F; + case NPY_BOOL: return CV_Bool; + default: return -1; + } } using namespace cv; diff --git a/modules/python/test/test_houghcircles.py b/modules/python/test/test_houghcircles.py index 942e2c8e34..46bcae75a4 100644 --- a/modules/python/test/test_houghcircles.py +++ b/modules/python/test/test_houghcircles.py @@ -23,7 +23,7 @@ def circleApproximation(circle): contour.append(([circle[0] + circle[2]*cos(i*dPhi), circle[1] + circle[2]*sin(i*dPhi)])) - return np.array(contour).astype(int) + return np.array(contour).astype(np.int32) def convContoursIntersectiponRate(c1, c2): diff --git a/modules/python/test/test_misc.py b/modules/python/test/test_misc.py index cd0c117f92..2d5b1f07a9 100644 --- a/modules/python/test/test_misc.py +++ b/modules/python/test/test_misc.py @@ -263,7 +263,9 @@ class Arguments(NewOpenCVTests): self.assertEqual(res2_1, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=2 dims(-1)=2 size(-1)=1x2 type(-1)=CV_64FC1") res2_2 = cv.utils.dumpInputArray(1.5) # Scalar(1.5, 1.5, 1.5, 1.5) self.assertEqual(res2_2, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=4 dims(-1)=2 size(-1)=1x4 type(-1)=CV_64FC1") - a = np.array([[1, 2], [3, 4], [5, 6]]) + # dtype is explicit: NumPy's default integer type is platform/version + # dependent, and 64-bit integers now map to CV_64S rather than CV_32S. + a = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.int32) res3 = cv.utils.dumpInputArray(a) # 32SC1 self.assertEqual(res3, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=6 dims(-1)=2 size(-1)=2x3 type(-1)=CV_32SC1") a = np.array([[[1, 2], [3, 4], [5, 6]]], dtype='f') @@ -293,8 +295,9 @@ class Arguments(NewOpenCVTests): self.assertEqual(res2_1, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_64FC1 dims(0)=2 size(0)=1x4") res2_2 = cv.utils.dumpInputArrayOfArrays([1.5]) self.assertEqual(res2_2, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=1 dims(-1)=1 size(-1)=1x1 type(0)=CV_64FC1 dims(0)=2 size(0)=1x4") - a = np.array([[1, 2], [3, 4], [5, 6]]) - b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + # see test_InputArray: keep the integer dtype explicit + a = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.int32) + b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.int32) res3 = cv.utils.dumpInputArrayOfArrays([a, b]) self.assertEqual(res3, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_32SC1 dims(0)=2 size(0)=2x3") c = np.array([[[1, 2], [3, 4], [5, 6]]], dtype='f') @@ -327,6 +330,32 @@ class Arguments(NewOpenCVTests): with self.assertRaises(Exception): cv.rectangle(array, (0, 0), (5, 5), (255), 2) + def test_64bit_integers_map_to_64bit_depths(self): + # 64-bit integer arrays used to fall through to a CV_32S cast. + for dtype, expected in ((np.int64, "CV_64SC1"), (np.uint64, "CV_64UC1")): + array = np.zeros((2, 2), dtype=dtype) + self.assertIn(expected, cv.utils.dumpInputArray(array)) + + def test_64bit_integers_are_not_truncated(self): + # Regression: values outside the int32 range were silently truncated + # (2**40 came back as 0) because of that cast. + for dtype in (np.int64, np.uint64): + for value in (2 ** 40, 2 ** 40 + 12345): + array = np.array([[value]], dtype=dtype) + result = cv.add(array, np.zeros_like(array)) + self.assertEqual(result.dtype, np.dtype(dtype)) + self.assertEqual(result[0, 0], value) + signed = np.array([[-2 ** 40]], dtype=np.int64) + self.assertEqual(cv.add(signed, np.zeros_like(signed))[0, 0], -2 ** 40) + + def test_64bit_integer_dtype_number_is_preserved(self): + # np.dtype('uint64') == np.dtype('ulonglong') compares equal even though + # their type numbers differ, so an exported array can look correct while + # being unusable as an input. Compare .num explicitly. + for dtype in (np.int64, np.uint64): + array = np.zeros((2, 2), dtype=dtype) + self.assertEqual(cv.add(array, array).dtype.num, np.dtype(dtype).num) + def test_20968(self): pixel = np.uint8([[[40, 50, 200]]]) _ = cv.cvtColor(pixel, cv.COLOR_RGB2BGR) # should not raise exception diff --git a/modules/python/test/test_squares.py b/modules/python/test/test_squares.py index 7d008760b4..0778ab1bb9 100644 --- a/modules/python/test/test_squares.py +++ b/modules/python/test/test_squares.py @@ -44,8 +44,12 @@ def find_squares(img): return squares def intersectionRate(s1, s2): - area, _intersection = cv.intersectConvexConvex(np.array(s1), np.array(s2)) - return 2 * area / (cv.contourArea(np.array(s1)) + cv.contourArea(np.array(s2))) + # dtype is explicit: these helpers take plain integer lists, and the geometry + # functions accept only CV_32S/CV_32F point coordinates. + s1 = np.array(s1, dtype=np.int32) + s2 = np.array(s2, dtype=np.int32) + area, _intersection = cv.intersectConvexConvex(s1, s2) + return 2 * area / (cv.contourArea(s1) + cv.contourArea(s2)) def filterSquares(squares, square): diff --git a/modules/python/test/tests_common.py b/modules/python/test/tests_common.py index 6591733803..75e4aff13e 100644 --- a/modules/python/test/tests_common.py +++ b/modules/python/test/tests_common.py @@ -102,11 +102,12 @@ class NewOpenCVTests(unittest.TestCase): def intersectionRate(s1, s2): + # dtype is explicit: the geometry functions accept only CV_32S/CV_32F points. x1, y1, x2, y2 = s1 - s1 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]]) + s1 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]], dtype=np.int32) x1, y1, x2, y2 = s2 - s2 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]]) + s2 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]], dtype=np.int32) area, _intersection = cv.intersectConvexConvex(s1, s2) return 2 * area / (cv.contourArea(s1) + cv.contourArea(s2)) diff --git a/modules/python/test/tst_scene_render.py b/modules/python/test/tst_scene_render.py index 33fef512e9..9736b12be8 100644 --- a/modules/python/test/tst_scene_render.py +++ b/modules/python/test/tst_scene_render.py @@ -37,7 +37,7 @@ class TestSceneRender(): self.yAmpl = self.sceneBg.shape[1] - (self.center[1] + fgImg.shape[1]) self.initialRect = np.array([ (self.h/2, self.w/2), (self.h/2, self.w/2 + self.w/10), - (self.h/2 + self.h/10, self.w/2 + self.w/10), (self.h/2 + self.h/10, self.w/2)]).astype(int) + (self.h/2 + self.h/10, self.w/2 + self.w/10), (self.h/2 + self.h/10, self.w/2)]).astype(np.int32) self.currentRect = self.initialRect np.random.seed(10)