지난번에는 OpenCV의 함수를 이용하여 Gaussian Blur를 구현해보았다.
이번에는 함수 없이 직접 data에 접근하여서 흑백 이미지에 필터를 씌워보려고 한다.
가우시안에 대한 설명은 아주 간단하지만 이전 포스트에 있으니 참고하자!
https://nowtimeisthat.tistory.com/16
[OpenCV] cv::Gaussian Blur 함수 사용하기
Parallel Image Processing ( Gaussian Blur Project ) OpenCV 기본 함수에는 Gaussian Blur를 사용하여 이미지에 blur 처리를 할 수 있다. 이번 프로젝트에서는 해당 Gaussian Blur를 OpenCV, OpenMP, CUDA, Open..
nowtimeisthat.tistory.com
Pipeline
우선 파이프라인은 간단하다.
1. 원하는 kernel size에 맞는 Gaussian Kernel을 생성한다.
2. Gaussian Kernel을 모든 pixel에 filter를 적용하여 원하는 이미지를 재생성한다.
이제 한번 만들어보자!
Code
Gaussian Kernel 생성 함수
가우시안 커널을 생성한다.
가우시안 커널은 kernel size만큼 정규분포 형태의 값을 저장한다.
커널 생성 함수는 kernel size^2 만큼의 값을 저장한다.
가우시안 커널은 그림과 같이 정규분포가 되어야 한다.
인자로는 kerenl size, sigma를 input으로 받는다.
sigma는 아주 간단하게 설명하면 위 식과 같이 정규분포의 퍼짐 정도를 결정한다.
kernel size는 입히고 싶은 가우시안 필터의 크기를 말한다. 구하는 픽셀의 원래 값을 중앙에 두고, kernel size만큼의 주변 픽셀을 정규분포에 기반하여 convolution 후 blur를 진행한다.
이때 kernel은 kernel_size x kernel_size의 어레이를 가지며 추후 병렬영상 처리에도 사용하기 위하여 width에 따라 일렬로 저장한다.
int mSize = kernel_size / 2;
float sum = 0.0f;
// kernel_size * kernel_size에 알맞은 값을 저장한다
for (int y = -mSize; y <= mSize; y++) {
for (int x = -mSize; x <= mSize; x++) {
kernel[x + mSize + (y + mSize) * kernel_size] =
exp(-(pow(x, 2) + pow(y, 2)) / (2 * sigma * sigma)) / (2 * (float)CV_PI * sigma * sigma);
sum += kernel[x + mSize + (y + mSize) * kernel_size];
}
}
이를 위해 주변 픽셀은 정규분포에 따라 계산되는데 이때 정규분포는 무한대까지 적분 시에만 합이 1이 되므로
커널 사이즈 내 픽셀만 convolution 시, 총 합이 1이 되지 않아 어두운 이미지가 만들어질 수 있다.
따라서 아래 코드를 통해 kernel의 모든 값을 더하여 normalization을 진행한다.
// kernel 총 덧셈 시 1이 되도록 normalization 진행
for (int i = 0; i < kernel_size * kernel_size; i++) {
kernel[i] /= sum;
}
따라서 kernel 생성의 전체 코드는 아래와 같다.
void gaussian_kernel_2D(int kernel_size, float sigma, float *kernel) {
int mSize = kernel_size / 2;
float sum = 0.0f;
// kernel_size * kernel_size에 알맞은 값을 저장한다
for (int y = -mSize; y <= mSize; y++) {
for (int x = -mSize; x <= mSize; x++) {
kernel[x + mSize + (y + mSize) * kernel_size] =
exp(-(pow(x, 2) + pow(y, 2)) / (2 * sigma * sigma)) / (2 * (float)CV_PI * sigma * sigma);
sum += kernel[x + mSize + (y + mSize) * kernel_size];
}
}
// kernel 총 덧셈 시 1이 되도록 normalization 진행
for (int i = 0; i < kernel_size * kernel_size; i++) {
kernel[i] /= sum;
}
}
이때 나는 가우시안 필터를 2D로 진행했지만 1D 정규분포로도 계산이 가능하다. 즉, kerenl을 1D Gaussian으로 생성하고, vertical, horizon을 따로 계산하면 더욱 빠른 연산이 가능하다.
이 또한 추후 작성해보겠다!
Gaussian Convolution
기존 cv::GaussianBlur 함수와 거의 동일하게 함수를 생성한다.
물론 kernel을 미리 생성했으므로, input image, output image, kernel size, kernel 만을 인수로 받는다.
kernel size는 2로 나누어 mSize라는 변수로 새로 저장한다.
이를 통해 -mSize ~ mSize까지 kernel 사이즈만큼 이미지에 효과적으로 필터를 입힐 수 있다.
void serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel) {
// input 이미지 크기 저장
int w = src.cols;
int h = src.rows;
// output 이미지 할당
dst = Mat(h, w, CV_8U);
// temp에 output 이미지의 변수를 저장하며 계산을 진행한다.
int mSize = kernel_size / 2;
float temp;
```
}
이후 이미지의 픽셀 수만큼 for문을 진행하며 convolution을 진행한다.
하지만 이때 아래와 같이 boundary에서 kernel size 값에 따라 알맞은 계산이 진행되지 않을 수 있다.
padding, sweep plane등의 정말 다양한 방법들이 많지만 나는 단순히 이전 이미지의 픽셀을 복사해올 것이다.
이는 if문을 통해 boundary, boundary가 아닐 때를 나누어 계산한다.
Mat 형식은 data로 접근한다면 1D로 이뤄져있으므로 index에 i + j * width로 픽셀에 접근한다.
조금 더 정확한 픽셀 계산을 위하여 float 형태로 계산을 하고, 저장 시에만 (uchar)로 temp를 저장한다.
void serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel) {
int w = src.cols;
int h = src.rows;
dst = Mat(h, w, CV_8U);
int mSize = kernel_size / 2;
float temp;
for (int index = 0; index < w * h; index++) {
temp = 0.f;
// kernel_size에 해당하지 않는 boundary 부분 예외 처리
if ((index % w >= mSize) && (index / w >= mSize) && (index % w < w - mSize) && (index / w < h - mSize)) {
for (int j = -mSize; j <= mSize; j++) {
for (int i = -mSize; i <= mSize; i++) {
// float으로 계산한다
temp += (float)src.data[index + i + j * w] * kernel[i + mSize + (j + mSize) * kernel_size];
}
}
// uchar의 형태로 저장한다
dst.data[index] = (uchar)temp;
}
// boundary는 이전 이미지를 복사한다
else dst.data[index] = src.data[index];
}
}
main
이제 앞서 구한 함수를 통해 gaussian blur를 이미지에 입힌다.
시간 계산을 위해서는 opencv TickMeter를 활용한다.
TickMeter 활용 방안 또한 포스트로 나눠서 작성할테니 확인 부탁한다!
우선은 간단하게 1번씩만 계산해서 시간을 확인했다.
코드는 아래와 같다!
// 커널 사이즈, 시그마 선언
#define KERNEL_SIZE 5
#define SIGMA 3.f
int main() {
// 시간 check
vector<TickMeter> tm;
TickMeter tm_now;
float pTime;
float gKernel[KERNEL_SIZE * KERNEL_SIZE];
// 가우시안 커널 생성
gaussian_kernel_2D(KERNEL_SIZE, SIGMA, gKernel);
// 이미지를 흑백으로 불러온다
Mat input_img = imread("Knee.jpg", IMREAD_GRAYSCALE);
Mat ocv_output_img;
Mat my_output_img;
// ---------------- ocv 가우시안
tm.push_back(tm_now);
tm.at(0).start();
GaussianBlur(input_img, ocv_output_img, Size(KERNEL_SIZE, KERNEL_SIZE), SIGMA);
tm.at(0).stop();
pTime = tm.at(0).getTimeMilli();
printf("processing time : %.3f ms\n", pTime);
// ---------------- ocv 가우시안
// ---------------- my 가우시안
tm.push_back(tm_now);
tm.at(1).start();
serial_gaussian_blur_2D(input_img, my_output_img, KERNEL_SIZE, gKernel);
tm.at(1).stop();
pTime = tm.at(1).getTimeMilli();
printf("processing time : %.3f ms\n", pTime);
// ---------------- my 가우시안
imshow("ocv gaussian", ocv_output_img);
imshow("my gaussian", my_output_img);
waitKey(0);
return 0;
}
Result
왼쪽이 opencv, 오른쪽이 내가 만든 gaussian blur의 결과이다.
pixel 단위로는 생각보다 많은 차이가 있지만 우선 육안으로는 거의 차이가 보이지 않는다.
opencv는 단순히 내가 한 것과 다른 작업이 추가되어 처리 속도에서 큰 이점이 있는 것을 확인할 수 있다.
opencv processing time : 1.553 ms
my processing time : 6.259 ms
거의 5배 차이가 난다.
조금은 성능 향상을 위해 OpenMP를 통하여 간단하게 속도 향상을 시켜보려고 한다.
원래는 하나로 빼고 하려고 했는데... 정말 간단하게 사용하여서 이번 포스트에 작성했다.
OpenMP
한번 간단하게 OpenMP를 진행하여 시간 비교를 진행해보았다. 확실히 많은 성능 향상이 확인된다.
OpenMP는 CPU core를 병렬적으로 나누고, 할당하여 계산의 속도를 향상시킨다.
보통 section으로 나눠서 계산을 진행하지만 이번 포스트에서는
#pragma omp parallel for 만을 이용하여 속도적인 이점을 가져오겠다.
솔루션 속성 -> C/C++ -> 언어 에서 OpenMP 지원을 "예"로 변경한다.
이후 serial_gaussian_blur_2D 코드를 아래와 같이 변경한다.
정말 단순하게 #pragma omp parallel for 만으로 병렬 처리를 진행한다.
void omp_serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel) {
int w = src.cols;
int h = src.rows;
dst = Mat(h, w, CV_8U);
int mSize = kernel_size / 2;
float temp;
// OpenMP 병렬 연산 처리 진행
#pragma omp parallel for
for (int index = 0; index < w * h; index++) {
temp = 0.f;
// kernel_size에 해당하지 않는 boundary 부분 예외 처리
if ((index % w >= mSize) && (index / w >= mSize) && (index % w < w - mSize) && (index / w < h - mSize)) {
for (int j = -mSize; j <= mSize; j++) {
for (int i = -mSize; i <= mSize; i++) {
// float으로 계산한다
temp += (float)src.data[index + i + j * w] * kernel[i + mSize + (j + mSize) * kernel_size];
}
}
// uchar의 형태로 저장한다
dst.data[index] = (uchar)temp;
}
// boundary는 이전 이미지를 복사한다
else dst.data[index] = src.data[index];
}
}
최종적으로 serial 함수에 비해 2배 정도만의 차이가 나도록 만들었다!
opencv processing time : 1.166 ms
my processing time : 5.927 ms
my omp processing time : 2.759 ms
아래는 최종 모든 코드이다.
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <vector>
using namespace cv;
using namespace std;
// 커널 사이즈, 시그마 선언
#define KERNEL_SIZE 5
#define SIGMA 3.f
void gaussian_kernel_2D(int kernel_size, float sigma, float *kernel);
void serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel);
void omp_serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel);
int main() {
// 시간 check
vector<TickMeter> tm;
TickMeter tm_now;
float pTime;
float gKernel[KERNEL_SIZE * KERNEL_SIZE];
// 가우시안 커널 생성
gaussian_kernel_2D(KERNEL_SIZE, SIGMA, gKernel);
// 이미지를 흑백으로 불러온다
Mat input_img = imread("Knee.jpg", IMREAD_GRAYSCALE);
Mat ocv_output_img;
Mat my_output_img;
Mat my_omp_output_img;
// ---------------- ocv 가우시안
tm.push_back(tm_now);
tm.at(0).start();
GaussianBlur(input_img, ocv_output_img, Size(KERNEL_SIZE, KERNEL_SIZE), SIGMA);
tm.at(0).stop();
pTime = tm.at(0).getTimeMilli();
printf("opencv processing time : %.3f ms\n", pTime);
// ---------------- ocv 가우시안
// ---------------- my 가우시안
tm.push_back(tm_now);
tm.at(1).start();
serial_gaussian_blur_2D(input_img, my_output_img, KERNEL_SIZE, gKernel);
tm.at(1).stop();
pTime = tm.at(1).getTimeMilli();
printf("my processing time : %.3f ms\n", pTime);
// ---------------- my 가우시안
// ---------------- my omp 가우시안
tm.push_back(tm_now);
tm.at(2).start();
omp_serial_gaussian_blur_2D(input_img, my_omp_output_img, KERNEL_SIZE, gKernel);
tm.at(2).stop();
pTime = tm.at(2).getTimeMilli();
printf("my omp processing time : %.3f ms\n", pTime);
// ---------------- my omp 가우시안
imshow("ocv gaussian", ocv_output_img);
imshow("my gaussian", my_output_img);
imshow("my omp gaussian", my_omp_output_img);
/*imwrite("1.png", ocv_output_img);
imwrite("2.png", my_output_img);
imwrite("3.png", my_omp_output_img);*/
waitKey(0);
return 0;
}
void gaussian_kernel_2D(int kernel_size, float sigma, float *kernel) {
int mSize = kernel_size / 2;
float sum = 0.0f;
// kernel_size * kernel_size에 알맞은 값을 저장한다
for (int y = -mSize; y <= mSize; y++) {
for (int x = -mSize; x <= mSize; x++) {
kernel[x + mSize + (y + mSize) * kernel_size] =
exp(-(pow(x, 2) + pow(y, 2)) / (2 * sigma * sigma)) / (2 * (float)CV_PI * sigma * sigma);
sum += kernel[x + mSize + (y + mSize) * kernel_size];
}
}
// kernel 총 덧셈 시 1이 되도록 각각 normalization 진행
for (int i = 0; i < kernel_size * kernel_size; i++) {
kernel[i] /= sum;
}
}
void serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel) {
int w = src.cols;
int h = src.rows;
dst = Mat(h, w, CV_8U);
int mSize = kernel_size / 2;
float temp;
for (int index = 0; index < w * h; index++) {
temp = 0.f;
// kernel_size에 해당하지 않는 boundary 부분 예외 처리
if ((index % w >= mSize) && (index / w >= mSize) && (index % w < w - mSize) && (index / w < h - mSize)) {
for (int j = -mSize; j <= mSize; j++) {
for (int i = -mSize; i <= mSize; i++) {
// float으로 계산한다
temp += (float)src.data[index + i + j * w] * kernel[i + mSize + (j + mSize) * kernel_size];
}
}
// uchar의 형태로 저장한다
dst.data[index] = (uchar)temp;
}
// boundary는 이전 이미지를 복사한다
else dst.data[index] = src.data[index];
}
}
void omp_serial_gaussian_blur_2D(Mat &src, Mat &dst, int kernel_size, float *kernel) {
int w = src.cols;
int h = src.rows;
dst = Mat(h, w, CV_8U);
int mSize = kernel_size / 2;
float temp;
// OpenMP 병렬 연산 처리 진행
#pragma omp parallel for
for (int index = 0; index < w * h; index++) {
temp = 0.f;
// kernel_size에 해당하지 않는 boundary 부분 예외 처리
if ((index % w >= mSize) && (index / w >= mSize) && (index % w < w - mSize) && (index / w < h - mSize)) {
for (int j = -mSize; j <= mSize; j++) {
for (int i = -mSize; i <= mSize; i++) {
// float으로 계산한다
temp += (float)src.data[index + i + j * w] * kernel[i + mSize + (j + mSize) * kernel_size];
}
}
// uchar의 형태로 저장한다
dst.data[index] = (uchar)temp;
}
// boundary는 이전 이미지를 복사한다
else dst.data[index] = src.data[index];
}
}
'IT > Computer Vision' 카테고리의 다른 글
[OpenCV] cv::Gaussian Blur 함수 사용하기 (0) | 2021.08.08 |
---|