해당 포스팅은 Mastering OpenCV 4 with Python 원서를 바탕으로 작성했습니다. 원서를 옮기는 과정에서 부자연스러운 부분이 있을 수 있습니다. 잘못 작성되거나 어색한 부분에 대해서 알려주시면 감사하겠습니다! 코드 정보는 여기를 클릭하시면 확인하실 수 있습니다.
지난 포스팅에 이어 오늘은 이미지 처리를 위한 방법들 중 kernel을 활용한 방법과 사진을 만화 같은 이미지로 변환하기를 다뤄보겠습니다.
- 채널 분할 및 병합(Splitting and merging channels)
- 이미지의 기하학적인 변환 - 회전, 스케일링, 아핀 변환, 자르기
- 이미지를 사용한 산술 연산 - 비트 연산(AND, OR, XOR, NOT), 마스킹
- smoothing and sharpening 기법
- 모폴로지 연산
- Color spaces
- Color maps
1. kernel
kernel은 위에서 보시는 것처럼 값을 가지는 행렬을 말합니다. kernel은 이미지 픽셀과 동일 위치의 값을 곱하고 합하는 방식(행렬 곱 X)을 통해, 기존 이미지를 변화시키는 역할을 합니다. 그 예시로 컴퓨터 비전에서의 convolution layer를 볼 수 있습니다. 물론, 딥러닝에서 활용하는 convolution layer의 경우에는 kernel을 통해 이미지의 크기를 줄이기도 하고, pedding을 추가하여 크기를 유지시키기도 합니다. OpenCV도 convolution layer와 동일하게 각 픽셀 값을 kernel과 곱해서 값을 변환하는 함수가 존재합니다. cv2.filter2D()를 활용하면, 다양한 kernel을 활용하여 이미지 변형을 만들 수 있습니다. 이때, OpenCV에서는 이미지 크기가 동일하도록 만들기 위해 pedding처럼 이미지 주변을 특정 값으로 채우는 파라미터(borderType)가 존재합니다.
| 사이에 있는 값들은 실제 이미지 값이라고 보시면 되고, 앞 뒤로 적혀진 값으로 채운다고 보시면 좋을 것 같습니다. 특정 값으로 채워주기도 하고, 가장 근접한 픽셀 값으로 다 채우기도 합니다.
오늘 활용해볼 사진은 저희 시골 강아지인 금성이 사진입니다. filter2D를 활용해서 다양한 kernel을 적용해본 결과는 아래와 같습니다.
edge detection의 경우 1에서 3으로 갈수록 edge가 선명해지는 것을 볼 수 있습니다. 특히, 신기한 부분은 sobel x는 세로에 대한 정보를, sobel y는 가로에 대한 정보를 잘 나타내는 것으로 보입니다. outline image의 경우에는 강아지 얼굴 윤곽을 잘 잡아내는 것으로 보입니다. 이해를 돕기 위해 위에 적용한 일부를 wiki 참고자료를 포함했습니다.
다양한 kernel을 적용하여 원하는 형태의 이미지를 만들어 볼 수 있습니다. 이러한 전처리는 컴퓨터 비전 프로젝트를 진행할 때, 유용하게 활용할 수 있을 것 입니다. 아래에는 소스코드를 추가하였으니, 한번 직접 작성해보시길 추천드립니다. (참고로 py파일로 실행했을 때, 이미지가 load 되지 않는 문제가 있어서 ipynb 파일에서 실행했습니다.)
# 라이브러리 불러오기
import cv2
import numpy as np
import matplotlib.pyplot as plt
def show_with_matplotlib(color_img, title, pos):
# Convert BGR image to RGB
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(3, 4, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
plt.figure(figsize=(24, 12))
plt.suptitle("Comparing different kernels using cv2.filter2D()", fontsize=14, fontweight='bold')
# 원본 이미지 불러오기
image = cv2.imread('./img.jpg') # image 불러오기
image = image[100:2500, 100:2300] # image 크롭하기
# 다양한 커널 생성
# Identify kernel : 동일한 이미지 생성
kernel_identity = np.array([[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
# Try different kernels for edge detection:
kernel_edge_detection_1 = np.array([[1, 0, -1],
[0, 0, 0],
[-1, 0, 1]])
kernel_edge_detection_2 = np.array([[0, 1, 0],
[1, -4, 1],
[0, 1, 0]])
# Ridge detection
kernel_edge_detection_3 = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]])
# sharpen kernel
kernel_sharpen = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]])
# unsharp_masking
kernel_unsharp_masking = -1 / 256 * np.array([[1, 4, 6, 4, 1],
[4, 16, 24, 16, 4],
[6, 24, -476, 24, 6],
[4, 16, 24, 16, 4],
[1, 4, 6, 4, 1]])
# blur
kernel_blur = 1 / 9 * np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 1]])
gaussian_blur = 1 / 16 * np.array([[1, 2, 1],
[2, 4, 2],
[1, 2, 1]])
# emboss
kernel_emboss = np.array([[-2, -1, 0],
[-1, 1, 1],
[0, 1, 2]])
# sobel kernel
sobel_x_kernel = np.array([[1, 0, -1],
[2, 0, -2],
[1, 0, -1]])
sobel_y_kernel = np.array([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]])
# ridge detection 동일
outline_kernel = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]])
original_image = cv2.filter2D(image, -1, kernel_identity)
edge_image_1 = cv2.filter2D(image, -1, kernel_edge_detection_1)
edge_image_2 = cv2.filter2D(image, -1, kernel_edge_detection_2)
edge_image_3 = cv2.filter2D(image, -1, kernel_edge_detection_3)
sharpen_image = cv2.filter2D(image, -1, kernel_sharpen)
unsharp_masking_image = cv2.filter2D(image, -1, kernel_unsharp_masking)
blur_image = cv2.filter2D(image, -1, kernel_blur)
gaussian_blur_image = cv2.filter2D(image, -1, gaussian_blur)
emboss_image = cv2.filter2D(image, -1, kernel_emboss)
sobel_x_image = cv2.filter2D(image, -1, sobel_x_kernel)
sobel_y_image = cv2.filter2D(image, -1, sobel_y_kernel)
outline_image = cv2.filter2D(image, -1, outline_kernel)
show_with_matplotlib(original_image, "identity kernel", 1)
show_with_matplotlib(edge_image_1, "edge detection 1", 2)
show_with_matplotlib(edge_image_2, "edge detection 2", 3)
show_with_matplotlib(edge_image_3, "edge detection 3", 4)
show_with_matplotlib(sharpen_image, "sharpen", 5)
show_with_matplotlib(unsharp_masking_image, "unsharp masking", 6)
show_with_matplotlib(blur_image, "blur image", 7)
show_with_matplotlib(gaussian_blur_image, "gaussian blur image", 8)
show_with_matplotlib(emboss_image, "emboss image", 9)
show_with_matplotlib(sobel_x_image, "sobel x image", 10)
show_with_matplotlib(sobel_y_image, "sobel y image", 11)
show_with_matplotlib(outline_image, "outline image", 12)
# Show the Figure:
plt.show()
2. 만화같은 이미지 만들기
이전에 배웠던 방법들을 활용해서 사진을 만화 같은 이미지로 변환해보려고 합니다. 먼저 변환 결과를 보시고 코드를 보면서 어떠한 필터를 활용했는지 설명해보겠습니다.
굉장히 밝은 사진이 나올 줄 알았는데, 다크한 분위기의 사진들이 많이 나왔네요. 하나씩 한번 살펴보겠습니다.
1) Sketch image 함수
def sketch_image(img):
# BGR to Gray
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# median Blur
img_gray = cv2.medianBlur(img_gray, 5)
# cv2.Laplacian()을 활용해서 edge 검출
edges = cv2.Laplacian(img_gray, cv2.CV_8U, ksize=5)
# 검출한 edge를 threshold로 나눠서 표시
ret, thresholded = cv2.threshold(edges, 70, 255, cv2.THRESH_BINARY_INV)
return thresholded
스케치된 이미지를 만들어내기 위해, 가장 중요한 것은 edge부분을 찾아내는 것입니다. 그래서 여기서는 cv2.Laplacian() 함수를 활용했습니다. 그리고 medianBlur를 활용해서 noise를 제거해줬습니다.
위의 함수대로 이미지가 변화하는 모습을 보면 다음과 같습니다. 확실히 Laplacian을 적용하면서 윤곽을 잘 잡아내는 것을 볼 수 있습니다.
Laplacian
Laplacian은 이미지의 기울기를 활용하여 변환하는 연산입니다.
위의 그림처럼 값이 존재한다고 할 때, 1차 미분하면 우측과 같은 값이 됩니다. 값이 증가하는 경계선을 0으로 만들어 주기 위해 한번 더 미분하면 아래와 같은 그림을 얻게 됩니다.
이러한 방법을 통해서 사물과 사물 사이의 경계선을 찾을 수 있습니다.
MedianBlur
Median Blur를 활용하지 않았을 때에는 강아지의 털 질감과 가지의 윤곽까지 잘 나타나는 모습을 볼 수 있습니다. Median Blur를 활용하면 노이즈 제거를 통해, 비교적 물체와 배경을 구분할 수 있도록 도와줍니다. 모든 과정을 끝내고 나면, BGR에서 RGB형태로 바꿔서 plot 하면 오른쪽 그림처럼 결과를 얻을 수 있습니다.
Threshold
sketch_image 함수에서 가장 마지막으로 threshold를 지정하게 되는데, cv2.threshold(edges, x, 255, cv.THRESH_BINARY_INV)에서 x값은 임계값으로 원본 이미지의 픽셀 값이 x값보다 크면 255로, 이하일 경우 0으로 바꿔주는 방법(cv.THRESH_BINARY 일때)입니다.
위에 수식을 보시면 저희가 활용한 cv.THRESH_BINARY_INV는 임계값보다 큰 경우에는 0(검은색)으로, 작은 경우에는 255(흰색) 값으로 바꾸게 됩니다.
2) Cartoonize image 함수
def cartoonize_image(img, gray_mode=False):
"""cv2.bilateralFilter()를 활용하여 만화같은 이미지 생성"""
# sketch_image 가져오기
thresholded = sketch_image(img)
# bilateralFilter 적용
filtered = cv2.bilateralFilter(img, 10, 250, 250)
# bitwise_and 적용
cartoonized = cv2.bitwise_and(filtered, filtered, mask=thresholded)
if gray_mode:
return cv2.cvtColor(cartoonized, cv2.COLOR_BGR2GRAY)
return cartoonized
만화 같은 이미지를 만들어 내기 위해, bilateralFilter와 bitwise_and 연산을 활용하고 있습니다.
BilateralFilter
bilateralFilter는 Median Blur처럼 노이즈 감소를 위해 활용됩니다.
위의 함수에서는 10을 활용했는데, 50, 100으로 값을 키워가면 위에 그림처럼 모자이크 처리한 것과 같은 효과를 얻을 수 있습니다. cv2.bitwise_and 연산은 sketch_image에서 나온 이미지와 bilateralFilter를 처리한 이미지를 bitwise 연산을 통해 이미지를 얻어냅니다.
Bitwise_and
bitwise 연산에는 and, or, xor, not이라는 연산이 있는데, 이 내용은 다음 section에서 자세히 다루기 때문에 넘어가도록 하겠습니다.
cv2.pencilSketch, cv2.stylization과 비교
저희가 커스텀한 모델과 비교하기 위해, OpenCV에서 제공하는 cv2.pencilSketch()와 cv2.stylization()을 활용했습니다. cv2.pencilSketch() 함수는 저희가 만든 sketch_image와 유사한 것을 볼 수 있습니다. 그리고 cv2.stylization을 활용한 이미지는 오래된 그림처럼 보입니다. 원본 이미지를 다양하게 바꿔보니 생각보다 재미있는 작업이었네요. 아래에 코드를 포함하였으니, 직접 다양한 이미지에 적용해보시면 좋을 것 같습니다!
# 라이브러리 불러오기
import cv2
import matplotlib.pyplot as plt
def show_with_matplotlib(color_img, title, pos):
# Convert BGR image to RGB
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(3, 4, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
def sketch_image(img):
# BGR to Gray
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# median Blur
img_gray = cv2.medianBlur(img_gray, 5)
# cv2.Laplacian()을 활용해서 edge 검출
edges = cv2.Laplacian(img_gray, cv2.CV_8U, ksize=5)
# 검출한 edge를 threshold로 나눠서 표시
ret, thresholded = cv2.threshold(edges, 70, 255, cv2.THRESH_BINARY_INV)
return thresholded
def cartoonize_image(img, gray_mode=False):
"""cv2.bilateralFilter()를 활용하여 만화같은 이미지 생성"""
# sketch_image 가져오기
thresholded = sketch_image(img)
# bilateralFilter 적용
filtered = cv2.bilateralFilter(img, 10, 250, 250)
# bitwise_and 적용
cartoonized = cv2.bitwise_and(filtered, filtered, mask=thresholded)
if gray_mode:
return cv2.cvtColor(cartoonized, cv2.COLOR_BGR2GRAY)
return cartoonized
# figure 생성 및 title 작성
plt.figure(figsize=(30, 15))
plt.suptitle("Cartoonizing images", fontsize=14, fontweight='bold')
# 이미지 불러오기
image = cv2.imread('image.jpg')
# sketch image, cartoonize image 만들기
custom_sketch_image = sketch_image(image)
custom_cartonized_image = cartoonize_image(image)
custom_cartonized_image_gray = cartoonize_image(image, True)
# cv2.pencilSketch, cv2.stylization으로 변환하기
sketch_gray, sketch_color = cv2.pencilSketch(image, sigma_s=30, sigma_r=0.1, shade_factor=0.1)
stylizated_image = cv2.stylization(image, sigma_s=60, sigma_r=0.07)
# 결과 표시하기
show_with_matplotlib(image, "image", 1)
show_with_matplotlib(cv2.cvtColor(custom_sketch_image, cv2.COLOR_GRAY2BGR), "custom sketch", 2)
show_with_matplotlib(cv2.cvtColor(sketch_gray, cv2.COLOR_GRAY2BGR), "sketch gray cv2.pencilSketch()", 3)
show_with_matplotlib(sketch_color, "sketch color cv2.pencilSketch()", 4)
show_with_matplotlib(stylizated_image, "cartoonized cv2.stylization()", 5)
show_with_matplotlib(custom_cartonized_image, "custom cartoonized", 6)
show_with_matplotlib(cv2.cvtColor(custom_cartonized_image_gray, cv2.COLOR_GRAY2BGR), "custom cartoonized gray", 7)
plt.show()
아무래도 저는 제조업에서 근무하다 보니, 추후 비전검사에서 edge를 detection 하기 위해 오늘 활용한 방법들이 유용하게 활용될 것 같습니다. 오늘 소개해드릴 내용은 여기서 마치고, 다음에는 bitwise 연산에 대한 내용을 가지고 오도록 하겠습니다! 읽어주셔서 감사합니다.