Python과 OpenCV – 43 : 카메라 보정(Camera Calibration)

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

카메라나 비디오 영상의 왜곡 현상과 카메라의 내부, 외부 파라메터에 대해 학습하며, 이런 파라메터를 얻는 방법과 이미지의 왜곡 현상을 제거하는 내용을 설명합니다.

현대의 값 싼 소형 카메라(노트북이나 스마트폰에 장착된 개미 눈크기 만한 렌즈같은..)은 상당한 이미지의 왜곡을 발생시킵니다. 주요한 왜곡은 방사 왜곡과 탄젠티얼(Tangential) 왜곡이 있습니다.

방사 왜곡으로 인해, 아래의 그림처럼 반듯한 형상이 휘어지게 됩니다. 이런 현상은 이미지의 중심으로부터 멀어질수록 심해집니다.

위와 같은 왜곡 현상은 아래의 공식을 이용하면 해결할 수 있습니다.

유사하게, 또 다른 왜곡은 탄젠티얼 왜곡이며 이는 카메라의 렌즈가 이미지 평면에 완전하게 수평이 아닌 이유로 발생합니다. 이미지의 어떤 영역은 예상밖으로 더 가깝게 보이기도 합니다. 이는 아래의 공식으로 해결할 수 있습니다.

요약하면, 5개의 파라메터가 필요한데, 이 파라메터를 아래처럼 왜곡 계수라고 합니다.

추가적으로, 카메라의 내부(Intrinsic) 및 외부(Extrinsic) 파라메터에 대한 몇가지 더 많은 정보가 필요합니다. 내부 파라메터는 카메라에 특정되어 있습니다. 먼저 초점 길이(), 광학 중심() 등입니다. 이들을 카메라 메트릭스라고 부릅니다. 이는 오직 카메라의 특성에 의존하므로, 한번 계산되면 변하지 않으므로 계속 사용할 수 있습니다. 아래의 행렬과 같습니다.

외부 파라메터는 3차원 포인트를 좌표체계로 이동하는 회전 및 이동 벡터에 해당합니다.

스테레오 어플리케이션에서는 이러한 왜곡을 가장 먼저 보정해야 합니다. 이들 모든 파라메터를 구하기 위해, 해야할 것은 잘 정의된 패턴(예를들어, 체스판)이 담긴 샘플 이미지들을 제공하는 것입니다. 이 이미지 내부에 지정된 지점들(체스보드의 사각형 모서리)을 찾습니다. 이미 알고 있는 실세계에서의 좌표가 이미지의 어디에 해당하는지를 파악할 수 있는 지점입니다. 이러한 데이터를 가지고, 몇가지 수학적 문제가 왜곡 계수를 얻기 위한 바탕 위에서 해결됩니다. 지금까지가 전체 이야기에 대한 요약입니다. 카메라 보정을 위한 더 좋은 결과를 위해 최소한 10개의 패턴 이미지가 필요합니다.

앞서 언급했던 것처럼, 카메라 보정을 위해 최소한 10개의 테스트 패턴 이미지가 필요하다고 했습니다. 이해를 돕기 위해, 체스 보드에 대한 하나의 이미지에 집중하겠습니다. 카메라 보정을 위해 필요한 중요한 입력 데이터는 3차원의 실세계의 지점에 대한 집합과 이 집합에 대한 2차원 이미지 상의 지점 집합입니다. 2차원 이미지 지점은 이미지로부터 쉽게 발결할 수 있기만 하면 됩니다. (이들 이미지 지점들은 2개의 검정색 사각형이 맞닿은 위치가 됩니다)

실세계 공간에 대한 3차원 지점은 무엇일까요? 이들 이미지는 정적인 카메라로부터 촬영되었고 체스 보드는 다른 위치와 방향에 놓여 있습니다. 그래서 우리는 좌표를 파악할 필요가 있습니다. 그러나 단순화시키기 위해, 체스판이 XY 평면을 유지하고 있다고 말할 수 있어야 합니다(즉 항상 Z=0). 그리고 카메라는 이에 따라 움직이고 있습니다. 이러한 중요한 가정이 오직 X, Y값만을 계산하는 것으로 단순화시켜 줍니다. 이제 X,Y 값에 대해, 지점의 위치를 나타내는 (0,0), (1,0), (2,0), … 형식으로 단순하게 전달할 수 있습니다. 이 경우, 결과는 체스판에서의 사각형의 크기 축척으로 구해집니다. 그러나 만약 이 사각형의 크기를 알고 있다면(30mm라고 말할 수 있다면), mm 단위 결과로써 (0,0), (30,0), (60,0), …처럼 전달할 수 있습니다.

