运动检测-基于帧间比较减少灯光影响-opencv-python

Github:https://github.com/asd123pwj/motion-detect

这篇是一次比赛的记录,用的测试视频是亿联公司提供的,保护隐私就不放图了。
这篇是效果最好的代码介绍,之后我还会把很好消除光照但模糊运动检测无能的代码补上,为各位提供其它思路。
补上了:运动检测-基于边缘提取与帧间作差-opencv-python

检测场景

  • 由固定角度超高清摄像头拍摄的室内视频。
  • 30帧,3840 * 2160分辨率。
  • 灯光:
    • 室内灯光时常变化,大灯小灯隔一段时间开关一次。
  • 场景大小:
    • 在容纳百人时依然有宽松的运动空间。
  • 运动物体:
    • 人。
    • 因人走动而飘起的桌布。
  • 运动方式:
    • 小动作:
      • 人:眨眼、张嘴、说话、笑、撇头、抬手、扭头、点头。
      • 桌布:轻微飘动
    • 大动作:
      • 转身、调整姿势、甩头发、起身、蹲下、走到遮挡物后、从遮挡物后走出。
    • 快速运动:
      • 从镜头外突然出现、快速走动、拥抱。
  • 运动位置:
    • 从离镜头最远的房间另一角,到能造成难以聚焦位置的镜头前。

算法基本框架

  • 按帧读取视频并转为灰度图。
  • 利用KNN模型进行前景检测:
    • 去除小变化噪声。
    • 使用膨胀、腐蚀、区域绘制等方式使得临近区域连接在一起。
    • 给出保存检测出运动的区域列表。
  • 对上一步给出的区域列表进行前后帧关联比较,以减少光照影响:
    • 判断是否出现运动区域巨大变化的帧:
      • 求关联帧面积变化斜率,并根据面积变化斜率阈值判断。
      • 求该帧运动面积与关联帧平均运动区域面积的比值,并根据面积变化阈值判断。
      • 求该帧区域数目变化幅度,并根据区域数目阈值判断。
    • 判断为正常运动的帧:
      • 直接使用KNN检测结果。
    • 判断为运动区域巨大变化的帧:
      • 与前后帧运动区域求交集,并根据关联比例阈值判断。
    • 输出处理后的运动区域列表。
  • 绘制区域,写入日志。

算法关键步骤

  • 去除噪声并相邻区域连接:
    • 传入一个被认为有运动的区域。
    • 噪声阈值为noise_thres。
    • 使用findContours找到轮廓,并将轮廓面积小于阈值一半的去除。
    • 对去除的结果进行膨胀腐蚀,膨胀算子为3*5十字结构,腐蚀算子为3*3十字结构,以便在去除噪声的同时,将上下相邻的区域连接(人的运动区域一般是长方形,所以选用长方形算子膨胀,正方形算子腐蚀)。
    • 对前面的结果再使用findContours,去除面积小于阈值的区域。
    • 对前面的结果再使用findContours,将相交的区域连接起来,并去除面积小于阈值两倍的区域。
    • 传出去噪且更合理的运动区域。
  • 关联帧面积变化斜率计算:
    • 对于斜率的判断,我认为,在不到一秒的的时间内,开关灯的影响会让变化区域先变大,再变小,而正常运动,则是要么变大,要么变小。但也可能会因为时间过于短暂或拍摄设备的问题让灯光残留时间过久,导致这个判断不准确。如果开关灯的变化刚好在关联帧内,那么理论上阈值取得高点比较好,反之则取低一点。
    • 操作:
      • 传入帧列表,列表长度为q_len。设定斜率值slope为0,斜率变化阈值取slope_thres。
      • 对帧列表的各帧的区域求和。
      • 处理前q_len/2帧:
        • 如果当前帧的面积大于上一帧的1.5倍,则slope + 1
      • 处理后q_len/2帧:
        • 如果当前帧的面积大于下一帧的1.5倍,则slope + 1
      • 如果当前斜率与q_len的比值超过阈值,则被视为出现了开关灯情况。
  • 帧面积变化幅度计算:
    • 对于面积的判断,开光灯必然使得被视作运动的区域的面积变大,因此可以将变化幅度过大的帧视作出现巨大变化的帧。
    • 操作:
      • 面积变化阈值取area_thres
      • 计算关联帧的平均区域面积。
      • 如果当前面积与平均面积的比值超过阈值,则被视为出现了大幅面积变化。
  • 帧区域数目变化幅度计算:
    • 对于区域数目的判断,开关灯结束后的一段时间,运动面积趋于平稳,但个数可能起伏不定,因此如果变化过大,可能是噪声。
    • 操作:
      • 区域数目变化阈值取num_thres
      • 计算关联帧的平均区域数目。
      • 如果当前区域数目与平均数目的比值超过阈值,则被视为出现了大量噪声。
  • 帧间比较求运动区域:
    • 我认为,一个正常的运动物体,一般运动持续时间比灯光变化的时间要长,因此,如果关联帧中某区域一直运动,那么可以认为该区域属于运动区域,关联比例越大,运动区域的识别准确率越高。
    • 操作:
      • 传入运动区域
      • 根据帧面积变化斜率与面积变化幅度判断当前帧是否是非正常运动帧。
      • 若为正常运动:
        • 直接输出运动区域。
      • 若不是:
        • 当前帧运动区域个数为c_len。设c_len长度的confidence_contours数组,用来保存该区域在其他帧出现的次数,每个元素初始值为0。相关比例为relevance。
        • 如果当前帧某运动区域与其它帧的运动区域相交,则将对应的confidence_contours数组元素+1,表示其相交次数。
        • 如果次数大于relevance与c_len的乘积,则表示当前帧在关联帧中多次出现,可被视为正常运动区域,将其加入返回列表。
        • 若小于,则视为开关灯影响区域,
      • 返回处理后的区域。

