안녕하세요? 이번 글은 OpenCV-Python으로 영상 배경 제거(Background Subtraction)하는 방법을 학습해 보도록 하겠습니다.
일단 파이썬에서 OpenCV를 이용하기 위해 관련 패키지를 설치해 보겠습니다.
opencv-contrib-python은 비공식적으로 사전 빌드된 파이썬용 OpenCV 패키지입니다.
opencv-contrib-python | https://pypi.org/project/opencv-contrib-python/
1 2 3 4 5 6 7 | # opencv-contrib-python 설치 import sys, subprocess subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'opencv-contrib-python']) import cv2 as cv print(cv.__version__) | cs |
아래 자료는 소백산국립공원 생태통로에 설치된 카메라 트랩(camera trap)이 포착한 고라니(waterdeer) 영상입니다.
보시는 것처럼 정적인 배경(static background)이 있고, 그 위에 움직이는 전경(moving foreground)이 존재하고 있습니다.
위 고라니 영상을 재생해 보겠습니다. 먼저 VideoCapture 객체를 생성하고 동영상 파일을 지정해 줍니다.
그리고 반복문을 통해 각각의 프레임을 읽어들이고 화면에 보여줍니다. 이 때 cv.waitKey() 함수는 키보드
입력을 대기하는 함수로 여기서는 30 밀리초(1초 = 1000밀리초, millisecond, ms) 값을 지정하였습니다.
'q' 키를 누르거나 동영상의 끝까지 도달하면 반복문은 중지됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | cap = cv.VideoCapture('waterdeer.avi') while(cap.isOpened()): # ret: frame capture 결과(boolean) # frame: capture한 frame ret, frame = cap.read() if (ret): cv.imshow('frame', frame) k = cv.waitKey(30) & 0xFF if k == ord('q'): break else: break cap.release() cv.destroyAllWindows() | cs |
만약 고라니가 없는, 동적 전경이 없는 상태의 배경 이미지를 가지고 있다면 배경 제거는 어떻게 구할 수 있을까요?!
아마도 이상적인 환경에서는 배경 이미지에서 새 이미지를 차감하면 전경 객체만을 손쉽게 탐지할 수 있을 것입니다.
이제 OpenCV에서 제공하는 배경 제거 알고리즘을 직접 적용해보도록 하겠습니다.
Background Subtraction | https://docs.opencv.org/3.4.4/db/d5c/tutorial_py_bg_subtraction.html
OpenCV는 아래와 같이 다양한 배경 제거 알고리즘을 제공하고 있습니다:
이미지 출처: https://docs.opencv.org/3.4/d7/df6/classcv_1_1BackgroundSubtractor.html
여기서는 'BackgroundSubtractorMOG2' 알고리즘을 시험해 보고자 합니다. MOG2는 가우시안 혼합 기반 배경/전경 분할
알고리즘 (Gaussian Mixture-based Background/Foreground Segmentation Algorithm)으로 아래 2편의 논문에 기반하여 구현되었습니다.
Zivkovic, Z. 2004. Improved adaptive Gausian mixture model for background subtraction.
International Conference Pattern Recognition, 2, 28-31.
논문 바로가기: http://www.zoranz.net/Publications/zivkovic2004ICPR.pdf
Zivkovic, Z., van der Heijden, F. 2006. Efficient adaptive density estimation per image pixel for the task of background subtraction.
Pattern Recognition Letters, 27(7), 773-780.
논문 바로가기: http://www.zoranz.net/Publications/zivkovicPRL2006.pdf
아래와 같이 배경 제거 알고리즘 MOG2를 적용하는 구문을 삽입하였습니다. 결과를 볼까요?!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | cap = cv.VideoCapture('waterdeer.avi') # BackgroundSubtractorMOG2 fgbg = cv.createBackgroundSubtractorMOG2() while(cap.isOpened()): # ret: frame capture 결과(boolean) # frame: capture한 frame ret, frame = cap.read() if (ret): fgmask = fgbg.apply(frame) cv.imshow('frame', fgmask) k = cv.waitKey(30) & 0xFF if k == ord('q'): break else: break cap.release() cv.destroyAllWindows() | cs |
자, 이제 동적인 전경과 정적인 배경이 흑백으로 구분(회색은 그림자 탐지)된 것을 보실 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | cap = cv.VideoCapture('waterdeer.avi') frame_width, frame_height, frame_rate = int(cap.get(3)), int(cap.get(4)), int(cap.get(5)) fourcc = cv.VideoWriter_fourcc(*'DIVX') out = cv.VideoWriter('waterdeer-mog2.avi', fourcc, frame_rate, (frame_width, frame_height), 0) # BackgroundSubtractorMOG2 fgbg = cv.createBackgroundSubtractorMOG2() while(cap.isOpened()): # ret: frame capture 결과(boolean) # frame: capture한 frame ret, frame = cap.read() if (ret): fgmask = fgbg.apply(frame) cv.imshow('mog2', fgmask) out.write(fgmask) k = cv.waitKey(30) & 0xFF if k == ord('q'): break else: break cap.release() out.release() cv.destroyAllWindows() | cs |
한편, 배경 제거 영상에 원치 않는 잡음들이 보이는데요, 이러한 노이즈를 제거하는데는
'형태 변환(Morphological Transformations)' 알고리즘을 유용하게 사용하실 수 있습니다.
형태 변환은 일반적으로 이진 이미지에 수행되며, 원본 이미지와 커널(kernel)을 입력값으로 사용합니다.
이 중 Opening은 잡음 제거에 유용한 방법으로 침식 후 팽창(erosion followed by dilation)을 통해 본래
이미지 크기로 돌려놓은 것입니다.
형태 변환(Morphological Transformations) | https://docs.opencv.org/3.4.4/d9/d61/tutorial_py_morphological_ops.html
좌측부터 원본 이미지, 침식(erosion), 팽창(dilation) 순입니다. 간단한 구현이지만 잡음 제거에 확실한 도움이 되겠죠?!
그림출처: https://docs.opencv.org/3.4.4/j.png
그림출처: https://docs.opencv.org/3.4.4/erosion.png
그림출처: https://docs.opencv.org/3.4.4/dilation.png
아래는 Opening 기법의 적용 전후 비교 이미지입니다.
그림 출처: https://docs.opencv.org/3.4.4/opening.png
자, 이제 코드를 한 번 더 업데이트하는데요, 커널을 생성하고 형태 변환 중 Opening 기법을 적용하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | cap = cv.VideoCapture('waterdeer.avi') frame_width, frame_height, frame_rate = int(cap.get(3)), int(cap.get(4)), int(cap.get(5)) fourcc = cv.VideoWriter_fourcc(*'DIVX') out = cv.VideoWriter('waterdeer-mog2.avi', fourcc, frame_rate, (frame_width, frame_height), 0) # BackgroundSubtractorMOG2 fgbg = cv.createBackgroundSubtractorMOG2() kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE,(3,3)) while(cap.isOpened()): # ret: frame capture 결과(boolean) # frame: capture한 frame ret, frame = cap.read() if (ret): fgmask = fgbg.apply(frame) fgmask = cv.morphologyEx(fgmask, cv.MORPH_OPEN, kernel) cv.imshow('mog2', fgmask) out.write(fgmask) k = cv.waitKey(30) & 0xFF if k == ord('q'): break else: break cap.release() out.release() cv.destroyAllWindows() | cs |
결과는 아래와 같습니다. 더 깔끔해졌죠?! 다음 글에서는 동적 전경 영역만을 프레임으로 추출해 보도록 하겠습니다.