天天看點

Opencv4(C++)實戰案例4:數白塊需求分析案例方法:實作運動目标檢測(追蹤)初步構思通過測試代碼算法分析

需求分析

讀入一個視訊流,對視訊流中的白塊進行計數.

案例方法:實作運動目标檢測(追蹤)

待選方法:

①幀差法

基本原理就是在圖像序列相鄰兩幀或三幀間采用基于像素的時間差分通過閉值化來提取出圖像中的運動區域。幀差法僅僅做運動檢測。網上經常有人做個運動檢測,再找個輪廓,拟合個橢圓就說跟蹤了,并沒有建立幀與幀之間目标聯系的,沒有判斷目标産生和目标消失的都不能算是跟蹤吧。

首先,将相鄰幀圖像對應像素值相減得到差分圖像,然後對差分圖像二值化,在環境亮度變化不大的情況下,如果對應像素值變化小于事先确定的門檻值時,可以認為此處為背景像素:如果圖像區域的像素值變化很大,可以認為這是由于圖像中運動物體引起的,将這些區域标記為前景像素,利用标記的像素區域可以确定運動目标在圖像中的位置。

由于相鄰兩幀間的時間間隔非常短,用前一幀圖像作為目前幀的背景模型具有較好的實時性,其背景不積累,且更新速度快、算法簡單、計算量小。算法的不足在于對環境噪聲較為敏感,門檻值的選擇相當關鍵,選擇過低不足以抑制圖像中的噪聲,過高則忽略了圖像中有用的變化。對于比較大的、顔色一緻的運動目标,有可能在目标内部産生空洞,無法完整地提取運動目标。

②背景減除法

基本思想是利用背景的參數模型來近似背景圖像的像素值,将目前幀與背景圖像進行差分比較實作對運動區域的檢測,其中差別較大的像素區域被認為是運動區域,而差別較小的像素區域被認為是背景區域。

背景減除法必須要有背景圖像,并且背景圖像必須是随着光照或外部環境的變化而實時更新的,是以背景減除法的關鍵是背景模組化及其更新。針對如何建立對于不同場景的動态變化均具有自适應性的背景模型,減少動态場景變化對運動分割的影響,研究人員已提出了許多背景模組化算法,但總的來講可以概括為非回歸遞推和回歸遞推兩類。非回歸背景模組化算法是動态的利用從某一時刻開始到目前一段時間記憶體儲的新近觀測資料作為樣本來進行背景模組化。非回歸背景模組化方法有最簡單的幀間差分、中值濾波方法、Toyama等利用緩存的樣本像素來估計背景模型的線性濾波器、Elg~al等提出的利用一段時間的曆史資料來計算背景像素密度的非參數模型等。回歸算法在背景估計中無需維持儲存背景估計幀的緩沖區,它們是通過回歸的方式基于輸入的每一幀圖像來更新某個時刻的背景模型。這類方法包括廣泛應用的線性卡爾曼濾波法、Stauffe:與Grimson提出的混合高斯模型等。

在opencv中有個BackgroundSubtractorMOG2函數,是以高斯混合模型為基礎的背景/前景分割算法,但算法隻實作了檢測部分。這個算法的一個特點是它為每一個像素選擇一個合适數目的高斯分布,其對由于亮度等發生變化引起的場景變化産生更好的适應。
           

混合高斯模型算法原理,不重複贅述:

https://download.csdn.net/download/m0_37407756/10733651

初步構思

首先已經實作了一個想法:由于drawcontours的填充特性,會對輪廓滿足條件的方塊進行塗色,一旦進入螢幕,白塊内原本是空的,然後就會迅速被填充,根據這個比較明顯的差異,我們可以以此為依據進行偵差法

該類算法對時間上連續的兩幀或三幀圖像進行差分運算,不同幀對應的像素點相減,判斷灰階差的絕對值,當絕對值超過一定門檻值時,即可判斷為運動目标,進而實作目标的檢測功能。

那麼對本題而言,可以采取以下的辦法:

在周遊視訊流的過程中,儲存上一幀,把這一幀和上一幀對比,我們要找的是這兩幀的差别,由于白塊移動的幅度,如果出現了新的白塊填充現象,那麼兩幀發生"白塊填充"的那個區域,就會出現大量的白色像素,那麼據此可以斷定出現了白塊然後cnt++,至于繼續讀取下一幀,這個新的白塊會不會被檢測到而cnt++呢,是不會的,這一點需要測試者在處理的時候進行列印輪廓區域,打擂找到門檻值,根據這個思路就可以初步寫出代碼。

需要考慮的問題有:如果是單純地作差,那麼如果說上一幀是被填充的,而下一幀的白塊是被劃走了的,那麼就會出現負像素值,我們要剔除這種像素點,直接将其設定為0即可。

通過測試代碼

#include <iostream>
#include <opencv2/opencv.hpp>
#include <algorithm>
using namespace std;
using namespace cv;
const int MAX = 0x3f3f3f3f;

