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 데이터를 해제하지는 않는다