Home [데브코스] 7주차 - OpenCV parallel computing
Post
Cancel
Preview Image

[데브코스] 7주차 - OpenCV parallel computing


Parallel computing

영상의 병렬 처리란 cpu하나로만 처리하는 것이 아니라 코어를 다 이용해서 처리한다는 것이다. 기본적인 코드를 사용하면 첫번째에 있는 코어만 사용하게 된다. 이 처리를 코어를 다 사용한다는 것은 대체로 행 단위로 나누어 각각 처리한 후 병합하여 결과를 도출한다.

병렬 프로그래밍 기법

  • intel TBB(threading building blocks) - intel
  • HPX(High Performance ParalleX)**
  • OpenMP(Open multi-processing)**
  • APPLE GCD(Grand Central Dispatch) - apple
  • Windows RT concurrency
  • Windows concurrency - windows(visual studio에 적용되어 있음)
  • Pthreads - linux


cmd를 켜서 opencv_version -v를 쳐보면 리스트가 죽 나올 것이다. 거기서 아래 parallel framework를 보면 concurrency가 되어 있을 것이다.

1
2
3
4
> opencv_version -v
...
Parallel framework:            Concurrency  
...



  • 병렬처리용 for루프 코드

parallel_for_만 알아도 어떤 프레임워크를 사용하든 알아서 병렬 처리를 해준다.

1
2
void parallel_for_(const Range& range, const ParallelLoopBody& body, double nstripes = -1.)
void parallel_for_(const Range& range, std::function<void(const Range&)> functor, double nstripes = -1.)
  • range : 병렬 처리를 수행할 범위(start~end)
  • body : 함수 객체, ParallelLoopBody 클래스를 상속받은 클래스 또는 C++ 람다 표현식(opencv 3.3 이상부터 가능)


반드시 parallelloopbody를 상속받도록 작성해야 하고, 연산자를 재정의해줘야 한다.

  • parallel - 1.1 : 클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ParallelContrast : public ParallelLoopBody
{
public:
	ParallelContrast(Mat& src, Mat& dst, const float alpha)
		: m_src(src), m_dst(dst), m_alpha(alpha) // 상속 클래스 정의, 생성자 멤버 변수 초기화
	{
		//m_dst = Mat::zeros(src.rows, src.cols, src.type());
	}

	virtual void operator ()(const Range& range) const // ()연산자 재정의, 이 부분이 실제 연산이 진행되는 부분이다
	{
		for (int r = range.start; r < range.end; r++)
		{
			uchar* pSrc = m_src.ptr<uchar>(r);
			uchar* pDst = m_dst.ptr<uchar>(r);

			for (int x = 0; x < m_src.cols; x++)
				pDst[x] = saturate_cast<uchar>((1 + m_alpha)*pSrc[x] - 128 * m_alpha);
		}
	}

	ParallelContrast& operator =(const ParallelContrast &) {
		return *this;
	};

private:
	Mat& m_src;
	Mat& m_dst;
	float m_alpha;
};
int main() {
	Mat src = imread("hongkong.jpg", IMREAD_GRAYSCALE);

	Mat dst;
	parallel_for_(Range(0, src.rows), ParallelContrast(src, dst, alpha));
    ...
}

여기서 m_dst는 새로 초기화 안해도 된다.

()연산자의 경우 원래는 y=0,x=0부터로 작성했지만, 이 때는 인자로 넘어온 range에 대해 r=range.start, range.end를 사용해야 한다. 이 range에는 각각 처리해줄 범위가 다 들어있어야 한다. 즉 예를 들어 0~512 까지행을 처리할 것이라면 range(0,64), range(64,128)… 가 들어가는데, 이는 opencv에서 알아서 해준다. range를 사용한다는 것 이외에는 다 이전과 동일하다.

operator = 연산자 또한, 출력을 위해 반드시 작성해줘야 한다.

