这两天在刷一些ROS的教程,看到了古月老师在课程《ROS机器视觉开发入门》课程当中关于cv_bridge实现移动侦测的Demo,刚好之前因为工作项目需求做到了类似的功能,特此发博文一篇,为打算学习机器视觉的小伙伴们做个交流。

移动侦测(Motion detection technology),一般也叫运动检测,常用于无人值守监控录像和自动报警。通过摄像头按照不同帧率采集得到的图像会被CPU按照一定算法进行计算和比较,当画面有变化时,如有人走过,镜头被移动,计算比较结果得出的数字会超过阈值并指示系统能自动作出相应的处理。

移动侦测通过OpenCV可以很轻松的实现,那么移动侦测的流程及原理是什么样的呢?我们继续看下去。

多张图像连续播放,当播放的频率较高时,我们看到的就是视频。每一张图像,也就是一帧。加入一段视频,它的分辨率是1920*1080,30FPS,那么就说明这个图像每秒有30张图像连续播放形成,每张图像的像素点是1920*1080.

移动侦测的原理有很多,最简单的办法就是将视频的上一帧图像和当前帧的图像进行比对,两张图像出现不吻合的位置,那就是出现了移动的物体。那么这是我们人为的识别,在计算机程序里面的实现是什么样的呢?图像的存储是通过数组矩阵来进行保存的,我们常见的图像都是有RGB三原色的像素点组成,加入我们现在有一张1920*1080分辨率的图像,其实就是说这张图像有1920*1080个像素点组成,而每个像素点通过RGB的形式来进行描述,所以这张图像就是一个1920*1080*3的一个张量(一阶标量,二阶向量,三阶张量)组成的,也就是一个三维的数据、矩阵。

我们可以通过减法来检测两张图像是否重合。两个图像如果是一模一样的话,那么它们转成数组之后的数值也一定是一模一样的,我们通过减法,让当前帧图像的矩阵减去上一帧的图像的矩阵,如果得到的新矩阵数值全是0,那么就说明着两个矩阵一样的,也就说明这两张图像是一样的;反之,如果存在不为0的地方,那么就说明当前帧和上一帧的对比当中,出现了移动的点,而不相等位置的数组下标,就是当前“移动目标”所在的“像素点坐标”。

理论上是这样的没错。当时图像算法在实际应用当中,需要考虑的情景太多了,你永远无法想象你的客户是在什么样的场景下来使用设备,当效果不好的时候还被骂产品垃圾,紧跟着就是无休止的加班,实在是痛的体会。

在实际的应用当中,强光、弱光、曝光、光斑、反射/折射等条件,晴天、雨天、阴天、多云转阴等等天气情况,实在是数不胜数,你现在做的程序没有问题,可能拿到下一个场景当中,就会发现完全无法使用。所以我们尽量对摄像头采集到的图像进行一些处理,来保证我们输入数据的“干净”。早i这些处理当中,大家最熟悉的应该就是“滤波”。OpenCV常用的滤波有,1)平均滤波,就是将一个区域内的像素值求和取平均值,然后用这个平均值替换区域中心的像素值。2)高斯滤波,也是将一个区域的像素值求取平均值替换区域中心的像素值,但是是加权平均,权重按照二维正态分布。3)中值滤波,之前的两个滤波都有个问题,如果区域中有极端值,很可能影响滤波效果,中值滤波采用区域中的中值来替换,有利于克服椒盐噪声。4)双边滤波,之前的滤波还有个问题,他们都会把轮廓给模糊了,有一些区域之间相差较大的像素,这往往能看出轮廓,所以如果我们给个限制范围,如果两点间的像素值差距大于这个范围就不滤波了,保留图像轮廓。

其实滤波是啥玩意呢,你现在采集了一组数据,数据的准确性存在“问题”,这些“问题”可能是真实的数据,有可能是人为记录时出现的失误。在我们看来,“离谱”的数据就是有问题的数据。好比你是大一的学生,你要去统计你们班同学的年龄,那么17到20之间的数据还是可以的,但是突然有个30岁的、50岁的、10岁的等等之类的,这些数据根本就是不着边,我们要么把这些数据剔除,要么给均值啊、给取中值啊等等,都是对原始数据的处理操作。

对于图像算法来说,其实对于绝大部分算法来说,通过滤波对数据进行一轮“清晰”,能在一定程度上来保证我们数据的的“干净”(数据的有效性及准确性等)。特别是对图像算法来说,我们通过滤波可以在绝大程度上消除因光线原因对图像数据产生的干扰。

欧克,科普先暂时到这里结束。我们现在来瞅一组代码~

#coding:utf-8

import cv2  #导入OpenCV库文件
import numpy as np  #导入numpy矩阵计算库文件
import time  #导入时间相关文件

capture = cv2.VideoCapture('rtsp://admin:yhj681970@192.168.2.88:554/h264/ch1/main/av_stream')  #设定摄像头(0是默认摄像头)
ret, frame = capture.read()  #读取第一帧图片数据,保存在frame当中,ret作为判定,当图片数据保存成功是,ret为1,否则为0
lastframe=None  #用于保存对比帧数据
 
