運動背景分割法Background Segment主要是指通過不同方法擬合模型建立背景影象,將當前幀與背景影象進行相減比較獲得運動區域。下圖所示爲檢測影象:
通過前面的檢測幀建立背景模型,獲得背景影象。然後檢測影象與背景影象相減即爲運動影象,黑色區域爲背景,白色區域爲運動目標,如下圖所示:
在OpenCV標註庫中有兩種背景分割器:KNN,MOG2。但是實際上OpenCV_contrib庫的bgsegm模組中還有其他幾種背景分割器。本文主要介紹OpenCV_contrib中的運動背景分割模型及其用法,並對不同檢測模型的效能進行對比。
OpenCV_contrib中主要有以下GMG, CNT, KNN, MOG, MOG2, GSOC, LSBP等7種背景分割器,其中KNN,MOG2可以在OpenCV庫中直接使用,其他需要在OpenCV_contrib庫中使用。具體各個方法介紹如下:
各個方法提出時間和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的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()
在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 |
各個方法,個人評價如下:
追求速度 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一般用於樹莓派和多檢測任務中。