2018-02-01 12:11:33

▶ 영상에 소금-후추 잡음 첨가


오늘은 영상에 소금-후추 잡음을 넣어보려고 한다. 


1. 영상 읽기

2. 소금-후추 잡음 넣을 픽셀 랜덤하게 선택

3. 잡음의 형태가 소금 또는 후추인지 랜덤하게 선택

4. 선택된 픽셀에 접근해서 소금 또는 후추 넣기

5. 소금-후추 잡음 첨가된 영상 전시


코드는 아래와 같다. 주석을 잘 살펴보자. 


#include <iostream>

#include <opencv2/core.hpp>

#include <opencv2/highgui.hpp>

#include <stdlib.h> // srand 함수 사용을 위해서

#include <time.h> // time 함수 사용을 위해서


void salt_pepper(cv::Mat image, int n) // 소금-후추 잡음 첨가 함수

{

int i, j;


srand((int)time(NULL));


for (int k = 0; k < n; k++) 

{

i = rand() % image.cols; // 이미지의 열크기 내에서 랜덤 수 생성, x 좌표값

j = rand() % image.rows; // 이미지의 행크기 내에서 랜덤 수 생성, y 좌표값

std::cout <<"at (" << j << ", " << i << "), add salt or pepper!" << std::endl; // 랜덤하게 결정된 픽셀 위치 출력, (x, y)


int salt_or_pepper = (rand() % 2) * 255; // 랜덤하게 0 또는 255, 0이면 후추, 255면 소금


if (image.type() == CV_8UC1) // 그레이레벨 영상이라면

{

image.at<uchar>(j, i) = salt_or_pepper; // 랜덤하게 선택된 픽셀에 0 또는 255을 대입 

}

else if (image.type() == CV_8UC3) // 3채널 컬러 영상이라면

{

        // 랜덤하게 선택된 픽셀에 0 또는 255을 대입, 흰색 또는 검정색을 만들기 위해 세 컬러 채널에 동일한 것이 들어감. (0, 0, 0) 또는 (255, 255, 255). 

                        image.at<cv::Vec3b>(j, i)[0] = salt_or_pepper; // B 채널

image.at<cv::Vec3b>(j, i)[1] = salt_or_pepper; // G 채널

image.at<cv::Vec3b>(j, i)[2] = salt_or_pepper; // R 채널

}

}

}


int main() // 메인 함수

{

cv::Mat img = cv::imread("huoguo.jpg"); // 이미지 읽기


cv::imshow("before", img); // 원본 이미지 전시

cv::waitKey(0);


salt_pepper(img, 5000);  //img에 5000알의 소금 또는 후추를 뿌려라!

cv::imshow("after", img); // 결과 이미지 전시


cv::waitKey(0);


    return 0;

}


소금-후추 잡음 첨가 전후 이미지를 살펴보자. 까맣고 하얀 점들이 영상 곳곳에 뿌려진 것을 확인할 수 있다. 




그리고 콘솔에는 소금-후추 잡음이 첨가된 픽셀의 위치값들이 출력되었다. 




▶ 좀 더 알고 넘어갈 것들


1) 랜덤 수(난수) 생성 관련


랜덤하게 소금-후추를 뿌릴 픽셀을 선정하기 위해서 랜덤 수를 생성하는 코드를 작성해줘야 한다. 나는 C언어를 공부할 때의 기억을 되살려서 랜덤 수를 생성했다.  


#include <stdlib.h> 

#include <time.h>


일단 이 두개의 해더파일을 포함시켜줘야 한다. stdlib.h는 srand() 함수를 호출해주기 위해서 필요하고, time.h는 time() 함수 때문에 필요하다. 그리고 랜덤 수를 생성하기 전에 한 줄의 코드가 더 필요하다. 


srand((int)time(NULL));