while ret:
	global message  #将消息定义为全局变量,在使用中发生了改变,需要定义为全局
	time.sleep(0.01)  #延时0.01秒,缓冲
		
	gray=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #将图片数据灰度化  3
	gray=cv2.GaussianBlur(gray,(21,21),0) #进行高斯滤波处理  4

	#上一帧数据不存在,则对比数据为gray
	if lastframe is None:
		lastframe=gray
		continue

	#两帧数据进行比对
	gray_diff = cv2.absdiff(lastframe,gray) #求差绝对值  5
	gray_diff=cv2.threshold(gray_diff,30,1,cv2.THRESH_BINARY)[1] #差值二值化  6
	gray_new=np.multiply(gray_diff,gray) #矩阵点乘  7
	

        #消除前景
	gray2=cv2.convertScaleAbs(gray_new,0,1,0) #设置图像深度  8
	thresh = cv2.threshold(gray2, 125, 255, cv2.THRESH_BINARY)[1] #图像二值化  9
	kenerl=cv2.getStructuringElement(0,(7,7),(-1,-1)) #核  10
	image=cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kenerl,0,(-1,-1),1,0,0) #形态学滤波

	#计算图形框
	x,y,w,h=cv2.boundingRect(image) #计算框属性值
	if w>10 or h>10:
		frame=cv2.rectangle(frame,(x,y),(x+w,y+h),(0,0,255),2) #画框,x是左边距,y是右边距,w是异物宽度,h是异物高度  11
	
        ret, frame = capture.read()  #获取新一帧的数据
	if cv2.waitKey(10) == 27:  #用户等待操作
		break
	lastframe=gray  #标记下一次对比帧数据

这是一组Python OpenCV移动侦测代码,大家可以在自己Python环境下进行一下测试,记得pip安装一下OpenCV。我这里使用的海康威视的RTSP视频数据流,大家可以把videoCapture里面的参数改为0即可调用自己电脑默认的摄像头。(RTSP是一种视频传输的网络流数据,这里不需要过分纠结,改成0就是你电脑默认的摄像头)

大家可以结合这个流程图来分析我们的代码。当然呢,为了方便大家的理解,我这里也将程序每一步执行后的图像数据提取了出来。提取的过程,较为麻烦(折腾了好几个小时),大家看就好了~

首先,我们先拿一一个图片来作为“上一帧数据”,如下图所示。

这张图像作为我们需要比对的“背景图像”,这个背景图像是经过滤波处理之后的。然后是我,出现了这个场景下,这张图像是我们的“当前帧图像”。

这个时候在当前的“背景”下出现了“物体”,按照我们算法的设计,“物体”是要被算法检测和提取到的。我们需要先对图像进行一下灰度化的处理,减少计算的数量。

这张图像就是进行了灰度化处理的图像,可以看到,彩色的图像变成了灰白的图像。灰度图像的色彩矩阵要比RGB彩色图像矩阵小的多,便于计算。紧跟着我们对图像进行滤波处理,在代码里面我使用的是高斯滤波,滤波后的效果是这样的。

可以看到,经过高斯滤波之后的图像要模糊的很多。到这里为止,我们的图像预处理操作就已经执行结束了,接下来我们将处理后的图像和“上一帧图像”(背景图像)来做一个减法,并且取一个绝对值。这是谁减谁无所谓,反正我们是要取绝对值的,毕竟,色彩只肯定不会出现负数的吧。

做完“减法”,并求取绝对值之后的图像就比较明显了,基本上已经提取出了“物体”的轮廓。但是大家可以看到,“物体”右侧的位置明显有一条线,那么这说明我们当前的“数据”还是存在着“问题数据”的(在人看来是提取成功了,但是为了应用与多种场景,我们需要来对这部分进行消除)。

我们将这张图像进行一下二值化处理。图像的二值化,就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果(灰色的照片其实不是黑白照)。下面是二值化处理好的图像数据。

乍一看,卧槽!图像呢??我的图像呢?!!咋全黑了!!!!

这个是和我们二值化时候的参数有关系,灰度图像是用0-255的色阶来表示图像的,而二值化呢,是通过一个阈值,超过这个阈值的数据直接为1,即黑;而低于这个阈值的数据呢则直接为0,即白。但是这个时候如果你输出矩阵数值的的话,还是有数值的,说明这张图像差异,只是我们人眼无法看到。我们这里来使用一次“矩阵点成”来处理下我们的图像数据。

图像又奇迹般的出现了!!是不是感觉很神奇~

这是我是将“求差+绝对值”之后的图像,与灰度化后的图像进行了一次“矩阵点乘”操作,这次操作之后的图像和“求差+绝对值”之后的图像相比,是不是某些部位更亮一些了呢。

