Search
Duplicate

OpenCV C++/ C#과 C++의 Mat 데이터 주고 받기

UI 부분은 C#으로 구현하고, 성능과 관련된 부분은 C++로 구현하는 경우가 많은데, 이런 환경에서 OpenCV를 사용해야 하는 경우, C#과 C++ 간에 Mat 데이터를 주고 받는 방법에 대해 알아본다.
이 글에서 C++은 OpenCV 를 사용하고 C#은 OpenCV의 C# Wrapper인 OpenCvSharp을 사용하는 것을 기준으로 하였다.

OpenCvSharp의 Mat 데이터를 C++로 보내기

우선 C#에서 Mat 데이터를 받아서 처리하는 로직을 가진 C++ 함수가 다음과 같이 생겼다고 하자.
// OpenCV의 uchar는 unsigned char의 축약형 표현이다. bool WriteImage(unsigned char* source, int width, int height, int type, int channels) { // height(rows), width(cols), type을 이용해서 빈 Mat을 만든다. Mat img(height, width, type); // width * height * channels을 이용해서 크기를 구한다. int size = width * height * channels; // C#에서 받은 source 데이터를 memcpy_s를 이용해서 비어 있는 Mat에 복사한다. // C#에서 받은 것은 수정이 불가능하기 때문에 이렇게 복사해서 사용한다. memcpy_s(img.data, size, source, size); // 적절한 위치에 복사하고 종료 return imwrite(path, img); }
C++
복사
위의 C++ 함수를 사용하는 C# 코드는 아래와 같다.
// NAME_DLL은 C++ DLL의 경로 // C++의 unsigned char는 C#에서 byte와 동일하다. // 포인터 형태로 전달해야 하므로 unsafe 키워드를 사용한다. [DllImport(NAME_DLL)] unsafe internal extern static bool WriteImage(byte* source, int width, int height, int type, int channels); void WriteImage() { // 적절한 경로의 이미지를 읽는다. using (Mat source = Cv2.ImRead(path)) { // pointer를 사용해야 하므로 unsafe 블록을 설정한다. unsafe { // C++ 함수를 호출한다. source.Data 에 C++로 넘길 정보가 담겨 있다. bool result = NativeMethod.WriteImage(source: (byte*)source.Data, width: source.Width, height: source.Height, type: source.Type(), channels: source.Channels()); } } }
C#
복사
참고로 C#에서 Mat 데이터를 수정한 후에 C++로 보내는 경우도 존재할 수 있는데, resize나 blur를 먹이는 것 등은 문제 없지만, C#에서 Sub Mat —이미지 내의 특정 영역만 추출하는— 을 구한 후에 C++로 전달하면 C++에서 데이터를 제대로 변환할 수 없다. 코드상에서 에러가 발생하지 않기 때문에 최종 결과를 보고 잘못된 부분을 찾는데 고생할 수 있음.
따라서 C++에서 처리해야 하는 이미지의 Sub Mat을 구해야 한다면, x, y, width, height만 보내서 C++에서 Sub Mat을 구하도록 하는 편이 낫다.

C++의 Mat 데이터를 C#으로 보내기

