2018-03-07 13:46:45

▶ 영상 선명하게 만들기


사진을 찍다보면 살짝 흔들리거나 초점이 잘 안 맞아서 뿌연 영상이 찍힐 때가 종종 있다. 이런 영상을 선명하게 만드는 기법 중에 가장 간단한 것은 영상에 라플라시안 커널로 필터링해주는 것이다. 그러면 밝기값이 센 픽셀은 더 세게, 약한 픽셀은 더 약하게 해줌으로 결과적으로 엣지가 더 두드러지게 된다. cv::filter2D 함수를 이용하면 쉽게 필터링할 수 있지만, 좀 더 영상의 구조와 특징을 이해하기 위해서 책에서 소개한 방식으로 코딩했다. 


1. 영상 읽기 및 띄우기

2. 선명화 된 영상 저장할 공간 마련 - create 메소드

3. 선명화 연산 - sharpen 함수

4. 선명화된 영상 띄우기


아래 코드를 한번 복붙해서 실행한 후에 코드에 대해 이해해가는 것을 추천한다. 


#include <iostream>

#include <opencv2\highgui.hpp>

#include <opencv2\core.hpp>


void sharpen(const cv::Mat &image, cv::Mat &result);


int main()

{

cv::Mat before = cv::imread("robot.jpg");

cv::imshow("Before", before);


cv::Mat after;

after.create(before.rows, before.cols, before.type()); 


sharpen(before, after);


cv::imshow("After", after);

cv::waitKey(0);


        return 0;

}


void sharpen(const cv::Mat &image, cv::Mat &result)

{

int nchannels = image.channels(); // 채널 개수 얻기, 컬러 영상이므로 여기서는 3.


// 모든 행 대상 (처음과 마지막 제외)

for (int j = 1; j < image.rows - 1; j++)

{

const uchar* previous = image.ptr<const uchar>(j - 1); // 이전 행 주소

const uchar* current = image.ptr<const uchar>(j); // 현재 행 주소

const uchar* next = image.ptr<const uchar>(j + 1); // 다음 행 주소


uchar* output = result.ptr<uchar>(j); // 결과 이미지의 현재 행 주소


for (int i = nchannels; i < (image.cols - 1)*nchannels ; i++) // int i = 3; i < 397*3; i++, 왜 3부터 시작할까? 그리고 왜 397*3-1까지만 할까? 아래 설명 참고. 

{

// 선명화 연산자 적용

*output++ = cv::saturate_cast<uchar>(5 * current[i] - current[i - nchannels] - current[i + nchannels] - previous[i] - next[i]); // *output++의 의미는? saturate_cast의 역할은? 아래 설명 참고. 

}

}


//처리하지 않는 화소를 검정색으로 설정

result.row(0).setTo(cv::Scalar(0, 0, 0));

result.row(result.rows - 1).setTo(cv::Scalar(0, 0, 0));

result.col(0).setTo(cv::Scalar(0, 0, 0));

result.col(result.cols - 1).setTo(cv::Scalar(0, 0, 0));

}


원본 영상과 결과 영상은 아래와 같다. 


원본 및 결과 이미지


원본 이미지와 결과 이미지를 비교해보면 한결 선명해진 것을 확인할 수 있다.  




▶ 좀 더 알고 넘어갈 것들


1) sharpen 함수


정의한 sharpen 함수에 대해 설명하겠다. 일단 첫번째 for 문을 보면, 첫번째 행과 마지막 행을 제외하고 작업한다. 왜냐하면 윗 행과 아래 행이 존재할 때만 선명화 작업을 해줄 수 있기 떄문이다(이 방식으로는). 그리고 두번째 for문은 열과 관련된 것인데, 첫번째 열과 마지막 열을 제외하고 작업한다. 마찬가지로 왼쪽 열과 오른쪽 열이 존재할 때만 선명화 작업을 해줄 수 있기 때문이다.  


for (int i = nchannels; i < (image.cols - 1)*nchannels ; i++)


이 부분은 좀 더 설명이 필요할 것 같다. nchannels 부터 (image.cols-1)*nchannels - 1까지 반복한다는 것인데, 여기서 nchannels는 3이고, image.cols는 398이므로 다시 적으면, 3부터 397*3-1까지 반복한다는 것이다. 왜 3부터일까? 그 이유는 첫번째 열에 b, g, r 세 개의 픽셀값들이 0번째, 1번째, 2번째 요소에 존재하기 떄문이다. 397*3 -1 까지인 이유는 마지막 열의 b, g, r 세 개의 픽셀값들을 제외하기 위함이다. 


이렇게 연산을 해줄 범위를 설정한 후에 선명화 연산자를 적용한다. 


*output++ = cv::saturate_cast<uchar>(5 * current[i] - current[i - nchannels] - current[i + nchannels] - previous[i] - next[i]);


일단 cv::saturate_cast<uchar>는 허용된 화소값의 범위를 벗어나지 않게 해주는 친구다. 만약 255 이상이면 255가 되게, 0이하면 0이 되게 해줌으로 허용된 화소값 내에 머무르게 한다.  


5*current[i] - current[i - nchannels] - current[i + nchnnels] - previous[i] - next[i]가 선명화 작업의 핵심인데 일단 현재 픽셀의 값에 5를 곱한 후 상하좌우에 있는 픽셀의 값들을 빼준다. 만약 5개 픽셀의 값이 모두 동일하다면 아무 변화가 없을 것이다. 반면 현재 픽셀의 값이 주변 픽셀의 값보다 크다면 그것은 더 부각될 것이고, 작다면 더 작아질 것이다. 결과적으로 엣지가 부각된다. 


*output++에 대해서 마지막으로 설명하겠다. output에는 결과 이미지의 현재 행의 주소가 저장되어 있는데, *연산자로 그 주소 안에 있는 값을 불러온다. 현재 행의 첫번째 열의 b 채널값을 불러올 것이다. 그리고 ++연산자로 인해 루프가 한번 돌았을 때는 첫번째 열의 g 채널값을 불러올 것이다. 이런 식으로 하나하나 접근해서 선명화 작업 결과를 저장한다. 



2) row 메소드와 col 메소드, 그리고 setTo 메소드


row 메소드는 하나의 행을, col 메소드는 하나의 열을 관심영역으로 지정한다.  


result.row(0).setTo(cv::Scalar(0, 0, 0));


위 코드는 result 영상의 0번째 행은 검정색으로 설정하겠다는 것이다. setTo 메소드는 행렬의 모든 요소에 설정해준 값을 할당한다. 




<참고자료>

[1] 로버트 라가니에 지음, 이문호 옮김, "OpenCV를 활용한 컴퓨터 비전 프로그래밍 3/e", 에이콘