一、基本原理

        核心思想:检测图像中目标的边缘,将边缘连接成轮廓,轮廓所在的区域就是目标。其检测的是目标与背景的交界线,只要物体不融入背景中,几乎所有的物体都有明确的边缘。

二、关键步骤

对于当前帧:

1.灰度化:去掉带有颜色的信息。

2.高斯模糊:去除噪声。

3.Canny边缘检测:找边界点,提取边缘线。

Canny边缘检测:首先高斯滤波去噪,然后计算梯度强度和方向,接着只保留梯度最强的点如花模糊边缘,再双阈值检测(高阈值确定强边缘,低阈值连接弱边缘),最后边缘连接(将与强边缘相连的弱边缘保留)。

4.形态学连接::把断裂的边缘连起来。

5.轮廓提取:找连通的区域。

6.筛选:根据面积、宽高比、距离选择最像目标的。

7.输出预测框

三、关键代码实现

1.类结构与初始化

class EdgeContourTracker(BaseTracker):
    def __init__(self, tracker_name="edge_contour"):
        super().__init__(tracker_name)

        self.init_bbox = None
        self.prev_bbox = None
        self.target_area = 0
        self.target_aspect = 1.0
        self.lost_count = 0
        self.frame_count = 0
        self.start_time = None

        # Canny参数(后续自适应调整)
        self.canny_low = 50        # 低阈值(默认)
        self.canny_high = 150      # 高阈值(默认)

        # 形态学核
        self.dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
        self.close_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))

2.初始化方法

def init(self, frame, bbox):
    self.init_bbox = bbox
    self.prev_bbox = bbox

    x, y, w, h = [int(v) for v in bbox]
    self.target_area = w * h
    self.target_aspect = w / (h + 1e-6)

    # ★ 自适应Canny阈值:根据目标的亮度来设定
    roi_gray = cv2.cvtColor(frame[y:y+h, x:x+w], cv2.COLOR_BGR2GRAY)
    median = np.median(roi_gray)              # 目标区域的中间亮度值
    self.canny_low = max(10, int(median * 0.3))    # 低阈值 = 亮度的30%
    self.canny_high = min(255, int(median * 1.0))  # 高阈值 = 亮度的100%
    if self.canny_low >= self.canny_high:
        self.canny_low = max(5, self.canny_high - 15)

    self.start_time = time.time()
    self.frame_count = 1
    self.trajectory = [bbox]

        采取自适应 Canny 阈值:亮目标,阈值自动设高,避免把大量纹理误认为边缘;暗目标,阈值自动设低,避免漏掉目标的边缘。

3.多尺度Canny边缘检测