실세계에서의 3D 지점을 객체 지점이라고하고, 2D 이미지 지점을 이미지 지점이라고 합니다.

체스보드의 패턴을 찾기 위해, cv2.findChessboardCorners() 함수를 사용합니다. 또한 찾고자 하는 패턴이 어떤 형태인지, 즉 8×8 격자인지 5×5격자인지 등과 같이 말입니다. 이 예제에서는 7×6 격자입니다. 이 함수는 코너 지점과 패턴이 발견되었는지의 여부를 반환합니다. 이들 코너 지점들은 위치상 왼쪽에서 오른쪽으로, 위에서 아래로 정렬되어 있습니다. 체스판과 같은 이미지 대신에 원형 그리고 이미지를 사용할 수도 있는데, 이때는 cv2.findCirclesGrid()를 사용합니다.

일단 코너를 발견하면, cv2.cornerSubPix() 함수를 사용하여 정확도를 높일 수 있습니다. 또한 cv2.drawChessboardCorners() 함수를 사용해 코너 결과를 그릴 수 있습니다. 지금까지의 이야기에 대한 전체 코드는 아래와 같습니다.

import numpy as np
import cv2
import glob

# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*7,3), np.float32)
objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)

# Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.

images = glob.glob('./data/chess/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    # Find the chess board corners
    ret, corners = cv2.findChessboardCorners(gray, (7,6),None)

    # If found, add object points, image points (after refining them)
    if ret == True:
        objpoints.append(objp)

        corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
        imgpoints.append(corners2)

        # Draw and display the corners
        img = cv2.drawChessboardCorners(img, (7,6), corners2,ret)
        cv2.imshow('img',img)
        cv2.waitKey(500)

cv2.destroyAllWindows()

동일한 카메로 위치와 각도에서, 서로 다른 체스판의 위치로 촬영된 13개의 이미지를 활용하여 패턴을 검출하는데, 실행 결과 중 한컷은 다음과 같습니다.

패턴 검출을 통해 객체 지점(objpoints)과 이미지 지점(imgpoints)을 파악했으므로, 이를 이용해 왜곡된 촬영 영상을 보정할 수 있습니다. 보정 전에 cv2.getOptimalNewCameraMatrix() 함수를 이용해 먼저 카메라 메트릭스를 구해야 합니다. 이 함수는 카메라 메트릭스, 왜곡 계수, 회전/이동 벡터 등이 반환됩니다. 이 코드는 위의 예제 코드에서 36번째 줄의 for 문 외부에 위치합니다.

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)

이제 이미지의 왜곡을 제거할 수 있는데, OpenCV에서는 2가지 방법을 제공합니다. 그러나 먼저 왜곡을 제거하기에 앞서 cv2.getOptimalNewCameraMatrix() 함수를 사용하여 카메라 메트릭스를 개선할 수 있습니다. 이 함수의 스케일링 인자 alpha = 0 일 경우, 원치않는 픽셀을 최소로 갖는 보정된 이미지가 얻어지는데, 코너 지점의 픽셀들이 제거될 수도 있습니다. alpha = 1일 경우 모든 픽셀은 유지됩니다. 이 함수는 또한 결과를 자르는데 사용할 수 있는 이미지 ROI를 반환합니다. 13개의 샘플 이미지 중 왜곡 현상을 제거할 하나를 사용해 이미지의 크기를 얻고, 카메라 메트릭스를 얻는 코드는 다음과 같습니다.

img = cv2.imread('./data/chess/left12.jpg')
h,  w = img.shape[:2]
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))

위에서 왜곡을 제거하는 방법이 OpenCV에서는 2가지를 제공한다고 했는데, 첫번재는 다음과 같습니다.

dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.png',dst)

위와 동일한 결과를 제공하는 또 다른 방법은 다음과 같습니다.

mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameramtx,(w,h),5)
dst = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)

x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.png',dst)

