[OpenCV] Background Subtraction 배경 제거
Computer Vision 2026. 5. 13. 11:42 |배경을 제거하고 전경(마스크)만 남겨 보자.
#include <opencv2/opencv.hpp>
int main()
{
cv::VideoCapture cap("Cars_On_Highway.mp4");
if (!cap.isOpened())
{
std::cerr << "Error opening video stream or file" << std::endl;
return -1;
}
int fps = static_cast<int>(cap.get(cv::CAP_PROP_FPS));
cv::Ptr<cv::BackgroundSubtractor> pBgSub = cv::createBackgroundSubtractorMOG2(500, 16.0, false);
// history: 모델이 배경으로 간주하기 위해 고려하는 프레임 수. 기본값은 500.
// 최초 500 프레임이 배경 모델을 구축하는 데 사용되고, 이후 프레임은 모델 업데이트에 영향을 미친다.
// 그러므로 최초에는 배경 모델이 구축될 수 있도록 충분한 프레임이 필요하다. history가 너무 작으면 모델이
// 빠르게 업데이트되어 일시적인 변화에 민감해질 수 있고, 너무 크면 모델이 느리게 업데이트되어 변화에
// 적응하는 데 시간이 걸릴 수 있다.
// varThreshold: 픽셀이 배경 모델과 일치하는지 여부를 결정하는 분산 임계값. 기본값은 16.0.
// 분산 임계값이 낮을수록 모델과 일치하는 픽셀이 더 많아지고, 높을수록 모델과 일치하는 픽셀이 더 적어진다.
// detectShadows: 그림자 감지를 활성화할지 여부를 결정하는 부울 값. 기본값은 true.
// 이 예에서는 detectShadows만 false로 설정하여 그림자 감지를 비활성화했다.
// 배경은 0으로 표시되고 전경은 255, 그림자는 128로 표시된다.
// 그림자는 배경 모델이 그림자를 배경으로 간주하지 않도록 도와주지만, 그림자 감지를 활성화하면 속도가 느려질 수 있다.
cv::Mat frame;
cv::Mat fgMask;
while (true)
{
cap >> frame;
if (frame.empty())
break;
cv::resize(frame, frame, cv::Size(), 0.5, 0.5);
pBgSub->apply(frame, fgMask);
// apply 메서드는 프레임을 입력으로 받아 fgMask에 전경 마스크를 출력한다.
// fgMask는 8비트 단일 채널 이미지로, 배경 픽셀은 0, 전경 픽셀은 255로 표시된다.
// apply 메서드는 또한 배경 모델을 업데이트한다. learningRate 매개변수를 사용하여 업데이트 속도를 제어할 수 있다.
// learningRate가 음수이면 알고리즘이 자동으로 학습 속도를 선택한다. 0이면 모델이 업데이트되지 않고, 1이면 모델이
// 마지막 프레임에서 재초기화된다.
// 노이즈가 많을 경우, 노이즈 제거를 위해 모폴로지 연산을 적용한다. (열림 연산)
//cv::Mat mask = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
//cv::morphologyEx(fgMask, fgMask, cv::MORPH_OPEN, mask);
cv::imshow("Frame", frame);
cv::imshow("FG Mask", fgMask);
if (cv::waitKey(1000 / fps) >= 0)
break;
}
cv::Mat bgImage;
pBgSub->getBackgroundImage(bgImage);
// getBackgroundImage 메서드는 현재 배경 모델을 기반으로 배경 이미지를 반환한다.
// 이 이미지는 배경 모델이 구축된 후에 호출해야 한다. 배경 제거 알고리즘이 배경으로 간주하는 픽셀의 평균값을 포함하는
// 이미지를 확인할 수 있다. 이 이미지는 때때로 매우 흐릿할 수 있으며, 배경 통계가 포함되어 있기 때문이다.
cv::imshow("BG Image", bgImage);
cv::waitKey(0);
cap.release();
cv::destroyAllWindows();
return 0;
}


영상의 초반(이 예의 경우 500프레임)에 배경만 나오는 영상을 사용한다면 훨씬 나은 결과를 보일 것으로 생각된다.