이와 반대로 결과를 이미지 형태로 다시 C#에서 받아야 하는 상황이라고 하자. 이런 경우 Mat 데이터를 그대로 받을 수 없기 때문에 C++과 C#에서 동일한 형태를 갖는 구조체를 이용해서 데이터를 받아야 한다. —왜 C++의 데이터를 C#에서 그냥 받을 수 없는지는 아래 글 참조
우선 C++에 다음과 같이 구조체를 선언한다.
// unsigned char는 OpenCV에서 uchar와 동일하다. struct MatSample { public: MatSample(int index, int rows, int columns, int channels, int type, unsigned char* data) : index(index), rows(rows), columns(columns), channels(channels), type(type), data(data) { } int GetIndex() const { return this->index; } int GetRows() const { return this->rows; } int GetColumns() const { return this->columns; } int GetChannels() const { return this->channels; } int GetType() const { return this->type; } unsigned char* GetData() const { return this->data; } private: int index, rows, columns, channels, type; unsigned char* data; }
C++
복사
C#에서 C++과 대응시킬 구조체를 선언한다. 배열을 받을 변수는 IntPtr로 선언한다.
[StructLayout(LayoutKind.Sequential)] struct MatSample { public MatSample(int index, int rows, int columns, int channels, int type, IntPtr data) { this.Index = index; this.Rows = rows; this.Columns = columns; this.Channels = channels; this.Type = type; this.Data = data; } public int Index { get; private set; } public int Rows { get; private set; } public int Columns { get; private set; } public int Channels { get; private set; } public int Type { get; private set; } public IntPtr Data { get; private set; } };
C#
복사
C++에서 C#으로 Mat 데이터를 넘겨주기 위해 다음과 같이 heap에 배열을 생성한 후, 해당 배열에 Mat의 데이터를 복사하고, heap에 생성된 배열을 구조체에 담아 C#으로 보낸다.
배열을 C++의 heap에 생성했으므로 이 배열은 C#에서 삭제할 수 없다. 놔두면 memory leak이 발생하므로, C#에서 C++에 메모리를 삭제할 수 있도록 하는 함수도 추가한다. —DeleteMatSample
MatSample ReadImage(const char* path) { Mat mat = imread(path); int size = mat.rows * mat.cols * mat.channels(); unsigned char* data = new unsigned char[size]; memcpy_s(data, size, mat.data, size); return MatSample(0, mat.rows, mat.cols, mat.channels(), mat.type(), data); } bool DeleteMatSample(const MatSample& matSample) { delete[] matSample->GetData(); return true; }
C#
복사
C#에서 위의 C++ DLL 함수를 호출 할 수 있는 함수를 선언한다.
// DllImport에 지정되는 이름은 C++ DLL의 이름과 경로를 의미한다. [DllImport("Test.dll")] static extern MatSample ReadImage(string path); // *를 사용하고 있으므로 unsafe 키워드를 붙여 준다. [DllImport("Test.dll")] static extern bool DeleteMatSample(MatSample matSample);
C#
복사
C++에서 받아온 unsigned char 배열은 다음의 두 가지 방법으로 OpenCvSharp에서 Mat 데이터로 변환할 수 있다.

배열을 이용한 Mat 데이터 변환

Marshal을 이용하면 C++의 배열 데이터를 C#의 배열 데이터로 변환할 수 있다. 그렇게 변환한 데이터는 OpenCvSharp의 Mat 데이터에 생성자로 넣어서 Mat 데이터를 생성할 수 있다.
MatSample matSample = NativeMethod.ReadImage("Path"); int size = matSample.Rows * matSample.Colums * matSample.Channels; byte[] arr = new byte[size]; // C++의 unsigned char는 C#에서 byte로 대응된다. Marshal.Copy(matSample.Data, arr, 0, size); // IntPtr로 받아온 데이터를 배열로 복사 using (Mat mat = new Mat(matSample.Rows, matSample.Columns, matSample.Type, arr)) { // C++에서 받아온 Mat 데이터 사용 } // C++을 통해 matSample의 데이터를 해제한다. NativeMethod.DeleteMatSample(matSample: matSample);
C#
복사

IntPtr을 이용한 Mat 데이터 변환

앞선 방법은 C++과 C#의 배열 데이터 전달 방법을 이용한 것이지만, 보다 간편한 방법이 있다. OpenCvSharp은 결국 C++ OpenCV의 Wrapper이기 때문에 아예 IntPtr을 이용해서 Mat을 생성할 수 있다. —사실 OpenCvSharp에서 Mat의 Data는 이미 IntPtr로 정의되어 있다.
이 방법은 Marshal.Copy이 없이 사용 가능하기 때문에 성능면에서도 더 낫다.
MatSample matSample = NativeMethod.ReadImage("Path"); // IntPtr을 Mat의 생성자에 넘기면 바로 Mat으로 변환된다. using (Mat mat = new Mat(matSample.Rows, matSample.Columns, matSample.Type, matSample.Data)) { // C++에서 받아온 Mat 데이터 사용 } // C++을 통해 matSample의 데이터를 해제한다. NativeMethod.DeleteMatSample(matSample: matSample);
C#
복사
그러나 이 방법으로 생성한 Mat도 파괴될 때, C++에서 생성한 heap 을 제거하지는 못하기 때문에 C++에 별도로 heap 데이터 해제를 수행해야 한다. —해당 heap을 가리키는 pointer만 갖고 있을 뿐이기 때문에 Mat 데이터가 해제 되도 pointer 변수만 해제될 뿐 heap 데이터를 해제하지는 않는다

참조 자료