실행해보면, 먼저 13장의 샘플 이미지를 통해 패턴을 분석하고, 분석된 패턴을 통해 카메라 메트릭스가 얻어지며 이 메트릭스를 개선한 뒤 최종적으로 왜곡된 부분을 제거해 calibresult.png 파일로 저장합니다. calibresult.png 파일은 다음과 같이 왜곡이 제거된 것을 볼 수 있습니다.

앞서 계산한 카메라 행렬과 왜곡계수들은 저장해두고 재활용할 수 있습니다.

왜곡 제거는 이미지의 프로젝션입니다. 이 왜곡 제거 시 수행된 프로젝션에 발생하는 오차가 얼마인지를 알기 위해 cv2.projectPoints() 함수가 사용됩니다. 결과적으로 얻어지는 값이 0에 가까울수록 정확한 것입니다.

tot_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
    tot_error += error

print("total error: ", tot_error/len(objpoints))

Python과 OpenCV – 42 : 배경 빼냄

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

배경 빼냄(Background Subtraction)은 많은 비전(Vision) 기반의 어플리케이션에서 중요한 처리입니다. 예를들어서, 고정 카메라에서 찍은 영상을 통해 몇명의 방문자가 방을 들어가고 나왔는지의 횟수를 센다거나, 교통 카메라에서 찍은 영상에서 차량의 정보를 추출하는 경우 등입니다. 이런 모든 경우에, 사람이나 차량을 추출해야 합니다. 기술적으로 변하지 않는 배경에서 움직이는 전경을 추출할 필요가 있습니다.

만약 배경만 가진 이미지라면, 즉 방문자나 차량이 전혀 없는 방이나 도로라면, 이것은 쉬운 작업입니다. 그러나 대부분의 경우에, 이렇지 않으므로 어떤 이미지든 배경을 추출할 필요가 있습니다. 만약 그림자까지 고려한다면 좀더 복잡해 집니다. 이동하는 그림자로 인해 배경을 전경으로 간주될 수 있기 때문입니다.

몇가지 알고리즘을 이런 목적을 위해 소개합니다. OpenCV에서는 3가지 알고리즘을 구현하였으며, 사용하기 쉽게 제공합니다. 하나씩 살펴보면..

먼저 BackgroundSubtractorMOG 알고리즘으로,가우시안 분산값 K(K=3~5)의 홉합에 의해 각 배경 픽셀을 구성하는 방법입니다. 홉합의 가중치는 장면에서 이들 색상값들이 머무르고 있는 시간 비율입니다. 배경으로써 판단될 수 있는 색상은 더 오랜 시간동안 변하지 않는 것입니다.

이 알고리즘의 구현은 cv2.bgsegm.createBackgroundSubtractorMOG()이며, 선택된 인자로써, 연산 인력의 길이, 가우시안 믹스쳐(혼합)의 개수, 임계값 등인데, 이는 모두 기본값으로 설정되어져 있습니다. 비디오의 각 프레임을 얻는 루프에서 apply 매서드를 호출하여 전경에 대한 마스크를 얻습니다. 예제는 다음과 같습니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('./data/vtest.avi')

fgbg = cv2.bgsegm.createBackgroundSubtractorMOG()

while(1):
    ret, frame = cap.read()

    fgmask = fgbg.apply(frame)

    cv2.imshow('frame',fgmask)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

결과는 다음과 같습니다.

다음 알고리즘은 BackgroundSubtractorMOG2로써, 장면에서의 조도값이 변경되는 경우에도 좋은 결과를 제공하며, 그림자에 대한 처리가 가능합니다. 또한 BackgroundSubtractorMOG와 다르게 각 픽셀마다 가우시안 분산값의 개수를 적당한 값을 선택해 줍니다.

이 알고리즘은 cv2.createBackgroundSubtractorMOG2() 함수로 실행 가능하며, detectShadows 선택 인자를 통해 그림자에 대한 처리 여부를 결정할 수 있는데, 기본값은 True입니다. 그림자는 마스크 결과로 회색으로 마킹됩니다. 아래는 예제 코드입니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('./data/vtest.avi')

fgbg = cv2.createBackgroundSubtractorMOG2()

while(1):
    ret, frame = cap.read()

    fgmask = fgbg.apply(frame)

    cv2.imshow('frame',fgmask)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

아래는 실행 결과입니다.

