视频分析
视频是含有大量时序关系的图像集合 对视频的处理 可以 结合时序关系
挖掘到更深层的信息 例如 判断视频相机是否移动 判断场景中是否存在
移动的物体 确定场景中物体的三维信息
本文将重点介绍如何检测视频中的移动的物体 并且对该物体进行跟踪
差值法检测移动的物体
大多数摄像头应用的场景是 摄像头不动 背景不动 而运动物体在背景中
所以计算 当前图像 和 背景的差值 就可以判断哪些是在移动的
可以计算所有的帧和背景图像 也可以 计算相邻帧差值
可以将图像转为灰度图后在进行计算
但是很容易受到光照,噪声的影响
所以 在计算差值后 需要进一步处理
如 二值化 开闭运算
opencv提供了 保留差值绝对值的函数
dst = cv.absdiff(src1,src2)
和初始背景的差值:
import cv2 as cv
if __name__ == '__main__':
video_obj = cv.VideoCapture(0)
_, begin_img = video_obj.read()
begin_img = cv.cvtColor(begin_img, cv.COLOR_BGR2GRAY)
begin_img = cv.GaussianBlur(begin_img, (5, 5), 10, 10)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5))
while video_obj.isOpened():
_, img = video_obj.read()
gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
pro_img = cv.GaussianBlur(gray_img, (5, 5), 10, 10)
# 拿到和背景图的差值 并且二值化 两处有变化
res1 = cv.absdiff(gray_img, begin_img)
_, res1 = cv.threshold(res1, 50, 255, cv.THRESH_BINARY)
# 开运算 先腐蚀 再膨胀
res1 = cv.morphologyEx(res1, cv.MORPH_OPEN, kernel)
cv.imshow('diff', res1)
if cv.waitKey(1000 // 60) == ord('q'):
break
由于背景的固定 所以 仅仅会出现一个区域的差值
由于是相邻的计算 所以会出现前后两部分的差值
相邻帧差值;
begin_img = pro_img.copy()
差值法缺陷:
背景 摄像头不动的情况下 可以检测到运动的物体
但是 当背景运动时 就不行了
均值迁移法目标跟踪
根据差值法来检查移动的物体 要求物体移动时背景不能发生变化 一旦物体移动时背景也发生移动
那么差值法不能检测到移动的物体。
但是有时候 我们不但需要检测移动的物体 而且还需要跟踪这个物体 无论这个物体是静止还是移动的
我们都可以直观的表示它在图像中的位置 进而分析其运动轨迹和运动状态
均值迁移法:
可以实现目标的跟踪 其中的原理是首先计算给定区域内的均值 如果均值不符合最优值条件
就会将区域向靠近最优条件方向移动 经过不断地迭代来找到目标区域
均值迁移法 又被叫做爬山算法 与寻找点密集度最大区域相同
如果将点密集看为一座山的高度
随机选择一个圆形区域 计算圆形区域内整体的平均高度 然后计算扇形区域的平均高度
然后每次都向着山峰处移动 直到圆心在山峰处
根据均值迁移法 我们要选择一个搜索区域 结合前面的直方图反向投影
利用均值迁移法 我们需要首先知道目标区域的直方图反向投影 之后不断迭代求值
步骤如下:
1. 选择需要跟踪的目标区域 人为选取 ROI
2. 选择目标区域的直方图和直方图反向投影 作为搜索图像
3. 在图像中 给出初始的目标区域 计算区域的均值
4. 比较区域均值是否满足阈值 如果没有 区域将向接近目标的方向移动
重复3 4 知道满足阈值
Opencv 提供了3 4 步的函数
retval,window = cv.meanShift(probImg,window,criteria)
probImg: 目标区域的直方图的反向投影
window: 初始搜索和结束时的窗口
criteria 迭代停止条件
迭代条件 :
1. cv.TERM_CRITERIA_COUNT 迭代次数
2. cv.TERM_CRITERIA_EPS 迭代精度
函数根据目标区域的直方图的反向投影 和 区域的初始位置 搜索目标区域在新图像中的位置
将是否搜索到目标 True/False 返还
其中窗口位置 以(x,y,w,h) 保存在元组
使用均值迁移法需要指定跟踪的目标区域 我们根据物体特性计算 或者 人为选取目标区域
opencv提供了通过鼠标来选取目标区域的cv.selectROI() 函数
retval = cv.selectROI(windowName,img,是否用十字线显示中心)
该函数鼠标左键框选区域 返还(x,y,w,h)
通过均值迁移法寻找目标就是 通过在ROI周围寻找与目标最相似的区域 该方法不仅可以跟踪动态目标
还可以跟踪静止目标
import cv2 as cv
import sys
if __name__ == '__main__':
cap = cv.VideoCapture(0)
if not cap.isOpened():
print('视频开启失败')
sys.exit()
# 读第一张图片
_, img = cap.read()
# 拿到感兴趣区域
x, y, w, h = cv.selectROI('get_ROI', img, True, False)
track_window = (x, y, w, h)
# 得到坐标
roi = img[y:y + h, x:x + w]
#换为HSV 减少光照影响
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 感兴趣区域的直方图
cv.imshow('hsv_roi', hsv_roi)
# 转为直方图 H范围是[0,180] 将其分为180份
roi_hist = cv.calcHist([hsv_roi], [0], None, [180], [0, 180])
# 所谓归一化 就是放缩 将数据映射到一定的范围内 minmax方法 将数据映射到 给出的[min,max]
roi_hist = cv.normalize(roi_hist, None, 0, 180, cv.NORM_MINMAX)
# 迭代停止条件
criteria = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 100, 1)
while True:
# 读取一张图片
_, frame = cap.read()
# 转为HSV
obj_hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
# 直方图反射,返还一张图像 可以用来判断 是否存在某特征 并且使其突出
# 将特征部分 按 直方图 突出 返还的是图片
obj_hist = cv.calcBackProject([obj_hsv], [0], roi_hist, [0, 180], 1)
cv.imshow('hist', obj_hist)
# obj_hist 返还的是一张灰度图
# 会根据像素点 在模板 直方图中的比例 来进行转换
# 结果就是让符合的特征值更大
# 运用均值迁移法 得到特征部分 及拿到obj_hist的track_window部分
# 求均值 将track_window 向尽可能大的地方靠拢
ret, track_window = cv.meanShift(obj_hist, track_window, criteria)
# 将迭代的结果 作为新的位置 及物体当前帧的位置,下次继续向新位置靠拢
x, y, w, h = track_window
# print(track_window)
cv.rectangle(frame, (x, y), (x + w, y + h), (255, 255, 255), 2)
cv.imshow('aim', frame)
if cv.waitKey(50) == ord('q'):
break
cap.release()
cv.destroyAllWindows()
分析:
对于颜色不明显的 效果非常差
并且有可能会丢失掉目标 如果目标消失 再出现
可能在有限的迭代次数中 不能到达
通过自适应均值迁移法实现目标跟踪:
通过均值迁移法可以实现目标跟踪 但是该方法有一个很大的缺点
无法根据目标的状态改变目标区域的大小 例如 物体
在镜头较近的情况下 较大
利用均值迁移法对目标进行跟踪时 无论物体的远近
目标区域都是初始确定的尺寸 当物体较远
图像中跟踪结果的目标区域内有较多的物体 不利于后续处理
自适应均值迁移法进行了改进
可以根据跟踪目标的大小自动调整搜索窗口的大小
除此之外 改进的均值迁移法不仅可以返还目标的位置
还可以返回角度信息
函数:
retval,window = cv.CamShift(pI,window,criteria)
pI 反向投影
window 窗口
该函数可以返回目标中心在图像的位置 目标大小 目标方向
window和cv.meanShift() 相同
retval中保存了结果的中心位置
目标大小 目标方向信息
((x,y),(axes1,axes2),angle)
# 计算特征图像中 感兴趣坐标的
ret, track_window = cv.CamShift(obj_hist, track_window, criteria)
x, y, w, h = track_window
cv.rectangle(frame, (x, y), (x + w, y + h), (255, 255, 255), 2)
xp, yp = map(int,ret[0])
cv.circle(frame, (xp,yp), 5, (0, 0, 255), 5)
cv.imshow('aim', frame)
分析:
效果非常好 特别对颜色有对比度的物体
光流法目标跟踪
光流是空间运动物体在成像图像平面上每个像素移动的瞬时速度 在较短时间间隔可以等于像素得位移
在忽略光照变化的前提下 光流主要是由场景中得目标移动 相机移动 或者 两者共同运动产生
光流代表了图像得变化 由于它包含了目标的运动信息 因此可以杯观察者来确定目标的运动情况
实现目标的跟踪
光流法是利用图像序列中像素的变化来寻找前一帧图像和当前帧图像间的对应关系
进而得到两种图像间物体运动状态的一种方法
光流法的两个严格假设:
第一 同一个物体在图像中对应的像素亮度不变
第二 要求两帧图像必须具有较小运动
这两个条件极大的限制了光流法的应用范围
由于光流法要求像素的移动距离较小 但是有时连续图像的移动距离较大 因此需要采用图像金字塔
来解决这个问题
(200,200)的移动(4,4)
在(100,100)的移动就仅有(2,2)
根据计算光流速度的像素数目 光流法可以分为稠密光流法 和 稀疏光流法
稠密光流法是 计算光流时图像中全部的像素均使用
稀疏光流法是 仅使用部分像素 例如Harris角点
Farneback 稠密光流法
该方法计算全部的像素的运动速度
flow = cv.calcOpticalFlowFarneback(prev,next,None,pyr_scale,levels,winsize,iterations,poly_n,poly_sigma,flags)
flow 双通道图像 分别存储了x和y方向的光流速度
pyr_scale: 图像金字塔的比例 小于一
levels: 图像金字塔层数 为1 表示不构建金字塔
winsize: 越大 鲁棒性(Robust)越好 为快速运动提高更好的检测机会 但是会参数更模糊的光流运动场
iterations: 在每个金字塔的迭代次数
poly_n: 邻域大小 越大越光滑 但是更模糊 一般为 5,7
poly_sigma: 高斯标准差 用于平滑导数 一般为1 -1.5 当poly_n=5 poly_sigma = 1.1
flags: 计算标准位
cv.OPTFLOW_USE_INITAL_FLOW输入流作为初始流的近似值
cv.OPTFLOW_FARNEBACK_GAUSSIAN 使用高斯滤波器来代替方框滤波器 更准确但更慢
如果相机移动 会导致全部的像素均移动 无法追踪目标 所以 稠密光流法通常用于相机固定的情况下
因为上式提供的是一个x和y的速度 我们通常用其向量和来代表最终结果
magnitude,angle = cv.cartToPolar(x,y)
效果很不好 因为假定了亮度的不变性 但是摄像头的实际亮度在改变
该程序将光流映射到到了一个HSV视图里面 其中颜色代表了光流的方向即像素的方向 而亮度代表了光流的速度
import cv2 as cv
import numpy as np
import sys
if __name__ == '__main__':
cap = cv.VideoCapture(0)
# 读取图片 并且将其转为灰度图像
_,pre_img = cap.read()
pre_img = cv.cvtColor(pre_img,cv.COLOR_BGR2GRAY)
# 创建个存储光流的图像
res_hsv = np.zeros_like(hsv_img)
res_hsv[..., 1] = 255
# 打开摄像头
while cap.isOpened():
# 再读入图片 与最开始一张进行比较
_,next_img = cap.read()
next_img = cv.cvtColor(next_img,cv.COLOR_BGR2GRAY)
# 得到光流图像
flow = cv.calcOpticalFlowFarneback(pre_img,next_img,None,0.5,3,15,3,5,1.2,0)
# 得到向量
mag,ang = cv.cartToPolar(flow[...,0],flow[...,1])
# 将角度转为色彩
res_hsv[...,0] = ang*180 / np.pi /2
# 将大小转为亮度
res_hsv[...,2] = cv.normalize(mag,None,0,255,cv.NORM_MINMAX)
res = cv.cvtColor(res_hsv,cv.COLOR_HSV2BGR)
cv.imshow('old',next_img)
cv.imshow('flow',res)
pre_img = next_img
if cv.waitKey(1000//60) == ord('q'):
break
cap.release()
cv.destroyAllWindows()
物体不移动的时候
物体移动时 颜色代表物体移动的方向 亮度代表了速度
LK稀疏光流法
稠密光流法考虑到了图像中所有的像素信息 但是巨大的信息量导致了程序的运行速度很慢
很难实现实时追踪
我们有时候仅仅需要关注部分区域的光流特性
nextPts,status,err = cv.calcOptcicalFlowPyrLK(primg,nextimg,prevPts,nextPts,status,winsize,maxLevel,criteria,flags,minEigThreshold)
该函数通过迭代实现LK稀疏光流法
前两个参数是图像
prevPts和nextPts 是单精度浮点数
status: 输出状态向量 当寻找到匹配光流点 返还1 否则返还0
err: 对应点误差
winsize 有默认值(21,21)
flags: cv.OPTFLOW_USE_INITIAL_FLOW 使用初始估计
cv.OPTFLOW_LK_GET_MIN_EIGENVALS 表示使用最小的特征值作为误差测量标准 当小于最后一个参数 算光流点丢失
该函数需要人为的输入图像中稀疏光流点的坐标 一般情况下可以为特征点或者角点
将特征点或者角点作为初始坐标输入
之后跟踪的过程中 不断根据匹配结果来更新光流点的数目和坐标
例如:
第一张图片得到100个角点 然后 利用 函数对第二帧进行检测
得到匹配的80个角点
然后再对第二帧 第三帧进行检测
但是 角点会逐渐减少 并且不变的区域角点不减少
所以需要设置阈值来再次检测角点 并且判断删除不移动角点
import cv2 as cv
import numpy as np
import sys
# 稀疏光流法 返还的是变换后对应的坐标
if __name__ == '__main__':
cap = cv.VideoCapture(0)
_,pre_frame = cap.read()
pre_gray = cv.cvtColor(pre_frame,cv.COLOR_BGR2GRAY)
# 拿到角点 作为特征点
points = cv.goodFeaturesToTrack(pre_gray,maxCorners=1000,qualityLevel=0.01,minDistance=10,blockSize=3,useHarrisDetector=False,k=0.04)
while cap.isOpened():
_,frame = cap.read()
frame_gray = cv.cvtColor(frame,cv.COLOR_BGR2GRAY)
# 迭代停止条件
criteria = (cv.TERM_CRITERIA_EPS|cv.TERM_CRITERIA_COUNT,30,0.01)
next_pts,status,err = cv.calcOpticalFlowPyrLK(pre_gray,frame_gray,points,None,winSize=(31,31),maxLevel=3,criteria = criteria,flags=0)
# 是否是匹配的光流点
# 在初始点 和 之后的点 过滤掉不符合的点
good_next = next_pts[status==1]
good_pre = points[status==1]
for i,(next_item,pre_item) in enumerate(zip(good_next,good_pre)):
# 打成一维
a,b = next_item.ravel()
c,d = pre_item.ravel()
dist = abs(a-c) + abs(b -d)
# 过滤掉不移动 或 移动幅度较小的角点
if dist > 2:
frame = cv.circle(frame,(a,b),3,(0,255,0),-1,8)
frame = cv.line(frame,(a,b),(c,d),(0,0,255),2,8,0)
cv.imshow('res',frame)
if cv.waitKey(1000//60) == ord('q'):
break
pre_gray = frame_gray
points = good_next.reshape(-1,1,2)
cap.release()
cv.destroyAllWindows()