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도 가능합니다.

Python과 OpenCV – 7 : 이미지에 대한 산술 연산

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

이 글에서는 이미지에 대한 더하기 연산, 빼기 연산, Bitwise 연산 등에 대한 글이며, cv2.add(), cv2.addWeighted() 함수에 대해 살펴봅니다.

다음 코드는 OpenCV의 add 함수와 일반적인 덧셈에 대한 차이를 보여줍니다.

import cv2
import numpy as np

x = np.uint8([250]) # 250 + 10 = 260 => 255
y = np.uint8([10]) # 250 + 10 = 260 => 260 % 256 = 4

print(cv2.add(x,y))
print(x+y)

출력 결과는 각각 255와 4입니다. OpenCV의 경우 데이터 타입의 범위를 벗어나면 해당 데이터 타입이 갖을 수 있는 최대값이 됩니다.

다음은 이미지의 블랜딩(Blending)에 대한 코드입니다.

import cv2
import numpy as np

img1 = cv2.imread('./data/ml.png')
img2 = cv2.imread('./data/opencv_logo.png')

dst = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)

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

결과는 다음과 같은데요. 아래의 3개의 이미지 중 첫번째는 ml.png이고 두번째는 opencv_log.png입니다. 그리고 세번째가 cv2.addWeighted 함수의 적용 결과 이미지입니다.

위의 코드에서 cv2.addWeighted 함수의 인자가 (imgA, a, imgB,b, c)라고 한다면 산술식은 다음과 같습니다.

result = imgA * a + imgB * b + c

다음은 Bitwise 연산에 대한 예제로써, 2개의 이미지를 하나의 이미지로 배경을 살리면서 합하는 예제입니다.

import cv2
import numpy as np

# Load two images
img1 = cv2.imread('./data/Penguins2.jpg')
img2 = cv2.imread('./data/opencv_logo.jpg')

# I want to put logo on top-left corner, So I create a ROI
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols ]

# Now create a mask of logo and create its inverse mask also
img2gray = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
cv2.imshow('mask_inv',mask_inv)

# Now black-out the area of logo in ROI
img1_bg = cv2.bitwise_and(roi,roi,mask = mask_inv)

# Take only region of logo from logo image.
img2_fg = cv2.bitwise_and(img2,img2,mask = mask)

# Put logo in ROI and modify the main image
dst = cv2.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst

cv2.imshow('res',img1)
cv2.waitKey(0)
cv2.destroyAllWindows()

결과는 아래와 같습니다.

위의 결과는 2개의 이미지를 보여주고 있는데.. 첫번째는 배경 이미지에 섞을 이미지를 Mask로 사용하기 위해 만들어진 것이고 두번째는 그 결과 이미지입니다. 첫번째 이미지에 대한 이미지 객체는 위의 코드에서 mask 입니다. 이미지에 대한 Grayscale 변환 후 Threshold를 이용한 이진 비트맵 변환, Mask 기법과 bitwise 연산을 적용하고 있습니다.

Python과 OpenCV – 6 : 이미지에 대한 기본 연산

이 글을 작성하기 위해 참조한 글은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_core/py_basic_ops/py_basic_ops.html 입니다.

이 글에서 이미지에 대한 기본 연산이라함은 다음과 같습니다.

  • 이미지의 픽셀값을 읽거나 변경하기
  • 이미지의 속성 읽기
  • ROI(Region Of Image, 이미지 영역) 지정하기
  • 이미지를 채널별로 나누거나 합치기
  • 이미지의 가장자리 만들기

위의 항목만 읽어서는 무엇을 의미하는지 완벽하게 이해하긴 어렵습니다. 하나씩 살펴보겠습니다.

먼저 다음 코드로 시작해 하나씩 덧붙여 보겠습니다.

import cv2
import numpy as np

img = cv2.imread('./data/people.png')

people.png라는 이미지를 하나 읽어 img 변수에 저장합니다. 그리고..

px = img[0,100] # [row,column]
print(px) # [B,G,R]