마지막으로 BackgroundSubtractorGMG 알고리즘입니다. 이 알고리즘은 정적인 배경 이미지 추정과 픽셀 당 Bayesian 분할을 조합하는 방식입니다. 일단 이 알고리즘은 동영상의 처음 몇 개의 프레임(기본값 120)을 배경 모델링을 위해 사용합니다. (그래서 이 알고리즘을 사용하면 처음 실행시 까만 화면만 표시됩니다.) 그리고 이를 Bayesian 추정을 사용하여 전경이라고 판단되는 것을 찾아 냅니다. 이러한 판단은 조정이 가능한데, 더 새로운 관측이 조도값이 변경되는 것을 수용할 수 있도록 더 높은 가중치를 갖습니다. 전경에 대한 마스크 이미지에 대한 잡음을 제거하기 위해 Closing과 Opening과 같은 Morphological 필터링 연산이 수행되는데, Opening이 더 잡음을 잘 제거합니다. 예제는 다음과 같습니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('./data/vtest.avi')

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
fgbg = cv2.bgsegm.createBackgroundSubtractorGMG()

while(1):
    ret, frame = cap.read()

    fgmask = fgbg.apply(frame)
    fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)

    cv2.imshow('frame',fgmask)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

결과는 다음과 같습니다.

Python과 OpenCV – 41 : Optical Flow

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

이 글에서는 광학 흐름(Optical Flow)을 이해하고 Lucas-Kanade 방법을 사용하여 이를 예측해 보는 예제를 살펴봅니다. OpenCv에서는 Lucas-Kanade 방법은 cv2.calcOpticalFlowPyrLK 함수를 사용합니다.

먼저 Optical Flow는 카메라 또는 물체의 이동에 의해 생기는 연속된 2개의 이미지 간의 어떤 이동에 대한 패턴입니다. 이 이동 패턴은 첫번재 프레임에서 두번째 프레임 간의 어떤 지점의 이동을 보여주는 2차원 변위 벡터(Displacement Vector)입니다. 아래 이미지를 보면..

5개의 연속된 프레임에서 빨간 공이 이동하고 있는 그림입니다. 하얀색 화살표가 바로 변위 벡터입니다. Optical Flow는 다양한 분야에서 활용되는데, 다음과 같은 응용 분야가 있습니다.

  • 물체의 이동 분석
  • 비디오 압축
  • 비디오 화질 개선
  • 등등

Optical Flow는 몇가지 가정이 있으며, 그중 2가지는 다음과 같습니다.

  1. 객체의 픽셀 강도는 연속된 프레임 간에 변환지 않는다.
  2. 한 픽셀의 이동과 그 인근 픽셀의 이동은 유사하다.

이러한 Optical Flow를 분석하는 방법으로 Lucas-Kanade 매서드가 있는데, OpenCV에서 cv2.calcOpticalFlowPyrLK()라는 하나의 함수를 통해 가능합니다. 비디오에서 어떤 포인트를 추적해 보는 하나의 예제를 살펴보겠습니다. 어떤 포인트에 대한 결정은 cv2.goodFeaturesToTrack() 함수를 사용하는데, 이 함수는 Shi-Tomasi 방식에 의한 특징점으로써 코너(Corenr)를 찾아냅니다. 첫번째 프레임에서 코너를 찾아내어 추적할 포인트로 결정합니다. 그리고 연속된 2개의 프레임들 간에 Lucas-Kanade Optical Flow를 사용하여 반복적으로 추적합니다. cv2.calcOpticalFlowPyrLK() 함수에 이전 프레임, 추적할 이전 포인트, 다음 프레임 등을 인자로 전달함으로써 이루어집니다. 이 함수는 이전 프레임에서 추적할 포인트가 연속된 다음 프레임에서 추적될 경우 상태값 1을 반환하고 발견되지 못할 경우 0을 반환합니다. 또한 추적할 포인트가 이동한 새로운 위치값도 함께 반환합니다. 다음 예제를 통해 더 깊이 이해할 수 있습니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('./data/vtest.avi')

# params for ShiTomasi corner detection
feature_params = dict( maxCorners = 100,
                       qualityLevel = 0.01,
                       minDistance = 30,
                       blockSize = 14)

# Parameters for lucas kanade optical flow
lk_params = dict( winSize  = (15,15),
                  maxLevel = 0,
                  criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# Create some random colors
color = np.random.randint(0,255,(100,3))

# Take first frame and find corners in it
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params)

# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)

