[OpenCV實戰]43 使用OpenCV進行背景分割

2020-08-12 17:18:02

運動背景分割法Background Segment主要是指通過不同方法擬合模型建立背景影象,將當前幀與背景影象進行相減比較獲得運動區域。下圖所示爲檢測影象:

通過前面的檢測幀建立背景模型,獲得背景影象。然後檢測影象與背景影象相減即爲運動影象,黑色區域爲背景,白色區域爲運動目標,如下圖所示:

在OpenCV標註庫中有兩種背景分割器:KNN,MOG2。但是實際上OpenCV_contrib庫的bgsegm模組中還有其他幾種背景分割器。本文主要介紹OpenCV_contrib中的運動背景分割模型及其用法,並對不同檢測模型的效能進行對比。

1 方法介紹

OpenCV_contrib中主要有以下GMG, CNT, KNN, MOG, MOG2, GSOC, LSBP等7種背景分割器,其中KNN,MOG2可以在OpenCV庫中直接使用,其他需要在OpenCV_contrib庫中使用。具體各個方法介紹如下:

  • GMG:基於畫素顏色進行背景建模
  • CNT:基於畫素點計數進行背景建模
  • KNN:基於K最近鄰進行背景建模
  • MOG:基於混合高斯進行背景建模
  • MOG2:基於混合高斯進行背景建模,MOG的升級版本
  • GSOC:類似LSBP
  • LSBP:基於LBP進行背景建模

各個方法提出時間和OpenCV函數介面介紹如下表所示:

方法 提出時間 OpenCV函數介面介紹
GMG 2012 BackgroundSubtractorGMG
CNT 2016 BackgroundSubtractorCNT
KNN 2006 BackgroundSubtractorKNN
MOG 2001 BackgroundSubtractorMOG
MOG2 2004 BackgroundSubtractorMOG2
GSOC 2016 BackgroundSubtractorGSOC
LSBP 2016 BackgroundSubtractorLSBP

OpenCV contrib庫的安裝見:

OpenCV_contrib庫在windows下編譯使用指南

2 程式碼與方法評估

2.1 程式碼

下述程式碼介紹了OpenCV_contrib的bgsegm模組中不同背景分割方法C++和Python的呼叫。對比了不同背景分割方法在範例視訊下,單執行緒和多執行緒的效果。

程式碼和範例視訊下載地址:

https://github.com/luohenyueji/OpenCV-Practical-Exercise

完整程式碼如下:

C++

#include <opencv2/opencv.hpp>
#include <opencv2/bgsegm.hpp>
#include <iostream>

using namespace cv;
using namespace cv::bgsegm;

const String algos[7] = { "GMG", "CNT", "KNN", "MOG", "MOG2", "GSOC", "LSBP" };

// 建立不同的背景分割識別器
static Ptr<BackgroundSubtractor> createBGSubtractorByName(const String& algoName)
{
	Ptr<BackgroundSubtractor> algo;
	if (algoName == String("GMG"))
		algo = createBackgroundSubtractorGMG(20, 0.7);
	else if (algoName == String("CNT"))
		algo = createBackgroundSubtractorCNT();
	else if (algoName == String("KNN"))
		algo = createBackgroundSubtractorKNN();
	else if (algoName == String("MOG"))
		algo = createBackgroundSubtractorMOG();
	else if (algoName == String("MOG2"))
		algo = createBackgroundSubtractorMOG2();
	else if (algoName == String("GSOC"))
		algo = createBackgroundSubtractorGSOC();
	else if (algoName == String("LSBP"))
		algo = createBackgroundSubtractorLSBP();

	return algo;
}