def _extract_edges(self, gray):
    """多尺度Canny边缘检测 + 形态学处理"""
    # 用三组不同的Canny阈值检测边缘,然后合并
    edges_low = cv2.Canny(gray, max(5, self.canny_low // 2), self.canny_high)
    edges_mid = cv2.Canny(gray, self.canny_low, self.canny_high)
    edges_high = cv2.Canny(gray, min(255, self.canny_low * 2), min(255, self.canny_high + 30))

    # 合并:取三个结果的并集(OR运算)
    edges = cv2.bitwise_or(edges_low, edges_mid)
    edges = cv2.bitwise_or(edges, edges_high)

    # 膨胀:把断裂的边缘连起来
    edges = cv2.dilate(edges, self.dilate_kernel, iterations=2)

    # 闭运算:填充边缘内部的空洞
    edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, self.close_kernel, iterations=1)

    return edges

        单个 Canny 阈值很难同时捕捉所有边缘——低阈值会包含更多噪声,高阈值会漏掉弱边缘。多尺度 Canny可以很好的解决这个问题。
4.候选框筛选

def _find_best_contour(self, edges, prev_bbox, frame_shape):
    x, y, w, h = prev_bbox
    max_dim = max(w, h)

    # 多尺度搜索范围(依次扩大)
    margins = [0.5, 1.0, 1.5, 2.5, 4.0]
    if self.lost_count > 3:
        margins = [1.0, 2.0, 4.0, 8.0]   # 丢失后扩大搜索

    # 全图轮廓提取(只做一次,节省计算)
    all_contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not all_contours:
        return None

    prev_center = self._center(prev_bbox)
    best_box = None
    best_score = 0

    for margin in margins:
        # 当前尺度的搜索框
        x1 = max(0, x - int(max_dim * margin))
        y1 = max(0, y - int(max_dim * margin))
        x2 = min(frame_shape[1], x + w + int(max_dim * margin))
        y2 = min(frame_shape[0], y + h + int(max_dim * margin))

        for c in all_contours:
            area = cv2.contourArea(c)
            # 边缘轮廓面积通常远小于目标面积,用很宽松的下限
            if area < max(30, self.target_area * 0.003) or area > self.target_area * 8:
                continue

            bx, by, bw, bh = cv2.boundingRect(c)
            # 检查轮廓中心是否在当前搜索范围内
            if not (x1 <= bx + bw//2 <= x2 and y1 <= by + bh//2 <= y2):
                continue

            aspect = bw / (bh + 1e-6)
            if abs(aspect - self.target_aspect) > 5:
                continue

            center = np.array([bx + bw/2, by + bh/2])
            dist = np.linalg.norm(center - prev_center)

            dist_score = 1.0 / (1.0 + dist / max_dim)
            area_score = min(area / self.target_area, self.target_area / area)
            aspect_score = 1.0 / (1.0 + abs(aspect - self.target_aspect))

            score = 0.5 * dist_score + 0.3 * area_score + 0.2 * aspect_score

            if score > best_score:
                best_score = score
                best_box = (int(bx), int(by), int(bw), int(bh))

        if best_box is not None:
            break  # 找到就停止扩大搜索

    return best_box

5.主更新方法

def update(self, frame):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (5, 5), 0)      # 高斯滤波去噪

    # ===== 每帧动态更新Canny阈值 =====
    x, y, w, h = self.prev_bbox
    if w > 5 and h > 5:
        roi_gray = gray[max(0,y):min(frame.shape[0],y+h), max(0,x):min(frame.shape[1],x+w)]
        if roi_gray.size > 0:
            median = np.median(roi_gray)           # 当前帧目标区域的亮度
            self.canny_low = max(10, int(median * 0.3))
            self.canny_high = min(255, int(median * 1.0))
            if self.canny_low >= self.canny_high:
                self.canny_low = max(5, self.canny_high - 15)

    # ===== 边缘检测 =====
    edges = self._extract_edges(gray)

    # ===== 找最佳轮廓 =====
    cand = self._find_best_contour(edges, self.prev_bbox, frame.shape)

    # ===== 跟丢处理 =====
    if cand is None:
        cand = self.prev_bbox

    # ===== 有效性验证 =====
    c1 = self._center(self.prev_bbox)
    c2 = self._center(cand)
    dist = np.linalg.norm(c1 - c2)
    max_dim = max(self.prev_bbox[2], self.prev_bbox[3])
    new_area = cand[2] * cand[3]

    if dist < max_dim * 5 and 0.02 < new_area / (self.target_area + 1e-6) < 10:
        self.prev_bbox = cand
        self.lost_count = 0
    else:
        self.lost_count += 1

    self.frame_count += 1
    self.trajectory.append(self.prev_bbox)
    return self.prev_bbox

        每帧动态更新 Canny 阈值_:不同于初始时只算一次,update() 中根据上一帧目标区域的亮度重新计算阈值。如果目标从明亮区域移动到暗处,阈值会自动降低以适配。

四、局限性

1.边缘不等于完整目标:得到的通常是轮廓线而非填充区域,框可能只框住边缘的一部分。。

2.对纹理背景敏感:如果背景纹理复杂(比如树叶、砖墙),会产生大量无关边缘,干扰轮廓筛选。

3.对模糊敏感:目标运动过快导致运动模糊时,边缘会变模糊甚至消失。

 

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