1. 배경 제거(Background Subtraction)
■ 배경 제거는 영상에서 움직이는 객체(전경)를 파악하기 위해 객체를 포함하는 영상에서 객체가 없는 배경 영상을 빼는 방법이다. 그 결과로 다음 그림과 같이 전경 마스크를 생성할 수 있다.

■ OpenCV에서는 배경 제거를 구현하는 객체 생성 함수 fgbg = cv2.bgsegm.createBackgroundSubtractorMOG(history, nmixtures, backgroundRatio, noiseSigma)를 제공한다.
- history는 배경 이미지를 학습하기 위해 사용하는 프레임의 개수
- nmixtures는 가우시안 믹스처의 개수, 디폴트는 5이다.
- backgroundRatio는 배경이라고 간주할 비율
- noiseSigma는 노이즈 강도
■ MOG(Mixture of Gaussians)는 가우시안 혼합(Gaussian Mixture)을 기반으로 각 픽셀의 색상 분포를 여러 개의 가우시안 분포로 모델링하여 전경과 배경을 분할하는 알고리즘이다.
- 배경에 속하는 픽셀 값은 주로 가우시안 분포에 속하며, 새로운 객체가 등장하면 배경 모델에 적합하지 않은 값이 발생해 전경으로 인식된다.
- 즉, 가우시안 분포들 중 어느 하나에 속하지 않는 픽셀이 전경으로 간주되며, 시간이 지나도 픽셀 값이 일정하게 유지되면 해당 픽셀을 배경으로 간주한다.
- 따라서 가우시안 분포 수를 얼마로 설정하느냐에 따라 성능이 크게 달라질 수 있으며, 복잡한 배경에서 노이즈가 발생할 수 있다.
■ cv2.bgsegm.createBackgroundSubtractorMOG( ) 함수로 생성된 객체에 배경 제거 객체의 인터페이스 함수 apply( )를 호출하면, 프레임에 대해 전경 마스크를 얻을 수 있다.
■ foregroundmask = fgbg.apply(img, foregroundmask, learningRate) 함수는 영상에서 전경을 추출하는 함수이다.
- img는 처리할 비디오 프레임
- foregroundmask는 출력될 전경 마스크. 255(흰색)은 전경 픽셀이고 0(검은색)은 배경 픽셀
- learningRate는 배경 훈련 속도로 0 ~ 1 사이의 값을 갖는다. -1로 지정할 경우 자동으로 적절한 학습률이 선택된다.
cap = cv2.VideoCapture('walking.avi') # 비디오 파일 불러오기
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수
print(fps)
```#결과#```
10.0
````````````
## 배경 제거 객체 생성
fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()
while cap.isOpened(): ## 비디오 열기
ret, frame = cap.read()
if not ret: break # 프레임을 다 읽으면 종료
fgmask = fgbg.apply(frame) # 전경 마스크 생성(=배경 제거 마스크). 전경은 흰색(255), 배경은 검은색(0)
cv2.imshow('frame',frame)
cv2.imshow('bgsub',fgmask)
if cv2.waitKey(1) & 0xff == 27: break # esc키 누르면 종료
cap.release() # 비디오 객체 해제
cv2.destroyAllWindows()
■ 또 다른 배경 제거 객체 생성 함수로 cv2.createBackgroundSubtractorMOG2(history, varThreshold, detectShadows)가 있다.
- varThreshold는 Mahalanobis 거리 제곱의 임곗값. 픽셀이 가우시안 분포를 잘 설명되는지 판별할 때 사용한다. 임곗값이 높으면 전경으로 판별되는 기준이 올라가서 큰 차이가 있는 픽셀만 전경으로 분류한다.
- detectShadows를 True로 지정하면 속도가 조금 느려지지만, 그림자를 탐지해서 표시한다. 그림자는 회색으로 표시된다. 디폴트는 True
■ MOG의 개선된 버전인 MOG2 함수도 가우시안 믹스쳐 기반으로 전경과 배경을 분할하는 알고리즘이다.
■ k개의 가우시안 분포를 선택하는 MOG 함수와 다른 점은 영상의 각 픽셀에서 적절한 가우시안 분포의 수를 선택한다는 것이다. 따라서 빛(조명) 변화가 심한 영상에서 좋은 적응력을 보인다.
■ 또 다른 점은 그림자 탐지 여부를 설정할 수 있다. 그림자를 제거하면 전경 검출의 정확도를 더 높일 수 있다.
cap = cv2.VideoCapture('walking.avi') # 비디오 파일 열기
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수
## 배경 제거 객체 생성
fgbg = cv2.createBackgroundSubtractorMOG2()
while cap.isOpened():
ret, frame = cap.read()
if not ret: break # 프레임을 다 읽으면 종료
fgmask = fgbg.apply(frame) # 전경 마스크 생성
cv2.imshow('frame',frame)
cv2.imshow('bgsub',fgmask)
if cv2.waitKey(1) & 0xff == 27: break # esc키 누르면 종료
cap.release() # 비디오 객체 해제
cv2.destroyAllWindows()
- varThreshold의 값을 디폴트로 사용한 경우 전경 영상에 노이즈가 나타는 것을 확인할 수 있다. 이는 적절한 varThreshold 값을 지정하여 완화할 수 있다.
2. 광학 흐름(Optical Flow)
■ 광학 흐름은 물체나 카메라의 움직임으로 인해 연속된 두 프레임(이전 프레임과 다음 프레임) 사이에서 나타나는 영상 내 객체의 움직인 패턴(이전 프레임과 다음 프레임 간 픽셀이 이동한 방향과 거리 분포)
■ 이는 2차원 벡터장으로, 다음 그림과 같이 첫 번째 프레임에서 두 번째 프레임으로 점이 이동한 이동 벡터를 나타낸다. 이를 통해 영상 내 객체가 어느 방향으로 얼마나 움직였는지 파악할 수 있으며, 객체 움직임을 예측할 수도 있다.