实现效果

  • 整体实现情况:
    • 对于静止物体,仅在被灯光照射时才可能出现标注错误的情况,且该情况仅在大面积灯光变化时会持续几帧。
    • 对于运动物体,当其小范围运动时能准确标注运动区域,当其大幅度运动会先检测出刚运动的区域,然后逐渐扩散到整个运动物体。
    • 对于灯光变化,仅在灯光变化区域内出现运动物体时,可能出现几帧的检测错误。
  • 检测情况:
    • 能较好标注如抬手,撇头等小运动区域。
    • 能准确标注物体的大幅运动。
    • 能准确标注突然出现的物体。
    • 能快速准确标注从静止到运动的整体。
    • 能快速准确收敛从运动到静止的物体。
    • 能准确避免无灯光照射下静止区域的误标。
    • 能准确避免灯光照射下静止区域的误标。
    • 能快速准确对灯光照射下运动区域误标的收敛。

项目难点及解决思路

  1. 为了实现运动的检测,前后使用了三种检测方法:
    1. 最开始基于GMM前景检测的结果进行处理。
      • 能准确检测出运动区域。
      • 但噪声很多,且受光照影响大。
    2. 之后以常用的图像处理手段进行处理。
      • 结合了彩色图像转灰度图,高斯滤波,边缘提取,帧间作差,膨胀腐蚀,开运算闭运算,区域连接等处理方式。
      • 能非常好的消除光照影响,并能准确标注有明显边缘的运动物体。
      • 但因为边缘提取无法提取模糊物体的边缘,因此对于快速运动以至于模糊的运动几乎没有任何检测能力。
      • 还配合了锐化、白平衡等方式,但效果不佳。
    3. 最后使用帧间关联检测运动区域的方式。
      • 将当前需要检测运动的帧,与前后一系列帧进行比较得到可信度较高的运动区域。
      • 再将帧间关联检测思想与KNN前景检测模型结合使用。
      • 效果非常好,能减轻绝大多数光照影响,大面积的光照影响也会在几帧内快速收敛,并且能根据需求检测出运动区域。
  2. 对于如何将一个运动整体进行更美观的标注:
    • 使用的所有运动检测方式中,都会出现在检测单个物体的运动时,出现多个标注区域。
      • 思考了原因,对于刚开始运动的物体,因为很可能只是部分地方先动,所以出现多个运动区域是很正常的情况。
      • 但对于已经在走动的人体,也出现了多个运动区域,甚至数十个运动区域,是因为人的衣服颜色与头发颜色在一定区域是一致的,所以虽然在运动,但那块区域的颜色变化并不大。
    • 因此使用了腐蚀、膨胀、开运算、闭运算等形态学图像处理方式,以及运动区域多次绘制的方式来实现运动区域的扩散,以便达到整体标注效果。
    • 对于不同的处理方式,需要的结构算子不同,操作次数也不同,经过大量调整之后,获得了一个观感不错的标注效果。
  3. 对于如何进行帧间关联检测:
    • 观察测试视频,及配合常识可知,正常开关灯只会在一瞬间出现光照影响,不到一秒就会趋于稳定。
      • 因此,只需要在这一秒内判断这个区域是否可能是光照影响即可。
    • 对于如何判断帧是否属于正常运动帧:
      • 对帧运动面积变化幅度判断,检测是否出现了突然运动。
        • 开关灯时会出现大幅区域变化。
      • 对帧运动面积变化斜率判断,检测是否出现类似开灯影响的区域变化。
        • 开关灯造成的错误区域面积,一般从小到大,再变小。
      • 对帧运动区域数目幅度判断,检测是否可能出现噪声。
        • 被前两个方法漏掉的帧中,噪声很多,因此用此方法弥补。
    • 对于如何判断帧的运动区域可信度:
      • 测试该区域各运动帧是否与前后关联帧的运动区域有交集。
      • 根据相交百分比来确定该区域的可信度。
    • 实现了能很好避免灯光影响,且对运动敏感度高的效果。

