[OpenCV实战]43 使用OpenCV进行背景分割

运动背景分割法Background Segment主要是指通过不同方法拟合模型建立背景图像,将当前帧与背景图像进行相减比较获得运动区域。下图所示为检测图像:

通过前面的检测帧建立背景模型,获得背景图像。然后检测图像与背景图像相减即为运动图像,黑色区域为背景,白色区域为运动目标,如下图所示:

在OpenCV标注库中有两种背景分割器:KNN,MOG2。但是实际上OpenCV_contrib库的bgsegm模块中还有其他几种背景分割器。本文主要介绍OpenCV_contrib中的运动背景分割模型及其用法,并对不同检测模型的性能和效果进行对比。

1 方法介绍

OpenCV_contrib中bgsegm模块主要有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函数接口介绍
GMG2012BackgroundSubtractorGMG
CNT2016BackgroundSubtractorCNT
KNN2006BackgroundSubtractorKNN
MOG2001BackgroundSubtractorMOG
MOG22004BackgroundSubtractorMOG2
GSOC2016BackgroundSubtractorGSOC
LSBP2016BackgroundSubtractorLSBP

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
GMG38.631.3
CNT4.62.9
KNN19.89.3
MOG16.315.6
MOG215.37.7
GSOC66.349.4
LSBP193.894.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 参考

posted @ 2020-08-14 13:16  落痕的寒假  阅读(411)  评论(0编辑  收藏  举报