━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 13. 시각 정보를 다루어보자

이번 시간의 목차

1. 이미지 데이터를 자세하게 알아보자!

2. 맷플롯립으로도 간단한 이미지를 그릴 수 있다!

3. 좀 더 간편한 이미지 도구는 없을까?

4. OpenCV의 다양한 기능을 활용해 보자!

5. 사진을 합성하고 마스크를 씌워 보자!

6. 원하는 색깔이 있는 픽셀만 뽑아내고 싶어!

7. 이미지에 필터를 씌워 보자!

8. 필터링으로 잡음을 제거해 보자!

9. 원하는 곳만 남기려면 어떻게 해야 할까?

10. 윤곽선을 뽑아내 보자!

11. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

이미지 데이터를 자세하게 알아보자!

 

Aditya Vyas, https://unsplash.com/@aditya1702

 

 이미지(Image)는 원래 라틴어 이마고(Imago)에서 온 말로, 무언가를 닮도록 인위적으로 만든 것을 뜻한다. 옛날에는 그림이나 조각을 통해 사물과 사람을 흉내냈기 때문에 이미지라고 하면 그림이나 조각상을 뜻하는 경우가 많았는데, 카메라가 발명된 이후로는 사진이 대표적인 이미지의 한 종류가 되었다. 

 

 디지털 세상에서 이미지는 색상을 표현하는 점들이 모여 한 장의 이미지가 되는 비트맵(Bitmap) 방식과, 점, 곡선, 면이 수학적인 식으로 표현된 벡터(Vector) 이미지로 나뉘는데, 우리는 이 중에서도 비트맵 방식에 대해 다룰 것이다. 

 

 

간단하게 그림판으로 예시를 들어보았다. 

비트맵 방식의 이미지는 그림의 기본 구성요소를 의미하는 픽셀(Pixel, 그림판에서 격자무늬 한 칸에 해당한다.)로 이루어진다.

 

Black and White Image Representationin Binary, https://learnlearn.uk/binary/black-white-image-representation-binary/

 

 가장 간단한 형태의 비트맵 이미지는 아래와 같이 한 픽셀을 0과 1로 표현하는 이미지이다. 위의 예시에서는 0은 이미지 요소가 없음을 뜻하고, 1은 있음을 의미하는 검은색으로 나타낸다. 비트맵 방식은 이와 같은 형태로 2차원 배열을 구성한 다음 이를 화면에 점으로 나타낸다. 

 

 

 하나의 픽셀이 색상(Hue) 없이 밝기(Brightness) 정보만 있는 이미지는 회색조(Grayscale)라 한다. 쉽게 생각하면 흔히 '흑백 사진'이라고 부르는 그 이미지이다. 이때 밝기 정보만 표현하는 하나의 채널(Channel)로 픽셀을 표현할 수 있다. 보통 하나의 채널을 하나의 바이트로 표현하고, 위 그림은 0단계에서 255단계까지의 회색조 이미지를 나타낸다.  

 

헐... 그리고 나서 보니까 모자랑 안경 색깔이 틀렸다... 

 

이미지는 가로로 w 개의 열과 세로로 h 개의 행을 가진 2차원 행렬과 같은 모양을 갖는다. 이를 그대로 파일에 옮겨 저장할 때는 보통 비트맵을 의미하는 확장자 .bmp를 사용한다. 이 이미지 데이터는 텍스트보다 용량이 매우 커서 한 번 압축한 뒤 저장 용량을 줄여서 저장하는데, 이때 쓰이는 확장자가 우리에게 익숙할 .jpg나 .png이다. jpg는 JPEG 형식이고, .png는 Portable Network Graphics의 약어로, png는 투명 배경까지 지원한다. 

 

 

 색을 표현하는 방법은 여러 가지가 있는데, 대표적인 방법은 빛의 스펙트럼을 대표하는 빨강, 녹색, 파랑의 강도를 섞어서 나타내는 것이다. 이 방법을 RGB 방식이라 한다. 이때 각각의 대표 스펙트럼 별로 하나의 채널이 할당되고, 한 픽셀을 표현하는 데에 모두 3개의 채널이 사용된다. 어떤 경우는 파랑, 녹색, 빨강의 순서로 채널을 배치하고 BGR 방식이라 부를 때도 있다. 

 

컬러 써클, Clip Studio

 

세 개의 채널을 사용할 때는 빛의 스펙트럼이 아니라 색상(Hue)채도(Saturation)밝기(혹은 명도. Brightness, Value)로 표현할 수도 있다. 이 방식을 일반적으로 HSV 방식이라고 부른다. 위의 그림은 특정 H 값에서 명도를 조절하는 V 채널과 채도를 조절하는 S 채널 값을 변경하여 얻을 수 있는 색을 사각형으로 나타낸다. 

 


 

