'subtraction'에 해당되는 글 1건

  1. 2026.05.13 [OpenCV] Background Subtraction 배경 제거
반응형

배경을 제거하고 전경(마스크)만 남겨 보자.

 

#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;
}

 

분석된 전경(마스크) 영상

 

프로그램을 실행한 뒤 2~3초 후 생성된 배경 모델의 배경 이미지. 학습 시간이 너무 짧아 배경이 제대로 구축되지 않았다.

영상의 초반(이 예의 경우 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

Background Subtraction

Improved Background-Foreground Segmentation Methods

 

반응형
Posted by J-sean
: