在这里插入图片描述

1、背景分割器介绍

在OpenCV中,背景分割器是处理视频或图像序列以区分前景(如移动物体)和背景的重要工具。以下是对OpenCV中几种常用背景分割器(MOG2、KNN、GMG)的使用场景、优缺点的详细解释:

一、Mixture of Gaussians (MOG2)

使用场景:

  • 适用于场景中存在很多变化和动态光照条件的情况。
  • 在处理光照变化和动态背景时表现良好

优点:

  • 对光照变化有较好的适应性。
  • 在动态背景环境下表现良好。

缺点:

  • 对于相对静态的背景可能效果不佳。

二、K-Nearest Neighbors (KNN)

使用场景:

  • 在处理运动目标和部分遮挡情况时可能更有效。

优点:

  • 根据像素的颜色特征和邻近像素的情况进行分类,因此在处理运动目标和部分遮挡时可能更有效。

缺点:

  • 在复杂动态背景下表现可能欠佳。

三、Geometric Multigrid (GMG)

使用场景:

  • 在动态背景和光照变化下表现较好。

优点:

  • 结合了几何学和统计学的方法,对动态背景和光照变化具有较好的鲁棒性。

缺点:

  • 对于较为静态的场景可能不够精确。

四、总结

MOG2 更适合光照变化和动态背景较多的场景。

KNN 在处理运动目标和部分遮挡时可能更有效,但在复杂动态背景下可能表现不佳。

GMG 在动态背景和光照变化下表现良好,但可能在静态场景下不够精确。

在选择背景分割器时,需要根据具体的应用场景和需求进行权衡。例如,如果场景中光照变化较大,那么MOG2可能是更好的选择;而如果主要目标是检测运动目标并处理部分遮挡,那么KNN可能更为合适。

opencv 中背景分割器可借助 BackgroundSubtractor 类,检测阴影,用阈值排除阴影,从而关注实际特征

2、cv2.createBackgroundSubtractorMOG2

OpenCV图像处理- 视频背景消除与前景ROI提取

createBackgroundSubtractorMOG2 是 OpenCV 库中的一个函数,用于创建基于混合高斯模型(Mixture of Gaussians, MOG2)的背景减除器。以下是对 createBackgroundSubtractorMOG2 的中文文档,清晰地分点表示和归纳了其主要内容:

一、函数概述

createBackgroundSubtractorMOG2 是 OpenCV 中用于背景/前景分割的算法之一,基于混合高斯模型(MOG2)进行背景建模和前景检测。该函数允许用户通过调整参数来适应不同的应用场景。

二、函数原型

retval = cv2.createBackgroundSubtractorMOG2([, history[, varThreshold[, detectShadows]]])

三、参数解释

history(可选,整型,默认值 200):

  • 表示用于训练背景模型的时间长度,单位是帧数。它决定了背景模型更新的速度,值越大,背景模型更新的速度就越慢——过往帧数,500帧,选择history = 1就变成两帧差

varThreshold(可选,浮点型,默认值 15):

  • 用于判断像素是否为背景的阈值。这个参数对结果有很大影响,值越小,检测到的运动物体就越多,但也可能增加误检——像素与模型之间的马氏距离,值越大,只有那些最新的像素会被归到前景,值越小前景对光照越敏感。

detectShadows(可选,布尔型,默认值 True):

  • 一个布尔值,用于指示算法是否检测阴影并对其进行标记。启用阴影检测会稍微降低速度,因此如果不需要此功能,建议将其设置为 False。

四、返回值

该函数返回一个背景减除器对象,该对象可以用于对视频帧或图像序列进行前景检测。

五、工作原理

模型初始化:在开始时,算法会对视频中的每个像素建立一个混合高斯模型。这个模型会学习并适应场景中的背景变化。

背景建模:随着新帧的到来,算法会更新每个像素的高斯分布。对于与现有高斯分布匹配良好的像素,这些分布会被更新以反映最新的像素值。对于不匹配任何现有分布的像素,会创建新的高斯分布或替换最不可能代表背景的高斯分布。

前景检测:如果某个像素的值与所有高斯分布都不匹配,或者只与表示前景的高斯分布匹配,则该像素被视为前景像素。

阴影检测(如果启用):算法还可以检测并标记出由于前景对象遮挡而产生的阴影区域。

六、使用场景

动态背景:在背景经常变化的情况下,如光照变化、树叶摇动等,MOG2 表现出较好的鲁棒性。

