Python과 OpenCV – 10 : 기하학 변환

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_geometric_transformations/py_geometric_transformations.html#geometric-transformations 입니다.

OpenCV는 cv2.wrapAffine, cv2.warpPerspective라는 2개의 변환 함수를 제공하는데 각각 2×3 행렬과 3×3 행렬을 인자로 받습니다. 이 함수를 통해 이미지에 대한 크기 변환, 이동, 회전, Affine 변환, Perspective 변환을 수행할 수 있습니다.

먼저 이미지 크기 변환인데, 이 경우 위의 함수를 통한 변환도 가능하지만 OpenCV에서는 cv2.resize 함수를 이용해 크기 변환을 자주 수행하며 예제는 아래와 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/Penguins.jpg', 0)

res1 = cv2.resize(img,None,fx=2, fy=2, interpolation = cv2.INTER_CUBIC)

height, width = img.shape[:2]
res2 = cv2.resize(img,(2*width, 2*height), interpolation = cv2.INTER_CUBIC)

cv2.imshow('res1', res1)
cv2.imshow('res2', res2)

cv2.waitKey()

cv2.destroyAllWindows()

6번과 9번 코드 모두 이미지를 2배 확대하는데, 9번 코드의 경우 이미지의 픽셀 크기로 확대정도를 지정하므로 소수점으로 확대, 축소는 할 수 없습니다.

다음은 이미지를 이동하는 변환입니다. 이동 변환을 위한 행렬은 다음과 같습니다.

위의 행렬을 적용하는 코드는 다음과 같은데, 이미지를 x축으로 100만큼, y축으로 50만큼 이동합니다. 이동되고 남은 공간은 0값으로 채워지므로 검정색으로 표시됩니다.

import cv2
import numpy as np

img = cv2.imread('./data/Penguins.jpg', 0)

M = np.float32(
    [
        [1, 0,100],
        [0, 1, 50]
    ]
)

rows,cols = img.shape

dst = cv2.warpAffine(img, M, (cols,rows))

cv2.imshow('img',dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

다음은 회전입니다. 2차원에 대한 일반적인 회전 행렬은 다음과 같습니다.

이를 2×3인 행렬로 표현해야 Affine 변환이 가능한데, 이를 위해 OpenCV는 cv2.getRotationMatrix2D 라는 함수를 통해 회전 행렬을 얻을 수 있습니다. 이 함수는 장점은 회전값의 지정뿐만 아니라 크기 변환과 회전중심점도 지정할 수 있습니다. 이 함수를 통해 얻을 수 있는 행렬은 다음과 같습니다.

위의 기호에 대해서

예제는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/Penguins.jpg', 0)

rows,cols = img.shape

M = cv2.getRotationMatrix2D((cols/2,rows/2), 45, 0.6)
print(M)
dst = cv2.warpAffine(img,M,(cols,rows))

cv2.imshow('img',dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

8번 코드를 보면 회전 중심을 이미지의 중심 좌표로 했고, 회전각도는 45도, 크기변환 비율은 0.6으로 지정하여 원래 크기의 60%로 변환됩니다. 결과는 다음과 같습니다.

Affine 변환에 대해 살펴보겠습니다. Affine 변환은 크기변환, 이동변환, 회전변환이 발생해도 원래 평행했던 특성이 그대로 유지됩니다. 이러한 특성을 갖는 Affine 변환에 대한 행렬은 cv2.getAffineTransform을 통해 얻을 수 있으며 그 결과는 2×3 행렬입니다. 아래는 예제입니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/chessboard2.jpg')
rows,cols,ch = img.shape

pts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[10,100],[200,50],[100,250]])

M = cv2.getAffineTransform(pts1,pts2)

dst = cv2.warpAffine(img,M,(cols,rows))

plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()

결과는 다음과 같은데..

위 코드에서 8번의 3개의 좌표가 9번의 3개의 각각의 좌표로 변환됨에 있어서 평행을 유지하도록 하는 행렬을 얻는다는 것입니다.

다음은 Perspective 변환, 즉 투영변환입니다. 이 변환은 지금까지의 2×3 행렬이 아닌 3×3 행렬입니다. 변환 후에 평행성은 더 이상 유효하지 않습니다. 예제는 다음과 같습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/chessboard2.jpg')

rows,cols,ch = img.shape

pts1 = np.float32([[0,0],[368,52],[28,387],[389,390]])
pts2 = np.float32([[32,32],[300,0],[0,300],[300,300]])

M = cv2.getPerspectiveTransform(pts1,pts2)

dst = cv2.warpPerspective(img,M,(cols,rows))

plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()

결과는 다음과 같습니다.

9번에서 지정한 4개의 좌표가 10번에서 지정한 4개의 좌표로, 각각 매칭되어 변환이 이루어지는 행렬을 얻는 것이고 이렇게 얻은 행렬은 14번 코드의 cv2.warpPerspective 함수에 의해 변환이 수행됩니다.

