当前位置: 首页 > article >正文

机器人/智能车纯视觉巡线经典策略—滑动窗口+直方图法

作者:SkyXZ

CSDN:SkyXZ~-CSDN博客

博客园:SkyXZ - 博客园

        在机器人或智能车的自主导航任务中,视觉巡线是一项最为基础且关键的能力之一。通过摄像头实时获取道路图像,并基于图像信息判断行驶路径,是实现智能车自动行驶的前提。其中,“滑动窗口 + 直方图”法作为一种经典的视觉巡线策略,因其实现简单、效果稳定、对环境适应性强,广泛应用于竞赛与实际项目中。接下来我将趁着学弟参加比赛的机会,手把手带着大家理解并实现这一经典纯视觉的巡线策略

一、滑动窗口 + 直方图算法原理解析

(1)什么是图像直方图

        图像直方图是用来表现图像中亮度分布的一种数据图表,其给出的是图像中某个亮度或者某个范围亮度下像素的多少,即能统计一幅图在某个坐标下有效像素数量。其计算代价较小,且具有图像平移、旋转、缩放不变性等众多优点,因此广泛地应用于图像处理的各个领域,特别是灰度图像的阈值分割、基于颜色的图像检索以及图像分类,因此在纯视觉巡线的任务中,直方图可以比较清除的显示车道线(或引导线)在图像中的位置。特别是在处理已经经过二值化处理的图像时(如黑白图像中,白色为车道线,黑色为背景),可以通过统计图像某一区域(如下半部分)的垂直方向像素分布,快速定位车道线的起始位置。

        如下图所示,我们通常对图像的底部进行水平方向(X轴)的直方图统计,找到像素值为1(或255)的像素在每一列的数量,从而绘制出一个表示白色像素数量随水平位置变化的图像直方图,在这幅直方图中出现峰值的位置,往往就是左右车道线起始位置所在的区域

road

image-20250608195809051

        相比于其他的巡线算法,这一寻找直方图的过程不仅计算量小,而且对图像噪声有一定的鲁棒性,因此非常适合作为滑动窗口法的初始定位步骤。有了直方图的辅助,我们可以快速粗略判断左右车道线的大致位置,为后续的滑窗跟踪打下基础。

示例用到的代码如下,仅供参考:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as pltyellow_low = [20, 100, 100]
yellow_up = [30, 255, 255]
img = cv.imread('road.png')
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv, np.array(yellow_low), np.array(yellow_up))
# 计算沿x轴方向每列的白色像素数量
height, width = mask.shape
x_positions = np.arange(width)
white_pixel_counts = np.sum(mask == 255, axis=0)
# 显示结果
plt.figure(figsize=(12, 5))
plt.subplot(121)
plt.imshow(mask, cmap='gray')
plt.title('Binary Image')
plt.axis('off')
plt.subplot(122)
plt.plot(x_positions, white_pixel_counts)
plt.title('Line Strength')
plt.xlabel('x')
plt.ylabel('white pixel counts')
plt.grid(True, alpha=0.3)plt.tight_layout()
plt.show()

(2)什么是滑动窗口法

        滑动窗口法(Sliding Window Method)是一种经典的图像区域搜索方法,广泛应用于目标检测、路径提取等视觉任务中。在视觉巡线中,它的核心思想是:固定一个长宽一定的窗口,从整幅图像下方开始,沿垂直方向逐步上移,通过滑动“窗口”在每一个窗口内部的图像中寻找我们期望目标的位置,并不断调整窗口的中心位置以跟随寻找我们的目标对象。

        而在我们纯视觉巡线的具体任务中我们通过直方图法确定了左右车道线在图像底部的大致位置,那么底部的位置便可以作为我们第一个滑动窗口的中心点,接着我们便可将图像沿垂直方向划分为若干水平层,每层设置一个固定大小的窗口(通常为矩形)。从底部向上依次遍历每一层并在每一个滑动窗口区域中,统计白色像素(即二值化后的车道线像素)的位置,如果超过一定数量,则认为找到了车道线位置。而我们可以根据窗口内的像素分布计算质心(如平均X坐标),并以此更新下一层滑动窗口的中心位置,从而实现“跟踪”车道线的效果。