视频监控:在视频监控系统中,用于检测运动物体或行人。

实时分析:由于 MOG2 的计算效率较高,因此适用于需要实时处理的应用场景。

七、注意事项

参数调整:根据具体的应用场景和需求,可能需要调整 history、varThreshold 和 detectShadows 等参数以获得最佳效果。

性能考虑:虽然 MOG2 在大多数情况下都能提供较好的性能,但在某些极端情况下(如快速变化的光照条件),可能需要考虑使用其他更复杂的背景减除算法。

import cv2
import os
# bs = cv2.createBackgroundSubtractorKNN(detectShadows=True)
bs = cv2.createBackgroundSubtractorMOG2(detectShadows=True)
os.makedirs("frame1", exist_ok=True)
os.makedirs("frame2", exist_ok=True)
os.makedirs("frame3", exist_ok=True)

camera = cv2.VideoCapture('car.mkv')
index = 0
while True:
    ret, frame = camera.read()
    index += 1
    frame_h, frame_w, _ = frame.shape
    fgmask = bs.apply(frame)
    th = cv2.threshold(fgmask.copy(), 244, 255, cv2.THRESH_BINARY)[1]
    dilated = cv2.dilate(th, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)),
                         iterations=2)
    contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    for c in contours:
        # if cv2.contourArea(c) > frame_w*0.075 * frame_h*0.075:
        if cv2.contourArea(c) > 1000:
            (x, y, w, h) = cv2.boundingRect(c)
            cv2.rectangle(frame, (x,y), (x+w, y+h), (0, 0, 255), 5)
    cv2.imshow("mog", fgmask)
    cv2.imwrite("./frame1/{}.jpg".format(index), fgmask)
    cv2.imshow("thresh", th)
    cv2.imwrite("./frame2/{}.jpg".format(index), th)
    cv2.imshow("detection", frame)
    cv2.imwrite("./frame3/{}.jpg".format(index), frame)

    if cv2.waitKey(30) & 0xff == ord("q"):
        break

camera.release()
cv2.destroyAllWindows()

请添加图片描述

请添加图片描述

请添加图片描述
做 gif 的时候只设置了播放一次,重复播放需要刷新该页面

3、cv2.createBackgroundSubtractorKNN

用法基本和 cv2.createBackgroundSubtractorMOG2 一致,工作原理如下

模型初始化:在开始时,KNN 算法会收集一系列初始帧来构建背景模型。

背景建模:对于每个新的帧,KNN 算法会计算当前像素与背景模型中所有样本之间的距离,并基于这些距离来决定该像素是前景还是背景。

前景检测:如果当前像素与所有背景模型中的样本距离都大于 dist2Threshold,则该像素被视为前景。

阴影检测(如果启用):如果 detectShadows 设置为 True,KNN 算法会尝试检测并标记出前景中的阴影区域。

import cv2
import numpy as np
bs = cv2.createBackgroundSubtractorKNN(detectShadows=True)
camera = cv2.VideoCapture('car.mkv')
index = 0
while True:
    ret, frame = camera.read()
    index += 1
    frame_h, frame_w, _ = frame.shape
    fgmask = bs.apply(frame)
    th = cv2.threshold(fgmask.copy(), 244, 255, cv2.THRESH_BINARY)[1]
    dilated = cv2.dilate(th, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)),
                         iterations=2)
    contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    for c in contours:
        # if cv2.contourArea(c) > frame_w*0.075 * frame_h*0.075:
        if cv2.contourArea(c) > 1000:
            (x, y, w, h) = cv2.boundingRect(c)
            cv2.rectangle(frame, (x,y), (x+w, y+h), (0, 0, 255), 5)
    cv2.imshow("mog", fgmask)
    cv2.imwrite("./frame1/{}.jpg".format(index), fgmask)
    cv2.imshow("thresh", th)
    cv2.imwrite("./frame2/{}.jpg".format(index), th)
    cv2.imshow("detection", frame)
    cv2.imwrite("./frame3/{}.jpg".format(index), frame)

    if cv2.waitKey(30) & 0xff == ord("q"):
        break

camera.release()
cv2.destroyAllWindows()

请添加图片描述

请添加图片描述

请添加图片描述

4、跟踪目标中引入目标 id

上面相当于检测出来了目标,下面给每个目标一个 id,就更贴近跟踪的应用场景了(虽然还做不到同一个目标来回出现,id 不跳动)

import cv2
from tracker import *

# Create tracker object
tracker = EuclideanDistTracker()