int main()
{
	// 視訊路徑
	String videoPath = "./video/vtest.avi";

	// 背景分割識別器序號
	int algo_index = 0;
	// 建立背景分割識別器
	Ptr<BackgroundSubtractor> bgfs = createBGSubtractorByName(algos[algo_index]);

	// 開啓視訊
	VideoCapture cap;
	cap.open(videoPath);

	// 如果視訊沒有開啓
	if (!cap.isOpened())
	{
		std::cerr << "Cannot read video. Try moving video file to sample directory." << std::endl;
		return -1;
	}

	// 輸入影象
	Mat frame;
	// 運動前景
	Mat fgmask;
	// 最後顯示的影象
	Mat segm;

	// 延遲等待時間
	int delay = 30;
	// 獲得執行環境CPU的核心數
	int nthreads = getNumberOfCPUs();
	// 設定執行緒數
	setNumThreads(nthreads);

	// 是否顯示運動前景
	bool show_fgmask = false;

	// 平均執行時間
	float average_Time = 0.0;
	// 當前幀數
	int frame_num = 0;
	// 總執行時間
	float sum_Time = 0.0;

	for (;;)
	{
		// 提取幀
		cap >> frame;

		// 如果圖片爲空
		if (frame.empty())
		{
			// CAP_PROP_POS_FRAMES表示當前幀
			// 本句話表示將當前幀設定爲第0幀
			cap.set(CAP_PROP_POS_FRAMES, 0);
			cap >> frame;
		}

		double time0 = static_cast<double>(getTickCount());

		// 背景建模
		bgfs->apply(frame, fgmask);
		time0 = ((double)getTickCount() - time0) / getTickFrequency();
		// 總執行時間
		sum_Time += time0;
		// 平均每幀執行時間
		average_Time = sum_Time / (frame_num + 1);

		if (show_fgmask)
		{
			segm = fgmask;
		}
		else
		{
			// 根據segm = alpha * frame + beta改變圖片
			// 參數分別爲,輸出影象,輸出影象格式,alpha值,beta值
			frame.convertTo(segm, CV_8U, 0.5);
			// 影象疊加
			// 參數分別爲,輸入影象/顏色1,輸入影象/顏色2,輸出影象,掩膜
			// 掩膜表示疊加範圍
			add(frame, Scalar(100, 100, 0), segm, fgmask);
		}

		// 顯示當前方法
		cv::putText(segm, algos[algo_index], Point(10, 30), FONT_HERSHEY_PLAIN, 2.0, Scalar(255, 0, 255), 2, LINE_AA);
		// 顯示當前執行緒數
		cv::putText(segm, format("%d threads", nthreads), Point(10, 60), FONT_HERSHEY_PLAIN, 2.0, Scalar(255, 0, 255), 2, LINE_AA);
		// 顯示當前每幀執行時間
		cv::putText(segm, format("averageTime %f s", average_Time), Point(10, 90), FONT_HERSHEY_PLAIN, 2.0, Scalar(255, 0, 255), 2, LINE_AA);

		cv::imshow("FG Segmentation", segm);

		int c = waitKey(delay);

		// 修改等待時間
		if (c == ' ')
		{
			delay = delay == 30 ? 1 : 30;
		}

		// 按C背景分割識別器
		if (c == 'c' || c == 'C')
		{
			algo_index++;
			if (algo_index > 6)
				algo_index = 0;

			bgfs = createBGSubtractorByName(algos[algo_index]);
		}

		// 設定執行緒數
		if (c == 'n' || c == 'N')
		{
			nthreads++;
			if (nthreads > 8)
				nthreads = 1;

			setNumThreads(nthreads);
		}

		// 是否顯示背景
		if (c == 'm' || c == 'M')
		{
			show_fgmask = !show_fgmask;
		}

		// 退出
		if (c == 'q' || c == 'Q' || c == 27)
		{
			break;
		}

		// 當前幀數增加
		frame_num++;
		if (100 == frame_num)
		{
			String strSave = "out_" + algos[algo_index] + ".jpg";
			imwrite(strSave, segm);
		}
	}

	return 0;
}

Python

# -*- coding: utf-8 -*-
"""
Created on Wed Aug 12 19:20:56 2020

@author: luohenyueji
"""

import cv2
from time import *

# TODO 背景減除演算法集合
ALGORITHMS_TO_EVALUATE = [
    (cv2.bgsegm.createBackgroundSubtractorGMG(20, 0.7), 'GMG'),
    (cv2.bgsegm.createBackgroundSubtractorCNT(), 'CNT'),
    (cv2.createBackgroundSubtractorKNN(), 'KNN'),
    (cv2.bgsegm.createBackgroundSubtractorMOG(), 'MOG'),
    (cv2.createBackgroundSubtractorMOG2(), 'MOG2'),
    (cv2.bgsegm.createBackgroundSubtractorGSOC(), 'GSOC'),
    (cv2.bgsegm.createBackgroundSubtractorLSBP(), 'LSBP'),
]


