C# - OpenCvSharp 사용 시 C/C++을 이용한 속도 향상 (for 루프 연산)
지난 글에서,
C# - OpenCvSharp을 이용한 동영상(avi, mp4, ...) 처리 + Direct2D
; https://www.sysnet.pe.kr/2/0/11404
RGB 이미지를 RGBA로 변환하기 위해 C# 코딩을 했었는데요, 이게 꽤나 성능이 안 좋았습니다. 반면, OpenCvSharp의 Mat 타입에서 제공하는 CvtColor 연산은 놀라울 정도로 높은 성능을 보였습니다. 따라서 당연히 CvtColor 메서드를 비롯해 가능하면 OpenCV가 제공하는 함수를 사용하는 것이 좋겠지만, 그래도 때로는 사용자 정의 루프를 작성해야 할 때가 있습니다.
그럴 때 성능이 안 좋은 C# 코딩보다는 그 부분만을 C/C++로 대체해 OpenCV 수준의 성능으로 끌어올리는 것이 바람직한데요, 그런 경우 어떤 식으로 해야 OpenCV 정도까지 성능이 개선되는지 테스트를 해봤습니다. ^^
연산 대상은 "
C# - OpenCvSharp을 이용한 동영상(avi, mp4, ...) 처리 + Direct2D" 글에서도 다뤘던 RGB to RGBA 코드입니다.
우선, 기준이 되는 OpenCV 연산을 다음과 같이 할 수 있습니다.
static void Main(string[] args)
{
using (Mat mat = new Mat(new Size(1920, 1080), MatType.CV_8UC3))
{
Convert(mat);
}
}
static int Convert(Mat mat)
{
using (Mat dstMat = mat.CvtColor(ColorConversionCodes.BGR2BGRA))
{
return dstMat.Width;
}
}
그다음 동일한 연산을 C#으로 직접 For loop로 구현을 했습니다.
static unsafe int Convert2(Mat srcMat)
{
byte *srcPtr = (byte *)srcMat.Data;
using (Mat dstMat = new Mat(new Size(1920, 1080), MatType.CV_8UC4))
{
byte *dstPtr = (byte *)dstMat.Data;
for (int y = 0; y < srcMat.Height; y++)
{
for (int x = 0; x < srcMat.Width - 1; x++)
{
int* src = (int*)srcPtr;
int* dst = (int*)dstPtr;
*dst = *src;
*(dstPtr + 3) = 0xff;
srcPtr = srcPtr + 3;
dstPtr = dstPtr + 4;
}
Buffer.MemoryCopy(srcPtr, dstPtr, 3, 3);
*(dstPtr + 3) = 0xff;
}
}
return 0;
}
역시 동일한 코드를 C/C++ DLL을 만들어 export 함수로 C#에서 다음과 같이 호출하는 식으로 구현했습니다.
// ==== C# ====
[DllImport("MemCopyLib.dll")]
public unsafe extern static void RGB2RGBA(byte* srcPtr, byte* dstPtr, int width, int height);
static unsafe int Convert3(Mat srcMat)
{
byte* srcPtr = (byte*)srcMat.Data;
using (Mat dstMat = new Mat(new Size(1920, 1080), MatType.CV_8UC4))
{
byte* dstPtr = (byte*)dstMat.Data;
RGB2RGBA(srcPtr, dstPtr, srcMat.Width, srcMat.Height);
}
return 0;
}
// ==== C/C++ ====
__declspec(dllexport) void RGB2RGBA(BYTE *srcPtr, BYTE *dstPtr, int width, int height)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
memcpy(dstPtr, srcPtr, 3);
*(dstPtr + 3) = 0xff;
srcPtr = srcPtr + 3;
dstPtr = dstPtr + 4;
}
}
}
이렇게 만들고 실행해 보면, 다음과 같은 성능 수치를 볼 수 있습니다.
// 각각 Release 빌드로 100회씩 실행했으며, JIT 컴파일 보정을 위해 1회를 미리 실행한 후 시간 측정
opencv(100) : 690
c# for(100) : 7120
C++ for(100) : 1284
보면 OpenCV의 Mat.CvtColor 메서드가 단연 빠르고, 약 2배 늦은 속도로 단순 C++ for 구문 속도가 나오며 C#은 그보다도 7배까지 느린 것을 볼 수 있습니다. 즉, 이미지 처리 시 무거운 for 루프를 처리하는 경우라면 C#보다는 C/C++에 작업을 맡기는 것을 충분히 고려할만합니다.
그렇다면 OpenCV의 처리가 왜 그토록 빠른 것일까요? 이에 대한 해답은
지난번에 소개했던 것처럼 다음의 글에서 찾아볼 수 있습니다.
OpenCV - 속도 분석 (1)
; https://laonple.blog.me/220861902363
즉, 병렬 처리입니다. 이를 위해 C/C++ 코드를 다음과 같이 병렬 처리로 바꾸면,
// i5-4670 코어 4개에서 테스트
#include <ppl.h>
using namespace concurrency;
__declspec(dllexport) void RGB2RGBA_Parallel(BYTE *srcPtr, BYTE *dstPtr, int width, int height)
{
parallel_for (0, height, [&](size_t y)
{
BYTE *srcPtrY = srcPtr + (y * width * 3);
BYTE *dstPtrY = dstPtr + (y * width * 4);
for (size_t x = 0; x < width; x++)
{
memcpy(dstPtrY, srcPtrY, 3);
*(dstPtrY + 3) = 0xff;
srcPtrY = srcPtrY + 3;
dstPtrY = dstPtrY + 4;
}
});
}
1284ms였던 것이 609ms까지 내려갑니다. OpenCV 속도가 690ms이니 충분히 빨라진 것입니다. (물론, OpenCV가 범용 처리를 하는 것을 고려했을 때 당연히 우리가 만든 C/C++ 코드가 저 정도는 빨라야 합니다.)
그렇다면 혹시 C# 코드도 병렬 처리를 하면 많이 빨라질까요?
// i5-4670 코어 4개에서 테스트
static unsafe int Convert3(Mat srcMat)
{
byte* srcPtr = (byte*)srcMat.Data;
using (Mat dstMat = new Mat(new Size(1920, 1080), MatType.CV_8UC4))
{
byte* dstPtr = (byte*)dstMat.Data;
Parallel.For(0, srcMat.Height, (y) =>
{
byte* srcPtrY = srcPtr + (y * srcMat.Width * 3);
byte* dstPtrY = dstPtr + (y * srcMat.Width * 4);
for (int x = 0; x < srcMat.Width - 1; x++)
{
int* src = (int*)srcPtrY;
int* dst = (int*)dstPtrY;
*dst = *src;
*(dstPtrY + 3) = 0xff;
srcPtrY = srcPtrY + 3;
dstPtrY = dstPtrY + 4;
}
Buffer.MemoryCopy(srcPtrY, dstPtrY, 3, 3);
*(dstPtrY + 3) = 0xff;
});
}
return 0;
}
테스트해보면, 단독 스레드일 때 7355ms인 것에 비하면 2036ms를 기록하며 확실히 빨라졌지만 이는 C/C++ 단독 스레드 처리일 때보다도 느린 수치입니다.
(
첨부 파일은 이 글의 소스 코드를 포함합니다.)
참고로 다음은 성능 수치를 엑셀 그래프로 그린 것입니다. 훨씬 직관적이군요. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]