Python과 OpenCV – 23 : 히스토그램(Histogram) 4/4

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

2차원 히스토그램을 응용하여 이미지에서 원하는 객체만을 추출해 내는 방법인 Backprojection에 대한 코드를 살펴보겠습니다. 바로 예제 나갑니다. 상세한 설명은 원문을 참고하시기 바랍니다.

import cv2
import numpy as np

roi = cv2.imread('./data/messi5_g.jpg')
hsv = cv2.cvtColor(roi,cv2.COLOR_BGR2HSV)

target = cv2.imread('./data/messi5.jpg')
hsvt = cv2.cvtColor(target,cv2.COLOR_BGR2HSV)

# calculating object histogram
roihist = cv2.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )

# normalize histogram and apply backprojection
cv2.normalize(roihist,roihist,0,255,cv2.NORM_MINMAX)
dst = cv2.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)

# Now convolute with circular disc
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
cv2.filter2D(dst,-1,disc,dst)

# threshold
ret,thresh = cv2.threshold(dst,50,255,0)

# threshold and binary AND
thresh = cv2.merge((thresh,thresh,thresh))
res = cv2.bitwise_and(target,thresh)

res = np.vstack((thresh,res))

cv2.imshow('result', res)
cv2.waitKey()
cv2.destroyAllWindows()

위에서 messi5.jpg는 어떤 객체를 추출할 대상이고 messi5_g.jpg는 추출할 대상입니다. messi5.jpg는 다음과 같습니다.

messi5_g.jpg는 다음과 같은데, 이 이미지는 messi5.jpg에서 잘라내기로 추출한 이미지입니다.

즉, 이미지에서 잔디에 해당되는 영역을 추출하겠다는 것입니다. 위의 코드의 실행 결과는 다음과 같습니다.

결과로 생성된 이미지가 2개이고, 이를 하나로 합쳐서 표시한 것인데.. 위에는 잔디에 해당되는 영역에 대한 결과 마스크 이미지이고 두번째는 이 마스크 이미지와 원본 이미지에 대한 bitwise 연산을 통해 실제 잔디 영상만을 추출한 결과입니다.

Python과 OpenCV – 22 : 히스토그램(Histogram) 3/4

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

지금까지는 하나의 값을 가진 화소, 즉 Grayscale 이미지에 대한 히스토그램을 살펴봤습니다. 이를 1차원 히스토그램이라고 합니다. 이 글에서는 2차원 히스토그램에 대해 살펴봅니다. 2차원 히스토그램은 2개의 값을 가지는 화소에 대한 히스토그램으로, HSV 중 H와 S의 값을 의미합니다. H는 Hue, S는 Saturation입니다.

2차원 히스토그램을 얻는 방법은 OpenCV와 numpy 방식이 있습니다. 먼저 OpenCV 방식에 대한 코드는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)

hist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

이미지를 읽고 BGR 채널을 HSV로 변환합니다. H에 대한 값의 범위는 0-180이고 S에 대한 값의 범위는 0-256라는 점을 통해 cv2.calcHist의 인자값의 의미를 이해할 수 있습니다. 이제 Numpy 방식에 대한 코드를 살펴보면..

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

img = cv2.imread('./data/home.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)

hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])

2차원 히스토그램을 그래프로 표시하는 방법으로 Matplotlib을 사용해 예제를 살펴보면…

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