여기 안에는 두 개의 함수가 사용되고 있다. 일단 srand()는 하나의 인자를 전달받는데, 이 인자를 씨드값이라고 한다. 이 씨드값에 따라 rand() 함수 호출 시 생성되는 난수들이 달라진다. 지금 여기서는 씨드값으로 (int)time(NULL)를 넣어주고 있는데, time() 함수는 현재 컴퓨터의 시간과 1970년 1월 1일과의 시간 차이를 초 단위로 계산해서 반환해 준다. 컴퓨터의 시간은 시간이 흐를수록 계속 변하므로 1970년 1월 1일과의 차이도 계속 변한다. 이 말은 곧 씨드값이 계속해서 변한다는 것이다. 결과적으로 우리는 진정한 난수를 얻게 된다. 이제 난수를 얻기 위해서 아래와 같은 간단한 명령어만 필요하다. 


rand()



2) 나머지 연산자 %


랜덤하게 픽셀 위치를 얻기 위해 rand()와 나머지 연산자를 활용했다. 


i = rand() % image.cols;

j = rand() % image.rows;


rand() 함수로 생성된 난수를 이미지의 가로 길이로 나눈 나머지 값을 x 좌표값으로 설정해주었다. 이미지의 가로 길이가 여기서는 800이므로 어떤 난수가 생성되었든 800으로 나눠주면 나머지는 0~799 중에 하나가 될 것이다. 마찬가지로 y좌표값은 rand()함수로 생성된 난수를 이미지의 세로 길이로 나눈 나머지 값으로 설정해주었다. 여기서는 세로 길이가 530이므로 0~529의 값 중 하나가 y좌표값으로 선정된다. 결과적으로 이미지 내에 있는 픽셀 중에 하나가 랜덤하게 선택된다. 이곳에 소금-후추 잡음을 첨가해주었다. 


소금인지 후추인지 결정할 때도 rand()와 나머지 연산자를 활용했다. 방식은 동일하다. 단 이번에는 0과 255 둘 중 하나만 랜덤하게 선택해줘야 한다. 따라서 이번에는 rand()로 얻은 난수를 2로 나눠서 나머지가 0 또는 1이 되게 한 다음에 255를 곱해주었다. 나머지가 0이면 그대로 0이 될 것이고, 1이면 255가 될 것이다. 



3) type 메소드


type 메소드로 입력된 영상이 그레이 레벨인지 컬러인지 확인한다. 위에서 입력해준 영상은 컬러였기 때문에 image.type() == CV_8UC3이 true가 되어서 else if 내의 명령들을 실행했다. 



4) 영상 원소 접근 관련


영상 원소에 접근할 때는 at 메소드를 사용할 수 있다. image라는 이름을 갖고 있는 그레이레벨 영상의 100번째 행, 200번째 열에 존재하는 픽셀의 화소값을 255, 즉 흰색으로 대입하는 경우는 아래와 같이 코딩한다. 


image.at<uchar>(100, 200) = 255;


보다시피 at 메소드를 호출할 때는 영상의 원소 타입을 지정해야 한다. 그레이레벨 영상이므로 uchar, 즉 unsigned character이다. 부호없는 8비트 값이다. 


컬러 영상의 경우에는 세개의 채널에 각각 화소값들을 모두 넣어줘야 한다. 예를 들어 300번째 행, 100번째 열에 존재하는 픽셀을 완전한 파란색으로 만들어주고 싶다면 B채널에 255, G채널과 R채널에는 0을 대입해주면 된다. 컬러 영상은 cv::Vec3b, 즉 uchar 타입의 벡터로 원소 타입을 지정해준다. 세 개의 부호 없는 8비트의 벡터라는 뜻이다. 


image.at<cv::Vec3b>(300, 100)[0] = 255;

image.at<cv::Vec3b>(300, 100)[1] = 0;

image.at<cv::Vec3b>(300, 100)[2] = 0;


이것을 아래와 같이 한줄로 표현할 수도 있다. 동일한 결과를 산출해낸다. 


image.at<cv::Vec3b>(300, 100) = cv::Vec3b(255, 0, 0);




<참고자료>

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

[2] 윤성우, "C 프로그래밍", 프리렉