Python과 OpenCV – 9 : Image Thresholding (2/2)

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html#thresholding 입니다.

이미지에 대한 Thresholding 연산을 수행할 때, 임계치 값을 개발자가 직접 지정해 주었는데 이를 자동으로 계산하기 위한 방법이 있습니다. 바로 Otsu의 이진화(Binarization)입니다. Otsu의 이진화(Binarization)가 가장 효과적으로 발휘될 수 있는 이미지는 Historam의 피크(Peak)가 2개 인 경우입니다. 피크가 2개가 아닌 경우 결과는 부정확할 수 있습니다. 이미지의 히스토그램에서 피크를 2개 갖도록 블러링을 통해 잡음을 제거하기도 합니다. 아래의 예제를 살펴보겠습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/apple.jpg', 0)

# 임계치를 127로 지정한 경우
ret1,th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

# Otsu 방법으로 임계치를 내부적으로 계산하여 지정한 경우
ret2,th2 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

# 잡음 감소를 위해 Gaussian filtering 연산 후 Otsu 방법으로 임계치를 계산한 경우
blur = cv2.GaussianBlur(img, (5,5), 0)
ret3,th3 = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

print(ret1, ret2, ret3)

# plot all the images and their histograms
images = [img, 0, th1,
          img, 0, th2,
          blur, 0, th3]

titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
          'Original Noisy Image','Histogram',"Otsu's Thresholding",
          'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]

for i in range(3):
    plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
    plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
    plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
    plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
    plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
    plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])

plt.show()

결과는 아래와 같습니다.

동일한 이미지에 대해 3번의 Thresholding 연산을 수행한 결과입니다. 8번 코드는 임계치를 127로 명시하여 Thresholding 연산을 수행한 것이고 11번 코드는 임계치를 미지정(0)하고 4번째에 cv2.THRESH_OTSU 옵션을 지정하여 임계치를 자동으로 계산하도록 하였습니다. 이 경우 v2.threshold 함수는 ret2 변수에 계산된 임계치 값이 반환됩니다. 15번 코드는 이미지를 먼저 가우시안 블러링 처리를 수행하고 Othu 방식의 Thresholding을 수행합니다. 이미지에 잡음이 많은 경우 이 방식을 통해 Peak가 2개인 히스토그램을 생성합니다. 이미 원본 이미지의 히스토그램이 Peak를 2개 가지고 있으므로 3개의 결과가 큰 차이가 없지만 잡음이 매우 심한 이미지의 경우 가우시안 블러링 처리를 수행 후 Othu 방식의 Thresholding 연산은 큰 의미가 있습니다.

Python과 OpenCV – 9 : Image Thresholding (1/2)

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html 입니다.

이 글에서는 Simple thresholding, Adaptive thresholding, Otsu’s thresholding 중 처음 2가지에 대해 언급합니다. 그리고 글을 나눠 Otsu’s thresholding을 설명하겠습니다.

Image Thresholding 즉, 이미지의 임계값 처리란 픽셀값(대부분은 Grayscale 값)이 어떤 조건(임계 조건)을 만족할때 해당 픽셀의 값을 어떤 값으로 치환할 것인가를 결정하는 것인데, 먼저 Simple thresholding에 대한 예제를 살펴보면..

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/gray-gradient.jpg', cv2.IMREAD_GRAYSCALE)

ret,thresh1 = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img, 180, 255, cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img, 180, 255, cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img, 180, 255, cv2.THRESH_TOZERO_INV)

titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]

for i in range(6):
    plt.subplot(2,3,i+1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

결과를 먼저 보고 코드에서 중요한 부분을 언급하겠습니다.

Original Image가 입력 이미지인 원본인데, 5개에 대한 Simple thresholding에 대한 결과가 표시되고 있습니다. Simple thresholding 처리에 대한 함수는 cv.threshold 인데, 첫번째 인자는 입력 이미지, 두번째는 조건값에 대한 픽셀값의 최소값, 세번째는 조건값에 대한 픽셀값의 최대값, 네번째는 thresholding 조건에 대한 결과값 처리 방식인데 살펴보면..

  • cv2.THRESH_BINARY – 조건을 만족하는 픽셀값을 255로 만족하지 않는 픽셀값을 0으로 설정
  • cv2.THRESH_BINARY_INV – 조건을 만족하는 픽셀값을 0로 만족하지 않는 픽셀값을 255으로 설정
  • cv2.THRESH_TRUNC – 조건을 만족하는 픽셀값을 255로 만족하지 않는 픽셀값을 원본값으로 설정
  • cv2.THRESH_TOZERO – 조건을 만족하는 픽셀값을 원본값으로 만족하지 않는 픽셀값을 0으로 설정
    cv2.THRESH_TOZERO_INV – 조건을 만족하는 픽셀값을 0로 만족하지 않는 픽셀값을 원본값으로 설정

이제 다음은 Adaptive thresholding인데요. Simple thresholding의 문제점은 이미지가 전체적으로 동일한 밝기를 가져야 한다는 것인데.. 예를 들어서 하나의 이미지에 어느 부분은 그림자가 들어가 있을 경우 이 그림자 부분에 대한 처리가 제대로 되지 않습니다. 이를 개선하기 위한 방식이 Adaptive thresholding이며 예제 코드는 다음과 같습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/srcThreshold.png', 0)
img = cv2.medianBlur(img, 5) # 잡음제거

ret,th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

th2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)