cap = cv2.VideoCapture("highway.mp4")

# Object detection from Stable camera
object_detector = cv2.createBackgroundSubtractorMOG2(history=100, varThreshold=40)

num = 0

while True:
    num += 1
    ret, frame = cap.read()
    height, width, _ = frame.shape

    # Extract Region of interest
    roi = frame[340: 720, 500: 800]

    # 1. Object Detection
    mask = object_detector.apply(roi)
    _, mask = cv2.threshold(mask, 254, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    detections = []
    for cnt in contours:
        # Calculate area and remove small elements
        area = cv2.contourArea(cnt)
        if area > 100:
            #cv2.drawContours(roi, [cnt], -1, (0, 255, 0), 2)
            x, y, w, h = cv2.boundingRect(cnt)
            detections.append([x, y, w, h])

    # 2. Object Tracking
    boxes_ids = tracker.update(detections)
    for box_id in boxes_ids:
        x, y, w, h, id = box_id
        cv2.putText(roi, str(id), (x, y - 15), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)
        cv2.rectangle(roi, (x, y), (x + w, y + h), (0, 255, 0), 3)

    cv2.imshow("roi", roi)
    cv2.imshow("Frame", frame)
    cv2.imshow("Mask", mask)

    if 0:  # 保存结果
        index = str(num).zfill(3)
        cv2.imwrite(f"./roi/{index}.jpg", roi)
        cv2.imwrite(f"./Frame/{index}.jpg", frame)
        cv2.imwrite(f"./Mask/{index}.jpg", mask)

    key = cv2.waitKey(30)
    if key == 27:
        break

cap.release()
cv2.destroyAllWindows()

上面展示的代码和之前章节的没有多大差异

看看核心赋予 id 的部分, EuclideanDistTracker 的实现如下

import math

class EuclideanDistTracker:
    def __init__(self):
        # Store the center positions of the objects
        self.center_points = {}
        # Keep the count of the IDs
        # each time a new object id detected, the count will increase by one
        self.id_count = 0

    def update(self, objects_rect):
        # Objects boxes and ids
        objects_bbs_ids = []  # 存放历史 bbox

        # Get center point of new object
        for rect in objects_rect:  # 遍历当前帧的所有 bbox
            x, y, w, h = rect
            cx = (x + x + w) // 2  # 计算中心点坐标
            cy = (y + y + h) // 2

            # Find out if that object was detected already
            same_object_detected = False
            for id, pt in self.center_points.items():  # 遍历历史帧所有存储的 bbox
                dist = math.hypot(cx - pt[0], cy - pt[1])  # 计算当前帧和历史帧的中心点距离

                if dist < 25:  # 中心点的距离小于 25,就判定是已存在的目标
                    self.center_points[id] = (cx, cy)
                    # print(self.center_points)
                    objects_bbs_ids.append([x, y, w, h, id])
                    same_object_detected = True
                    break

            # New object is detected we assign the ID to that object
            if same_object_detected is False:
                self.center_points[self.id_count] = (cx, cy)  # 如果当前bbox不存在于历史的bbox,加入历史列表中
                objects_bbs_ids.append([x, y, w, h, self.id_count])
                self.id_count += 1

        if 0:  # 感觉不到下面代码的作用???
            # Clean the dictionary by center points to remove IDS not used anymore
            new_center_points = {}
            for obj_bb_id in objects_bbs_ids:
                _, _, _, _, object_id = obj_bb_id
                center = self.center_points[object_id]
                new_center_points[object_id] = center

            # Update dictionary with IDs not used removed
            self.center_points = new_center_points.copy()

        # print(objects_bbs_ids)
        return objects_bbs_ids

核心思想是通过判定历史帧所有 bbox 的中心点与当前帧所有中心点的 bbox 的欧氏距离,小于一定阈值就默认为同一物体,id 不更新,高于阈值则认为新的物体出现,赋予新的 id

缺点也是很明显,移动较快的时候 id 更新不过来,同一物体来回在画面移动,id 也会一直更新

最后看看实际效果

原始视频

tracking-highway

roi 区域,我们仅跟踪 roi 区域

tracking-roi

跟踪时候的 mask,也即目标检测的输入(通过找轮廓检测出来目标)

tracking-mask

5、涉及到的库函数

5.1、cv2.threshold

cv2.threshold 是 OpenCV 库中的一个重要函数,用于对图像进行阈值化处理。阈值化处理是数字图像处理中常用的一种方法,可以将图像转换为二值图像,即图像中每个像素只有黑白两种颜色。以下是 cv2.threshold 函数的详细中文文档:

一、函数原型

retval, dst = cv2.threshold(src, thresh, maxval, type[, dst])

二、参数说明

src:输入图像,必须是单通道灰度图像。

thresh:阈值,用于对像素值进行比较的界限值。可以是一个固定的数值,也可以是一个自适应阈值算法。

maxval:当像素值大于阈值时,赋予的新像素值。在二值化处理中,通常设置为 255(即白色)。

type:阈值类型,决定了对像素值大于或小于阈值的像素进行何种操作。常见的阈值类型包括:

  • cv2.THRESH_BINARY:大于阈值的像素设置为 maxval,小于等于阈值的像素设置为 0。
  • cv2.THRESH_BINARY_INV:大于阈值的像素设置为 0,小于等于阈值的像素设置为 maxval。
  • cv2.THRESH_TRUNC:大于阈值的像素设置为阈值,小于等于阈值的像素保持不变。
  • cv2.THRESH_TOZERO:大于阈值的像素保持不变,小于等于阈值的像素设置为 0。
  • cv2.THRESH_TOZERO_INV:大于阈值的像素设置为 0,小于等于阈值的像素保持不变。

dst:输出图像,可选参数。如果未提供,将创建一个新的图像来存储结果。

三、返回值

retval:选取的阈值。在某些情况下,如果使用了自适应阈值算法,这个值可能有所不同。

dst:输出图像,与输入图像具有相同的大小和类型。

四、应用场景

阈值处理在图像处理中有广泛的应用,包括但不限于:

  • 图像分割:将图像中的目标物体与背景进行分离。
  • 目标检测:使用轮廓提取等方法进行目标检测和识别。
  • 图像增强:改善图像的对比度和清晰度。
  • 文字识别:更容易地提取出文字区域。
  • 图像压缩:减小图像文件的大小,实现图像的压缩和存储。

5.2、cv2.dilate

cv2.dilate 是 OpenCV 库中的一个函数,用于对图像进行膨胀操作。膨胀是一种形态学操作,它可以用来增加图像中物体的大小,填充图像中的空洞,连接相邻的物体等。以下是 cv2.dilate 函数的详细中文文档:

一、函数原型

dst = cv2.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])