- 위의 이미지는 5개의 연속된 프레임에서 공이 움직이는 모습이다. 화살표는 공의 이동 벡터를 나타낸 것이다.
■ 광학 흐름이 작동하는 기본 가정 두 가지는
- (1) 어떤 물체(픽셀)의 강도는 연속된 프레임에서 변하지 않는다.
- (2) 이웃한 픽셀들은 서로 비슷한 움직임을 가진다.
■ 예를 들어 첫 번째 프레임에서 픽셀 I(x,y,y)를 고려해 보자. I는 영상/이미지, t는 시간
- 연속된 프레임을 다루기 때문에 시간 차원 t가 필요하다.
■ 이 픽셀이 Δt 후의 다음 프레임에서 (dx,dy)만큼 움직인다고 할 때, 두 프레임에서 해당 픽셀의 밝기가 동일하다고 가정하면 다음과 같은 식이 성립한다. I(x,y,t)=I(x+dx,y+dy,t+dt) ■ 이 식의 오른쪽 항을 테일러 급수로 근사하고 공통항을 제거한 뒤 δt로 나누면 다음 방정식이 성립한다. fxu+fyv+ft=0 - 이 식을 광학 흐름 방정식이라고 부른다.
- fx=∂f∂x, fy=∂f∂y, u=∂x∂t, v=∂y∂t
- fx,fy는 영상(픽셀 밝기)의 기울기, ft는 시간에 따른 기울기(=밝기의 변화율)를 의미한다.
- (u,v)는 미지수로, 한 개의 방정식(광학 흐름 방정식)으로는 (u,v) 두 변수를 알아낼 수 없다. 이 문제를 해결하기 위해 여러 기법이 제시되었는데, 그 중 하나가 루카스-카나데(Lucas-Kanade) 알고리즘이다.
■ 루카스-카나데 알고리즘은 주변 픽셀들은 모두 비슷한 움직임을 가진다는 가정을 이용한다. 관심 지점을 중심으로 작은 윈도우(3 x 3 패치(patch))를 잡아 해당 영역의 픽셀들을 모두 같은 움직임으로 간주한다.
■ 즉, 작은 윈도우(3 x 3 패치)에 속한 9개 픽셀에 대해 (fx,fy,ft) 값을 구한 뒤, 9개의 방정식을 세운다. 단, 미지수는 u,v 2개이므로 과결정 연립방정식(미지수보다 많은 방정식이 있는 연립방정식으로 보통 해가 없다.) 상태이다.
- 이를 풀기 위해 최소자승법(최소제곱법)을 사용해서 최종적인 근사해 u,v를 결정한다.