其有几个主要的参数,分别是:

  • 窗口数量(nwindows:即图像在垂直方向被划分为多少层。该值越大,滑动窗口越密集,精度越高,但计算量也随之增加。一般取 8~15 层较为合理。
  • 窗口高度(window_height:由图像高度除以窗口数量确定,每个滑窗在垂直方向的尺寸。窗口高度越小,每层的“扫描”范围越细,有助于追踪曲线,但对图像噪声也更敏感。
  • 窗口宽度(margin:窗口在水平方向的搜索半径,即从当前中心向左右扩展多少像素作为当前层的检测区域。该参数直接影响车道线捕获的“包容性”,通常设置为 50~100 像素。
  • 最小像素阈值(minpix:当某一层窗口中白色像素数量超过该值时,认为当前窗口中存在车道线,并据此更新窗口中心。若像素数小于该值,则保持上一层的中心点。这一参数有助于抑制噪声干扰,常设置为 50~100。

如下图展现所示:

image-20250608201206161

        使用这个方法获取的车道线鲁棒性强,即使车道线弯曲、有轻微遮挡,滑窗也能较好地进行跟踪;具体实现见第二章实战部分

(3)路径拟合的基本原理

        在滑动窗口法中,我们从图像中提取到了若干条白色像素点的坐标,这些点大致分布在车道线或引导线的轨迹上。为了将这些离散点转化为一条连续、光滑的曲线以用于导航控制,我们通常会采用曲线拟合的方法。其中,最常用的是二阶多项式拟合,$ y = Ax^2 + Bx + C ,或者换个角度(在巡线任务中更常见): ,或者换个角度(在巡线任务中更常见): ,或者换个角度(在巡线任务中更常见):x = Ay^2 + By + C$ 这里的 xy 是图像中的像素坐标,A,B,C是拟合出的多项式系数。由于图像中车道线是沿垂直方向延伸的,因此我们更倾向于以 纵轴 y 为自变量,以便更稳定地拟合整条线。我们可以使用NumPy包中的np.polyfit来实现多项式拟合

fit = np.polyfit(y_vals, x_vals, 2)  # 得到 A, B, C 系数

        根据拟合结果,可以计算车道线中点、偏离中心的距离、曲率半径等信息,为后续的舵机转向或PID控制提供输入

二、实战步骤详解:构建你的视觉巡线系统

        接下来我来带着大家手把手实现一个属于你自己的视觉巡线系统,整个流程涵盖从图像获取、预处理,到车道线提取、拟合与偏差计算,适用于各类巡线场景。我们使用 Python 作为实现语言,使用 OpenCV + NumPy 作为实现工具,思路清晰,代码简洁,便于复现与调试,出于对大家学习效果的考量,本Blog将不提供完整直接可用的代码,仅根据下方各章节的内容提供分模块的代码教学,在认真学习完后可以快速构建属于自己的巡线系统

(1)摄像头标定与透视变换

        在计算机视觉领域,我们是将三维物体转换到二维平面,这就需要确定空间物体表面某点的三维几何位置与其在二维图像中对应点之间的相互关系,这又需要建立摄像头成像的几何模型,而这些几何模型的参数就是摄像头参数,包括内参、外参、畸变参数等。而我们进行标定的目的便是求出摄像头的内、外参数,以及畸变参数,进而可以建立摄像头成像的几何模型。在我们的视觉巡线任务中,图像的几何畸变会影响后续处理效果。尤其是视角变形(Perspective distortion),会导致车道线在图像中呈现非线性弯曲,影响定位准确性。因此我们便需要对摄像头进行畸变矫正,标定我们主要使用的是张正友标定法,ROS中可以直接使用camera_calibration来实现,而如果想自己进行标定的话,推荐大家一篇文章:自动驾驶感知——摄像头标定(相机标定)及张正友标定法(张氏标定法) - 知乎来学习实现,这里我们便不展开叙述了

        而透视变换是做什么用处的呢?前面我们说了,我们的这套巡线的核心是使用直方图来确定车道线的起始点,我们的车道线原先是一个平行的结构,而在我们正常的摄像头视角里,车道线却是一个有透视畸变的梯形结构 —— 离摄像头近的部分看起来宽,远处看起来窄,这种“收缩感”会导致车道线在图像中的形状发生变化,这便带来两个问题:一是直方图波峰位置不稳定,由于车道线宽度不一致,底部像素密集而顶部稀疏,直方图往往只在底部区域有效,无法获得整个车道的分布信息;二是滑动窗口漂移严重,车道线的非平行投影会让滑窗误判车道边界,尤其在弯道或转角处更明显。

image-20250608203702595

        因此,我们可以通过透视变换将原图中呈梯形的车道区域“拉伸”成一个标准的矩形视图,也就是常说的“俯视图”(Bird’s-eye View),让车道线在图像中看起来像是在平地上的两条平行直线。这样不仅能使车道线宽度保持一致,从而提升直方图提取的稳定性,还能提高滑动窗口的定位精度,使拟合曲线更加平滑。同时,这种视角变换也便于进行几何测量,例如计算车辆的横向偏移和航向角,并在处理弯道等复杂场景时表现出更强的鲁棒性。具体的效果如下:

image-20250608205325083

        接下来我们首先完成一个小工具,用来选择图像车道线的矩形区域获取四个点来实现对车道线的透视变换,这里没有什么难度,主要就是使用OpenCV打开摄像头显示图像然后利用鼠标回调事件来点击图像选择目标点,因此不做过多解释,自行浏览代码中的注释进行理解

import cv2 as cv
import numpy as np
points = []  # 存储选择的四个点
img_original = None  # 原始图像
img_display = None   # 用于显示的图像副本
def mouse_callback(event, x, y, flags, param):"""鼠标回调函数 - 当鼠标点击时被调用"""global points, img_display# 检查是否是左键点击if event == cv.EVENT_LBUTTONDOWN:# 如果还没有选择4个点if len(points) < 4:# 添加点击的坐标到列表points.append([x, y])print(f"选择了第 {len(points)} 个点: ({x}, {y})")# 在图像上绘制圆点标记选择的位置cv.circle(img_display, (x, y), 5, (0, 255, 0), -1)# 在点旁边显示点的编号cv.putText(img_display, str(len(points)), (x+10, y-10), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)# 如果已经选择了4个点,连接成多边形if len(points) == 4:# 将点转换为numpy数组pts = np.array(points, np.int32)# 绘制多边形边框cv.polylines(img_display, [pts], True, (255, 0, 0), 2)print("已选择4个点!")print("选择的坐标:", points)# 更新显示cv.imshow('选择四个点', img_display)
def reset_points():"""重置选择的点"""global points, img_displaypoints = []img_display = img_original.copy()cv.imshow('选择四个点', img_display)print("重新选择点...")
def main():"""主函数"""global img_original, img_display# 读取图像img_original = cv.imread('road.png')if img_original is None:print("无法读取图像 'road.png',请确保文件存在")return# 创建显示用的图像副本img_display = img_original.copy()# 创建窗口并设置大小cv.namedWindow('选择四个点', cv.WINDOW_NORMAL)cv.resizeWindow('选择四个点', 800, 600)# 设置鼠标回调函数cv.setMouseCallback('选择四个点', mouse_callback)# 显示图像cv.imshow('选择四个点', img_display)print("=== 透视变换点选择工具 ===")print("请用鼠标左键点击选择4个点")print("建议按顺序选择:左下 -> 左上 -> 右上 -> 右下")print("操作说明:")print("  - 鼠标左键:选择点")print("  - r键:重新选择点")print("  - ESC键:退出程序")# 主循环while True:key = cv.waitKey(1) & 0xFFif key == 27:  # ESC键 - 退出breakelif key == ord('r'):  # r键 - 重新选择reset_points()cv.destroyAllWindows()return points
if __name__ == "__main__":selected_points = main()print(f"最终选择的四个点: {selected_points}")

        在我们获取了我们的四个目标点src_points后我们便可以将这四个目标点拉伸透视变换到我们的目标点dst_points,也就是把原本的梯形车道区域“拉”成规则矩形。通常,dst_points 会被设为输出图像的四个角,例如 [[0,0], [w,0], [0,h], [w,h]],这样就能得到一个尺寸为 (w, h) 的俯视图,们一般不会直接使用图像的四个角作为目标点,这是因为这样做虽然能完成变换,但未必能保证变换后的车道线位于图像中心或比例合适,可能导致变换结果失真或信息丢失。为了更合理地展示车道线,我们通常会**自定义一个“矩形区域”**作为目标区域 dst_points,例如将目标区域的位置稍微向中间偏移,或者缩放到更适合视觉分析的大小,使变换后的俯视图既包含完整的车道信息,又便于后续的滑动窗口操作、拟合和可视化叠加。在这里我们使用如下作为我们的dst_points,同时实现透视变换只需调用如下三个函数即可实现:

dst_points = np.float32([[img_width * 0.25, img_height],     # 左下[img_width * 0.25, 0],        		# 左上[img_width * 0.75, 0],        		# 右上[img_width * 0.75, img_height]    	# 右下])# 需与前面的src_point保持点顺序一致M   = cv2.getPerspectiveTransform(src_points, dst_points)   # 透视变换矩阵
Minv = cv2.getPerspectiveTransform(dst_points, src_points) # 逆变换矩阵(后续把结果投回原图时用)
warped = cv2.warpPerspective(frame, M, (img_width, img_height))             # 得到俯视图

        其中 M 是 3 × 3 的透视变换矩阵,warped 就是拉直后的车道图像。通过这一步,我们把原先在图像中呈梯形收缩的车道线拉伸为平行直线,不仅让直方图峰值位置更稳定、滑动窗口跟踪更准确,也方便后续用 Minv 将拟合好的车道线重新绘制回原始视角,实现可视化叠加与偏差计算,实现代码如下:

import cv2 as cv
import numpy as np
img = cv.imread("road.png")
img_height, img_width = img.shape[:2]
src_points = np.float32([[0,522],[234,39],[900,52],[1136,538]])
dst_points = np.float32([[img_width * 0.25, img_height],     # 左下[img_width * 0.25, 0],        		# 左上[img_width * 0.75, 0],        		# 右上[img_width * 0.75, img_height]    	# 右下])
M = cv.getPerspectiveTransform(src_points, dst_points) 
warped = cv.warpPerspective(img, M, (img_width, img_height))
cv.imshow("img",img)
cv.imshow("warped",warped)
cv.waitKey(0)
cv.destroyAllWindows()

特别注意的是,如果我们在后续通过图像中心点以及车道中心点来计算偏差的话,我们在这里可以获取一下透视变换后的车道线宽度,如果我们不对选取的车道线进行外延处理的话,那么车道线宽度就是变换后的图像宽度,如果我们进行了延拓的话,那么车道线宽度就是图像宽度减去两边外延宽度之和,在这里即:

line_width = int(img_width * 0.75 - img_width * 0.25)  # 透视变换后车道线宽度

(2)获取并预处理图像

        在完成摄像头的标定和透视变换点的选取之后,我们就可以正式开始视觉巡线系统的代码编写了。第一步是图像的获取,这一部分相对简单,我们可以通过读取摄像头帧(如 OpenCV 的 cv2.VideoCapture)或读取本地视频/图像文件来完成。为了提高处理效率并实现帧的连续处理,我们可以使用一个队列(Queue)来缓存摄像头获取的图像帧,从而避免因处理速度波动而丢帧。我们只需要每次取上一帧即可:

import cv2 as cv
from collections import deque
cap = cv.VideoCapture("road.mp4")
frame_queue = deque(maxlen=2)
while True:ret, frame = cap.read()if not ret:break# 将当前帧加入队列frame_queue.append(frame.copy())# 显示上一帧(如果存在)if len(frame_queue) > 1:last_frame = frame_queue[0]cv.imshow("frame", last_frame)if cv.waitKey(1) & 0xFF == ord('q'):break
cap.release()

        接着我们对图像进行预处理,在这里我们可以先对图像进行透视变换然后通过阈值或是其他方法将车道线提取出来实现对图像的二值化处理,同时为提升直方图的提取效果我们可以开闭运算来消除图像中的部分噪点,具体如下:

def process_frame(frame):src_points = np.float32([[0, 522],[234, 39],[900, 52],[1136, 538]])dst_points = np.float32([[frame.shape[1] * 0.25, frame.shape[0]],     # 左下[frame.shape[1] * 0.25, 0],                  # 左上[frame.shape[1] * 0.75, 0],                  # 右上[frame.shape[1] * 0.75, frame.shape[0]]      # 右下])# 完成透视变换M = cv.getPerspectiveTransform(src_points, dst_points)warped = cv.warpPerspective(frame, M, (frame.shape[1], frame.shape[0]))# 转换为HSV并提取黄色车道线hsv_warped = cv.cvtColor(warped, cv.COLOR_BGR2HSV)yellow_low = [20, 100, 100]yellow_up = [30, 255, 255]mask = cv.inRange(hsv_warped, np.array(yellow_low), np.array(yellow_up))# 形态学操作处理噪点kernel = np.ones((5, 5), np.uint8)eroded = cv.erode(mask, kernel, iterations=1)dilated = cv.dilate(eroded, kernel, iterations=1)cv.imshow("dsad",dilated)return dilated

image-20250608215413011

(3)使用直方图定位初始车道位置

        在我们得到了透视变换后的二值车道线图像后,接下来就可以利用图像直方图来确定车道线的初始位置。这里我们主要关注图像下半部分的像素分布,因为车辆当前所处的车道线大多集中在图像底部区域,这一部分的信息更为清晰、可靠。

def get_linebase(frame):height, width = frame.shape[:2]# 找到直方图底部一半的峰值作为起始点histogram = np.sum(frame[height//2:,:], axis=0)midpoint = len(histogram) // 2leftx_base = np.argmax(histogram[:midpoint])rightx_base = np.argmax(histogram[midpoint:]) + midpointreturn leftx_base, rightx_base

        我们通过 np.sum(frame[height//2:, :], axis=0) 计算了图像下半部分每一列的像素和,形成了一个横向的一维直方图,这个直方图的波峰往往对应着车道线的左右边缘。接着我们将直方图一分为二,通过np.argmax(histogram[:midpoint]) 找到左半边的最大值索引,即左车道线的起始 x 坐标np.argmax(histogram[midpoint:]) + midpoint 找到右半边的最大值索引,加上偏移量后得到右车道线的起始 x 坐标。最终函数 get_linebase 返回的 leftx_baserightx_base,就是我们滑动窗口搜索的起点,后续滑窗的中心位置将以这两个点为基础进行逐层向上搜索。

image-20250608220211667

(4)应用滑动窗口逐步寻找车道线

        在通过直方图获取了左右车道线的起始位置 leftx_baserightx_base 后,我们就可以使用滑动窗口(Sliding Window)方法,其核心思想是:从图像底部开始,依据起始点设置左右两个滑动窗口,然后逐层向上滑动,根据每一层窗口内的有效像素分布不断更新窗口中心,从而实现对车道线的“跟踪”。我们首先先定义我们滑动窗口的基本参数:

# 滑动窗口参数
nwindows = 9  # 窗口数量
window_height = height // nwindows  # 每个窗口的高度
margin = 100  # 窗口宽度的一半
minpix = 50   # 重新定位窗口中心所需的最小像素数

        然后我们初始化窗口的起点和所需的变量:

leftx_base, rightx_base = get_linebase(frame)
# 创建输出图像来绘制滑动窗口
out_img = np.dstack((mask, mask, mask)) * 255
# 初始化当前位置
leftx_current = leftx_base
rightx_current = rightx_base
# 存储车道线像素的索引
left_lane_inds = []
right_lane_inds = []
# 获取所有非零像素的位置
nonzero = mask.nonzero()
nonzeroy = np.array(nonzero[0])
nonzerox = np.array(nonzero[1])

        然后开始遍历每一层滑动窗口,查找对应的车道线像素,这一部分代码就是滑动窗口“自适应搜索”的核心。每一层窗口都是基于上一层的结果动态更新中心点位置的,这种做法可以很好地应对弯道和部分缺失的车道线情况。如果某一层像素数量较少,则窗口位置不会改变,保持当前趋势,保证搜索的连贯性和鲁棒性。

# 遍历所有窗口
for window in range(nwindows):# 计算窗口边界win_y_low = height - (window + 1) * window_heightwin_y_high = height - window * window_height# 左车道线窗口边界win_xleft_low = leftx_current - marginwin_xleft_high = leftx_current + margin# 右车道线窗口边界win_xright_low = rightx_current - marginwin_xright_high = rightx_current + margin# 在输出图像上绘制窗口cv.rectangle(out_img, (win_xleft_low, win_y_low), (win_xleft_high, win_y_high), (0, 255, 0), 2)cv.rectangle(out_img, (win_xright_low, win_y_low), (win_xright_high, win_y_high), (0, 255, 0), 2)# 找到窗口内的非零像素good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]# 添加这些索引到列表中left_lane_inds.append(good_left_inds)right_lane_inds.append(good_right_inds)# 如果找到足够的像素,重新计算窗口中心if len(good_left_inds) > minpix:leftx_current = int(np.mean(nonzerox[good_left_inds]))if len(good_right_inds) > minpix:rightx_current = int(np.mean(nonzerox[good_right_inds]))

        在滑动窗口完成逐层搜索之后,我们已经得到了每一层中可能属于左右车道线的像素点索引,保存在 left_lane_indsright_lane_inds 中。但此时它们是一个按层存储的二维列表(列表嵌套),为了便于后续处理,我们需要将它们合并为一个一维数组:

# 将所有窗口中找到的像素索引连接成一个一维数组
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)

        接着,利用这些索引,我们就可以从原始的非零像素集合中提取出左右车道线各自的所有像素点坐标了:

# 提取左右车道线的所有像素点的x和y坐标
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]

        这样一来,leftx, lefty 表示左车道线上的所有像素点,rightx, righty 表示右车道线的所有像素点。我们可以使用这些坐标进行多项式拟合(例如二次曲线拟合),进而重建车道线的几何模型,实现车道线的平滑重建与可视化。

完整的函数处理如下:

def sliding_window_search(mask, leftx_base, rightx_base):height, width = mask.shape# 滑动窗口参数nwindows = 9window_height = height // nwindowsmargin = 100minpix = 50# 创建输出图像来绘制滑动窗口out_img = np.dstack((mask, mask, mask)) * 255# 初始化当前位置leftx_current = leftx_baserightx_current = rightx_base# 存储车道线像素的索引left_lane_inds = []right_lane_inds = []# 获取所有非零像素的位置nonzero = mask.nonzero()nonzeroy = np.array(nonzero[0])nonzerox = np.array(nonzero[1])# 遍历所有窗口for window in range(nwindows):# 计算窗口边界win_y_low = height - (window + 1) * window_heightwin_y_high = height - window * window_height# 左车道线窗口边界win_xleft_low = leftx_current - marginwin_xleft_high = leftx_current + margin# 右车道线窗口边界win_xright_low = rightx_current - marginwin_xright_high = rightx_current + margin# 在输出图像上绘制窗口(修复类型错误)cv.rectangle(out_img, (int(win_xleft_low), int(win_y_low)), (int(win_xleft_high), int(win_y_high)), (0, 255, 0), 2)cv.rectangle(out_img, (int(win_xright_low), int(win_y_low)), (int(win_xright_high), int(win_y_high)), (0, 255, 0), 2# 找到窗口内的非零像素good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & (nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]# 添加这些索引到列表中left_lane_inds.append(good_left_inds)right_lane_inds.append(good_right_inds)# 如果找到足够的像素,重新计算窗口中心if len(good_left_inds) > minpix:leftx_current = int(np.mean(nonzerox[good_left_inds]))if len(good_right_inds) > minpix:rightx_current = int(np.mean(nonzerox[good_right_inds]))# 连接数组left_lane_inds = np.concatenate(left_lane_inds)right_lane_inds = np.concatenate(right_lane_inds)# 提取左右车道线像素位置leftx = nonzerox[left_lane_inds]lefty = nonzeroy[left_lane_inds]rightx = nonzerox[right_lane_inds]righty = nonzeroy[right_lane_inds]# 给车道线像素着色out_img[lefty, leftx] = [255, 0, 0]  # 红色表示左车道线out_img[righty, rightx] = [0, 0, 255]  # 蓝色表示右车道线return leftx, lefty, rightx, righty, out_img

(5)多项式拟合输出拟合曲线

        在通过滑动窗口法提取到了左右车道线的像素坐标后,我们可以利用多项式拟合来构建出连续、平滑的车道线模型。由于实际道路中的车道线往往呈现一定的弯曲程度,因此我们通常选择使用**二次多项式(即二次曲线)**对提取到的车道线像素点进行拟合,具体函数如下: x = A y 2 + B y + C x = Ay^2 + By + C x=Ay2+By+C ,通过拟合后的多项式函数,我们可以在每一行(y 方向)计算出左右车道线的 x 坐标,从而生成连续的车道曲线。这不仅能让车道线显示得更加平滑,还能够显著提升在曲线路段的鲁棒性。最终,我们可以将这些拟合出的曲线绘制在原图或透视图上,辅助路径规划或偏移计算,这部分仅需使用np.polyfit() 拟合即可

def fit_polynomial(leftx, lefty, rightx, righty, img_shape):height, width = img_shape# 多项式拟合left_fit = np.polyfit(lefty, leftx, 2)right_fit = np.polyfit(righty, rightx, 2)# 生成拟合曲线上的 y 值ploty = np.linspace(0, height - 1, height)# 根据拟合参数计算对应的 x 值left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]return left_fit, right_fit, left_fitx, right_fitx, ploty

(6)实际应用——计算偏差

        完成车道线的拟合后,我们就可以基于拟合结果来估算车辆当前相对于车道中心的横向偏差(Lateral Offset),这是视觉巡线中最核心的控制依据之一。首先,我们在图像的最底部(即车辆所在位置对应的图像底边)获取左右车道线的横坐标值,分别记为 left_fitx[-1]right_fitx[-1],二者的平均值即为当前帧所估计的车道中心 lane_center。再获取图像的实际中心点位置 image_center = width // 2,即代表车辆相机的当前中心视角。车辆相对于车道中心的偏差即为这两者之差:

offset = image_center - lane_center

        此时的 offset 单位为像素,如果我们知道图像每个像素代表的实际距离(例如通过标定得到 1 pixel ≈ 3.7 / 700 m),我们就可以将其换算为实际偏移距离,从而反馈给控制器,进行方向调整或打角补偿。更进一步,由于我们拟合出了整条车道线的曲线信息(left_fitright_fit),我们不仅可以计算横向偏差,还可以获得当前车道曲率、车辆偏航角等几何信息。

  • 车道曲率(Curvature):通过拟合的二次多项式可计算道路的弯曲程度;

  • 车身航向角(Yaw angle):通过底部点处的导数估算当前车辆与车道方向的夹角;

  • 预测前方路径:可将拟合曲线延展,进行路径规划和控制前馈。

三、优化技巧与调参建议

(1)滑动窗口参数(数量、宽度、高度)的调试建议

在使用滑动窗口算法进行车道线提取时,不同场景、图像分辨率以及车道线形态的不同都会影响最终的检测效果,因此合理设置滑动窗口的核心参数对于精度与效率的平衡至关重要。以下是主要参数的详细说明及推荐的初始值:

参数名作用说明推荐初始值(以720p图像为例)
n_windows滑动窗口数量(从图像底部到顶部)9 或 10
margin每个窗口的宽度(左右偏移范围)100 ~ 150 px
minpix若窗口内像素数大于该值,则重新定位窗口中心50 ~ 100
window_height每个窗口的高度(由图像高度 / n_windows 得到)自动计算

总的来说,我们先固定图像尺寸和透视变换区域,接着从中间参数开始(如 n=9margin=100minpix=50)测试几张图,观察窗口是否对准线条,然后据识别结果调整参数:识别不稳?减小 margin,丢失线条?降低 minpix,拟合不光滑?增大 n_windows

(2)多帧融合与卡尔曼滤波

        为了提升车道线检测的稳定性和连续性,尤其是在帧率不高或图像存在噪声时,我们可以使用**多帧融合或状态估计方法(如卡尔曼滤波)**对拟合结果进行优化。

  • 多帧融合:我们可以保存上一帧的拟合参数(如二次曲线系数),然后将当前帧的检测结果与上一帧进行加权平均,用以缓解某一帧检测异常或缺失的问题
smooth_fit = alpha * current_fit + (1 - alpha) * previous_fit #其中 alpha 为平滑因子(如 0.7 ~ 0.9)
  • 卡尔曼滤波应用:我们可以定义状态量为拟合曲线的参数(如 [a, b, c]),然后使用拟合结果作为观测值,由于建模车道线变化为平滑变化过程所以可以用 KF 进行预测 + 更新
优势效果说明
抗噪性提升某一帧检测异常不致影响整体输出
时序连续曲线变化连续平滑,避免跳动
可扩展到路径预测滤波器预测的车道线可用于未来路径规划

        当然,除了这几个优化方法。我们还可以使用动态窗口宽度(根据车道线曲率动态调整 margin)、带权拟合(给靠近底部的点更高权重,提高近距离识别精度)等多种方法来提高我们拟合巡线的精准度

相关文章:

机器人/智能车纯视觉巡线经典策略—滑动窗口+直方图法

作者&#xff1a;SkyXZ CSDN&#xff1a;SkyXZ&#xff5e;-CSDN博客 博客园&#xff1a;SkyXZ - 博客园 在机器人或智能车的自主导航任务中&#xff0c;视觉巡线是一项最为基础且关键的能力之一。通过摄像头实时获取道路图像&#xff0c;并基于图像信息判断行驶路径&#xff0…...

附加模块--Qt OpenGL模块功能及架构

一、模块功能&#xff1a; 主要变化 Qt OpenGL 模块的分离&#xff1a; 在 Qt 6 中&#xff0c;原来的 Qt OpenGL 功能被拆分为多个模块 传统的 Qt OpenGL 模块 (QGL*) 已被标记为废弃 新的图形架构&#xff1a; Qt 6 引入了基于 QRhi (Qt Rendering Hardware Interface) 的…...

503 Service Unavailable:服务器暂时无法处理请求,可能是超载或维护中如何处理?

处理 "503 Service Unavailable" 错误是服务器管理者面临的常见挑战之一。这种错误通常表示服务器暂时无法处理请求&#xff0c;可能是由于服务器超载、维护中或其他临时性问题导致的。在本文中&#xff0c;我将介绍如何处理 "503 Service Unavailable" 错…...

抖音怎么下载没有水印的视频?

你是不是经常在抖音上刷到喜欢的视频&#xff0c;想保存下来却总是带着烦人的水印&#xff1f;无论是想收藏精彩片段&#xff0c;还是二次创作&#xff0c;水印都成了“拦路虎”。别急&#xff01;今天就来教你3种超简单方法&#xff0c;轻松下载无水印抖音视频&#xff0c;高清…...

虚拟机时间同步

一、常见同步方式 常见的虚拟机同步方式有给虚拟机配置ntp、或者用平台提供的agent对时与虚拟机所在的宿主机。第一种依赖网络、第二种依赖平台的agent这个三方工具。 二、利用ptp_kvm.ko来直接和宿主机同步时间 关键组件 ptp_kvm驱动、chrony。 PTP_KVM同步原理 |--------…...

三级流水线是什么?

三级流水线是什么&#xff1f; “三级流水线” 英文名&#xff1a;Three-Stage Pipeline 或 Basic 3-Stage Pipeline&#xff0c;是计算机处理器&#xff08;CPU&#xff09;设计中一种基本的指令流水线技术&#xff0c;它将指令的执行过程划分为三个主要阶段&#xff0c;使得…...

软件更新机制的测试要点与稳定性提升

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…...

自定义protoc-gen-go生成Go结构体,统一字段命名与JSON标签风格

背景 在日常的 Go 微服务开发中&#xff0c;Protocol Buffers&#xff08;protobuf&#xff09; 是广泛使用的数据交换格式。其配套工具 protoc-gen-go 会根据 .proto 文件生成 Go 结构体代码&#xff0c;但默认生成的字段名、JSON tag 命名风格往往不能满足所有团队或项目的代…...

Context API 应用与局限性

核心概念 React 的 Context API 是为了解决组件间数据共享而设计的一种机制&#xff0c;其核心价值在于提供了一种不通过 props 层层传递就能在组件树中共享数据的方法。在 React 应用中&#xff0c;数据通常是自上而下&#xff08;从父组件到子组件&#xff09;通过 props 传…...

LLMs 系列科普文(11)

目前我们已经介绍了大语言模型训练的两个主要阶段。第一阶段被称为预训练阶段&#xff0c;主要是基于互联网文档进行训练。当你用互联网文档训练一个语言模型时&#xff0c;得到的就是所谓的 base 模型&#xff0c;它本质上就是一个互联网文档模拟器&#xff0c;我们发现这是个…...

DQN算法(详细注释版)

DQN算法 DQN算法使用的常见问题 Q1: 为什么用目标网络而非Q网络直接计算&#xff1f; 答案&#xff1a;避免“移动目标”问题&#xff08;训练中Q网络频繁变化导致目标不稳定&#xff09;&#xff0c;提高收敛性。 Q2: 为什么用 max 而不是像SARSA那样采样动作&#xff1f;…...

sizeof 与strlen的区别

sizeof 和 strlen 是C和C 中用于处理数据大小和字符串长度的两个不同的操作符/函数&#xff0c;它们的区别如下&#xff1a; 概念和用途 - sizeof 是一个操作符&#xff0c;用于计算数据类型或变量在内存中所占的字节数&#xff0c;它是在编译时确定的&#xff0c;与数据的…...

论文阅读:HySCDG生成式数据处理流程

论文地址: The Change You Want To Detect: Semantic Change Detection In Earth Observation With Hybrid Data Generation Abstract 摘要内容介绍 &#x1f4cc; 问题背景 “Bi-temporal change detection at scale based on Very High Resolution (VHR) images is crucia…...

10万QPS高并发请求,如何防止重复下单

1. 前端拦截 首先因为是10万QPS的高并发请求&#xff0c;我们要保护好系统&#xff0c;那就是尽可能减少用户无效请求。 1.1 按钮置灰 很多用户抢票、抢购、抢红包等时候&#xff0c;为了提高抢中的概率&#xff0c;都是疯狂点击按钮。会触发多次请求&#xff0c;导致重复下…...

Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native 接口)

相关文章&#xff1a; Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读&#xff08;仅Native RAM&#xff09; – 徐晓康的博客 Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP&#xff08;仅 Native RAM&#xff09; – 徐晓康的博客 V…...

什么是高考?高考的意义是啥?

能见到这个文章的群体&#xff0c;应该都经历过高考&#xff0c;突然想起“什么是高考&#xff1f;意义何在&#xff1f;” 一、高考的定义与核心功能 **高考&#xff08;普通高等学校招生全国统一考试&#xff09;**是中国教育体系的核心选拔性考试&#xff0c;旨在为高校选拔…...

RISC-V 开发板 + Ubuntu 23.04 部署 open_vins 过程

RISC-V 开发板 Ubuntu 23.04 部署 open_vins 过程 1. 背景介绍2. 问题描述3. 解决过程3.1 卸载旧版本3.2 安装 Suitesparse v5.8.03.3 安装 Ceres Solver v2.0.03.4 解决编译爆内存问题 同步发布在个人笔记RISC-V 开发板 Ubuntu 23.04 部署 open_vins 过程 1. 背景介绍 最近…...

量子计算突破:新型超导芯片重构计算范式

​​2024年IBM 1281量子比特超导芯片实现0.001%量子错误率&#xff0c;计算速度达经典超算2.5亿倍​​。本文解析&#xff1a; ​​物理突破​​&#xff1a;钽基超导材料使量子相干时间突破​​800μs​​&#xff08;提升15倍&#xff09;​​架构革命​​&#xff1a;十字形…...

Spring Cloud 多机部署与负载均衡实战详解

&#x1f9f1; 一、引言 为什么需要多机部署&#xff1f; 解决单节点性能瓶颈&#xff0c;提升系统可用性和吞吐量 在传统单机部署模式下&#xff0c;系统的所有服务或应用都运行在单一服务器上。这种模式在小型项目或低并发场景中可能足够&#xff0c;但随着业务规模扩大、用…...

基于定制开发开源AI智能名片S2B2C商城小程序的首屏组件优化策略研究

摘要&#xff1a;在数字化转型背景下&#xff0c;用户对首屏交互效率的诉求日益提升。本文以"定制开发开源AI智能名片S2B2C商城小程序"为技术载体&#xff0c;结合用户行为数据与认知心理学原理&#xff0c;提出首屏组件动态布局模型。通过分析搜索栏、扫码入口、个人…...

EasyRTC嵌入式音视频通信SDK音视频功能驱动视频业务多场景应用

一、方案背景​ 随着互联网技术快速发展&#xff0c;视频应用成为主流内容消费方式。用户需求已从高清流畅升级为实时互动&#xff0c;EasyRTC作为高性能实时音视频框架&#xff0c;凭借低延迟、跨平台等特性&#xff0c;有效满足市场对多元化视频服务的需求。 二、EasyRTC技术…...

Flink 失败重试策略 :restart-strategy.type

在 Apache Flink 中&#xff0c;restart-strategy.type 用于指定作业的重启策略&#xff08;Restart Strategy&#xff09;&#xff0c;它决定了作业在失败后如何恢复。 Flink 提供了 4 种内置重启策略&#xff0c;可以通过 flink-conf.yaml 或代码动态配置。 1. 可配置的 rest…...

linux下gpio控制

linux下gpio控制 文章目录 linux下gpio控制1.中断命令控制/sys/class/gpio/export终端命令控制led 2.应用程序控制 3.驱动代码控制 1.中断命令控制 通用GPIO主要用于产生输出信号和捕捉输入信号。每组GPIO均可以配置为输出输入以及特定的复用功能。 当作为输入时&#xff0c;内…...

Spring Boot 从Socket 到Netty网络编程(下):Netty基本开发与改进【心跳、粘包与拆包、闲置连接】

上一篇&#xff1a;《Spring Boot 从Socket 到Netty网络编程&#xff08;上&#xff09;&#xff1a;SOCKET 基本开发&#xff08;BIO&#xff09;与改进(NIO)》 前言 前文中我们简单介绍了基于Socket的BIO&#xff08;阻塞式&#xff09;与NIO&#xff08;非阻塞式&#xff0…...

Orthanc:轻量级PACS服务器与DICOMweb支持的技术详解

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用…...

量子计算导论课程设计 之 PennyLane环境搭建

文章目录 具体配置conda 虚拟环境配置Pennylane 正所谓&#xff0c;磨刀不误砍柴工&#xff0c;想要进行量子计算导论的课程设计&#xff0c;首先就是搭建好平台&#xff0c;推荐大家就是本地搭建&#xff0c;那么下面有三种选择 QiskitTensorFlow QuantumPennylane 具体配置…...

GAN优化与改进:从条件生成到训练稳定性

摘要 本文聚焦生成对抗网络&#xff08;GAN&#xff09;的核心优化技术与改进模型。系统解析 条件生成对抗网络&#xff08;CGAN&#xff09; 的可控生成机制、深度卷积GAN&#xff08;DCGAN&#xff09; 的架构创新&#xff0c;揭示GAN训练崩溃的本质原因&#xff0c;并介绍W…...

【Dv3Admin】系统视图下载中心API文件解析

大文件导出与批量数据下载常常成为后台系统性能瓶颈&#xff0c;合理管理下载任务是保障系统稳定运行的关键。任务化下载机制通过异步处理&#xff0c;避免前端等待阻塞&#xff0c;提升整体交互体验。 围绕 download_center.py 模块&#xff0c;剖析其在下载任务创建、查询、…...

linux库(AI回答)

STL POSIX关系 DeepSeek-R1 回答完成 搜索全网22篇资料 STL&#xff08;标准模板库&#xff09;和 POSIX&#xff08;可移植操作系统接口&#xff09;是两种不同领域的技术标准&#xff0c;它们在 C/C 开发中各有侧重&#xff0c;但可以协同使用。以下是它们的关系和区别&…...

CoordConv: CNN坐标感知特征适应

传统卷积 vs CoordConv 详细对比 传统卷积对空间位置不敏感&#xff0c;CoordConv通过显式添加坐标信息解决这个问题在特征图中嵌入(x, y)坐标和可选的径向距离r使模型能够感知空间位置关系 1. 传统卷积的"空间位置不敏感"问题 传统卷积的特点&#xff1a; 输入: …...