SingleTrack_Project(七)传统视觉追踪方法——边缘轮廓法
·
一、基本原理
核心思想:检测图像中目标的边缘,将边缘连接成轮廓,轮廓所在的区域就是目标。其检测的是目标与背景的交界线,只要物体不融入背景中,几乎所有的物体都有明确的边缘。
二、关键步骤
对于当前帧:
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.对模糊敏感:目标运动过快导致运动模糊时,边缘会变模糊甚至消失。
更多推荐



所有评论(0)