mi-task/utils/detect_dual_track_lines.py

1007 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cv2
import numpy as np
import os
import datetime
from utils.log_helper import LogHelper, get_logger, section, info, debug, warning, error, success, timing
def detect_dual_track_lines(image, observe=False, delay=1000, save_log=True, stone_path_mode=False):
"""
检测左右两条平行的黄色轨道线,优化后能够更准确处理各种路况
参数:
image: 输入图像,可以是文件路径或者已加载的图像数组
observe: 是否输出中间状态信息和可视化结果默认为False
delay: 展示每个步骤的等待时间(毫秒)
save_log: 是否保存日志和图像
stone_path_mode: 石板路模式,针对石板路上的黄线进行特殊处理
返回:
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)
# 根据是否为石板路模式选择不同的参数
if stone_path_mode:
# 石板路上的黄线通常对比度更低,需要更宽松的颜色范围
lower_yellow = np.array([8, 50, 50]) # 更宽松的黄色下限
upper_yellow = np.array([45, 255, 255]) # 更宽松的黄色上限
else:
# 标准黄色的HSV范围
lower_yellow = np.array([15, 80, 80])
upper_yellow = np.array([35, 255, 255])
# 创建黄色的掩码
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# 应用对比度增强
if stone_path_mode:
# 在处理掩码前先对原图像进行预处理
# 对原图进行自适应直方图均衡化增强对比度
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
l = clahe.apply(l)
lab = cv2.merge((l, a, b))
enhanced_img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
# 用增强后的图像重新检测黄色
enhanced_hsv = cv2.cvtColor(enhanced_img, cv2.COLOR_BGR2HSV)
enhanced_mask = cv2.inRange(enhanced_hsv, lower_yellow, upper_yellow)
# 组合原始掩码和增强掩码
mask = cv2.bitwise_or(mask, enhanced_mask)
if observe:
debug("增强对比度和颜色检测", "处理")
cv2.imshow("增强对比度", enhanced_img)
cv2.imshow("增强后的黄色掩码", enhanced_mask)
cv2.waitKey(delay)
# 形态学操作以改善掩码
if stone_path_mode:
# 石板路上的线条可能更细更断续,使用更大的膨胀核和更多的迭代次数
kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel, iterations=2)
mask = cv2.erode(mask, np.ones((3, 3), np.uint8), iterations=1)
else:
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: 创建黄色掩码", "处理")
cv2.imshow("黄色掩码", mask)
cv2.waitKey(delay)
# 裁剪底部区域重点关注近处的黄线
bottom_roi_height = int(height * 0.6) # 增加关注区域到图像底部60%
bottom_roi = mask[height-bottom_roi_height:, :]
if observe:
debug("步骤1.5: 底部区域掩码", "处理")
cv2.imshow("底部区域掩码", bottom_roi)
cv2.waitKey(delay)
# 边缘检测 - 针对石板路调整参数
if stone_path_mode:
edges = cv2.Canny(mask, 20, 100, apertureSize=3) # 进一步降低阈值以捕捉更弱的边缘
else:
edges = cv2.Canny(mask, 50, 150, apertureSize=3)
if observe:
debug("步骤2: 边缘检测", "处理")
cv2.imshow("边缘检测", edges)
cv2.waitKey(delay)
# 霍夫变换检测直线 - 根据是否为石板路调整参数
if stone_path_mode:
# 石板路上的线段可能更短更断续,使用更宽松的参数
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=15,
minLineLength=width*0.02, maxLineGap=60)
else:
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=25,
minLineLength=width*0.05, maxLineGap=40)
if lines is None or len(lines) == 0:
error("未检测到直线", "失败")
return None, None, None
if observe:
debug(f"步骤3: 检测到 {len(lines)} 条直线", "处理")
lines_img = img.copy()
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)
# 筛选近似垂直的线
vertical_lines = []
for line in 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)
# 根据是否为石板路调整斜率阈值
min_slope_threshold = 0.4 if stone_path_mode else 0.75
# 筛选接近垂直的线 (斜率较大),但允许更多倾斜度
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))
if len(vertical_lines) < 2:
# 石板路模式下,尝试放宽斜率条件重新筛选线段
if stone_path_mode:
vertical_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
# 仍然优先选择图像底部的线
if y1 < height * 0.6 and y2 < height * 0.6:
continue # 忽略上部分的线
# 计算斜率 (避免除零错误)
if abs(x2 - x1) < 5: # 几乎垂直的线
slope = 100
else:
slope = (y2 - y1) / (x2 - x1)
# 使用更宽松的斜率阈值
if abs(slope) > 0.2: # 进一步放宽斜率阈值
line_length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
mid_x = (x1 + x2) / 2
mid_y = (y1 + y2) / 2
vertical_lines.append((line[0], mid_x, mid_y, slope, line_length))
if len(vertical_lines) < 2:
error("未检测到足够的垂直线", "失败")
return None, None, None
if observe:
debug(f"步骤4: 找到 {len(vertical_lines)} 条垂直线", "处理")
v_lines_img = img.copy()
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)
# 优先选择更接近图像底部的线 - 根据y坐标均值排序
vertical_lines.sort(key=lambda x: x[2], reverse=True) # 按mid_y从大到小排序
# 石板路模式下,可能需要处理断续的线段或合并相近的线段
if stone_path_mode and len(vertical_lines) >= 3:
# 尝试合并相近的线段
merged_lines = []
processed = [False] * len(vertical_lines)
for i in range(len(vertical_lines)):
if processed[i]:
continue
current_line = vertical_lines[i]
_, current_mid_x, current_mid_y, current_slope, current_length = current_line
# 查找相近的线段
similar_lines = [current_line]
processed[i] = True
for j in range(i+1, len(vertical_lines)):
if processed[j]:
continue
candidate_line = vertical_lines[j]
_, candidate_mid_x, candidate_mid_y, candidate_slope, candidate_length = candidate_line
# 如果x坐标接近且斜率相似认为是同一条线的不同部分
x_diff = abs(current_mid_x - candidate_mid_x)
slope_diff = abs(current_slope - candidate_slope)
y_diff = abs(current_mid_y - candidate_mid_y)
# 放宽相似线段的判断条件
if ((x_diff < width * 0.08 and slope_diff < 0.4) or # 更宽松的x差异和斜率差异
(x_diff < width * 0.05 and y_diff < height * 0.2)): # 或者x和y都比较接近
similar_lines.append(candidate_line)
processed[j] = True
# 如果找到多条相近的线,合并它们
if len(similar_lines) > 1:
# 按线长度加权合并
total_weight = sum(line[4] for line in similar_lines)
merged_x1 = sum(line[0][0] * line[4] for line in similar_lines) / total_weight
merged_y1 = sum(line[0][1] * line[4] for line in similar_lines) / total_weight
merged_x2 = sum(line[0][2] * line[4] for line in similar_lines) / total_weight
merged_y2 = sum(line[0][3] * line[4] for line in similar_lines) / total_weight
merged_line = (np.array([int(merged_x1), int(merged_y1),
int(merged_x2), int(merged_y2)]),
(merged_x1 + merged_x2) / 2,
(merged_y1 + merged_y2) / 2,
(merged_y2 - merged_y1) / (merged_x2 - merged_x1 + 1e-6),
np.sqrt((merged_x2-merged_x1)**2 + (merged_y2-merged_y1)**2))
merged_lines.append(merged_line)
else:
merged_lines.append(current_line)
vertical_lines = merged_lines
if observe:
debug(f"步骤4.5: 合并后找到 {len(vertical_lines)} 条垂直线", "处理")
merged_img = img.copy()
for line_info in vertical_lines:
line, _, _, slope, _ = line_info
x1, y1, x2, y2 = line
cv2.line(merged_img, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 255), 2)
# 显示斜率
cv2.putText(merged_img, f"{slope:.2f}", (int((x1+x2)//2), int((y1+y2)//2)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
cv2.imshow("合并后的垂直线", merged_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
# 改进的评分函数 - 同时考虑斜率、位置、长度和在图像中的位置
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)
length_score = min(1.0, length_ratio * 1.2) # 适当提高长线段的权重
# 位置评分 - 线段位于预期位置得分高
# 根据是否为石板路模式调整预期位置
center_x = width / 2
if stone_path_mode:
# 石板路上的轨道宽度可能不同
expected_track_width = width * 0.5 # 石板路轨道宽度估计
else:
# 普通轨道的期望位置
expected_track_width = width * 0.45 # 普通轨道宽度估计
# 计算预期的线位置(基于图像中心和轨道宽度)
expected_x = center_x - expected_track_width * 0.5 if is_left else center_x + expected_track_width * 0.5
# 计算中点与预期位置的偏差
x_distance = abs(mid_x - expected_x)
x_score = max(0, 1.0 - x_distance / (width * 0.25))
# 计算底部点与预期位置的偏差
# 估计线延伸到底部的x坐标
bottom_x = x1
if abs(y2 - y1) > 1: # 非水平线
t = (height - y1) / (y2 - y1)
if t >= 0: # 确保是向下延伸
bottom_x = x1 + t * (x2 - x1)
bottom_x_distance = abs(bottom_x - expected_x)
bottom_x_score = max(0, 1.0 - bottom_x_distance / (width * 0.25))
# 斜率评分 - 轨道线应该有一定的倾斜度
# 判断是否几乎垂直
if abs(slope) > 5:
# 几乎垂直的线,不太可能是轨道线
slope_score = 0.3 # 给予低分但不完全排除
else:
# 期望的斜率 - 左右轨道线应该稍有内倾
# 左侧期望负斜率,右侧期望正斜率
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
# 线段直线度评分 - 使用端点拟合的直线与原始线段的接近度
# 在这里我们已经假设线段是直线所以直线度是100%
# 线段在图像中的位置评分 - 轨道线应该大致垂直并且从底部延伸
# 检查线段是否从底部区域开始
bottom_region_threshold = height * 0.7 # 底部30%区域
reaches_bottom = max(y1, y2) > bottom_region_threshold
bottom_reach_score = 1.0 if reaches_bottom else 0.5
# 综合评分 - 调整权重
if stone_path_mode:
# 石板路模式下更关注位置和底部接近程度
final_score = (
y_score * 0.25 + # 底部接近度
length_score * 0.15 + # 线段长度
x_score * 0.15 + # 中点位置
bottom_x_score * 0.2 + # 底部点位置
slope_score * 0.15 + # 斜率合适性
bottom_reach_score * 0.1 # 是否到达底部
)
else:
# 普通轨道模式下更平衡考虑各因素
final_score = (
y_score * 0.2 + # 底部接近度
length_score * 0.2 + # 线段长度
x_score * 0.15 + # 中点位置
bottom_x_score * 0.2 + # 底部点位置
slope_score * 0.15 + # 斜率合适性
bottom_reach_score * 0.1 # 是否到达底部
)
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)
# 只考虑前几名的线段
max_candidates = min(5, len(left_lines), len(right_lines))
left_candidates = left_lines[:max_candidates]
right_candidates = right_lines[:max_candidates]
for left_line in left_candidates:
left_score = score_line(left_line, True)
left_slope = left_line[3]
for right_line in right_candidates:
right_score = score_line(right_line, False)
right_slope = right_line[3]
# 计算两条线的斜率差异 - 平行线斜率应该相近但符号相反
# 对于接近垂直的线,使用符号相反但绝对值相近作为判断依据
if abs(left_slope) > 5 and abs(right_slope) > 5:
# 几乎垂直的线,判断它们是否都几乎垂直
slope_diff = abs(abs(left_slope) - abs(right_slope)) / max(abs(left_slope), abs(right_slope))
parallel_score = max(0, 1.0 - slope_diff * 3.0)
else:
# 非垂直线,检查它们是否平行且方向相反(一个正斜率,一个负斜率)
if left_slope * right_slope < 0: # 一正一负
slope_diff = abs(abs(left_slope) - abs(right_slope)) / max(0.1, max(abs(left_slope), abs(right_slope)))
parallel_score = max(0, 1.0 - slope_diff * 2.0)
else:
# 斜率符号相同,不太可能是轨道线对
parallel_score = 0.0
# 计算两条线的宽度是否合理
left_x = left_line[1]
right_x = right_line[1]
track_width = right_x - left_x
# 检查宽度是否正常
if track_width <= 0:
# 宽度为负,不合理
width_score = 0.0
else:
# 轨道宽度应该在合理范围内 - 调整范围更加精确
if stone_path_mode:
expected_width = width * 0.5 # 石板路可能更宽一些
allowed_deviation = width * 0.3 # 允许的偏差范围
else:
expected_width = width * 0.45 # 普通轨道相对窄一些
allowed_deviation = width * 0.25 # 允许的偏差范围
width_diff = abs(track_width - expected_width)
width_score = max(0, 1.0 - width_diff / allowed_deviation)
# 计算线段的垂直对称性 - 理想的轨道线应该大致以图像中心为对称轴
center_x = width / 2
left_dist_to_center = abs(center_x - left_x)
right_dist_to_center = abs(right_x - center_x)
# 计算对称性得分,如果左右距离中心的距离相近,得分高
symmetry_diff = abs(left_dist_to_center - right_dist_to_center) / max(left_dist_to_center, right_dist_to_center)
symmetry_score = max(0, 1.0 - symmetry_diff)
# 底部点距离评分 - 确保两条线底部的点在合理距离内
# 估计左右线段延伸到图像底部时的x坐标
left_x1, left_y1, left_x2, left_y2 = left_line[0]
right_x1, right_y1, right_x2, right_y2 = right_line[0]
left_bottom_x = left_x1
if abs(left_y2 - left_y1) > 1:
t = (height - left_y1) / (left_y2 - left_y1)
left_bottom_x = left_x1 + t * (left_x2 - left_x1)
right_bottom_x = right_x1
if abs(right_y2 - right_y1) > 1:
t = (height - right_y1) / (right_y2 - right_y1)
right_bottom_x = right_x1 + t * (right_x2 - right_x1)
bottom_width = right_bottom_x - left_bottom_x
if bottom_width <= 0:
bottom_width_score = 0.0
else:
bottom_width_diff = abs(bottom_width - expected_width)
bottom_width_score = max(0, 1.0 - bottom_width_diff / allowed_deviation)
# 综合评分 - 调整权重
# 更重视平行性和底部宽度
pair_score = (left_score + right_score) * 0.3 + parallel_score * 0.25 + width_score * 0.2 + symmetry_score * 0.1 + bottom_width_score * 0.15
if pair_score > best_pair_score:
best_pair_score = pair_score
best_left_line = left_line
best_right_line = right_line
# 如果找到了最佳对,则使用它们
if best_left_line is not None and best_right_line is not None and best_pair_score > 0.4: # 要求最低评分
left_line = best_left_line
right_line = best_right_line
if observe:
debug(f"选择最佳线对,评分: {best_pair_score:.2f}", "线对")
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]
right_line = right_lines[0]
if observe:
debug("未找到合适的线对,使用各自评分最高的线", "线对")
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
# 尝试延长线段到图像底部,处理被石板路部分遮挡的情况
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)
error = np.mean(np.abs(predicted_xs - center_xs))
# 如果误差更小,更新最佳拟合
if error < best_error:
best_error = error
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 = bottom_x - center_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)
# 显示是否为石板路模式
if stone_path_mode:
cv2.putText(result_img, "Stone Path Mode", (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
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)
img_path = os.path.join(log_dir, f"dual_track_{timestamp}.jpg")
cv2.imwrite(img_path, result_img)
info(f"保存双轨迹线检测结果图像到: {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),
"stone_path_mode": stone_path_mode
}
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]), # 两轨迹线之间的距离
"stone_path_mode": stone_path_mode # 记录是否使用了石板路模式
}
return center_info, left_track_info, right_track_info
def auto_detect_dual_track_lines(image, observe=False, delay=1000, save_log=True, max_retries=2):
"""
自动检测双轨迹线,尝试多种模式并采用最佳结果
参数:
image: 输入图像,可以是文件路径或者已加载的图像数组
observe: 是否输出中间状态信息和可视化结果默认为False
delay: 展示每个步骤的等待时间(毫秒)
save_log: 是否保存日志和图像
max_retries: 最大重试次数,用于处理复杂情况
返回:
tuple: (中心线信息, 左轨迹线信息, 右轨迹线信息)
"""
# 尝试石板路模式
stone_result = detect_dual_track_lines(image, observe, delay, save_log, stone_path_mode=True)
# 尝试普通模式
normal_result = detect_dual_track_lines(image, observe, delay, save_log, stone_path_mode=False)
# 评估两种模式的结果,选择更可靠的一个
if stone_result[0] is not None and normal_result[0] is not None:
# 两种模式都成功,评估哪个更可靠
stone_center_info = stone_result[0]
normal_center_info = normal_result[0]
# 评估轨道宽度是否在合理范围内
if isinstance(image, str):
img = cv2.imread(image)
else:
img = image.copy()
if img is not None:
width = img.shape[1]
expected_width_range = (width * 0.3, width * 0.7) # 轨道宽度应在图像宽度的30%-70%之间
stone_width = stone_center_info.get("track_width", 0)
normal_width = normal_center_info.get("track_width", 0)
stone_width_score = 1.0 if expected_width_range[0] <= stone_width <= expected_width_range[1] else 0.0
normal_width_score = 1.0 if expected_width_range[0] <= normal_width <= expected_width_range[1] else 0.0
# 如果一种模式的轨道宽度更合理,选择它
if stone_width_score > normal_width_score:
info("选择石板路模式结果 - 轨道宽度更合理", "自动检测")
return stone_result
elif normal_width_score > stone_width_score:
info("选择普通模式结果 - 轨道宽度更合理", "自动检测")
return normal_result
# 如果石板路模式成功,返回结果
if stone_result[0] is not None:
info("选择石板路模式结果", "自动检测")
return stone_result
# 如果普通模式成功,返回结果
if normal_result[0] is not None:
info("选择普通模式结果", "自动检测")
return normal_result
# 如果都失败,尝试调整参数重新检测
if max_retries > 0:
warning(f"标准检测失败,尝试调整参数重新检测 (剩余重试次数: {max_retries})", "自动检测")
# 对图像进行预处理以增强黄线检测
if isinstance(image, str):
img = cv2.imread(image)
else:
img = image.copy()
if img is not None:
# 增强图像对比度
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
l = clahe.apply(l)
lab = cv2.merge((l, a, b))
enhanced_img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
# 使用增强后的图像重新尝试
return auto_detect_dual_track_lines(enhanced_img, observe, delay, save_log, max_retries-1)
error("所有模式和重试均失败", "自动检测")
return None, None, None