해당 포스팅은 Mastering OpenCV 4 with Python 원서를 바탕으로 작성했습니다. 원서를 옮기는 과정에서 부자연스러운 부분이 있을 수 있습니다. 잘못 작성되거나 어색한 부분에 대해서 알려주시면 감사하겠습니다! 코드 정보는 여기를 클릭하시면 확인하실 수 있습니다.
이미지 처리 기술은 컴퓨터 비전과 관련된 다양한 task를 수행할 때 유용한 방법이며 중요합니다. 이번 Chapter에서는 가장 일반적인 이미지 처리 기술에 대해 주로 다룹니다.
이번 Chapter 5에서는 아래의 내용을 포함하고 있습니다.
- 채널 분할 및 병합(Splitting and merging channels)
- 이미지의 기하학적인 변환 - 회전, 스케일링, 아핀 변환, 자르기
- 이미지를 사용한 산술 연산 - 비트 연산(AND, OR, XOR, NOT), 마스킹
- smoothing and sharpening 기법
- 모폴로지 연산
- Color spaces
- Color maps
위의 내용이 정확하게 어떤 것인지에 대해 차차 설명하고 이번 글에서는 기하학적인 변환까지 2개의 내용을 다룰 예정입니다.
1. Splitting and merging channels
컴퓨터 비전 분야를 다루다 보면, 여러 개의 채널로 이뤄진 이미지나 영상에서 특정 채널에 대해 작업해야 할 때가 종종 있습니다. 또한 작업을 마친 뒤에 해당 채널들을 하나로 합치는 작업이 필요합니다. 이번 내용에서는 채널별로 나누고, 합치는 과정에 대해 다루게 됩니다. Split과 Merge를 하기 위해서 OpenCV 라이브러리의 cv2.split()과 cv2.merge()라는 함수를 사용할 수 있습니다.
- split() : 여러 채널의 source로부터 여러 개의 단일 채널 이미지들을 얻는 함수
- merge() : 여러 개의 단일 채널 이미지들을 여러 채널의 단일 이미지로 만드는 함수
이전 글에서도 소개했듯이 cv2는 기본적으로 BGR 형태의 채널 순서를 가지고 있습니다. 반면, Matplotlib에서는 RGB 형태의 채널 순서를 가지고 있습니다. 그렇기 때문에 split을 할 때에는 BGR 순서를 기억하셔야 합니다. 그리고 merge 할 때에도 동일한 순서를 고려하여야 합니다. 아래처럼 튜플 형태로 입력해줘도 좋고, 튜플 대신 리스트를 사용해도 가능합니다.
# cv2.split
(b, g, r) = cv2.split(image)
# cv2.merge
image_copy = cv2.merge((b, g, r)) # cv2.merge([b, g, r])
cv2.split은 시간이 많이 걸리는 작업입니다. 반드시 필요한 경우에만 사용하기를 권장하고 그렇지 않은 경우에는 NumPy 기능을 사용하여 특정 채널을 가져올 수 있습니다.
# blue channel에 대한 정보 추출
b = image[:, :, 0]
위의 채널 값을 0으로 만들면 해당 값을 제거한 이미지를 얻을 수 있습니다.
# 파란색 채널 제거하기
image_without_blue = image.copy()
image_without_blue[:, :, 0] = 0
# 초록색 채널 제거하기
image_without_green = image.copy()
image_without_green[:, :, 1] = 0
# 빨간색 채널 제거하기
image_without_red = image.copy()
image_without_red[:, :, 2] = 0
# splitting_and_merging.py
# 필요한 라이브러리 불러오기
import cv2
import matplotlib.pyplot as plt
def show_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
# BGR image를 RGB image로 변환
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(3, 6, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
# 원래 이미지 가져오기
image = cv2.imread('color_spaces.png')
# figure() object 만들기
plt.figure(figsize=(13, 5))
plt.suptitle("Splitting and merging channels in OpenCV", fontsize=14, fontweight='bold')
# BGR image 출력
show_with_matplotlib(image, "BGR - image", 1)
# image split하기 (blue, green and red)
(b, g, r) = cv2.split(image)
# BGR image의 각각의 채널 출력:
show_with_matplotlib(cv2.cvtColor(b, cv2.COLOR_GRAY2BGR), "BGR - (B)", 2)
show_with_matplotlib(cv2.cvtColor(g, cv2.COLOR_GRAY2BGR), "BGR - (G)", 2 + 6)
show_with_matplotlib(cv2.cvtColor(r, cv2.COLOR_GRAY2BGR), "BGR - (R)", 2 + 6 * 2)
# BGR image로 합치기
image_copy = cv2.merge((b, g, r))
# 합친 BGR image 출력하기
show_with_matplotlib(image_copy, "BGR - image (copy)", 1 + 6)
# numpy indexing으로 blue channel 가져오기
b_copy = image[:, :, 0]
# 이미지 복사
image_without_blue = image.copy()
# blue channel 제거
image_without_blue[:, :, 0] = 0
# green channel 제거
image_without_green = image.copy()
image_without_green[:, :, 1] = 0
# red channel 제거
image_without_red = image.copy()
image_without_red[:, :, 2] = 0
# 각각 1개의 채널을 제거한 이미지 출력
show_with_matplotlib(image_without_blue, "BGR without B", 3)
show_with_matplotlib(image_without_green, "BGR without G", 3 + 6)
show_with_matplotlib(image_without_red, "BGR without R", 3 + 6 * 2)
# blue 채널을 제거한 이미지 split
(b, g, r) = cv2.split(image_without_blue)
# blue channel 제거한 이미지의 각 채널 출력
show_with_matplotlib(cv2.cvtColor(b, cv2.COLOR_GRAY2BGR), "BGR without B (B)", 4)
show_with_matplotlib(cv2.cvtColor(g, cv2.COLOR_GRAY2BGR), "BGR without B (G)", 4 + 6)
show_with_matplotlib(cv2.cvtColor(r, cv2.COLOR_GRAY2BGR), "BGR without B (R)", 4 + 6 * 2)
# green 채널을 제거한 이미지 split
(b, g, r) = cv2.split(image_without_green)
# green channel 제거한 이미지의 각 채널 출력
show_with_matplotlib(cv2.cvtColor(b, cv2.COLOR_GRAY2BGR), "BGR without G (B)", 5)
show_with_matplotlib(cv2.cvtColor(g, cv2.COLOR_GRAY2BGR), "BGR without G (G)", 5 + 6)
show_with_matplotlib(cv2.cvtColor(r, cv2.COLOR_GRAY2BGR), "BGR without G (R)", 5 + 6 * 2)
# red 채널을 제거한 이미지 split
(b, g, r) = cv2.split(image_without_red)
# red channel 제거한 이미지의 각 채널 출력
show_with_matplotlib(cv2.cvtColor(b, cv2.COLOR_GRAY2BGR), "BGR without R (B)", 6)
show_with_matplotlib(cv2.cvtColor(g, cv2.COLOR_GRAY2BGR), "BGR without R (G)", 6 + 6)
show_with_matplotlib(cv2.cvtColor(r, cv2.COLOR_GRAY2BGR), "BGR without R (R)", 6 + 6 * 2)
# image 출력
plt.show()
위의 접은 글을 실행하면, 아래와 같은 그림을 확인할 수 있습니다. 상단에 있는 링크에서 color_spaces.png 파일을 py 파일과 같은 폴더에 넣고 실행해주셔야 합니다.
가장 좌측은 원본사진과 split한 뒤에 merge 한 이미지입니다. 두 이미지가 다르지 않은 것을 확인할 수 있습니다. 두 번째 열은 원본 이미지를 split 한 각 채널을 출력한 것입니다. 세 번째 열은 각각의 채널을 제거한 이미지이며, 네 번째부터는 각 채널을 제거한 이미지를 split 하여 각 채널을 출력한 것입니다. 보시면 B를 제거한 경우에 B 채널은 0으로 대체하였기 때문에 검은색으로 나온 것을 확인할 수 있습니다.
2. Geometric transformations
이번에는 scaling, translation, rotation, affine transform 등과 같은 이미지 변환에 대해 알아봅니다. 이러한 변환을 수행하는 기본적인 함수가 2가지 존재합니다. 바로 cv2.warpAffine()과 cv2.warpPerspective()입니다.
- warpAffine() : 이미지를 $2 \times 3$ 형태의 행렬 변환을 사용하는 함수입니다.
- $dst(x, y) = src(M11x + M12y + M13, M21x + M22y + M23)$
- warpPerspective() : 이미지를 $3 \times 3$ 형태의 행렬 변환을 사용하는 함수입니다.
- $dst(x, y) = src((M11x + M12y + M13)/(M31x + M32y + M33), (M21x + M22y + M23)/(M31xM32y + M33))$
1) Scaling
기본적으로 scale 변환에서 가장 많이 활용되는 함수는 cv2.resize() 입니다. 말 그대로 특정 사이즈로 변환하는 함수입니다.
# resize: 사이즈를 직접 입력
resized_image = cv2.resize(image, (width * 2, height * 2), interpolation=cv2.INTER_LINEAR)
# resize: fx, fy를 활용하여 비율로 변경하는 방법
dst_image = cv2.resize(image, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
만약 이미지를 확대하고 싶을 때에는 interpolation으로 cv2.INTER_CUBIC이나 cv2.INTER_LINEAR를 사용하는 것을 추천합니다. INTER_CUBIC 보간법은 INTER_LINEAR에 비해 시간이 많이 소요되는 보간법입니다. 만약 이미지를 축소하고 싶을 때에는 cv2.INTER_LINEAR를 사용하는 것을 추천합니다. 이외에도 cv2.INTER_NEAREST, INTER_LANCZOS4 등 다양한 보간법이 존재합니다.
가장 왼쪽의 사진은 원본사진이고 가운데는 INTER_AREA 보간법을 사용하여 절반으로 축소한 사진입니다. 확실히 pixel수가 적어짐에 따라 깨지는 것처럼 보입니다. 가장 우측의 image는 INTER_LINEAR 보간법을 사용하여 2배로 확대한 사진입니다.
2) Translating
translating을 위해서는 위에서 언급했던 wrapAffine() 함수를 활용합니다.
# image 불러오기
image = cv2.imread('lena_image.png')
# image height, width
height, width = image.shape[:2]
# Translation Matrix 정의 : 기울기는 바꾸지 않고 x값과 y값에 대해 평행이동
M = np.float32([[1, 0, 200], [0, 1, 30]])
# wrapAffine 변환
dst_image = cv2.warpAffine(image, M, (width, height))
# 음의 방향으로 평행 이동
M = np.float32([[1, 0, -200], [0, 1, -30]])
dst_image = cv2.warpAffine(image, M, (width, height))
lena_image 파일을 wrapAffine을 통해 좌우상하로 평행이동이 가능합니다. 여기서 주의할 것은 좌측 상단의 값이 (0, 0)이고 오른쪽과 아래가 양의 방향, 왼쪽과 위가 음의 방향입니다.
3) Rotating
사진을 기울이는 방법은 기본적으로 위의 translating과 동일합니다. $2 \times 3$ 행렬을 통해서 변환하는데, 위의 평행이동할 때에는 [1 , 0], [0, 1]로 단위행렬이기 때문에 자기 자신이 나오게 됩니다. 하지만 45도를 돌린다면 sin과 cos을 활용해서 단위행렬을 구해야 할 것입니다. 그래도 OpenCV는 getRotationMatrix2D라는 함수로 쉽게 행렬을 얻을 수 있도록 제공하고 있습니다.
# Rotation 행렬 구하기(180도 회전)
M = cv2.getRotationMatrix2D((width / 2.0, height / 2.0), 180, 1) #(좌표, 각도, scale)
# wrapAffine
dst_image = cv2.warpAffine(image, M, (width, height))
# Rotation하는 기준인 중심 표시
cv2.circle(dst_image, (round(width / 2.0), round(height / 2.0)), 5, (255, 0, 0), -1)
# Rotation 행렬 구하기(30도 회전, 기준점 변경)
M = cv2.getRotationMatrix2D((width / 1.5, height / 1.5), 30, 1)
# wrapAffine
dst_image = cv2.warpAffine(image, M, (width, height))
# Rotation하는 기준인 중심 표시
cv2.circle(dst_image, (round(width / 1.5), round(height / 1.5)), 5, (255, 0, 0), -1)
위의 명령어를 실행하면, 위와 같은 그림 2개가 생깁니다. 파란색 원은 회전할 때의 기준이 되는 점입니다. 이처럼 getRotationMatrix2D와 wrapAffine을 통해서 쉽게 이미지를 회전시킬 수 있습니다.
4) Affine transformation
아핀 변환은 점, 직선, 평면을 보존하는 선형 매핑 방법입니다. 아핀 변환 후에도 평행한 선들은 평행한 상태로 유지되는 특징을 가지고 있습니다. 아핀 변환에는 위에서 보여드렸던 평행이동, 스케일링, 회전을 포함합니다. 마지막으로 사각형을 평행사변형의 형태로 변형하는 아핀 변환을 소개합니다. 기본적으로 아핀변환할 때에는 기존 점들과 이후에 변화되는 지점의 쌍이 필요합니다.
# 기준 포인트 3개(마지막의 x값만 변화)
pts_1 = np.float32([[135, 45], [385, 45], [135, 230]])
pts_2 = np.float32([[135, 45], [385, 45], [150, 230]])
# Affine Matrix
M = cv2.getAffineTransform(pts_1, pts_2)
# Affine transformation
dst_image = cv2.warpAffine(image_points, M, (width, height))
기존에는 사각형 형태였던 점들의 위치가 Affine 변환에 의해 x = 135에서 150으로 바뀌었지만 기존에 가지고 있던 평행한 선의 형태를 유지한 채 평행사변형으로 변한 것을 확인할 수 있습니다.
5) Perspective transformation
Perspective transformation은 사다리꼴을 사각형으로 만드는 방법이라고 생각하시면 좋을 것 같습니다. 여기서 앞에서 언급했던 warpPerspective() 함수를 활용합니다. 기본적으로 Perspective transformation을 사용할 때에는 기준이 되는 4개의 점이 필요합니다. 그리고 해당 점들을 어디의 좌표로 이동시킬 지에 대한 값도 필요합니다. 이때에도 Matrix를 계산해야 하지만 OpenCV에서는 getPerspectiveTransform()이라는 함수를 제공하고 있습니다.
# 기준이 되는 점
pts_1 = np.float32([[450, 65], [517, 65], [431, 164], [552, 164]])
pts_2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
# 변환을 위한 Matrix
M = cv2.getPerspectiveTransform(pts_1, pts_2)
# 변환 후 이미지
dst_image = cv2.warpPerspective(image, M, (300, 300))
위의 결과처럼 사다리꼴의 형태를 사각형에 가깝게 만들 수 있습니다. 원근법에 의해 왜곡된 사진을 복원하는 데 주로 활용되는 변환방법입니다.
6) Cropping
마지막으로 crop은 굉장히 간단하게 구현이 가능합니다. 위에서는 OpenCV 라이브러리 함수를 사용했지만, crop은 Numpy의 slicing을 이용하면 쉽게 구현이 가능합니다.
# crop 영역
cv2.circle(image_points, (230, 80), 5, (0, 0, 255), -1)
cv2.circle(image_points, (330, 80), 5, (0, 0, 255), -1)
cv2.circle(image_points, (230, 200), 5, (0, 0, 255), -1)
cv2.circle(image_points, (330, 200), 5, (0, 0, 255), -1)
cv2.line(image_points, (230, 80), (330, 80), (0, 0, 255))
cv2.line(image_points, (230, 200), (330, 200), (0, 0, 255))
cv2.line(image_points, (230, 80), (230, 200), (0, 0, 255))
cv2.line(image_points, (330, 200), (330, 80), (0, 0, 255))
# cropping
dst_image = image[80:200, 230:330]
이처럼 굉장히 쉽게 구현할 수 있습니다. 기하학적인 변형에 활용했던 코드는 아래 접은 글로 포함하였습니다.
# geometric_image_transformations.py
# 필요한 라이브러리 불러오기
import cv2
import matplotlib.pyplot as plt
def show_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
# BGR image를 RGB image로 변환
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(3, 6, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
# 이미지 파일 가져오기
image = cv2.imread('lenna.png')
# 원본 이미지 출력
show_with_matplotlib(image, 'Original image')
# 1. Scaling or resizing
# 절반으로 축소
dst_image = cv2.resize(image, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_LINEAR)
# 이미지 크기 저장
height, width = image.shape[:2]
# 2배 확대
dst_image_2 = cv2.resize(image, (width * 2, height * 2), interpolation=cv2.INTER_LINEAR)
# 이미지 출력
show_with_matplotlib(dst_image, 'Resized image')
show_with_matplotlib(dst_image_2, 'Resized image 2')
# 원래 이미지 가져오기
image = cv2.imread('lena_image.png')
# 이미지 크기 저장
height, width = image.shape[:2]
# 2. Translation
# 양의 값으로 평행이동
M = np.float32([[1, 0, 200], [0, 1, 30]])
dst_image = cv2.warpAffine(image, M, (width, height))
# 이미지 출력
show_with_matplotlib(dst_image, 'Translated image (positive values)')
# 음의 값으로 평행이동
M = np.float32([[1, 0, -200], [0, 1, -30]])
dst_image = cv2.warpAffine(image, M, (width, height))
# 이미지 출력
show_with_matplotlib(dst_image, 'Translated image (negative values)')
# 3. Rotation
# 180도 회전에 필요한 행렬 구하기
M = cv2.getRotationMatrix2D((width / 2.0, height / 2.0), 180, 1)
# rotation transformation
dst_image = cv2.warpAffine(image, M, (width, height))
# rotate 기준점과 Rotation 결과 출력
cv2.circle(dst_image, (round(width / 2.0), round(height / 2.0)), 5, (255, 0, 0), -1)
show_with_matplotlib(dst_image, 'Image rotated 180 degrees')
# 30도 회전에 필요한 행렬 구하기
M = cv2.getRotationMatrix2D((width / 1.5, height / 1.5), 30, 1)
# rotation transformation
dst_image = cv2.warpAffine(image, M, (width, height))
# rotate 기준점과 Rotation 결과 출력
cv2.circle(dst_image, (round(width / 1.5), round(height / 1.5)), 5, (255, 0, 0), -1)
show_with_matplotlib(dst_image, 'Image rotated 30 degrees')
# 4. Affine Transformation
image_points = image.copy()
cv2.circle(image_points, (135, 45), 5, (255, 0, 255), -1)
cv2.circle(image_points, (385, 45), 5, (255, 0, 255), -1)
cv2.circle(image_points, (135, 230), 5, (255, 0, 255), -1)
# 이미지 출력
show_with_matplotlib(image_points, 'before affine transformation')
# 변환 전과 후의 점의 위치
pts_1 = np.float32([[135, 45], [385, 45], [135, 230]])
pts_2 = np.float32([[135, 45], [385, 45], [150, 230]])
# Affine Transformation을 위한 행렬
M = cv2.getAffineTransform(pts_1, pts_2)
# Affine Transformation
dst_image = cv2.warpAffine(image_points, M, (width, height))
# 이미지 출력
show_with_matplotlib(dst_image, 'Affine transformation')
# 5. Perspective transformation
image_points = image.copy()
cv2.circle(image_points, (450, 65), 5, (255, 0, 255), -1)
cv2.circle(image_points, (517, 65), 5, (255, 0, 255), -1)
cv2.circle(image_points, (431, 164), 5, (255, 0, 255), -1)
cv2.circle(image_points, (552, 164), 5, (255, 0, 255), -1)
# 이미지 출력
show_with_matplotlib(image_points, 'before perspective transformation')
# 행렬을 구하기 위한 4개의 점
pts_1 = np.float32([[450, 65], [517, 65], [431, 164], [552, 164]])
pts_2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
# Perspective Transformation을 위한 행렬
M = cv2.getPerspectiveTransform(pts_1, pts_2)
# Perspective Transformation
dst_image = cv2.warpPerspective(image, M, (300, 300))
# 이미지 출력
show_with_matplotlib(dst_image, 'perspective transformation')
# 6. Cropping
image_points = image.copy()
# crop할 영역
cv2.circle(image_points, (230, 80), 5, (0, 0, 255), -1)
cv2.circle(image_points, (330, 80), 5, (0, 0, 255), -1)
cv2.circle(image_points, (230, 200), 5, (0, 0, 255), -1)
cv2.circle(image_points, (330, 200), 5, (0, 0, 255), -1)
cv2.line(image_points, (230, 80), (330, 80), (0, 0, 255))
cv2.line(image_points, (230, 200), (330, 200), (0, 0, 255))
cv2.line(image_points, (230, 80), (230, 200), (0, 0, 255))
cv2.line(image_points, (330, 200), (330, 80), (0, 0, 255))
# 점과 선들 포함한 이미지 출력
show_with_matplotlib(image_points, 'Before cropping')
# cropping
dst_image = image[80:200, 230:330]
# 이미지 출력
show_with_matplotlib(dst_image, 'Cropping image')
다음에는 chapter 5의 다른 주제를 통해 도움이 될 수 있는 글을 작성해보겠습니다. 여기까지 읽어주셔서 감사합니다.