术语介绍

  1. 算子:可看做n*m的矩阵,十字结构算子表示值为1的位置排列是十字型的。
  2. 彩色图像转灰度图:将彩色图像转为灰度图像,方便进行图像处理。
  3. 高斯滤波:一种灰度图去噪方式。
  4. 边缘提取:使用算子计算图像不同区域的值,并设定阈值来区分是否被视为边缘。
  5. 帧间作差:前后两帧的图像作差,得到新图像。
  6. 膨胀、腐蚀、开运算、闭运算:二值图中能让算子与区域乘积符合规则的一系列操作,其中,开闭运算是膨胀腐蚀的叠加算法。
  7. 锐化:使用算子让边缘更加明显的处理方式。
  8. 白平衡:消减图像光照影响的一种图像处理方式。
  9. KNN:K Nearest Neighbor,最邻近结点算法。
  10. GMM:Gaussian Mixed Model,高斯混合模型。
  11. 区域:可表示为(x, y, w, h)的数组,x为矩形左下角横坐标,y为矩形左下角纵坐标,w为矩形在横轴上的长度,h为矩形在纵轴上的长度。
  12. 前景检测:将运动的物体作为前景,将静止的物体作为背景的一种分割方式,也称运动检测。
  13. 帧间关联检测/帧间检测/前后帧检测/前后帧关联检测:在本文中,对以当前帧为中心,前后多帧作为参考的一种运动区域可信度检测方式。

算法性能

  • 测试环境:
    • 系统:Windows10
    • CPU:i7-8750H 2.20GHz
    • 显卡:GTX1050Ti(笔记本显卡)
    • 内存:8G
  • 处理情况:
    • 测试视频:2分04秒,30帧,3840 * 2160分辨率。
    • 处理用时:14分35秒。
  • 可拓展性:
    • 每个区域处理方法的输出都是opencv的contours区域,因此可以很方便的扩展,修改。
      • 例如像使用其他的opencv前景检测模型,如GMM,只需要将传入模型更改即可。
      • 也可以将前景检测的方法改成自己的方法,只要保证传出为区域列表即可。
      • 改成使用x, y, w, h的区域表示也可以,因为这里实际上还是用的矩形区域绘制。
    • 判断方法易增加:
      • 通过开启日志,能清楚看到每帧的判断结果,如果某个动作不易识别,或识别错误,可以直接在判断方法中增加新方法。
  • 适用性:
    • 通过调整阈值与关联时间,可以很方便的实现不同的运动敏感。
    • 例如,为了实现运动不敏感,让一些短暂出现的动作不被标注,可以将面积变化幅度阈值设为0。
      • 可以将关联时间设置长一点,关联性高一点。
    • 又如,对于光照强度一定的室内,想识别所有运动区域,可以将关联时间设为0。
    • 又如,只想检测大幅的运动,可以将噪声比例提高。

