2025-05-14 12:42:01 +08:00
|
|
|
|
import cv2
|
|
|
|
|
import numpy as np
|
2025-05-14 13:32:07 +08:00
|
|
|
|
from sklearn import linear_model
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-14 13:32:07 +08:00
|
|
|
|
def detect_horizontal_track_edge(image, observe=False, delay=1000):
|
2025-05-14 12:42:01 +08:00
|
|
|
|
"""
|
|
|
|
|
检测正前方横向黄色赛道的边缘,并返回y值最大的边缘点
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
image: 输入图像,可以是文件路径或者已加载的图像数组
|
|
|
|
|
observe: 是否输出中间状态信息和可视化结果,默认为False
|
2025-05-14 20:06:09 +08:00
|
|
|
|
delay: 展示每个步骤的等待时间(毫秒)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
edge_point: 赛道前方边缘点的坐标 (x, y)
|
|
|
|
|
edge_info: 边缘信息字典
|
|
|
|
|
"""
|
|
|
|
|
# 如果输入是字符串(文件路径),则加载图像
|
|
|
|
|
if isinstance(image, str):
|
|
|
|
|
img = cv2.imread(image)
|
|
|
|
|
else:
|
|
|
|
|
img = image.copy()
|
|
|
|
|
|
|
|
|
|
if img is None:
|
|
|
|
|
print("无法加载图像")
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
# 获取图像尺寸
|
|
|
|
|
height, width = img.shape[:2]
|
|
|
|
|
|
|
|
|
|
# 计算图像中间区域的范围(用于专注于正前方的赛道)
|
|
|
|
|
center_x = width // 2
|
|
|
|
|
search_width = int(width * 2/3) # 搜索区域宽度为图像宽度的2/3
|
|
|
|
|
search_height = height # 搜索区域高度为图像高度的1/1
|
|
|
|
|
left_bound = center_x - search_width // 2
|
|
|
|
|
right_bound = center_x + search_width // 2
|
|
|
|
|
bottom_bound = height
|
|
|
|
|
top_bound = height - search_height
|
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
print("步骤1: 原始图像已加载")
|
|
|
|
|
search_region_img = img.copy()
|
|
|
|
|
# 绘制搜索区域
|
|
|
|
|
cv2.rectangle(search_region_img, (left_bound, top_bound), (right_bound, bottom_bound), (255, 0, 0), 2)
|
|
|
|
|
cv2.line(search_region_img, (center_x, 0), (center_x, height), (0, 0, 255), 2) # 中线
|
|
|
|
|
cv2.imshow("搜索区域", search_region_img)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 转换到HSV颜色空间以便更容易提取黄色
|
|
|
|
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
|
|
|
|
|
|
|
|
# 黄色的HSV范围
|
|
|
|
|
lower_yellow = np.array([20, 100, 100])
|
|
|
|
|
upper_yellow = np.array([30, 255, 255])
|
|
|
|
|
|
|
|
|
|
# 创建黄色的掩码
|
|
|
|
|
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
|
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 添加形态学操作以改善掩码
|
|
|
|
|
kernel = np.ones((3, 3), np.uint8)
|
|
|
|
|
mask = cv2.dilate(mask, kernel, iterations=1)
|
|
|
|
|
|
2025-05-14 12:42:01 +08:00
|
|
|
|
if observe:
|
|
|
|
|
print("步骤2: 创建黄色掩码")
|
|
|
|
|
cv2.imshow("黄色掩码", mask)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 应用掩码,只保留黄色部分
|
|
|
|
|
yellow_only = cv2.bitwise_and(img, img, mask=mask)
|
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
print("步骤3: 提取黄色部分")
|
|
|
|
|
cv2.imshow("只保留黄色", yellow_only)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 裁剪掩码到搜索区域
|
|
|
|
|
search_mask = mask[top_bound:bottom_bound, left_bound:right_bound]
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 找到掩码在搜索区域中最底部的非零点位置
|
|
|
|
|
bottom_points = []
|
|
|
|
|
non_zero_cols = np.where(np.any(search_mask, axis=0))[0]
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 寻找每列的最底部点
|
|
|
|
|
for col in non_zero_cols:
|
|
|
|
|
col_points = np.where(search_mask[:, col] > 0)[0]
|
|
|
|
|
if len(col_points) > 0:
|
|
|
|
|
bottom_row = np.max(col_points)
|
|
|
|
|
bottom_points.append((left_bound + col, top_bound + bottom_row))
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
if len(bottom_points) < 3:
|
|
|
|
|
# 如果找不到足够的底部点,使用canny+霍夫变换
|
|
|
|
|
edges = cv2.Canny(mask, 50, 150, apertureSize=3)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
|
|
|
|
if observe:
|
2025-05-15 20:16:57 +08:00
|
|
|
|
print("步骤3.1: 边缘检测")
|
|
|
|
|
cv2.imshow("边缘检测", edges)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 使用霍夫变换检测直线 - 调低阈值以检测短线段
|
|
|
|
|
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=30,
|
|
|
|
|
minLineLength=width*0.1, maxLineGap=30)
|
|
|
|
|
|
|
|
|
|
if lines is None or len(lines) == 0:
|
|
|
|
|
if observe:
|
|
|
|
|
print("未检测到直线")
|
|
|
|
|
return None, None
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
|
|
|
|
if observe:
|
2025-05-15 20:16:57 +08:00
|
|
|
|
print(f"步骤4: 检测到 {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)
|
|
|
|
|
|
|
|
|
|
# 筛选水平线,但放宽斜率条件
|
|
|
|
|
horizontal_lines = []
|
|
|
|
|
for line in lines:
|
|
|
|
|
x1, y1, x2, y2 = line[0]
|
|
|
|
|
|
|
|
|
|
# 计算斜率 (避免除零错误)
|
|
|
|
|
if abs(x2 - x1) < 5: # 几乎垂直的线
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
slope = (y2 - y1) / (x2 - x1)
|
|
|
|
|
|
|
|
|
|
# 筛选接近水平的线 (斜率接近0),但容许更大的倾斜度
|
|
|
|
|
if abs(slope) < 0.3:
|
|
|
|
|
# 确保线在搜索区域内
|
|
|
|
|
if ((left_bound <= x1 <= right_bound and top_bound <= y1 <= bottom_bound) or
|
|
|
|
|
(left_bound <= x2 <= right_bound and top_bound <= y2 <= bottom_bound)):
|
|
|
|
|
# 计算线的中点y坐标
|
|
|
|
|
mid_y = (y1 + y2) / 2
|
|
|
|
|
line_length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
|
|
|
|
|
# 保存线段、其y坐标和长度
|
|
|
|
|
horizontal_lines.append((line[0], mid_y, slope, line_length))
|
|
|
|
|
|
|
|
|
|
if not horizontal_lines:
|
|
|
|
|
if observe:
|
|
|
|
|
print("未检测到水平线")
|
|
|
|
|
return None, None
|
|
|
|
|
|
2025-05-14 12:42:01 +08:00
|
|
|
|
if observe:
|
2025-05-15 20:16:57 +08:00
|
|
|
|
print(f"步骤4.1: 找到 {len(horizontal_lines)} 条水平线")
|
|
|
|
|
h_lines_img = img.copy()
|
|
|
|
|
for line_info in horizontal_lines:
|
|
|
|
|
line, _, slope, _ = line_info
|
|
|
|
|
x1, y1, x2, y2 = line
|
|
|
|
|
cv2.line(h_lines_img, (x1, y1), (x2, y2), (0, 255, 255), 2)
|
|
|
|
|
# 显示斜率
|
|
|
|
|
cv2.putText(h_lines_img, f"{slope:.2f}", ((x1+x2)//2, (y1+y2)//2),
|
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
|
|
|
|
|
cv2.imshow("水平线", h_lines_img)
|
|
|
|
|
cv2.waitKey(delay)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 按y坐标排序 (从大到小,底部的线排在前面)
|
|
|
|
|
horizontal_lines.sort(key=lambda x: x[1], reverse=True)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 取最靠近底部且足够长的线作为横向赛道线
|
|
|
|
|
selected_line = None
|
|
|
|
|
selected_slope = 0
|
|
|
|
|
for line_info in horizontal_lines:
|
|
|
|
|
line, _, slope, length = line_info
|
|
|
|
|
if length > width * 0.1: # 确保线足够长
|
|
|
|
|
selected_line = line
|
|
|
|
|
selected_slope = slope
|
|
|
|
|
break
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
if selected_line is None and horizontal_lines:
|
|
|
|
|
# 如果没有足够长的线,就取最靠近底部的线
|
|
|
|
|
selected_line = horizontal_lines[0][0]
|
|
|
|
|
selected_slope = horizontal_lines[0][2]
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
if selected_line is None:
|
|
|
|
|
if observe:
|
|
|
|
|
print("无法选择合适的线段")
|
|
|
|
|
return None, None
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
x1, y1, x2, y2 = selected_line
|
|
|
|
|
else:
|
|
|
|
|
# 使用底部点拟合直线
|
|
|
|
|
if observe:
|
|
|
|
|
bottom_points_img = img.copy()
|
|
|
|
|
for point in bottom_points:
|
|
|
|
|
cv2.circle(bottom_points_img, point, 3, (0, 255, 0), -1)
|
|
|
|
|
cv2.imshow("底部边缘点", bottom_points_img)
|
|
|
|
|
cv2.waitKey(delay)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 使用RANSAC拟合直线以去除异常值
|
|
|
|
|
x_points = np.array([p[0] for p in bottom_points]).reshape(-1, 1)
|
|
|
|
|
y_points = np.array([p[1] for p in bottom_points])
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 如果点过少或分布不够宽,返回None
|
|
|
|
|
if len(bottom_points) < 3 or np.max(x_points) - np.min(x_points) < width * 0.1:
|
|
|
|
|
if observe:
|
|
|
|
|
print("底部点太少或分布不够宽")
|
|
|
|
|
return None, None
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
ransac = linear_model.RANSACRegressor(residual_threshold=5.0)
|
|
|
|
|
ransac.fit(x_points, y_points)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 获取拟合参数
|
|
|
|
|
selected_slope = ransac.estimator_.coef_[0]
|
|
|
|
|
intercept = ransac.estimator_.intercept_
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 检查斜率是否在合理范围内
|
|
|
|
|
if abs(selected_slope) > 0.3:
|
|
|
|
|
if observe:
|
|
|
|
|
print(f"拟合斜率过大: {selected_slope:.4f}")
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
# 使用拟合的直线参数计算线段端点
|
|
|
|
|
x1 = left_bound
|
|
|
|
|
y1 = int(selected_slope * x1 + intercept)
|
|
|
|
|
x2 = right_bound
|
|
|
|
|
y2 = int(selected_slope * x2 + intercept)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
|
|
|
|
if observe:
|
2025-05-15 20:16:57 +08:00
|
|
|
|
fitted_line_img = img.copy()
|
|
|
|
|
cv2.line(fitted_line_img, (x1, y1), (x2, y2), (0, 255, 255), 2)
|
|
|
|
|
cv2.imshow("拟合线段", fitted_line_img)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 确保x1 < x2
|
|
|
|
|
if x1 > x2:
|
|
|
|
|
x1, x2 = x2, x1
|
|
|
|
|
y1, y2 = y2, y1
|
|
|
|
|
|
|
|
|
|
# 找到线上y值最大的点作为边缘点(最靠近相机的点)
|
|
|
|
|
if y1 > y2:
|
|
|
|
|
bottom_edge_point = (x1, y1)
|
|
|
|
|
else:
|
|
|
|
|
bottom_edge_point = (x2, y2)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 获取线上的更多点
|
|
|
|
|
selected_points = []
|
|
|
|
|
step = 5 # 每5个像素取一个点
|
|
|
|
|
for x in range(max(left_bound, int(min(x1, x2))), min(right_bound, int(max(x1, x2)) + 1), step):
|
|
|
|
|
y = int(selected_slope * (x - x1) + y1)
|
|
|
|
|
if top_bound <= y <= bottom_bound:
|
|
|
|
|
selected_points.append((x, y))
|
2025-05-14 12:42:01 +08:00
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
print(f"步骤5: 找到边缘点 {bottom_edge_point}")
|
|
|
|
|
edge_img = img.copy()
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 画线
|
|
|
|
|
cv2.line(edge_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
|
|
# 绘制所有点
|
|
|
|
|
for point in selected_points:
|
2025-05-14 12:42:01 +08:00
|
|
|
|
cv2.circle(edge_img, point, 3, (255, 0, 0), -1)
|
|
|
|
|
# 标记边缘点
|
|
|
|
|
cv2.circle(edge_img, bottom_edge_point, 10, (0, 0, 255), -1)
|
|
|
|
|
cv2.imshow("选定的横向线和边缘点", edge_img)
|
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 计算这个点到中线的距离
|
|
|
|
|
distance_to_center = bottom_edge_point[0] - center_x
|
|
|
|
|
|
|
|
|
|
# 计算中线与检测到的横向线的交点
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 横向线方程: y = slope * (x - x1) + y1
|
2025-05-14 12:42:01 +08:00
|
|
|
|
# 中线方程: x = center_x
|
|
|
|
|
# 解这个方程组得到交点坐标
|
|
|
|
|
intersection_x = center_x
|
2025-05-15 20:16:57 +08:00
|
|
|
|
intersection_y = selected_slope * (center_x - x1) + y1
|
2025-05-14 12:42:01 +08:00
|
|
|
|
intersection_point = (int(intersection_x), int(intersection_y))
|
|
|
|
|
|
|
|
|
|
# 计算交点到图像底部的距离(以像素为单位)
|
|
|
|
|
distance_to_bottom = height - intersection_y
|
|
|
|
|
|
|
|
|
|
if observe:
|
|
|
|
|
slope_img = img.copy()
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 画出检测到的线
|
|
|
|
|
cv2.line(slope_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
|
|
# 标记边缘点
|
2025-05-14 12:42:01 +08:00
|
|
|
|
cv2.circle(slope_img, bottom_edge_point, 10, (0, 0, 255), -1)
|
|
|
|
|
# 画出中线
|
|
|
|
|
cv2.line(slope_img, (center_x, 0), (center_x, height), (0, 0, 255), 2)
|
2025-05-15 20:16:57 +08:00
|
|
|
|
# 标记中线与横向线的交点
|
2025-05-14 12:42:01 +08:00
|
|
|
|
cv2.circle(slope_img, intersection_point, 12, (255, 0, 255), -1)
|
|
|
|
|
cv2.circle(slope_img, intersection_point, 5, (255, 255, 255), -1)
|
|
|
|
|
# 画出交点到底部的距离线
|
|
|
|
|
cv2.line(slope_img, intersection_point, (intersection_x, height), (255, 255, 0), 2)
|
|
|
|
|
|
2025-05-15 20:16:57 +08:00
|
|
|
|
cv2.putText(slope_img, f"Slope: {selected_slope:.4f}", (10, 30),
|
2025-05-14 12:42:01 +08:00
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
|
|
|
|
cv2.putText(slope_img, f"Distance to center: {distance_to_center}px", (10, 70),
|
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
|
|
|
|
cv2.putText(slope_img, f"Distance to bottom: {distance_to_bottom:.1f}px", (10, 110),
|
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
|
|
|
|
cv2.putText(slope_img, f"中线交点: ({intersection_point[0]}, {intersection_point[1]})", (10, 150),
|
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
|
|
|
|
cv2.imshow("边缘斜率和中线交点", slope_img)
|
2025-05-14 13:32:07 +08:00
|
|
|
|
cv2.imwrite("res/path/test/edge_img.png", slope_img)
|
2025-05-14 12:42:01 +08:00
|
|
|
|
cv2.waitKey(delay)
|
|
|
|
|
|
|
|
|
|
# 创建边缘信息字典
|
|
|
|
|
edge_info = {
|
|
|
|
|
"x": bottom_edge_point[0],
|
|
|
|
|
"y": bottom_edge_point[1],
|
|
|
|
|
"distance_to_center": distance_to_center,
|
2025-05-15 20:16:57 +08:00
|
|
|
|
"slope": selected_slope,
|
|
|
|
|
"is_horizontal": abs(selected_slope) < 0.05, # 判断边缘是否接近水平
|
|
|
|
|
"points_count": len(selected_points), # 该组中点的数量
|
2025-05-14 12:42:01 +08:00
|
|
|
|
"intersection_point": intersection_point, # 中线与横向线的交点
|
|
|
|
|
"distance_to_bottom": distance_to_bottom, # 交点到图像底部的距离
|
2025-05-15 20:16:57 +08:00
|
|
|
|
"points": selected_points # 添加选定的点组
|
2025-05-14 12:42:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bottom_edge_point, edge_info
|
|
|
|
|
|
|
|
|
|
# 用法示例
|
|
|
|
|
if __name__ == "__main__":
|
2025-05-14 20:06:09 +08:00
|
|
|
|
pass
|