--- 본 포스팅은 데이콘 서포터즈 "데이크루 2기" 활동의 일환입니다 ---
- 안녕하세요 데이콘 서포터즈 데이크루 2기 포스(POS)팀의 Rahites입니다 :)
- POS팀은 Python OpenCV Study의 약자로 활동 기간동안 저희 팀은 '파이썬으로 만드는 OpenCV 프로젝트' 책을 가지고 OpenCV를 공부해보고 프로젝트를 진행할 것입니다.
- 자세한 스터디 계획과 운영 방안은 아래의 포스팅에서 확인하실 수 있습니다.
https://dacon.io/codeshare/4759?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
https://dacon.io/codeshare/4858?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
지금부터 3주차 활동 시작하겠습니다~!!
4. 이미지 프로세싱 기초¶
4.1. 관심영역¶
이미지에 어떤 연산을 적용해서 새로운 이미지나 정보를 얻어내려고 할 때 전체 이미지를 대상으로 연산하는 것보다 관심이 있는 부분만 잘라내서 하는 것이 훨씬 효과적입니다. 이 때 단순히 연산할 데이터 양을 줄이고 수행 시간을 단축시키는 이점도 있지만, 데이터의 양이 줄어들면 형태 또한 단순해져 적용하는 알고리즘과 좌표를 구하는 것이 단순해진다는 장점이 있습니다.
4.1.1. 관심영역 지정¶
이미지에서 분석의 대상으로 지정한 영역을 관심 영역 (ROI : Region Of Interest)라고 합니다. 전체 이미지가 img라는 변수에 있을 때 관심 있는 좌표를 x, y, 영역의 폭을 w, 높이를 h라고 하면 관심 영역을 지정하는 코드를 img[y:y+h, x:x+w]로 짤 수 있습니다. 이 때 NumPy배열은 행, 열 순으로 접근하기 때문에 높이, 폭 순서대로 크기를 지정해 주어야합니다. 또한 파이썬의 리스트 슬라이싱과 달리 NumPy배열의 슬라이싱은 원본의 참조를 반환하기 때문에 슬라이싱 결과를 수정한다면 원본 값에 영향을 미친다는 점에 주의해야 합니다.
예시 그림은 저희학과의 마스코트인 융융이로 진행해 보겠습니다.
# 관심영역 지정
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('../img/yungyung.jpg')
x=170; y=100; w=100; h=100 # roi 좌표
roi = img[y:y+h, x:x+w] # roi 지정 ---①
print(roi.shape) # roi shape, (50,50,3)
cv2.rectangle(roi, (0,0), (h-1, w-1), (0,255,0)) # roi 전체에 사각형 그리기 ---②
plt.imshow(img[:,:,(2,1,0)])
plt.xticks([])
plt.yticks([])
plt.show()
(100, 100, 3)
img = cv2.imread('../img/yungyung.jpg')
x=170; y=100; w=100; h=100
roi = img[y:y+h, x:x+w] # roi 지정
img2 = roi.copy() # roi 배열 복제했기 때문에 초록색 사각형이 표시되지 않았다.
img[y:y+h, x+w:x+w+w] = roi # 새로운 좌표에 roi 추가, 태양 2개 만들기
cv2.rectangle(img, (x,y), (x+w+w, y+h), (0,255,0)) # 2개의 태양 영역에 사각형 표시
imgs = {'img':img, 'img2':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2, i + 1)
plt.imshow(v[:,:,::-1])
plt.title(k)
plt.xticks([]); plt.yticks([])
plt.show()
4.1.2. 마우스로 관심영역 지정¶
매번 정확한 값으로 해당 위치의 값을 입력해주는 것은 어렵습니다. 마우스를 이용해 원하는 영역을 직접 지정하고 좌표를 알아내면 훨씬 더 편하게 관심영역을 지정할 수 있습니다.
from matplotlib import pyplot as plt
isDragging = False # 마우스 드래그 상태 저장
x0, y0, w, h = -1,-1,-1,-1 # 영역 선택 좌표 저장
blue, red = (255,0,0),(0,0,255) # 색상 값
def onMouse(event,x,y,flags,param): # 마우스 이벤트 핸들 함수 ---①
global isDragging, x0, y0, img # 전역변수 참조
if event == cv2.EVENT_LBUTTONDOWN: # 왼쪽 마우스 버튼 다운, 드래그 시작 ---②
isDragging = True
x0 = x
y0 = y
elif event == cv2.EVENT_MOUSEMOVE: # 마우스 움직임 ---③
if isDragging: # 드래그 진행 중
img_draw = img.copy() # 사각형 그림 표현을 위한 이미지 복제
# 매번 같은 이미지에 사각형을 그리면 사각형이 누적된 채 그려져서 보기 좋지 않기 때문
cv2.rectangle(img_draw, (x0, y0), (x, y), blue, 2) # 드래그 진행 영역 표시
cv2.imshow('img', img_draw) # 사각형 표시된 그림 화면 출력
elif event == cv2.EVENT_LBUTTONUP: # 왼쪽 마우스 버튼 업 ---④
if isDragging: # 드래그 중지
isDragging = False
w = x - x0 # 드래그 영역 폭 계산
h = y - y0 # 드래그 영역 높이 계산
print("x:%d, y:%d, w:%d, h:%d" % (x0, y0, w, h))
if w > 0 and h > 0: # 폭과 높이가 음수이면 드래그 방향이 옳음 ---⑤
img_draw = img.copy() # 선택 영역에 사각형 그림을 표시할 이미지 복제
# 선택 영역에 빨간 사각형 표시
cv2.rectangle(img_draw, (x0, y0), (x, y), red, 2)
cv2.imshow('img', img_draw) # 빨간 사각형 그려진 이미지 화면 출력
roi = img[y0:y0+h, x0:x0+w] # 원본 이미지에서 선택 영영만 ROI로 지정 ---⑥
cv2.imshow('cropped', roi) # ROI 지정 영역을 새창으로 표시
cv2.moveWindow('cropped', 0, 0) # 새창을 화면 좌측 상단에 이동
cv2.imwrite('./cropped.jpg', roi) # ROI 영역만 파일로 저장 ---⑦
plt.imshow(roi[:,:,::-1]) # 잘라낸 영역 보여줌
plt.xticks([])
plt.yticks([])
plt.show()
print("cropped.")
else:
cv2.imshow('img', img) # 드래그 방향이 잘못된 경우 사각형 그림이 없는 원본 이미지 출력
print("좌측 상단에서 우측 하단으로 영역을 드래그 하세요.")
img = cv2.imread('../img/me.jpg')
cv2.imshow('img', img)
cv2.setMouseCallback('img', onMouse) # 마우스 이벤트 등록 ---⑧
cv2.waitKey()
cv2.destroyAllWindows()
x:6, y:11, w:434, h:368
cropped.
OpenCV에서 제공하는 관심영역 지정함수 ( 마우스 이벤트 처리를 위한 코드 없이도 마우스로 ROI를 지정할 수 있다. )
ret = cv2.selectROI([win_name,] img[, showCrossHair=True, fromCenter=False])
- win_name : ROI 선택을 진행할 창의 이름
- img : ROI 선택을 진행할 이미지, NumPy ndarray
- showCrossHair : 선택 영역 중심에 십자 모양 표시 여부
- fromCenter : 마우스 시작 지점을 영역의 중심으로 지정
- ret : 선택한 영역 좌표와 크기(x,y,w,h), 선택을 취소한 경우 모두 0
영역을 선택하고 나서 키보드의 space나 enter키를 누르면 선택한 영역의 x,y 좌표와 영역의 폭과 높이를 튜플에 담아 반환한다. 선택을 취소하고 싶으면 c키를 누르면 된다.(이때는 반환하는 모든 값이 0이된다) showCrossHair에 True를 전달하면 마우스로 선택하는 동안 그려지는 사각형 안 중심에 십자 모양을 그려주고, fromCenter에 True를 전달하면 마우스의 시작 지점을 중심점으로 간주해서 좌표를 반환합니다.
import cv2
import numpy as np
img = cv2.imread('../img/me.jpg')
x,y,w,h = cv2.selectROI('img',img,False)
if w and h :
roi = img[y:y+h, x:x+w] # 지정할 영역
cv2.imshow('cropped', roi)
cv2.moveWindow('cropped', 0, 0)
cv2.imwrite('./cropped2.jpg', roi) # ROI 영역만 따로 파일저장
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
4.2. 컬러 스페이스¶
영상에 색상과 명암을 표현하는 방법들과 각각의 차이와 활용 방법을 알아보겠습니다.
4.2.1. 디지털 영상의 종류¶
디지털화된 이미지는 픽셀 단위가 여러개 모여서 그림을 표현합니다. 하나의 픽셀을 어떻게 구성하느냐에 따라 이미지를 구분할 수 있습니다. 한 개의 픽셀을 두 가지로 표현하면 바이너리 이미지라고 하고 0을 검은색, 1이나 255를 흰색으로 표시합니다. 두 가지의 값만 존재하기 때문에 값대신 점의 밀도로 명암을 표현해야 합니다. 한 픽셀은 0~255까지의 값을 가질 수 있기 때문에 음수가 없어 부호 없는 1바이트 크기로 표현합니다. 이미지 프로세싱을 할 때에는 색상 정보가 필요없기 때문에 컬러 이미지의 색상 정보를 제거해 연산의 양을 줄이려 그레이 스케일을 사용합니다.
4.2.2. RGB, BGR, RGBA¶
컴퓨터로 이미지에 색상을 표현하는 방법 중 가장 많이 사용하는 방법은 RGB 컬러 스페이스 입니다. 컬러 스페이스는 각 바이트마다 어떤 색상 표현의 역할을 맡을지를 결정하는 시스템을 의미합니다. 3가지 색상을 표현하는 RGB 이미지는 row x column x channel인 3차원 배열로 표현됩니다. 하지만 OpenCV는 이 순서의 반대인 BGR 방법을 사용합니다. RGBA 방법은 배경을 투명하게 처리하기 위해 알파(alpha) 채널을 추가한 것을 의미하는데 주로 배경의 투명도를 표현하기 위해 0,255만을 사용합니다. cv2.imread()의 두번째 인자가 cv2.IMREAD_COLOR일 경우 BGR로 인식하고 cv2.IMREAD_UNCHANGED인 경우에는 이미지가 알파채널을 가지고 있을 때 BGRA로 읽어 들입니다. 알파채널은 전경과 배경을 손쉽게 분리할 수 있어서 마스크 채널이라고도 부릅니다.
# 기본 값 옵션
img = cv2.imread('../img_1/opencv_logo.png')
# IMREAD_COLOR 옵션
bgr = cv2.imread('../img_1/opencv_logo.png', cv2.IMREAD_COLOR)
# IMREAD_UNCHANGED 옵션
bgra = cv2.imread('../img_1/opencv_logo.png', cv2.IMREAD_UNCHANGED)
# 각 옵션에 따른 이미지 shape
print("default", img.shape, "color", bgr.shape, "unchanged", bgra.shape)
cv2.imshow('bgr', bgr)
cv2.imshow('bgra', bgra)
cv2.imshow('alpha', bgra[:,:,3]) # 알파 채널만 표시
cv2.waitKey(0)
cv2.destroyAllWindows()
default (120, 98, 3) color (120, 98, 3) unchanged (120, 98, 4)
4.2.3. 컬러 스페이스 변환¶
컬러 이미지를 그레이 스케일로 변환하는 것은 이미지 연산의 양을 줄여 속도를 높이는데 꼭 필요합니다. 이미지를 읽어올 때부터 그레이 스케일로 읽어오고 싶다면 2장에서 배웠던 것 처럼 cv2.imread(img, cv2.IMREAD_GRAYSCALE)을 사용하면 됩니다. 하지만 처음에는 컬러 스케일로 받아오고 중간에 이를 변경하고 싶다면 cv2.cvtColor() 함수를 이용합니다.
# ( 3채널의 평균 값을 구해서 그레이 스케일로 변환하는 방법 & OpenCV 함수 이용)
import cv2
import numpy as np
img = cv2.imread('../img/girl.jpg')
img2 = img.astype(np.uint16) # dtype 변경 ---①
b,g,r = cv2.split(img2) # 채널 별로 분리 ---②
#b,g,r = img2[:,:,0], img2[:,:,1], img2[:,:,2]
gray1 = ((b + g + r)/3).astype(np.uint8) # 평균 값 연산후 dtype 변경 ---③
gray2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # BGR을 그레이 스케일로 변경 ---④
grays = {'gray1':gray1, 'gray2':gray2}
plt.subplot(131)
plt.xticks([]); plt.yticks([])
plt.title('original')
plt.imshow(img[:,:,::-1])
for i, (k, v) in enumerate(grays.items()):
plt.subplot(1,3, i + 2)
plt.imshow(v, cmap='gray')
plt.title(k)
plt.xticks([]); plt.yticks([])
plt.show()
- out = cv2.cvtColor(img, flag)
- img : NumPy 배열, 변환할 이미지
- flag : 변환할 컬러 스페이스, cv2.COLOR_로 시작하는 이름
- cv2.COLOR_BGR2GRAY : BGR컬러 이미지를 그레이 스케일로 변환
- cv2.COLOR_GRAY2BGR : 그레이 스케일 이미지를 BGR 컬러 이미지로 변환
- cv2.COLOR_BGR2RGB : BGR 컬러 이미지를 RGB 컬러 이미지로 변환
- cv2.COLOR_BGR2HSV : BGR 컬러 이미지를 HSV 컬러 이미지로 변환
- cv2.COLOR_HSV2BGR : HSV 컬러 이미지를 BGR 컬러 이미지로 변환
- cv2.COLOR_BGR2YUV : BGR 컬러 이미지를 YUV 컬러 이미지로 변환
- cv2.COLOR_YUV2BGR : YUV 컬러 이미지를 BGR 컬러 이미지로 변환
- out : 반환한 결과 이미지 (NumPy 배열)
COLOR_GRAY2BGR flag는 흑백 사진을 컬러 사진으로 바꿔주는 것이 아니라 2차원 배열 이미지를 3개 채널이 모두 같은 값을 갖는 3차원 배열로 변환하는 것입니다. 차원이 다르면 서로 연산을 할 수 없기 때문에 차원을 맞춰주는 용도로 사용합니다.
# 모든 플래그 상수를 출력해서 볼 수 있는 코드
# [i for i in dir(cv2) if i.startswith('COLOR_')]
4.2.4. HSV, HSI, HSL¶
HSV는 RGB와 마찬가지로 3채널로 컬러 이미지를 표시합니다. H(Hue, 색조), S(Saturation, 채도), V(Value, 명도)로 명도를 표현하는 방법에 따라 마지막 V를 I(Intensity, 밀도)로 표기하는 HSI, L(Lightness, 명도)로 표기하는 HSL 컬러 시스템도 존재합니다. 이들은 밝기 값을 계산하는 방법의 차이가 조금씩 있지만 편의상 같은 시스템으로 보고 HSV를 기준으로 설명하겠습니다.
H 값은 해당 픽셀이 어떤 색인지를 표현합니다. 원 안에서 360도 범위에 값을 갖게해 색상에 매칭되는 숫자로 색을 표현합니다. 하지만 OpenCV에서는 dtype이 255를 넘지 못하기 때문에 360을 반으로 나눈 0 ~ 180 범위의 값으로 표현하고 180보다 클 경우에는 180으로 간주합니다. 대략 빨강은 165~180,0~15 초록은 45~75 파랑은 90~120의 값으로 이루어져 있습니다.
S 값은 해당 색상이 얼마나 순수하게 포함되어 있는지를 표현합니다. 이 때 255값이 가장 순수한 색상을 표현합니다.
V 값은 빛이 얼마나 밝은지 어두운지를 표현하는 값입니다. 255값이 가장 밝은 상태이고 0이 가장 어두운 상태로 검은색을 표현합니다.
픽셀의 색상이 궁금할 때 RGB 는 3가지 채널의 값을 모두 조사해야 하지만 HSV는 H 채널 값만 확인하면 되기 때문에 색상을 기반으로 하는 여러 작업에서 효과적입니다.
#---① BGR 컬러 스페이스로 원색 픽셀 생성
red_bgr = np.array([[[0,0,255]]], dtype=np.uint8) # 빨강 값만 갖는 픽셀
green_bgr = np.array([[[0,255,0]]], dtype=np.uint8) # 초록 값만 갖는 픽셀
blue_bgr = np.array([[[255,0,0]]], dtype=np.uint8) # 파랑 값만 갖는 픽셀
yellow_bgr = np.array([[[0,255,255]]], dtype=np.uint8) # 노랑 값만 갖는 픽셀
#---② BGR 컬러 스페이스를 HSV 컬러 스페이스로 변환
red_hsv = cv2.cvtColor(red_bgr, cv2.COLOR_BGR2HSV);
green_hsv = cv2.cvtColor(green_bgr, cv2.COLOR_BGR2HSV);
blue_hsv = cv2.cvtColor(blue_bgr, cv2.COLOR_BGR2HSV);
yellow_hsv = cv2.cvtColor(yellow_bgr, cv2.COLOR_BGR2HSV);
#---③ HSV로 변환한 픽셀 출력
print("red:",red_hsv)
print("green:", green_hsv)
print("blue", blue_hsv)
print("yellow", yellow_hsv)
red: [[[ 0 255 255]]] green: [[[ 60 255 255]]] blue [[[120 255 255]]] yellow [[[ 30 255 255]]]
4.2.5. YUV, YCbCr¶
YUV 는 사람이 색상을 인식할 때 밝기에 더 민감하고 색상은 상대적으로 둔감한 점을 고려하여 만든 컬러 스페이스입니다. Y는 밝기(Luma), U(Chroma Blue, CB)는 밝기와 파란색과의 색상 차, V(Chroma Red, Cr)는 밝기와 빨간색과의 색상 차를 표현합니다. Y에는 많은 비트수를 할당하고 U,V에는 적은 비트 수를 할당해서 데이터를 압축하는 효과를 갖습니다. YUV는 YCbCr 과 혼용되기도 하는데 YCbCr은 MPEG나 JPEG같은 디지털 컬러 정보를 인코딩하는데 사용하였습니다. 실제로 파일을 변환하는 공식이 달라서 OpenCV에서도 BGR2YUV, BGR2YCrCb가 따로 있지만 미세하게 다른 차이기 때문에 편의상 같은 시스템으로 보고 YUV 컬러 스페이스에 대해서만 소개하겠습니다. YUV는 밝기 정보와 컬러 정보를 분리해서 사용하기 때문에 명암대비가 좋지 않은 영상을 좋게 만드는데 대표적으로 활용됩니다.
# ( 픽셀의 밝기를 제어해야할 때 YUV는 Y채널만 작업하면 되므로 효과적 )
#---① BGR 컬러 스페이스로 3가지 밝기의 픽셀 생성
dark = np.array([[[0,0,0]]], dtype=np.uint8) # 3 채널 모두 0인 가장 어두운 픽셀
middle = np.array([[[127,127,127]]], dtype=np.uint8) # 3 채널 모두 127인 중간 밝기 픽셀
bright = np.array([[[255,255,255]]], dtype=np.uint8) # 3 채널 모두 255인 가장 밝은 픽셀
#---② BGR 컬러 스페이스를 YUV 컬러 스페이스로 변환
dark_yuv = cv2.cvtColor(dark, cv2.COLOR_BGR2YUV)
middle_yuv = cv2.cvtColor(middle, cv2.COLOR_BGR2YUV)
bright_yuv = cv2.cvtColor(bright, cv2.COLOR_BGR2YUV)
#---③ YUV로 변환한 픽셀 출력
print("dark:",dark_yuv)
print("middle:", middle_yuv)
print("bright", bright_yuv)
dark: [[[ 0 128 128]]] middle: [[[127 128 128]]] bright [[[255 128 128]]]
4.3. 스레시홀딩¶
이미지를 검은색과 흰색으로만 표현한 것을 바이너리 이미지라고 합니다. 이렇게 하는 이유는 이미지에서 원하는 피사체의 모양을 좀더 정확히 판단하기 위해서입니다. 스레시 홀딩이란 여러 점수를 커트라인을 기준으로 합격과 불합격으로 나누는 것처럼 여러 경계값들을 기준으로 두 가지 부류로 나누는 것으로 바이너리 이미지를 만드는 가장 대표적인 방법입니다.
4.3.1. 전역 스레시홀딩¶
바이너리 이미지를 만들기 위해서는 컬러이미지를 그레이 스케일로 바꾸고 각 픽셀의 값이 경계 값을 넘으면 255, 넘지 못하면 0으로 지정합니다. OpenCV에서는 cv2.threshold()함수로 더 많은 기능을 제공합니다.
- ret, out = cv2.threshold(img, threshold, value, type_flag)
- img : NumPy 배열, 변환할 이미지
- threshold ; 경계 값
- value : 경계 값 기준에 만족하는 픽셀에 적용할 값
- type_flag : 스레스홀드 적용 방법 지정
- cv2.THRESH_BINARY : 픽셀값이 경계값을 넘으면 value를 지정하고 넘지 못하면 0을 지정
- cv2.THRESH_BINARY_INV : THRESH_BINARY의 반대
- cv2.THRESH_TRUNC : 픽셀 값이 경계 값을 넘으면 경계 값을 지정하고 넘지 못하면 원래 값 유지
- cv2.THRESH_TOZERO : 픽셀 값이 경계 값을 넘으면 원래 값을 유지, 넘지 못하면 0을 지정
- CV2.THRESH_TOZERO_INV : THRESH_TOZERO의 반대
- ret : 스레시 홀딩에 사용한 경계 값
- out : 결과 바이너리 이미지
# 바이너리 이미지 만들기
import matplotlib.pylab as plt
img = cv2.imread('../img/girl.jpg', cv2.IMREAD_GRAYSCALE) #이미지를 그레이 스케일로 읽기
# --- ① NumPy API로 바이너리 이미지 만들기
thresh_np = np.zeros_like(img) # 원본과 동일한 크기의 0으로 채워진 이미지
thresh_np[ img > 127] = 255 # 127 보다 큰 값만 255로 변경
# ---② OpenCV API로 바이너리 이미지 만들기
ret, thresh_cv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
print(ret) # 127.0, 바이너리 이미지에 사용된 문턱 값 반환
# ---③ 원본과 결과물을 matplotlib으로 출력
imgs = {'Original': img, 'NumPy API':thresh_np, 'cv2.threshold': thresh_cv}
for i , (key, value) in enumerate(imgs.items()):
plt.subplot(1, 3, i+1)
plt.title(key)
plt.imshow(value, cmap='gray')
plt.xticks([]); plt.yticks([])
plt.show()
127.0
# 스레시홀딩 플래그 실습
img = cv2.imread('../img/girl.jpg', cv2.IMREAD_GRAYSCALE)
_, t_bin = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
_, t_bininv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
_, t_truc = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
_, t_2zr = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
_, t_2zrinv = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)
imgs = {'origin':img, 'BINARY':t_bin, 'BINARY_INV':t_bininv, \
'TRUNC':t_truc, 'TOZERO':t_2zr, 'TOZERO_INV':t_2zrinv}
for i, (key, value) in enumerate(imgs.items()):
plt.subplot(2,3, i+1)
plt.title(key)
plt.imshow(value, cmap='gray')
plt.xticks([]); plt.yticks([])
plt.show()
4.3.2. 오츠의 알고리즘¶
바이너리 이미지를 만들 때 중요한 작업은 경계 값을 얼마로 정하느냐 입니다. 예를들어 새하얀 종이에 검은색으로 출력된 문서의 영상이라면 굳이 스레시홀드를 적용할 필요가 없습니다. 하지만 현실에는 흰색 종이뿐만 아니라 다양한 색의 종이가 사용되기 때문에 적절한 경계값을 찾아내는 것이 중요합니다.
1979년 오츠 노부유키는 경계값을 찾는데 있어 반복적인 시도 없이 한 번에 효율적으로 경계 값을 찾을 수 있는 방법을 제안했습니다. 그의 이름을 딴 '오츠의 이진화 알고리즘'이라고 부르는 이 알고리즘은 경계 값을 임의로 정해서 픽셀들을 두 부류로 나누고 두 부류의 명암 분포를 반보개서 구한 다음 두 부류의 명암 분포를 가장 균일하게 하는 경계 값을 선택합니다.
$\sigma^2_w(t) = w_1(t)\sigma^2_1(t) + w_2(t)\sigma^2_2(t)$
- t : 0~255 경계값
- $w_1, w_2$ : 각 부류의 비율 가중치
- $\sigma^2_1, \sigma^2_2$ : 각 부류의 분산
오츠의 알고리즘을 사용하기 위해서는 cv2.threshold() 함수의 마지막 인자에 cv2.THRESH_OTSU를 추가해서 전달하기만 하면됩니다.
# 이미지를 그레이 스케일로 읽기
img = cv2.imread('../img/girl.jpg', cv2.IMREAD_GRAYSCALE)
# 경계 값을 130으로 지정 ---①
_, t_130 = cv2.threshold(img, 130, 255, cv2.THRESH_BINARY)
# 경계 값을 지정하지 않고 OTSU 알고리즘 선택 ---② 경계값으로 의미없는 -1을 적용한 모습
t, t_otsu = cv2.threshold(img, -1, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('otsu threshold:', t) # Otsu 알고리즘으로 선택된 경계 값 출력
imgs = {'Original': img, 't:130':t_130, 'otsu:%d'%t: t_otsu}
for i , (key, value) in enumerate(imgs.items()):
plt.subplot(1, 3, i+1)
plt.title(key)
plt.imshow(value, cmap='gray')
plt.xticks([]); plt.yticks([])
plt.show()
otsu threshold: 92.0
하지만 오츠 알고리즘은 모든 경계 값을 조사하기 때문에 시간이 오래걸리고 노이즈가 많은 영상에서는 오츠의 알고리즘을 적용해도 좋은 결과를 얻지 못한다는 단점이 있습니다. 노이즈가 많은 영상에 오츠의 알조리즘을 적용할 때에는 블러링 필터를 먼저 적용해야 합니다.
4.3.3 적응형 스레시홀드¶
노이즈가 많은 영상에는 이미지를 먼저 여러 영역으로 나눈 다음 주변 픽셀 값만 가지고 계산을 해서 경계 값을 구해주어야 합니다. 이를 적응형 스레시 홀드라고 하고 OpenCV에서는 다음 함수로 이를 구현할 수 있습니다.
- cv2.adaptiveThreshold(img, value, method, type_flag, block_size, C)
- img : 입력 영상
- value : 경계 값을 만족하는 픽셀에 적용할 값
- method : 경계 값 결정 방법
- cv2.ADPTIVE_THRESH_MEAN_C : 이웃 픽셀의 평균으로 결정
- cv2.ADPTIVE_THRESH_GAUSSIAN_C : 가우시안 분포에 따른 가중치의 합으로 결정
- type_flag : 스레시홀드 적용 방법 지정
- block_size : 영역으로 나눌 이웃의 크기(n x n), 홀수
- C : 계산된 경계 값 결과에서 가감할 상수 ( 음수 가능 )
blk_size = 9 # 블럭 사이즈
C = 5 # 차감 상수
img = cv2.imread('../img/happyman.jpg', cv2.IMREAD_GRAYSCALE) # 그레이 스케일로 읽기
# ---① 오츠의 알고리즘으로 단일 경계 값을 전체 이미지에 적용
ret, th1 = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
# ---② 어뎁티드 쓰레시홀드를 평균과 가우시안 분포로 각각 적용
th2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C,\
cv2.THRESH_BINARY, blk_size, C)
th3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, \
cv2.THRESH_BINARY, blk_size, C)
# ---③ 결과를 Matplot으로 출력
imgs = {'Original': img, 'Global-Otsu:%d'%ret:th1, \
'Adapted-Mean':th2, 'Adapted-Gaussian': th3}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v,'gray')
plt.xticks([]),plt.yticks([])
plt.show()
평균 값으로 적응형 스레시홀드를 적용한 결과보다 가우시안 분포를 사용한 결과가 noise가 훨씬 적습니다.
경계 값을 전체 이미지에 적용하는 것을 전역적(global) 적용이라고 하고 이미지를 여러 구역으로 나누어 해당 구역에 맞는 경계 값을 찾는 것을 지역적(local) 적용이라고 합니다.
4.4. 이미지 연산¶
4.4.1. 영상과 영상의 연산¶
영상에 연산을 적용하는 방법에는 NumPy의 브로드캐스팅 연산을 적용하는 방법과 OpenCV 함수를 사용하는 방법이 있습니다.
OpenCV에서 제공하는 함수
- dest = cv2.add(src1, src2[, dest, mask, dtype]): src1과 src2 더하기
- src1 : 입력 영상 1 or 수
- src2 : 입력 영상 2 or 수
- dest : 출력 영상
- mask : 0이 아닌 픽셀만 연산
- dtype : 출력 dtype
- dest = cv2.substract(src1, src2[, dest, mask, dtype]) : src1에서 src2를 빼기
- dest = cv2.multiply(src1, src2[, dest, scale, dtype]) : src1과 src2를 곱하기
- scale : 연산 결과에 추가 연산할 값
- dest = cv2.divide(src1, src2[, dest, scale, dtype]) : src1을 src2로 나누기
# ---① 연산에 사용할 배열 생성
a = np.uint8([[200, 50]])
b = np.uint8([[100, 100]])
#---② NumPy 배열 직접 연산
add1 = a + b
sub1 = a - b
mult1 = a * 2
div1 = a / 3
# ---③ OpenCV API를 이용한 연산
add2 = cv2.add(a, b)
sub2 = cv2.subtract(a, b)
mult2 = cv2.multiply(a , 2)
div2 = cv2.divide(a, 3)
#---④ 각 연산 결과 출력
print(add1, add2)
print(sub1, sub2)
print(mult1, mult2)
print(div1, div2)
[[ 44 150]] [[255 150]] [[100 206]] [[100 0]] [[144 100]] [[255 100]] [[66.66666667 16.66666667]] [[67 17]]
NumPy 배열을 직접 계산했을 때는 200과 100을 더한 결과가 255를 초과하여 44라는 결과가 나왔고, OpenCV API를 사용할 때는 최대값인 255로 출력되는 것을 확인할 수 있습니다. 반대로 50에서 100을 뺄때는 NumPy 배열을 직접 계산할 땐 255에서 뺀 결과인 206이 출력 되었고 OpenCV로 계산 할 때는 최솟값인 0으로 출력된 것을 확인할 수 있습니다. 즉 OpenCV를 사용해 연산을 할 경우에는 0 ~ 255 값을 갖습니다.
또한 더한 결과를 새로운 인자로 지정하고 싶을 때에는 c = cv2.add(a, b)외에도 cv2.add(a, b, c)를 사용하면 됩니다. 하지만 4번째 인자로 mask를 지정해 주면 해당 mask 배열의 위치에 해당하는 요소들의 연산은 이루어지지 않습니다.
#---① 연산에 사용할 배열 생성
a = np.array([[1, 2]], dtype=np.uint8)
b = np.array([[10, 20]], dtype=np.uint8)
#---② 2번째 요소가 0인 마스크 배열 생성
mask = np.array([[1, 0]], dtype=np.uint8)
#---③ 누적 할당과의 비교 연산
c1 = cv2.add( a, b , None, mask)
print(c1)
c2 = cv2.add( a, b , b, mask)
print(c2, b)
c3 = cv2.add( a, b , b.copy(), mask) # b의 값을 연산 전 상태 그대로 유지하고 싶을 때
print(c3, b)
[[11 0]] [[11 20]] [[11 20]] [[12 20]] [[11 20]]
4.4.2. 알파 블렌딩¶
cv2.add() 연산을 실행하면 대부분의 픽셀값이 255로 몰리는 현상이 일어나기 때문에 영상이 하얗게 나오게 되는 단점이 있습니다.
# ---① 연산에 사용할 이미지 읽기
img1 = cv2.imread('../img/cat.jpg')
img2 = cv2.imread('../img/tiger.jpg')
# ---② 이미지 덧셈
img3 = img1 + img2 # 더하기 연산
img4 = cv2.add(img1, img2) # OpenCV 함수
imgs = {'img1':img1, 'img2':img2, 'img1+img2': img3, 'cv.add(img1, img2)': img4}
# ---③ 이미지 출력
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2, i + 1)
plt.imshow(v[:,:,::-1])
plt.title(k)
plt.xticks([]); plt.yticks([])
plt.show()
img1 + img2는 화소가 고르지 못하고 값이 255를 초과한 영역에 이상한 색이 보입니다. 반대로 cv.add를 한 부분은 전체적으로 하얀 픽셀이 많이 보입니다. 따라서 두 영상을 합성하기 위해서는 각 픽셀의 합이 255가 되지 않게 각각의 영상에 가중치를 줘서 계산해야 합니다. 이 때 alpha 값을 각 영상에 가중치로 적용해 적절한 비중으로 배분하는 방법을 사용합니다.
$g(x)=(1-\alpha)f_0(x)+\alpha f_1(x)$
- $f_0(x)$ : 첫 번째 이미지 픽셀 값
- $f_1(x)$ : 두 번째 이미지 픽셀 값
- $\alpha$ : 가중치
- g(x) : 합성 결과 픽셀 값
OpenCV 함수
- cv2.addWeight(img1, alpha, img2, beta, gamma)
- img1, img2 : 합성할 두 영상
- alpha : img1에 지정할 가중치
- beta : img2에 지정할 가중치, 주로 (1-alpha)
- gamma : 연산 결과에 가감할 상수, 주로 0
# 각 50%의 가중치
import cv2
import numpy as np
alpha = 0.5 # 합성에 사용할 알파 값
#---① 합성에 사용할 영상 읽기
img1 = cv2.imread('../img/cat.jpg')
img2 = cv2.imread('../img/tiger.jpg')
# ---② NumPy 배열에 수식을 직접 연산해서 알파 블렌딩 적용
blended = img1 * alpha + img2 * (1-alpha)
blended = blended.astype(np.uint8) # 소수점 발생을 제거하기 위함
# ---③ addWeighted() 함수로 알파 블렌딩 적용
dst = cv2.addWeighted(img1, alpha, img2, (1-alpha), 0)
imgs = {'img1 * alpha + img2 * (1-alpha)':blended, 'addWeighted':dst}
# ---③ 이미지 출력
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2, i + 1)
plt.imshow(v[:,:,::-1])
plt.title(k)
plt.xticks([]); plt.yticks([])
plt.show()
알파 블렌딩은 흔히 fade-in/out 기법으로 영상이 전환될 때 사용되며 영화의 변신 장면에서 얼굴 모핑이라는 기법으로도 사용됩니다.
win_name = 'Alpha blending' # 창 이름
trackbar_name = 'fade' # 트렉바 이름
# ---① 트렉바 이벤트 핸들러 함수
def onChange(x):
alpha = x/100
dst = cv2.addWeighted(img1, 1-alpha, img2, alpha, 0)
cv2.imshow(win_name, dst)
# ---② 합성 영상 읽기
img1 = cv2.imread('../img/cat.jpg')
img2 = cv2.imread('../img/tiger.jpg')
# ---③ 이미지 표시 및 트렉바 붙이기
cv2.imshow(win_name, img1)
cv2.createTrackbar(trackbar_name, win_name, 0, 100, onChange)
cv2.waitKey()
cv2.destroyAllWindows()
4.4.3. 비트와이즈 연산¶
OpenCV는 두 영상의 각 픽셀에 대한 비트와이즈(비트 단위) 연산을 제공합니다. 이는 두 영상을 합성할 때 특정 영역만 선택하거나 특정 영역만 제외하는 경우에 사용됩니다.
- bitwise_and(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 AND 연산
- bitwise_or(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 OR 연산
- bitwise_xor(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 XOR 연산
- bitwise_not(img1, mask=None) : 각 픽셀에 대해 비트와이즈 NOT 연산
- img1, img2 : 동일한 shape의 두 영상
- mask : 0이 아닌 픽셀만 연산, 바이너리 이미지
#--① 연산에 사용할 이미지 생성
img1 = np.zeros( ( 200,400), dtype=np.uint8)
img2 = np.zeros( ( 200,400), dtype=np.uint8)
img1[:, :200] = 255 # 왼쪽은 검정색(0), 오른쪽은 흰색(255)
img2[100:200, :] = 255 # 위쪽은 검정색(0), 아래쪽은 흰색(255)
#--② 비트와이즈 연산
bitAnd = cv2.bitwise_and(img1, img2)
bitOr = cv2.bitwise_or(img1, img2)
bitXor = cv2.bitwise_xor(img1, img2)
bitNot = cv2.bitwise_not(img1)
#--③ Plot으로 결과 출력
imgs = {'img1':img1, 'img2':img2, 'and':bitAnd,
'or':bitOr, 'xor':bitXor, 'not(img1)':bitNot}
for i, (title, img) in enumerate(imgs.items()):
plt.subplot(3,2,i+1)
plt.title(title)
plt.imshow(img, 'gray')
plt.xticks([]); plt.yticks([])
plt.show()
img = cv2.imread('../img/girl.jpg')
#--② 마스크 만들기
mask = np.zeros_like(img) # 원본과 동일한 크기로 0배열 ( 검정색 )
cv2.circle(mask, (340,140), 100, (255,255,255), -1) # 원하는 위치에 하얀색 원
#cv2.circle(대상이미지, (원점x, 원점y), 반지름, (색상), 채우기)
'''
mask 인자를 용하면 2차원 배열으로도 가능하다
mask = np.zeros(img.shape[:2], dtype=np.uint8)
cv2.circle(mask, (340,140), 100, (255), -1)
masked = cv2.bitwise_and(img, img, mask=mask)
'''
#--③ 마스킹 ( 흰부분만 출력되도록)
masked = cv2.bitwise_and(img, mask)
#--④ 결과 출력
imgs = {'original':img, 'mask':mask, 'masked': masked}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3, i + 1)
plt.imshow(v[:,:,::-1])
plt.title(k)
plt.xticks([]); plt.yticks([])
plt.show()
4.4.4. 차영상¶
영상에서 영상을 빼기 연산하면 두 영상의 변화를 알 수 있는데 이를 차영상(image differencing)이라고 합니다. 이 때 바로 두 영상을 빼기 연산하면 음수가 나올 수 있기 때문에 절대 값을 구해야 합니다.
- diff = cv2.absdiff(img1, img2)
# 사람의 눈으로 찾기 힘든 차이를 찾아낼 수 있습니다.
#--① 연산에 필요한 영상을 읽고 그레이스케일로 변환
img1 = cv2.imread('../img_1/robot_arm1.jpg')
img2 = cv2.imread('../img_1/robot_arm2.jpg')
img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
#--② 두 영상의 절대값 차 연산
diff = cv2.absdiff(img1_gray, img2_gray)
#--③ 차 영상을 극대화 하기 위해 쓰레시홀드 처리 및 컬러로 변환
_, diff = cv2.threshold(diff, 1, 255, cv2.THRESH_BINARY)
diff_red = cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
diff_red[:,:,2] = 0
#--④ 두 번째 이미지에 변화 부분 표시
spot = cv2.bitwise_xor(img2, diff_red)
#--⑤ 결과 영상 출력
fig = plt.figure(figsize=(10, 8))
plt.subplot(221), plt.axis('off'), plt.imshow(img1), plt.title("img1")
plt.subplot(222), plt.axis('off'), plt.imshow(img2), plt.title("img2")
plt.subplot(223), plt.axis('off'), plt.imshow(diff,'gray'), plt.title("diff")
plt.subplot(224), plt.axis('off'), plt.imshow(spot), plt.title("spot")
plt.show()
4.4.5. 이미지 합성과 마스킹¶
여러개의 영상에서 특정 영역끼리 합성하기 위해, 우선 영상에서 원하는 영역을 떼어낼 때 마스크를 사용합니다.
#--① 합성에 사용할 영상 읽기, 전경 영상은 4채널 png 파일
img_fg = cv2.imread('../img_1/opencv_logo.png', cv2.IMREAD_UNCHANGED)
img_bg = cv2.imread('../img/girl.jpg')
#--② 알파채널을 이용해서 마스크와 역마스크 생성
_, mask = cv2.threshold(img_fg[:,:,3], 1, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
#--③ 전경 영상 크기로 배경 영상에서 ROI 잘라내기
img_fg = cv2.cvtColor(img_fg, cv2.COLOR_BGRA2BGR)
h, w = img_fg.shape[:2]
roi = img_bg[10:10+h, 10:10+w ]
#--④ 마스크 이용해서 오려내기
masked_fg = cv2.bitwise_and(img_fg, img_fg, mask=mask)
masked_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)
#--⑥ 이미지 합성
added = masked_fg + masked_bg
img_bg[10:10+h, 10:10+w] = added
plt.figure(figsize=(5,5))
imgs = {'masked_fg' : masked_fg,'masked_bg' :masked_bg, 'added':added, 'result':img_bg}
plt.subplot(321)
plt.xticks([]); plt.yticks([])
plt.imshow(mask,'gray')
plt.subplot(322)
plt.xticks([]); plt.yticks([])
plt.imshow(mask_inv,'gray')
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(3,2, i+3)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]); plt.yticks([])
plt.show()
- dst = cv2.inRange(img, from, to) : 범위에 속하지 않은 픽셀 판단
- img : 입력 영상
- from : 범위의 시작 배열
- to : 범위의 끝 배열
- dst : img가 from ~ to 에 포함되면 255, 아니면 0을 픽셀 값으로 하는 배열
import cv2
import numpy as np
import matplotlib.pylab as plt
#--① 큐브 영상 읽어서 HSV로 변환
img = cv2.imread("../img/balloon.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#--② 색상별 영역 지정
blue1 = np.array([90, 50, 50])
blue2 = np.array([120, 255,255])
green1 = np.array([45, 50,50])
green2 = np.array([75, 255,255])
red1 = np.array([0, 50,50])
red2 = np.array([15, 255,255])
red3 = np.array([165, 50,50])
red4 = np.array([180, 255,255])
yellow1 = np.array([20, 50,50])
yellow2 = np.array([35, 255,255])
# --③ 색상에 따른 마스크 생성
mask_blue = cv2.inRange(hsv, blue1, blue2)
mask_green = cv2.inRange(hsv, green1, green2)
mask_red = cv2.inRange(hsv, red1, red2)
mask_red2 = cv2.inRange(hsv, red3, red4)
mask_yellow = cv2.inRange(hsv, yellow1, yellow2)
#--④ 색상별 마스크로 색상만 추출
res_blue = cv2.bitwise_and(img, img, mask=mask_blue)
res_green = cv2.bitwise_and(img, img, mask=mask_green)
res_red1 = cv2.bitwise_and(img, img, mask=mask_red)
res_red2 = cv2.bitwise_and(img, img, mask=mask_red2)
res_red = cv2.bitwise_or(res_red1, res_red2)
res_yellow = cv2.bitwise_and(img, img, mask=mask_yellow)
#--⑤ 결과 출력
imgs = {'original': img, 'blue':res_blue, 'green':res_green,
'red':res_red, 'yellow':res_yellow}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,3, i+1)
plt.title(k)
plt.imshow(v[:,:,::-1])
plt.xticks([]); plt.yticks([])
plt.show()
이와 같이 색상을 이용한 마스크를 이용하는 것이 크로마 키의 원리 입니다. 일기예보나 영화를 촬영할 때 초록색 또는 파란색 배경을 두고 찍어서 나중에 원하는 배경과 합성하는 것을 크로마 키잉이라고 하고 배경을 크로마 키(chroma key)라고 합니다.
img1 = cv2.imread('../img_1/man_chromakey.jpg')
img2 = cv2.imread('../img/sky.jpg')
#--② ROI 선택을 위한 좌표 계산
height1, width1 = img1.shape[:2]
height2, width2 = img2.shape[:2]
x = (width2 - width1)//2
y = height2 - height1
w = x + width1
h = y + height1
#--③ 크로마키 배경 영상에서 크로마키 영역을 10픽셀 정도로 지정
chromakey = img1[:10, :10, :]
offset = 20
#--④ 크로마키 영역과 영상 전체를 HSV로 변경
hsv_chroma = cv2.cvtColor(chromakey, cv2.COLOR_BGR2HSV)
hsv_img = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
#--⑤ 크로마키 영역의 H값에서 offset 만큼 여유를 두어서 범위 지정
# offset 값은 여러차례 시도 후 결정
#chroma_h = hsv_chroma[0]
chroma_h = hsv_chroma[:,:,0]
lower = np.array([chroma_h.min()-offset, 100, 100])
upper = np.array([chroma_h.max()+offset, 255, 255])
#--⑥ 마스크 생성 및 마스킹 후 합성
mask = cv2.inRange(hsv_img, lower, upper)
mask_inv = cv2.bitwise_not(mask)
roi = img2[y:h, x:w]
fg = cv2.bitwise_and(img1, img1, mask=mask_inv)
bg = cv2.bitwise_and(roi, roi, mask=mask)
img2[y:h, x:w] = fg + bg
#--⑦ 결과 출력
plt.figure(figsize=(6,5))
plt.subplot(121)
plt.xticks([]); plt.yticks([])
plt.imshow(img1[:,:,(2,1,0)])
plt.subplot(122)
plt.xticks([]); plt.yticks([])
plt.imshow(img2[:,:,(2,1,0)])
plt.show()
cv2.inRange() 함수를 사용해서 크로마 키가 있는 영역에서 색상 값 중에 가장 큰 값과 가장 작은 값을 범위로 지정해서 배경만 제거합니다. 이렇게 영상을 합성할 때에는 대부분 알파 블렌딩이나 마스킹이 필요한데, 블렌딩을 위한 적절한 알파 값 선택과 마스킹을 위한 모양의 좌표, 색상 값 선택에는 많은 노력과 시간이 필요합니다. 이에 OpenCV에서는 알아서 두 영사의 특징을 살려 합성하는 기능이 존재합니다.
- dst = cv2.seamlessClone(src, dst, mask, coords, flags[, output])
- src : 입력 영상, 일반적으로 전경
- dst : 대상 영상, 일반적으로 배경
- mask : 마스크, src에서 합성하고자 하는 영역은 255, 나머지는 0
- coodrs : src가 놓여지기 원하는 dst의 좌표(중앙)
- flags : 합성 방식
- cv2.NORMAL_CLONE : 입력 원본 유지
- cv2.MIXED_CLONE : 입력과 대상을 혼합
- output : 합성 결과
- dst : 합성 결과
import cv2
import numpy as np
import matplotlib.pylab as plt
#--① 합성 대상 영상 읽기
img1 = cv2.imread("../img_1/drawing.jpg")
img2= cv2.imread("../img_1/my_hand.jpg")
#--② 마스크 생성, 합성할 이미지 전체 영역을 255로 셋팅
mask = np.full_like(img1, 255)
#--③ 합성 대상 좌표 계산(img2의 중앙)
height, width = img2.shape[:2]
center = (width//2, height//2)
#--④ seamlessClone 으로 합성
normal = cv2.seamlessClone(img1, img2, mask, center, cv2.NORMAL_CLONE)
mixed = cv2.seamlessClone(img1, img2, mask, center, cv2.MIXED_CLONE)
#--⑤ 결과 출력
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.xticks([]); plt.yticks([])
plt.imshow(normal[:,:,(2,1,0)]) # 그림 주위가 원본의 그림이 아닌것을 확인할 수 있습니다.
plt.subplot(122)
plt.xticks([]); plt.yticks([])
plt.imshow(mixed[:,:,(2,1,0)])
plt.show()
4.5. 히스토그램¶
4.5.1. 히스토그램 계산과 표시¶
영상 분야에서 히스토그램은 전체 영상에서 픽셀 값이 1인 픽셀이 몇 개인지, 2인 픽셀이 몇 개 인지... 255인 픽셀이 몇 개인지까지 세는 것을 말합니다. 이렇게 하는 이유는 전체 영상에서 픽셀들의 색상이나 명암의 분포를 파악하기 위해서이고 OpenCV에서는 이를 위해 cv2.calcHist()함수를 제공합니다.
- cv2.calcHist(ing, channel, mask, histSize, ranges)
- img : 입력 영상, 리스트로 감싸서 표현
- channel : 처리할 채널, 리스트로 감싸서 표현
- mask : 마스크에 지정한 픽셀만 히스토그램 계산
- histSize : 계급(bin)의 개수, 채널 개수에 맞게 리스트로 표현
- ranges : 각 핅셀이 가질 수 있는 값의 범위 ( RGB의 경우 [0,256] )
#--① 이미지 그레이 스케일로 읽기 및 출력
plt.subplot(2,1,1)
img = cv2.imread('../img/sky.jpg', cv2.IMREAD_GRAYSCALE)
plt.imshow(img, cmap='gray')
#--② 히스토그램 계산 및 그리기
plt.subplot(2,1,2)
hist = cv2.calcHist([img], [0], None, [256], [0,255]) # grayscale이므로 채널이 1개이다.
plt.plot(hist)
print("hist.shape:", hist.shape) #--③ 히스토그램의 shape (256,1)
print("hist.sum():", hist.sum(), "img.shape:",img.shape) #--④ 히스토그램 총 합계와 이미지의 크기
plt.show()
hist.shape: (256, 1) hist.sum(): 273184.0 img.shape: (427, 640)
plt.subplot(2,1,1)
img = cv2.imread('../img/sky.jpg')
plt.imshow(img[:,:,(2,1,0)])
#--② 히스토그램 계산 및 그리기
plt.subplot(2,1,2)
channels = cv2.split(img)
colors = ('b', 'g', 'r')
for (ch, color) in zip (channels, colors):
hist = cv2.calcHist([ch], [0], None, [256], [0, 255])
plt.plot(hist, color = color)
plt.show()
파란 하늘이 넓은 영역을 차지하고 있기 때문에 파란색 분포가 높은 것을 확인할 수 있습니다.
4.5.2. 노멀라이즈¶
OpenCV에서는 픽셀 값들이 0~255에 골고루 분포하지 않고 특정 영역에 몰려 있는 경우 화질을 개선하고 영상 간의 연산에 어려움이 있기 때문에 서로 조건이 다른 경우 조건을 같게 만들기 위한 노멀라이즈(정규화) 기능을 제공합니다.
- dst = cv2.normalize(src, dst, alpha,b beta, type_flag)
- src : 노멀라이즈 이전 데이터
- dst : 노멀라이즈 이후 데이터
- alpha : 노멀라이즈 구간 1
- beta : 노멀라이즈 구간 2 ( 구간 노멀라이즈가 아닌 경우 사용 x )
- type_flag : 알고리즘 선택 플래그 상수
- cv2.NORM_MINMAX : alpha와 beta 구간으로 노멀라이즈
- cv2.NORM_L1 : 전체 합으로 나누기, alpha = 노멀라이즈 전체 합
- cv2.NORM_L2 : 단위 벡터로 노멀라이즈
- cv2.NORM_INF : 최대 값으로 나누기
# 뿌연 영상에 노멀라이즈를 적용해서 화질을 개선하는 예제
import cv2
import numpy as np
import matplotlib.pylab as plt
#--① 그레이 스케일로 영상 읽기
img = cv2.imread('../img/blur.jpg', cv2.IMREAD_GRAYSCALE)
#--② 직접 연산한 정규화
img_f = img.astype(np.float32)
img_norm = ((img_f - img_f.min()) * (255) / (img_f.max() - img_f.min())) # minmax 정규화
img_norm = img_norm.astype(np.uint8)
#--③ OpenCV API를 이용한 정규화
img_norm2 = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
#--④ 히스토그램 계산
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
hist_norm = cv2.calcHist([img_norm], [0], None, [256], [0, 255])
hist_norm2 = cv2.calcHist([img_norm2], [0], None, [256], [0, 255])
imgs = {'Before' : img, 'Manual':img_norm, 'cv2.normalize()':img_norm2}
plt.figure(figsize=(11,6))
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.xticks([])
plt.yticks([])
plt.imshow(v, cmap='gray')
plt.show()
plt.figure(figsize=(10,5))
hists = {'Before' : hist, 'Manual':hist_norm, 'cv2.normalize()':hist_norm2}
for i, (k, v) in enumerate(hists.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.plot(v)
plt.show()
위의 분포를 살펴보면 중앙에 몰려 있던 분포가 저체적으로 고르게 퍼져서 화질이 개선된 것을 확인할 수 있습니다.
구간 노멀라이즈가 아니라 서로 다른 히스토그램의 빈도를 같은 조건으로 비교하는 경우에는 전체의 비율로 노멀라이즈 해야하는데 이때는 아래의 함수를 사용합니다.
- norm = cv2.normalize(hist, None, 1, 0, cv2.NORM_L1)
4.5.3. 이퀄라이즈¶
노멀라이즈는 분포가 한곳에 집중되어 있을 때는 효과적이지만 집중된 영역에서 멀리 떨어진 값이 있을 경우에는 효과가 없습니다. 즉, 90~100점 사이의 점수를 받은 학생을 70~100인 새로운 점수 분포로 만들때에는 노멀라이즈가 효과가 있지만 70, 93, 96, 98과 같이 집중된 영역에서 먼 값이 존재할 경우 새롭게 70~100 분포로 만들어도 같은 결과가 나와 효과가 없다는 의미입니다. 이 때는 이퀄아이즈(equalize, 평탄화) 작업이 필요합니다. 이퀄라이즈는 히스토그램으로 빈도를 구해서 그것을 노멀라이즈한 후 누적값을 전체 개수로 나누어 나온 결과 값을 히스토그램 원래 픽셀 값에 매핑합니다.
- dst = cv2.equalizeHist(src, [, dst])
- src : 대상 이미지, 8비트 1채널
- dst : 결과 이미지
# 어둡게 나온 사진을 그레이 스케일로 바꾸어 이퀄라이즈를 적용해서 개선시키는 예제
import cv2
import numpy as np
import matplotlib.pylab as plt
#--① 대상 영상으로 그레이 스케일로 읽기
img = cv2.imread('../img/umbrella.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape[:2]
#--② 이퀄라이즈 연산을 직접 적용
hist = cv2.calcHist([img], [0], None, [256], [0, 256]) #히스토그램 계산
cdf = hist.cumsum() # 누적 히스토그램
cdf_m = np.ma.masked_equal(cdf, 0) # 0(zero)인 값을 NaN으로 제거
cdf_m = (cdf_m - cdf_m.min()) /(rows * cols) * 255 # 이퀄라이즈 히스토그램 계산
cdf = np.ma.filled(cdf_m,0).astype('uint8') # NaN을 다시 0으로 환원
print(cdf.shape)
img2 = cdf[img] # 히스토그램을 픽셀로 맵핑
#--③ OpenCV API로 이퀄라이즈 히스토그램 적용
img3 = cv2.equalizeHist(img)
#--④ 이퀄라이즈 결과 히스토그램 계산
hist2 = cv2.calcHist([img2], [0], None, [256], [0, 256])
hist3 = cv2.calcHist([img3], [0], None, [256], [0, 256])
#--⑤ 결과 출력
imgs = {'Before':img, 'Manual':img2, 'cv2.equalizeHist()':img3}
plt.figure(figsize=(10,5))
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.xticks([])
plt.yticks([])
plt.imshow(v, cmap='gray')
plt.show()
plt.figure(figsize=(10,5))
hists = {'Before':hist, 'Manual':hist2, 'cv2.equalizeHist()':hist3}
for i, (k, v) in enumerate(hists.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.plot(v)
plt.show()
(256,)
img = cv2.imread('../img/umbrella.jpg') #이미지 읽기, BGR 스케일
#--① 컬러 스케일을 BGR에서 YUV로 변경
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
#--② YUV 컬러 스케일의 첫번째 채널에 대해서 이퀄라이즈 적용
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])
#--③ 컬러 스케일을 YUV에서 BGR로 변경
img2 = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
imgs = {'Before':img, 'After':img2 }
plt.figure(figsize=(10,5))
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.xticks([])
plt.yticks([])
plt.imshow(v[:,:,(2,1,0)])
plt.show()
# 어두운 부분의 밝기가 개선된 것을 확인할 수 있습니다.
4.5.4. CLAHE¶
CLAHE(Contrast Limiting Adaptive Histogram Equalization)는 영상 전체에 이퀄라이즈를 적용했을 때 너무 밝은 부분이 날아가는 현상을 막기 위해 영상을 일정한 영역으로 나눠서 이퀄라이즈를 적용하는 것을 말합니다. 이 때 노이즈가 증폭되는 것을 막기 위해 어느 히스토그램 계급이든 지정한 제한 값을 넘으면 해당 픽셀을 다른 계급으로 배분하고 나서 이퀄라이즈를 적용합니다.
- clahe = cv2.createCLAHE(clipLimit, tileGridSize) : CLAHE 생성
- clipLimit : Contrast 제한 경계 값, 기본 40
- tileGridSize : 영역 크기, 기본 8x8
- clahe : 생성된 CLAHE 객체
- clahe.apply(src) : CLAHE 적용
- src : 입력 영상
import cv2
import numpy as np
import matplotlib.pylab as plt
#--①이미지 읽어서 YUV 컬러스페이스로 변경
img = cv2.imread('../img/umbrella.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
#--② 밝기 채널에 대해서 이퀄라이즈 적용
img_eq = img_yuv.copy()
img_eq[:,:,0] = cv2.equalizeHist(img_eq[:,:,0])
img_eq = cv2.cvtColor(img_eq, cv2.COLOR_YUV2BGR)
#--③ 밝기 채널에 대해서 CLAHE 적용
img_clahe = img_yuv.copy()
clahe = cv2.createCLAHE(clipLimit= 2, tileGridSize=(8,8)) #CLAHE 생성 ( clipLimit값을 적절히 변환 )
img_clahe[:,:,0] = clahe.apply(img_clahe[:,:,0]) #CLAHE 적용
img_clahe = cv2.cvtColor(img_clahe, cv2.COLOR_YUV2BGR)
#--④ 결과 출력
imgs = {'Before':img, 'CLAHE':img_clahe, 'equalizeHist': img_eq }
plt.figure(figsize=(10,5))
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.xticks([])
plt.yticks([])
plt.imshow(v[:,:,(2,1,0)])
plt.show()
CLAHE는 Image Classification, Object Detection, Semantic Segmentation에서 Data augmentation에 사용되기도 합니다.
4.5.5. 2D 히스토그램¶
각 픽셀이 몇 개씩인지 세어서 그래프로 표현을 해주는 1차원 히스토그램과 비슷하게, 2차원 히스토그램은 같은 축이 2개이고 각각의 축이 만나는 지점의 개수를 표현합니다.
import cv2
import matplotlib.pylab as plt
plt.style.use('classic') # --①컬러 스타일을 1.x 스타일로 사용
img = cv2.imread('../img/sky.jpg')
plt.subplot(131)
hist = cv2.calcHist([img], [0,1], None, [32,32], [0,256,0,256]) #--②
p = plt.imshow(hist) #--③
plt.title('Blue and Green') #--④
plt.colorbar(p) #--⑤ 컬러바 표시
plt.subplot(132)
hist = cv2.calcHist([img], [1,2], None, [32,32], [0,256,0,256]) #--⑥
p = plt.imshow(hist)
plt.title('Green and Red')
plt.colorbar(p)
plt.subplot(133)
hist = cv2.calcHist([img], [0,2], None, [32,32], [0,256,0,256]) #--⑦
p = plt.imshow(hist)
plt.title('Blue and Red')
plt.colorbar(p)
plt.show()
# 원본 그림
plt.imshow(img[:,:,(2,1,0)])
plt.xticks([])
plt.yticks([])
plt.show()
계급 수를 32개로 잡았고 빨간색으로 표시될 수록 픽셀의 개수가 많고 파란색은 픽셀이 적은 것을 나타냅니다.
4.5.6. 역투영¶
2차원 히스토그램과 HSV 컬러 스페이스를 이용하면 색상으로 특정 물체나 사물의 일부분을 배경에서 분리할 수 있습니다. 이 방법의 원리는 물체가 있는 관심영역의 H와 V 값의 분포를 얻어낸 후 전체 영상에서 해당 분포의 픽셀만 찾아내는 것입니다.
# 마우스로 선택한 영역의 물체 배경 제거
win_name = 'back_projection'
img = cv2.imread('../img_1/pump_horse.jpg')
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
draw = img.copy()
#--⑤ 역투영된 결과를 마스킹해서 결과를 출력하는 공통함수
def masking(bp, win_name):
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
cv2.filter2D(bp,-1,disc,bp)
_, mask = cv2.threshold(bp, 1, 255, cv2.THRESH_BINARY)
result = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow(win_name, result)
#--⑥ 직접 구현한 역투영 함수
def backProject_manual(hist_roi):
#--⑦ 전체 영상에 대한 H,S 히스토그램 계산
hist_img = cv2.calcHist([hsv_img], [0,1], None,[180,256], [0,180,0,256])
#--⑧ 선택영역과 전체 영상에 대한 히스토그램 그램 비율계산 ( 분모가 0이 되지 않기위해 1을 더합니다 )
hist_rate = hist_roi/ (hist_img + 1) # 관심영역과 비슷한 색상일수록 1에 가깝습니다.
#--⑨ 비율에 맞는 픽셀 값 매핑
h,s,v = cv2.split(hsv_img)
# 핵심적인 코드( H와 S가 교차되는 지점의 비율을 픽셀의 값으로 하는 1차원 배열을 얻게됩니다.)
bp = hist_rate[h.ravel(), s.ravel()]
bp = np.minimum(bp, 1)
bp = bp.reshape(hsv_img.shape[:2])
cv2.normalize(bp,bp, 0, 255, cv2.NORM_MINMAX)
bp = bp.astype(np.uint8)
#--⑩ 역 투영 결과로 마스킹해서 결과 출력
masking(bp,'result_manual')
# OpenCV API로 구현한 함수 ---⑪
def backProject_cv(hist_roi):
# 역투영 함수 호출 ---⑫
bp = cv2.calcBackProject([hsv_img], [0, 1], hist_roi, [0, 180, 0, 256], 1)
# 역 투영 결과로 마스킹해서 결과 출력 ---⑬
masking(bp,'result_cv')
# ROI 선택 ---①
(x,y,w,h) = cv2.selectROI(win_name, img, False)
if w > 0 and h > 0:
roi = draw[y:y+h, x:x+w]
cv2.rectangle(draw, (x, y), (x+w, y+h), (0,0,255), 2)
#--② 선택한 ROI를 HSV 컬러 스페이스로 변경
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
#--③ H,S 채널에 대한 히스토그램 계산
hist_roi = cv2.calcHist([hsv_roi],[0, 1], None, [180, 256], [0, 180, 0, 256] )
#--④ ROI의 히스토그램을 매뉴얼 구현함수와 OpenCV 이용하는 함수에 각각 전달
backProject_manual(hist_roi)
backProject_cv(hist_roi)
cv2.imshow(win_name, draw)
cv2.waitKey()
cv2.destroyAllWindows()
# ⑨에서 1차원 배열을 얻는 과정을 단순화
v = np.arange(6).reshape(2,3)
print('v :', v)
row = np.array([1,1,1,0,0,0])
col = np.array([0,1,2,0,1,2])
print('v[row,col] : ',v[row,col])
v : [[0 1 2] [3 4 5]] v[row,col] : [3 4 5 0 1 2]
원본 영상의 중앙에 마우스로 영역을 선택하고 OpenCV함수와 직접 구현한 함수의 역투영(back projection) 결과를 보여주는 코드입니다. 역투영의 장점은 알파 채널이나 크로마 키 같은 보조 역할이 없이도 복잡한 모양의 사물을 분리할 수 있다는 것입니다. 하지만 대상 사물의 색상과 비슷한 색상이 뒤섞여 있을 때는 효과가 떨어진다는 단점도 존재합니다. 역투영 방법을 제공하는 OpenCV 함수는 다음과 같습니다.
- cv2.calcBackProject(img, channel, hist, ranges, scale)
- img : 입력 영상
- channel : 처리할 채널
- hist : 역투영에 사용할 히스토그램
- ranges : 각 픽셀이 가질 수 있는 값의 범위
- scale : 결과에 적용할 배율 계수
4.5.7. 히스토그램 비교¶
히스토그래믄 영상의 픽셀 값의 분포를 갖는 정보이므로 이를 비교하면 영상에서 사용한 픽셀의 색상 비중이 얼마나 비슷한지 알 수 있습니다.
- cv2.compareHist(hist1, hist2, method)
- hist1, hist2 : 비교할 2개의 히스토그램, 크기와 차원이 같아야 함
- method : 비교 알고리즘 선택 플래그 상수
- cv2.HISTCMP_CORREL : 상관관계 ( 1 : 완전 일치, -1 : 최대 불일치, 0 : 무관계 )
- cv2.HISTCMP_CHISQR : 카이제곱 ( 0 : 완전 일치, 큰 값 : 최대 불일치 )
- cv2.HISTCMP_INTERSECT : 교차 ( 1 : 완전 일치, 0 : 최대 불일치( 1로 정규화한 경우 ))
- cv2.HISTCMP_BHATTACHARYYA : 바타차야 ( 0 : 완전 일치, 1: 최대 불일치)
- cv2.HISTCMP_HELLINGER : 바타차야 방법과 동일
이 함수는 첫 번째와 두 번째 인자에 비교하고자 하는 히스토그램을 전달하고 마지막 인자에 어떤 플래그 상수를 전달하느냐에 따라 반환 값의 의미가 달라집니다. CORREL은 상관 관계를 기반으로 피어슨 상관계수로 유사성을 측정하고, CHISQR는 피어슨 상관계수 대신 카이제곱으로 유사성을 측정합니다. INTERSECT는 두 히스토그램의 교차점의 작은 값을 선택해서 그 합을 반환하고 BHATTACHARYYA는 두 분포의 중첩되는 부분을 측정합니다.
서로 다른 영상의 히스토그램을 같은 조건으로 비교하기 위해서는 먼저 히스토그램을 노멀라이즈 해주어야 합니다.
import cv2
import numpy as np
import matplotlib.pylab as plt
img1 = cv2.imread('../img/cat.jpg')
img2 = cv2.imread('../img/cat.jpg')
img3 = cv2.imread('../img/dog.jpg')
img4 = cv2.imread('../img/tiger.jpg')
imgs = [img1, img2, img3, img4]
hists = []
for i, img in enumerate(imgs) :
plt.subplot(1,len(imgs),i+1)
plt.title('img%d'% (i+1))
plt.axis('off')
plt.imshow(img[:,:,::-1])
#---① 각 이미지를 HSV로 변환
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#---② H,S 채널에 대한 히스토그램 계산
hist = cv2.calcHist([hsv], [0,1], None, [180,256], [0,180,0, 256])
#---③ 0~1로 정규화
cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)
hists.append(hist)
query = hists[0]
methods = {'CORREL' :cv2.HISTCMP_CORREL, 'CHISQR':cv2.HISTCMP_CHISQR,
'INTERSECT':cv2.HISTCMP_INTERSECT,
'BHATTACHARYYA':cv2.HISTCMP_BHATTACHARYYA}
for j, (name, flag) in enumerate(methods.items()):
print('%-10s'%name, end='\t')
for i, (hist, img) in enumerate(zip(hists, imgs)):
#---④ 각 메서드에 따라 img1과 각 이미지의 히스토그램 비교
ret = cv2.compareHist(query, hist, flag)
if flag == cv2.HISTCMP_INTERSECT: #교차 분석인 경우
ret = ret/np.sum(query) #비교대상으로 나누어 1로 정규화
print("img%d:%7.2f"% (i+1 , ret), end='\t')
print()
plt.show()
# img1은 완전일치, 나머지는 가장 멀어진 값으로 나타나는 것을 확인할 수 있습니다.
CORREL img1: 1.00 img2: 1.00 img3: 0.14 img4: 0.23 CHISQR img1: 0.00 img2: 0.00 img3: 405.21 img4:3859.73 INTERSECT img1: 1.00 img2: 1.00 img3: 0.06 img4: 0.31 BHATTACHARYYA img1: 0.00 img2: 0.00 img3: 0.75 img4: 0.59
4.6. 실전 워크숍¶
4.6.1. 반해골 괴물 얼굴 합성¶
두 영상의 절반씩을 단순 합성하면 두 영상을 이어붙인것과 같이 어색합니다. 따라서 두 영상이 만나는 지점의 일정 부분을 알파 값이 서서히 변하게 알파 블렌딩하면 자연스럽게 두 개의 얼굴이 하나의 얼굴로 합성됩니다. 알파 값은 블렌딩의 시작 지점은 1:0, 중간 지점은 0.5:0.5, 끝 지점은 0:1이 되게 합니다.
import cv2
import numpy as np
# 영상의 15%를 알파 블렌딩의 범위로 지정
alpha_width_rate = 15
# 합성할 두 영상 읽기
img_face = cv2.imread('../img_1/man_face.jpg')
img_skull = cv2.imread('../img_1/skull.jpg')
# 입력 영상과 같은 크기의 결과 영상 준비
img_comp = np.zeros_like(img_face)
# 연산에 필요한 좌표 계산
height, width = img_face.shape[:2]
middle = width//2 # 영상의 중앙 좌표
alpha_width = width * alpha_width_rate // 100 # 알파 블렌딩 범위
start = middle - alpha_width//2 # 알파 블렌딩 시작 지점
step = 100/alpha_width # 알파 값 간격
# 입력 영상의 절반씩 복사해서 결과 영상에 합성
img_comp[:, :middle, : ] = img_face[:, :middle, :].copy()
img_comp[:, middle:, :] = img_skull[:, middle:, :].copy()
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.title('half')
plt.xticks([])
plt.yticks([])
plt.imshow(img_comp[:,:,(2,1,0)])
# 알파 값을 바꾸면서 알파 블렌딩 적용
for i in range(alpha_width+1 ):
alpha = (100 - step * i) / 100 # 증감 간격에 따른 알파 값 (1~0)
beta = 1 - alpha # 베타 값 (0~1)
# 알파 블렌딩 적용
img_comp[:, start+i] = img_face[:, start+i] * \
alpha + img_skull[:, start+i] * beta
plt.subplot(122)
plt.title('half skull')
plt.xticks([])
plt.yticks([])
plt.imshow(img_comp[:,:,(2,1,0)])
plt.show()
4.6.2. 모션 감지 CCTV¶
카메라를 고정하더라도 책상이나 건물이 미세하게 움직이기 때문에 단순한 영상간의 차이를 구해서는 움직임을 감지할 수 없습니다. 따라서 세 프레임 a, b, c를 순차적으로 얻어서 a와 b의 차이, b와 c의 차이가 모두 발견되는 경우에 한해서 움직임이 있는 것으로 판단할 것입니다. 이 때 각 프레임의 차이가 특정 기준치보다 작은 픽셀은 무시하고 차이가 없는 것으로 간주하면 최종적으로 차이가 있는 것으로 판단한 픽셀의 개수가 특정 기준치보다 많은 경우에 움직임이 있는 것으로 판단합니다.
import cv2
import numpy as np
# 감도 설정(카메라 품질에 따라 조정 필요)
thresh = 25 # 달라진 픽셀 값 기준치 설정
max_diff = 5 # 달라진 픽셀 갯수 기준치 설정
# 카메라 캡션 장치 준비
a, b, c = None, None, None
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 480) # 프레임 폭을 480으로 설정
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 320) # 프레임 높이를 320으로 설정
if cap.isOpened():
ret, a = cap.read() # a 프레임 읽기
ret, b = cap.read() # b 프레임 읽기
while ret:
ret, c = cap.read() # c 프레임 읽기
draw = c.copy() # 출력 영상에 사용할 복제본
if not ret:
break
# 3개의 영상을 그레이 스케일로 변경
a_gray = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY)
b_gray = cv2.cvtColor(b, cv2.COLOR_BGR2GRAY)
c_gray = cv2.cvtColor(c, cv2.COLOR_BGR2GRAY)
# a-b, b-c 절대 값 차 구하기
diff1 = cv2.absdiff(a_gray, b_gray)
diff2 = cv2.absdiff(b_gray, c_gray)
# 스레시홀드로 기준치 이내의 차이는 무시
ret, diff1_t = cv2.threshold(diff1, thresh, 255, cv2.THRESH_BINARY)
ret, diff2_t = cv2.threshold(diff2, thresh, 255, cv2.THRESH_BINARY)
# 두 차이에 대해서 AND 연산, 두 영상의 차이가 모두 발견된 경우
diff = cv2.bitwise_and(diff1_t, diff2_t)
# 열림 연산으로 노이즈 제거 ---① 6장 영상필터에서 자세히 다룰 것입니다.
k = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3))
diff = cv2.morphologyEx(diff, cv2.MORPH_OPEN, k)
# 차이가 발생한 픽셀이 갯수 판단 후 사각형 그리기
diff_cnt = cv2.countNonZero(diff)
if diff_cnt > max_diff:
nzero = np.nonzero(diff) # 0이 아닌 픽셀의 좌표 얻기(y[...], x[...])
cv2.rectangle(draw, (min(nzero[1]), min(nzero[0])), \
(max(nzero[1]), max(nzero[0])), (0,255,0), 2)
cv2.putText(draw, "Motion Detected", (10,30), \
cv2.FONT_HERSHEY_DUPLEX, 0.5, (0,0,255))
# 컬러 스케일 영상과 스레시홀드 영상을 통합해서 출력
stacked = np.hstack((draw, cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)))
cv2.imshow('motion sensor',stacked )
# 다음 비교를 위해 영상 순서 정리
a = b
b = c
if cv2.waitKey(1) & 0xFF == 27:
cv2.destroyAllWindows()
break
감사합니다!¶
- 지금까지 OpenCV 함수를 사용해서 이미지에 변화를 주는 방법을 알아보았습니다.
- 이를 활용하여 Data Augmentation 등의 작업을 할 수 있을 것 같습니다!
감사합니다 :)
'대외활동 > DACrew 2기' 카테고리의 다른 글
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 5장. 기하학적 변환 (0) | 2022.05.03 |
---|---|
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 2장. 기본 입출력 (0) | 2022.04.22 |
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 3장. Numpy와 Matplotlib (0) | 2022.04.14 |
댓글