int main()
{
	//首先讀取圖像:将圖像中的每一幀都讀出來
	VideoCapture capture ("E:/computer view/2021.9.4/???.mkv");
	Mat frame,last_frame;//擷取上一幀圖像
	capture.read(frame);

	//預處理
	last_frame = Mat::zeros(frame.size(), frame.type());
	cvtColor(last_frame, last_frame, COLOR_BGR2GRAY);
	int cnt = 0;
	int MIN = MAX;

	//目标:檢測目标輪廓内的顔色變換,如果 原本什麼都沒有後來出現了大面積的填充,那麼就證明出現了一個白塊
    //是以需要對比,需要儲存上一幀的圖像與這一幀圖像進行比較	

	while (!frame.empty())
	{
		//轉化為二值圖像便于處理
		Mat gray,canny,sub;//分别為初步處理的灰階圖像,Canny檢測後的圖像,作差後圖像
		cvtColor(frame,gray,COLOR_BGR2GRAY);
		//檢測邊緣,擷取方塊的形狀
		//1.Canny邊緣檢測
		Canny(gray, canny, 50, 150);//灰階值圖像進行Canny邊緣檢測
		//2.findContours的寫法
		std::vector<std::vector <Point> > contours;
		vector<Vec4i> hierarchy;
		cv::findContours(canny,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point(0,0));
		//設定單通道的畫布,用于描出基礎的輪廓
		Mat drawing = Mat::zeros(canny.size(), CV_8UC1);
		//設定單通道的畫布,用于放置作差處理後的圖像,初始均為0像素灰階值
		sub = Mat::zeros(canny.size(), CV_8UC1);
		//3.drawContours寫法
		for (int i = 0; i < contours.size(); i++)
		{
			Scalar color = Scalar(255, 255, 255);
			drawContours(drawing,contours,i,color,-1,8, hierarchy,0,Point());

			//這兩步用來調試的,找Area的門檻值
			//int Area = (int)(abs)(cv::contourArea(contours[i]));//如果是由暗轉明,那麼就一次		
			//cout << Area << endl;
		}
		//初步輪廓的顯示,調試
		imshow("drawing", drawing);
		//通過這一步過濾掉不需要的變化,隻保留我們想要的那個變化

		//4.周遊這一幀圖像和上一幀圖像
		//如果說這一幀圖像的像素點灰階值-上一幀圖像的像素點灰階值 是小于0的(原圖像隻有0或255)
		//證明:這一幀為0,上一幀為255,是那種滑塊從螢幕劃走的情況,不要了直接跳過處理
		//如果不是,那麼就是滑塊出現了,把作差處理畫布對應像素值設定為255
		for (int i = 0; i < drawing.rows; i++)
		{
			for (int j = 0; j < drawing.cols; j++)
			{
				if (drawing.at<uchar>(i, j) - last_frame.at<uchar>(i, j) <= 0)
				{
					continue;
				}
				sub.at<uchar>(i, j) = drawing.at<uchar>(i, j);
			}
		}
		//測試作差的處理的輸出圖像
		imshow("sub", sub);
		
		//5.畫作差圖像的輪廓
		std::vector<std::vector <Point> > contours_sub;
		vector<Vec4i> hierarchy_sub;
		cv::findContours(sub, contours_sub, hierarchy_sub, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
		Mat drawing_sub = Mat::zeros(canny.size(), CV_8UC1);

		//6.周遊作差圖像輪廓,算輪廓面積,把之前用到的MIN值用上來篩選
		for (int i = 0; i < contours_sub.size(); i++)
		{
			Scalar color = Scalar(255, 255, 255);
			drawContours(drawing_sub, contours_sub, i, color, -1, 8, hierarchy, 0, Point());
			int Area = (int)abs(cv::contourArea(contours_sub[i]));
			if (Area >= 10000)
			{
				MIN = std::min(Area, MIN);
				//cout << Area<<endl;
				cnt++;
			}
		}
		//觀察者c
		char c = waitKey(10);

		//設定疊代


		last_frame = drawing.clone();
		capture.read(frame);
		
		if (c == 27)
		{
			break;
		}
		if (c == '0')
		{
			waitKey(0);
		}
		
	}
	cout << MIN<<endl<<"檢測到方塊總數為:"<< cnt;
	return 0;
}
           

算法分析

這個方法實際上很穩定,算法很精準,但是非常耗時,對于本題所用的測試資料來說,12分鐘視訊需要計算7e次(every frame),那麼也就是說,對于視訊流的處理基本上沒有優化。

總之就是算法效率非常低下,進行了很多不必要的計算,有很多次的全圖像素操作,Canny->findcontours->drawcontours->for for這些無疑都會加長計算時間,(好像給老師跑,用顯示卡都跑了好久),最根本的原因就是大量的像素周遊問題。

那麼根據這個可以改進算法,是不是非要周遊所有像素呢?與同學交流之後認為他們的算法思想是比較先進的:設定一條檢測線,同樣的檢測這一幀和上一幀的像素,如果這條線出現了本來是白像素點,後面變成了黑像素點,那麼就是出現了白塊了,這樣避免了大量的周遊像素點的操作。據此可以進行改進,本文用來記錄學習過程,就不擴充代碼了。

繼續閱讀