컴퓨터 비전 프로젝트를 진행하다 보면 영상에서 배경을 분리하고 "움직이는 객체(Foreground)"만 찾아내야 하는 경우가 많습니다. 이때 가장 널리 쓰이는 대표적인 배경 차분(Background Subtraction) 알고리즘이 바로 MOG2(Mixture of Gaussians 2)입니다.
오늘은 OpenCV에서 제공하는 BackgroundSubtractorMOG2의 핵심 동작 원리부터 기존 모델과의 차이점, 그리고 실무에서 바로 쓸 수 있는 C++ 구현 코드와 팁까지 핵심만 쏙쏙 정리해 보겠습니다.
1. MOG2의 핵심 원리: 가우시안 혼합 모델 (GMM)
MOG2를 이해하기 위한 핵심 키워드는 GMM(Gaussian Mixture Model)입니다.
동영상 내의 한 픽셀(Pixel)을 장시간 관찰하면, 그 픽셀의 색상이나 밝기 값은 고정되어 있지 않고 미세하게 변합니다. 바람에 흔들리는 나뭇잎, 카메라 센서의 노이즈, 또는 날씨에 따른 조명 변화 때문이죠.
MOG2는 각 픽셀의 색상 분포를 수년간 축적된 여러 개의 가우시안 확률 분포(정규분포)의 합으로 모델링합니다.
- 배경(Background) 모델링: 픽셀 값이 빈번하게, 그리고 안정적으로 머무르는 주요 분포들을 '배경'으로 학습합니다.
- 전경(Foreground) 판단: 새로운 프레임이 들어왔을 때, 해당 픽셀 값이 기존에 학습된 배경 분포의 범위를 벗어나면 "새로운 움직이는 물체가 나타났다"고 판단하는 원리입니다.