맷플롯립으로도 간단한 이미지를 그릴 수 있다!

 

 이미지 파일을 읽고 화면에 표시하는 것은 다양한 방식으로도 가능하다. 이미지는 2차원 배열의 형태를 띠기 때문에 이 2차원 배열을 다루는 데에 유용한 넘파이를 사용하여 만든 배열을 맷플롯립으로 나타내는 등... 매우 많은 방법이 존재한다. 

 

 우리는 맷플롯립으로 이미지를 화면에 띄워 보자. 

그 전에 이미지 데이터가 필요하다. 실습에서는 d:\ 폴더에 mandrill.png 라는 이미지를 저장하여 사용할 것이다. 경로는 마음대로 설정해도 좋지만, 지금부터 내가 쓸 코드를 그대로 쓸 때도 경로를 모두 수정해줘야 함을 유의하자. 

 

https://github.com/dongupak/DataSciPy/blob/master/data/image/mandrill.png

 

 위의 이미지를 저장했다면 아래의 대화창 실습과 같이 맷플롯립 패키지에서 pyplot 모듈과 image 모듈을 불러오자. 

 

import matplotlib.pyplot as plt
import matplotlib.image as mpimg
 
img = mpimg.imread("D:\mandrill.png")
print(img)
-------------------------------
[[[0.45882353 0.41960785 0.21960784]
  [0.26666668 0.22352941 0.14509805]
  [0.42352942 0.36078432 0.20784314]
  ...
cs

 

이미지를 읽어 오려면 imread() 함수를 호출한다. 

imread() 함수는 파일에 저장된 내용을 읽어서 넘파이 배열로 반환한다. 위의 코드에서는 이 넘파이 배열을 img라는 변수에 넣었다. 각 행은 색깔의 채널이고, 순서대로 B, G, R의 정보를 담는다.

 

어쨌든 이렇게 숫자로만 되어 있으면 우리가 내용을 확인하기 어렵다. 이를 시각적으로 확인하고 싶으면 pyplot의 모듈이 가진 imshow() 함수를 사용한다. 그런 다음에 pyplot의 show() 함수를 불러 그림이 그려지도록 한다. 

 

image_plot = plt.imshow(img)
plt.show()
cs

 


 

좀 더 간편한 이미지 도구는 없을까?

 

 하지만 맷플롯립으로 이미지를 다루려면 저렇게 pyplot과 image를 섞어 쓰면서 처리해야 하는 불편함이 있다. 파이썬은 범용적인 프로그래밍 언어인만큼 이미지를 조작하는 강력한 모듈을 제공한다. 바로 OpenCV 이다. OpenCV는 Open Source Computer Vision을 의미한다. 파이썬 환경에서 OpenCV를 사용하려면 oepncv-python 패키지를 설치해야 한다. 

 

% pip install opencv-python 
cs

패키지를 설치하는 방법은 Ch01 에서 이미 한 번 다뤘었다. 

 

OpenCV는 너무 방대한 라이브러리라서 이 글에서 전부 다루기는 불가능하다. 대신 이 라이브러리를 이용한 이미지 처리의 기본적 개념을 익히도록 할 것이다. 원래 OpenCV는 컴퓨터 비전(Computer Vision)을 위해 만들어졌지만, 현재는 다양한 기게학습 관련 기능으로도 사용된다. 

import cv2
 
img_gray = cv2.imread('D:\mandrill.png', cv2.IMREAD_GRAYSCALE) #회색조로 불러온다.
img_color = cv2.imread('D:\mandrill.png', cv2.IMREAD_COLOR)    #컬러로 불러온다. 
 
cv2.imshow('grayscale', img_gray)
cv2.imshow('color image', img_color)
 
cv2.waitKey(0)           #키보드 입력을 기다림.
cv2.destroyAllWindows()  #모든 창을 없애고 프로그램 종료. 
 
cs

 

OpenCV 모듈은 import cv2 명령을 통해 사용할 수 있다. 

우선 이미지를 imread() 함수로 읽고 이미지 데이터를 저장한다. 첫 번째 인자는 파일명이고, 두 번째 인자는 회색조 혹은 컬러(IMREAD_GRAYSCALE = 0, IMREAD_COLOR = 1)로 읽을지 결정한다. 그 다음 imshow() 함수로 새 창을 만들어 이미지를 띄운다. 첫 번째 인자는 띄우는 창의 이름이고, 두 번째 인자는 그려질 이미지를 받는다. 

 

마지막 두 줄의 코드는 사용자의 키보드 입력을 기다리다가 입력이 들어오면 모든 창을 닫고 프로그램을 종료하는 역할을 한다. 굳이 이런 코드가 있어야 하나? 하는 의문이 들 수도 있지만 이 두 줄의 코드가 없으면 창이 열리자마자 닫혀버리니 꼭 있어야 한다

 


 

OpenCV의 다양한 기능을 활용해 보자!

 

 OpenCV가 있다는 것을 알았으니 이제 이미지를 좀 더 복잡하게 다뤄 보자. 우선 이미지에 그림을 그려보자. 이미지 위에 덧그릴 수 있는 그림의 종류는 여러가지가 있다. 단순한 선부터 화살표 선, 사각형, 텍스트까지 덧그릴 수 있다. 

 

import cv2
 
img = cv2.imread('D:\mandrill.png'1)     #컬러로 맨드릴을 불러온다. 
 
#img에다 직선을 (0,0)에서 (200, 200)까지 긋고, 색은 (0, 0, 255)로 설정하고 두께는 5로 한다.
cv2.line(img, (00), (200200), (00255), 5
 
#img에다 화살표를 (0, 200)에서 (200, 20)까지 긋고, 색은 (0, 0, 255)로 설정하고 두께는 5로 한다.
cv2.arrowedLine(img, (0200), (20020), (00255), 5)
cv2.imshow('lined', img)
 
cs

 

직선과 화살표 직선을 그으려면 line()과 arrowedLine() 함수를 사용한다. 이때 두 함수는 아래와 같은 매개변수를 가진다.

cv2.line(혹은 arrowedLine)( 이미지, 시작지점 좌표, 끝지점 좌표, 색상, 굵기, 선의 종류, 좌표 시프트 )

위의 코드에서는 선의 종류와 좌표 시프트는 쓰지 않았다. 

 

port cv2
 
img = cv2.imread('D:\mandrill.png'1)
 
#img에 (0, 200)에서 (200, 20)까지 사각형을 그린다. 색깔은 (0, 0, 0)이고 두께는 5.
cv2.rectangle(img, (0200), (20020), (000), 5)
 
#img의 (70, 70) 자리에 'mandrill'이라는 (255, 0, 0) 색깔 글씨를 쓴다. 
cv2.putText(img, 'madrill', (7070), fontFace = 2, fontScale = 1, color = (25500))
cv2.imshow('lined', img)
 
cs

 


 

사진을 합성하고 마스크를 씌워 보자!

 

 

 이번에는 이미지 두 개를 섞어보자. 이번 실습에 사용할 이미지는 이 green_back.png 와 iceberg.png이다. 

 

두 이미지를 섞을 때 사용하는 함수는 addWeighted()이다. 

 

image_merged = cv2.addWeighted(image_a, weight_for_a, image_b, weight_for_b)
#image_merged[x, y] 
# = image_a[x, y] * weight_for_a + image_b[x, y] * weight_for_b
cs

 

이 함수를 사용할 때 주의할 점은 두 이미지의 크기가 같아야 한다는 것이다. 그렇지 않으면 cv2.resize() 함수를 사용해서 이미지의 크기를 동일하게 만들어줘야 한다. 

 

createTrackbar(조절할 값의 이름, 부착할 창 이름, 최솟값, 최댓값, 변경 시 호출 함수) 
cs

 

 가중치를 조절하기 위해서 OpenCV에서 제공하는 트랙바(Trackbar)를 사용해보자. 우선 namedWindow()를 이용하여 이름을 가진 창을 하나 만든다. 그리고 createTrackbar를 사용해서 창에 트랙바를 단다. 

 

 

가중치 10 / 50 / 90

import cv2
global img1, img2         #두 이미지를 프로그램 전체에서 사용 
 
def on_change_weight(x):  #상단 트랙바가 움직이면 이 함수가 작동한다. 
    weight = x / 100      # x값이 0에서 100 사이이므로 100으로 나누어 0~1로 바꿈.
    img_merged = cv2.addWeighted(img1, 1 - weight, img2, weight, 0)
    cv2.imshow('Display', img_merged)
 
cv2.namedWindow('Display')
cv2.createTrackbar('wieght''Display'0100, on_change_weight)
 
img1 = cv2.imread('D:\green_back.png')
img2 = cv2.imread('D:\iceberg.png')
img1 = cv2.resize(img1, (300400)) #이미지 크기를 300 * 400 px 로 바꿈. 
img2 = cv2.resize(img2, (300400)) 
 
cs

 

 

 OpenCV에서는 이미지를 비트 단위로 연산할 수도 있다. 초반에 배운 and, or, not, xor 연산을 기억하는가? cv2 모듈에는 bitwise_and, bitwise_or, bitwise_not, bitwise_xor 함수가 있다. 

 

연산자 의미 설명
& 비트 단위 AND 두 개의 피연산자의 해당 비트가 모두 1이면 1, 아니면 0
| 비트 단위 OR 두 피연산자의 해당 비트 중 하나라도 1이면 1, 아니면 0
^ 비트 단위 XOR 두 개의 피연산자의 해당 비트 값이 같으면 0, 아니면 1
~ 비트 단위 NOT 0은 1로 만들고, 1은 0으로 만든다.

 

 이쯤되면 가물가물할 수도 있어서 Ch03의 비트 연산자를 다시 가져와 봤다. 이미지에서 이 비트 연산이 쓰이는 대표적인 예시가 바로 마스크(Mask)이다. 연산 결과에 따라서 특정한 부분만 남기거나 사라지게 만들 수 있다. 

 

https://github.com/dongupak/DataSciPy/tree/master/data/image

 

 이번 마스크 씌우기 실습에 사용할 이미지는 위의 두 이미지(mask_circle, iceberg)이다. mask_circle.png는 검은 부분이 0을 갖고, 배경의 하얀 부분은 1의 값을 가진다. 연산을 실행하기 전에는 resize(image, (w size, h size)) 함수를 실행하자. 비트 연산은 두 이미지의 사이즈가 같아야 실행할 수 있기 때문에 그림의 크기가 서로 다르다면 resize( ) 함수로 사이즈를 똑같게 만들어 주어야 한다. 

 

AND 연산

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_ANDed = cv2.bitwise_and(mask_image, back_image)
>>> cv2.imshow('AND', mask_ANDed)
cs

 

이미지끼리 AND 연산을 시키면 두 이미지를 겹쳐놓은 뒤에 동일한 위치의 비트가 모두 1인 경우에만 1이 되고, 그렇지 않으면 0이 된다. 마스크로 사용되는 이미지의 0, 즉 검은 부분은 배경 이미지에 관계 없이 모두 0으로 만들어버린다. 마스크의 1, 즉 흰색 부분은 배경 이미지 값이 해당 픽셀의 색을 결정하게 된다. 쉽게 설명하자면 마스크의 흰색 부분은 투명화되고 검은색만 남긴 채 두 이미지를 합치는 것이다.

 

 

OR 연산

 

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_ORed = cv2.bitwise_or(mask_image, back_image)
>>> cv2.imshow('OR', mask_ORed)
cs

 

OR 연산은 마스크의 1인 부분, 즉 흰색에 해당하는 픽셀은 무조건 1이 되고, 마스크의 0인 부분은 배경 이미지의 픽셀에 따라 결정한다. 따라서 비트 단위 OR은 흰색이 그림을 가리는 마스크의 역할을 하게 된다. 

 

 

XOR 연산

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_XORed = cv2.bitwise_xor(mask_image, back_image)
>>> cv2.imshow('XOR', mask_XORed)
cs

 

XOR 연산은 0인 부분은 원래 이미지 값을 유지시키고 1인 부분은 비트를 뒤집기 때문에 위처럼 1(흰색)이었던 부분의 비트가 뒤집혀 색 반전이 일어났음을 볼 수 있다. 

 


 

원하는 색이 있는 픽셀만 뽑아내고 싶어!

 

 이제부터는 OpenCV로 이미지에서 데이터를 뽑아볼 텐데, 그 중에서도 특정 색상이 있는 픽셀만 뽑아보는 작업을 해 보자.  

 이렇게 mandrill.png 에서 푸른색만 뽑아내려 한다. 이런 특정 색상의 픽셀만 추출할 때는 RGB나 BGR 방식 보다는 HSV 방식이 더 적합하다. 

 

컬러 써클, Clip Studio

 

푸른색은 HSV의 H(색상)의 190~260 사이를 차지한다. 따라서 푸른색을 추출하려면 색상 표시를 HSV로 바꾸고 H 값이 190~260도 사이에 있는 픽셀만 추출하면 될 것이다. 

 

이때 한 가지를 주의해야 한다. 이미지에서 색상의 범위를 0에서 360 이상의 정수로 표현하게 되면 한 바이트 이상이 필요하기 때문에 이미지 파일에서는 이 값을 0.5배로 줄여 0도에서 180도 범위로 표현한다. 따라서 푸른색을 추출하려면 95에서 130 사이의 색상 값을 가진 픽셀을 찾는다.

 

>>> import numpy as np
>>> import cv2
 
>>> image = cv2.imread('D:\mandrill.png'#이미지 불러오기 
>>> image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) #색 기준 HSV로 전환 
 
>>> blue_low = np.array([9500])       #파랑 최소값 
>>> blue_high = np.array([130255255]) #파랑 최댓값 
 
#inRange(image, low, high)는 image에서 low~high 사이의 픽셀은 1, 나머지는 0으로 표시한다.
>>> my_mask = cv2.inRange(image_hsv, blue_low, blue_high) 
>>> extracted = cv2.bitwise_and(image, image, mask = my_mask) #마스크와 AND
 
>>> cv2.imshow('original', image)
>>> cv2.imshow('mask', my_mask)
>>> cv2.imshow('extracted', extracted)
cs

 

어떤 범위에 해당하는 픽셀을 찾는 것은 inRange(image, low, high) 함수가 해 준다. 지정된 범위 내의 픽셀은 1을 반환하고, 나머지는 0을 반환해서 예시에서는 파란 부분이 하얀색으로, 나머지 색이 검은색으로 나타난다.

 

이때 bitwise_and 에서 이미지와 마스크를 바로 합치지 않는 이유는 컬러를 가진 이미지 image와 회색조 이미지인 my_mask가 가진 채널의 수가 달라 배열의 크기가 다르기 때문이다. 같은 이미지로 and를 하면서 특정 영역에서만 연산이 일어나도록 마스킹을 적용하는 방식을 따라야 한다. 

 


 

이미지에 필터를 씌워 보자!

 

p = p1 * a + p2 * b + p3 * c + ... + p8 * h + p9 * i

 

 이미지를 조작할 때는 커널(Kernel)이라고 불리는 작은 행렬을 사용하여 필터링(Filtering)하는 방법이 있다. 커널을 통해 이미지를 변형하는 것은 위와 같은 방식을 사용한다. p5 픽셀의 값을 커널이 적용된 이미지의 p로 바꾸는 작업에서는 커널과 대응하는 원본 이미지의 값들이 서로 곱해지고 더해진 다음 새로운 이미지의 픽셀값 p를 구하게 된다. 커널을 움직여가며 이 작업을 모든 픽셀에 적용하면 이미지 전체의 변경이 이루어진다. 주변 색을 참조해서 가운데 색이 정해지는 것이기 때문에 커널이 커질수록 이미지는 흐려진다

 

 커널을 생성하려면 넘파이를 이용한 행렬을 만들어야 한다. 

 

>>> kernel = np.ones((33,), np.float32) / 9
>>> cv2.filter2D(src, ddepth, kernel[ dst [ anchor[ delta[ borderType]]]])
#src = 변경할 이미지
#ddepth = 이미지 깊이, -1은 주어진 입력 이미지와 동일한 값
cs

 

 kernel = np.ones((3, 3), np.float32) / 9 로 만든 커널을 적용하는 것이 filtef2D() 함수이다. 처음에 변경할 이미지가 입력되고, ddepth에 이미지 깊이를, 그리고 커널 행렬을 입력한다. 뒤에 나타나는 매개변수는 필수로 들어가야 하는 것은 아니고, 이에 대한 자세한 설명은 아래와 같다. 

 

*dst : 출력 결과 또는 영상을 나타내는 변수 이름. 

* anchor: 필터링할 이미지 픽셀의 기준이 커널의 어디에 있어야 하는지 정하는 커널의 앵커(=닻). 디폴트값은 커널의 중심(-1, 1)이다. 

*delta : 필터링된 픽셀에 delta만큼의 값을 더하고 dst에 저장한다.

 

 

원본 / 3*3 커널 / 9*9 커널 적용

 

import numpy as np
import cv2
 
org = cv2.imread('D:\mandrill.png'1)
 
kernel1 = np.ones((33), np.float32) / 9  #주변 8개 픽셀과의 평균 
kernel2 = np.ones((99), np.float32) / 81  #주변 80개 픽셀과의 평균 
 
averaged33 = cv2.filter2D(org, -1, kernel1)
averaged99 = cv2.filter2D(org, -1, kernel2)
 
cv2.imshow('original', org)
cv2.imshow('filtered1', averaged33)
cv2.imshow('filtered2', averaged99)
 
cs

 

커널의 크기가 커질수록 이미지가 더 흐려지는 것을 볼 수 있다. 

이렇게 흐려지는 효과를 상자 필터(Box Filter) 혹은 블러 효과(Blur Effect)라고 하는데, 이 블러 효과는 워낙 자주 쓰는 필터이다보니 이미 함수로 만들어져 있다. 

 

kernel1_1 = np.ones((33), np.float32) / 9
kernel1_2 = cv2.blur(image, (33)) 
cs

 

위 두 코드를 image라는 이미지에 적용하면 동일한 결과를 가져온다. 

 


 

필터링으로 잡음을 제거해 보자!

 

 OpenCV에는 방금 살펴본 blur() 함수 뿐만 아니라 다양한 필터를 적용할 수 있는 함수가 많이 있다. 특히 이미지의 잡음(Noise)을 줄이는 블러링(Blurring)을 위해 여러 종류의 필터를 적용할 수 있다.

 

Aditya Vyas, https://unsplash.com/@aditya1702

대표적인 필터가 가우스(Gauss) 필터를 적용하는 가우시안 블러(Gaussian Blur)이다. 가우스 필터는 우리가 앞서 사용해 본 필터처럼 커널 영역의 모든 픽셀에 동일한 중요성을 부여하는 것이 아니라, 중심 픽셀에는 더 높은 중요도를 부여하고, 중심에서 멀어질수록 중요성을 낮춰서 더한다. 중요도를 가중치(Weight)라고 부르고, 이렇게 더하는 것을 가중합(Weighted Sum)이라 한다. 이 가중치를 가우스 함수(=정규분포(Normal Distribution))를 통해 결정하는 것이 가우스 필터이다. 

 

 하지만 실제 이미지를 적용할 때는 연속함수를 적용하지 않고 픽셀 공간에서 정해지는 이산적(Discrete) 근사치를 사용한다. 즉, 5*5 커넬로 예시를 들면 위와 같다. 이런 커널을 일일이 만들어 쓰면 효율이 굉장히 떨어질 것이다. 그래서 우리는 OpenCV의 GaussianBlur() 함수를 쓴다.

 

dst = cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
#src = 입력 이미지
#ksize = 커널 사이즈
#sigmaX = x축 방향 정규분포의 표준편차 (가우스 함수 모양) 
cs

 

사용 방법은 아까 살펴본 filter2D와 비슷하다. src는 입력 이미지고, ksize는 커널의 크기, sigmaX는 x축 방향으로 뻗은 정규분포의 표준편차(Standard Deviation)인데, 이 표준편차의 값에 따라 가우스 함수의 모양이 달라지기 때문에 중요한 수이다. sigmaY도 설정할 수 있는데, 디폴트 값은 0으로, 0으로 설정해두면 x축 표준편차와 동일한 값으로 처리된다. 상자 필터와 비슷한 효과를 얻으려면 sigmaX를 크게 설정하면 된다. 

 

원본 / 3*3 커널 / 9*9 커널 

 

import numpy as np
import cv2
 
org = cv2.imread('D:\mandrill.png'1)
 
averaged33 = cv2.GaussianBlur(org, (33), 1)
averaged99 = cv2.GaussianBlur(org, (99), 1)
 
cv2.imshow('original', org)
cv2.imshow('Gaussian33', averaged33)
cv2.imshow('Gaussian99', averaged99)
 
cs

 

위처럼 가우시안 블러를 사용하면 이전에 비해서 부드러운 느낌이 들기는 하지만 원본 이미지의 상이 크게 왜곡없이 나타난다.

 

 

가우시안 블러는 조금 약하게 흐려지지만, 이보다 훨씬 세게 흐려지는 필터도 존재한다. 이는 잡음을 제거하는 방법으로 자주 쓰이는데, 그 이름은 바로 중앙값 흐림(Median Blur) 양방향 필터(Bilateral Filter)이다. 

 

original_image = cv2.imread('D:\salt_pepper.png'0)
result_image = cv2.medianBlur(original_image, 5)
 
cs

 

 중앙값 흐림을 수행하려면 medianBlur() 함수를 호출한다. 이 함수는 두 개의 매개변수를 갖는데, 첫 번째 인자는 입력 이미지이고, 두 번째는 정수 n을 받는다. 정수 n * n 크기의 필터 영역에서 중앙값을 찾아 그 중앙값을 해당 픽셀의 값으로 설정한다. 이 흐림 효과는 이미지에서 주위에 비해 튀는 작은 잡음(소금과 후추 잡음, Salt and Pepper Noise)을 제거하는 데에 효과적이다. 

 

 

 

>>> org = cv2.imread('D:\mandrill.png'1)
>>> result_image2 = cv2.bilateralFilter(org, 95050)
cs

 

양방향 필터를 사용하면 잡음은 제거하면서 이미지의 특징은 더 잘 유지한다

첫 번째 매개변수는 원본 이미지를, 두 번째 매개변수에는 필터가 적용되는 커널 영역의 지름이며, 뒤의 매개변수 50, 50은 각각 sigmaColor, sigmaSpace의 자리에 들어간 것이다. sigmaColor는 픽셀의 값에 따라 결정되는 필터이고, sigmaSpace는 가우스 필터와 같은 공간 필터를 조정한다. 이 값들과 커널 중심 픽셀과의 차이가 클수록 영향력이 커진다. 

 

 

오늘 우리가 써 본 필터를 총 정리하자면 이런 느낌이다. 

 


 

원하는 곳만 남기려면 어떻게 해야 할까?

 

 이미지를 다룰 때 특정한 임계값(Threshold) 이상인 값은 유효한 값으로, 그렇지 않은 값은 유효하지 않은 값으로 구분하는 작업이 필요할 때가 있다. 이런 작업을 이진화(Binarization)이라 한다. 

 

 OpenCV에서는 이런 작업을 threshold() 함수로 수행할 수 있다. 이 함수의 특이한 점은 이진화를 하는 것은 물론 특정 임계치를 넘는 값은 일정한 값으로 모두 바꾸는 일도 한다는 것이다. 

 

>>> ret, result_image = threshold(src_image, thresh_value, 
                                    maxValue, thresh_option) 
 
#scr_image = 원본 이미지
#thresh_value = 임계값
#maxValue = 이진화 후 픽셀의 최댓값 
cs

 

 이 함수는 (사용된 임계값, 임계값 적용 후 새 이미지) 형태의 튜플을 반환한다. thresh_option 을 제외한 매개변수의 설명은 위에 해 두었다. thresh_option 에는 임계치 적용 방식을 결정한다. 선택 가능한 사항은 아래와 같다.

 

  • THRESH_BINARY : 조건을 만족하는 픽셀값을 최댓값으로, 만족하지 않으면 0으로 설정
  • THRESH_BINARY_INV : 조건을 만족하는 픽셀값을 0으로, 만족하지 않으면 최댓값으로 설정
  • THRESH_TRUNC : 조건을 만족하는 픽셀값을 최댓값으로, 만족하지 않으면 원본값으로 설정
  • THRESH_TOZERO : 조건을 만족하는 픽셀값을 원본값으로, 만족하지 않으면 0으로 설정
  • THRESH_TOZERO_INV : 조건을 만족하는 픽셀값을 0으로, 만족하지 않으면 원본값으로 설정 

 

 이제 이 함수를 이용해서 이 이미지에서 촛불만 추출해 보자. 트랙바를 사용해서 임계치를 변경하고, 트랙바에서 얻어온 임계치로 이진화한 뒤 창에 그림을 그린다. 임계값을 높이면 기준을 만족하는 픽셀의 수가 적어지고, 적절하게 높은 임계치를 주면 밝은 촛불 영역만 얻을 수 있을 것이다. 

 

import cv2
global color_image, gray_image
 
#트랙바 조절 시 그 값을 임계치로 설정. 회색조 이미지를 이진화해서 보여줌 
def on_change_threshold(x):
   _, th_image = cv2.threshold(gray_image, x, 255, cv2.THRESH_BINARY)
   cv2.imshow('Thresholding', th_image)
 
#새 창을 생성함
cv2.namedWindow('Thresholding')
cv2.createTrackbar('threshold''Thresholding'0255, on_change_threshold)
 
#원본과 회색조 이미지 
color_image = cv2.imread('D:/candles.jpg', cv2.IMREAD_COLOR)
gray_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2GRAY)
 
#원본 이미지에서 트랙바 조절 시 이진화함 
cv2.imshow('Thresholding', color_image)
 
#키보드 입력 후 프로그램 종료 
cv2.waitKey(0)
cv2.destroyAllWindows()
 
cs

 

상단의 트랙바를 건드리는 즉시 candle.jpg의 색조는 이진화하고, 트랙바를 오른쪽으로 당겨 값을 증가시킬수록 이진화가 심하게 일어난다. 이를 원본 이미지에 적용하면 각 채널별 이진화가 일어나 몇 가지 색으로만 이루어진 이미지를 얻을 수도 있다. 

 

#트랙바 조절 시 그 값을 임계치로 설정.  이미지를 이진화해서 보여줌 
def on_change_threshold(x):
   _, th_image = cv2.threshold(color_image, x, 255, cv2.THRESH_BINARY)
   cv2.imshow('Thresholding', th_image)
cs

 


 

윤곽선을 뽑아내 보자!

 

 지금까지 살펴본 임계치 적용 방식은 픽셀이 주위의 픽셀 값과 어떤 관계를 가지는지 고려하지 않았다. 그런데 때로는 고정된 임계값이 아니라 주변 픽셀이 가진 값을 기준으로 임계값을 정해야 할 때도 있다. 대표적인 예시가 윤곽선이다. 윤곽선은 주위에 비해 튀는 값을 가진 픽셀이므로, 주변 값에따라 임계값을 정해야 한다. 이런 임계값을 적응적 임계값(Adaptive Threshold)이라 한다. 

 

>>> cv2.adaptiveThreshold(img, value, method, thresholdType, blocksize, C)
#img = 입력 이미지
#value = 조건 만족하는 픽셀 최댓값
#method = 임계치 결정 
#thresholdType = THRESH_BINARY 또는 THRESH_BINARY_INV 중 하나
#blocksize = 평균값 구하는 범위
#C = 임계치 보정용 수 
cs

 

적응적 임계값을 사용하는 함수는 adaptiveThreshold() 이다. 이 중에서 method 가 임계치를 결정하는데, 방법은 2가지가 있다. 

 

  • ADAPTIVE_TRHESH_MEAN_C : 픽셀 중심 blocksize * blocksize 구역 내 픽셀 평균값 - C가 임계치
  • ADAPTIVE_THRESH_GAUSSIAN_C : 픽셀 중심 blocksize * blocksize 구역 내 픽셀에 가우스 가중치 적용 평균값 - C가 임계치. 가우스 가중치를 사용하므로 현재 검사하는 픽셀에서 가까운 픽셀이 더 중요함.  

 

C = 0

 

import cv2
 
# green_back.png를 회색조로 읽어옴 
img_gray = cv2.imread('D:\green_back.png', cv2.IMREAD_GRAYSCALE)
 
#adaptiveThreshold 적용
#주변 9*9 픽셀 공간의 평균값 - 5가 임계치. 이보다 크면 255, 아니면 0
img_edge = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                 cv2.THRESH_BINARY, blockSize = 9, C = 0)
 
cv2.imshow('edge', img_edge)
 
cs

 

  여기서 C를 높이면 사람을 기준으로 밝은 톤의 윤곽선이 나오고, 낮추면 전체적으로 어두운 톤의 윤곽선이 나온다. 

C = -5 / C = 1 / C = 5 일 때 추출한 윤곽선 

 


 

마무리

 

 이번 시간에는 이미지를 다루는 방법에 대해 알아보았다. 

마지막으로 심화문제 13.1과 13.2를 풀어보고 마치도록 하자. 접은글 속 해답 코드는 참고만 하자. 

 

13.1: 이 책의 github 저장소에서 myData.png 파일과 bag_cartoon.png 파일을 다운로드 받을 수 있다. 이 두 이미지를 각각 50% 씩 합성하여 다음과 같은 그림을 만들어 보라.

더보기
import cv2
global img_myData, img_bag
 
def on_change_weight(x):  #상단 트랙바가 움직이면 이 함수가 작동한다. 
    weight = x / 100      # x값이 0에서 100 사이이므로 100으로 나누어 0~1로 바꿈.
    img_merged = cv2.addWeighted(img_myData, 1 - weight, img_bag, weight, 0)
    cv2.imshow('Display', img_merged)
    
cv2.namedWindow('Display')
cv2.createTrackbar('wieght''Display'0100, on_change_weight)
 
img_myData = cv2.imread('d:/myData.png')
img_bag = cv2.imread('d:/bag_cartoon.png')
img_myData = cv2.resize(img_myData, (300400))
img_bag = cv2.resize(img_bag, (300400))
 
cs

 

13.2: 신경망을 이용한 인공지능 분야에서 테스트 데이터를 흔히 사용하는 데이터가 손글씨 이미지를 담은 MNIST 데이터와 패션 소품들을 담은 Fashion MNIST 데이터인데, 이들 데이터는 28 * 28 크기의 회색조 이미지이다. 앞서 다루어 본 myData.png와 bag_cartoon.png 파일도 28 * 28 크기의 회색조 파일 형태로 변환하여 다음 그림처럼 나타나게 해 보아라. 발목 부츠 사진인 myData.png는 좌우로 반전이 일어나도록 해 보라. (힌트: 원래 발목 부츠가 img1이라고 하면 발목 부츠 이미지의 좌우를 뒤집는 것은 다음의 슬라이싱을 이용하여 할 수 있다. img1 = img1[:, ::-1])

더보기
import cv2
 
img1 = cv2.imread('d:/myData.png', cv2.IMREAD_GRAYSCALE) #회색조로 읽는다.
img2 = cv2.imread('d:/bag_cartoon.png', cv2.IMREAD_GRAYSCALE)
img3 = img1[:, ::-1]  #좌우반전 시키기 
 
 
img1 = cv2.resize(img1, (2828))  #사이즈를 28로 줄인다. 
img2 = cv2.resize(img2, (2828))
img3 = cv2.resize(img3, (2828))
 
 
cv2.imshow('img_myData', img1)
cv2.imshow('img_bag', img2)
cv2.imshow('img_myData2', img3)
 
cs
COMMENT