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# 코드는 아래와 같다.

// 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#에서 받아야 하는 상황이라고 하자. 이런 경우 Mat 데이터를 그대로 받을 수 없기 때문에 C++과 C#에서 동일한 형태를 갖는 구조체를 이용해서 데이터를 받아야 한다. —왜 C++의 데이터를 C#에서 그냥 받을 수 없는지는 아래 글 참조

C++/ C++ DLL을 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; }
};