while(1):
    ret,frame = cap.read()

    if not ret:
        break

    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # calculate optical flow
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)

    # Select good points
    good_new = p1[st==1]
    good_old = p0[st==1]

    # draw the tracks
    for i,(new,old) in enumerate(zip(good_new,good_old)):
        a,b = new.ravel()
        c,d = old.ravel()
        mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2)
        frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1)
    img = cv2.add(frame,mask)

    cv2.imshow('frame',img)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

    # Now update the previous frame and previous points
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1,1,2)

cv2.destroyAllWindows()

위의 예제는 다음 추적된 키포인트가 얼마나 정확한지 검사하지 않습니다. 그러므로 어떤 특징점 포인트가 이미지에서 사라질 수 있는데, 이 경우라고 할지라도 Optical Flow는 이를 개선할 수 있습니다. 실제 더 견고한 추적을 위해, 특징점인 코너는 특정한 구간(예를들어 5개의 프레임 마다)에서 검출되어야 합니다. 아래는 실행 결과입니다.

실제 실행 결과를 보면, 동영상 속에서 이동되는 객체(사람)가 이동된 경로로 선이 그려지는 것을 볼 수 있습니다.

다음은 앞서 살펴본, Optical Flow를 이동 경로로 보여주는 것이 아닌 밀도로 보여주는 것을 살펴봅니다. 이는 Gunner Farneback 알고리즘에 기반하고 있습니다. 아래의 예제 코드는 이 알고리즘을 사용하여 Optical Flow의 밀도(Dense)를 보여줍니다.

import cv2
import numpy as np
cap = cv2.VideoCapture('./data/vtest.avi')

ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1,cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[...,1] = 255

while(1):
    ret, frame2 = cap.read()
    next = cv2.cvtColor(frame2,cv2.COLOR_BGR2GRAY)

    flow = cv2.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0)

    mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
    hsv[...,0] = ang*180/np.pi/2
    hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX)
    rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)

    cv2.imshow('frame2',rgb)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    elif k == ord('s'):
        cv2.imwrite('opticalfb.png',frame2)
        cv2.imwrite('opticalhsv.png',rgb)
    prvs = next

cap.release()
cv2.destroyAllWindows()

실행 결과 중 이미지의 한 컷은 다음과 같습니다. (위 예제의 코드에 따라 s키를 눌러 저장된 이미지임)

Python과 OpenCV – 40 : Meanshift와 Camshift를 이용한 동영상에서의 객체 추적

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

비디오의 연속된 이미지에서 동일한 객체를 찾고 추적하는 알고리즘인 Meanshift와 Camshift에 대한 내용입니다. Meanshift는 매우 직관적입니다. 어떤 포인트의 집합을 가지고 있다고 합시다. (이 포인트의 집합은 Histogram Backprojection과 같은 픽셀들의 분포라고도 할 수 있음) 아래 그림처럼 원 모양의 작은 윈도우가 있고, 이 윈도우를 최대 픽셀 밀도값(또는 포인트의 최대 개수)을 가지는 영역으로 이동합니다. 아래 그림처럼 말입니다.

초기 윈도우는 파란색의 원이며 C1이라고 합시다. 이 C1의 원래 중심점은 파란색 사각형으로 표시되어 있고 C1_o라고 합시다. 그러나 만약 이 윈도우에 포함된 포인트들의 무게중심점을 구해보면, C1_o과 위치가 다르고C1_r가 됩니다. 이제 파란색 원의 중심점을 c1_r로 이동하고 이동된 윈도우에 포함된 포인트의 무게중심점을 구해 원의 중심점과 동일하지 않다면 다시 원의 중심을 새롭게 구한 무게중심점으로 이동시키기를 반복합니다. 이렇게 반복하다가 원의 중심점과 윈에 포함된 포인트의 무게 중심점 위치가 동일할때 반복을 멈춥니다. 바로 이 최종적으로 얻은 원, 즉 윈도우가 최대 픽셀 분포를 가지게 됩니다. 이를 위의 그림에서 초록색 원으로 표시하고 C2라고 합시다. 위의 그림에서 보듯 이 윈도우가 가장 많은 개수의 포인트를 포함하고 있습니다. 그렇다면 이미지에서는 이러한 처리가 어떻게 진행될까요? 아래 그림과 같습니다.