二、参数说明

src:输入图像,可以是灰度图像或彩色图像。

kernel:

  • 用于膨胀操作的结构元素,控制膨胀的形状和大小。
  • 可以使用 cv2.getStructuringElement() 函数来创建结构元素,该函数接受形状、大小等参数。
  • 常见的形状有:MORPH_RECT(矩形)、MORPH_CROSS(交叉形)、MORPH_ELLIPSE(椭圆形)。

dst:

  • 输出图像,与输入图像具有相同的尺寸和深度。
  • 可选参数,如果未提供,将创建一个新的图像来存储结果。

anchor:

  • 结构元素的锚点位置,决定了结构元素在图像上的位置。
  • 默认为 (-1, -1),表示锚点位于结构元素的中心。

iterations:

  • 膨胀操作的迭代次数。
  • 默认为 1,表示只进行一次膨胀操作。

borderType:

  • 边界扩充的类型。
  • 默认为 BORDER_CONSTANT。

borderValue:

  • 边界扩充的数值。
  • 默认为 0。

三、返回值

dst:膨胀后的输出图像。

四、用法解释

cv2.dilate 函数的作用是将图像中的物体进行膨胀操作,使其变大。膨胀操作的效果取决于所使用的结构元素的形状和大小。通常情况下,结构元素可以是矩形、圆形或者自定义形状。膨胀操作会将结构元素覆盖区域内的像素值取最大值(对于灰度图像)或进行逻辑运算(对于二值图像),从而使目标物体的边界变得更加平滑,填充空洞,连接相邻的物体。

5.3、cv2.findContours

cv2.findContours 是 OpenCV 库中用于在二值图像中查找轮廓的函数。以下是对 cv2.findContours 函数的详细中文文档,包括函数原型、参数说明、返回值以及示例代码。

一、函数原型

contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

二、参数说明
image:

  • 输入的二值化图像,一般为灰度图像或二值图像。

mode:轮廓检索模式,指定轮廓的层级关系。

  • cv2.RETR_EXTERNAL:只返回最外层的轮廓。
  • cv2.RETR_LIST:返回所有轮廓,不建立轮廓间的层级关系。
  • cv2.RETR_TREE:返回所有轮廓,并建立完整的层级关系。
  • cv2.RETR_CCOMP:返回所有轮廓,并重建嵌套轮廓的完整层次结构(二级)。