我们接下来需要设置一下图像的深度。图像的通道表示每个点能存放多少个数,类似于RGB彩色图中的每个像素点有三个值,即三通道的。图像的深度表示每个值由多少位来存储,是一个精度问题,一般图片是8bit(位)的,则深度是8。我这里设置的深度是8bit,这里设置深度主要是为了“提取前景”,这块我们稍后再说,继续看下去。

再重新得到图像之后,我们再来一次二值化,让图像里面的特征点数值(轮廓)更加明显一些,同时也可以来提取边框了,二值化后的图像只有0和1。再次经历二值化后的图像是这样的。

可以看到,人间消失不见了,留下了就是一个轮廓线,而且图像的亮暗的问题也没有了,黑白分明。我们再来一次滤波,效果是这样的~

这次的滤波是形态学滤波,我们通过morphologyEx方法的MORPH_OPEN进行了“开运算”(效果和先腐蚀后膨胀是一样的),可以看到很明显额一点就是图像左侧一个小白点没了,这就是进行滤波之后的效果!

到这里,我们的看以看到已经提取出来了当前的“物体”,我们通过OpenCV内置的boundingRect方法就可以得到当前的“物体属性”,主要包含了X“物体”像素点横坐标、Y“物体”像素点纵坐标、W“物体”的像素点宽度、H“物体”的像素点高度四个数据。至此,我们就可以开始进行画框的操作~

这里关于Python OpenCV的API我就不过多说明了,大家可以自行找下Python OpenCV的资料进行学习和掌握!

这里我们将当前帧和上一帧图像做减法来实现移动侦测的算法叫做“帧差法”。

其实“帧差法”是移动侦测算法当中相对来说很弱的一种(称之为垃圾也不为过),随着深度学习、机器学习、人工智能的发展,前景建模背景消除法、背景建模前景消除法、背景模型建立法,而且OpenCV的内部也给我们集成了想KNN、MOG等高级一些的模块来供我们使用。

这个效果看起来是不是很炫酷呢~

其实我就是把每一步执行后的数据进行了图像的显示,而上面大家看到的效果,也是我将每一步执行后的数据进行本地保存成jpg文件的形式为大家展示的。

来一张壮阔一些的图!

这是原图,是通过截取萤石云的视频流数据拿到的数据,传入我的移动侦测检测算法。

这个时候获取的“背景”,是这样的~

是不是比较壮阔呢~

关于移动侦测算法的介绍我们到这里就结束了,还有兴趣的小伙伴可以去深入的研究学习一下。Python OpenCV的编程还是挺简单的,C++搞OpenCV确实是困难。

在ROS系统下,我们通过启动关于摄像头的节点来开启我们的摄像头,并将图像数据通过image在ROS系统下进行传输。那么我们能不能将ROS下摄像头数据在OpenCV下使用呢,答案肯定是可以的。

但是ROS下摄像头话题的数据类型和OpenCV下的数据类型是不一样的,我需要通过一个cv_bridge的功能包来实现ROS下图像数据和OpenCV下图像数据的转换操作。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image

class image_converter:
    def __init__(self):    
        # 创建cv_bridge,声明图像的发布者和订阅者
        self.image_pub = rospy.Publisher("cv_bridge_image", Image, queue_size=1)
        self.bridge = CvBridge()
        self.image_sub = rospy.Subscriber("/usb_cam/image_raw", Image, self.callback)

    def callback(self,data):
        # 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
        try:
            cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
        except CvBridgeError as e:
            print e

        # 在opencv的显示窗口中绘制一个圆,作为标记
        (rows,cols,channels) = cv_image.shape
        if cols > 60 and rows > 60 :
            cv2.circle(cv_image, (60, 60), 30, (0,0,255), -1)

        # 显示Opencv格式的图像
        cv2.imshow("Image window", cv_image)
        cv2.waitKey(3)

        # 再将opencv格式额数据转换成ros image格式的数据发布
        try:
            self.image_pub.publish(self.bridge.cv2_to_imgmsg(cv_image, "bgr8"))
        except CvBridgeError as e:
            print e

if __name__ == '__main__':
    try:
        # 初始化ros节点
        rospy.init_node("cv_bridge_test")
        rospy.loginfo("Starting cv_bridge_test node")
        image_converter()
        rospy.spin()
    except KeyboardInterrupt:
        print "Shutting down cv_bridge_test node."
        cv2.destroyAllWindows()

这是古月老师在《ROS机器视觉开发入门》课程中关于ROS下图像话题数据和Python OpenCV下图像数据相互转换的一个Demo程序,熟悉ROS+Python小伙伴可以尝试将这个段代码和我们移动侦测的代码融合一下,实现在ROS系统的移动侦测。

如果我们能融合成功,那么是不是意味着我们可以Python著名的face_recognition融合实现在ROS系统下的人脸识别呢,或者将Python做的物体识别程序、目标检测追踪之类的进行融合呢!这个问题留给大家来试一试吧,我还是只给大家看一下效果~