# TODO 主函數
def main():
    # 背景分割識別器序號
    algo_index = 0
    subtractor = ALGORITHMS_TO_EVALUATE[algo_index][0]
    videoPath = "./video/vtest.avi"
    show_fgmask = False

    # 獲得執行環境CPU的核心數
    nthreads = cv2.getNumberOfCPUs()
    # 設定執行緒數
    cv2.setNumThreads(nthreads)

    # 讀取視訊
    capture = cv2.VideoCapture(videoPath)

    # 當前幀數
    frame_num = 0
    # 總執行時間
    sum_Time = 0.0

    while True:
        ret, frame = capture.read()
        if not ret:
            return
        begin_time = time()
        fgmask = subtractor.apply(frame)
        end_time = time()
        run_time = end_time - begin_time
        sum_Time = sum_Time + run_time
        # 平均執行時間
        average_Time = sum_Time / (frame_num + 1)

        if show_fgmask:
            segm = fgmask
        else:
            segm = (frame * 0.5).astype('uint8')
            cv2.add(frame, (100, 100, 0, 0), segm, fgmask)

        # 顯示當前方法
        cv2.putText(segm, ALGORITHMS_TO_EVALUATE[algo_index][1], (10, 30), cv2.FONT_HERSHEY_PLAIN, 2.0, (255, 0, 255),
                    2,
                    cv2.LINE_AA)
        # 顯示當前執行緒數
        cv2.putText(segm, str(nthreads) + " threads", (10, 60), cv2.FONT_HERSHEY_PLAIN, 2.0, (255, 0, 255), 2,
                    cv2.LINE_AA)
        # 顯示當前每幀執行時間
        cv2.putText(segm, "averageTime {} s".format(average_Time), (10, 90), cv2.FONT_HERSHEY_PLAIN, 2.0,
                    (255, 0, 255), 2, cv2.LINE_AA);

        cv2.imshow('some', segm)
        key = cv2.waitKey(1) & 0xFF
        frame_num = frame_num + 1

        # 按'q'健退出回圈
        if key == ord('q'):
            break

    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

2.2 評價

在i5六代CPU(太渣就不具體介紹),12G記憶體,VS2017 C++ Release平臺下,各種方法處理速度如下表所示。

方法 單執行緒單幀處理平均時間/ms 四執行緒單幀處理平均時間/ms
GMG 38.6 31.3
CNT 4.6 2.9
KNN 19.8 9.3
MOG 16.3 15.6
MOG2 15.3 7.7
GSOC 66.3 49.4
LSBP 193.8 94.9

各個方法,個人評價如下:

  • GMG 初始建模幀會快速變化,導致全螢幕運動,對鄰近運動目標檢測效果一般,GMG需要自行設定參數(所以新的OpenCV標準庫移除了GMG)總體效果一般。效果如圖所示:
  • CNT 初始建模幀在一段時間持續變化導致全螢幕運動,運動目標過快可能會出現鬼影,低端裝置速度很快,高階硬體速度和MOG2相近,總體效果不錯。效果如圖所示:
  • KNN 初始建模在一段時間持續變化導致全螢幕運動,運動目標都能較好檢測出來,速度也還不錯,總體效果不錯。效果如圖所示:
  • MOG 建模會丟失運動目標,速度不錯,總體效果不錯。效果如圖所示:
  • MOG2 運動區域過大,容易出現細微變化區域,總體效果最好,MOG的升級版本,運動區域基本能檢測出來,不過需要自行設定參數。效果如圖所示:
  • GSOC 建模時間過短出現鬼影,隨着建模時間越來越長,檢測效果會變好,會逐漸消除鬼影,LSBP的升級版本,相對還行。效果如圖所示:
  • LSBP 極易出現鬼影,建模次數越多,建模消耗時間有所減少,但是鬼影會偶爾出現。效果如圖所示:

2.3 方法選擇

  • 追求速度 CNT or MOG2 or KNN
    如果是低端裝置或者並行任務多毫無疑問是CNT最好,高階裝置還是MOG2更好,畢竟MOG2檢測效果優於CNT,KNN也是不錯的選擇。

  • 追求品質 MOG2 or KNN or GSOC
    檢測品質MOG2和KNN差不多,GSOC建模時間長會很不錯,但是GSOC太慢了。如果不在意速度GSOC很好,其他還是MOG2和KNN。

  • 平衡品質和速度 MOG2 or KNN
    品質和速度均衡MOG2和KNN最不錯,不然爲什麼MOG2和KNN放在標準庫,其他在contrib庫。MOG2需要調整參數,不過速度和品質優於KNN。如果圖省心,不想調整參數,選KNN最好。

總的來說實際應用中,MOG2用的最多,KNN其次,CNT一般用於樹莓派和多檢測任務中。

3 參考