method:轮廓逼近方法,指定轮廓的表示方式。

  • cv2.CHAIN_APPROX_NONE:保存所有的轮廓点。
  • cv2.CHAIN_APPROX_SIMPLE:仅保存轮廓的端点。
  • cv2.CHAIN_APPROX_TC89_L1 和 cv2.CHAIN_APPROX_TC89_KCOS:应用 Teh-Chin 链逼近算法。

contours(可选):

  • 用于存储检测到的轮廓的变量,如果传递了该参数,则不会返回此值。

hierarchy(可选):

  • 用于存储轮廓的层次关系信息的变量,如果传递了该参数,则不会返回此值。

offset(可选):

  • 可选的偏移量,用于在输出轮廓坐标中移动每个点。默认为 Point()。

三、返回值
contours:
轮廓列表,其中每个轮廓都是一个 NumPy 数组,包含该轮廓上所有点的坐标。
hierarchy:
轮廓的层次关系信息的 NumPy 数组,其中每个轮廓的层次关系由四个整数 [next, previous, first child, parent] 组成。

5.4、cv2.boundingRect

cv2.boundingRect 是 OpenCV 库中的一个函数,用于计算点集或灰度图像中非零像素的最小外接矩形(边界框)。以下是关于 cv2.boundingRect 函数的详细中文文档:

一、函数原型

rect = cv2.boundingRect(points)

或者
x, y, w, h = cv2.boundingRect(points)

二、参数说明

points:

  • 输入的轮廓点集合,可以是 numpy.ndarray 类型,通常通过 cv2.findContours 函数获取。

三、返回值

rect(或者 x, y, w, h):

  • 返回一个矩形边界框的信息。
  • 如果返回单个变量 rect,它是一个包含四个整数值的元组,即 (x, y, w, h)。
  • 如果返回四个单独的变量,则 x 和 y 是矩形左上角的坐标(通常是整数),w 是矩形的宽度,h 是矩形的高度。

四、细节解析

坐标系统:在 OpenCV 中,坐标系的原点 (0, 0) 位于图像的左上角,x 轴向右为正方向,y 轴向下为正方向。

边界框属性:

  • (x, y):矩形左上角的坐标。
  • w:矩形的宽度(水平方向上的像素数)
  • h:矩形的高度(垂直方向上的像素数)

与 cv2.findContours 的关系:

  • 通常,cv2.boundingRect 函数用于处理 cv2.findContours 函数返回的轮廓数据。cv2.findContours 可以从二值图像中检测出轮廓,并返回轮廓的点集信息。

5.5、math.hypot

math.hypot 是 Python 中 math 模块的一个函数,用于计算欧几里得范数,也就是直角三角形的斜边长度,或者说是两个或多个数值作为直角边的直角三角形的斜边长度。这个函数可以接受任意数量的参数,返回这些参数作为直角边所构成的直角三角形的斜边的长度。它实际上是计算了这些参数的平方和的平方根。

这个函数非常有用,尤其是在处理二维或三维空间中的距离计算时。例如,如果你想计算一个点在二维空间中的距离到原点的距离,你可以使用 math.hypot(x, y),其中 x 和 y 分别是该点的横纵坐标。对于三维空间中的点,你可以使用 math.hypot(x, y, z) 来计算该点到原点的距离。

使用 math.hypot 而不是直接计算平方和的平方根(如 s q r t ( x 2 + y 2 ) sqrt(x^2 + y^2) sqrt(x2+y2))的好处是,math.hypot 可以避免在计算过程中出现中间结果的溢出,因为它使用了更精确的算法来处理大数和小数的组合。

示例代码:

import math  
  
# 计算二维空间中点到原点的距离  
distance_2d = math.hypot(3, 4)  
print(distance_2d)  # 输出 5.0  
  
# 计算三维空间中点到原点的距离  
distance_3d = math.hypot(3, 4, 5)  
print(distance_3d)  # 输出 7.0710678118654755

在这个例子中,math.hypot(3, 4) 计算了直角边为3和4的直角三角形的斜边长度,结果是5,因为3-4-5是一个勾股数。同样,math.hypot(3, 4, 5) 计算了直角边分别为3、4和5的三维直角三角形的斜边长度,即点到原点的空间距离。

6、参考

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