参数及使用方法

  • 使用方式:
    • Python版本及库需求:
      • Python 3.7
      • opencv 4.5.1.48
      • numpy 1.20.2
      • argparse
    • 工作目录:
      • motion_detect.py #运动检测代码
      • testVideo.mp4 #测试视频
    • 启动命令:
      • 默认参数执行:python motion_detect.py
      • 自订参数执行:python motion_detect.py --relevance 0.9
    • 输出:
      • 首先命令行输出视频fps与关联帧数,随后将处理好的帧数的序号打印出。
      • 结束后会生成output.mp4视频文件。
    • 实时输出及日志:
      • 当show_frame参数为True时,实时显示处理结果,按q键提前退出,并正常保存视频。
      • 当show_log参数为True时,将当前帧的处理日志写入视频帧中。
      • 当skip_time参数有正数值时,将会跳过一定秒数
      • 推荐在测试时将上面三个参数按需使用,方便根据结果修改阈值参数。
  • 参数
    1. src: 待检测视频路径
      • 默认值:’testVideo.mp4′
    2. area_thres: 面积变化阈值,非负数
      • 默认值:2
    3. slope_thres: 面积变化斜率阈值,取值范围[0,1]
      • 默认值:0.5
    4. num_thres: 区域数目变化阈值,取值范围[0,1]
      • 默认值:3
    5. relevance: 关联比例,关联比例越小,被视作正常运动的帧越多,取值范围[0,1]
      • 默认值:0.8
    6. relevance_time: 关联时间,单位秒,以该帧为中心,前后n/2秒的帧被视作相关帧,非负数
      • 默认值:0.5
    7. noise_proportion: 噪声比例,可被视作噪声的区域占视频面积的比例,不应过大也不应过小,一般以万分一为调整单位。非负数
      • 默认值:0.00002
    8. show_frame: 实时显示,为真时实时显示处理结果
      • 默认值:0
    9. show_log: 日志显示,为真时将日志写入视频帧
      • 默认值:0
    10. output: 输出路径
      • 默认值:’output.mp4′
    11. skip_time: 跳过时间,从指定时间开始检测,非负数
      • 默认值:0

运动检测代码

import cv2 as cv
import numpy as np
import argparse


