mirror of
https://github.com/opencv/opencv.git
synced 2026-11-09 18:04:29 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
138
modules/python/test/test_ml_dtypes.py
Normal file
138
modules/python/test/test_ml_dtypes.py
Normal 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()
|
||||
Reference in New Issue
Block a user