mi-task/utils/detect_dual_track_lines.py

851 lines
38 KiB
Python
Raw Normal View History

import cv2
import numpy as np
import os
import datetime
2025-05-31 12:33:28 +08:00
from utils.base_line_handler import _merge_collinear_lines_iterative
from utils.log_helper import LogHelper, get_logger, section, info, debug, warning, error, success, timing
2025-05-31 12:33:28 +08:00
def detect_dual_track_lines(image, observe=False, delay=1000, save_log=True,
2025-05-31 13:36:16 +08:00
min_slope_threshold=0.4,
2025-05-31 12:33:28 +08:00
min_line_length=0.05,
max_line_gap=40,
detect_height=0.4):
"""
检测左右两条平行的黄色轨道线优化后能够更准确处理各种路况
参数:
image: 输入图像可以是文件路径或者已加载的图像数组
observe: 是否输出中间状态信息和可视化结果默认为False
delay: 展示每个步骤的等待时间(毫秒)
save_log: 是否保存日志和图像
2025-05-31 12:33:28 +08:00
min_slope_threshold: 最小斜率阈值
min_line_length: 最小线段长度
max_line_gap: 最大线段间距
返回:
tuple: (中心线信息, 左轨迹线信息, 右轨迹线信息)
如果检测失败返回(None, None, None)
改进:
- 使用多点采样+多项式拟合计算更准确的中心线
- 优化对左右轨道线的选择考虑平行性和合理宽度
- 增强对倾斜轨道和石板路的处理能力
"""
# 如果输入是字符串(文件路径),则加载图像
if isinstance(image, str):
img = cv2.imread(image)
else:
img = image.copy()
if img is None:
error("无法加载图像", "失败")
return None, None, None
# 获取图像尺寸
height, width = img.shape[:2]
# 计算图像中间区域的范围
center_x = width // 2
# 转换到HSV颜色空间以便更容易提取黄色
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
2025-05-31 12:33:28 +08:00
# 标准黄色的HSV范围
lower_yellow = np.array([15, 80, 80])
upper_yellow = np.array([35, 255, 255])
# 创建黄色的掩码
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
2025-05-31 12:33:28 +08:00
kernel = np.ones((5, 5), np.uint8)
mask = cv2.dilate(mask, kernel, iterations=1)
mask = cv2.erode(mask, np.ones((3, 3), np.uint8), iterations=1)
if observe:
debug("步骤1: 创建黄色掩码", "处理")
2025-05-31 12:33:28 +08:00
mask_display = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
cv2.putText(mask_display, "Step 1: Yellow Mask", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.putText(mask_display, f"Lower: {lower_yellow}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)
cv2.putText(mask_display, f"Upper: {upper_yellow}", (10, 55), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)
cv2.imshow("黄色掩码", mask_display)
cv2.waitKey(delay)
# 裁剪底部区域重点关注近处的黄线
bottom_roi_height = int(height * detect_height) # 增加关注区域到图像底部60%
bottom_roi = mask[height-bottom_roi_height:, :]
if observe:
debug("步骤1.5: 底部区域掩码", "处理")
2025-05-31 12:33:28 +08:00
bottom_roi_display = cv2.cvtColor(bottom_roi, cv2.COLOR_GRAY2BGR)
cv2.putText(bottom_roi_display, "Step 1.5: Bottom ROI", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.putText(bottom_roi_display, f"ROI Height: {bottom_roi_height}px ({bottom_roi_height/height*100:.0f}%)", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)
cv2.imshow("底部区域掩码", bottom_roi_display)
cv2.waitKey(delay)
2025-05-31 12:33:28 +08:00
# INFO 边缘检测
# Apply Canny to bottom_roi instead of the full mask
edges_roi = cv2.Canny(bottom_roi, 50, 150, apertureSize=3)
if observe:
debug("步骤2: 边缘检测 (底部ROI)", "处理") # Updated text
# Displaying edges from bottom_roi directly
edges_display = cv2.cvtColor(edges_roi, cv2.COLOR_GRAY2BGR)
cv2.putText(edges_display, "Step 2: Edge Detection (Canny on Bottom ROI)", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
2025-05-31 12:33:28 +08:00
cv2.putText(edges_display, "Thresholds: (50, 150)", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)
cv2.imshow("边缘检测 (底部ROI)", edges_display) # Updated window title
cv2.waitKey(delay)
# INFO 霍夫变换检测直线 (on edges from bottom_roi)
lines = cv2.HoughLinesP(edges_roi, 1, np.pi/180, threshold=25,
2025-05-31 12:33:28 +08:00
minLineLength=width*min_line_length, maxLineGap=max_line_gap)
# Adjust y-coordinates of lines detected in bottom_roi to be relative to the full image
if lines is not None:
offset_y = height - bottom_roi_height
for i in range(len(lines)):
# line[0] is [x1, y1, x2, y2]
lines[i][0][1] += offset_y # y1
lines[i][0][3] += offset_y # y2
if lines is None or len(lines) == 0:
error("未检测到直线 (在底部ROI)", "失败") # Updated error message
return None, None, None
if observe:
debug(f"步骤3: 检测到 {len(lines)} 条直线", "处理")
lines_img = img.copy()
2025-05-31 12:33:28 +08:00
cv2.putText(lines_img, "Step 3: Hough Lines", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.putText(lines_img, f"Th:25, MinLen:{width*0.05:.1f}, MaxGap:{max_line_gap}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lines_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow("检测到的直线", lines_img)
cv2.waitKey(delay)
# 筛选近似垂直的线
2025-05-31 12:49:01 +08:00
vertical_only_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
2025-05-31 12:49:01 +08:00
# 优先选择图像底部的线
if y1 < height * 0.4 and y2 < height * 0.4:
continue # 忽略上半部分的线
# 计算斜率 (避免除零错误)
if abs(x2 - x1) < 5: # 几乎垂直的线
slope = 100 # 设置一个较大的值表示接近垂直
else:
slope = (y2 - y1) / (x2 - x1)
# 筛选接近垂直的线 (斜率较大),但允许更多倾斜度
2025-05-31 13:36:16 +08:00
if abs(slope) > min_slope_threshold:
vertical_only_lines.append(line[0])
2025-05-31 12:49:01 +08:00
if observe:
debug(f"步骤3.2: 筛选出 {len(vertical_only_lines)} 条垂直候选线 (合并前)", "可视化")
pre_merge_lines_img = img.copy()
cv2.putText(pre_merge_lines_img, "Step 3.2: Vertical Candidates (Pre-Merge)", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
colors = [(0, 0, 255), (0, 255, 0), (0, 255, 255), (255, 0, 0), (255, 0, 255), (255, 255, 0)]
for i, line in enumerate(vertical_only_lines):
x1, y1, x2, y2 = line
2025-05-31 12:49:01 +08:00
cv2.line(pre_merge_lines_img, (x1, y1), (x2, y2), colors[i % len(colors)], 2) # 使用红色显示合并前的线
cv2.imshow("合并前的垂直候选线", pre_merge_lines_img)
cv2.waitKey(delay)
2025-05-31 13:03:35 +08:00
if observe:
vertical_only_lines_tmp = vertical_only_lines.copy()
2025-05-31 13:36:16 +08:00
2025-05-31 12:49:01 +08:00
vertical_only_lines = _merge_collinear_lines_iterative(vertical_only_lines,
2025-05-31 13:03:35 +08:00
min_initial_len=5.0, # 最小初始线段长度
2025-05-31 13:36:16 +08:00
max_angle_diff_deg=4.0, # 最大允许角度差(度)
2025-05-31 13:03:35 +08:00
max_ep_gap_abs=max_line_gap, # 端点间最大绝对距离
max_ep_gap_factor=1, # 端点间最大相对距离因子
max_p_dist_abs=max_line_gap, # 点到线段最大绝对距离
max_p_dist_factor=1) # 点到线段最大相对距离因子
2025-05-31 12:49:01 +08:00
if observe:
2025-05-31 13:03:35 +08:00
print(f"合并前: {len(vertical_only_lines_tmp)} 条线, 合并后: {len(vertical_only_lines)} 条线")
2025-05-31 12:49:01 +08:00
debug(f"步骤3.5: 合并筛选出 {len(vertical_only_lines)} 条垂直候选线 (合并后)", "可视化")
# 创建两个图像用于对比显示
pre_merge_img = img.copy()
post_merge_img = img.copy()
# 在两张图上添加标题
cv2.putText(pre_merge_img, "合并前的垂直候选线", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.putText(post_merge_img, "合并后的垂直候选线", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
2025-05-31 12:49:01 +08:00
# 为每条线分配不同的颜色
colors = [
(255, 0, 0), # 蓝色
(0, 255, 0), # 绿色
(0, 0, 255), # 红色
(255, 255, 0), # 青色
(255, 0, 255), # 品红
(0, 255, 255), # 黄色
]
# 绘制合并前的线
for i, line in enumerate(vertical_only_lines_tmp):
x1, y1, x2, y2 = line
color = colors[i % len(colors)]
cv2.line(pre_merge_img, (x1, y1), (x2, y2), color, 2)
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(pre_merge_img, str(i+1), (mid_x, mid_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
# 绘制合并后的线
2025-05-31 12:49:01 +08:00
for i, line in enumerate(vertical_only_lines):
x1, y1, x2, y2 = line[0]
color = colors[i % len(colors)]
cv2.line(post_merge_img, (x1, y1), (x2, y2), color, 2)
2025-05-31 12:49:01 +08:00
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(post_merge_img, str(i+1), (mid_x, mid_y),
2025-05-31 12:49:01 +08:00
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
# 水平拼接两张图片
combined_img = np.hstack((pre_merge_img, post_merge_img))
cv2.imshow("合并前后的垂直候选线对比", combined_img)
2025-05-31 12:49:01 +08:00
cv2.waitKey(delay)
2025-05-31 12:49:01 +08:00
vertical_lines = []
for line in vertical_only_lines:
x1, y1, x2, y2 = line[0]
# 优先选择图像底部的线
if y1 < height * 0.4 and y2 < height * 0.4:
continue # 忽略上半部分的线
# 计算斜率 (避免除零错误)
if abs(x2 - x1) < 5: # 几乎垂直的线
slope = 100 # 设置一个较大的值表示接近垂直
else:
slope = (y2 - y1) / (x2 - x1)
# 筛选接近垂直的线 (斜率较大),但允许更多倾斜度
2025-05-31 13:36:16 +08:00
if abs(slope) > min_slope_threshold:
line_length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
# 计算线的中点x坐标
mid_x = (x1 + x2) / 2
# 计算线的中点y坐标
mid_y = (y1 + y2) / 2
# 保存线段、其坐标、斜率和长度
vertical_lines.append((line[0], mid_x, mid_y, slope, line_length))
2025-05-31 12:33:28 +08:00
if len(vertical_lines) < 2:
if len(vertical_lines) < 2:
error("未检测到足够的垂直线", "失败")
return None, None, None
if observe:
debug(f"步骤4: 找到 {len(vertical_lines)} 条垂直线", "处理")
v_lines_img = img.copy()
2025-05-31 12:33:28 +08:00
cv2.putText(v_lines_img, "Step 4: Vertical Lines Filtered", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.putText(v_lines_img, f"Min Slope Abs: {min_slope_threshold:.2f}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1)
for line_info in vertical_lines:
line, _, _, slope, _ = line_info
x1, y1, x2, y2 = line
cv2.line(v_lines_img, (x1, y1), (x2, y2), (0, 255, 255), 2)
# 显示斜率
cv2.putText(v_lines_img, f"{slope:.2f}", ((x1+x2)//2, (y1+y2)//2),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
cv2.imshow("垂直线", v_lines_img)
cv2.waitKey(delay)
# 按x坐标将线分为左右两组
left_lines = [line for line in vertical_lines if line[1] < center_x]
right_lines = [line for line in vertical_lines if line[1] > center_x]
# 如果任一侧没有检测到线,则放宽左右两侧线的分组条件
if not left_lines or not right_lines:
# 按x坐标排序所有垂直线
vertical_lines.sort(key=lambda x: x[1])
# 如果有至少两条线,将最左侧的线作为左轨迹线,最右侧的线作为右轨迹线
if len(vertical_lines) >= 2:
left_lines = [vertical_lines[0]]
right_lines = [vertical_lines[-1]]
else:
error("左侧或右侧未检测到轨迹线", "失败")
return None, None, None
if observe:
debug(f"左侧候选线数量: {len(left_lines)}, 右侧候选线数量: {len(right_lines)}", "线候选")
2025-05-31 12:33:28 +08:00
# 创建左右线可视化图像
left_right_img = img.copy()
cv2.putText(left_right_img, "Left and Right Candidate Lines", (10, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
# 绘制左侧候选线(蓝色)
for line_info in left_lines:
line = line_info[0]
x1, y1, x2, y2 = line
cv2.line(left_right_img, (x1, y1), (x2, y2), (255, 0, 0), 2)
# 显示斜率
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(left_right_img, f"L:{line_info[3]:.2f}", (mid_x, mid_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
# 绘制右侧候选线(红色)
for line_info in right_lines:
line = line_info[0]
x1, y1, x2, y2 = line
cv2.line(left_right_img, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 显示斜率
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(left_right_img, f"R:{line_info[3]:.2f}", (mid_x, mid_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
cv2.imshow("左右候选线", left_right_img)
cv2.waitKey(delay)
# 优化说明:在默认模式下,评分函数和线对选择都优先考虑更靠近图像中心的线段
# 这有助于减少对图像边缘可能出现的干扰线的选择,提高轨道线检测的准确性
2025-05-31 12:33:28 +08:00
# INFO 改进的评分函数 - 同时考虑斜率、位置、长度和在图像中的位置
def score_line(line_info, is_left):
_, mid_x, mid_y, slope, length = line_info
line_points = line_info[0] # 获取线段的端点坐标
x1, y1, x2, y2 = line_points
# 确保线段从上到下排序
if y1 > y2:
x1, x2 = x2, x1
y1, y2 = y2, y1
# 线段靠近底部评分 - y越大越靠近底部分数越高
bottom_ratio = mid_y / height
y_score = min(1.0, bottom_ratio * 1.2) # 适当提高底部线段的权重
# 线段长度评分 - 线越长分数越高
length_ratio = length / (height * 0.3)
2025-05-31 13:36:16 +08:00
length_score = min(1.0, length_ratio * 1.2)
2025-05-31 13:36:16 +08:00
# 计算到图像中心的距离得分 - 更重视靠近垂直中线的线
center_x = width / 2
distance_to_center = abs(mid_x - center_x)
2025-05-31 13:36:16 +08:00
# 使用更陡峭的衰减函数,使得距离中心越近的线得分越高
center_proximity_score = max(0, 1.0 - (distance_to_center / (width * 0.25)) ** 2)
2025-05-31 13:36:16 +08:00
# 计算底部点与中心的偏差
bottom_x = x1
if abs(y2 - y1) > 1: # 非水平线
t = (height - y1) / (y2 - y1)
if t >= 0: # 确保是向下延伸
bottom_x = x1 + t * (x2 - x1)
2025-05-31 13:36:16 +08:00
bottom_x_distance = abs(bottom_x - center_x)
bottom_x_score = max(0, 1.0 - (bottom_x_distance / (width * 0.25)) ** 2)
# 斜率评分 - 轨道线应该有一定的倾斜度
if abs(slope) > 5:
2025-05-31 13:36:16 +08:00
slope_score = 0.3
else:
2025-05-31 13:36:16 +08:00
ideal_slope_magnitude = 0.8
ideal_slope = -ideal_slope_magnitude if is_left else ideal_slope_magnitude
if (is_left and slope > 0) or (not is_left and slope < 0):
slope_sign_score = 0.3
else:
slope_sign_score = 1.0
slope_diff = abs(slope - ideal_slope)
slope_magnitude_score = max(0, 1.0 - slope_diff / 2.0)
slope_score = slope_sign_score * slope_magnitude_score
# 检查线段是否从底部区域开始
2025-05-31 13:36:16 +08:00
bottom_region_threshold = height * 0.7
reaches_bottom = max(y1, y2) > bottom_region_threshold
bottom_reach_score = 1.0 if reaches_bottom else 0.5
2025-05-31 13:36:16 +08:00
# 综合评分 - 大幅提高中心接近性的权重
2025-05-31 12:33:28 +08:00
final_score = (
2025-05-31 13:36:16 +08:00
y_score * 0.1 + # 底部接近度
2025-05-31 12:33:28 +08:00
length_score * 0.05 + # 线段长度
2025-05-31 13:36:16 +08:00
center_proximity_score * 0.6 + # 与中心的接近度 (权重提高)
2025-05-31 12:33:28 +08:00
bottom_x_score * 0.15 + # 底部点位置
2025-05-31 13:36:16 +08:00
slope_score * 0.05 + # 斜率合适性
bottom_reach_score * 0.05 # 是否到达底部
2025-05-31 12:33:28 +08:00
)
return final_score
# 如果有多条线,评估左右线对是否平行
if len(left_lines) > 0 and len(right_lines) > 0:
# 计算最佳的左右线对
best_pair_score = -1
best_left_line = None
best_right_line = None
# 先对左右线按照评分排序,只考虑评分较高的候选线(减少计算量)
left_lines = sorted(left_lines, key=lambda line: score_line(line, True), reverse=True)
right_lines = sorted(right_lines, key=lambda line: score_line(line, False), reverse=True)
2025-05-31 13:36:16 +08:00
left_line = left_lines[0] if left_lines else None
right_line = right_lines[0] if right_lines else None
if observe:
debug(f"选择最佳线对,评分: {best_pair_score:.2f}", "线对")
2025-05-31 13:36:16 +08:00
# 创建评分可视化图像
score_img = img.copy()
cv2.putText(score_img, "Line Scores Visualization", (10, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
2025-05-31 13:36:16 +08:00
# 显示左侧线的评分
for i, line_info in enumerate(left_lines):
line = line_info[0]
x1, y1, x2, y2 = line
score = score_line(line_info, True)
cv2.line(score_img, (x1, y1), (x2, y2), (255, 0, 0), 2)
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(score_img, f"L{i+1}:{score:.2f}", (mid_x, mid_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
# 显示右侧线的评分
for i, line_info in enumerate(right_lines):
line = line_info[0]
x1, y1, x2, y2 = line
score = score_line(line_info, False)
cv2.line(score_img, (x1, y1), (x2, y2), (0, 0, 255), 2)
mid_x = (x1 + x2) // 2
mid_y = (y1 + y2) // 2
cv2.putText(score_img, f"R{i+1}:{score:.2f}", (mid_x, mid_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
2025-05-31 13:36:16 +08:00
cv2.imshow("线评分可视化", score_img)
cv2.waitKey(delay)
else:
# 如果只有单侧有线,使用评分最高的线
left_lines = sorted(left_lines, key=lambda line: score_line(line, True), reverse=True)
right_lines = sorted(right_lines, key=lambda line: score_line(line, False), reverse=True)
left_line = left_lines[0] if left_lines else None
right_line = right_lines[0] if right_lines else None
# 确保两侧都有线
if left_line is None or right_line is None:
error("无法确定合适的左右轨迹线对", "失败")
return None, None, None
# 获取两条线的坐标
left_x1, left_y1, left_x2, left_y2 = left_line[0]
right_x1, right_y1, right_x2, right_y2 = right_line[0]
# 确保线段的顺序是从上到下
if left_y1 > left_y2:
left_x1, left_x2 = left_x2, left_x1
left_y1, left_y2 = left_y2, left_y1
if right_y1 > right_y2:
right_x1, right_x2 = right_x2, right_x1
right_y1, right_y2 = right_y2, right_y1
2025-05-31 12:33:28 +08:00
# 尝试延长线段到图像底部,处理被石板路部分遮挡的情况
left_extended_y2 = height
if abs(left_x2 - left_x1) < 5: # 几乎垂直
left_extended_x2 = left_x2
else:
left_slope = (left_y2 - left_y1) / (left_x2 - left_x1)
left_extended_x2 = left_x1 + (left_extended_y2 - left_y1) / left_slope
right_extended_y2 = height
if abs(right_x2 - right_x1) < 5: # 几乎垂直
right_extended_x2 = right_x2
else:
right_slope = (right_y2 - right_y1) / (right_x2 - right_x1)
right_extended_x2 = right_x1 + (right_extended_y2 - right_y1) / right_slope
# 更新线段端点为延长后的坐标
left_x2, left_y2 = int(left_extended_x2), left_extended_y2
right_x2, right_y2 = int(right_extended_x2), right_extended_y2
# 尝试延长线段到图像底部,处理被石板路部分遮挡的情况
left_extended_y2 = height
if abs(left_x2 - left_x1) < 5: # 几乎垂直
left_extended_x2 = left_x2
else:
left_slope = (left_y2 - left_y1) / (left_x2 - left_x1)
left_extended_x2 = left_x1 + (left_extended_y2 - left_y1) / left_slope
right_extended_y2 = height
if abs(right_x2 - right_x1) < 5: # 几乎垂直
right_extended_x2 = right_x2
else:
right_slope = (right_y2 - right_y1) / (right_x2 - right_x1)
right_extended_x2 = right_x1 + (right_extended_y2 - right_y1) / right_slope
# 更新线段端点为延长后的坐标
left_x2, left_y2 = int(left_extended_x2), left_extended_y2
right_x2, right_y2 = int(right_extended_x2), right_extended_y2
# 改进的中心线计算方法
# 首先确定两条轨迹线的有效部分 - 以两条线段的y坐标重叠部分为准
min_y = max(left_y1, right_y1)
max_y = min(left_y2, right_y2)
# 如果两条线没有重叠的y部分则使用整个图像范围
if min_y >= max_y:
min_y = 0
max_y = height
# 计算中间路径点的方式:在多个高度上计算左右线的中点,然后拟合中心线
num_points = 20 # 增加采样点数量以提高精度
center_points = []
# 优化采样策略 - 在底部区域采样更密集
sample_ys = []
# 前半部分采样点均匀分布
first_half = np.linspace(min_y, min_y + (max_y - min_y) * 0.5, num_points // 4)
# 后半部分(更靠近底部)采样点更密集
second_half = np.linspace(min_y + (max_y - min_y) * 0.5, max_y, num_points - num_points // 4)
sample_ys = np.concatenate([first_half, second_half])
for y in sample_ys:
# 计算左侧线在当前y值处的x坐标
if abs(left_y2 - left_y1) < 1: # 防止除零
left_x = left_x1
else:
t = (y - left_y1) / (left_y2 - left_y1)
left_x = left_x1 + t * (left_x2 - left_x1)
# 计算右侧线在当前y值处的x坐标
if abs(right_y2 - right_y1) < 1: # 防止除零
right_x = right_x1
else:
t = (y - right_y1) / (right_y2 - right_y1)
right_x = right_x1 + t * (right_x2 - right_x1)
# 计算中心点
center_x = (left_x + right_x) / 2
center_points.append((center_x, y))
# 使用采样的中心点拟合中心线
center_xs = np.array([p[0] for p in center_points])
center_ys = np.array([p[1] for p in center_points])
# 使用多项式拟合改进中心线 - 选择合适的多项式阶数
if len(center_points) >= 5: # 需要更多点来拟合更高阶多项式
try:
# 尝试不同阶数的多项式拟合,选择最佳结果
best_poly_coeffs = None
best_error = float('inf')
for degree in [1, 2, 3]: # 尝试1-3阶多项式
try:
# 使用多项式拟合
poly_coeffs = np.polyfit(center_ys, center_xs, degree)
poly_func = np.poly1d(poly_coeffs)
# 计算拟合误差
predicted_xs = poly_func(center_ys)
err = np.mean(np.abs(predicted_xs - center_xs))
# 如果误差更小,更新最佳拟合
if err < best_error:
best_error = err
best_poly_coeffs = poly_coeffs
except:
continue
# 如果找到了有效的拟合,使用它
if best_poly_coeffs is not None:
poly_coeffs = best_poly_coeffs
poly_func = np.poly1d(poly_coeffs)
# 为了提高底部拟合精度,给予底部区域更高的权重重新拟合
weights = np.ones_like(center_ys)
# 计算距离底部的归一化距离 (0表示底部1表示顶部)
normalized_distance = (max_y - center_ys) / max(1, (max_y - min_y))
# 设置权重,底部权重更高
weights = 1.0 + 2.0 * (1.0 - normalized_distance)
# 使用加权多项式拟合
try:
weighted_poly_coeffs = np.polyfit(center_ys, center_xs, len(best_poly_coeffs) - 1, w=weights)
weighted_poly_func = np.poly1d(weighted_poly_coeffs)
# 比较加权拟合与原始拟合
weighted_predicted_xs = weighted_poly_func(center_ys)
weighted_error = np.mean(np.abs(weighted_predicted_xs - center_xs))
# 如果加权拟合更好,则使用它
if weighted_error < best_error * 1.2: # 允许一定的误差增加,因为我们更关注底部精度
poly_coeffs = weighted_poly_coeffs
poly_func = weighted_poly_func
except:
pass # 如果加权拟合失败,继续使用未加权的拟合
# 使用多项式生成中心线的起点和终点
center_line_y1 = min_y
center_line_y2 = height # 延伸到图像底部
# 计算多项式在这些y值处的x坐标
center_line_x1 = poly_func(center_line_y1)
center_line_x2 = poly_func(center_line_y2)
# 计算中心线在图像底部的x坐标 - 用于计算偏离度
bottom_x = poly_func(height)
# 确保坐标在图像范围内
bottom_x = max(0, min(width - 1, bottom_x))
center_point = (int(bottom_x), int(height))
# 计算中心线的加权平均斜率 - 更关注底部区域的斜率
if abs(center_line_y2 - center_line_y1) < 1: # 防止除零
center_slope = 0
else:
# 计算全局斜率
global_slope = (center_line_x2 - center_line_x1) / (center_line_y2 - center_line_y1)
# 计算底部区域的斜率,使用多个点来提高精度
bottom_slopes = []
bottom_region_start = height - height * 0.3 # 底部30%区域
if bottom_region_start > min_y:
# 在底部区域采样多个点计算局部斜率
bottom_sample_count = 5
bottom_ys = np.linspace(bottom_region_start, height, bottom_sample_count)
for i in range(len(bottom_ys) - 1):
y1, y2 = bottom_ys[i], bottom_ys[i+1]
x1, x2 = poly_func(y1), poly_func(y2)
# 确保坐标有效
x1 = max(0, min(width - 1, x1))
x2 = max(0, min(width - 1, x2))
local_slope = (x2 - x1) / max(0.1, (y2 - y1))
bottom_slopes.append(local_slope)
# 计算底部斜率的加权平均值,越靠近底部权重越高
if bottom_slopes:
weights = np.linspace(1, 2, len(bottom_slopes))
bottom_slope = np.average(bottom_slopes, weights=weights)
# 加权平均,底部斜率权重更高
center_slope = bottom_slope * 0.8 + global_slope * 0.2
else:
center_slope = global_slope
else:
center_slope = global_slope
else:
# 如果所有多项式拟合都失败,退回到简单的线性拟合
raise Exception("多项式拟合失败")
except Exception as e:
warning(f"多项式拟合失败,使用简单中点计算: {e}", "拟合")
# 如果多项式拟合失败,退回到简单的中点计算方法
center_line_x1 = (left_x1 + right_x1) / 2
center_line_y1 = (left_y1 + right_y1) / 2
center_line_x2 = (left_x2 + right_x2) / 2
center_line_y2 = (left_y2 + right_y2) / 2
# 计算中心线的斜率
if abs(center_line_x2 - center_line_x1) < 5:
center_slope = 100 # 几乎垂直
else:
center_slope = (center_line_y2 - center_line_y1) / (center_line_x2 - center_line_x1)
# 计算中心线延伸到图像底部的点
if abs(center_slope) < 0.01: # 几乎水平
bottom_x = center_line_x1
else:
bottom_x = center_line_x1 + (height - center_line_y1) / center_slope
bottom_x = max(0, min(width - 1, bottom_x))
center_point = (int(bottom_x), int(height))
else:
# 如果点数不足,退回到简单的中点计算方法
center_line_x1 = (left_x1 + right_x1) / 2
center_line_y1 = (left_y1 + right_y1) / 2
center_line_x2 = (left_x2 + right_x2) / 2
center_line_y2 = (left_y2 + right_y2) / 2
# 计算中心线的斜率
if abs(center_line_x2 - center_line_x1) < 5:
center_slope = 100 # 几乎垂直
else:
center_slope = (center_line_y2 - center_line_y1) / (center_line_x2 - center_line_x1)
# 计算中心线延伸到图像底部的点
if abs(center_slope) < 0.01: # 几乎水平
bottom_x = center_line_x1
else:
bottom_x = center_line_x1 + (height - center_line_y1) / center_slope
bottom_x = max(0, min(width - 1, bottom_x))
center_point = (int(bottom_x), int(height))
# 计算中心线与图像中心线的偏差
deviation = width / 2 - bottom_x
result_img = None
if observe or save_log:
result_img = img.copy()
# 绘制左右轨迹线
try:
# 确保坐标在图像范围内并且是整数
left_x1_safe = max(0, min(width - 1, left_x1))
left_y1_safe = max(0, min(height - 1, left_y1))
left_x2_safe = max(0, min(width - 1, left_x2))
left_y2_safe = max(0, min(height - 1, left_y2))
cv2.line(result_img, (int(left_x1_safe), int(left_y1_safe)),
(int(left_x2_safe), int(left_y2_safe)), (255, 0, 0), 2)
except Exception as e:
warning(f"绘制左轨迹线错误: {e}", "绘图")
try:
# 确保坐标在图像范围内并且是整数
right_x1_safe = max(0, min(width - 1, right_x1))
right_y1_safe = max(0, min(height - 1, right_y1))
right_x2_safe = max(0, min(width - 1, right_x2))
right_y2_safe = max(0, min(height - 1, right_y2))
cv2.line(result_img, (int(right_x1_safe), int(right_y1_safe)),
(int(right_x2_safe), int(right_y2_safe)), (0, 0, 255), 2)
except Exception as e:
warning(f"绘制右轨迹线错误: {e}", "绘图")
# 绘制中心线 - 如果有多项式拟合,绘制拟合曲线
if len(center_points) >= 3:
# 绘制拟合的中心曲线
try:
curve_ys = np.linspace(min_y, height, 100)
curve_xs = poly_func(curve_ys)
for i in range(len(curve_ys) - 1):
try:
# 确保坐标在图像范围内并且是整数
x1 = max(0, min(width - 1, curve_xs[i]))
y1 = max(0, min(height - 1, curve_ys[i]))
x2 = max(0, min(width - 1, curve_xs[i+1]))
y2 = max(0, min(height - 1, curve_ys[i+1]))
pt1 = (int(x1), int(y1))
pt2 = (int(x2), int(y2))
cv2.line(result_img, pt1, pt2, (0, 255, 0), 2)
except Exception as e:
warning(f"绘制曲线段错误: {e}", "绘图")
continue
# 绘制采样点
for pt in center_points:
try:
# 确保坐标在图像范围内并且是整数
x = max(0, min(width - 1, pt[0]))
y = max(0, min(height - 1, pt[1]))
cv2.circle(result_img, (int(x), int(y)), 3, (0, 255, 255), -1)
except Exception as e:
warning(f"绘制采样点错误: {e}", "绘图")
continue
except Exception as e:
warning(f"绘制曲线错误: {e}", "绘图")
else:
# 绘制简单中心线
try:
# 确保坐标在图像范围内并且是整数
x1 = max(0, min(width - 1, center_line_x1))
y1 = max(0, min(height - 1, center_line_y1))
x2 = max(0, min(width - 1, center_line_x2))
y2 = max(0, min(height - 1, center_line_y2))
cv2.line(result_img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
except Exception as e:
warning(f"绘制中心线错误: {e}", "绘图")
# 绘制图像中心线
try:
center_x_safe = max(0, min(width - 1, center_x))
cv2.line(result_img, (int(center_x_safe), 0), (int(center_x_safe), height), (0, 0, 255), 1)
except Exception as e:
warning(f"绘制图像中心线错误: {e}", "绘图")
# 标记中心点
try:
# 确保中心点坐标在图像范围内
center_x_safe = max(0, min(width - 1, center_point[0]))
center_y_safe = max(0, min(height - 1, center_point[1]))
cv2.circle(result_img, (int(center_x_safe), int(center_y_safe)), 10, (255, 0, 255), -1)
except Exception as e:
warning(f"绘制中心点错误: {e}", "绘图")
# 显示偏差信息
cv2.putText(result_img, f"Deviation: {deviation:.1f}px", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
2025-05-31 12:33:28 +08:00
cv2.putText(result_img, "Final Result", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
if 'best_pair_score' in locals() and best_pair_score != -1:
cv2.putText(result_img, f"Pair Score: {best_pair_score:.2f}", (10, 85), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
current_y_offset = 105
else:
current_y_offset = 85
2025-05-31 12:33:28 +08:00
cv2.putText(result_img, f"L-Slope: {left_line[3]:.2f}, R-Slope: {right_line[3]:.2f}", (10, current_y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
current_y_offset += 20
cv2.putText(result_img, f"Track Width: {right_line[1] - left_line[1]:.1f}px", (10, current_y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
if observe:
cv2.imshow("轨迹线检测结果", result_img)
cv2.waitKey(delay)
# 保存日志图像
if save_log and result_img is not None:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
log_dir = "logs/image"
os.makedirs(log_dir, exist_ok=True)
# 保存结果图像
result_img_path = os.path.join(log_dir, f"dual_track_{timestamp}.jpg")
cv2.imwrite(result_img_path, result_img)
# 保存原图
orig_img_path = os.path.join(log_dir, f"dual_track_orig_{timestamp}.jpg")
cv2.imwrite(orig_img_path, img)
info(f"保存双轨迹线检测结果图像到: {result_img_path}", "日志")
info(f"保存原始图像到: {orig_img_path}", "日志")
# 保存文本日志信息
log_info = {
"timestamp": timestamp,
"center_point": (int(center_point[0]), int(center_point[1])),
"deviation": float(deviation),
"left_track_mid_x": float(left_line[1]),
"right_track_mid_x": float(right_line[1]),
"track_width": float(right_line[1] - left_line[1]),
"center_slope": float(center_slope),
}
info(f"双轨迹线检测结果: {log_info}", "日志")
# 创建左右轨迹线和中心线信息
left_track_info = {
"line": (left_x1, left_y1, left_x2, left_y2),
"slope": left_line[3],
"x_mid": left_line[1]
}
right_track_info = {
"line": (right_x1, right_y1, right_x2, right_y2),
"slope": right_line[3],
"x_mid": right_line[1]
}
center_info = {
"point": (int(center_point[0]), int(center_point[1])),
"deviation": float(deviation),
"slope": float(center_slope),
"is_vertical": abs(center_slope) > 5.0, # 判断是否接近垂直
"track_width": float(right_line[1] - left_line[1]), # 两轨迹线之间的距离
}
return center_info, left_track_info, right_track_info