UI 부분은 C#으로 구현하고, 성능과 관련된 부분은 C++로 구현하는 경우가 많은데, 이런 환경에서 OpenCV를 사용해야 하는 경우, C#과 C++ 간에 Mat 데이터를 주고 받는 방법에 대해 알아본다.
이 글에서 C++은 OpenCV 를 사용하고 C#은 OpenCV의 C# Wrapper인 OpenCvSharp을 사용하는 것을 기준으로 하였다.
우선 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# 코드는 아래와 같다.
// 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#에서 Mat 데이터를 수정한 후에 C++로 보내는 경우도 존재할 수 있는데, resize나 blur를 먹이는 것 등은 문제 없지만, C#에서 Sub Mat —이미지 내의 특정 영역만 추출하는— 을 구한 후에 C++로 전달하면 C++에서 데이터를 제대로 변환할 수 없다. 코드상에서 에러가 발생하지 않기 때문에 최종 결과를 보고 잘못된 부분을 찾는데 고생할 수 있음.
따라서 C++에서 처리해야 하는 이미지의 Sub Mat을 구해야 한다면, x, y, width, height만 보내서 C++에서 Sub Mat을 구하도록 하는 편이 낫다.
이와 반대로 결과를 이미지 형태로 다시 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++과 대응시킬 구조체를 선언한다. 배열을 받을 변수는 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; }
};