■ 하지만, 루카스-카나데 알고리즘은 작은 윈도우(3 x 3 패치)를 사용하여 움직임을 계산하기 때문에, 큰 움직임이 있을 때 기존 방법으로는 제대로 작동하지 않을 수 있다.
■ 이 문제를 개선하기 위해 이미지 피라미드 방식을 사용한다. 해상도를 낮춰 큰 움직임을 작은 움직임처럼 다룰 수 있게 한 뒤, 다시 높은 해상도로 올려서 보정한다. 즉, 큰 움직임을 작은 움직임처럼 보이게 한 후, 보정을 한다.
■ 이렇게 하면 루카스-카나데 알고리즘을 사용해도 큰 움직임을 추적할 수 있다. 즉, 스케일이 다른 움직임을 모두 추적할 수 있다.
■ OpenCV에서는 이 모든 과정을 수행할 수 있는 nextPts, status, err = cv2.calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts, status, err, winSize, maxLevel, criteria, flags, minEigThreshold) 함수를 제공한다.
- prevImg는 이전 프레임 영상(8비트 영상) 또는 buildOpticalFlowPyramid로 생성된 이미지 피라미드
- nextImg는 다음 프레임 영상(8비트 영상) 또는 buildOpticalFlowPyramid로 생성된 이미지 피라미드
- prevPts는 이전 프레임의 코너 특징점, nextPts는 다음 프레임에서 이동한 코너 특징점으로
- nextPts에는 nextImg에서 추적된 prevImg의 새 위치가 기록된다.
- 특징점은 시-토마시 검출 함수인 cv2.goodFeaturesToTrack( )으로 검출한다.
- status는 반환되는 상태 벡터로 광학 흐름을 성공적으로 찾은 점(대응점)에는 1, 실패한 점(대응점이 아님)에는 0이 기록된다.
- err는 반환되는 오류 벡터로 각 요소는 해당 특징점에서의 에러 값(대응점 간의 오차)을 나타낸다.
- winSize는 각 이미지 피라미드의 레벨에서 사용하는 윈도우 크기로 디폴트는 (21, 21)
- maxLevel은 이미지 피라미드의 최대 레벨(최대 계층 수). 0이면 피라미드를 사용하지 않음
- criteria는 반복 검색을 멈출 종료 기준
criteria | 의미 |
cv2.TERM_CRITERIA_EPS | 정확도가 epsilon보다 작으면 중지 |
cv2.TERM_CRITERIA_MAX_ITER | max_iter 횟수를 채우면 정지 |
cv2.TERM_CRITERIA_COUNT | MAX_ITER와 동일 |
- criteria의 디폴트는 (TERM_CRITERIA_COUNT + TERM_CRITERIA_EPS, 30, 0.01)
- flags는 연산 모드로 디폴트는 0
flags | 의미 |
0 | prevPts를 nextPts에 저장된 초깃값으로 사용 |
cv2.OPTFLOW_USE_INITAL_FLOW | nextPts의 값을 초깃값으로 사용 |
cv2.OPTFLOW_LK_GET_MIN_EIGENVALS | 오차(에러)를 최소 고윳값(minEigThreshold)으로 계산 |
- cv2.OPTFLOW_LK_GET_MIN_EIGENVALS으로 지정하면 최소 고윳값(minEigThreshold) 방식으로 에러를 계산한다. 플래그가 없으면(flags = 0), 기본적으로 윈도우(패치) 간의 L1 거리 등을 에러로 사용한다.
- minEigThreshold는 최소 임계 고윳값으로 디폴트는 1e-4
cap = cv2.VideoCapture('walking.avi') # 비디오 파일 열기
fps = cap.get(cv2.CAP_PROP_FPS) # 프레임 수
delay = int(1000/fps) # 영상 딜레이
color = np.random.randint(0, 255, (200, 3)) # 추적 경로 (랜덤)색상 100개로 표시
feature_params = dict(maxCorners = 200, qualityLevel = 0.01, minDistance = 10) # 시-토마시 검출 파라미터
termcriteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03) # # calcOpticalFlowPyrLK 중지 조건
lines = None # 추적 경로를 그릴 마스크 이미지 변수
prevImg = None
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
img_draw = frame.copy()
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if prevImg is None: # 최초 프레임
prevImg = frame_gray
lines = np.zeros_like(frame) # 추적 경로를 그릴 마스크 이미지를 프레임 크기에 맞게
prevPt = cv2.goodFeaturesToTrack(prevImg, mask = None, **feature_params) # 이전 프레임의 코너 특징점 # 시-토마시 코너 검출
else:
nextImg = frame_gray
## 루카스-카나데 방법으로 광학 흐름 계산
nextPt, status, err = cv2.calcOpticalFlowPyrLK(prevImg, nextImg, prevPt, None, criteria=termcriteria)
## 광학 흐름을 성공적으로 찾은 점(= 추적에 성공한 점(대응점)) 선택
good_old = prevPt[status==1]
good_new = nextPt[status==1]
## 추적 경로 그리기(추적된 점의 궤적을 그림)
for i, (new, old) in enumerate(zip(good_new, good_old)):
old_x, old_y = old.ravel().astype(np.int32)
new_x, new_y = new.ravel().astype(np.int32)
lines = cv2.line(lines, (old_x, old_y), (new_x, new_y), color[i].tolist(), 2)
img_draw = cv2.circle(img_draw, (new_x, new_y), 2, color[i].tolist(), -1)
img_draw = cv2.add(img_draw, lines) # 누적된 추적 경로를 출력 결과에 합성
cv2.imshow('LK Optical Flow', img_draw)
## 이전 프레임과 이전 포인트(특징점)를 업데이트
prevImg = nextImg
prevPt = good_new.reshape(-1,1,2)
key = cv2.waitKey(delay)
if key == 27 : break
elif key == 8: prevImg = None # Backspace키로 추적 경로 새로 시작
cap.release()
cv2.destroyAllWindows()
- 루카스-카나데 방법은 시-토마시 검출 함수 cv2.goodFeatureToTrack( )로 이전 프레임의 특징점을 검출한 다음, cv2.calcOpticalFlowPyrLK( ) 함수로 광학 흐름을 계산해 다음 프레임의 특징점을 찾는 방식이다.
- 그다음, 두 프레임의 대응되는 특징점(추적에 성공한 점)만 선택해서 추적 경로를 나타내는 마스크 이미지를 업데이트하고, 원본 영상의 프레임에 두 번째 프레임(다음 프레임)의 점을 만들어서 add( ) 함수를 통해 원본 이미지에 추적 경로를 표시한다.
■ 광학 흐름을 계산하는 방법은 두 가지이다.
- 루카스-카나데 방법은 희소(sparse) 특징점 집합(시-토마시 검출로 검출된 코너들)에 대해 광학 흐름을 계산한다. 즉, 일부 픽셀만 이용해 계산한다.
- 두 번째 방법은 군나르 파너백(Gunner Farneback) 방법이다. 이 방법은 영상 전체 픽셀을 모두 계산하는 밀집(dense) 광학 흐름을 구한다.
■ 군나르 파너백 알고리즘은 모든 지점에 대해 (u,v) 형태의 광류 벡터를 2채널 배열로 만든다. 이 배열에서 광류 벡터의 크기와 방향을 구한 후, 결과를 색상으로 표현해 더 시각적으로 이해하기 쉽게 만든다.
- 여기서 방향은 이미지의 Hue(색조) 값이며, 크기는 Value(명도) 값
■ 그리고 영상 전체 픽셀을 이용하기 때문에 희소 광학 흐름을 계산하는 루카스-카나데 방법과 달리 추적할 특징점을 찾을 필요가 없다.
■ OpenCV에서는 이 모든 과정을 수행할 수 있는 flow = cv2.calcOpticalFlowFarneback(prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags) 함수를 제공한다.
- prev와 next는 8비트 단일 채널(그레이스케일)의 이전(첫 번째), 이후(두 번째) 영상/프레임
- flow는 군나르 파너백 방법을 이용한 광학 흐름 계산 결과.
- pyr_scale은 이미지 피라미드를 생성할 때 사용하는 스케일. 예를 들어 pyr_scale = 0.5이면 가로와 세로가 절반 크기로 축소된 이미지 피라미드
- levels는 이미지 피라미드 개수. 이 값이 1이면 추가 레벨없이 원본 영상만 사용
- winsize는 평균 윈도우 크기. 값이 클수록 노이즈에 대해 강인해진다. 단, 움직이는 영역이 더 흐릿해질 수 있다.
- iterations는 각 피라미드 레벨에서 알고리즘이 반복 수행되는 횟수
- poly_n은 각 픽셀에서 다항식 근사를 찾기 위해 사용하는 픽셀 주변 이웃 픽셀의 크기. 값이 클수록 영상이 더 매끄러운 표면으로 근사되어 움직이는 영역이 흐릿해진다. 일반적으로 poly_n값으로 5 또는 7을 사용한다.
- poly_sigma는 다항식 근사에 사용하는 그래디언트를 스무스(smooth)하게 만들기 위해 적용되는 가우시안 시그마(표준편차). poly_n = 5일 때, poly_sigma = 1.1, poly_n = 7일 때는 1.5 정도가 적절하다.
- flags는 연산 모드로 cv2.OPTFLOW_USE_INITAL_FLOW를 지정하면 입력으로 주어진 flow 파라미터 값을 초기 광학 흐름 추정값, 즉 초깃값으로 사용한다. cv2.OPTFLOW_FARNEBACK_GAUSSIAN를 지정하면 박스 필터 대신 가우시안 필터(winsize x winsize 크기)를 사용해 광학 흐름을 추정한다. 박스 필터에 비해 정확도가 높지만, 계산 속도가 다소 느려질 수 있다.
cap = cv2.VideoCapture('walking.avi')
fps = cap.get(cv2.CAP_PROP_FPS)
delay = int(1000/fps)
ret, prv_frame = cap.read() # prv_frame은 첫 번째 프레임
prv = cv2.cvtColor(prv_frame, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(prv_frame) # 첫 번째 프레임과 같은 크기의 HSV 맵 생성
hsv[..., 1] = 255 # 채도 채널을 255로
while cap.isOpened():
ret, next_frame = cap.read()
if not ret: break
ne = cv2.cvtColor(next_frame, cv2.COLOR_BGR2GRAY) # ne는 두 번째 프레임
## 군나르 파너백 방법으로 광학 흐름 계산
flow = cv2.calcOpticalFlowFarneback(prv, ne, None, 0.5, 3, 15, 3, 5, 1.1,
cv2.OPTFLOW_FARNEBACK_GAUSSIAN)
## flow[..., 0](x 방향), flow[..., 1](y 방향)
# 극좌표 변환 함수 cartToPolar( )로 flow의 x, y 성분을 이용해 광류 벡터의 크기와 각도(방향) 계산
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
hsv[..., 0] = ang*180/np.pi/2 # H(색조)값을 벡터 방향(ang)으로
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) # V(명도)값을 벡터 크기(mag)로
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) # HSV 형식으로 만든 광학 흐름 영상/이미지를 다시 BGR 형태로 변환
cv2.imshow('GF Optical Flow', bgr)
key = cv2.waitKey(delay)
if key == 27 : break
prv = ne # 다음 프레임 계산을 위해 현재 프레임(ne)을 이전 프레임(prv)으로 업데이트
cap.release()
cv2.destroyAllWindows()
- OpenCV의 H(색조) 채널의 범위는 [0, 180), 0~179. 그러므로 ang*180/pi/2로 스케일링한 뒤 H 채널에 할당
'OpenCV' 카테고리의 다른 글
매칭(Matching) (2) (2) | 2024.12.24 |
---|---|
매칭(Matching) (1) (0) | 2024.12.23 |
분할(segmentation) (2) (0) | 2024.12.22 |
분할(segmentation) (1) (0) | 2024.12.22 |
모폴로지(Morphology) 연산, 이미지 피라미드(Image Pyramid) (0) | 2024.12.21 |