다 정의한 paralleConstrast는 아래에 parallel_for_을 통해 사용된다.



  • parallel - 2 : 람다 표현식
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main() {
	Mat src = imread("hongkong.jpg", IMREAD_GRAYSCALE);

	Mat dst(src.rows,src.cols, src.type());
    float alpha = 1.f;

	parallel_for_(Range(0, src.rows), [&](const Range& range) {
		for (int r = range.start; r < range.end; r++) {
			uchar* pSrc = src.ptr<uchar>(r);
			uchar* pDst = dst.ptr<uchar>(r);

			for (int x = 0; x < src.cols; x++) {
				pDst[x] = saturate_cast<uchar>((1 + alpha)*pSrc[x] - 128 * alpha);
			}
		}
	});

클래스를 정의한 것보다 훨씬 더 간단해졌다.



  • 여러 연산 비교하기
  1. 일반 연산자(+,-,*) 사용
  2. 연산자 직접 구현
  3. 연산자 직접 구현
  4. parallelConstrast 클래스 구현
  5. 람다 표현식 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include "opencv2/opencv.hpp"
#include "opencv2/core/ocl.hpp" // 이를 실행해야 ocl코드를 사용할 수 있음
#include <iostream>

using namespace cv;
using namespace std;

class ParallelContrast : public ParallelLoopBody
{
public:
	ParallelContrast(Mat& src, Mat& dst, const float alpha)
		: m_src(src), m_dst(dst), m_alpha(alpha)
	{
		m_dst = Mat::zeros(src.rows, src.cols, src.type());
	}

	virtual void operator ()(const Range& range) const
	{
		for (int r = range.start; r < range.end; r++)
		{
			uchar* pSrc = m_src.ptr<uchar>(r);
			uchar* pDst = m_dst.ptr<uchar>(r);

			for (int x = 0; x < m_src.cols; x++)
				pDst[x] = saturate_cast<uchar>((1 + m_alpha)*pSrc[x] - 128 * m_alpha);
		}
	}

	ParallelContrast& operator =(const ParallelContrast &) {
		return *this;
	};

private:
	Mat& m_src;
	Mat& m_dst;
	float m_alpha;
};

int main()
{
	ocl::setUseOpenCL(true); // opcnCL을 사용하라는 코드, 밑에서 시간 측정 시 opencv버전마다 다르지만 코드를 실행할 때 openCL을 사용할지에 대한 체크를 하려는 오류가 있기도 해서 미리 정리해줌

	Mat src = imread("hongkong.jpg", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	cout << "getNumberOfCPUs(): " << getNumberOfCPUs() << endl;
	cout << "getNumThreads(): " << getNumThreads() << endl;
	cout << "Image size: " << src.size() << endl;

	namedWindow("src", WINDOW_NORMAL);
	namedWindow("dst", WINDOW_NORMAL);
	resizeWindow("src", 1280, 720);
	resizeWindow("dst", 1280, 720);

	Mat dst;
	TickMeter tm;
	float alpha = 1.f;

	// 1. Operator overloading
	tm.start();

	dst = (1 + alpha) * src - 128 * alpha;

	tm.stop();
	cout << "1. Operator overloading: " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 2. Pixel access by at() (No parallel)
	dst = Mat::zeros(src.rows, src.cols, src.type());

	tm.reset();
	tm.start();

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			dst.at<uchar>(y, x) = saturate_cast<uchar>((1 + alpha)*src.at<uchar>(y, x) - 128 * alpha); // 2 * src -> 기울기 2로 만듬
		}
	}

	tm.stop();
	cout << "2. Pixel access by at(): " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 3. Pixel access by ptr() (No parallel)
	dst = Mat::zeros(src.rows, src.cols, src.type());
	
	tm.reset(); 
	tm.start();

	for (int y = 0; y < src.rows; y++) {
		uchar* pSrc = src.ptr<uchar>(y);
		uchar* pDst = dst.ptr<uchar>(y);

		for (int x = 0; x < src.cols; x++) {
			pDst[x] = saturate_cast<uchar>((1 + alpha)*pSrc[x] - 128 * alpha);
		}
	}

	tm.stop();
	cout << "3. Pixel access by ptr(): " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 4. cv::parallel_for_ with ParallelLoopBody subclass
	dst = Mat::zeros(src.rows, src.cols, src.type());
	tm.reset();
	tm.start();

	parallel_for_(Range(0, src.rows), ParallelContrast(src, dst, alpha));

	tm.stop();
	cout << "4. With parallel_for_ (ParallelLoopBody):  " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 5. cv::parallel_for_ with lambda expression
	dst = Mat::zeros(src.rows, src.cols, src.type());
	tm.reset();
	tm.start();

	parallel_for_(Range(0, src.rows), [&](const Range& range) {
		for (int r = range.start; r < range.end; r++) {
			uchar* pSrc = src.ptr<uchar>(r);
			uchar* pDst = dst.ptr<uchar>(r);

			for (int x = 0; x < src.cols; x++) {
				pDst[x] = saturate_cast<uchar>((1 + alpha)*pSrc[x] - 128 * alpha);
			}
		}
	});

	tm.stop();
	cout << "5. With parallel_for_ (lambda expression): " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	return 0;
}

