高空抛物监测
本实践为AidLux智慧社区训练营学习总结,感谢授课讲师张大刀。
参考链接:
https://blog.csdn.net/qq_32214321/article/details/107912440
https://zhuanlan.zhihu.com/p/398944211
直接从技术路线开始
高空抛物识别难点
- 高空抛物相机一般以仰视的角度,往住宅楼拍摄,抛出物体相对于整个楼栋,目标太小;
- 干扰因素较多,如白天的飞鸟、飘落的树叶、夜晚的背景楼栋灯光等;
- 环境影响如雨天、雾天、逆光等环境对结果影响较大。
数据采集
数据采集模块的难点在于相机的位置和角度的安装,对于不同的层高,建议的安装距离和分辨率选择对应的相机焦距等可以参考如下:高空抛物的相机可以查看主流一线安防厂家海康,大华都有类似的产品,选型需要考虑:
- 夜间低照度,小区夜间光照不足,不建议通过补光的方式来提高环境照度,这样的环境下可以选型超星光摄像头,最低支持0.0002lux,可以有效保障夜间环境下的监控。
- 白天高空抛物摄像头对着天空,白天有强烈的太阳光会造成逆光现象,可以选型摄像头支持120db的宽动态摄像头,同时也支持背光补偿,在光线复杂和强烈的环境下,仍然能显示好的图像效果。
算法设计
高空抛物识别
- 使用传统的动态目标检测,如光流检测和帧差法;
- 使用目标检测+目标追踪算法,对抛出的物体先做目标检测,并对检测到的物体做追踪判断;
- 使用物体追踪+过滤算法;
- 使用视频分类的算法。
对于第一种方法
- 传统方法的动态目标检测,如光流检测和帧差法,稳定性稍差。
- 优点在于对于数据要求低。
对于第二种方法,
- 使用目标检测检测被抛物体
- 通过目标追踪对抛出物体的运动轨迹做追踪
受背景的影响很大,因为楼宇间的灯光等,同时使用目标检测+目标追踪的方法,其难点在于小目标的检测,很容易出现漏检。同时运动的物体很多,如晒得被子等,容易出现误检,同时需要大量的数据。
对于第三种方法,
- 针对第二种方法中的目标检测算法的效果不佳,采用高斯背景建模的方法,过滤背景信息;
- 再使用目标追踪如kalman滤波,完成运动轨迹的记录,同时针对第二种方法中视频中会出现的树叶、飞鸟以及晒衣服等的摆动等不符合抛物运动的轨迹的误检,通过SOM网络进行聚类,SOM(自组织映射神经网络)会对不同运动的轨迹进行分析:
- 对运动轨迹进行分类,排除掉不符合抛物运动的轨迹:
另外可以对卡尔曼滤波追踪的前景图像块,做图像分类,做一次筛选,排除掉不是抛物的物体轨迹。
以上三种方法的识别逻辑是一样的,都是先做目标检测,再对目标检测的物体做追踪。
第二种方法相对于第一种方法,在目标检测环节采用的是深度学习方法。
第三种方法相对于第二种方法,在目标追踪后,针对运动的干扰项,通过SOM对运动轨迹过滤,通过图像分类对目标过滤;同时目标检测环节用的是传统动态目标检测,这里也可以换成第二种方法中的深度学习目标检测的方法(如果不考虑实时性)。
对于第四种方法
- 使用视频分类的方式,即对一段时间内的视频流做视频分类,这里可以通过第三步中先通过视频抽帧完成高斯背景建模,过滤掉背景后,再对前处理后的视频完成视频的分类。
- 在后处理时,对分类阈值的判断逻辑,以及对输入模型的视频时长等比较讲究,这种方法目前看在满足实时性和精确性时较为折中的一种方法。
算法实现
在本次训练营课程中,因为涉及到高空抛物数据集的缺乏,所以在上面的四种方法中,主要选择第一种方法。
实际在做项目有数据集支撑的情况下,建议选择第三种或者第四种方法。
第二种方法目标检测+追踪的方式,对上游任务目标检测的要求较高,实际情况下的小目标容易漏检和误检,不建议使用。
而针对于第一种的传统算法中,一般会有帧差法或者光流检测。
但是这都是最初级的方法,因为有许多局限性,比如帧差法对噪声敏感,无法避免对树叶的误检,在摄像头有轻微摇晃的情况下也会有很多误检,也无法适应光线变化等;
光流法也是相同的问题,而且光流法还有另外一个最大的问题是其基于稀疏特征点匹配的算法,因此实际上没有很好的办法将整张图的特征点分为不同的目标——虽然有稠密光流检测算法,但是耗时较长。基于此,我们可以将第三种方法中的“背景建模”加入第一种方法中。
去抖动
背景建模的前提是保证摄像机拍摄位置不变,保证背景是基本不发生变化的。
如路口的监控摄像机,只有车流人流等前景部分能发生移动,而马路树木等背景不能发生移动。所以这里我们防止相机镜头发生抖动,可以加上去抖动的算法,通过匹配算法实现
其主要的原理是通过每张图的特征,找到两个图片的关键点,并基于关键点获得变换矩阵后,将原图通过变换矩阵变换后,与第一张原始图片对齐。
#!/usr/bin/python# -*- coding:utf8 -*-# import the necessary packagesimport numpy as np
import cv2
import time
classAdjuster:def__init__(self, start_image, edge=(60,20)):# determine if we are using OpenCV v3.X
self.start_image = cv2.resize(start_image,(int(start_image.shape[1]/1),int(start_image.shape[0]/1)))
self.edge = edge
self.descriptor = cv2.ORB_create()
self.matcher = cv2.DescriptorMatcher_create("BruteForce")(self.kps, self.features)= self.detectAndDescribe(self.start_image)defdebouncing(self, image, ratio=0.7, reprojThresh=4.0, showMatches=False):
image = cv2.resize(image,(int(image.shape[1]/1),int(image.shape[0]/1)))# start = time.time()(kps, features)= self.detectAndDescribe(image)# print(f"take {time.time() - start} s")
M = self.matchKeypoints(kps, self.kps, features, self.features, ratio, reprojThresh)if M isNone:returnNone(matches, H, status)= M
result = cv2.warpPerspective(image, H,(image.shape[1]+ image.shape[1], image.shape[0]+ image.shape[0]))
result = result[int(self.edge[1]):int(image.shape[0]- self.edge[1]),int(self.edge[0]):int(image.shape[1]- self.edge[0])]# cv2.namedWindow("result", cv2.WINDOW_NORMAL)# cv2.imshow("result", result)
start_img = self.start_image[int(self.edge[1]):int(image.shape[0]- self.edge[1]),int(self.edge[0]):int(image.shape[1]- self.edge[0])]
sub_img = cv2.absdiff(result, start_img)# cv2.namedWindow("start_img", cv2.WINDOW_NORMAL)# cv2.imshow("start_img", start_img)# cv2.namedWindow("sub_img", cv2.WINDOW_NORMAL)# cv2.imshow("sub_img", sub_img)return result
defdetectAndDescribe(self, image):# convert the image to grayscaleiflen(image.shape)>2:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)else:
gray = image
# detect and extract features from the image(kps, features)= self.descriptor.detectAndCompute(image,None)# convert the keypoints from KeyPoint objects to NumPy# arrays
kps = np.float32([kp.pt for kp in kps])# return a tuple of keypoints and featuresreturn(kps, features)defmatchKeypoints(self, kpsA, kpsB, featuresA, featuresB,
ratio, reprojThresh):# compute the raw matches and initialize the list of actual# matches
rawMatches = self.matcher.knnMatch(featuresA, featuresB,2)
matches =[]# loop over the raw matchesfor m in rawMatches:# ensure the distance is within a certain ratio of each# other (i.e. Lowe's ratio test)iflen(m)==2and m[0].distance < m[1].distance * ratio:
matches.append((m[0].trainIdx, m[0].queryIdx))# computing a homography requires at least 4 matchesiflen(matches)>4:# construct the two sets of points
ptsA = np.float32([kpsA[i]for(_, i)in matches])
ptsB = np.float32([kpsB[i]for(i, _)in matches])# compute the homography between the two sets of points# ptsA = np.array([[0, 0], [100, 0], [100, 100], [0, 100]])# ptsB = np.array([[50, 50], [200, 50], [200, 200], [50, 200]])(H, status)= cv2.findHomography(ptsA, ptsB, cv2.RANSAC,
reprojThresh)# return the matches along with the homograpy matrix# and status of each matched pointreturn(matches, H, status)# otherwise, no homograpy could be computedreturnNonedefdrawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):# initialize the output visualization image(hA, wA)= imageA.shape[:2](hB, wB)= imageB.shape[:2]
vis = np.zeros((max(hA, hB), wA + wB,3), dtype="uint8")
vis[0:hA,0:wA]= imageA
vis[0:hB, wA:]= imageB
# loop over the matchesfor((trainIdx, queryIdx), s)inzip(matches, status):# only process the match if the keypoint was successfully# matchedif s ==1:# draw the match
ptA =(int(kpsA[queryIdx][0]),int(kpsA[queryIdx][1]))
ptB =(int(kpsB[trainIdx][0])+ wA,int(kpsB[trainIdx][1]))
cv2.line(vis, ptA, ptB,(0,255,0),2)# return the visualizationreturn vis
去抖动后,将当前图与初始图对比获得差分图,能显示前后图片之间的差异。
背景建模
背景建模主要是为了检测运动物体,输出前景图片。在获得图片的差分图后,将差分图放入背景建模中,获取前景运动图。
背景建模在opencv中主要包含knn建模和高斯建模(MOG2)两种方法,这里我们选择的是KNN的方法, 其中history表示影响背景模型的历史帧数,dist2Threshold 表示像素和样本之间平方距离的阈值,当大于阈值的话,则为前景:
import numpy as np
import cv2
import time
classknnDetector:def__init__(self, history, dist2Threshold, minArea):
self.minArea = minArea
self.detector = cv2.createBackgroundSubtractorKNN(history, dist2Threshold,False)
self.kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))defdetectOneFrame(self, frame):if frame isNone:returnNone
start = time.time()
mask = self.detector.apply(frame)
stop = time.time()print("detect cast {} ms".format(stop - start))
start = time.time()
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, self.kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, self.kernel)
stop = time.time()print("open contours cast {} ms".format(stop - start))
start = time.time()
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop = time.time()print("find contours cast {} ms".format(stop - start))
i =0
bboxs =[]
start = time.time()for c in contours:
i +=1if cv2.contourArea(c)< self.minArea:continue
bboxs.append(cv2.boundingRect(c))
stop = time.time()print("select cast {} ms".format(stop - start))return mask, bboxs
大家会发现通过knn的方法对后面的背景中的光线变化敏感,但是也不太容易丢失掉小物体,这里高空抛物的目标一般为小目标,所以选择这个算法。这里大家可以试试高斯混合建模、MOG2等其他方法。
形态学处理
从上图中可以看到,前景的mask 中存在很多的干扰,如灯光的干扰等,再通过形态学处理将干扰项移除。
首先通过开运算将前景中的毛刺过滤掉,再通过膨胀操作,将目标项变大,方便后面的目标追踪,获得效果如下:
目标检测
在第三步过滤掉干扰过后,找到目标的外接轮廓,同时过滤掉小的斑点干扰后,提取目标的外接矩形,至此基于传统视觉的目标检测完成。
目标追踪
因为动态目标检测出来的是一个个的目标块,这个时候根本不知道上一帧与下一帧的目标对应关系,也就谈不上移动距离。
因此需要跟踪算法,将目标一一对应起来,传统的跟踪算法,要么是根据HOG特征,要么是根据特征点来进行跟踪。
但是因为高空抛物目标很小,而且移动速度较快,所以不能用传统的基于特征的方式进行跟踪。我们唯一知道的就是在一帧里面的许多目标框,能不能只依据坐标来跟踪多个目标呢?
答案是有,那就是SORT算法(注意,不是DeepSort,DeepSort依旧会用到特征,在这里不适用)。
SORT是 SIMPLE ONLINE AND REALTIME TRACKING 的简写,并不是什么排序算法。其核心算法是匈牙利算法+卡尔曼滤波。SORT算法没有用到特征跟踪,其本质实际上是根据观测的位置预测下一帧出现的位置,而我们预测的高空抛物实际上是有很强的规律的(重物规律强,较轻的物体如塑料袋或者纸板等,不是很规律,但是其速度不快,在每一帧之间基本上都有IOU重叠,因此也不会漏检),所以完全可以用此算法。
判断是否在高空抛物的逻辑代码在KalmanBoxTracker.get_state方法内:
defget_state(self):...if disdance >2*(self.org_box[2]-self.org_box[0]+bbox[2]-bbox[0])and \
disdance >(self.org_box[3]-self.org_box[1]+bbox[3]-bbox[1]):
self.is_throw =True...return bbox, self.is_throw
- 可以看出当追踪到的目标移动的欧氏距离大于了当前检测框与初始检测框宽度和的两倍,并且大于当前检测框与初始检测框高度之和,就判断为高空抛物
- 从逻辑来说,这样判断也容易出现误检测,比如将飞鸟,大风天晃动的树枝都有可能误检测为高空抛物:
- 可考虑修改判断逻辑为判断x,y轴的位移:左右移动0.5倍宽度和,向下移动2倍高度和,即为高空抛物(可根据实际情况调整判断逻辑)。
defget_state(self):...
x =(bbox[0]+bbox[2])/2-(self.org_box[0]+self.org_box[2])/2
y =(bbox[1]+ bbox[3])/2-(self.org_box[1]+ self.org_box[3])/2if math.fabs(x)>0.5*(self.org_box[2]-self.org_box[0]+bbox[2]-bbox[0])and \
y >2*(self.org_box[3]-self.org_box[1]+bbox[3]-bbox[1]):
self.is_throw =Truereturn bbox, self.is_throw
- 测试如下:左边类飞鸟往斜上方移动的就不再识别为抛物了,而右边旋转视频后模拟的向下运动则能识别为抛物。
- 当然,若不缺数据的话建议考虑第三种方法,对检测到的轨迹进行分类识别
效果演示
每组测试展示了实际检测抛物效果和基于背景建模的目标检测情况,这里展示的两组测试从上到下分别是:
- KNN背景建模 + sort(3,5,0.1) + 欧式距离判断抛物
- MOG2背景建模 + sort(6,1,0.1) + x,y轴移动距离判断抛物
实际多轮测试中发现:
- sort的参数变化对该测试视频影响不大,背景建模的影响较大,两种对抛物的逻辑判断不影响该测试视频的最终检测结果,但是若有更多干扰(如飞鸟)则结果未可知。
- 从mask的情况可以看出,虽然有对视频做去抖动处理,但测试视频的抖动还是对检测框产生了较大影响,最终影响sort的效果opencv+sort算法高空抛物监测 下面是用aidlux部署在手机上的演示视频:缩小了视频尺寸,在手机上检测速度有所提升,但同时检测效果也稍差 https://www.bilibili.com/video/BV1JY4y117qh?p=2
项目代码
百度云盘链接:https://pan.baidu.com/s/147gi2MLqC6KiBGHyzSsbkw 提取码:aid3
本人调试的代码:https://download.csdn.net/download/kalahali/87620832
版权归原作者 神秘人士 所有, 如有侵权,请联系我们删除。