이미지의 픽셀 중 y=0, x=100 인 값을 읽어 표시하는 코드입니다. 이 예제의 경우 결과는 [ 68 129 117]가 출력되는데, 이미지가 3개의 채널로 구성되어 있으므로 3개의 값이 표시되며 순서대로 Blue, Green, Red 채널의 값입니다.

blue = img[0,100,0] # [row,column,channel]
print(blue) # Blue

위의 코드는 이미지의 y=0, x=100인 픽셀의 첫번째 채널의 값을 읽습니다. 이미지의 픽셀 색상은 BGR 순서이므로 첫번째는 Blue 채널의 값을 읽습니다. 이 예제의 경우 결과는 68로 표시 되었습니다.

img[0,100] = [255,255,255]
img[0,100,0] = 0

위의 코드에서 1번은 x=99, y=0인 픽셀의 색상을 BGR 순서로 255,255,255로 변경하는 것입니다. 그리고 2번은 x=100, y=0인 픽셀의 첫번째 채널을 0으로 변경하는 코드입니다. 첫번째 채널이므로 Blue 채널입니다.

px = img.item(0,100,0)
print(px)

위의 코드는 이미지의 y=0, x=100 인 픽셀의 첫번째 채널의 값을 출력합니다. 값은 255가 출력됩니다.

img.itemset((0,100,0),255)
px = img.item(0,100,0)
print(px)

위의 코드는 먼저 y=0, x=100인 픽셀의 첫번째(0) 채널의 값을 255로 변경하고, 다시 동일한 픽셀의 첫번째 채널의 값을 표시합니다. 255가 출력됩니다.

print(img.shape)
print(img.size)
print(img.dtype)

위의 코드 중 1번은 이미지의 차원 크기를 출력합니다. 이미지가 640×480 크기이고 1픽셀이 3Byte인 RGB 형식이므로 출력은 (480, 640, 3)입니다. 2번 코드는 이미지의 전체 Byte 크기로 480*640*3 값인 921600가 출력됩니다. 3번째 코드는 채널의 데이터 형식으로써 출력은 uint8입니다.

roi = img[280:340, 330:390]
img[273:333, 100:160] = roi

cv2.rectangle(img, (330,280), (390,340), (0,0,255), 1)
cv2.rectangle(img, (100,273), (160,333), (255,0,0), 1)

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

위의 코드 ROI에 대한 코드로써 1번 코드는 y가 280번째에서 (340-1)번째이면서 x는 330번째에서 (390-1)번째까지의 영역의 픽셀값을 roi 객체에 복사(Copy)합니다. 그리고 2번 코드를 통해 roi 객체에 저장된 픽셀값을 이미지(img 객체)의 y가 273번째에서 (333-1)번째이면서 x는 100번째에서 (160-1)번째까지의 영역에 복사합니다. 4번은 1번 코드의 ROI 영역을 빨간색 사각형으로 그리고 5번 코드는 2번 코드의 ROI 영역을 파란색 사각형으로 그리라는 것입니다. 결과는 다음과 같습니다.

b,g,r = cv2.split(img)
img = cv2.merge((b,g,r))

위의 1번 코드는 img 객체의 이미지를 각 채널로 분리하여 각각 b, g, r 객체에 복사(Copy)합니다. 그리고 2번 코드는 채널을 조합하는 코드입니다.

b = img[:,:,0]
img[:,:,2] = 0

위의 1번 코드는 이미지의 0번째 채널을 b 객체에 복사(Copy)하고, 2번은 이미지의 3번째 채널의 모든 값을 0으로 만듭니다. 결과적으로 이미지에서 Red 색감이 모두 제거됩니다.

끝으로 이미지에 가장자리를 만드는 예제입니다.

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

RED = [255,0,0]

img1 = cv2.imread('./data/opencv_logo.png')

replicate = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT)
reflect101 = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_WRAP)
constant= cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_CONSTANT,value=RED)

plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL')
plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE')
plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT')
plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101')
plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT')

plt.show()

만들어진 가장자리만큼 이미지의 크기는 커집니다. 결과는 아래와 같습니다.

결과에서 보이는 것처럼 cv2.copyMakeBorder 함수를 통해 가장자리가 추가되는데, 이 함수의 6번째 인자에 따라 추가되는 가장자리의 형태가 결정됩니다.