视频局部区域移动检测, 删除相似帧

2023年 8月 21日 29.3k 0

视频局部区域移动检测, 删除相似帧

完整方案在本文最后, 不想听故事的直接跳转到完整方案即可

起因

老板的一个东西找不到了, 让查监控

场景

东西放在一个架子上, 由一个海康威视全天候录像的摄像头监控, 但是巧就巧在这个要找的东西被放在了摄像头的死角里, 正好被柜子的隔板给挡住了, 没办法通过对比的方法直接找出来移动痕迹.

于是只能导出从这个东西被放进去,到发现这个东西丢失的这段时间, 全部的录像资料(跨度近1个月, 共163G视频)

开始尝试

尝试一 观看所有移动事件

24*30个小时的视频不可能挨着看, 不过好在海康威视的NVR4.0有事件检测, 直接导出目标时间段内的所有移动事件检测视频(时长不好统计).

花了4个工作日, 将一个月的移动事件视频资料, 使用PotPlayer12倍速看完, 发现那东西放进去后根本就没人动过. 遂报告老板, 老板不信, 让再查一遍监控.

第二次观看移动事件视频时发现, 海康的移动检测有问题, 经常出现闪回(同一个片段重复两次)就算了, 还会出现移动事件丢失(直观感受就是人物明明还在画面中移动, 突然就消失了), 这让我失去了对海康移动事件检测的信任.

于是开始第二次尝试

尝试二 压缩原视频后观看

24*30个小时的视频,就算12倍速, 也得60个小时才能看完, 让我集中精力60小时看监控视频? 我选离职!

于是在网上搜索, 想找到一个视频相似帧删除的工具, 这样就能大大压缩视频时长并能保留下移动变化. 接着就在B站上找到一个宝藏UP, 写的这样一个工具: 侦测视频相似画面自动清除,抓取监控重点变化,批量生成浓缩视频, 使用这个工具, 光是压缩24*30个小时的视频, 就花掉了超过24小时的时间…..原海康威视导出的1G大小, 时长在几个小时-十几小时不等的视频, 经过该工具压缩后, 时长缩短到十几分钟-一个小时不等, 时长被压缩了10倍以上

我用了5个工作日, 将这些压缩后的视频以12倍速全部看完, 仍然没有发现那东西被人拿走的痕迹, 遂又向老板报告, 老板说: "……算了吧"

老板算了, 我不能算