th3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)

titles = ['Original Image', 'Simple Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]

for i in range(4):
    plt.subplot(2, 2, i+1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

결과를 보면..

총 4개의 영상이 표시되는데 Original Image는 입력 원본 영상, Simple Threshold는 앞서 살펴본 cv2.threshold에 의한 처리 영상입니다. 원본 이미지에서 다른 부분보다 상대적으로 어두운 부분에 대해도 원하는 처리가 된것을 비교해 볼 수 있습니다. 나머지 2개가 Adaptive thresholding인데 첫번째는 단순 평균값, 두번째는 가중치에 의해 처리된 값이 합입니다. 이는 cv.adaptiveThreshold 함수의 세번째 인자에 의해 지정되며 아래와 같습니다.

  • cv2.ADAPTIVE_THRESH_MEAN_C – 대상 픽셀값 주위의 픽셀값들의 평균값에 C 값을 더함
  • cv2.ADAPTIVE_THRESH_GAUSSIAN_C – Gaussian Window에 의해 가중치 값이 적용된 픽셀값들의 합에 C 값을 더함

cv.adaptiveThreshold의 전체 인자를 살펴보면 첫번째는 입력 이미지, 두번째는 픽셀값의 최대값, 세번째와 네번째는 앞서 설명한 바가 같으며, 다섯번째는 주의값의 픽셀을 얻을 Window의 크기, 여섯번째는 C 값입니다.

Python과 OpenCV – 8 : 색상공간(Colorspace) 변경하기

이 글의 원문은 다음과 같습니다. https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.html

다음과 같은 내용을 학습합니다.

  • 서로 다른 Colorspace로 변경하기. 즉 BGR을 Gray로, BGR을 HSV로 ..
  • 비디오의 영상을 통해 원하는 색상을 가진 물체 추출하기

위의 내용을 학습하면서cv2.cvtColor과 cv2.inRange 함수를 학습합니다.

먼저 색상공간 중 HSV는 Hue(색상), Saturation(채도), Value(명도)의 요소로써 각각 [0,179], [0,255], [0,255] 범위의 값을 갖습니다. HSV 중 H에 해당하는 색상값을 이용해 이미지에서 특정 색상을 띠는 물체를 추출할 수 있는데, BGR로 [255,0,0]인 파랑색을 HSV로 변환하기 위한 코드는 다음과 같습니다.

import cv2
import numpy as np

blue = np.uint8([[[255, 0, 0]]])
hsv_blue = cv2.cvtColor(blue,cv2.COLOR_BGR2HSV)
print(hsv_blue)

출력값은 [120 255, 255]입니다. 채도가 120인데, 이 특정색과 비슷한 계열의 색상은 [Hue-10,100,100]에서 [Hue+10,255,255]까지의 범위로 나타낼 수 있고, 채도가 120인 파란색의 경우 [110,50,50]에서 [130,255,255]까지가 됩니다.

이제 비디오 영상에 받은 이미지에서 파란색 계열의 물체를 추출해 내는 예제를 살펴보겠습니다.

import cv2
import numpy as np

cap = cv2.VideoCapture(0)

while(1):
    _, frame = cap.read()

    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

    # 앞서 설명한 파랑색 계열의 범위
    lower_blue = np.array([110,50,50])
    upper_blue = np.array([130,255,255])

    mask = cv2.inRange(hsv, lower_blue, upper_blue)

    res = cv2.bitwise_and(frame, frame, mask=mask)

    cv2.imshow('frame', frame)
    cv2.imshow('mask', mask)
    cv2.imshow('res', res)
    
    k = cv2.waitKey(5) & 0xFF
    if k == 27:
        break

cv2.destroyAllWindows()

총 3개의 이미지가 표시되는데, 첫번째는 비디오로부터 받은 원본 이미지로 아래와 같습니다.

그리고 파랑색 계열에 대한 물체를 추출하기 위해 사용할 마스크 이미지는 아래와 같습니다.

최종적으로 원본 이미지와 마스크 이미지에 대한 처리를 위해 bitwise_and 연산을 사용했고 그 결과는 아래와 같습니다. 참고로 마스크 이미지의 처리를 위한 목적이므로 bitwise_or도 가능합니다.