분석된 전경(마스크)을 이용해 움직이는 전경에 바운딩 박스를 표시해 보자.
#include <opencv2/opencv.hpp>
int main()
{
cv::VideoCapture cap("Cars_On_Highway.mp4");
if (!cap.isOpened())
{
std::cerr << "Error opening video stream or file" << std::endl;
return -1;
}
int fps = static_cast<int>(cap.get(cv::CAP_PROP_FPS));
cv::Ptr<cv::BackgroundSubtractor> pBgSub = cv::createBackgroundSubtractorMOG2(500, 16.0, false);
// history: 모델이 배경으로 간주하기 위해 고려하는 프레임 수. 기본값은 500.
// 최초 500 프레임이 배경 모델을 구축하는 데 사용되고, 이후 프레임은 모델 업데이트에 영향을 미친다.
// 그러므로 최초에는 배경 모델이 구축될 수 있도록 충분한 프레임이 필요하다. history가 너무 작으면 모델이
// 빠르게 업데이트되어 일시적인 변화에 민감해질 수 있고, 너무 크면 모델이 느리게 업데이트되어 변화에
// 적응하는 데 시간이 걸릴 수 있다.
// varThreshold: 픽셀이 배경 모델과 일치하는지 여부를 결정하는 분산 임계값. 기본값은 16.0.
// 분산 임계값이 낮을수록 모델과 일치하는 픽셀이 더 많아지고, 높을수록 모델과 일치하는 픽셀이 더 적어진다.
// detectShadows: 그림자 감지를 활성화할지 여부를 결정하는 부울 값. 기본값은 true.
// 이 예에서는 detectShadows만 false로 설정하여 그림자 감지를 비활성화했다.
// 배경은 0으로 표시되고 전경은 255, 그림자는 128로 표시된다.
// 그림자는 배경 모델이 그림자를 배경으로 간주하지 않도록 도와주지만, 그림자 감지를 활성화하면 속도가 느려질 수 있다.
cv::Mat frame;
cv::Mat fgMask;
std::vector<std::vector<cv::Point>> contours;
while (true)
{
cap >> frame;
if (frame.empty())
break;
cv::resize(frame, frame, cv::Size(), 0.3, 0.3);
pBgSub->apply(frame, fgMask);
// apply 메서드는 프레임을 입력으로 받아 fgMask에 전경 마스크를 출력한다.
// fgMask는 8비트 단일 채널 이미지로, 배경 픽셀은 0, 전경 픽셀은 255로 표시된다.
// apply 메서드는 또한 배경 모델을 업데이트한다. learningRate 매개변수를 사용하여 업데이트 속도를 제어할 수 있다.
// learningRate가 음수이면 알고리즘이 자동으로 학습 속도를 선택한다. 0이면 모델이 업데이트되지 않고, 1이면 모델이
// 마지막 프레임에서 재초기화된다.
// 점 노이즈 제거 (열림 연산)
cv::Mat maskOpen = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(fgMask, fgMask, cv::MORPH_OPEN, maskOpen);
// 사물이 여러 조각으로 나뉘는 것을 방지하기 위해 닫힘(Closing) 연산 적용
// 분리된 작은 조각들이 하나로 뭉쳐진다. 필요하면 Size(15, 15)를 증감.
cv::Mat maskClose = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(15, 15));
cv::morphologyEx(fgMask, fgMask, cv::MORPH_CLOSE, maskClose);
cv::findContours(fgMask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
//std::cout << "Contours found: " << contours.size() << std::endl;
double minArea = 300.0; // 합쳐지면서 전체 면적이 커지므로 최소 면적 임계값도 그에 맞게 조정
for (size_t i = 0; i < contours.size(); ++i)
{
if (cv::contourArea(contours[i]) < minArea)
{
contours.erase(contours.begin() + i);
// contours.erase의 매개변수는 삭제할 요소의 위치를 나타내는 반복자이므로, contours.begin() + i로 표현한다.
// contours.erase(contours[i]); 이렇게 표현할 수 없다.
--i; // 인덱스 조정
}
else
{
// 윤곽선 대신 직사각형 바운딩 박스로 감싸서 묶어준다.
cv::Rect rect = cv::boundingRect(contours[i]);
cv::rectangle(frame, rect, cv::Scalar(0, 255, 0), 2);
}
}
//std::cout << "Contours after filtering: " << contours.size() << std::endl;
cv::imshow("Frame", frame);
cv::imshow("FG Mask", fgMask);
if (cv::waitKey(1000 / fps) >= 0)
break;
}
cv::Mat bgImage;
pBgSub->getBackgroundImage(bgImage);
// getBackgroundImage 메서드는 현재 배경 모델을 기반으로 배경 이미지를 반환한다.
// 이 이미지는 배경 모델이 구축된 후에 호출해야 한다. 배경 제거 알고리즘이 배경으로 간주하는 픽셀의 평균값을 포함하는
// 이미지를 확인할 수 있다. 이 이미지는 때때로 매우 흐릿할 수 있으며, 배경 통계가 포함되어 있기 때문이다.
cv::imshow("BG Image", bgImage);
cv::waitKey(0);
cap.release();
cv::destroyAllWindows();
return 0;
}