병렬처리를 통해 훨씬 빠른 연산 속도를 확인할 수 있다.



유용한 OpenCV 활용 기법

메모리 버퍼로부터 Mat 객체 생성

사용자가 할당한 메모리 버퍼로부터 Mat 객체 생성하기

  • 외부 함수 또는 외부 라이브러리로부터 생성된 영상 데이터 메모리 버퍼가 있을 경우, 해당 메모리 버퍼를 참조하는 Mat 객체를 생성하여 사용 가능하다.
  • 이는 Mat객체 생성후 OpenCV 라이브러리를 사용할 수 있다.

VideoCapture은 범용성을 중시하기에 최고의 성능을 내기는 어렵다. 영상 데이터에 대한 Mat객체를 복사하는 것은 최소화해야 속도가 빠르다. 그 대신 포인터 함수로 메모리를 가리키도록만 해주면 복사는 없어지게 된다.

1
2
3
Mat mst; // 복사 -> 비효율적

uchar* data; // 메모리의 시작점을 가리키는 포인터


  • 사용자가 할당한 메모리 버퍼를 이용하여 Mat객체 생성
1
Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO-STEP);
  • rows : 행의 개수
  • cols : 열의 개수
  • type : 행렬 원소의 타입
  • data : 사용자가 할당한 메모리 버퍼 주소
  • step : 한 행이 차지하는 바이트 수 (패딩할 경우 주의해야 함)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "opencv2/opencv.hpp"
#include <fstream>
#include <iostream>

using namespace cv;
using namespace std;

int main(void)
{
	// Raw 파일 열기, 512x512
	FILE* fp = fopen("elaine.raw", "rb"); 
	if (fp == NULL) {
		cout << "File load error!" << endl;
		return -1;
	}

	// 연속된 메모리 동적 할당 & 파일 읽기
	int width = 512;
	int height = 512;
	int area = width * height;
	int step = 512;

	uchar* pixels = new uchar[area];
	int ret = (int)fread(pixels, sizeof(uchar), area, fp);

	fclose(fp);

	// 이미 존재하는 메모리 버퍼를 참조하는 Mat 객체 생성
	Mat src(height, width, CV_8UC1, pixels, step);

	// OpenCV Operations
	Mat dst;
	bilateralFilter(src, dst, -1, 20, 5);

	imshow("src", src);
	imshow("dst", dst);

	waitKey();

	delete [] pixels;

	return 0;
}

이렇게 src를 생성하면 area공간의 메모리를 가리키는 포인터를 받아서 그 area만을 가리키게 된다.

new연산자를 통한 동적 메모리 할당을 진행하면 반드시 해제해줘야 한다. 내가 직접 생성한 변수를 통해 src를 생성했기 때문에, 직접 생성한 변수만 메모리 할당을 해제해주면 되고, src에 대해서는 자동으로 해제가 된다.

1
delete [] pixels;



이 메모리 버퍼 방식은 예전에 했던 data를 참조하는 방식과 동일하다.

