友情提示,想直接看实现就跳到后面,但如果是跟我一样的初学者,可以把过程看完,有助于对 OpenCVSharp、NumSharp 以及相关东西有更多的了解。
初学OpenCV,虽然从语法上Python更人性化,但考虑到学习成本,而且总是想用自己较为熟悉的语言,所以选择了 OpenCVSharp 和 NumSharp。其实 SciSharp 的库上已经有了融合了 OpenCV 和 NumPy 的 SharpCV,但由于这个库对OpenCV API的实现相对不完全,所以暂不考虑。
因为市面的教材大部分都基于Python,少部分基于C++,没有办法,只能选择基于Python的教材。刚上手就碰见了Cv2.ImShow(string winName, Mat mat)
无法直接使用NDArray
作为参数生成图像,网上还找不到解决方案,还能怎样,自己不想学Python,那就看看怎么解决吧。
看到Mat
有一个构造函数Mat(int rows, int cols, MatType type, Array data, long step = 0)
,在 NdArray.Implicit.Array.cs 中找到了NDArray
到Array
显性转换的实现,而NDArray
有一个构造函数NDArray(Array values, Shape shape = default, char order = 'C')
,Mat又有一个函数bool GetArray<T>(out T[] data)
可以提取Array,有戏!
NDArray
支持的类型在 NPTypeCode,Mat
支持的类型在 MatType,然后我鬼知道这些奇奇怪怪的类型是什么东西,更不知道他们之间是什么对应关系。
public unsafe NDArray GetData()
{
// we pass donothing as it keeps reference to src preventing its disposal by GC
switch (MatType)
{
case MatType.CV_32SC1:
case MatType.CV_32SC2:
{
cv2_native_api.core_Mat_data(_handle, out int* dataPtr);
dtype = TF_DataType.TF_INT32;
return new NDArray(new IntPtr(dataPtr), shape, dtype);
}
case MatType.CV_32FC1:
{
cv2_native_api.core_Mat_data(_handle, out float* dataPtr);
dtype = TF_DataType.TF_FLOAT;
return new NDArray(new IntPtr(dataPtr), shape, dtype);
}
case MatType.CV_8UC1:
case MatType.CV_8UC3:
{
cv2_native_api.core_Mat_data(_handle, out byte* dataPtr);
dtype = TF_DataType.TF_UINT8;
return new NDArray(new IntPtr(dataPtr), shape, dtype);
}
default:
throw new NotImplementedException($"Can't find type: {_matType}");
}
}
所以基本确定Mat
和NDArray
之间的转换支持5种类型,分别是CV_32SC1、CV_32SC2、CV_32FC1、CV_8UC1、CV_8UC3,然后查了一下,在 LIST OF MAT TYPE IN OPENCV 这里可以知道这些字母分别代表什么数据类型,综合看两者对应关系应该是
NPTypeCode.Int32
(1通道) - MatType.CV_32SC1
NPTypeCode.Int32
(2通道) - MatType.CV_32SC2
NPTypeCode.Float
(1通道) - MatType.CV_32FC1
NPTypeCode.Byte
(1通道) - MatType.CV_8UC1
NPTypeCode.Byte
(3通道) - MatType.CV_8UC3
OK,写个扩展函数就搞定。然而,写完测试发现 bool GetArray<T>(out T[] data)
这个函数导出的数据只有通道数是1的类型才能被NDArray(Array values, Shape shape = default, char order = 'C')
支持,Mat 里的acceptableTypesMap
这个字典列出了对应类型。
至于为什么不直接用上面 SharpCV 里GetData()
的代码,原因很简单,因为 OpenCVSharp 只引入了public static extern unsafe ExceptionStatus core_Mat_data(IntPtr self, out byte* returnValue)
这个API (NativeMethods_core_Mat.cs),也就是只支持byte
,不支持int32
和float
,😠😠😠
那只能从NDArray
其他的构造函数入手了,没错,就是这个 public NDArray(IArraySlice values, Shape shape = default, char order = 'C')
。看着看着NDArray
的源码,然后试出了可以使用的方法。接下来就直接上菜吧。
后来在学习 OpenCV 的过程中发现上面提到的几种转换是不够的,至少要把4通道的考虑进去,不然没法操作透明度。
Mat 转 NDArray (safe)
public static NDArray ToNDArray(this Mat mat)
{
var matType = mat.Type();
var channels = mat.Channels();
var size = mat.Rows * mat.Cols * channels;
var shape = channels == 1 ? new Shape(mat.Rows, mat.Cols) : new Shape(mat.Rows, mat.Cols, channels);
if (matType == MatType.CV_32SC1 || matType == MatType.CV_32SC2)
{
var managedArray = new int[size];
Marshal.Copy(mat.Data, managedArray, 0, size);
var aslice = ArraySlice.FromArray(managedArray);
return new NDArray(aslice, shape);
}
if (matType == MatType.CV_32FC1)
{
var managedArray = new float[size];
Marshal.Copy(mat.Data, managedArray, 0, size);
var aslice = ArraySlice.FromArray(managedArray);
return new NDArray(aslice, shape);
}
if (matType == MatType.CV_64FC1)
{
var managedArray = new double[size];
Marshal.Copy(mat.Data, managedArray, 0, size);
var aslice = ArraySlice.FromArray(managedArray);
return new NDArray(aslice, shape);
}
if (matType == MatType.CV_8UC1 || matType == MatType.CV_8UC3 || matType == MatType.CV_8UC4)
{
var managedArray = new byte[size];
Marshal.Copy(mat.Data, managedArray, 0, size);
var aslice = ArraySlice.FromArray(managedArray);
return new NDArray(aslice, shape);
}
throw new Exception($"mat data type = {matType} is not supported");
}
NDArray 转 Mat (safe)
public static Mat ToMat(this NDArray nDArray) =>
new Mat(nDArray.shape[0], nDArray.shape[1], nDArray.GetMatType(), (Array)nDArray);
public static MatType GetMatType(this NDArray nDArray)
{
int channels = nDArray.ndim == 3 ? nDArray.shape[2] : 1;
return nDArray.typecode switch
{
NPTypeCode.Int32 => channels == 1 ? MatType.CV_32SC1 :
channels == 2 ? MatType.CV_32SC2 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Float => channels == 1 ? MatType.CV_32FC1 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Double => channels == 1 ? MatType.CV_64FC1 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Byte => channels == 1 ? MatType.CV_8UC1 :
channels == 3 ? MatType.CV_8UC3 :
channels == 4 ? MatType.CV_8UC4 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
_ => throw new ArgumentException($"nDArray data type = {nDArray.typecode} is not supported")
};
}
其实中途还找到了应该是 SharpCV 的 Mat 旧版的实现,里面的GetData()
是这样的
public unsafe NDArray GetData()
{
// we pass donothing as it keeps reference to src preventing its disposal by GC
switch (MatType)
{
case MatType.CV_32FC1:
{
cv2_native_api.core_Mat_data(_handle, out float* dataPtr);
var block = new UnmanagedMemoryBlock<float>(dataPtr, shape.Size, () => DoNothing(_handle));
var storage = new UnmanagedStorage(new ArraySlice<float>(block), shape);
return new NDArray(storage);
}
case MatType.CV_8UC1:
case MatType.CV_8UC3:
{
cv2_native_api.core_Mat_data(_handle, out byte* dataPtr);
var block = new UnmanagedMemoryBlock<byte>(dataPtr, size, () => DoNothing(_handle));
var storage = new UnmanagedStorage(new ArraySlice<byte>(block), shape);
return new NDArray(storage);
}
default:
throw new NotImplementedException($"Can't find type: {_matType}");
}
}
[MethodImpl(MethodImplOptions.NoOptimization)]
private void DoNothing(IntPtr ptr)
{
var p = ptr;
}
有人(应该是)根据上面的代码实现了转换 issues17
public static unsafe NDArray OpenCvSharpToNDArray(Mat input, int channels = 3)
{
Shape shape = new Shape(new int[] { input.Height, input.Width, channels });
var i = IntPtr.Zero;
if (input.Type() == MatType.CV_32SC1 || input.Type() == MatType.CV_32SC2)
{
int* ptr = (int*)input.DataPointer;
var block = new UnmanagedMemoryBlock<int>(ptr, shape.Size, () => DoNothing(i));
var storage = new UnmanagedStorage(new ArraySlice<int>(block), shape);
return new NDArray(storage);
}
else if (input.Type() == MatType.CV_32FC1)
{
float* ptr = (float*)input.DataPointer;
var block = new UnmanagedMemoryBlock<float>(ptr, shape.Size, () => DoNothing(i));
var storage = new UnmanagedStorage(new ArraySlice<float>(block), shape);
return new NDArray(storage);
}
else if (input.Type() == MatType.CV_8UC1 || input.Type() == MatType.CV_8UC3)
{
byte* ptr = (byte*)input.DataPointer;
var block = new UnmanagedMemoryBlock<byte>(ptr, shape.Size, () => DoNothing(i));
var storage = new UnmanagedStorage(new ArraySlice<byte>(block), shape);
return new NDArray(storage);
}
else
throw new NotImplementedException($"Can't find type: {input.Type()}");
}
[MethodImpl(MethodImplOptions.NoOptimization)]
private static void DoNothing(IntPtr ptr)
{
var p = ptr;
}
这里的 new Shape
没有考虑 channels
数,但改一改其实就能用了(没有亲自测试过)。另一点是触碰到指针,SharpCV 库修改了GetData()
的实现可能也是基于安全的考虑,我是觉得无问题不大,只是想看看还有没有别的实现方法。反正问题已经解决又可以不碰指针,完事。
20211119
在实践中发现上面的 ToNDArray()
方法效率太低,一张 (500, 500, 3),np.uint8 的画布转换一次(6700HQ)竟然需要40-50ms,相比之下 ToMat()
仅需不到1ms。高分辨率画布的转换会更明显,而通过指针进行转化几乎不受分辨率的影响。虽然目前还不知道有什么应用场景需要高性能的转化,但推荐还是通过指针转换,这样可以适应更多的应用场景。
Mat 转 NDArray (unsafe, hi-perf)
public static unsafe NDArray ToNDArrayUnsafe(this Mat mat)
{
var matType = mat.Type();
var channels = mat.Channels();
var size = mat.Rows * mat.Cols * channels;
var shape = channels == 1 ? new Shape(mat.Rows, mat.Cols) : new Shape(mat.Rows, mat.Cols, channels);
if (matType == MatType.CV_32SC1 || matType == MatType.CV_32SC2)
{
int* ptr = (int*)mat.DataPointer;
var block = new UnmanagedMemoryBlock<int>(ptr, shape.Size, () => DoNothing(IntPtr.Zero));
var storage = new UnmanagedStorage(new ArraySlice<int>(block), shape);
return new NDArray(storage);
}
if (matType == MatType.CV_32FC1)
{
float* ptr = (float*)mat.DataPointer;
var block = new UnmanagedMemoryBlock<float>(ptr, shape.Size, () => DoNothing(IntPtr.Zero));
var storage = new UnmanagedStorage(new ArraySlice<float>(block), shape);
return new NDArray(storage);
}
if (matType == MatType.CV_64FC1)
{
double* ptr = (double*)mat.DataPointer;
var block = new UnmanagedMemoryBlock<double>(ptr, shape.Size, () => DoNothing(IntPtr.Zero));
var storage = new UnmanagedStorage(new ArraySlice<double>(block), shape);
return new NDArray(storage);
}
if (matType == MatType.CV_8UC1 || matType == MatType.CV_8UC3 || matType == MatType.CV_8UC4)
{
byte* ptr = (byte*)mat.DataPointer;
var block = new UnmanagedMemoryBlock<byte>(ptr, shape.Size, () => DoNothing(IntPtr.Zero));
var storage = new UnmanagedStorage(new ArraySlice<byte>(block), shape);
return new NDArray(storage);
}
throw new Exception($"mat data type = {matType} is not supported");
}
[MethodImpl(MethodImplOptions.NoOptimization)]
private static void DoNothing(IntPtr ptr)
{
var p = ptr;
}
NDArray 转 Mat (unsafe, hi-perf)
public unsafe static Mat ToMatUnsafe(this NDArray nDArray) =>
new Mat(nDArray.shape[0], nDArray.shape[1], nDArray.GetMatType(), new IntPtr(nDArray.Unsafe.Address));
public static MatType GetMatType(this NDArray nDArray)
{
int channels = nDArray.ndim == 3 ? nDArray.shape[2] : 1;
return nDArray.typecode switch
{
NPTypeCode.Int32 => channels == 1 ? MatType.CV_32SC1 :
channels == 2 ? MatType.CV_32SC2 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Float => channels == 1 ? MatType.CV_32FC1 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Double => channels == 1 ? MatType.CV_64FC1 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
NPTypeCode.Byte => channels == 1 ? MatType.CV_8UC1 :
channels == 3 ? MatType.CV_8UC3 :
channels == 4 ? MatType.CV_8UC4 :
throw new ArgumentException($"nDArray data type = {nDArray.typecode} & channels = {channels} is not supported"),
_ => throw new ArgumentException($"nDArray data type = {nDArray.typecode} is not supported")
};
}