Merge pull request #30183 from abhishek-gola/python-ml-dtypes

python: keep exact bfloat16 / float8 dtypes in the bindings via ml_dtypes
This commit is contained in:
Abhishek Gola
2026-10-31 20:26:08 +05:30
committed by GitHub
11 changed files with 441 additions and 47 deletions

View File

@@ -191,3 +191,36 @@ public:
CV_WRAP_AS(get) Mat getMat(int flags CV_WRAP_DEFAULT(ACCESS_RW)) const;
};
@endcode
Non-conventional NumPy data types
---------------------------------
OpenCV has three depths that NumPy does not know about: `CV_16BF`, `CV_8F_E4M3FN` and
`CV_8F_E4M3FNUZ`. The optional [ml_dtypes](https://github.com/jax-ml/ml_dtypes) package
registers the matching NumPy types `bfloat16`, `float8_e4m3fn` and `float8_e4m3fnuz`.
OpenCV never imports `ml_dtypes` itself, so import it in your script before you create
such arrays.
Input arrays of these types are always accepted and become Mats of the matching depth.
Not every OpenCV function supports these depths, though.
Output Mats of these depths are returned as `float32` arrays by default, so the type of a
result does not depend on whether some other module happened to import `ml_dtypes`. To get
the exact type back, enable it once for the whole process:
@code{.py}
import cv2 as cv
import ml_dtypes
cv.setUseExactNumpyDtypes(True)
out = net.forward() # bfloat16 / float8 outputs keep their type
@endcode
`cv.useExactNumpyDtypes()` returns the current value, which is `False` by default. An
array passed as an output argument, such as `dst`, is filled in place and returned as is,
whatever the setting. If the setting is on but `ml_dtypes` has not been imported, outputs
are still converted to `float32` and a warning is printed once.
The setting is global, so it changes the outputs of every piece of code in the process that
calls OpenCV. Turn it on in your application. A library should not change it, because the
application that imports the library may expect `float32` outputs.

View File

@@ -244,7 +244,9 @@ static int DLPackTypeToCVType(const DLDataType& dtype, int channels) {
{"CV_32UC", (PyCFunction)(pycvMakeTypeCh<CV_32U>), METH_O, "CV_32UC(channels) -> retval"}, \
{"CV_64UC", (PyCFunction)(pycvMakeTypeCh<CV_64U>), METH_O, "CV_64UC(channels) -> retval"}, \
{"CV_64SC", (PyCFunction)(pycvMakeTypeCh<CV_64S>), METH_O, "CV_64SC(channels) -> retval"}, \
{"CV_16BFC", (PyCFunction)(pycvMakeTypeCh<CV_16BF>), METH_O, "CV_16BFC(channels) -> retval"},
{"CV_16BFC", (PyCFunction)(pycvMakeTypeCh<CV_16BF>), METH_O, "CV_16BFC(channels) -> retval"}, \
{"CV_8F_E4M3FNC", (PyCFunction)(pycvMakeTypeCh<CV_8F_E4M3FN>), METH_O, "CV_8F_E4M3FNC(channels) -> retval"}, \
{"CV_8F_E4M3FNUZC", (PyCFunction)(pycvMakeTypeCh<CV_8F_E4M3FNUZ>), METH_O, "CV_8F_E4M3FNUZC(channels) -> retval"},
#endif // HAVE_OPENCV_CORE
#endif // OPENCV_CORE_PYOPENCV_CORE_HPP

View File

@@ -720,6 +720,13 @@ CV__DNN_INLINE_NS_BEGIN
* @param outputName name for layer which output is needed to get
* @return blob for first output of specified layer.
* @details By default runs forward pass for the whole network.
* @note In Python, CV_16BF / CV_8F_E4M3FN / CV_8F_E4M3FNUZ output blobs are
* returned as NumPy float32 arrays by default. To preserve their exact dtypes,
* import the optional ml_dtypes package and call
* `cv.setUseExactNumpyDtypes(True)` before forward(). This process-wide
* setting affects outputs only; registered bfloat16 / float8 input arrays are
* accepted regardless of it. It belongs to the application: libraries should not
* change it. See @ref tutorial_py_bindings_basics for details.
*/
CV_WRAP Mat forward(const String& outputName = String());
@@ -736,12 +743,14 @@ CV__DNN_INLINE_NS_BEGIN
* @param outputBlobs contains all output blobs for specified layer.
* @param outputName name for layer which output is needed to get
* @details If @p outputName is empty, runs forward pass for the whole network.
* @note Python output dtype handling is described in forward(const String&).
*/
CV_WRAP void forward(CV_ND OutputArrayOfArrays outputBlobs, const String& outputName = String());
/** @brief Runs forward pass to compute outputs of layers listed in @p outBlobNames.
* @param outputBlobs contains blobs for first outputs of specified layers.
* @param outBlobNames names for layers which outputs are needed to get
* @note Python output dtype handling is described in forward(const String&).
*/
CV_WRAP void forward(CV_ND OutputArrayOfArrays outputBlobs,
const std::vector<String>& outBlobNames);
@@ -749,6 +758,7 @@ CV__DNN_INLINE_NS_BEGIN
/** @brief Runs forward pass to compute outputs of layers listed in @p outBlobNames.
* @param outputBlobs contains all output blobs for each layer specified in @p outBlobNames.
* @param outBlobNames names for layers which outputs are needed to get
* @note Python output dtype handling is described in forward(const String&).
*/
CV_WRAP_AS(forwardAndRetrieve) void forward(CV_OUT std::vector<std::vector<Mat> >& outputBlobs,
const std::vector<String>& outBlobNames);

View File

@@ -118,9 +118,30 @@ static PyObject* pycvRegisterMatType(PyObject *self, PyObject *value)
Py_RETURN_NONE;
}
static PyObject* pycvSetUseExactNumpyDtypes(PyObject*, PyObject* value)
{
int flag = PyObject_IsTrue(value);
if (flag < 0)
return NULL;
setUseExactNumpyDtypes(flag != 0);
Py_RETURN_NONE;
}
static PyObject* pycvUseExactNumpyDtypes(PyObject*, PyObject*)
{
return PyBool_FromLong(useExactNumpyDtypes());
}
static PyMethodDef special_methods[] = {
{"_registerMatType", (PyCFunction)(pycvRegisterMatType), METH_O, "_registerMatType(cv.Mat) -> None (Internal)"},
{"redirectError", CV_PY_FN_WITH_KW(pycvRedirectError), "redirectError(onError) -> None"},
{"setUseExactNumpyDtypes", (PyCFunction)(pycvSetUseExactNumpyDtypes), METH_O,
"setUseExactNumpyDtypes(flag) -> None\n. Keep output bfloat16 / float8 arrays at their exact dtype "
"(registered by the ml_dtypes package, which must be imported) instead of widening them "
"to float32. Off by default. Input arrays are accepted regardless of this flag. "
"The flag is process-wide: set it in the application, libraries should not change it."},
{"useExactNumpyDtypes", (PyCFunction)(pycvUseExactNumpyDtypes), METH_NOARGS,
"useExactNumpyDtypes() -> retval"},
#ifdef HAVE_OPENCV_HIGHGUI
{"createTrackbar", (PyCFunction)pycvCreateTrackbar, METH_VARARGS, "createTrackbar(trackbarName, windowName, value, count, onChange) -> None"},
{"createButton", CV_PY_FN_WITH_KW(pycvCreateButton), "createButton(buttonName, onChange [, userData, buttonType, initialButtonState]) -> None"},
@@ -594,6 +615,21 @@ static bool init_body(PyObject * m)
PUBLISH(CV_BoolC2);
PUBLISH(CV_BoolC3);
PUBLISH(CV_BoolC4);
PUBLISH(CV_16BF);
PUBLISH(CV_16BFC1);
PUBLISH(CV_16BFC2);
PUBLISH(CV_16BFC3);
PUBLISH(CV_16BFC4);
PUBLISH(CV_8F_E4M3FN);
PUBLISH(CV_8F_E4M3FNC1);
PUBLISH(CV_8F_E4M3FNC2);
PUBLISH(CV_8F_E4M3FNC3);
PUBLISH(CV_8F_E4M3FNC4);
PUBLISH(CV_8F_E4M3FNUZ);
PUBLISH(CV_8F_E4M3FNUZC1);
PUBLISH(CV_8F_E4M3FNUZC2);
PUBLISH(CV_8F_E4M3FNUZC3);
PUBLISH(CV_8F_E4M3FNUZC4);
#undef PUBLISH_
#undef PUBLISH

View File

@@ -151,6 +151,8 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
bool needcopy = !PyArray_ISNOTSWAPPED(oarr), needcast = needcopy;
int typenum = PyArray_TYPE(oarr), new_typenum = typenum;
int type = numpyTypeToCvDepth(typenum);
if( type < 0 )
type = nonConventionalTypeToCvDepth(typenum); // bfloat16 / float8 registered by the caller
if( type < 0 )
{
@@ -285,18 +287,18 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
for(int i = 0; i < sz && filled; i++ )
{
PyObject* oi = PyArray_GETITEM(oarr, base_ptr + step[0] * i); // new object
if( PyInt_Check(oi) )
m.at<double>(i) = (double)PyInt_AsLong(oi);
else if( PyFloat_Check(oi) )
m.at<double>(i) = (double)PyFloat_AsDouble(oi);
else
double v = oi ? PyFloat_AsDouble(oi) : -1.0;
if( v == -1.0 && PyErr_Occurred() )
{
PyErr_Clear();
failmsg("%s has some non-numerical elements", info.name);
m.release();
filled = false;
}
else
m.at<double>(i) = v;
Py_DECREF(oi);
Py_XDECREF(oi);
}
if(needcopy)
@@ -332,42 +334,67 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
return true;
}
// Copies an empty or 0-D Mat into a new array of dtype `descr`, which must match m.depth().
// Steals the reference to `descr`. Channels become the last axis, so a 0-D Mat has shape (cn,).
static PyObject* pyopencv_from_new_array(const cv::Mat& m, PyArray_Descr* descr)
{
const int cn = m.channels();
int dims = m.dims;
cv::AutoBuffer<npy_intp> _sizes(dims + 1);
for( int i = 0; i < dims; i++ )
_sizes[i] = (npy_intp)m.size[i];
if( cn > 1 )
_sizes[dims++] = cn;
PyObject* o = PyArray_NewFromDescr(&PyArray_Type, descr, dims, _sizes.data(),
nullptr, nullptr, 0, nullptr);
if( o && !m.empty() )
memcpy(PyArray_DATA((PyArrayObject*)o), m.data, m.elemSize());
return o;
}
template<>
PyObject* pyopencv_from(const cv::Mat& m)
{
// NumPy has no bfloat16 or float8 dtype: widen these to float32 (lossless).
if( m.depth() == CV_16BF || m.depth() == CV_8F_E4M3FN || m.depth() == CV_8F_E4M3FNUZ )
PyArray_Descr* exotic = nullptr;
if( isNonConventionalDepth(m.depth()) )
{
cv::Mat m32f;
ERRWRAP2(m.convertTo(m32f, CV_32F));
return pyopencv_from(m32f);
}
if( m.empty() )
{
// empty() also catches a live buffer with a zero-length dim: return an empty array, not None.
if( m.dims >= 1 )
exotic = nonConventionalDescrForCvDepth(m.depth());
if( !exotic )
{
int cn = m.channels();
int typenum = cvDepthToNumpyType(m.depth());
int dims = m.dims;
cv::AutoBuffer<npy_intp> _sizes(dims + 1);
for( int i = 0; i < dims; i++ )
_sizes[i] = (npy_intp)m.size[i];
if( cn > 1 )
_sizes[dims++] = cn;
return PyArray_SimpleNew(dims, _sizes.data(), typenum);
// The caller's own array (e.g. dst=) already has the exact dtype: return it as is.
// Like the shared path below, this assumes m spans the whole array, not a ROI of it.
if( m.u && m.allocator == &GetNumpyAllocator() )
{
PyObject* o = (PyObject*)m.u->userdata;
Py_INCREF(o);
return o;
}
if( useExactNumpyDtypes() )
CV_LOG_ONCE_WARNING(NULL, "Python bindings: setUseExactNumpyDtypes(True) is set, "
"but NumPy does not know bfloat16 / float8 and they are "
"returned as float32. "
"Import ml_dtypes to keep the exact dtype.");
// NumPy alone cannot name these; widen to float32 (lossless).
cv::Mat m32f;
ERRWRAP2(m.convertTo(m32f, CV_32F));
return pyopencv_from(m32f);
}
}
if( m.empty() && m.dims == 0 )
Py_RETURN_NONE;
}
// 0D (scalar) Mat: return a true 0D numpy array.
if( m.dims == 0 )
// empty() also catches a live buffer with a zero-length dim: return an empty array, not None.
// A 0D (scalar) Mat becomes a true 0D numpy array.
if( m.empty() || m.dims == 0 )
{
int typenum = cvDepthToNumpyType(CV_MAT_DEPTH(m.type()));
PyObject* o = PyArray_SimpleNew(0, nullptr, typenum);
if( o )
memcpy(PyArray_DATA((PyArrayObject*)o), m.data, CV_ELEM_SIZE(m.type()));
return o;
PyArray_Descr* descr = exotic;
if( descr )
Py_INCREF(descr);
else
descr = PyArray_DescrFromType(cvDepthToNumpyType(m.depth()));
return pyopencv_from_new_array(m, descr);
}
// NumpyAllocator gives bfloat16 / float8 arrays their exact dtype here too.
cv::Mat temp, *p = (cv::Mat*)&m;
if(!p->u || p->allocator != &GetNumpyAllocator())
{

View File

@@ -34,19 +34,35 @@ UMatData* NumpyAllocator::allocate(int dims0, const int* sizes, int type, void*
int depth = CV_MAT_DEPTH(type);
int cn = CV_MAT_CN(type);
// Backing a CV_16BF Mat with the float32 buffer cvDepthToNumpyType() asks for
// would pair a 2-byte element step with 4-byte NumPy strides and corrupt the data.
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);
for( i = 0; i < dims; i++ )
_sizes[i] = sizes[i];
if( cn > 1 )
_sizes[dims++] = cn;
// bfloat16 / float8 need their exact NumPy descriptor: cvDepthToNumpyType()'s float32
// fallback would pair a narrow element step with wide NumPy strides and corrupt the buffer.
PyArray_Descr* exotic = nonConventionalDescrForCvDepth(depth);
if( exotic )
{
Py_INCREF(exotic); // PyArray_NewFromDescr steals the reference
PyObject* eo = PyArray_NewFromDescr(&PyArray_Type, exotic, dims, _sizes.data(),
nullptr, nullptr, 0, nullptr);
if(!eo)
CV_Error_(Error::StsError, ("The numpy array of depth=%d, ndims=%d can not be created", depth, dims));
return allocate(eo, dims0, sizes, type, step);
}
// Mat::create() falls back to the default allocator on this error;
// pyopencv_from() then widens the result to float32.
if( isNonConventionalDepth(depth) )
CV_Error(Error::StsNotImplemented, useExactNumpyDtypes()
? "bfloat16 / float8 arrays cannot be allocated through the NumPy allocator: "
"import ml_dtypes first"
: "bfloat16 / float8 arrays cannot be allocated through the NumPy allocator: "
"call cv.setUseExactNumpyDtypes(True) first");
int typenum = cvDepthToNumpyType(depth);
PyObject* o = PyArray_SimpleNew(dims, _sizes.data(), typenum);
if(!o)
CV_Error_(Error::StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims));

View File

@@ -1,8 +1,17 @@
#define NO_IMPORT_ARRAY // the numpy C-API table is imported once, in cv2.cpp
#define PY_ARRAY_UNIQUE_SYMBOL opencv_ARRAY_API
#include "cv2_util.hpp"
#include "opencv2/core.hpp"
#include "opencv2/core/utils/configuration.private.hpp"
#include "opencv2/core/utils/logger.hpp"
// NumPy 2.x headers provide PyDataType_ELSIZE() as an inline function, not a macro;
// 1.x headers expose the field directly.
#if NPY_ABI_VERSION < 0x02000000
#define PyDataType_ELSIZE(descr) ((descr)->elsize)
#endif
PyObject* opencv_error = NULL;
cv::TLSData<std::vector<std::string> > conversionErrorsTLS;
@@ -21,8 +30,6 @@ int cvDepthToNumpyType(int depth)
case CV_32F: return NPY_FLOAT;
case CV_64F: return NPY_DOUBLE;
case CV_16F: return NPY_HALF;
// NumPy has no bfloat16 dtype; pyopencv_from() converts the values to float32.
case CV_16BF: return NPY_FLOAT;
case CV_Bool: return NPY_BOOL;
default:
CV_Error(cv::Error::StsNotImplemented,
@@ -30,6 +37,99 @@ int cvDepthToNumpyType(int depth)
}
}
namespace {
// Dtypes registered by ml_dtypes on import. Their type numbers depend on registration order,
// so they are resolved by name; OpenCV never imports ml_dtypes itself.
struct NonConventionalDtype
{
int depth;
const char* name;
int itemsize;
};
const NonConventionalDtype kNonConventionalDtypes[] = {
{ CV_16BF, "bfloat16", 2 },
{ CV_8F_E4M3FN, "float8_e4m3fn", 1 },
{ CV_8F_E4M3FNUZ, "float8_e4m3fnuz", 1 },
};
const int kNumNonConventionalDtypes =
sizeof(kNonConventionalDtypes) / sizeof(kNonConventionalDtypes[0]);
bool g_useExactNumpyDtypes = false;
// Process-lifetime cache, accessed under the GIL. Misses are not cached, so a later
// `import ml_dtypes` still takes effect.
PyArray_Descr* g_nonConventionalDescrs[kNumNonConventionalDtypes] = {};
PyArray_Descr* nonConventionalDescr(int idx)
{
if (!g_nonConventionalDescrs[idx])
{
const NonConventionalDtype& t = kNonConventionalDtypes[idx];
PyObject* name = PyUnicode_FromString(t.name);
PyArray_Descr* descr = nullptr;
if (!name || !PyArray_DescrConverter(name, &descr))
descr = nullptr;
Py_XDECREF(name);
PyErr_Clear();
// Reject a same-named dtype with another layout.
if (descr && (descr->type_num < NPY_USERDEF || PyDataType_ELSIZE(descr) != t.itemsize))
{
Py_DECREF(descr);
descr = nullptr;
}
g_nonConventionalDescrs[idx] = descr;
}
return g_nonConventionalDescrs[idx];
}
int nonConventionalIndex(int depth)
{
for (int i = 0; i < kNumNonConventionalDtypes; i++)
if (kNonConventionalDtypes[i].depth == depth)
return i;
return -1;
}
} // namespace
void setUseExactNumpyDtypes(bool flag)
{
g_useExactNumpyDtypes = flag;
}
bool useExactNumpyDtypes()
{
return g_useExactNumpyDtypes;
}
bool isNonConventionalDepth(int depth)
{
return nonConventionalIndex(depth) >= 0;
}
PyArray_Descr* nonConventionalDescrForCvDepth(int depth)
{
const int idx = nonConventionalIndex(depth);
if (!g_useExactNumpyDtypes || idx < 0)
return nullptr;
return nonConventionalDescr(idx);
}
int nonConventionalTypeToCvDepth(int typenum)
{
if (typenum < NPY_USERDEF)
return -1;
for (int i = 0; i < kNumNonConventionalDtypes; i++)
{
PyArray_Descr* descr = nonConventionalDescr(i);
if (descr && descr->type_num == typenum)
return kNonConventionalDtypes[i].depth;
}
return -1;
}
int numpyTypeToCvDepth(int typenum)
{
// Only canonical NPY_* values may be used as case labels: the fixed-width

View File

@@ -17,6 +17,15 @@ PyObject* failmsgp(const char *fmt, ...);
int cvDepthToNumpyType(int depth);
int numpyTypeToCvDepth(int typenum);
// Output bfloat16 / float8 Mats keep their exact dtype only when enabled (off by default).
void setUseExactNumpyDtypes(bool flag);
bool useExactNumpyDtypes();
bool isNonConventionalDepth(int depth);
// Output descriptor: nullptr when disabled, unregistered, or without an OpenCV counterpart.
PyArray_Descr* nonConventionalDescrForCvDepth(int depth);
// Input depth: -1 when unregistered or without an OpenCV counterpart; ignores the output flag.
int nonConventionalTypeToCvDepth(int typenum);
//======================================================================================================================
class PyAllowThreads

View File

@@ -58,6 +58,12 @@ def apply_manual_api_refinement(root: NamespaceNode) -> None:
)
])
root.add_function("setUseExactNumpyDtypes", [
FunctionNode.Arg("flag", PrimitiveTypeNode.bool_())
])
root.add_function("useExactNumpyDtypes",
return_type=FunctionNode.RetType(PrimitiveTypeNode.bool_()))
def make_optional_none_return(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
@@ -75,16 +81,20 @@ def make_optional_none_return(root_node: NamespaceNode,
def export_matrix_type_constants(root: NamespaceNode) -> None:
MAX_PREDEFINED_CHANNELS = 4
CN_SHIFT = 5
depth_names = ("CV_8U", "CV_8S", "CV_16U", "CV_16S", "CV_32U", "CV_32S",
"CV_64U", "CV_64S", "CV_32F", "CV_64F", "CV_16F", "CV_16BF" "CV_Bool")
for depth_value, depth_name in enumerate(depth_names):
# Values must match modules/core/include/opencv2/core/hal/interface.h
depths = (("CV_8U", 0), ("CV_8S", 1), ("CV_16U", 2), ("CV_16S", 3),
("CV_32S", 4), ("CV_32F", 5), ("CV_64F", 6), ("CV_16F", 7),
("CV_16BF", 8), ("CV_Bool", 9), ("CV_64U", 10), ("CV_64S", 11),
("CV_32U", 12), ("CV_8F_E4M3FN", 13), ("CV_8F_E4M3FNUZ", 14))
for depth_name, depth_value in depths:
# Export depth constants
root.add_constant(depth_name, str(depth_value))
# Export predefined types
for c in range(MAX_PREDEFINED_CHANNELS):
root.add_constant(f"{depth_name}C{c + 1}",
f"{depth_value + 8 * c}")
f"{depth_value + (c << CN_SHIFT)}")
# Export type creation function
root.add_function(
f"{depth_name}C",

View File

@@ -240,6 +240,19 @@ class Bindings(NewOpenCVTests):
self.assertEqual(ref, cv.CV_MAKETYPE(depth, channels))
self.assertEqual(ref, func(channels))
def test_type_constants_for_all_depths(self):
# Same depth list as export_matrix_type_constants() in api_refinement.py.
depths = ("CV_8U", "CV_8S", "CV_16U", "CV_16S", "CV_32S", "CV_32F", "CV_64F", "CV_16F",
"CV_16BF", "CV_Bool", "CV_64U", "CV_64S", "CV_32U",
"CV_8F_E4M3FN", "CV_8F_E4M3FNUZ")
for name in depths:
depth = getattr(cv, name)
make_type = getattr(cv, name + "C")
for channels in range(1, 5):
ref = cv.CV_MAKETYPE(depth, channels)
self.assertEqual(getattr(cv, "%sC%d" % (name, channels)), ref, name)
self.assertEqual(make_type(channels), ref, name)
class Arguments(NewOpenCVTests):

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
try:
import ml_dtypes
except ImportError:
ml_dtypes = None
from tests_common import NewOpenCVTests
NON_CONVENTIONAL_DEPTHS = (cv.CV_16BF, cv.CV_8F_E4M3FN, cv.CV_8F_E4M3FNUZ)
class non_conventional_dtypes_test(NewOpenCVTests):
# Runs with or without ml_dtypes.
def setUp(self):
super(non_conventional_dtypes_test, self).setUp()
self.addCleanup(cv.setUseExactNumpyDtypes, cv.useExactNumpyDtypes())
cv.setUseExactNumpyDtypes(False)
def test_flag_roundtrip(self):
cv.setUseExactNumpyDtypes(True)
self.assertTrue(cv.useExactNumpyDtypes())
cv.setUseExactNumpyDtypes(False)
self.assertFalse(cv.useExactNumpyDtypes())
def test_disabled_widens_to_float32(self):
for depth in NON_CONVENTIONAL_DEPTHS:
for m in cv.utils.generateVectorOfMat(1, 3, 4, depth):
self.assertEqual(m.dtype, np.float32)
def test_enabled_without_ml_dtypes_widens_to_float32(self):
if ml_dtypes is not None:
self.skipTest("ml_dtypes is installed")
cv.setUseExactNumpyDtypes(True)
for depth in NON_CONVENTIONAL_DEPTHS:
for m in cv.utils.generateVectorOfMat(1, 3, 4, depth):
self.assertEqual(m.dtype, np.float32)
def test_short_bool_array_scalar_operands(self):
for channels in range(1, 5):
src = np.zeros((2, 3, channels), dtype=np.float32)
operand = np.array([True, False, True, True][:channels])
np.testing.assert_array_equal(cv.add(src, operand),
cv.add(src, tuple(float(v) for v in operand)))
class ml_dtypes_test(NewOpenCVTests):
def setUp(self):
super(ml_dtypes_test, self).setUp()
if ml_dtypes is None:
self.skipTest("ml_dtypes is not installed")
self.addCleanup(cv.setUseExactNumpyDtypes, cv.useExactNumpyDtypes())
cv.setUseExactNumpyDtypes(True)
# (numpy dtype, OpenCV depth, typeToString prefix, relative half-ULP)
self.cases = [
(ml_dtypes.bfloat16, cv.CV_16BF, "CV_16BF", 2.0 ** -8),
(ml_dtypes.float8_e4m3fn, cv.CV_8F_E4M3FN, "CV_8F", 2.0 ** -4),
(ml_dtypes.float8_e4m3fnuz, cv.CV_8F_E4M3FNUZ, "CV_8FNUZ", 2.0 ** -4),
]
def assertSameBits(self, actual, expected):
self.assertEqual(actual.dtype, expected.dtype)
self.assertEqual(actual.shape, expected.shape)
self.assertTrue(np.array_equal(np.ascontiguousarray(actual).view(np.uint8),
np.ascontiguousarray(expected).view(np.uint8)))
def test_numpy_to_mat_keeps_exact_type(self):
for dt, _, name, _ in self.cases:
a = np.arange(6, dtype=np.float32).reshape(2, 3).astype(dt)
self.assertIn("type(-1)=%sC1" % name, cv.utils.dumpInputArray(a))
self.assertIn("type(-1)=%sC3" % name, cv.utils.dumpInputArray(np.stack([a, a, a], -1)))
self.assertIn("type(-1)=%sC1" % name, cv.utils.dumpInputArray(np.array(1.5, dtype=dt)))
def test_inputs_accepted_with_outputs_disabled(self):
cv.setUseExactNumpyDtypes(False)
for dt, _, name, _ in self.cases:
a = np.arange(6, dtype=np.float32).reshape(2, 3).astype(dt)
self.assertIn("type(-1)=%sC1" % name, cv.utils.dumpInputArray(a))
result = cv.flip(a, 1)
self.assertEqual(result.dtype, np.float32)
np.testing.assert_array_equal(result, a[:, ::-1].astype(np.float32))
# The caller's own dst keeps its dtype and is returned as is.
d = np.empty_like(a)
self.assertIs(cv.flip(a, 1, dst=d), d)
self.assertSameBits(d, a[:, ::-1])
def test_short_array_scalar_operands(self):
# A float32 src keeps the result float32, so the operand is checked with the flag off too.
for exact in (False, True):
cv.setUseExactNumpyDtypes(exact)
for dt, _, _, _ in self.cases:
for channels in range(1, 5):
src = np.zeros((2, 3, channels), dtype=np.float32)
values = np.arange(channels, dtype=np.float32) - 1
operand = values.astype(dt)
expected = cv.add(src, tuple(float(v) for v in values))
reversed_expected = cv.add(src, tuple(float(v) for v in values[::-1]))
np.testing.assert_array_equal(cv.add(src, operand), expected)
np.testing.assert_array_equal(cv.add(src, operand[::-1]), reversed_expected)
img = np.zeros((2, 3, 3), dtype=ml_dtypes.bfloat16)
operand = np.array([1, 2, 3], dtype=ml_dtypes.bfloat16)
self.assertSameBits(cv.add(img, operand), img + operand)
def test_roundtrip_through_numpy_allocator(self):
# 240 is the largest finite float8_e4m3fnuz value, so every case keeps these values exactly.
src = np.array([[0.5, -1.25, 3.0, 7.0], [240.0, -240.0, 0.0, -0.5]], dtype=np.float32)
for dt, _, _, _ in self.cases:
a = src.astype(dt)
np.testing.assert_array_equal(a.astype(np.float32), src)
self.assertSameBits(cv.flip(a, 1), a[:, ::-1])
self.assertSameBits(cv.transpose(a), a.T)
strided = a[:, ::2]
self.assertSameBits(cv.flip(strided, 0), strided[::-1])
def test_dst_is_returned(self):
for dt, _, _, _ in self.cases:
a = np.arange(6, dtype=np.float32).reshape(2, 3).astype(dt)
d = np.empty_like(a)
self.assertIs(cv.flip(a, 1, dst=d), d)
def test_mat_allocated_in_cpp_to_numpy(self):
ref = cv.utils.generateVectorOfMat(2, 3, 4, cv.CV_32F)
for dt, depth, _, rtol in self.cases:
mats = cv.utils.generateVectorOfMat(2, 3, 4, depth)
self.assertEqual(len(mats), len(ref))
for m, r in zip(mats, ref):
self.assertEqual(m.dtype, dt)
self.assertEqual(m.shape, (3, 4))
np.testing.assert_allclose(m.astype(np.float32), r, rtol=rtol, atol=2.0 ** -9)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()