在使用宝藏UP主写的工具的时候, 我感受到了这个工具的不足之处:

  • 相似度检测, 容易受到窗外树木, 地砖倒影变化的严重干扰

  • 相似度检测, 会受到视频左上角时间水印的严重干扰

  • 相似度检测, 会受到夜间噪点的严重干扰

  • 图中蓝色框内是需要重点观察移动变化的区域

    两个黄色框内是对相似度检测造成严重干扰的区域(如果调整参数, 滤过这两部分的干扰, 那么相对的物体移动的帧也会被删除一部分, 使得结果不可靠)

    于是, 我就想, 有没有一种可以对视频的局部区域内进行相似度检测压缩的方法

    微信截图_20230820102153.png

    尝试三 ffmpeg mpdecimate 日志提取

    由于宝藏up的工具是调用了ffmpeg这个传说中的库来做的, 于是我问AI(gpt3.5): ffmpeg能不能对视频的局部区域做相似帧删除?

    经过一阵对线, AI最后给了我一句指令:

    ffmpeg -i ./ceshi.mp4 -vf "crop=850:486:1070:0,mpdecimate=hi=64*120:lo=65*50:frac=0.33,setpts=N/FRAME_RATE/TB" ceshi3.mp4
    
    

    该指令执行后, 会生成一个被相似度压缩后的视频, 但是有一点缺陷, 就是该视频的长宽只有crop剪裁的长宽(画幅只有蓝色框框那么大). 以至于物体的移动失去了时间这个参照物, 我看见它动了, 但不知道是什么时间点动的, 想要去原视频找出对应的一幕也很困难.

    然后我又去和AI对线, 试试看ffmpeg能不能对视频的局部区域做相似帧检测, 然后在全局上删除相似帧. 最后直到AI向我抱歉,AI对线详情

    后来我突发奇想, mpdecimate的日志里,应该包含了被删除, 或者被保留的帧信息, 那么我可以提取这些帧信息, 在原视频上抽取这些帧, 组成新的视频, 就完美符合我的需求了. AI还真的给了我一个提取日志信息的方式AI相关对话记录

    ffmpeg -i ./ceshi.mp4 -vf 'crop=850:486:1070:0,mpdecimate=hi=64*120:lo=65*50:frac=0.33,showinfo' -f null - > mpdecimate_log.txt 2>&1
    
    

    AI还向我解释了日志中, 各个参数的含义:

    # ffmpeg mpdecimate 部分日志如下:
    [Parsed_showinfo_2 @ 000002c311b40a00] n:   1 pts:2141190 pts_time:23.791  duration:   3600 duration_time:0.04    pos: 
      660900 fmt:yuvj420p sar:0/1 s:850x486 i:P iskey:0 type:P checksum:E7F119D0 plane_checksum:[CE36ADF8 B187A046 0BDFCB74
    ] mean:[128 117 133] stdev:[70.1 12.7 8.3]
    [Parsed_showinfo_2 @ 000002c311b40a00] color_range:pc color_space:unknown color_primaries:unknown color_trc:unknown
    [Parsed_showinfo_2 @ 000002c311b40a00] n:   2 pts:2148930 pts_time:23.877  duration:   3600 duration_time:0.04    pos: 
      662364 fmt:yuvj420p sar:0/1 s:850x486 i:P iskey:0 type:P checksum:4EF9DC25 plane_checksum:[74FE6EE1 4B23A19F 35F7CB96
    ] mean:[128 117 133] stdev:[70.1 12.7 8.3]
    [Parsed_showinfo_2 @ 000002c311b40a00] color_range:pc color_space:unknown color_primaries:unknown color_trc:unknown
    [Parsed_showinfo_2 @ 000002c311b40a00] n:   3 pts:2163510 pts_time:24.039  duration:   3600 duration_time:0.04    pos: 
      665464 fmt:yuvj420p sar:0/1 s:850x486 i:P iskey:1 type:I checksum:5F63E050 plane_checksum:[72CCB4CD 3A06A540 A6F28634
    ] mean:[127 117 133] stdev:[69.9 12.6 8.1]
    
    # AI对各参数解释如下:
    n: 帧的编号,表示该帧是原视频中的第49帧。    # 实际n代表的是该帧在新视频中的帧编号, 与原视频无关
    pts: 帧的展示时间戳(Presentation Timestamp),在这个例子中,为352800。
    pts_time: 帧的展示时间戳,以秒为单位,即3.92秒。        # 我的程序最终所使用的参数
    duration: 帧的时长,即持续时间为3600。
    duration_time: 帧的时长,以秒为单位,即0.04秒。
    pos: 帧在输入文件中的位置,这里显示为947116。
    fmt: 像素格式,这里为yuvj420p。
    sar: 像素的采样比例,这里为0/1,表示未定义。
    s: 视频帧的分辨率,这里为1920x1080。
    i: 帧的类型,这里为P帧。
    iskey: 帧是否为关键帧(keyframe),这里为0,表示不是关键帧。
    type: 帧的类型,这里为P帧。
    checksum: 帧的校验和,这里为EBA0BE0F。
    plane_checksum: 平面校验和,表示帧的不同色彩通道的校验和。
    mean: 帧的像素平均值,表示每个色彩通道的像素平均值为[117, 126, 129]。
    stdev: 帧的像素标准差,表示每个色彩通道的像素标准差为[61.5, 8.7, 7.3]。
    
    

    获取到了保留帧信息日志, 接下来问题就变为了如何在视频中提取指定帧, 于是, 针对日志中的各种参数, 开始了旷日持久的尝试….

    尝试四 ffmpeg concat

    使用ffmpeg concat 读取文件的功能,AI相关对话记录

    # 指令如下:
    ffmpeg -f concat -i timestamps.txt -c copy ceshi3.mp4
    
    # AI给出的 timestamps.txt 文件内格式为:
    file 'ceshi.mp4'    # 文件地址
    inpoint 3.437000    # 片段开始时间
    outpoint 4.789000    # 片段结束时间
    file 'ceshi.mp4'
    inpoint 5.123456
    outpoint 6.987654
    ...
    
    

    测试结果:

  • 输出视频会出现闪回的现象, 就是这个片段明明刚刚过了, 又来一遍(非常影响观看)

  • 输出视频在无移动时(只是光线变化造成捕获), 会卡在某一帧不动, 然后移动到下一个卡顿处, 又卡住不动

  • 猜测是指定时间时, 会往前回溯关键帧, 然后取持续时间的视频长度, 或者是指定的时间不连贯, 导致的频繁回溯

    经统计, 需要捕获的帧里面各种类型数量对比: {'I': 1332, 'P': 32201}

    经统计, 原视频关键帧分布较为均匀, 约24秒一个关键帧, 无论是否出现移动, 关键帧频率几乎不受影响

    优化方案: 指定一个时间跨度, 将需要捕获的相邻两个帧的时间差 大于 跨度的帧丢弃, 小于 跨度的帧连接(取前一帧的头至后一帧的尾, 作为一个concat节点)

    经测试发现: 更改时间跨度的值(具体为一帧时间长度的倍数), 可以缓解闪回和卡顿现象, 但是无法完全消除

    尝试五 ffmpeg select='eq()'

    使用 ffmpeg select= eq(pts,…)+eq(pts,…)进行抽帧,相关AI对话记录

    ffmpeg -i ./ceshi.mp4 -vf "select='eq(pts,2856816180)+eq(pts,2856848760)+eq(...',setpts=N/FRAME_RATE/TB" -y ceshi_15.mp4
    
    

    这个方式对于视频帧数较少时, 非常适用, 压缩后的视频也没有问题

    但是当视频时长达到: 9小时/40W帧, 需要保留其中7000+帧时

    • win10 cmd 最大只支持8000+个字符的指令, 而且ffmpeg本身对指令长度也有限制. 一个包含7000帧的完整指令, 需要拆分成20+个小指令

      • 我一度沉迷于把该指令的select参数写入文件中, 以绕过cmd的指令长度限制, 但均告失败, 相关AI对线实录1相关AI对线实录2
    • 当pts靠近视频前面时, 抽帧完会卡住(CPU保持100%运转), 程序无法结束

    • 当pts靠近视频后面时, 一开始就会卡住(CPU保持100%运转), 无法开始抽帧

    猜测是因为程序需要逐帧读取, 以到达指定的帧位置

    至此, 只使用ffmpeg 的方式已经无法达到我想要的结果了….于是开始考虑使用其它软件来抽取视频中的指定帧,比如opencv

    尝试六 opencv 逐帧读取, 比对帧号

    # 日志文件内容如下:
    [Parsed_showinfo_2 @ 000002c311b40a00] n:   1 pts:2141190 pts_time:23.791  duration:   3600 duration_time:0.04    pos: 
      660900 fmt:yuvj420p sar:0/1 s:850x486 i:P iskey:0 type:P checksum:E7F119D0 plane_checksum:[CE36ADF8 B187A046 0BDFCB74
    ] mean:[128 117 133] stdev:[70.1 12.7 8.3]
    
    # 当duration恒定时, pts/duration的值就是该帧在原视频中的帧号
    
    cap = cv2.VideoCapture(file_path)
    fourcc = cv2.VideoWriter_fourcc('M', 'P', '4', '2')  # 初始化视频写入器
    fps = cap.get(cv2.CAP_PROP_FPS)  # 帧率
    out = cv2.VideoWriter('./ceshi3.avi', fourcc, fps, (int(cap.get(3)), int(cap.get(4))))    # 构建新视频
    with open('./mpdecimate_log.txt', encoding='utf16') as f:
        time_message = f.read()        # 读取日志文件内容
    n = -1
    for item in re.finditer(r'n:.*?pts: *(?Pd+).*?duration: *(?Pd+)',time_message):
        # 从ffmpeg mpdecimate 的日志中提取 pts 和 duration参数, 计算该帧处于原视频中的序号
        pts = item.group('pts')
        duration = item.group('duration')
        frame_num = int(pts/duration)
        while True:
            n += 1
            ret, frame = cap.read()  # 逐帧读取
            if not ret: break
            if n &1
    
    # CPU解码, GPU编码, 40W帧 耗时: 600秒
    ffmpeg -i ./ceshi.mp4 -c:v h264_nvenc -vf 'crop=850:486:1070:0,mpdecimate=hi=64*120:lo=65*50:frac=0.33,showinfo' -f null - > mpdecimate_log.txt 2>&1
    
    # GPU解码, GPU编码, 40W帧 耗时: 更慢....直奔1000秒去了
    ffmpeg -c:v hevc_cuvid -i ./ceshi.mp4 -c:v h264_nvenc -vf 'crop=850:486:1070:0,mpdecimate=hi=64*120:lo=65*50:frac=0.33,showinfo' -f null - > mpdecimate_log.txt 2>&1
    

    结论是, CPU是最快的…..

    猜测是因为mpdecimate只能用CPU去运算, 数据需要在GPU与CPU之间传递, 导致速度变慢

    尝试十 opencv 跳转到指定时间读取帧

    cap.set(cv2.CAP_PROP_POS_MSEC, pts_time)  # 跳转到指定毫秒数, 比较耗时 ≈0.3秒
    
    cap = cv2.VideoCapture(file_path)
    fourcc = cv2.VideoWriter_fourcc('M', 'P', '4', '2')  # 初始化视频写入器
    fps = cap.get(cv2.CAP_PROP_FPS)  # 帧率
    out = cv2.VideoWriter('./ceshi3.avi', fourcc, fps, (int(cap.get(3)), int(cap.get(4))))    # 构建新视频
    with open('./mpdecimate_log.txt', encoding='utf16') as f:
        time_message = f.read()        # 读取日志文件内容

    pts_time_last = 0
    for item in re.finditer(r'n:.*?pts_time: *(?Pd+(.d+)?)', time_message):
        # 从ffmpeg mpdecimate 的日志中提取 pts_time 数据
        pts_time_str = item.group('pts_time')
        pts_time = int(float(pts_time_str) * 1000)  # 毫秒
        if (pts_time - pts_time_last) / 1000 * fps >= 270:
            # 如果相邻两帧时间差值相距超过 270帧, 则直接跳转到该帧读取, 不再逐帧跳过
            # 这个值是在CPU: i5 9300h 条件下测试得到
            # 该条件下 cap.grab() 连续跳过270帧耗时与 cap.set(cv2.CAP_PROP_POS_MSEC, pts_time)相当, 0.3秒
            cap.set(cv2.CAP_PROP_POS_MSEC, pts_time)  # 跳转到指定毫秒数, 比较耗时 ≈0.3秒
            ret, frame = cap.retrieve()
            pts_time_last = pts_time
            out.write(frame)
            continue
        while True:
            ret = cap.grab()  # 跳转到下一帧
            if not ret: break
            if cap.get(cv2.CAP_PROP_POS_MSEC) = skip_frame:
                cap.set(cv2.CAP_PROP_POS_MSEC, pts_time)  # 跳转到指定毫秒数, 比较耗时 ≈0.3秒
                ret, frame = cap.retrieve()
                pts_time_last = pts_time
                sys.stdout.write(f"r 进度:{round(pts_time / cv_total_msec * 100, 2)}")
                sys.stdout.flush()
                yield frame
                continue
            while True:
                ret = cap.grab()  # 逐帧跳过
                if not ret: break
                if cap.get(cv2.CAP_PROP_POS_MSEC) 

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论