摄像机采集的视频序列具有连续性的特点。如果场景内没有运动目标,则连续帧的变化很微弱,如果存在运动目标,则连续的帧和帧之间会有明显地变化。
帧间差分法(Temporal Difference)就是借鉴了上述思想。由于场景中的目标在运动,目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧或三帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能。
对于前后视频中两帧图片,运动的目标区域会有相应变化:
转换成二值图来看:
将两个二值图作差,可以看到运动目标区域为白色:
或者直接将两帧RGB图片作差,差值大于一定阙值的区域则标注为运动区域:
这样的话,我们只需要检测到视频中相邻两帧图片之间像素有较大变化的区域,就可以实现一个简单的运动物体检测;
当然这里还有许多问题需要解决
下面会一一讲解:
帧差法:
其中video_index是摄像头编号,一般前置摄像头为0,USB摄像头为1或2:
# open_camera.py
import cv2
def catch_video(name='my_video', video_index=0):
# cv2.namedWindow(name)
cap = cv2.VideoCapture(video_index) # 创建摄像头识别类
if not cap.isOpened():
# 如果没有检测到摄像头,报错
raise Exception('Check if the camera is on.')
while cap.isOpened():
catch, frame = cap.read() # 读取每一帧图片
# ————————————————————————————————
# 处理图片
# ————————————————————————————————
cv2.imshow(name, frame) # 在window上显示图片
key = cv2.waitKey(10)
if key & 0xFF == ord('q'):
# 按q退出
break
if cv2.getWindowProperty(name, cv2.WND_PROP_AUTOSIZE) < 1:
# 点x退出
break
# 释放摄像头
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
catch_video()
在程序运行期间,opencv会不断读取摄像头输入的每一帧图片,再将图片进行一定处理,不断显示在window上:
这里使用了read方法,返回的frame就是某一时刻视频的图片。
对读取到的每一帧图片,我们首先转换成灰度图:
catch, frame = cap.read() # 读取每一帧图片
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转换成灰度图
然后我们计算视频中这一帧图片与上一帧图片像素的绝对值差:
gray_diff = cv2.absdiff(gray, previous) # 计算绝对值差
# previous 是上一帧图片的灰度图
其中黑色区域是由于像素相同得0,灰色区域越接近白色,表示前后两帧图片该点的像素差越大。
再将帧差图进行二值化,即图片像素差大于某一阙值(我们这里定为40)的标注为运动点,赋值为255(白色),其余点赋值为0(黑色):
_, mask = cv2.threshold(
gray_diff, 40, 255, cv2.THRESH_BINARY)
得到的二值化图为:
这时候我们虽然得到了帧差图的二值化图,但图中有许多干扰是我们不想要的:
这时候我们就需要通过一定的方法去噪;
mask = cv2.medianBlur(mask, 3) # 中值滤波
中值滤波是一种图像平滑处理算法,基本原理就是,测试像素周围邻域像素集中的中值代替原像素,能够有效去除孤立的噪声点或较细的噪声线;
使用中值滤波前后对比:
可以看到一部分噪声已经被去除了,图像也变得更加平滑。
腐蚀和膨胀具体原理可以看一下这篇博客:
数字图像处理(六)形态学处理之腐蚀、膨胀、开运算、闭运算
腐蚀和膨胀也是图片平滑处理的一种算法,一般先腐蚀再膨胀能够有效去除干扰线:
es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 4))
m = cv2.erode(median, es, iterations=2) # 腐蚀
m = cv2.dilate(m, es, iterations=2) # 膨胀
完整代码:
import cv2
import numpy as np
import matplotlib.pyplot as plt
class Detector(object):
def __init__(self, name='my_video'):
self.name = name
self.threshold = 40
self.es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 4))
def catch_video(self, video_index=0):
# cv2.namedWindow(name)
cap = cv2.VideoCapture(video_index) # 创建摄像头识别类
if not cap.isOpened():
# 如果没有检测到摄像头,报错
raise Exception('Check if the camera is on.')
frame_num = 0
while cap.isOpened():
catch, frame = cap.read() # 读取每一帧图片
if not catch:
raise Exception('Error.')
if not frame_num:
# 这里是由于第一帧图片没有前一帧
previous = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转换成灰度图
gray = cv2.absdiff(gray, previous) # 计算绝对值差
gray = cv2.medianBlur(gray, 3) # 中值滤波
ret, mask = cv2.threshold(
gray, self.threshold, 255, cv2.THRESH_BINARY)
mask = cv2.erode(mask, self.es, iterations=1)
mask = cv2.dilate(mask, self.es, iterations=1)
cv2.imshow(self.name, frame) # 在window上显示图片
cv2.imshow(self.name+'_frame', mask) # 边界
previous = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame_num += 1
key = cv2.waitKey(10)
if key & 0xFF == ord('q'):
# 按q退出
break
if cv2.getWindowProperty(self.name, cv2.WND_PROP_AUTOSIZE) < 1:
# 点x退出
break
if cv2.getWindowProperty(self.name+'_frame', cv2.WND_PROP_AUTOSIZE) < 1:
# 点x退出
break
# 释放摄像头
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
detector = Detector()
detector.catch_video()
效果如图
这里使用cv2.findContours()函数来查找检测运动目标的轮廓,并标注在原图上:
_, cnts, _ = cv2.findContours(
mask.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_TC89_L1)
for c in cnts:
if cv2.contourArea(c) < min_area:
continue
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.findContours()参数:
当然这里会有重叠框,我们使用非极大值抑制进行筛选;
非极大值抑制的作用是去除相同目标的重叠的多余的轮廓框,只保留得分最大的那个,例如:
# nms.py
import numpy as np
def py_cpu_nms(dets, thresh):
y1 = dets[:, 1]
x1 = dets[:, 0]
y2 = y1 + dets[:, 3]
x2 = x1 + dets[:, 2]
scores = dets[:, 4] # bbox打分
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
# 打分从大到小排列,取index
order = scores.argsort()[::-1]
# keep为最后保留的边框
keep = []
while order.size > 0:
# order[0]是当前分数最大的窗口,肯定保留
i = order[0]
keep.append(i)
# 计算窗口i与其他所有窗口的交叠部分的面积
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
# 交/并得到iou值
ovr = inter / (areas[i] + areas[order[1:]] - inter)
# inds为所有与窗口i的iou值小于threshold值的窗口的index,其他窗口此次都被窗口i吸收
inds = np.where(ovr <= thresh)[0]
# order里面只保留与窗口i交叠面积小于threshold的那些窗口,由于ovr长度比order长度少1(不包含i),所以inds+1对应到保留的窗口
order = order[inds + 1]
return keep
# frame_nms.py
import cv2
import numpy as np
from nms import py_cpu_nms
from time import sleep
class Detector(object):
def __init__(self, name='my_video', frame_num=10, k_size=7):
self.name = name
self.nms_threshold = 0.3
self.time = 1/frame_num
self.es = cv2.getStructuringElement(
cv2.MORPH_ELLIPSE, (k_size, k_size))
def catch_video(self, video_index=0, k_size=7,
iterations=3, threshold=20, bias_num=1,
min_area=360, show_test=True, enhance=True):
# video_index:摄像头索引或者视频路径
# k_size:中值滤波的滤波器大小
# iteration:腐蚀+膨胀的次数
# threshold:二值化阙值
# bias_num:计算帧差图时的帧数差
# min_area:目标的最小面积
# show_test:是否显示二值化图片
if not bias_num > 0:
raise Exception('bias_num must > 0')
if isinstance(video_index, str):
is_camera = False
else:
is_camera = True
cap = cv2.VideoCapture(video_index) # 创建摄像头识别类
if not cap.isOpened():
# 如果没有检测到摄像头,报错
raise Exception('Check if the camera is on.')
frame_num = 0
previous = []
while cap.isOpened():
catch, frame = cap.read() # 读取每一帧图片
if not catch:
raise Exception('Unexpected Error.')
if frame_num < bias_num:
value = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
previous.append(value)
frame_num += 1
raw = frame.copy()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.absdiff(gray, previous[0])
gray = cv2.medianBlur(gray, k_size)
ret, mask = cv2.threshold(
gray, threshold, 255, cv2.THRESH_BINARY)
if enhance:
mask = cv2.dilate(mask, self.es, iterations)
mask = cv2.erode(mask, self.es, iterations)
_, cnts, _ = cv2.findContours(
mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
bounds = self.nms_cnts(cnts, mask, min_area)
for b in bounds:
x, y, w, h = b
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
if not is_camera:
sleep(self.time)
cv2.imshow(self.name, frame) # 在window上显示图片
if show_test:
cv2.imshow(self.name+'_frame', mask) # 边界
value = cv2.cvtColor(raw, cv2.COLOR_BGR2GRAY)
previous = self.pop(previous, value)
cv2.waitKey(10)
if cv2.getWindowProperty(self.name, cv2.WND_PROP_AUTOSIZE) < 1:
# 点x退出
break
if show_test and cv2.getWindowProperty(self.name+'_frame', cv2.WND_PROP_AUTOSIZE) < 1:
# 点x退出
break
# 释放摄像头
cap.release()
cv2.destroyAllWindows()
def nms_cnts(self, cnts, mask, min_area):
bounds = [cv2.boundingRect(
c) for c in cnts if cv2.contourArea(c) > min_area]
if len(bounds) == 0:
return []
scores = [self.calculate(b, mask) for b in bounds]
bounds = np.array(bounds)
scores = np.expand_dims(np.array(scores), axis=-1)
keep = py_cpu_nms(np.hstack([bounds, scores]), self.nms_threshold)
return bounds[keep]
def calculate(self, bound, mask):
x, y, w, h = bound
area = mask[y:y+h, x:x+w]
pos = area > 0 + 0
score = np.sum(pos)/(w*h)
return score
def pop(self, l, value):
l.pop(0)
l.append(value)
return l
if __name__ == "__main__":
detector = Detector()
detector.catch_video(0, bias_num=2, iterations=3,
k_size=5, show_test=True, enhance=False) # 第一个参数可以是数字(表示打开摄像头)也可以是视频路径地址
最终效果: