友情提示,想直接看實現就跳到后面,但如果是跟我一樣的初學者,可以把過程看完,有助於對 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")
};
}