2025-05-28 13:18:27 +00:00
|
|
|
|
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):
|
|
|
|
|
"""
|
2025-05-28 22:56:17 +08:00
|
|
|
|
检测左右两条平行的黄色轨道线,优化后能够更准确处理各种路况
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
image: 输入图像,可以是文件路径或者已加载的图像数组
|
|
|
|
|
observe: 是否输出中间状态信息和可视化结果,默认为False
|
|
|
|
|
delay: 展示每个步骤的等待时间(毫秒)
|
|
|
|
|
save_log: 是否保存日志和图像
|
|
|
|
|
stone_path_mode: 石板路模式,针对石板路上的黄线进行特殊处理
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
tuple: (中心线信息, 左轨迹线信息, 右轨迹线信息)
|
2025-05-28 22:56:17 +08:00
|
|
|
|
如果检测失败返回(None, None, None)
|
|
|
|
|
|
|
|
|
|
改进:
|
|
|
|
|
- 使用多点采样+多项式拟合计算更准确的中心线
|
|
|
|
|
- 优化对左右轨道线的选择,考虑平行性和合理宽度
|
|
|
|
|
- 增强对倾斜轨道和石板路的处理能力
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"""
|
|
|
|
|
# 如果输入是字符串(文件路径),则加载图像
|
|
|
|
|
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:
|
|
|
|
|
# 石板路上的黄线通常对比度更低,需要更宽松的颜色范围
|
2025-05-28 22:56:17 +08:00
|
|
|
|
lower_yellow = np.array([8, 50, 50]) # 更宽松的黄色下限
|
|
|
|
|
upper_yellow = np.array([45, 255, 255]) # 更宽松的黄色上限
|
2025-05-28 13:18:27 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# 裁剪底部区域重点关注近处的黄线
|
2025-05-28 22:56:17 +08:00
|
|
|
|
bottom_roi_height = int(height * 0.6) # 增加关注区域到图像底部60%
|
2025-05-28 13:18:27 +00:00
|
|
|
|
bottom_roi = mask[height-bottom_roi_height:, :]
|
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
debug("步骤1.5: 底部区域掩码", "处理")
|
|
|
|
|
cv2.imshow("底部区域掩码", bottom_roi)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 边缘检测 - 针对石板路调整参数
|
|
|
|
|
if stone_path_mode:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
edges = cv2.Canny(mask, 20, 100, apertureSize=3) # 进一步降低阈值以捕捉更弱的边缘
|
2025-05-28 13:18:27 +00:00
|
|
|
|
else:
|
|
|
|
|
edges = cv2.Canny(mask, 50, 150, apertureSize=3)
|
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
debug("步骤2: 边缘检测", "处理")
|
|
|
|
|
cv2.imshow("边缘检测", edges)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 霍夫变换检测直线 - 根据是否为石板路调整参数
|
|
|
|
|
if stone_path_mode:
|
|
|
|
|
# 石板路上的线段可能更短更断续,使用更宽松的参数
|
2025-05-28 22:56:17 +08:00
|
|
|
|
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=15,
|
|
|
|
|
minLineLength=width*0.02, maxLineGap=60)
|
2025-05-28 13:18:27 +00:00
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
# 优先选择图像底部的线
|
2025-05-28 22:56:17 +08:00
|
|
|
|
if y1 < height * 0.4 and y2 < height * 0.4:
|
2025-05-28 13:18:27 +00:00
|
|
|
|
continue # 忽略上半部分的线
|
|
|
|
|
|
|
|
|
|
# 计算斜率 (避免除零错误)
|
|
|
|
|
if abs(x2 - x1) < 5: # 几乎垂直的线
|
|
|
|
|
slope = 100 # 设置一个较大的值表示接近垂直
|
|
|
|
|
else:
|
|
|
|
|
slope = (y2 - y1) / (x2 - x1)
|
|
|
|
|
|
|
|
|
|
# 根据是否为石板路调整斜率阈值
|
2025-05-28 22:56:17 +08:00
|
|
|
|
min_slope_threshold = 0.4 if stone_path_mode else 0.75
|
2025-05-28 13:18:27 +00: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))
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# 使用更宽松的斜率阈值
|
2025-05-28 22:56:17 +08:00
|
|
|
|
if abs(slope) > 0.2: # 进一步放宽斜率阈值
|
2025-05-28 13:18:27 +00:00
|
|
|
|
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]
|
2025-05-28 22:56:17 +08:00
|
|
|
|
_, current_mid_x, current_mid_y, current_slope, current_length = current_line
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
# 查找相近的线段
|
|
|
|
|
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]
|
2025-05-28 22:56:17 +08:00
|
|
|
|
_, candidate_mid_x, candidate_mid_y, candidate_slope, candidate_length = candidate_line
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
# 如果x坐标接近且斜率相似,认为是同一条线的不同部分
|
|
|
|
|
x_diff = abs(current_mid_x - candidate_mid_x)
|
|
|
|
|
slope_diff = abs(current_slope - candidate_slope)
|
2025-05-28 22:56:17 +08:00
|
|
|
|
y_diff = abs(current_mid_y - candidate_mid_y)
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 放宽相似线段的判断条件
|
|
|
|
|
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都比较接近
|
2025-05-28 13:18:27 +00:00
|
|
|
|
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
|
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 改进的评分函数 - 同时考虑斜率、位置、长度和在图像中的位置
|
2025-05-28 13:18:27 +00:00
|
|
|
|
def score_line(line_info, is_left):
|
|
|
|
|
_, mid_x, mid_y, slope, length = line_info
|
2025-05-28 22:56:17 +08:00
|
|
|
|
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) # 适当提高长线段的权重
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 位置评分 - 线段位于预期位置得分高
|
|
|
|
|
# 根据是否为石板路模式调整预期位置
|
|
|
|
|
center_x = width / 2
|
2025-05-28 13:18:27 +00:00
|
|
|
|
if stone_path_mode:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 石板路上的轨道宽度可能不同
|
|
|
|
|
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 # 给予低分但不完全排除
|
2025-05-28 13:18:27 +00:00
|
|
|
|
else:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 期望的斜率 - 左右轨道线应该稍有内倾
|
|
|
|
|
# 左侧期望负斜率,右侧期望正斜率
|
|
|
|
|
ideal_slope_magnitude = 0.8 # 适当倾斜
|
|
|
|
|
ideal_slope = -ideal_slope_magnitude if is_left else ideal_slope_magnitude
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 检查斜率符号是否正确
|
|
|
|
|
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%
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 线段在图像中的位置评分 - 轨道线应该大致垂直并且从底部延伸
|
|
|
|
|
# 检查线段是否从底部区域开始
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# 综合评分 - 调整权重
|
2025-05-28 13:18:27 +00:00
|
|
|
|
if stone_path_mode:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 石板路模式下更关注位置和底部接近程度
|
|
|
|
|
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 # 是否到达底部
|
|
|
|
|
)
|
2025-05-28 13:18:27 +00:00
|
|
|
|
else:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 普通轨道模式下更平衡考虑各因素
|
|
|
|
|
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
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
# 获取两条线的坐标
|
|
|
|
|
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-28 22:56:17 +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
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
right_extended_y2 = height
|
|
|
|
|
if abs(right_x2 - right_x1) < 5: # 几乎垂直
|
|
|
|
|
right_extended_x2 = right_x2
|
2025-05-28 13:18:27 +00:00
|
|
|
|
else:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
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
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 计算中间路径点的方式:在多个高度上计算左右线的中点,然后拟合中心线
|
|
|
|
|
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))
|
2025-05-28 13:18:27 +00:00
|
|
|
|
else:
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 如果点数不足,退回到简单的中点计算方法
|
|
|
|
|
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))
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
# 计算中心线与图像中心线的偏差
|
|
|
|
|
deviation = bottom_x - center_x
|
|
|
|
|
|
|
|
|
|
result_img = None
|
|
|
|
|
if observe or save_log:
|
|
|
|
|
result_img = img.copy()
|
|
|
|
|
# 绘制左右轨迹线
|
2025-05-28 22:56:17 +08:00
|
|
|
|
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}", "绘图")
|
|
|
|
|
|
2025-05-28 13:18:27 +00:00
|
|
|
|
# 绘制图像中心线
|
2025-05-28 22:56:17 +08:00
|
|
|
|
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}", "绘图")
|
|
|
|
|
|
2025-05-28 13:18:27 +00:00
|
|
|
|
# 标记中心点
|
2025-05-28 22:56:17 +08:00
|
|
|
|
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}", "绘图")
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
# 显示偏差信息
|
2025-05-28 22:56:17 +08:00
|
|
|
|
cv2.putText(result_img, f"Deviation: {deviation:.1f}px", (10, 30),
|
2025-05-28 13:18:27 +00:00
|
|
|
|
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,
|
2025-05-28 22:56:17 +08:00
|
|
|
|
"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),
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"stone_path_mode": stone_path_mode
|
|
|
|
|
}
|
|
|
|
|
info(f"双轨迹线检测结果: {log_info}", "日志")
|
|
|
|
|
|
|
|
|
|
# 创建左右轨迹线和中心线信息
|
|
|
|
|
left_track_info = {
|
2025-05-28 22:56:17 +08:00
|
|
|
|
"line": (left_x1, left_y1, left_x2, left_y2),
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"slope": left_line[3],
|
|
|
|
|
"x_mid": left_line[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
right_track_info = {
|
2025-05-28 22:56:17 +08:00
|
|
|
|
"line": (right_x1, right_y1, right_x2, right_y2),
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"slope": right_line[3],
|
|
|
|
|
"x_mid": right_line[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
center_info = {
|
2025-05-28 22:56:17 +08:00
|
|
|
|
"point": (int(center_point[0]), int(center_point[1])),
|
|
|
|
|
"deviation": float(deviation),
|
|
|
|
|
"slope": float(center_slope),
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"is_vertical": abs(center_slope) > 5.0, # 判断是否接近垂直
|
2025-05-28 22:56:17 +08:00
|
|
|
|
"track_width": float(right_line[1] - left_line[1]), # 两轨迹线之间的距离
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"stone_path_mode": stone_path_mode # 记录是否使用了石板路模式
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return center_info, left_track_info, right_track_info
|
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
def auto_detect_dual_track_lines(image, observe=False, delay=1000, save_log=True, max_retries=2):
|
2025-05-28 13:18:27 +00:00
|
|
|
|
"""
|
2025-05-28 22:56:17 +08:00
|
|
|
|
自动检测双轨迹线,尝试多种模式并采用最佳结果
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
image: 输入图像,可以是文件路径或者已加载的图像数组
|
|
|
|
|
observe: 是否输出中间状态信息和可视化结果,默认为False
|
|
|
|
|
delay: 展示每个步骤的等待时间(毫秒)
|
|
|
|
|
save_log: 是否保存日志和图像
|
2025-05-28 22:56:17 +08:00
|
|
|
|
max_retries: 最大重试次数,用于处理复杂情况
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
tuple: (中心线信息, 左轨迹线信息, 右轨迹线信息)
|
|
|
|
|
"""
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 尝试石板路模式
|
|
|
|
|
stone_result = detect_dual_track_lines(image, observe, delay, save_log, stone_path_mode=True)
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
# 尝试普通模式
|
|
|
|
|
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)
|
2025-05-28 13:18:27 +00:00
|
|
|
|
|
2025-05-28 22:56:17 +08:00
|
|
|
|
error("所有模式和重试均失败", "自动检测")
|
|
|
|
|
return None, None, None
|