전체 비디오에서 초반의 영상은 주요 학습 데이터로 빠르게 학습하고, 후반의 영상은 천천히 업데이트하고 싶다면 아래와 같이 코딩한다.
.
.
.
// 필요하다면 varThreshold를 높여서(16 -> 100) 조명 변화나 미세 노이즈에 덜 민감하게 만든다.
// 이 값이 커지면 웬만한 픽셀 값의 미세한 변화(빛 변화, 노이즈)는 전경으로 취급하지 않고 무시하게 된다.
cv::Ptr<cv::BackgroundSubtractor> pBgSub = cv::createBackgroundSubtractorMOG2(500, 100.0, false);
cv::Mat frame;
cv::Mat fgMask;
std::vector<std::vector<cv::Point>> contours;
while (true)
{
cap >> frame;
if (frame.empty())
break;
cv::resize(frame, frame, cv::Size(), 0.5, 0.5);
//std::cout << "Processing frame: " << cap.get(cv::CAP_PROP_POS_FRAMES) << std::endl;
if (cap.get(cv::CAP_PROP_POS_FRAMES) < 500)
{
pBgSub->apply(frame, fgMask, 0.1);
// 최초 500 프레임 동안은 learningRate를 크게 설정하여 모델이 배경을 빠르게 구축할 수 있도록 한다. 이 기간 동안 모델이 배경을 학습하는 데 충분한 프레임이 필요하다.
continue; // 실시간 영상이 아닌 녹화된 영상이므로 학습이 충분히 이루어질 때까지는 후속 처리(노이즈 제거, 윤곽선 검출, 화면 디스플레이 등)를 건너뛴다.
}
else {
// 이후 프레임에서는 learningRate를 적절히 낮춰(예: 0.001) 모델이 일시적인 변화에 너무 민감해지는 것을 방지한다.
// 거의 완전히 멈추면(0.00001) 노이즈까지 모두 전경으로 검출되므로, 서서히 적응할 수 있도록 0.001 ~ 0.005 정도를 부여한다.
pBgSub->apply(frame, fgMask, 0.001);
}
.
.
.
배경 모델의 배경 이미지(bgImage)를 계속 화면에 출력하도록 관련 부분을 루프 내로 옮기면 배경 이미지가 업데이트되는 것을 직접 확인할 수 있다. 위에서 사용한 영상들과는 다른 영상이지만 자동차 배기 가스가 배출되는 영상에서 업데이트되는 배경 이미지를 확인해 보자.
.
.
.
cv::Mat bgImage;
.
.
.
while (true)
{
.
.
.
pBgSub->getBackgroundImage(bgImage);
cv::imshow("Frame", frame);
cv::imshow("FG Mask", fgMask);
cv::imshow("BG Image", bgImage);
if (cv::waitKey(1000 / fps) >= 0)
break;
}
.
.
.

※ 참고
Background Subtraction with OpenCV and BGS Libraries
Improved Background-Foreground Segmentation Methods
'Computer Vision' 카테고리의 다른 글
| [CUDA] Device Properties 디바이스 정보 확인 (0) | 2026.04.29 |
|---|---|
| [CUDA] C++ with CUDA (0) | 2026.04.28 |
| [OpenCV] OpenCV and memcpy (feat.CUDA) (0) | 2026.04.27 |
| [OpenCV] CPU, GPU Image Processing Comparison 이미지 처리 속도 비교 (0) | 2026.04.26 |
| [OpenCV] OpenCV with CUDA Build (0) | 2026.04.26 |
