img = cv2.imread('./data/home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )

plt.imshow(hist,interpolation = 'nearest')
plt.show()

실행 결과는 다음과 같습니다.

X축은 S이고 Y축은 H입니다.

Python과 OpenCV – 21 : 히스토그램(Histogram) 2/4

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

히스토그램을 활용하여 이미지의 품질을 개선하기 위한 방법이 히스토그램 균등화(Equalization)입니다. 이해를 돕기 위해 아래의 코드를 살펴보면..

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

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

hist,bins = np.histogram(img.flatten(),256,[0,256])

#cdf = hist.cumsum()
#cdf_m = np.ma.masked_equal(cdf,0)
#cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
#cdf = np.ma.filled(cdf_m,0).astype('uint8')
#img = cdf[img]

cv2.imshow('img', img)

plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.show()

결과는 다음과 같습니다.

이미지의 화소값이 0-255에 걸쳐 균등하게 분포하지 못하고 120-210 정도 사이에 밀집되어 있어 있습니다. 이 이미지의 품질을 히스토그램 균동화 방법을 이용해 개선해 보겠습니다. 먼저 numpy를 이용한 코드입니다.

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

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

hist,bins = np.histogram(img.flatten(),256,[0,256])

cdf = hist.cumsum()
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
cdf = np.ma.filled(cdf_m,0).astype('uint8')
img = cdf[img]

cv2.imshow('img', img)

plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.show()

결과는 다음과 같습니다.

뿌옇게 보였던 이미지가 좀더 명확이 보이고 있습니다. 이유는 위의 이미지의 히스토그램 그래프처럼 화소값이 0-255 사이에 고루게 분포하고 있습니다.

OpenCV의 함수를 이용한 방식에 대한 코드는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/tsukuba_l_clr.png',0)
equ = cv2.equalizeHist(img)
res = np.hstack((img,equ)) #stacking images side-by-side

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

결과는 다음과 같습니다.

왼쪽은 원본이고 오른쪽이 히스토그램 균등화를 통한 이미지입니다. 이미지가 명확해지긴했으나 석고상의 밝기값이 너무 큽니다. 이는 이미지가 전체적으로 빛이 고르게 비치지 못하고 석고상에 상대적으로 더 많은 빛이 비춰졌기 때문입니다. 이를 위해서는 히스토그램 균등화를 이미지 전체에 대해 적용하는게 아닌 일정한 영역을 분리하여 해당 영역에 대한 히스토그램 균등화 연산을 수행해 그 결과를 조합하면 됩니다. 이러한 알고리즘을 CLAHE(Contrast Limited Adaptive Histogram Equalization)라고 하는데, 해당 코드는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/tsukuba_l_clr.png',0)

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)

res = np.hstack((img,cl1)) #stacking images side-by-side

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

결과는 다음과 같습니다.

CLAHE 알고리즘을 구현한 cv2.createCLAHE 함수에서 tileGridSize=(8,8)이라는 의미는 8×8 격자 크기의 영역을 사용한다는 것입니다.

Python과 OpenCV – 20 : 히스토그램(Histogram) 1/4

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_histograms/py_histogram_begins/py_histogram_begins.html#histograms-getting-started 입니다.

아래 그림을 보면..

이미지에 대해 어떤 그래프를 표시하고 있는데, 화소의 강도(Gray 값 또는 하나의 채널에 대한 값)를 갖는 화소의 개수를 각각 X축과 Y축으로 표시하고 있습니다. 대부분의 경우 화소의 강도는 0-255입니다.

히스토그램을 얻는 방법은 Numpy를 이용하는 것과 OpenCV를 이용하는 것이 있습니다. 먼저 OpenCV를 이용한 예제를 보면..

import cv2

img = cv2.imread('./data/home.jpg',0)
hist = cv2.calcHist([img],[0],None,[256],[0,256])

4번째 cv2.calcHist 함수가 이미지에 대한 히스토그램 정보를 얻는데, 이 함수의 첫번째는 입력 이미지의 배열이며 두번째는 히스토그램을 얻을 채널 인덱스, 세번째는 Mask 이미지, 네번째는 X 축 요소(BIN)의 개수이고 다섯번째는 Y 축 요소값의 범위로 하나의 채널에 대한 화소 강도가 0~255이므로 대부분의 경우 [0,256]이 됩니다. 이 함수의 반환값은 256개의 요소를 갖는 배열입니다.

Numpy를 이용하여 히스토그램을 얻는 코드는 다음과 같습니다.

import cv2
import numpy as np

img = cv2.imread('./data/home.jpg',0)
hist,bins = np.histogram(img.ravel(),256,[0,256])

5번째 코드의 np.histogram 함수가 히스토그램을 얻는 함수인데 반환값은 반환값은 256개의 요소를 갖는 배열인 hist와 X축 요소의 값을 나타내는 배열인 bins입니다. 이 함수 이외에도 hist = np.bincount(img.ravel(),minlength=256) 와 같은 더 빠른 함수가 가능합니다.

속도면에서 Numpy보다 OpenCV 방식이 훨씬 빠르므로 OpenCV를 사용하는 것이 좋습니다.

히스토그램 값을 그래프로 표시하기 위한 예제는 다음과 같습니다.

import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/home.jpg',0)
plt.hist(img.ravel(), 256, [0,256]); 
plt.show()

결과는 다음과 같습니다.

BGR 형태와 같이 3개의 채널로 구성된 이미지에 대한 각 채널의 히스토그램도 시각화가 가능한데, 관련된 예제는 다음과 같습니다.

