【目标追踪】python帧差法原理及其实现

python基于帧差法的视频目标追踪

  • 帧差法目标追踪原理
    • 1. 基本思路:
    • 2. 图例讲解:
    • 3. 存在的问题&如何解决:
    • 4. 最终效果预览
  • 第一步——调用摄像头:
    • 1. 使用opencv打开摄像头:
    • 2. 读取逐帧图片:
  • 第二步——处理图片:
    • 1. 转换成灰度图:
    • 2. 计算像素差:
    • 3. 使用中值滤波和膨胀腐蚀去噪:
      • 3.1 中值滤波:
      • 3.2 图像腐蚀和膨胀:
    • 4. 视频处理效果:
  • 第三步——框出候选区域
    • 1. 找出所有目标的轮廓:
    • 2. 非极大值抑制NMS:
  • 完整代码:

帧差法目标追踪原理

1. 基本思路:

摄像机采集的视频序列具有连续性的特点。如果场景内没有运动目标,则连续帧的变化很微弱,如果存在运动目标,则连续的帧和帧之间会有明显地变化。
【目标追踪】python帧差法原理及其实现_第1张图片

帧间差分法(Temporal Difference)就是借鉴了上述思想。由于场景中的目标在运动,目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧或三帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能。

2. 图例讲解:

对于前后视频中两帧图片,运动的目标区域会有相应变化:
【目标追踪】python帧差法原理及其实现_第2张图片
转换成二值图来看:
【目标追踪】python帧差法原理及其实现_第3张图片
将两个二值图作差,可以看到运动目标区域为白色:
【目标追踪】python帧差法原理及其实现_第4张图片
或者直接将两帧RGB图片作差,差值大于一定阙值的区域则标注为运动区域:

【目标追踪】python帧差法原理及其实现_第5张图片
可以得到类似的结果。

这样的话,我们只需要检测到视频中相邻两帧图片之间像素有较大变化的区域,就可以实现一个简单的运动物体检测;

3. 存在的问题&如何解决:

当然这里还有许多问题需要解决

  1. 视频帧差图中有许多干扰,如:【目标追踪】python帧差法原理及其实现_第6张图片
  2. 如何将目标区域选定出来?(比如用矩形框圈出来)【目标追踪】python帧差法原理及其实现_第7张图片
  3. 如何去掉重叠的矩形框或者筛选出合适的矩形框?
  4. 以及如何用代码实现?

下面会一一讲解:

4. 最终效果预览

帧差法:

第一步——调用摄像头:

1. 使用opencv打开摄像头:

其中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()

这样的话我们就成功地打开了摄像头:
【目标追踪】python帧差法原理及其实现_第8张图片

2. 读取逐帧图片:

在程序运行期间,opencv会不断读取摄像头输入的每一帧图片,再将图片进行一定处理,不断显示在window上:
【目标追踪】python帧差法原理及其实现_第9张图片
这里使用了read方法,返回的frame就是某一时刻视频的图片。

第二步——处理图片:

1. 转换成灰度图:

对读取到的每一帧图片,我们首先转换成灰度图:

catch, frame = cap.read()  # 读取每一帧图片

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 转换成灰度图

【目标追踪】python帧差法原理及其实现_第10张图片

2. 计算像素差:

然后我们计算视频中这一帧图片与上一帧图片像素的绝对值差:

gray_diff = cv2.absdiff(gray, previous) # 计算绝对值差
# previous 是上一帧图片的灰度图

【目标追踪】python帧差法原理及其实现_第11张图片

其中黑色区域是由于像素相同得0,灰色区域越接近白色,表示前后两帧图片该点的像素差越大。

再将帧差图进行二值化,即图片像素差大于某一阙值(我们这里定为40)的标注为运动点,赋值为255(白色),其余点赋值为0(黑色):

_, mask = cv2.threshold(                
	gray_diff, 40, 255, cv2.THRESH_BINARY)

得到的二值化图为:

【目标追踪】python帧差法原理及其实现_第12张图片

3. 使用中值滤波和膨胀腐蚀去噪:

这时候我们虽然得到了帧差图的二值化图,但图中有许多干扰是我们不想要的:
【目标追踪】python帧差法原理及其实现_第13张图片
这时候我们就需要通过一定的方法去噪;

3.1 中值滤波:

mask = cv2.medianBlur(mask, 3) # 中值滤波

中值滤波是一种图像平滑处理算法,基本原理就是,测试像素周围邻域像素集中的中值代替原像素,能够有效去除孤立的噪声点或较细的噪声线;

使用中值滤波前后对比:

【目标追踪】python帧差法原理及其实现_第14张图片

可以看到一部分噪声已经被去除了,图像也变得更加平滑。

3.2 图像腐蚀和膨胀:

腐蚀和膨胀具体原理可以看一下这篇博客:
数字图像处理(六)形态学处理之腐蚀、膨胀、开运算、闭运算

腐蚀和膨胀也是图片平滑处理的一种算法,一般先腐蚀再膨胀能够有效去除干扰线:

es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 4))
m = cv2.erode(median, es, iterations=2) # 腐蚀
m = cv2.dilate(m, es, iterations=2) # 膨胀

处理前后对比:
【目标追踪】python帧差法原理及其实现_第15张图片

4. 视频处理效果:

完整代码:

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()

效果如图

  1. threshold=40

【目标追踪】python帧差法原理及其实现_第16张图片
2. threshold=20:【目标追踪】python帧差法原理及其实现_第17张图片

第三步——框出候选区域

1. 找出所有目标的轮廓:

这里使用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()参数:

  1. 第一个参数是寻找轮廓的图像;
  2. 第二个参数表示轮廓的检索模式,cv2.RETR_LIST表示检测的轮廓不建立等级关系;
  3. 第三个参数method为轮廓的近似办法,cv2.CHAIN_APPROX_TC89_L1使用teh-Chinl chain 近似算法;

第一步筛选我们去掉面积过小的轮廓框,效果如图:
【目标追踪】python帧差法原理及其实现_第18张图片

2. 非极大值抑制NMS:

当然这里会有重叠框,我们使用非极大值抑制进行筛选;

非极大值抑制的作用是去除相同目标的重叠的多余的轮廓框,只保留得分最大的那个,例如:
【目标追踪】python帧差法原理及其实现_第19张图片

在这里,我们将得分定义为运动点占轮廓框总像素点的比例;
【目标追踪】python帧差法原理及其实现_第20张图片

完整代码:

【目标追踪】python帧差法原理及其实现_第21张图片

# 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)                             # 第一个参数可以是数字(表示打开摄像头)也可以是视频路径地址

最终效果:

你可能感兴趣的:(运动目标检测)