基于泛洪填充算法的视频监控障碍物检测

构思

将摄像头拍到的画面大概分为三层:背景层,动态层,障碍层,如示意图:

分层示意图

背景层为绿色区域,动态层为蓝色区域,障碍层为红色区域。

其中障碍层是我们需要得到的结果,动态层和背景层需要被剔除。

我们读取一段完整视频,通过对比上一帧和下一帧找到画面中运动的区域,再对该区域进行泛洪填充算法扩散到背景层,像PS里的快速选择工具一样,每一个会动的物体的路径是快速选择画笔挥舞的方向,即可实现障碍层的分离。

PS中的快速选择工具

视频素材

b站找了些需要用到的素材

来源:监控录像里拍摄到的悲剧傻缺视频集锦_哔哩哔哩_bilibili

从中截取了代表性片段:

001-摩托车摔下

代码编译

运动比对

可以使用OpenCV中的帧差法(Frame Difference)来检测两个连续帧之间的差异,再将每一帧的结果叠加,记录运动路径,得到运动层区域。

while True:
    # 逐帧读取视频
    ret, curr_frame = video.read()

    # 如果视频读取结束,则跳出循环
    if not ret:
        break

    # 计算当前帧与初始帧的差异
    diff = cv2.absdiff(prev_frame, curr_frame)

    # 将差异图像转换为灰度图像
    gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)

    # 对灰度图像进行阈值处理
    _, thresh = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY)

    # 对二值图像进行腐蚀和膨胀操作,以去除噪声
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

    # 在原始帧上绘制静止区域的边界框
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    for contour in contours:
        (x, y, w, h) = cv2.boundingRect(contour)
        cv2.rectangle(curr_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

    # 显示当前帧
    cv2.imshow('Static Region', curr_frame)

    # 按下 'q' 键退出播放
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

    # 将当前帧设为初始帧
    prev_frame = curr_frame

将运动追踪结果使用绿色方框表示,得到的结果很细散,我们可以进一步对图像腐蚀和膨胀让运动像素连通,也可以忽略掉运动太小的区域,优化后对比如图:

运动比对优化前后

优化后画面右侧玻璃反光也得到了改善,且运行速度变快。

泛洪填充

泛洪填充的效果十分类似于PS中的快速选择工具或油漆桶,可以查找颜色相近的区域。

自动调节扩散容差

floodFill()函数的参数关键在于指定扩散容差,由loDiff和upDiff决定。官方是这样解释的:

loDiff(lower difference)参数是一个整数类型的值,指定了种子像素与相邻像素之间的最小颜色差异。当像素之间的颜色差异小于loDiff时,这些像素被填充到同一个区域内。

upDiff(upper difference)参数也是一个整数类型的值,指定了种子像素与相邻像素之间的最大颜色差异。当像素之间的颜色差异大于upDiff时,这些像素不会被填充到同一个区域内。

我读了之后云里雾里,不明白种子像素与相邻像素之间的最小与最大颜色的体现,具体应该怎么调节,所以这里只给loDiff和upDiff相同参数。

floodFill()函数返回值的第一个是填充像素数目,我们可以设置一个最小最大值来判断填充效果,以此调节扩散容差。为了防止程序跑死或始终不在调节期望区间,设置最大调节次数,加入阈值反复横跳判断。

在填充后的图像上画一个绿色圆圈代表已进行泛洪填充。

def Fill_Pexl(x, y, color, low, high):
    global mask, first_frame
    thresh = 5        # 初始阈值
    img_copy = []
    mask_copy = []
    last_step = 0     # 上一步操作
    times = 0         # 反复横跳次数
    runtimes = 0      # 运行次数
    while True:
        runtimes = runtimes + 1
        img_copy = first_frame.copy()
        mask_copy = mask.copy()
        area = cv2.floodFill(img_copy, mask_copy, (x, y), color, (thresh,thresh,thresh), (thresh,thresh,thresh))
        print(thresh, area[0])
        if low < area[0] < high:
            break
        elif runtimes > 10 and (last_step == 2 or runtimes > 100): # 防止跑死
            break
        elif times > 3 and last_step == 2:                         # 防止阈值反复横跳
            break
        elif low >= area[0]:
            if last_step == 1:
                times = 0
            else:
                times = times + 1
            last_step = 1
            thresh += 1
        elif high <= area[0]:
            if last_step == 2:
                times = 0
            else:
                times = times + 1
            last_step = 2
            if thresh > 0:
                thresh -= 1
            else:
                print("忽略")
                break
    mask = array2uint8(mask_copy)
    first_frame = img_copy
    cv2.namedWindow('mask res', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('mask res', 800, 450)
    cv2.imshow('mask res', mask)
    cv2.circle(first_frame, (x, y), 20, (0, 255, 0), 5)
    cv2.namedWindow('first_frame', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('first_frame', 800, 450)
    cv2.imshow('first_frame', first_frame)
    cv2.waitKey(1)

设置泛洪起始位置

我们在通过运动比对后得到的运动层区域的基础上,设置最小步进,如果该点是运动层区域且还未被上一步的泛洪填充,就进行泛洪填充。

def floodFill_step(step):
    lenx = len(flash_mask)        # 长宽
    leny = len(flash_mask[0])
    for x in range(50, lenx):     # 从50开始遍历,防止对画面边角进行泛洪填充
        for y in range(50, leny):
        # 判断是否满足步进长度            
        if x % step == 0 and y % step == 0 and flash_mask[x][y] == 255 and mask[x][y] != 255:                           
                print("next:", y, x)
                Fill_Pexl(y, x, (255, 0, 0), 1000, 200000)

理论上步进越小,该方法越准确,但为了运行速度设置为100像素。

泛洪填充结果

最后处理

把泛洪填充结果叠加运动层,黑色区域即为障碍层:

001-结果

结果大概是正确的,完成!

结果优化方向

可以发现结果大概是正确的,但存在某些问题:

填充部分不连续

地砖处有明显缝隙,部分地砖区域坑坑洼洼。

可以对泛洪填充原图像先进行高斯模糊,整体降低扩散容差,或对泛洪填充区域进行腐蚀和膨胀来消除。

# 高斯模糊
first_frame = cv2.GaussianBlur(curr_frame, (3, 3), 0)

# 腐蚀膨胀
# 定义结构元素(膨胀和腐蚀操作的内核)
kernel = np.ones((5, 5), np.uint8)
# 膨胀操作(填充散点)
mask = cv2.dilate(mask, kernel, iterations=1)
# 腐蚀操作(连接填充后的区域)
mask_fix = cv2.erode(mask, kernel, iterations=1)

扩散到了障碍层

左侧面包车部分识别错误。

在运动比对过程中,运动层的记录的不是精确的运动像素,而是大概的相邻运动像素组成的最小矩形,这是为了防止运动层的图像过于不连续和方便选取泛洪填充起始位置的无奈之举。

点与面

测试分析

发生错误代表性场景

我继续从b站挑选代表性场景:

002-送快递溜车
002-结果

在该视频中,快递员经过的区域都识别为非障碍物,但显然左右边墙在常规认知中属于障碍物,该算法会对运动区域无条件过滤掉,但从方法构思来看,效果是达到了目标的。

003-文明社区豹
003-结果

在该视频中,草坪和社区均出现识别失误,原因是因为该视频片段动态层区域过小,动态区域未完全经过背景层,该算法不适合识别时间过短或无人场景。

004-欢快蹦跶鹿
004-结果

在该视频中,摄像机被运动物体碰撞引起了大幅度抖动,导致动态层识别严重错误,该算法适合画面稳定的场景。

结论

该算法在摄像头稳定且运动物体活跃的情况下效果显著。

完整代码

import cv2
import numpy as np

# 创建窗口并设置初始大小
cv2.namedWindow('Dynamic Region', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Dynamic Region', 800, 450)
cv2.namedWindow('Occlusions', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Occlusions', 800, 450)
cv2.namedWindow('bin_img', cv2.WINDOW_NORMAL)
cv2.resizeWindow('bin_img', 800, 450)
cv2.namedWindow('flash_area', cv2.WINDOW_NORMAL)
cv2.resizeWindow('flash_area', 800, 450)
cv2.namedWindow('Dynamic kernel', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Dynamic kernel', 800, 450)
# 调用函数查找静止区域
file_path = '监控/001.mp4'
frame_height = 0
frame_width = 0
last_bin_img = []       # 二值化               图像最后一帧
first_frame = []        # 视频                 第一帧
mask = []               # 洪水填充遮罩结果
flash_mask = []         # 动态区域遮罩结果
outcome = []            # 最终结果


def array2uint8(array):
    mask8 = np.array(array, np.uint8)
    lenx = len(mask8)
    leny = len(mask8[0])
    for x in range(lenx):
        for y in range(leny):
            if mask8[x][y] == 1:
                mask8[x][y] = 255
    return mask8


def Fill_Pexl(x, y, color, low, high):
    global mask, first_frame
    thresh = 5
    img_copy = []
    mask_copy = []
    last_step = 0
    times = 0
    runtimes = 0
    while True:
        runtimes = runtimes + 1
        img_copy = first_frame.copy()
        mask_copy = mask.copy()
        area = cv2.floodFill(img_copy, mask_copy, (x, y), color, (thresh,thresh,thresh), (thresh,thresh,thresh))
        print(thresh, area[0])
        if low < area[0] < high:
            break
        elif runtimes > 10 and (last_step == 2 or runtimes > 100):
            break
        elif times > 3 and last_step == 2:
            break
        elif low >= area[0]:
            if last_step == 1:
                times = 0
            else:
                times = times + 1
            last_step = 1
            thresh += 1
        elif high <= area[0]:
            if last_step == 2:
                times = 0
            else:
                times = times + 1
            last_step = 2
            if thresh > 0:
                thresh -= 1
            else:
                print("忽略")
                break


    mask = array2uint8(mask_copy)
    first_frame = img_copy
    cv2.namedWindow('mask res', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('mask res', 800, 450)
    cv2.imshow('mask res', mask)
    cv2.circle(first_frame, (x, y), 20, (0, 255, 0), 5)
    cv2.namedWindow('first_frame', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('first_frame', 800, 450)
    cv2.imshow('first_frame', first_frame)
    cv2.waitKey(1)


def add_flash_area(x, y, w, h):
    global flash_mask
    # print(x, y, w, h)
    for i in range(w):
        for j in range(h):
            flash_mask[y + j][x + i] = 255


def find_static_region(file_path):
    global first_frame, frame_height, frame_width, flash_mask, outcome, last_bin_img
    frame_num = 0
    # 打开视频文件
    video = cv2.VideoCapture(file_path)
    # 获取视频帧的高度和宽度
    frame_height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
    frame_width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
    flash_mask = np.zeros((frame_height, frame_width), np.uint8)
    outcome = np.zeros((frame_height, frame_width), np.uint8)
    total_pixel = frame_height * frame_width
    # print(frame_height, frame_width, "总像素:", total_pixel)
    # 读取第一帧作为初始帧
    ret, prev_frame = video.read()

    while True:
        # 逐帧读取视频
        ret, curr_frame = video.read()
        if frame_num == 0:
            # first_frame = curr_frame
            first_frame = cv2.GaussianBlur(curr_frame, (3, 3), 0)
        # 如果视频读取结束,则跳出循环
        if not ret:
            break

        # # 转换为灰度图像
        gray_img = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
        # 计算全局阈值
        _, bin_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

        # 计算当前帧与初始帧的差异
        diff = cv2.absdiff(prev_frame, curr_frame)

        # 将差异图像转换为灰度图像
        gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)

        # 对灰度图像进行阈值处理
        _, thresh = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY)

        # 定义结构元素(膨胀和腐蚀操作的内核)
        kernel = np.ones((5, 5), np.uint8)

        # 膨胀操作(填充散点)
        dilated_img = cv2.dilate(thresh, kernel, iterations=1)

        # 腐蚀操作(连接填充后的区域)
        thresh = cv2.erode(dilated_img, kernel, iterations=1)

        # 对二值图像进行腐蚀和膨胀操作,以去除噪声
        kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (31, 31))
        bin_img = cv2.morphologyEx(bin_img, cv2.MORPH_OPEN, kernel1)
        last_bin_img = bin_img
        # 反转二值图像
        thresh_inv = cv2.bitwise_not(bin_img)
        # # 在数字区域添加白底
        # bg_color = (255, 0, 0)
        # cv2.rectangle(bin_img, (0, 0), (300, 80), bg_color, -1)
        # cv2.putText(bin_img, str(1 - (black_pixels / total_pixel)), (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0),
        #             2, cv2.LINE_AA)
        cv2.imshow('bin_img', bin_img)
        cv2.imshow('Occlusions', thresh_inv)
        rectangle_frame = curr_frame.copy()
        # 在原始帧上绘制静止区域的边界框
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        for contour in contours:
            (x, y, w, h) = cv2.boundingRect(contour)
            if w > 50 and h > 50:
                cv2.rectangle(rectangle_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                add_flash_area(x, y, w, h)
            # cv2.rectangle(rectangle_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
            # add_flash_area(x, y, w, h)
        # 显示当前帧
        cv2.imshow('Dynamic Region', rectangle_frame)
        cv2.imshow('Dynamic kernel', thresh)
        cv2.imshow('flash_area', flash_mask)
        # 按下 'q' 键退出播放
        if cv2.waitKey(1) & 0xFF in (ord('q'), 27):
            break

        # 将当前帧设为初始帧
        if frame_num % 2 == 0:
            prev_frame = curr_frame
        frame_num = frame_num + 1
        # cv2.imshow('prev_frame', curr_frame)

    # 关闭视频文件和窗口
    video.release()
    cv2.destroyAllWindows()


def floodFill_step(step):
    lenx = len(flash_mask)
    leny = len(flash_mask[0])
    for x in range(50, lenx):
        for y in range(50, leny):
            if x % step == 0 and y % step == 0 and flash_mask[x][y] == 255 and mask[x][y] != 255:
                print("next:", y, x)
                Fill_Pexl(y, x, (255, 0, 0), 1000, 200000)


def all_add():
    global last_bin_img, mask, flash_mask, outcome
    width, height = 800, 450
    last_bin_img = cv2.resize(last_bin_img, (width, height))
    mask = cv2.resize(mask, (width, height))
    flash_mask = cv2.resize(flash_mask, (width, height))
    # outcome = cv2.bitwise_or(last_bin_img, mask)
    outcome = cv2.bitwise_or(mask, flash_mask)
    # 计算黑色像素的数量
    black_pixels = cv2.countNonZero(outcome)
    num = black_pixels / (width * height)
    print("障碍物占比:", num)
    # 显示结果
    cv2.imshow('Union', outcome)
    cv2.imshow('last_bin_img', last_bin_img)
    cv2.imshow('flash_mask', flash_mask)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

find_static_region(file_path)
mask = np.zeros((frame_height + 2, frame_width + 2), np.uint8)
floodFill_step(100)
all_add()
# cv2.waitKey(5000)
  • 33
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
泛洪填充(Flood Fill)是一种图像处理算法,用于将图像中的一个区域或连通分量用指定的颜色进行填充。在Python OpenCV中,可以使用cv2.floodFill()函数来实现泛洪填充泛洪填充函数的原型如下: cv.floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]]) -> retval, image, mask, rect 其中,参数解释如下: - image:输入图像,可以是灰度图像或彩色图像。 - mask:掩膜图像,用于指定填充的区域。图像大小必须比输入图像的大小大2。 - seedPoint:起始点,填充的起始位置。 - newVal:新的像素值,填充的颜色。 - loDiff和upDiff:下界和上界,用于指定填充的范围。如果不指定,默认为(0,0,0)和(0,0,0),表示填充指定像素值的区域。 - flags:填充算法的标志,可以是cv.FLOODFILL_FIXED_RANGE或cv.FLOODFILL_MASK_ONLY。 在泛洪填充中,可以根据需要进行彩色图像填充或二值图像填充。为了演示不同的填充方式,我分别提供了两个例子: 1. 彩色图像填充: ```python import cv2 as cv import numpy as np def fill_color_demo(image): copyImg = image.copy() h, w = image.shape[:2] mask = np.zeros([h+2, w+2], np.uint8) cv.floodFill(copyImg, mask, (220, 250), (0, 255, 255), (100, 100, 100), (50, 50 ,50), cv.FLOODFILL_FIXED_RANGE) cv.imshow("fill_color_demo", copyImg) src = cv.imread('E:/imageload/baboon.jpg') cv.namedWindow('input_image', cv.WINDOW_AUTOSIZE) cv.imshow('input_image', src) fill_color_demo(src) cv.waitKey(0) cv.destroyAllWindows() ``` 2. 二值图像填充: ```python import cv2 as cv import numpy as np def fill_binary(): image = np.zeros([400, 400, 3], np.uint8) image[100:300, 100:300] = 255 mask = np.ones([402, 402], np.uint8) mask[101:301, 101:301] = 0 cv.floodFill(image, mask, (200,200), (255 , 0, 0), cv.FLOODFILL_MASK_ONLY) cv.imshow("filled_binary", image) fill_binary() cv.waitKey(0) cv.destroyAllWindows() ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hostonlin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值