통상 Histogram Backprojection된 이미지와 초기 대상 위치를 전달합니다. 객체가 이동하면, 분명히 그 움직임은 Histogram Backprojection된 이미지에 반영됩니다. 결과적으로, Meanshift 알고리즘은 최대 강도를 가지는 새로운 위치로 윈도우를 이동시키게 됩니다.

위의 Meanshift 알고리즘을 OpenCV에서 사용해 보겠습니다. 먼저 추적할 객체 대상을 설정하고 이 대상의 히스토그램을 얻는데, 이는 Meanshift 연산을 위한 동영상의 각 프레임 이미지에 이 대상을 Backprojection 하기위함입니다. 또한 윈도우의 초기 위치를 제공할 필요가 있습니다. 히스토그램을 위해 오직 HSV 중 Hue 값만을 고려합니다. 또한 가짜 값들을 피하기 위해 cv2.inRange() 함수를 사용해 어두운 부분을 제거합니다. 전체 예제는 다음과 같습니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('slow.flv')

# take first frame of the video
ret,frame = cap.read()

# setup initial location of window
r,h,c,w = 250,90,400,125  # simply hardcoded the values
track_window = (c,r,w,h)

# set up the ROI for tracking
roi = frame[r:r+h, c:c+w]
hsv_roi =  cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)

# Setup the termination criteria, either 10 iteration or move by atleast 1 pt
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )

while(1):
    ret ,frame = cap.read()

    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)

        # apply meanshift to get the new location
        ret, track_window = cv2.meanShift(dst, track_window, term_crit)

        # Draw it on image
        x,y,w,h = track_window
        img2 = cv2.rectangle(frame, (x,y), (x+w,y+h), 255,2)
        cv2.imshow('img2',img2)

        k = cv2.waitKey(60) & 0xff
        if k == 27:
            break
        else:
            cv2.imwrite(chr(k)+".jpg",img2)

    else:
        break

cv2.destroyAllWindows()
cap.release()

실행해 보면, 지정한 부분(10번 코드의 사각영역)에 대한 객체를 동영상에서 추적하기 시작합니다. 아래는 동영상 중 3개의 프레임 결과 이미지입니다.

다음은 Camshift 알고리즘을 이용한 동영상에서의 객체 추적입니다. 앞서 Meanshift의 실행 결과를 보면, 객체가 점점 커지고 있음에도 윈도우의 크기는 변하지 못하고 일정합니다. Camshift 알고리즘은 이를 개선한 형태입니다. CAMshift는 Continuously Adaptive Meanshift의 약자입니다. Camshift 알고리즘은 먼저 meanshift를 적용하고, 윈도우의 크기를 다음 공식에 의해 갱신합니다.

또한 이 윈도우에 가장 잘 맞는 타원을 계산합니다. 다시 새롭게 크기가 조정된 윈도우와 이전 윈도우의 위치를 가지고 Meanshift를 적용합니다. 이러한 과정은 원하는 정확도가 나올때까지 반복합니다.

OpenCV에서 Camshift에 대한 예제를 살펴보겠습니다. 예제는 Meanshift와 유사하지만, 회전된 사각형 영역이 반환된다는 점이 다릅니다. 이렇게 반환된 사각형 영역은 다음 반복에서 검색 윈도우로 사용됩니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('slow.flv')

# take first frame of the video
ret,frame = cap.read()

# setup initial location of window
r,h,c,w = 250,90,400,125  # simply hardcoded the values
track_window = (c,r,w,h)

# set up the ROI for tracking
roi = frame[r:r+h, c:c+w]
hsv_roi =  cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)

# Setup the termination criteria, either 10 iteration or move by atleast 1 pt
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )

while(1):
    ret ,frame = cap.read()

    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)

        # apply meanshift to get the new location
        ret, track_window = cv2.CamShift(dst, track_window, term_crit)

        # Draw it on image
        pts = cv2.boxPoints(ret)
        pts = np.int0(pts)
        img2 = cv2.polylines(frame,[pts],True, 255,2)
        cv2.imshow('img2',img2)

        k = cv2.waitKey(60) & 0xff
        if k == 27:
            break
        else:
            cv2.imwrite(chr(k)+".jpg",img2)

    else:
        break

cv2.destroyAllWindows()
cap.release()

아래는 결과 중 3개의 프레임 이미지입니다.

추적 대상이 되는 영역이 커질 때 검색 결과 창도 같이 커지는 것을 확인할 수 있습니다.