이미지 프로세싱 기초
이미지 프로세싱의 기본이 되는 내용을 알아보자.
관심영역
이미지에 어떤 연산을 적용해서 새로운 이미지나 정보를 얻어내려고 할대 전체 이미지를 대상으로 연산을 하는 것 보다는 관심이 있는 부분만 잘라내서 하는 것이 효과적이다.
관심 있는 영역만 잘라내서 연산을 하면 단순히 연산할 데이터의 양을 줄이고 수행시간을 단축시키는 이점도 있지만, 데이터의 양이 줄어들면 그 형태도 단순해지므로 적용해야 하는 알고리즘도 단순해지는 이점이 있다.
또한 좌표를 구하기가 쉽다는 이점도 있다.
관심영역 지정
전체 이미지에서 연산과 분석의 대상이 되는 영역만을 지정하고 떼어내는 것을 관심역역(Region Of Interest, ROI)을 지정한다고 한다.
전체 이미지가 img라는 변수에 있을 때, 관심 있는 영역의 좌표가 x,y이고 영역의 폭이 w, 높이가 h라고 하면 이것을 이용하여 관심 역격을 지정하는 코드는 다음과 같다.
roi = img[y:y+h, x:x+w]
이 코드는 img의 y행에서 y+h행까지, x열에서 x+w열까지를 슬라이싱 한것이다.
관심영역 지정
import cv2
import numpy as np
img = cv2.imread('./img/sunset.jpg')
x=320; y=150; w=50; h=50
roi = img[y:y+h, x:x+w]
img2 = roi.copy()
img[y:y+h, x+w:x+w+w] = roi
cv2.rectangle(img, (x,y), (x+w+w, y+h), (0,255,0))
cv2.imshow('img', img)
cv2.imshow('roi', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()
마우스로 관심영역 지정
마우스 이벤트 처리를 이용하여 관심역역을 지정할 수 있다.
import cv2
import numpy as np
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]
cv2.imshow('cropped',roi)
cv2.moveWindow('cropped', 0, 0)
cv2.imwrite('./cropped.jpg',roi)
print('cropped')
else:
cv2.imshow('img', img)
img = cv2.imread('./img/sunset.jpg')
cv2.imshow('img', img)
cv2.setMouseCallback('img', onMouse)
cv2.waitKey()
cv2.destroyAllWindows()
x:168, y49, w208, h161
cropped
OpenCV는 관심영역을 지정하기 위한 새로운 함수를 제공한다.
ret = cv2.selectROI([win_name,] img[, showCrossHair=True, fromCenter=False])
- win_name : ROI 선택을 진행할 창의 이름, str
- img : ROI 선택을 진행할 이미지, numpy ndarray
- showCrossHair : 선택 영역 중심에 십자 모양 표시 여부
- fromCenter : 마우스 시작 지점을 영역의 중심으로 지정
- ret : 선택한 영역 좌표와 크기(x, y, w, h), 선택을 취소한 경우 모두 0
selectROI를 이용하여 관심영역 지정
import cv2, numpy as np
img = cv2.imread('./img/sunset.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)
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
컬러 스페이스
영상의 색상과 명암을 표현하는 방법과 차이에 대해 알아보자.
BGR, BGRA, Alpha 채널
import cv2
import numpy as np
img = cv2.imread('./img/opencv_logo.png') # 기본값 옵션
bgr = cv2.imread('./img/opencv_logo.png', cv2.IMREAD_COLOR) # IMREAD_COLOR 옵션
bgra = cv2.imread('./img/opencv_logo.png', cv2.IMREAD_UNCHANGED) # IMREAD_UNCHANGED 옵션
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)
컬러 스페이스 변환
컬러 이미지를 그레이스케일로 변환하는 것은 이미지 연산의 양을 줄여 속도를 높일 수 있다.
맨처음부터 그레이 스케일로 이미지를 불러올 수 있지만, 상황에 따라 컬러 스케일로 읽고 필요에 따라 다른 컬러 스페이스로 변환해야 할때도 많다. 다음은 그레이 스케일로 변환하는 방법이다.
import cv2
import numpy as np
img = cv2.imread('./img/girl.jpg')
img2 = img.astype(np.uint16) # dtype 변경
b,g,r = cv2.split(img2) # 채널별로 분리
gray1 = ((b+g+r)/3).astype(np.uint8) # 평균값 연산 후 dtype 변경
gray2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # BGR을 그레이스케일로 변경
cv2.imshow('original', img)
cv2.imshow('gray1', gray1)
cv2.imshow('gray2', gray2)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.cvtColor(img, flag)
- img : Numpy 배열, 변환할 이미지
- flag : 변환할 컬러 스페이스, cv2.COLOR_ 로 시작하는 함수(274개)
- cv2.COLOR_BGR2GRAY : BGR 컬러 이미지를 그레이 스케일로 변환
- cv2.COLOR_GRAY@BGR : 그레이 스케일 이미지를 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 컬러 이미지로 변환
- [i for i in dir(cv2) if i startswith(‘COLOR_’) 로 함수 종류 확인
- out : 변환한 결과 이미지 (numpy 배열)
HSV, HSI, HSL
RGV 와 마찬가지로 3개 채널로 컬러 이미지를 표시. H(Hue, 색조), S(Saturation, 채도), V(Value, 명도) I(Intensity, 밀도) L(Lughtness, 명도)
H
- 빨강 : 165~180, 0~15
- 초록 : 45~75
- 파랑 : 90~120
S
- 0~255 로 표현하며 255일수록 순수한색이다
V
- 0~255 로 표현하며 255일수록 밝아진다.
BGR에서 HSV로 변환
import cv2
import numpy as np
# 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]]]
YUV, YCbCR
YUV 포맷은 사람이 색상을 인식할 때 밝기에 더 민감하고 색상은 상대적으로 둔감한 점을 고려해서 만든 컬러스페이스이다. Y(Luma, 밝기), U(Chroma Blue, Cb, 밝기와 파란색과의 색상 차), V(Chroma Red, Cr, 밝기와 빨간색과의 색상차)
BGR에서 YUV로
import cv2
import numpy as np
# BGR 컬러 스페이스로 세 가지 밝기의 픽셀 생성
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]]]
스레시 홀딩
이미지를 검은색과 흰색만으로 표현한 것을 binary 이미지라고 한다. thresholding 이란 여러 값을 경계점을 기준으로 두 부류로 나누는 것으로 바이너리 이미지를 만드는 대표적인 방법이다.
전역 스레시 홀딩
바이너리 이미지를 만들기 위해서는 컬러 이미지를 그레이 스케일로 바꿔야 한다. Numpy 연산 뿐만아니라 OpenCV 함수로 바이너리 이미지를 만들 수 있다.
ret, out = cv2.threshold(img, threshold, value, type_flag)
- img : Numpy 배열, 변환할 이미지
- threshold : 경계값
- value : 경계값 기준에 만족하는 픽셀에 적용할 값
- type_flag : 스레시홀드 적용방법 지정
- cv2.THRESH_BINARY : px > threshold ? value : 0 , 픽셀값이 경계값을 넘으면 value를 지정하고, 넘지 못하면 0을 지정
- cv2.THRESH_BINARY_INV : px > threshold ? 0 : value, cv2.THRESH_BINARY의 반대
- cv2.THRESH_TRUNC : px > threshold ? value : px , 픽셀값이 경계값을 넘으면 value를 지정하고, 넘지 못하면 그대로
- cv2.THRESH_TOZERO : px > threshold ? px : 0, 픽셀값이 경계값을 넘으면 그대로, 넘지 못하면 0을 지정
- cv2.THRESH_TOZERO_INV : px > threshold ? 0 : px , cv2.THRESH_TOZERO의 반대
- ret : 스레시홀딩에 사용한 경계 값
- out : 결과 바이너리 이미지
바이너리 이미지 만들기
import cv2
import numpy as np
import matplotlib.pylab as plt
# 이미지를 그레이스케일로 읽기
img = cv2.imread('./img/gray_gradient.jpg', cv2.IMREAD_GRAYSCALE)
thresh_np = np.zeros_like(img) # 원본과 동일한 크기로 0으로 채워진 이미지 생성
thresh_np[img > 127]=255 # 127보다 큰 값만 255로 변경
# opencv함수로 바이너리 이미지 만들기
ret, thresh_cv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
print(ret) # 바이너리 이미지에 사용된 경계값 반환
# 원본과 결과물 출력
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
스레시홀딩 플래그 실습
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/gray_gradient.jpg', cv2.IMREAD_GRAYSCALE)
ret, t_bin = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
ret, t_bininv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
ret, t_truc = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
ret, t_2zr = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
ret, 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()
오츠의 알고리즘
한번에 경계값을 찾을수 있는 알고리즘 함수. 모든 경우의 수에 대해 경계값을 조사해야해서 속도가 빠르지 못하다.
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/scaned_paper.jpg', cv2.IMREAD_GRAYSCALE)
ret, t_130 = cv2.threshold(img, 130, 255, cv2.THRESH_BINARY)
t, t_otsu = cv2.threshold(img, -1, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print('otsu threshold:', t)
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: 131.0
적응형 스레시 홀드
이미지를 여러 영역으로 나눈 다음 그 주변 픽셀 값만 가지고 계산해서 경계값을 구한다.
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 : 스레시홀드 적용 방법 지정(cv2.threshold() 함수와 동일)
- block_size : 영역으로 나눌 이웃의 크기(n x n), ghftn(3, 5, 7,…)
- C : 계산된 경계 값 결과에서 가감할 상수(음수 가능)
import cv2
import numpy as np
import matplotlib.pyplot as plt
blk_size = 9
C = 5
img = cv2.imread('./img/sudoku.png', 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)
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()
이미지 연산
영상과 영상의 연산
dest = cv2.add(src1, src2[ , dest, mask, dtype] ) : src1 과 src2 더하기
- src1 : 입력 영상 1 또는 수
- src2 : 입력 영상 2 또는 수
- dest : 출력 영상
- mask : 0이 아닌 픽셀만 연산
- dtype : 출력 dtype
dest = cv2.subtract(src1, src2[ , dest, mask, dtype] ) : src1 과 src2 빼기
dest = cv2.multiply(src1, src2[ , dest, mask, dtype] ) : src1 과 src2 곱하기
dest = cv2.divide(src1, src2[ , dest, mask, dtype] ) : src1 과 src2 나누기
영상의 사칙 연산
import cv2
import numpy as np
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]]
mask와 누적 할당
import cv2
import numpy as np
# 연산에 사용할 배열
a = np.array([[1, 2]], dtype=np.uint8)
b = np.array([[10, 20]], dtype=np.uint8)
# 두번째 요소가 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)
print(b)
print()
# 배열을 그대로 유지하고싶으면
c = np.array([[1, 2]], dtype=np.uint8)
d = np.array([[10, 20]], dtype=np.uint8)
c3 = cv2.add(c,d,d.copy(), mask)
print(c3)
print(d)
[[11 0]]
[[11 20]]
[[11 20]]
[[11 20]]
[[10 20]]
알파 블렌딩
아래 예시처럼 더하기 연산으로 원하는 결과를 얻지 못할 수 있다.
import cv2
import numpy as np
import matplotlib.pylab as plt
# 연산에 사용할 이미지 읽기
img1 = cv2.imread('./img/wing_wall.jpg')
img2 = cv2.imread('./img/yate.jpg')
# 이미지 더하기
img3 = img1 + img2
img4 = cv2.add(img1, img2)
imgs = {'img1':img1, 'img2':img2,
'img1+img2':img3, 'cv2.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()
cv2.addWeight(img1, alpha, img2, beta, gamma)
- img1, img2 : 합성할 두 영상
- alpha : img1 에 지정할 가중치(알파 값)
- beta : img2에 지정할 가중치, (1-alpha) 로 적용
- gamma : 연산 결과에 가감할 상수, 보통 0 적용
50% 알파 블렌딩
# 합성에 사용할 알파값
alpha = 0.5
img1 = cv2.imread('./img/wing_wall.jpg')
img2 = cv2.imread('./img/yate.jpg')
# 수식을 직접 연산해서 적용
blended = img1 * alpha + img2 * (1-alpha)
blended = blended.astype(np.uint8)
cv2.imshow('img1 * alpha + img2 * (1-alpha)', blended)
# addWeighted() 함수 적용
dst = cv2.addWeighted(img1, alpha, img2, (1-alpha), 0)
cv2.imshow('cv2.addWeighted', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
트랙바로 알파 변경할 수 있게 윈도 만들기
import cv2
import numpy as np
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/man_face.jpg')
img2 = cv2.imread('./img/lion_face.jpg')
cv2.imshow(win_name, img1)
cv2.createTrackbar(trackbar_name, win_name, 0, 100, onChange)
cv2.waitKey()
cv2.destroyAllWindows()
비트와이즈 연산
비트와이즈 연산은 두 영상을 합성할 때 특정 영역만 선택하거나 특정 영역만 제외하는 등의 선별적인 연산에 도움이 된다. 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, img2, mask=None) : 각 픽셀에 대해 비트와이즈 NOT 연산
- img1, img2 : 연산 대상 영상, 동일한 shape
- mask: 0이 아닌 픽셀만 연산, 바이너리 이미지
import numpy as np, cv2
import matplotlib.pylab as plt
img1 = np.zeros( (200, 400), dtype=np.uint8)
img2 = np.zeros( (200, 400), dtype=np.uint8)
img1[:, :200]=255
img2[100:200,:] =255
# 비트와이즈 연산
bitAnd = cv2.bitwise_and(img1, img2)
bitOr = cv2.bitwise_or(img1, img2)
bitXor = cv2.bitwise_xor(img1, img2)
bitNot = cv2.bitwise_not(img1, img2)
imgs = {'img1':img1, 'img2':img2, 'and':bitAnd,
'or':bitOr, 'xor':bitXor, 'not(img1)':bitNot}
# plot 으로 결과 출력
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()
bitwise_and 연산으로 마스킹하기
import numpy as np, cv2
import matplotlib.pylab as plt
img = cv2.imread('./img/girl.jpg')
# mask 만들기
mask = np.zeros_like(img)
# cv2.circle(대상 이미지, (원점x, 원점y), 반지름, (색상), 채우기)
cv2.circle(mask, (150, 140), 100, (255,255,255), -1)
# 마스킹
masked = cv2.bitwise_and(img,mask)
# 결과 출력
cv2.imshow('original', img)
cv2.imshow('mask', mask)
cv2.imshow('masked', masked)
cv2.waitKey()
cv2.destroyAllWindows()
차영상
영상에서 영상을 빼면 두 영상의 차이, 변화를 알 수 있다.
diff = cv2.absdiff(img1, img2)
- img1, img2 : 입력 영상
- diff : 두 영상의 차의 절대 값 반환
import numpy as np, cv2
# 필요한 이미지 읽고 그레이 스케일로 변환
img1 = cv2.imread('./img/robot_arm1.jpg')
img2 = cv2.imread('./img/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)
# 차 영상을 극대화하기 위해 스레시홀드 처리 및 컬러로 변환
ret, 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)
cv2.imshow('img1', img1)
cv2.imshow('img2', img2)
cv2.imshow('diff', diff)
cv2.imshow('spot', spot)
cv2.waitKey()
cv2.destroyAllWindows()
이미지 합성과 마스킹
import numpy as np, cv2
# 합성에 사용할 영상 읽기
img_fg = cv2.imread('./img/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
cv2.imshow('mask', mask)
cv2.imshow('mask_inv', mask_inv)
cv2.imshow('masked_fg', masked_fg)
cv2.imshow('masked_bg', masked_bg)
cv2.imshow('added', added)
cv2.imshow('result', img_bg)
cv2.waitKey()
cv2.destroyAllWindows()
색상의 범위로 마스크 생성
dst = cv2.inRange(img, from, to) : 범위에 속하지 않은 픽셀 판단
- img = 입력 영상
- from : 범위의 시작 배열
- to : 범위의 끝 배열
- dsr : img가 from ~ to 에 포함되면 255, 아니면 0을 픽셀 값으로 하는 배열
import cv2
import numpy as np
import matplotlib.pylab as plt
# 큐브 이미지를 읽어서 HSV 로 변환
img = cv2.imread('./img/cube.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()
import cv2
import numpy as np
import matplotlib.pylab as plt
# 크로마키 영상과 합성할 영상 읽기
img1 = cv2.imread('./img/man_chromakey.jpg')
img2 = cv2.imread('./img/street.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]
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
cv2.imshow('chromakey', img1)
cv2.imshow('added', img2)
cv2.waitKey()
cv2.destroyAllWindows()
- 제공하는 함수 사용
dst = cv2.seamlessClone(src, dst, mask, coords, flags [ , output ] )
- src : 입력 영상, 일반적으로 전경
- dst : 대상 영상, 일반적으로 배경
- mask : 마스트, src에서 합성하고자 하는 영역은 255, 나머지는 0
- coords : 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/drawing.jpg')
img2 = cv2.imread('./img/my_hand.jpg')
# 마스크 생성, 합성할 이미지 전체 영역을 255로 세팅
mask = np.full_like(img1, 255)
# 합성할 대상 좌표 계산
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)
cv2.imshow('normal', normal)
cv2.imshow('mixed', mixed)
cv2.waitKey()
cv2.destroyAllWindows()
히스토그램
어떤 항목이 몇개 있는지 세어 놓은것을 그림으로 표시한것.
히스토그램 계산과 표시
영상 분야에서는 1~255인 픽셀이 몇개인지 세고 그림으로 표시한 것이다. 픽셀들의 색상이나 명암의 분포를 파악할 수 있다.
cv2.calcHist(img, channel, mask, histSize, ranges)
- img : 입력 영상, [img] 처럼 리스트로 표현
- channel : 처리할 채널, 리스트로 표현
- 1채널 : [0] / 2채널 : [0, 1] / 3채널 : [0, 1, 2]
- mask : 마스크에 지정한 픽셀만 히스토그램 계산
- histSize : 계급의 수, 채널 개수에 맞게 리스트로 표현
- 1채널 : [256] / 2채널 : [256, 256] / 3채널 : [256, 256, 256]
- ranges : 각 픽셀이 가질 수 있는 값의 범위, RGB인경우 [0, 256]
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/mountain.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imshow('img', img)
hist = cv2.calcHist([img],[0],None,[256],[0,256])
plt.plot(hist)
print(hist.shape)
print(hist.sum(), img.shape)
plt.show()
cv2.waitKey()
cv2.destroyAllWindows()
(256, 1)
270000.0 (450, 600)
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/mountain.jpg')
cv2.imshow('img', img)
cv2.waitKey()
cv2.destroyAllWindows()
channels = cv2.split(img)
colors = {'b','g','r'}
for (ch, color) in zip (channels, colors):
hist = cv2.calcHist([ch],[0],None,[256],[0,256])
plt.plot(hist, color = color)
plt.show()
노멀라이즈
픽셀값이 특정 특정 영역에 몰려잇는 경우 화질을 개선하고 영상간의 조건을 같게 만들어 주기위해 사용
dst = cv2.normalize(src, dst, alpha, beta, type_flag)
- src : 노멀라이즈 이전 데이터
- dst : 노멀라이즈 이후 데이터
- alpha : 노멀라이즈 구간 1
- beta : 노멀라이즈 구간 2, 구간 노멀라이즈가 아닌 경우 사용 안함
- type_flag : 알고리즘 선택 플래그 상수
- cv2.NORM_MINMAX : alpha와 beta 구간으로 노멀라이즈
- cv2.NORM_L1 : 전체 합으로 나누기, alpha = 노멀라이즈 전체 합
- cv2.NORM_L2 : 단위 벡터(unit vector)로 노멀라이즈
- cv2.NORM_INF : 최대 값으로 나누기
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/abnormal.jpg', cv2.IMREAD_GRAYSCALE)
# 직접 연산 정규화
img_f = img.astype(np.float32)
img_norm = ((img_f - img_f.min())*(255)/(img_f.max()-img_f.min()))
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])
cv2.imshow('Before', img)
cv2.imshow('Manual', img_norm)
cv2.imshow('cv2.normalize()', img_norm2)
cv2.waitKey()
cv2.destroyAllWindows()
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()
이퀄라이즈
노멀라이즈는 집중된 곳에서 떨어진 값에는 효과가 없다. 이퀄라이즈는 히스토그램으로 빈도를 구해서 노멀라이즈 한후 누적값을 전체개수로 나누어 나온 결과를 히스토그램 원래 픽셀에 매핑한다.
dst = cv2.eaualizeHist(src[ , dst] )
- src : 대상 이미지, 8비트 1채널
- dst : 결과 이미지
import cv2
import numpy as np
import matplotlib.pylab as plt
img = cv2.imread('./img/yate.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)
cdf_m = (cdf_m - cdf_m.min())/(rows*cols)*255
cdf = np.ma.filled(cdf_m,0).astype('uint8')
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])
cv2.imshow('Before',img)
cv2.imshow('Manual',img2)
cv2.imshow('cv2.equalizeHist()',img3)
cv2.waitKey()
cv2.destroyAllWindows()
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()
- 컬러이미지에 대해 이퀄라이즈 적용
import numpy as np, cv2
img = cv2.imread('./img/yate.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])
img2 = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
cv2.imshow('Before', img)
cv2.imshow('After', img2)
cv2.waitKey()
cv2.destroyAllWindows()
CLAHE
CLAHE(Contrast Limiting Adaptive Histogram Equalization)는
영상 전체에 이퀄라이즈를 적용했을때 너무 밝은 부분이 날아가는 현상을 막기 위해 영상을 일정한 영역으로 나눠서 이퀄라이즈를 적용하는 것을 말한다.
노이즈가 증폭되는 것을 막기 위해 어느 히스토그램 계급(bin)이든 지정된 제한 값을 넘으면 그 픽셀은 다른 계급으로 배분하고 나서 이퀄라이즈를 적용한다.
clahe = cv2.createCLAHE(clipLimit, tileGridSize) : CLAHE 생성
- clipLimit : Contrast 제한 경계 값, 기본 40.0
- 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/bright.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
# 밝기 채널에 대해서 이퀄라이즈 적용
img_eq = img_yuv.copy()
img_eq[:,:,0] = cv2.equalizeHist(img_eq[:,:,0])
ing_eq = cv2.cvtColor(img_eq, cv2.COLOR_YUV2BGR)
# 밝기채널에 대해 CLAHE 적용
img_clahe = img_yuv.copy()
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
img_clahe[:,:,0] = clahe.apply(img_clahe[:,:,0])
img_clahe = cv2.cvtColor(img_clahe, cv2.COLOR_YUV2BGR)
cv2.imshow('Before', img)
cv2.imshow('CLAHE', img_clahe)
cv2.imshow('equalizeHist', img_eq)
cv2.waitKey()
cv2.destroyAllWindows()
2D 히스토그램
1차원 히스토그램은 각 픽셀이 몇개인지 세어서 그래프로 표현하는데,
2차원 히스토그램은 이와 같은 축이 2개이고 각각의 축이 만나는 지점의 개수를 표현한다.
다음은 화창한 가을 하늘의 산을 찍은 사진을 2차원 히스토그램으로 표현한 것이다.
import cv2
import matplotlib.pylab as plt
plt.style.use('classic')
img = cv2.imread('./img/mountain.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()
역투영
2차원 히스토그램과 HSV 컬러 스페이스를 이용하면 색상으로 특정 물체나 사물의 일부분을 배경에서 분리할 수 있다.
기본 원리는 물체가 있는 관심영역의 H 와 V 값의 분포를 얻어낸 후 전체 영상에서 해당 분포의 픽셀만 찾아내는 것이다.
아래 예제는 마우스로 선택한 특정 물체만 배경에서 분리할 수 있다.
cv2.calcBlackProject(img, channel, hist, ranges, scale)
- img : 입력 영상, [img]처럼 리스트로 감싸서 표현
- channel : 처리할 채널, 리스트로 감싸서 표현
- 1채널 : [0], 2채널 : [0,1], 3채널 : [0,1,2]
- hist : 역투영에 사용할 히스토그램
- ranges : 각 픽셀이 가질 수 있는 값의 범위
-
scale : 결과에 적용할 배율 계수
- 마우스로 선택한 영역의 물체 배경 제거
import cv2
import numpy as np
import matplotlib.pyplot as plt
win_name = 'black_projection'
img = cv2.imread('./img/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])
# 선택 영역과 전체 영상에 대한 히스토그램 비율 계산
hist_rate = hist_roi/ (hist_img+1)
# 비율에 맞는 픽셀값 매핑
h,s,v = cv2.split(hsv_img)
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()
히스토그램 비교
히스토그램은 영상의 픽셀값의 분포를 갖는 정보이다. 그러므로 이것을 비교하면 영상에 사용한 픽셀의 색상 비중이 얼마나 비슷한지 알 수 있다.
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 : HISTCMP_BHATTACHARYYA와 동일
서로 다른 영상의 히스토그램을 같은 조건으로 비교하기 위해서는 먼저 히스토그램을 노멀라이즈 해야한다.
이미지가 크면 픽셀수가 많고, 히스토그램의 값도 커지기 때문이다.
import cv2
import numpy as np
import matplotlib.pylab as plt
img1 = cv2.imread('./img/taekwonv1.jpg')
img2 = cv2.imread('./img/taekwonv2.jpg')
img3 = cv2.imread('./img/taekwonv3.jpg')
img4 = cv2.imread('./img/dr_ochanomizu.jpg')
cv2.imshow('query', img1)
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='')
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("\timg%d:%7.2f"%(i+1, ret), end='\t')
print()
plt.show()
CORREL img1: 1.00 img2: 0.70 img3: 0.56 img4: 0.23
CHISQR img1: 0.00 img2: 67.33 img3: 35.71 img4:1129.49
INTERSECT img1: 1.00 img2: 0.54 img3: 0.40 img4: 0.18
BHATTACHARYYA img1: 0.00 img2: 0.48 img3: 0.47 img4: 0.79