在许多计算机视觉应用程序中,处理能力很低。在这种情况下,我们必须使用简单而有效的技术。
在这篇文章中,我们将介绍一种这样的技术,用于估计场景背景时,相机是静态的,场景中有一些移动的物体。这种情况并不少见。例如,许多交通和监控摄像头都是固定的。
为了理解我们将在这篇文章中描述的想法,让我们考虑一个一维的更简单的问题。
假设我们每10毫秒估计一个量(比如房间的温度)。
假设房间的温度是70华氏度。
在上图中,我们展示了两个温度计的测量结果——一个好的温度计和一个坏的温度计。
好的温度计显示在左边,报告70度与一些噪声。为了得到更准确的温度估计值,我们可以简单地在几秒钟内将数值平均。由于噪声是正的和负的高斯值,平均值将抵消噪声。实际上,在这种特定情况下的平均值是70.01。
另一方面,坏温度计在大多数时候表现得和好的温度计一样,但偶尔,数字是完全错误的。
事实上,如果我们把不好的温度计所记录的数字平均一下,我们得到71.07度。这显然是高估了。
我们还能很好地估计温度吗?
答案是肯定的。当数据包含异常值时,中值是我们试图估计的值的一个更稳健的估计值。
中值是按升序或降序排序时数据的中值。
上面显示的曲线的中位数是70.05度,比71.07度的估计值要好得多。
唯一的缺点是,与平均值/平均值相比,中位数的计算成本更高。
现在,让我们回到相机静态时估计背景的问题。
我们可以假设大多数时候,每个像素看到的是相同的背景,因为相机没有移动。偶尔,一辆汽车或其他移动的物体出现在前面,模糊了背景。
对于一个视频序列,我们可以随机采样一些帧(比如25帧)。
换句话说,对于每个像素,我们现在有25个背景的估计。只要一个像素超过50%的时间没有被汽车或其他移动物体覆盖,那么在这25帧中像素的中值就可以很好地估计出该像素上的背景。
我们可以对每个像素重复这个操作,恢复整个背景。
(1)Python
import numpy as np
import cv2
from skimage import data, filters
# 打开视频
cap = cv2.VideoCapture('video.mp4')
# 随机选择25帧
frameIds = cap.get(cv2.CAP_PROP_FRAME_COUNT) * np.random.uniform(size=25)
# 将选定的帧存储在数组中
frames = []
for fid in frameIds:
cap.set(cv2.CAP_PROP_POS_FRAMES, fid)
ret, frame = cap.read()
frames.append(frame)
# 计算沿时间轴的中值
medianFrame = np.median(frames, axis=0).astype(dtype=np.uint8)
# 显示中值帧
cv2.imshow('frame', medianFrame)
cv2.waitKey(0)
(2)C++
#include
#include
#include
using namespace std;
using namespace cv;
int computeMedian(vector<int> elements)
{
nth_element(elements.begin(), elements.begin()+elements.size()/2, elements.end());
//sort(elements.begin(),elements.end());
return elements[elements.size()/2];
}
cv::Mat compute_median(std::vector<cv::Mat> vec)
{
// 注意:期望图片是CV_8UC3
cv::Mat medianImg(vec[0].rows, vec[0].cols, CV_8UC3, cv::Scalar(0, 0, 0));
for(int row=0; row<vec[0].rows; row++)
{
for(int col=0; col<vec[0].cols; col++)
{
std::vector<int> elements_B;
std::vector<int> elements_G;
std::vector<int> elements_R;
for(int imgNumber=0; imgNumber<vec.size(); imgNumber++)
{
int B = vec[imgNumber].at<cv::Vec3b>(row, col)[0];
int G = vec[imgNumber].at<cv::Vec3b>(row, col)[1];
int R = vec[imgNumber].at<cv::Vec3b>(row, col)[2];
elements_B.push_back(B);
elements_G.push_back(G);
elements_R.push_back(R);
}
medianImg.at<cv::Vec3b>(row, col)[0]= computeMedian(elements_B);
medianImg.at<cv::Vec3b>(row, col)[1]= computeMedian(elements_G);
medianImg.at<cv::Vec3b>(row, col)[2]= computeMedian(elements_R);
}
}
return medianImg;
}
int main(int argc, char const *argv[])
{
std::string video_file;
// 读取视频文件
if(argc > 1)
{
video_file = argv[1];
} else
{
video_file = "video.mp4";
}
VideoCapture cap(video_file);
if(!cap.isOpened())
cerr << "Error opening video file\n";
// 随机选择25帧
default_random_engine generator;
uniform_int_distribution<int>distribution(0,
cap.get(CAP_PROP_FRAME_COUNT));
vector<Mat> frames;
Mat frame;
for(int i=0; i<25; i++)
{
int fid = distribution(generator);
cap.set(CAP_PROP_POS_FRAMES, fid);
Mat frame;
cap >> frame;
if(frame.empty())
continue;
frames.push_back(frame);
}
// 计算沿时间轴的中值
Mat medianFrame = compute_median(frames);
// 显示
imshow("frame", medianFrame);
waitKey(0);
}
如你所见,我们随机选择25帧并计算25帧中每个像素的中位数。只要每个像素至少有50%的时间看到背景,这个中值帧就是对背景的一个很好的估计。
下一个显而易见的问题是,我们是否可以为每一帧创建一个mask,以显示在运动中的图像部分。
这可以通过以下步骤完成:
import numpy as np
import cv2
from skimage import data, filters
# 读取视频
cap = cv2.VideoCapture('video.mp4')
# 随之选择25帧
frameIds = cap.get(cv2.CAP_PROP_FRAME_COUNT) * np.random.uniform(size=25)
# 将选定的帧存储在数组中
frames = []
for fid in frameIds:
cap.set(cv2.CAP_PROP_POS_FRAMES, fid)
ret, frame = cap.read()
frames.append(frame)
# 计算沿时间轴的中值
medianFrame = np.median(frames, axis=0).astype(dtype=np.uint8)
# 显示中值帧
cv2.imshow('frame', medianFrame)
cv2.waitKey(0)
# 重置帧号为0
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
# 转换背景到灰度
grayMedianFrame = cv2.cvtColor(medianFrame, cv2.COLOR_BGR2GRAY)
# 循环所有帧
ret = True
while(ret):
# 读取帧
ret, frame = cap.read()
# 将当前帧转换为灰度
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 计算当前帧和中间帧的绝对差值
dframe = cv2.absdiff(frame, grayMedianFrame)
# 二值化
th, dframe = cv2.threshold(dframe, 30, 255, cv2.THRESH_BINARY)
# 显示
cv2.imshow('frame', dframe)
cv2.waitKey(20)
# 释放视频对象
cap.release()
# 关闭所有窗口
cv2.destroyAllWindows()
(2)C++
#include
#include
#include
using namespace std;
using namespace cv;
int computeMedian(vector<int> elements) {
nth_element(elements.begin(), elements.begin()+elements.size()/2, elements.end());
//sort(elements.begin(),elements.end());
return elements[elements.size()/2];
}
cv::Mat compute_median(std::vector<cv::Mat> vec) {
// 图像为CV_8UC3
cv::Mat medianImg(vec[0].rows, vec[0].cols, CV_8UC3, cv::Scalar(0, 0, 0));
for(int row=0; row<vec[0].rows; row++) {
for(int col=0; col<vec[0].cols; col++) {
std::vector<int> elements_B;
std::vector<int> elements_G;
std::vector<int> elements_R;
for(int imgNumber=0; imgNumber<vec.size(); imgNumber++) {
int B = vec[imgNumber].at<cv::Vec3b>(row, col)[0];
int G = vec[imgNumber].at<cv::Vec3b>(row, col)[1];
int R = vec[imgNumber].at<cv::Vec3b>(row, col)[2];
elements_B.push_back(B);
elements_G.push_back(G);
elements_R.push_back(R);
}
medianImg.at<cv::Vec3b>(row, col)[0] = computeMedian(elements_B);
medianImg.at<cv::Vec3b>(row, col)[1] = computeMedian(elements_G);
medianImg.at<cv::Vec3b>(row, col)[2] = computeMedian(elements_R);
}
}
return medianImg;
}
int main(int argc, char const *argv[])
{
std::string video_file;
// 读取视频文件
if(argc > 1) {
video_file = argv[1];
} else {
video_file = "video.mp4";
}
VideoCapture cap(video_file);
if(!cap.isOpened())
cerr << "Error opening video file\n";
// 随机选择25帧
default_random_engine generator;
uniform_int_distribution<int>distribution(0, cap.get(CAP_PROP_FRAME_COUNT));
vector<Mat> frames;
Mat frame;
for(int i=0; i<25; i++) {
int fid = distribution(generator);
cap.set(CAP_PROP_POS_FRAMES, fid);
Mat frame;
cap >> frame;
if(frame.empty())
continue;
frames.push_back(frame);
}
// 计算沿时间轴的中值
Mat medianFrame = compute_median(frames);
// 显示帧中值
imshow("frame", medianFrame);
waitKey(0);
// 重置帧号为0
cap.set(CAP_PROP_POS_FRAMES, 0);
// 转换背景到灰度
Mat grayMedianFrame;
cvtColor(medianFrame, grayMedianFrame, COLOR_BGR2GRAY);
// 循环所有帧
while(1) {
// 读取帧
cap >> frame;
if (frame.empty())
break;
// 当前帧转换为灰度
cvtColor(frame, frame, COLOR_BGR2GRAY);
// 计算当前帧和中值帧的绝对差值
Mat dframe;
absdiff(frame, grayMedianFrame, dframe);
// 二值化
threshold(dframe, dframe, 30, 255, THRESH_BINARY);
// 显示
imshow("frame", dframe);
waitKey(20);
}
cap.release();
return 0;
}
Demo视频地址链接:https://pan.baidu.com/s/1Es80KMhz3Cg95XPMZHjgnQ 提取码:123a
https://learnopencv.com/simple-background-estimation-in-videos-using-opencv-c-python/