def model_process(gray_frame, model, noise_thres):
    """
    使用模型检测运动区域,并对检测的区域去噪、连接
    :param gray_frame: 当前帧的灰度图
    :param model: 检测模型
    :param noise_thres: 被视作噪声面积的阈值,视频分辨率越高,该值应越大
    :return: 运动区域
    """
    model_frame = model.apply(gray_frame)
    contours, hierarchy = cv.findContours(model_frame, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    for c in contours:
        #   去除小面积变化噪声
        if cv.contourArea(c) < noise_thres * 0.5:
            continue
        (x, y, w, h) = cv.boundingRect(c)
        cv.rectangle(model_frame, (x, y), (x + w, y + h), (255, 255, 255), -1)

    #   连接相近区域并调整形状
    es2 = cv.getStructuringElement(cv.MORPH_CROSS, (3, 5))
    model_frame = cv.dilate(model_frame, es2, iterations=1)

    contours, hierarchy = cv.findContours(model_frame, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    for c in contours:
        #   去除小面积变化噪声
        if cv.contourArea(c) < noise_thres:
            continue
        (x, y, w, h) = cv.boundingRect(c)
        cv.rectangle(model_frame, (x, y), (x + w, y + h), (255, 255, 255), -1)

    #   去除小面积噪声
    contours, hierarchy = cv.findContours(model_frame, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    moderate_contours = []
    for c in contours:
        if cv.contourArea(c) > noise_thres * 2:
            moderate_contours.append(c)

    return moderate_contours


def get_contours_area(contours):
    """
    计算区域集的总面积
    :param contours: 区域集
    :return: 区域集总面积
    """
    area_sum = 0
    for c in contours:
        area_sum += cv.contourArea(c)
    return area_sum


def is_contours_intersect(contour1, contour2):
    """
    判断两区域是否有交集
    :param contour1: 区域1
    :param contour2: 区域2
    :return: 区域有交集则True,否则False
    """
    (x1, y1, w1, h1) = cv.boundingRect(contour1)
    (x2, y2, w2, h2) = cv.boundingRect(contour2)
    if x1 + w1 < x2 or x2 + w2 < x1:
        return False
    if y1 + h1 < y2 or y2 + h2 < y1:
        return False
    return True


def is_big_motion(queue_contours, area_thres, slope_thres, num_thres):
    """
    判断是否出现运动区域巨大变化的帧
    对于斜率的判断,我认为,在不到一秒的的时间内,开关灯的影响会让变化区域先变大,再变小,而正常运动,则是要么变大,要么变小。但也可能会因为时间过于短暂或拍摄设备的问题让灯光残留时间过久,导致这个判断不准确。如果开关灯的变化刚好在关联帧内,那么理论上阈值取得高点比较好,反之则取低一点。
    对于面积的判断,开光灯必然使得被视作运动的区域的面积变大,因此可以将变化幅度过大的帧视作出现巨大变化的帧。
    对于区域数目的判断,开关灯结束后的一段时间,运动面积趋于平稳,但个数可能起伏不定,因此如果变化过大,可能是噪声。
    对这三个参数的调整,可以将显示日志的参数打开,观察判断错误的帧中,这三个参数是如何变化的。
    值得一提的是,如果关联比例relevance与关联时间relevance_time取得好,可以不判断是否要处理,而是都进行前后帧关联处理,这样可以减少一些错误噪声,但是会降低对运动的敏感性。
    :param queue_contours: 前后一系列帧的区域的集
    :param area_thres: 面积变化阈值。
    :param slope_thres: 面积变化斜率阈值
    :param num_thres: 区域数目变化阈值
    :return: 日志与判断真假结果
    """
    #   直接判断需要处理
    if area_thres == 0 or slope_thres == 0 or num_thres == 0:
        return 'Always process.', True
    #   初始化
    q_len = len(queue_contours)
    contours = queue_contours[q_len//2]
    c_len = len(contours)
    area_average = 0
    area = get_contours_area(contours)
    slope = 0
    num = 0
    confidence_slope = False
    confidence_area = False
    confidence_num = False
    area_list = []
    for pos in range(q_len):
        area_list.append(get_contours_area(queue_contours[pos]))
    #   面积变化斜率判断
    for pos in range(q_len // 2 + 1):
        if area_list[pos] < 1.5 * area_list[pos + 1]:
            slope += 1
    for pos in range(q_len // 2 + 1, q_len):
        if area_list[pos - 1] > 1.5 * area_list[pos]:
            slope += 1
    if slope / q_len > slope_thres:
        confidence_slope = True
    #   面积变化大小判断
    for pos in range(q_len):
        area_average += area_list[pos]
    area_average = area_average / q_len
    if area_average and (area > area_average * area_thres or area * area_thres < area_average):
        confidence_area = True
    #   区域数目变化幅度判断
    for pos in range(q_len):
        num += len(queue_contours[pos])
    num /= q_len
    if c_len > num * num_thres or c_len * num_thres < num:
        confidence_num = True
    #   日志
    log = 'Area:' + str(area // 1) + '  Area_average:' + str(area_average // 1)
    log = log + '  Slope_conf: ' + str(confidence_slope) + '  Area_conf:' + str(confidence_area)
    log = log + '  Num_conf: ' + str(confidence_num)
    if confidence_area or confidence_slope or confidence_num:
        return log, True
    else:
        return log, False


def get_contours_intersect(contours, contours_other):
    """
    获取contours区域集中与contours_other有交集的区域
    :param contours: 当前帧被检测到的运动区域
    :param contours_other: 非当前帧的运动区域
    :return: 返回与contours等长的列表,列表中元素值为1,代表该contour与contours_other有交集
    """
    c_len = len(contours)
    contours_intersect = np.zeros(c_len)
    for c1 in range(c_len):
        for c2 in contours_other:
            if is_contours_intersect(contours[c1], c2):
                contours_intersect[c1] = 1
                break
    return contours_intersect


def get_better_contours(queue_contours,  area_thres, slope_thres, num_thres, relevance):
    """
    根据前后帧区域判断是否出现大面积变动,并与前后帧关联处理。
    我认为,一个正常的运动物体,一般运动持续时间比灯光变化的时间要长,因此,如果关联帧中某区域一直运动,那么可以认为该区域属于运动区域,relevance越大,运动区域的识别准确率越高
    :param queue_contours:  前后一系列帧的区域的列表
    :param relevance:   关联比例,关联比例越小,被视作正常运动的帧越多,取值范围[0,1]
    :param area_thres:  面积变化阈值
    :param slope_thres: 面积变化斜率阈值
    :param num_thres: 区域数目变化阈值
    :return: 更可能是正常运动的区域
    """
    #   初始化
    q_len = len(queue_contours)
    c_len = len(queue_contours[q_len//2])
    contours_related = queue_contours[q_len//2]
    num_pre_contours = len(contours_related)
    confidence_contours = np.zeros(c_len)
    #   区域变化情况检测
    log, is_big = is_big_motion(queue_contours, area_thres, slope_thres, num_thres)
    logs = [log]
    #   区域相关性检测
    if is_big:
        for pos in range(q_len):
            if pos != q_len//2:
                contours_intersect = get_contours_intersect(contours_related, queue_contours[pos])
                confidence_contours = np.sum([confidence_contours, contours_intersect], axis=0)
        #   相关区域整理
        contours_related = []
        for pos in range(c_len):
            if confidence_contours[pos] > relevance * c_len:
                contours_related.append(queue_contours[q_len//2][pos])
        #   日志
        log2 = 'big change: '
        queue_contours[q_len//2] = contours_related
        log2 = log2 + ' contours number: after process:' + str(len(contours_related))
        log2 = log2 + ' before: ' + str(num_pre_contours)
        logs.append(log2)
    return logs, contours_related


def enque(q, ele, max_len):
    """
    入队
    :param q: 队列
    :param ele: 元素
    :param max_len: 队列最大长度
    :return: 无返回值,直接修改队列
    """
    q.append(ele)
    if len(q) == max_len + 1:
        q.pop(0)


def add_log(logs, frame, size):
    """
    根据视频大小动态设置日志字体大小,为帧添加文本
    :param logs: 文本集,每个元素为一个字符串
    :param frame: 待作画区域
    :param size: 视频大小
    :return: 无返回值,在frame上直接修改
    """
    width = size[0] // 50
    per_height = size[1] // 40
    font_size = size[0] / 1000
    height = per_height
    for log in logs:
        height += per_height
        cv.putText(frame, log, (width, height), cv.FONT_HERSHEY_PLAIN, font_size, (255, 0, 0), 2)


def motion_detect(src, area_thres, slope_thres, num_thres, relevance, relevance_time, noise_proportion, show_frame, show_log, output, skip_time):
    """
    :param src: 待检测视频路径
    :param area_thres: 面积变化阈值,非负数
    :param slope_thres: 面积变化斜率阈值,取值范围[0,1]
    :param num_thres: 区域数目变化阈值,取值范围[0,1]
    :param relevance: 关联比例,关联比例越小,被视作正常运动的帧越多,取值范围[0,1]
    :param relevance_time: 关联时间,单位秒,以该帧为中心,前后n/2秒的帧被视作相关帧,非负数
    :param noise_proportion: 噪声比例,可被视作噪声的区域占视频面积的比例,不应过大也不应过小,一般以万分一为调整单位。非负数
    :param show_frame: 实时显示,为真时实时显示处理结果
    :param show_log: 日志显示,为真时将日志写入视频帧
    :param output: 输出路径
    :param skip_time: 跳过时间,从指定时间开始检测,非负数
    :return: 无返回值。
    """
    #   视频读取
    cap = cv.VideoCapture(src)
    #   视频fps读取
    fps = cap.get(cv.CAP_PROP_FPS)
    print(fps)
    #   视频大小读取
    size = (int(cap.get(cv.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv.CAP_PROP_FRAME_HEIGHT)))
    #   视频保存编码
    fourcc_mp4 = cv.VideoWriter_fourcc(*'mp4v')
    #   保存视频
    out_detect = cv.VideoWriter(output, fourcc_mp4, fps, size, True)

    #   参数初始化
    #   前景检测模型
    mog = cv.createBackgroundSubtractorMOG2()
    knn = cv.createBackgroundSubtractorKNN(detectShadows=False, history=5)
    #   帧数
    frame_count = 0
    skip_frame = skip_time * fps
    #   阈值
    noise_thres = size[0] * size[1] * noise_proportion
    queue_max_len = fps * relevance_time // 2 * 2 + 1
    print(queue_max_len)
    #   相关帧队列
    queue_frame = []
    queue_contours = []
    #   视频处理
    while cap.isOpened():
        #   日志初始化
        logs = []
        frame_count += 1
        print(frame_count)
        log = 'Frame:' + str(frame_count) + '  noise_thres:' + str(noise_thres) + '  noise_proportion:' + str(noise_proportion)
        logs.append(log)
        log = 'area_thres:' + str(area_thres) + '  slope_thres:' + str(slope_thres) + ' num_thres:' + str(num_thres)
        log = log + '  relevance:' + str(relevance) + '  relevance_time:' + str(relevance_time)
        logs.append(log)
        #   读入视频帧
        ret, frame = cap.read()
        #   跳过前n帧
        if frame_count < skip_frame:
            continue
        enque(queue_frame, frame, queue_max_len)
        #   视频结束,跳出循环
        if frame is None:
            break
        #   转灰度图
        gray_frame = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
        #   前景检测
        contours_KNN = model_process(gray_frame, knn, noise_thres)
        #   光照影响消减
        if len(queue_contours) < queue_max_len or queue_max_len <= 1:
            contours_better = contours_KNN
        else:
            log, contours_better = get_better_contours(queue_contours, area_thres, slope_thres, num_thres, relevance)
            for l in log:
                logs.append(l)   
        enque(queue_contours, contours_KNN, queue_max_len)
        frame = queue_frame[len(queue_frame) // 2]
        #   日志写入视频帧
        if show_log:
            add_log(logs, frame, size)

        #   绘制检测区
        for c in contours_better:
            (x, y, w, h) = cv.boundingRect(c)
            cv.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)

        #   图像实时显示
        if show_frame:
            cv.namedWindow("frame", cv.WINDOW_NORMAL)
            cv.imshow("frame", frame)
            #   按q键退出
            if cv.waitKey(1) == ord('q'):
                break

        #   处理结果写入
        out_detect.write(frame)

    #   资源释放
    cap.release()
    cv.destroyAllWindows()
    out_detect.release()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--src', type=str, default='testVideo.mp4')
    parser.add_argument('--area_thres', type=float, default=2)
    parser.add_argument('--slope_thres', type=float, default=0.5)
    parser.add_argument('--num_thres', type=float, default=3)
    parser.add_argument('--relevance', type=float, default=0.8)
    parser.add_argument('--relevance_time', type=float, default=0.5)
    parser.add_argument('--noise_proportion', type=float, default=0.00002)
    parser.add_argument('--show_frame', type=bool, default=0)
    parser.add_argument('--show_log', type=bool, default=0)
    parser.add_argument('--output', type=str, default='output.mp4')
    parser.add_argument('--skip_time', type=float, default=0)
    args = parser.parse_args()
    motion_detect(args.src,
                  args.area_thres,
                  args.slope_thres,
                  args.num_thres,
                  args.relevance,
                  args.relevance_time,
                  args.noise_proportion,
                  args.show_frame,
                  args.show_log,
                  args.output,
                  args.skip_time)


if __name__ == '__main__':
    main()

按帧查看视频

  • 附上一个按帧查看视频的源码,方便比对日志与错误区域。
  • 使用方式:
    • python read_frame.py --src output.mp4 --skip 10
    • 查看output.mp4,并跳过前10秒的帧。
    • 按Q退出,按其他键下一帧
import cv2 as cv
import argparse


def read_frame(src, skip):
    #   视频读取
    cap = cv.VideoCapture(src)
    skip_frame = skip * cap.get(cv.CAP_PROP_FPS)
    frame_count = 0
    while cap.isOpened():
        frame_count += 1
        ret, frame = cap.read()
        if frame_count < skip_frame:
            continue
        cv.namedWindow("frame", cv.WINDOW_NORMAL)
        cv.imshow("frame", frame)
        if cv.waitKey() == ord('q'):
            break
        else:
            pass

    #   资源释放
    cap.release()
    cv.destroyAllWindows()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--skip', type=float, default=0)
    parser.add_argument('--src', type=str, default='output.mp4')
    args = parser.parse_args()
    read_frame(args.src, args.skip)


if __name__ == '__main__':
    main()

You may also like...

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注