在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
OpenCV是一個基於Apache2.0許可(開源)發行的跨平臺電腦視覺和機器學習軟體庫,可以運行在Linux、Windows、Android和Mac OS操作系統上。 它輕量級而且高效——由一系列 C 函數和少量 C++ 類構成,同時提供了Python、Ruby、MATLAB等語言的介面,實現了圖像處理和電腦視覺方面的很多通用演算法。OpenCV用C++語言編寫,它具有C ++,Python,Java和MATLAB介面,並支持Windows,Linux,Android和Mac OS,OpenCV主要傾向於實時視覺應用。
OpenCvSharp 是一個OpenCV的.Net wrapper,應用最新的OpenCV庫開發,使用習慣比EmguCV更接近原始的OpenCV,有詳細的使用樣例供參考。該庫採用LGPL發行,對商業應用友好。使用OpenCvSharp,可用C#,VB.NET等語言實現多種流行的圖像處理(image processing)與電腦視覺(computer vision)演算法。
但是在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式
1. 問題分析
在日常開發中,由於一些庫不支持C#介面,因此在使用時我們需要藉助動態鏈接庫的方式,在C#中調用C++封裝的應用。由於C++與C#底層編譯方式不同,因此動態鏈接庫只可以傳遞基礎的數據類型,無法傳遞像Class這種高級的數據格式。
在日常開發中,我們在C#中使用OpenCvSharp進行圖像處理,但是我們調用的演算法是通過C++封裝的動態鏈接庫,且需要將圖片數據傳遞到C++封裝的動態鏈接庫中進行處理,因此實現高效的實現圖片數據傳遞是十分有必要的。常見的方式有兩種:
(1)第一種方式是在C#中將圖片數據轉為基本數據類型byte[]數組,然後將該數據傳遞到C++動態鏈接庫中,在接收到該數據後,由C++再將該數據重新轉為圖片數據進行處理。目前該方式經過測試,是可以實現的,但是這樣有一個弊端,圖片數據需要進行兩次的轉換,這樣會導致嚴重浪費時間和消耗大量記憶體。
(2)第二種方式是在C#中將數據保存到本地,然後再C++動態鏈接庫中讀取。與上一種方式一樣,這樣會導致嚴重浪費時間和消耗大量記憶體。
2. 解決辦法
為瞭解決這個問題,我們探究了一下OpenCvSharp 實現方式,通過其源碼可知,OpenCvSharp 在實現時,是通過對C++中的OpenCV進行了進一步封裝,將Mat數據定義成指針類型,然後以指針的方式在C++與C#中進行傳遞;而在C#中,重新定義了Mat數據類型,將C++傳遞來的Mat指針作為成員變數進行初始化,而後續基於Mat的所有操作,其低層都是通過傳遞這個指針進行操作的。
知道了Mat的這個數據類型的實現原理後,我們可以模仿這種方式,以指針的方式實現將OpenCvSharp的數據傳遞到OpenCV C++中,這樣就可以快速實現數據類型傳遞。實現方式如下圖所示。
在C#中使用OpenCvSharp獲取一個圖片數據,數據類型為Mat,我們可以先進行處理等操作;接下來我們可以獲取OpenCvSharp的地址CvPtr
,然後在C++中使用*Mat
指針進行獲取,然後通過*Mat
我們便可以獲取到OpenCV C++中的Mat數據。接下來,用戶就可以根據自己的需求進行處理即可。在處理完成後,在將獲得新的用Mat
數據轉為用*Mat
指針,然後再C#中,使用IntPtr
數據類型進行接收,然後使用OpenCvSharp的Mat以獲取的指針數據為初始值初始化Mat
數據類型即獲得新的Mat
數據。
通過上述方式,我們便可以很輕鬆的實現C#中的OpenCvSharp與C++中的OpenCv數據轉換。
3. 項目創建
為方便演示,下述所有程式設計與編譯皆是在Windows11環境下,使用Visual Studio 2022編輯器實現。
- OpenCV: 4.8.0
- OpenCvSharp: 4.9.0
大家可以根據上述版本進行配置,也可以使用其他版本配置,但要保證OpenCV與OpenCvSharp都是同一個基礎版本的,且版本差別不要太大。
3.1 創建C++項目
使用Visual Studio 2022創建一個空的C++項目,然後添加兩個文件,分別為:mat_conv.h
、mat_conv.cpp
。
接下來配置項目屬性,首先配置項目輸出類型,如下圖所示,設置圖片輸出類型為動態庫(.dll)
然後配置OpenCV C++項目依賴,主要是配置C++項目的包含目錄、庫目錄以及附加依賴項三個地方,如下圖所示:
以下是我的項目設置信息,大家可以根據自己安裝的OpenCV情況進行設置:
包含目錄: C:\3rdpartylib\opencv\build\include
庫目錄: C:\3rdpartylib\opencv\build\x64\vc16\lib
附加依賴項: opencv_world480.lib
3.2 創建C#項目
使用Visual Studio 2022創建一個新的C#控制台項目,然後使用NuGet安裝所需的程式集即可,此處只需要安裝OpenCvSharp即可,如下圖所示:
4. 介面測試
此處主要測試四個介面:
- 第一個介面測試在OpenCvSharp中讀取一張圖片,然後將圖片數據傳入到OpenCV中,測試傳入是否成功。
- 第二個測試介面在OpenCV創建一個圖片,繪製一個矩形,然後將創建好的圖片傳出到OpenCvSharp,測試傳出數據是否成功。
- 第三個測試介面是在OpenCvSharp中讀取一張圖片,然後將圖片數據傳入到OpenCV中,併進行一步處理,該處理結果會將數據保存到另一個新的圖片數據中,將該新的圖片數據傳出,然後在OpenCvSharp查看是否處理成功,測試該過程是否成功。
- 第四個測試介面是在OpenCvSharp中讀取一張圖片,然後將圖片數據傳入到OpenCV中,併進行一步處理,該處理結果會直接在原有數據上進行修改,然後在OpenCvSharp查看是否處理成功,測試該過程是否成功。
4.1 介面一測試
在以下文件中分別添加以下代碼:
mat_conv.h
#include "opencv2/opencv.hpp"
extern "C" __declspec(dllexport) void __stdcall mat_conv1(cv::Mat * mat);
mat_conv.cpp
#include "mat_conv.h"
void mat_conv1(cv::Mat *mat)
{
cv::imshow("image", *mat);
cv::waitKey(0);
}
Program.cs
using OpenCvSharp;
using OpenCvSharp.Internal;
using System.Runtime.InteropServices;
namespace opencv_csharp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
string image_path = "image.jpg";
Mat mat1 = Cv2.ImRead(image_path);
Methord.mat_conv1(mat1.CvPtr);
}
}
class Methord
{
private const string dll_path = "C:\\Users\\lenovo\\Desktop\\test_opencv\\x64\\Release\\opencv_cpp.dll";
[DllImport(dll_path, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public static extern void mat_conv1(IntPtr mat);
}
}
其中,mat_conv.h
、mat_conv.cpp
為C++項目文件,Program.cs
文件為C#項目文件。
在C++項目文件中,extern "C" __declspec(dllexport)
表示使用C語言的編譯方式進行編譯,並導出到dll中。 mat_conv1(cv::Mat * mat)
方法主要是接受傳入的Mat
指針,並使用cv::imshow("image", *mat)
將圖片數據展示出來。
在C#項目中,使用[DllImport]
屬性將動態鏈接庫中的mat_conv1
讀取出來,同時因為在C#中指針都是被封裝為IntPtr
類型的,因此使用IntPtr
表示此處傳入的參數為指針類型。在使用該介面時,直接調用該方法,並且傳入指針參數,該指針參數可以通過Mat.CvPtr
直接獲得。
如下圖所示,程式在運行後,成功將傳入的圖片數據繪製出來,如下圖所示,說明該介面測試成功,也證明瞭該方法是可行的。
4.2 介面二測試
在以下文件中分別添加以下代碼:
mat_conv.h
#include "opencv2/opencv.hpp"
extern "C" __declspec(dllexport) void __stdcall mat_conv2(cv::Mat **returnValue);
mat_conv.cpp
#include "mat_conv.h"
void mat_conv2(cv::Mat** returnValue)
{
// 創建一個空白圖像
cv::Mat image = cv::Mat::zeros(400, 400, CV_8UC3);
// 矩形的左上角和右下角坐標
cv::Point2f rect_start(50, 50);
cv::Point2f rect_end(350, 350);
// 矩形顏色 (B, G, R)
cv::Scalar color(255, 0, 0); // 紅色
// 矩形線條粗細
int thickness = 2;
// 繪製矩形
cv::rectangle(image, rect_start, rect_end, color, thickness);
*returnValue = new cv::Mat(image);
}
Program.cs
using OpenCvSharp;
using OpenCvSharp.Internal;
using System.Runtime.InteropServices;
namespace opencv_csharp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
IntPtr ptr2 = IntPtr.Zero;
Methord.mat_conv2(out ptr2);
Mat mat2 = new Mat(ptr2);
Cv2.ImShow("image2", mat2);
Cv2.WaitKey(0);
}
}
class Methord
{
private const string dll_path = "C:\\Users\\lenovo\\Desktop\\test_opencv\\x64\\Release\\opencv_cpp.dll";
[DllImport(dll_path, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public static extern void mat_conv2(out IntPtr returnValue);
}
}
其中,mat_conv.h
、mat_conv.cpp
為C++項目文件,Program.cs
文件為C#項目文件。
在C++項目文件中,mat_conv2(cv::Mat** returnValue)
主要是創建一個畫布,並繪製一個矩形,然後將創建好的圖片數據以指針的方式傳遞到C#中。
在C#項目中,使用[DllImport]
屬性將動態鏈接庫中的mat_conv2
讀取出來,傳出數據此處使用的時雙重指針,因此使用out IntPtr
進行接收。再獲取到該方法後,我們調用new Mat(IntPtr ptr)
構造方法初始化為新的Mat
數據。。
如下圖所示,程式在運行後,成功將傳出的圖片數據繪製出來,如下圖所示,說明該介面測試成功,也證明瞭該方法是可行的。
4.3 介面三測試
在以下文件中分別添加以下代碼:
mat_conv.h
#include "opencv2/opencv.hpp"
extern "C" __declspec(dllexport) void __stdcall mat_conv3(cv::Mat * mat, cv::Mat **returnValue);
mat_conv.cpp
#include "mat_conv.h"
void mat_conv3(cv::Mat * mat, cv::Mat **returnValue)
{
cv::Mat m;
cv::cvtColor(*mat, m, cv::COLOR_BGR2GRAY);
*returnValue = new cv::Mat(m);
}
Program.cs
using OpenCvSharp;
using OpenCvSharp.Internal;
using System.Runtime.InteropServices;
namespace opencv_csharp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
string image_path = "image.jpg";
Mat mat3 = Cv2.ImRead(image_path);
IntPtr ptr3 = IntPtr.Zero;
Methord.mat_conv3(mat1.CvPtr, out ptr3);
Mat mat3 = new Mat(ptr3);
Cv2.ImShow("image1", mat3);
Cv2.WaitKey(0);
}
}
class Methord
{
private const string dll_path = "C:\\Users\\lenovo\\Desktop\\test_opencv\\x64\\Release\\opencv_cpp.dll";
[DllImport(dll_path, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public static extern void mat_conv3(IntPtr mat, out IntPtr return_value);
}
}
其中,mat_conv.h
、mat_conv.cpp
為C++項目文件,Program.cs
文件為C#項目文件。
在C++項目文件中, mat_conv2(cv::Mat * mat, cv::Mat **returnValue)
方法主要是接受傳入的Mat
指針,並將傳入的圖片數據轉為灰度圖,同時將轉換好的圖片數據以指針的方式傳出到C#中。
在C#項目中,使用[DllImport]
屬性將動態鏈接庫中的mat_conv3
讀取出來,其中傳入數據為指針數據,所以直接使用IntPtr
即可;而對於傳出數據此處使用的時雙重指針,因此使用out IntPtr
進行接收。再獲取到該方法後,我們調用new Mat(IntPtr ptr)
構造方法初始化為新的Mat
數據。
如下圖所示,程式在運行後,成功將傳入的圖片數據進行灰度轉換,並將轉換後的圖片數據成功傳遞出來,說明該介面測試成功,也證明瞭該方法是可行的。同時我們測試了該過程所需時間,僅使用了3.69毫秒。
4.4 介面四測試
在以下文件中分別添加以下代碼:
mat_conv.h
#include "opencv2/opencv.hpp"
extern "C" __declspec(dllexport) void __stdcall mat_conv4(cv::Mat * mat);
mat_conv.cpp
#include "mat_conv.h"
void mat_conv4(cv::Mat* mat)
{
// 矩形的左上角和右下角坐標
cv::Point2f rect_start(50, 50);
cv::Point2f rect_end(350, 350);
// 矩形顏色 (B, G, R)
cv::Scalar color(255, 0, 0); // 紅色
// 矩形線條粗細
int thickness = 2;
// 繪製矩形
cv::rectangle(*mat, rect_start, rect_end, color, thickness);
}
Program.cs
using OpenCvSharp;
using OpenCvSharp.Internal;
using System.Runtime.InteropServices;
namespace opencv_csharp
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
string image_path = "image.jpg";
Mat mat4 = Cv2.ImRead(image_path);
Methord.mat_conv4(mat1.CvPtr);
Cv2.ImShow("image2", mat4);
Cv2.WaitKey(0);
}
}
class Methord
{
private const string dll_path = "C:\\Users\\lenovo\\Desktop\\test_opencv\\x64\\Release\\opencv_cpp.dll";
[DllImport(dll_path, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public static extern void mat_conv4(IntPtr mat);
}
}
其中,mat_conv.h
、mat_conv.cpp
為C++項目文件,Program.cs
文件為C#項目文件。
在C++項目文件中, mat_conv4(IntPtr mat)
方法主要是接受傳入的Mat
指針,併在傳入的圖片數據中繪製一個矩形,因為該操作是在原始數據上進行的操作,沒有殘生新的圖像數據,所以不需要傳出。
在C#項目中,使用[DllImport]
屬性將動態鏈接庫中的mat_conv4
讀取出來,其中傳入數據為指針數據,所以直接使用IntPtr
即可。然後該方法運行完後,我們直接查看該圖像數據信息,查看是否已經被修改。
如下圖所示,程式在運行後,結果如下圖所示,說明該介面測試成功,也證明瞭該方法是可行的。
5. 總結
在項目中,我們結合OpenCvSharp源碼,使用OpenCvSharp數據指針實現了在C#與C++之間傳遞圖像數據。與傳統的數據傳遞方式相比,該方式通過傳遞指針,通過指針的方式實現對同一塊圖像數據進行操作,避免了圖像數據的來迴轉換,極大的節省了程式運行時間以及記憶體消耗。
最後如果各位開發者在使用中有任何問題,歡迎大家與我聯繫。