1
2
float data[] = { 1,1,2,3 };
Mat mat1(2, 2, CV_32FC1, data); // 메모리 버퍼를 참조하는 객체 생성



룩업 테이블(LUT:Lookup Table)

특정 연산에 대해 미리 결과 값을 계산하여 배열 등으로 저장해놓은 것을 말한다. 픽셀 값을 변경하는 경우 256x1 크기의 unsigned char 행렬에 픽셀 값 변환 수식 결과 값을 미리 저장한 후, 실제 모든 픽셀에 대해 실제 연산을 수행하는 대신 행렬(룩업 테이블) 값을 참조하여 결과 영상 픽셀 값을 설정한다.

파이썬의 dictionary에 저장해놓고 불러오는 것과 비슷한 것으로 특정 값(픽셀 가로 idx)에 대한 결과값을 저장한다.


  • 룩업 테이블 연산 함수
1
void LUT(InputArray src, InputArray lut, OutputArray dst);
  • src : 입력 영상, 8비트
  • lut : n개 원소를 갖는 룩업 테이블, 보통은 1x256(1행 256열)
  • dst : 출력 영상, src와 같은 크기, 같은 채널수, lut와 같은 깊이

lut 룩업 테이블로부터 값을 받아와 dst 픽셀 값을 설정하는데, 이때 인덱스 정보는 입력 행렬의 픽셀 값을 사용한다.


  • 룩업 테이블을 이용한 명암비 향상 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(vold)
{
    Mat src = imread("hongkong.jpg",IMREAD_GRAYSCALE);

    Mat lut(1, 256, CV_8U);
	uchar* p = lut.ptr(0);
	for (int i = 0; i < 256; i++) {
		p[i] = saturate_cast<uchar>(2 * i - 128);
	}

	LUT(src, lut, dst);

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}


  • 결과 비교
  1. 일반 연산자 사용
  2. ptr을 통한 직접 구현
  3. LUT 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include "opencv2/opencv.hpp"
#include "opencv2/core/ocl.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main(void)
{
	ocl::setUseOpenCL(false); // 진행 코드에서 OpenCL확인 코드로 인해 혹시 모를 불필요를 막기 위함

	Mat src = imread("hongkong.jpg", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}
	
	cout << "getNumberOfCPUs(): " << getNumberOfCPUs() << endl;
	cout << "getNumThreads(): " << getNumThreads() << endl;
	cout << "Image size: " << src.size() << endl;

	namedWindow("src", WINDOW_NORMAL);
	namedWindow("dst", WINDOW_NORMAL);
	resizeWindow("src", 1280, 720);
	resizeWindow("dst", 1280, 720);

	Mat dst;
	TickMeter tm;

	// 1. Operator overloading
	tm.start();

	dst = 2 * src - 128;

	tm.stop();
	cout << "1. Operator overloading: " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 2. Pixel access by ptr()
	tm.reset();
	tm.start();

	dst = Mat::zeros(src.rows, src.cols, src.type());
	for (int j = 0; j < src.rows; j++) {
		uchar* pSrc = src.ptr<uchar>(j);
		uchar* pDst = dst.ptr<uchar>(j);
		for (int i = 0; i < src.cols; i++) {
			pDst[i] = saturate_cast<uchar>(2 * pSrc[i] - 128);
		}
	}

	tm.stop();
	cout << "2. Pixel access by ptr(): " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();

	// 3. LUT() function
	Mat lut(1, 256, CV_8U);
	uchar* p = lut.ptr(0);
	for (int i = 0; i < 256; i++) {
		p[i] = saturate_cast<uchar>(2 * i - 128);
	}

	tm.reset();
	tm.start();

	LUT(src, lut, dst);
	
	tm.stop();
	cout << "3. LUT() function: " << tm.getTimeMilli() << " ms." << endl;

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}


룩업 테이블을 사용하는 예로는 영상 데이터를 회전할 때 sin,cos연산이 되게 느리게 동작한다. 그래서 이를 0.1도씩 룩업 테이블을 만들어놓고 쓰면 훨씬 빨라질 수 있다.

This post is licensed under CC BY 4.0 by the author.