map 64bit int anf bfloat16

This commit is contained in:
vrooomy
2026-08-03 12:26:19 +05:30
parent 4ed880f045
commit 25ed5d4c0d
11 changed files with 129 additions and 88 deletions

View File

@@ -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.

View File

@@ -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<npy_intp> _sizes(dims + 1);

View File

@@ -8,21 +8,56 @@ cv::TLSData<std::vector<std::string> > 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;

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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))

View File

@@ -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)