import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
    histr = cv2.calcHist([img],[i],None,[256],[0,256])
    plt.plot(histr,color = col)
    plt.xlim([0,256])
plt.show()

결과는 다음처럼 3개 채널 각각의 히스토그램 결과가 표시됩니다.

지금까지는 히스토그램 분석을 이미지 전체에 대해서 수행했는데, 필요할 경우 이미지의 원하는 영역에 대한 마스크를 지정해 해당 영역에 대한 히스토그램만을 분석할 수 있습니다. 아래의 코드가 예입니다.

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

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

# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv2.bitwise_and(img,img,mask = mask)

# Calculate histogram with mask and without mask
# Check third argument for mask
hist_full = cv2.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv2.calcHist([img],[0],mask,[256],[0,256])

plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])

plt.show()

결과는 다음과 같습니다.

Python과 OpenCV – 19 : 이미지의 등치선(Contours) – 5/5

이 글은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_hierarchy/py_contours_hierarchy.html#contours-hierarchy 를 참조로 하였습니다.

먼저 다음과 같은 코드가 있습니다.

import numpy as np
import cv2

img = cv2.imread('./data/opencv_contour_h.png')
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255,0)

_, contours,hierarchy = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

print(hierarchy)
cv2.imshow('img', img)

for i in range(len(contours)):
    cv2.waitKey()
    img = cv2.drawContours(img, contours, i, (0,0,255), 2)
    cv2.imshow('img', img)

print('END')
cv2.waitKey()
cv2.destroyAllWindows()

8번 코드의 cv2.findContours 함수의 2번째 인자에 따라 그 결과가 달라지는데, 특히 반환값 중 3번째인 hierarchy 값이 크게 달라집니다. hierarchy 값은 추출된 등치선 간의 계층 정보를 나타냅니다. cv2.findContours 함수의 2번째 인자에 따라 어떻게 변경되는지 요약 그림을 언급하는 선에서 정리합니다.

먼저 cv2.RETR_LIST 일 경우, 추출된 등치선의 인덱스 번호에 대한 그림입니다.

그리고 반환된 계층 정보는 다음과 같습니다.

[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [ 3  1 -1 -1]
  [ 4  2 -1 -1]
  [ 5  3 -1 -1]
  [ 6  4 -1 -1]
  [ 7  5 -1 -1]
  [ 8  6 -1 -1]
  [-1  7 -1 -1]]]

총 9개의 등치선이 추출되었으므로 위와 같이 총 9개의 계층 정보가 반환되는데 위의 각 9개 요소의 순서는 앞선 그림에서 표시된 등치선의 인덱스 순서와 동일합니다. 0부터 시작하고요. 그리고 각 요소는 다시 4개로 구성되는데.. [Next, Previous, First_Child, Parent]와 같습니다. 즉, [다음 등치선의 인덱스, 이전 등치선의 인덱스, 첫번째 자식 등치선의 인덱스, 부모 등치선의 인덱스] 입니다. 위의 계층 정보에 대한 내용은 다음 그림으로 표시될 수 있습니다.

다음은 cv2.RETR_EXTERNAL 인자에 대한 등치선의 인덱스 번호에 대한 그림입니다.

계층 정보는 다음과 같습니다.

[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [-1  1 -1 -1]]]

위 계층정보의 의미는 다음과 같습니다.

다음은 cv2.RETR_CCOMP 인자에 대한 등치선의 인덱스 번호에 대한 그림입니다.

계층 정보는 다음과 같습니다.

[[[ 3 -1  1 -1]
  [ 2 -1 -1  0]
  [-1  1 -1  0]
  [ 5  0  4 -1]
  [-1 -1 -1  3]
  [ 7  3  6 -1]
  [-1 -1 -1  5]
  [ 8  5 -1 -1]
  [-1  7 -1 -1]]]

위 계층정보의 의미는 다음과 같고요.

마지막으로 cv2.RETR_TREE 인자에 대한 등치선의 인덱스 번호에 대한 그림입니다.

계층 정보는 다음과 같습니다.

[[[ 7 -1  1 -1]
  [-1 -1  2  0]
  [-1 -1  3  1]
  [-1 -1  4  2]
  [-1 -1  5  3]
  [ 6 -1 -1  4]
  [-1  5 -1  4]
  [ 8  0 -1 -1]
  [-1  7 -1 -1]]]

위의 계층정보의 의미를 도식화하면 다음과 같습니다.