2. 기존 MOG v1 vs MOG2 차이점 (왜 MOG2를 쓸까?)
OpenCV에는 MOG와 MOG2가 모두 존재합니다. MOG2는 이름에서 알 수 있듯 기존 알고리즘의 치명적인 단점들을 개선한 버전이며, 가장 큰 차별점은 다음 두 가지입니다.
① 픽셀별 가우시안 개수의 동적 선택 (Adaptive Number)
- 기존 MOG: 영상 전체의 모든 픽셀에 고정된 개수(예: 3~5개)의 가우시안 분포를 할당합니다. 변화가 없는 단순한 배경(벽면 등)에도 불필요한 연산을 하므로 비효율적입니다.
- MOG2: 각 픽셀의 복잡도에 따라 가우시안 분포의 개수를 동적으로 결정합니다. 정적인 구역은 최소한의 분포만 쓰고, 바람에 흔들리는 나무나 파도처럼 변화가 심한(Multi-modal) 구역에만 더 많은 분포를 할당합니다. 결과적으로 정확도는 높아지고 연산 속도는 대폭 개선되었습니다.
② 정교한 그림자 제거 (Shadow Detection)
비전 기반 객체 검출에서 가장 골치 아픈 요소 중 하나가 바로 물체와 함께 움직이는 '그림자'입니다. 그림자 역시 픽셀 값이 변하기 때문에 일반 차분법에서는 물체의 일부로 오인하기 쉽습니다.
- MOG2는 픽셀의 밝기(Intensity) 변화와 색상(Color) 변화를 구분하여 감시합니다.
- 색상 비율은 일정한데 밝기만 어두워진 구간을 감지하면, 이를 전경이 아닌 그림자(Shadow)로 판별합니다. OpenCV 결과 마스크에서 그림자는 회색(기본값 127)으로 구분되어 출력되므로 후처리가 매우 용이합니다.
3. OpenCV C++ 실무 구현 코드
OpenCV 라이브러리를 활용해 비디오 파일에서 MOG2로 전경을 추출하는 전형적인 C++ 구현 패턴입니다.
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 1. 비디오 소스 오픈
cv::VideoCapture cap("test_video.mp4");
if (!cap.isOpened()) {
std::cerr << "Video open failed!" << std::endl;
return -1;
}
// 2. MOG2 객체 생성
// createBackgroundSubtractorMOG2(history, varThreshold, detectShadows)
// - history: 배경 학습에 반영할 이전 프레임 개수 (기본값: 500)
// - varThreshold: 마할라노비스 거리에 대한 임계값. 낮을수록 예민하고, 높을수록 노이즈에 둔감해짐 (기본값: 16)
// - detectShadows: 그림자 감지 여부 (true 설정 시 마스크에 회색(127)으로 표시)
cv::Ptr<cv::BackgroundSubtractorMOG2> pMOG2 = cv::createBackgroundSubtractorMOG2(500, 16, true);
cv::Mat frame, fgMask;
while (true) {
cap >> frame;
if (frame.empty()) break;
// 3. 배경 모델 업데이트 및 전경 마스크(fgMask) 추출
// 세 번째 인자인 Learning Rate를 -1로 두면 알고리즘이 history 파라미터에 맞춰 자동으로 계산합니다.
pMOG2->apply(frame, fgMask, -1);
// 4. 실무 필수 후처리: 모폴로지 연산 (Morphological Operations)
// 자잘한 카메라 센서 노이즈를 제거하기 위해 오픈(Opening) 연산을 적용합니다.
cv::morphologyEx(fgMask, fgMask, cv::MORPH_OPEN, cv::Mat());
// 5. 결과 시각화
// fgMask 출력 값 가이드: 255(전경), 0(배경), 127(그림자)
cv::imshow("Original Frame", frame);
cv::imshow("Foreground Mask (MOG2)", fgMask);
if (cv::waitKey(30) == 27) break; // ESC 누르면 종료
}
return 0;
}
4. 실무 적용 시 장단점 및 꿀팁 (Takeaway)
👍 장점
- 환경 변화 적응력: 갑자기 구름이 끼거나 전등이 켜지는 등 서서히 변하는 조명 환경을 배경 모델에 지속적으로 업데이트(Adaptation)하므로 오작동이 적습니다.
- 실시간성: 알고리즘 내부적으로 최적화가 잘 되어 있어, 고해상도가 아니라면 CPU 환경에서도 충분히 실시간(30fps 이상) 구동이 가능합니다.
👎 한계점
- 카메라 고정 필수: 배경을 기반으로 학습하기 때문에, 카메라 자체가 흔들리거나 회전(Pan/Tilt)하면 화면 전체가 전경으로 인식됩니다. (CCTV와 같은 고정형 카메라 환경에 최적)
- Ghost(유령) 현상: 배경의 일부였던 정지 물체(예: 주차되어 있던 차량)가 출발하고 나면, 그 자리가 한동안 전경(Ghost)으로 남아있다가 시간이 지나 학습이 되어야 비로소 사라집니다.
💡 실무 한스푼 팁
MOG2가 반환하는 fgMask를 가지고 객체 검출(Contour 외곽선 추적, Bounding Box 설정 등)을 진행할 때, 그림자 값인 127까지 검출되는 경우가 있습니다.
그림자를 제외하고 실제 순수 움직이는 객체만 정확히 바운딩하고 싶다면, 모폴로지 연산 전후로 cv::threshold를 사용해 이진화 기준값을 250 이상으로 걸어 회색(127) 영역을 깔끔하게 날려버리는 후처리를 거치는 것이 좋습니다.
짚고 넘어가기
현대 컴퓨터 비전에서는 딥러닝 기반 Object Detection 모델(YOLO 등)이 대세이지만, 고정된 카메라 환경에서 단순 객체의 움직임 여부만 빠르게 스크리닝하거나 ROI(관심영역)를 초기에 제한하는 전처리 단계에서는 여전히 MOG2만큼 가볍고 강력한 툴이 없습니다. 상황에 맞는 적절한 하이브리드 설계가 중요합니다!
포스팅이 도움이 되셨다면 공감과 댓글 부탁드립니다. 궁금한 점은 언제든지 댓글로 남겨주세요!
'OpenCV' 카테고리의 다른 글
| [OpenCV] 이미지 정렬을 위한 ECC 알고리즘 (findTransformECC) (0) | 2026.05.15 |
|---|---|
| 왜 OpenCV는 빠르고, 왜 때로는 SIMD가 더 빠를까? (0) | 2026.05.06 |