ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

V-REP仿真多车道巡线小车:视觉识别与PID避障算法实践

V-REP仿真多车道巡线小车:视觉识别与PID避障算法实践 做移动机器人算法验证仿真环境真的能省掉一大半的烦恼。V-REP2020年后改名为 CoppeliaSim是我用下来比较顺手的一款物理引擎、传感器仿真和 API 生态都够扎实做轮式机器人、机械臂、多机协同都没问题。这次分享一个我花了大概两周时间从零搭建的项目在 V-REP 里搭一个多车道场景让一台差速小车既能沿指定彩色车道巡线行驶又能在前方出现障碍物时主动绕行、自动切换到另一条可通行车道。整个算法框架基于 V-REP Remote API Python 实现不依赖收费组件逻辑也可以比较平滑地移植到真实小车上。如果你正准备用仿真验证巡线算法、想在课程设计里加一个避障功能或者打算把手头的单线巡线小车升级成能应付多车道场景的版本这篇内容应该能帮上忙。我会从场景建模讲到算法实现和调参把踩过的坑一并整理出来。1. 项目概述与系统设计思路1.1 这个项目到底要解决什么问题单纯做一个巡线项目其实不难视觉识别一条黑线或白线用 PID 纠偏就能跑起来。但在真实的园区配送、仓储搬运场景里地面上往往有多条车道线而且前方随时可能出现障碍物。如果小车只会死盯着一条线那障碍物一出现就只能原地停车等着人工干预。这个项目的核心目标是让小车具备两个能力一是能识别多条不同颜色的车道线并动态选择当前最合理的车道行驶二是在目标车道上检测到障碍物时能够自主避障绕行绕行结束后还能成功切回或切换到另一条可通行的车道。听起来不复杂但把颜色识别、PID 控制、传感器融合和状态机串起来还是有不少细节值得记录。1.2 整体技术方案与选型思路我一开始就确定了方案用 V-REP 做仿真环境用 Python 的 Remote API 写控制逻辑。原因很简单直接在 V-REP 内置的 Lua 脚本里写整套算法不是不行但调试起来非常痛苦图像处理也不方便。Python 这边能直接用 OpenCV 做颜色分割、计算质心还可以开调试窗口实时看到小车“看”到了什么这对调参帮助极大。车辆模型采用常见的差速驱动结构两个主动轮分列车身左右前后加一个万向球保持平衡。差速驱动的好处是转向控制直观左右轮速差决定转动方向和角速度和真实小车底盘一致后续移植到 ROS 或者单片机平台也方便。传感器方面我布置了两类一个朝下的视觉传感器负责看地上的彩色车道线三个朝前方的超声波传感器负责检测障碍物距离和大致方位。视觉负责“看路”超声波负责“看障碍物”两者各自独立由顶层状态机协调。1.3 为什么选 V-REP 而不是其他仿真器可能有人会问Gazebo 不也可以吗确实可以而且 Gazebo 和 ROS 结合更紧密。但从我的实际体会来看V-REP 在这类小项目上上手快很多图形界面里拖拽模型、添加传感器、画地面都是所见即所得不需要一开始就维护复杂的 URDF 和世界文件。V-REP 自带的动力学、碰撞检测和传感器噪声模型也够用做巡线避障这个级别的验证完全没有压力。另外 V-REP Remote API 在 Windows、Linux、macOS 上都有现成库把sim.py拷贝到 Python 环境或者通过 pip 安装就能连对新手比较友好。后面如果想把项目扩展到多机协同、机械臂抓取V-REP 也都能继续支撑。2. V-REP 场景搭建与车辆模型构建2.1 绘制多车道地面场景我是从一块地面开始搭的。V-REP 里默认有一块适合物理仿真的地面我在上面新建了三个长方体薄片用不同颜色表示三条车道线红色、绿色、蓝色每条宽 0.1 米左右。选用彩色线而不是黑白线是为了让视觉分割更方便红色和绿色在 HSV 颜色空间里分离度很高不容易互相干扰。车道线之间的间距我留了 0.8 米。这个距离对应到真实园区通道比较合理也给小车避障切换车道留出了操作空间。如果间距太小视觉传感器的视场可能同时出现两条线反而增加识别难度间距太大绕行后回归车道的距离会变长状态机恢复也会更慢。在 V-REP 里新建长方体薄片时记得放在地面略上方高度设成 0.005 米左右避免和地面模型完全重合导致物理引擎抖动。此外最好把薄片设置成对碰撞无响应否则车轮经过时会因为这种极薄的凸起而发生不必要的跳动。2.2 差速驱动小车模型小车模型我没有直接用 V-REP 自带的移动机器人模板而是从零搭了一个简单的一个底盘、两个主动轮、两个随动万向球。底盘用 Cuboid尺寸大概 0.3 x 0.2 x 0.05 米两个车轮用 Cylinder半径 0.03 米厚度 0.02 米分别放在车体两侧。关键是两个主动轮各配一个 Revolute Joint左轮和右轮的 Joint 我用leftJoint、rightJoint命名方便 Python 端查找句柄。Joint 和车轮的父子关系要设置正确Joint 是父节点车轮是子节点Joint 的旋转轴要沿车轮轴向。这个关系搞反的话轮子并不会跟着 Joint 转车基本就废了。重力、质量、摩擦系数这些参数V-REP 默认值基本够用。我实际改过的只有两个车轮与地面之间的摩擦系数默认偏大时高速转弯会有明显侧滑调小一点后循迹更顺滑还有车轮的电机扭矩默认扭矩偏小时车遇到地面薄片边缘可能带不起来。Joint 模式记得设为 torque/force 模式这样 Python 端才能用simxSetJointTargetVelocity直接给目标速度底层动力学会按电机力矩去逼近这个速度效果接近真实直流减速电机。2.3 传感器布置与参数配置视觉传感器我放在车体正中央偏前的位置用一个支架抬高到离地面约 0.25 米朝向正下方视角约 60 度。这样能保证在车前 0.5 米左右的位置看到一段车道线又不至于因为视角过大把周围环境都拍进去。V-REP 的 Vision Sensor 参数面板里有分辨率、视角、近远裁剪面、曝光等设置。分辨率我用 256x256这个大小对颜色分割完全够用而且 Remote API 读取图像的耗时短。分辨率调太大只会拖慢控制频率对这个小项目没有必要。避障用的三个 Proximity Sensor一个朝正前方另外两个分别偏左和偏右 30 度左右检测距离统一设为 0.4 米。这样三个传感器组合起来基本能判断出障碍物在车的左前方、正前方还是右前方为避障决策提供依据。下面是传感器配置汇总传感器类型安装位置朝向检测距离用途视觉传感器Vision Sensor车身中部垂直向下-车道线识别左前超声波Proximity Sensor左前方偏左30°0.4m左前方障碍检测中前超声波Proximity Sensor车头中轴正前方0.4m正前方障碍检测右前超声波Proximity Sensor右前方偏右30°0.4m右前方障碍检测2.4 Remote API 连接与初始化场景搭好之后先在 V-REP 菜单的 Add-ons 里启用 Remote API 服务默认端口是 19997。然后在 Python 端导入 sim 库新版 CoppeliaSim 提供的是sim.py旧版 V-REP 里叫vrep.py接口基本一致。连接部分有几个容易踩的坑sim.simxStart握手阶段建议用simx_opmode_oneshot_wait确保连接稳定后续读取传感器数据改成 streaming 模式避免反复丢数据。图像数据从simxGetVisionSensorImage拿到后是一个字节数组顺序是 RGB需要先用 numpy 转成数组、reshape 成 256x256x3再用 OpenCV 转成 BGR 格式处理。3. 视觉巡线多车道识别与 PID 控制3.1 图像获取与颜色分割巡线算法的第一步是从视觉传感器拿到原始图像。Remote API 里用simxGetVisionSensorImage读取图像分辨率是 256x256返回的是一个包含 256x256x3 个字节的一维数组。我一般做两步处理先np.frombuffer转成 numpy 数组再 reshape 成 256x256x3最后转成 OpenCV 的 BGR 格式。颜色分割我选的是 HSV 空间而不是 RGB。原因很简单受光照和反光影响时RGB 数值波动非常剧烈但色相H基本能保持稳定。车道线的颜色用 H 的范围来过滤再配合 S 和 V 的最小阈值可以把地面背景和车道线分得很干净。以红色车道线为例红色在 HSV 里跨越了 0 度和 180 度的边界所以要取两段区间代码写起来稍麻烦一点def create_color_mask(hsv_frame, color_name): if color_name red: lower1 np.array([0, 80, 80]) upper1 np.array([10, 255, 255]) lower2 np.array([170, 80, 80]) upper2 np.array([180, 255, 255]) mask cv2.inRange(hsv_frame, lower1, upper1) \ cv2.inRange(hsv_frame, lower2, upper2) elif color_name green: lower np.array([40, 60, 60]) upper np.array([80, 255, 255]) mask cv2.inRange(hsv_frame, lower, upper) elif color_name blue: lower np.array([100, 60, 60]) upper np.array([130, 255, 255]) mask cv2.inRange(hsv_frame, lower, upper) return mask3.2 车道线质心提取与误差计算有了二值掩膜图下一步是找到目标颜色车道的中心。我习惯用图像的矩moments来算质心。质心横坐标和图像中心横坐标的差值就是巡线的核心误差量def get_line_center(mask, image_width): moments cv2.moments(mask) if moments[m00] 200: # 面积太小认为没有检测到线 return None cx int(moments[m10] / moments[m00]) cy int(moments[m01] / moments[m00]) return cx, cy误差的计算方式是err_pixel cx - image_width / 2。误差为 0 说明车道线正好在图像中心小车在线正上方误差越大说明偏得越多。我习惯再把这个像素误差映射到 -1 到 1 的比例值方便 PID 调参err err_pixel / (image_width / 2)。有人可能会问为什么用质心而不是用边缘检测、Hough 变换拟合直线我的实际体会是这个场景地面简单、车道线颜色单一质心法代码最少、计算最快、跑起来最稳。Hough 变换和最小二乘拟合当然更高级但在这里属于过度设计而且抗干扰不一定比质心好多少。等以后要处理弯曲车道、车道线断裂的场景再升级算法也不迟。3.3 PID 控制器与差速转向控制控制端我用的是经典 PID 加差速模型。PID 的输入是归一化后的像素误差 err输出是一个转向修正量 turn。然后把基础速度 base_speed 分别加减 turn得到左右轮的目标速度class PID: def __init__(self, kp, ki, kd): self.kp kp self.ki ki self.kd kd self.last_error 0.0 self.integral 0.0 def update(self, err, dt): self.integral err * dt derivative (err - self.last_error) / dt if dt 0 else 0.0 output self.kp * err self.ki * self.integral self.kd * derivative self.last_error err return output在 V-REP 里设置轮速用simxSetJointTargetVelocity第一个参数是 clientID第二个是 Joint 句柄第三个是速度值单位是 rad/s。我这里的 base_speed 通常设在 2.0 到 3.0 rad/s对应线速度约 0.2 到 0.3 m/s轮半径约 0.03 米对仿真小车来说比较稳。转向逻辑的实作片段pid PID(1.2, 0.05, 0.3) turn pid.update(err, dt) left_speed base_speed - turn right_speed base_speed turn set_wheels(left_speed, right_speed)注意 turn 的正负方向要和坐标系的约定保持一致不然会出现越错越离谱的“反打”现象。如果发现车越跑越歪先确认误差符号和轮速方向是否反了再动 PID 参数。3.4 多车道动态切换逻辑多车道的核心是让车多一个“当前目标车道”的概念。我在地图里放了三种颜色的车道线车启动时默认选红色车道为目标。如果红色车道前方有障碍避障状态结束后优先检测离车最近且可通行的车道把目标车道切换为它。具体实现上视觉部分我会同时计算三种颜色的质心存成候选列表再根据优先级和可见性决定最终目标def select_target_lane(frame_hsv, banned_colorNone): candidates [] for color in [red, green, blue]: if color banned_color: continue mask create_color_mask(frame_hsv, color) center get_line_center(mask, IMG_WIDTH) if center is not None: offset abs(center[0] - IMG_WIDTH / 2) candidates.append((offset, color, center)) if not candidates: return None candidates.sort() return candidates[0]这里有一个小技巧给select_target_lane加一个banned_color参数在避障绕行过程中暂时禁掉障碍物占用的车道。比如红色车道中间放了障碍物车绕行到一半时眼前同时出现红线和绿线如果直接选最近的可能会刚绕出去又一头撞回原车道。把红色 ban 掉强制选绿线就能避免这种反复。4. 障碍物检测与避障状态机4.1 超声波测距与障碍物方位判断避障部分的数据来源是三个 Proximity Sensor。V-REP 里读取的标准函数是simxReadProximitySensor返回的第二个值是检测状态0 或 1第三个值是距离单位是米。为了减少波动我做了简单滤波连续取三次读数中的最小值。障碍物方位判断规则也比较直接中间传感器距离小于 0.35 米时认为前方有障碍物需要转向左前传感器读数比右前传感器明显更小说明障碍物在左侧应该优先向右转向右前传感器读数更小则反过来。这里我不是只看哪个传感器触发而是比较左、右两个传感器的读数。因为超声波有波束角度只有一边读到的距离明显更小才能判断障碍物更靠近哪一边。def get_obstacle_direction(dists): if dists[center] 0.35: if dists[left] dists[right]: return left else: return right elif dists[left] 0.25: return right elif dists[right] 0.25: return left return None4.2 状态机设计与状态转移条件整个系统我拆成了四个状态正常巡线、向左绕行、向右绕行、恢复寻线。状态机不是最复杂的方案但胜在直观、容易调对巡线避障这类任务完全够用也方便后续在真车上实现。状态含义进入条件退出条件LINEFOLLOW正常巡线默认状态 / 恢复完成中心传感器 0.35mAVOIDLEFT向左绕行中心有障碍且左侧空间更大左侧传感器距离 0.5mAVOIDRIGHT向右绕行中心有障碍且右侧空间更大右侧传感器距离 0.5mRECOVER寻找可通行车道绕行结束但暂无目标线检测到可用车道线从正常巡线进入避障状态时我会先记住进入避障前的基础速度绕行时先把速度降到原来的一半左右避免转弯过猛导致动态失稳。4.3 绕行控制策略与回线算法绕行控制我用的是比较朴素的固定差速转弯向左绕行时左轮低速、右轮高速向右绕行时反过来。转弯持续时间不固定而是以传感器读数为准动态调整这样不管障碍物大小都能绕过去。绕行结束之后进入恢复寻线状态。这个状态要做的事很明确继续低速直行同时不断调用select_target_lane检测地面上的可用车道线。一旦找到距离图像中心最近的线就把目标车道设定为该线颜色然后切回正常巡线。为了防止恢复状态因为车道线抖动反复切换我加了一个 1 秒的稳定时间连续检测到目标车道超过 1 秒才正式切换。这个细节在仿真里看着不起眼实际跑起来能避免很多“蛇形”轨迹的问题。如果你发现车绕行后一直在原地转圈找线多半是恢复状态里的搜索速度太快或者车道检测的阈值没配对。5. 系统集成、调参与问题排查5.1 Python 主循环框架与控制频率把所有模块拼起来主循环大致长这样while sim.simxGetConnectionId(clientID) ! -1: frame get_vision_image(clientID, vision_handle) dists read_ultrasonics(clientID, [left_ultra, center_ultra, right_ultra]) if state LINEFOLLOW: if dists[center] 0.35: direction get_obstacle_direction(dists) state AVOIDLEFT if direction left else AVOIDRIGHT else: err compute_line_error(frame, target_color) if err is not None: turn pid.update(err, dt) set_wheel_speed(base_speed - turn, base_speed turn) else: set_wheel_speed(base_speed * 0.5, base_speed * 0.5) elif state AVOIDLEFT: set_wheel_speed(base_speed * 0.5, base_speed * 1.5) if dists[left] 0.5: state RECOVER elif state AVOIDRIGHT: set_wheel_speed(base_speed * 1.5, base_speed * 0.5) if dists[right] 0.5: state RECOVER elif state RECOVER: set_wheel_speed(base_speed * 0.6, base_speed * 0.6) result select_target_lane(frame_hsv) if result is not None and stable_timer 1.0: target_color result[1] state LINEFOLLOW dt time.time() - t0 time.sleep(max(0, 0.05 - dt))控制频率我控制在 20Hz 左右也就是 50ms 一个周期。频率太高的话Remote API 通信开销会吃掉不少 CPU而且 PID 的 dt 太小容易放大噪声频率太低车会反应迟钝。20Hz 对 0.2 到 0.3 m/s 速度的仿真小车完全够用。5.2 PID 与速度参数整定过程这里重点讲讲调参。我的方法一直是先 P 后 D 最后 I。先把 Ki、Kd 都设成 0Kp 从 0.8 开始往上加。加到 1.2 时车基本能稳定沿红线走但会在线上来回小幅摆动继续加大到 1.5摆动明显加剧说明 P 过大。于是回到 1.2加 Kd 抑制摆动Kd 从 0.1 开始试到 0.3 时摆动基本消失车走得比较直。最后加一点 Ki 消除稳态偏差Kp1.2、Ki0.05、Kd0.3是我最终定下来的一组参数。还要提一个容易被忽略的参数基础速度 base_speed。很多人一上来就把车速拉满结果小车在弯道直接甩出去。我的经验是先把速度调低比如 2.0 rad/s把整条路跑通确认巡线逻辑正确再慢慢提速到 3.0、3.5。提速的同时PID 的 Kp 要按比例调大一点否则误差相同的情况下速度变快会导致纠正不及时。如果你发现车一直在左右横摆除了调 PID还要检查 base_speed 和 turn 的相对大小。当 turn 接近 base_speed 时转弯侧的轮子几乎停转车会变成原地转圈也不理想。我会把 turn 的最大值限制在 base_speed 的 70% 左右给车保留最低前进速度。5.3 典型问题与避坑记录调试过程中比较典型的问题我整理成了一张速查表现象可能原因解决办法视觉图像全黑或全白Vision Sensor 未开启或参数异常检查传感器 enable 状态确认分辨率和裁剪面红色线识别不稳定H 阈值上下限卡在边界附近红色用双区间并给 S/V 加最小阈值车沿线画 S 形Kp 过大或 dt 不真实降低 Kp、增大 Kd确认 dt 来自实际循环时间避障后找不到车道RECOVER 时间太短或车速过快加稳定时间降低恢复状态的车速Remote API 读取超时未开启服务或端口不对确认 19997 端口等待握手成功再读数据车跑出地图转弯过猛 / 丢线后仍高速限幅 turn丢线时降速搜索除了表里的问题还有一个特别想强调的经验仿真中看不出来、但真实场景里一定会遇到的麻烦是视觉传感器视角太窄导致弯道丢线。我在转弯半径比较大的弯道处测试时发现车经常冲出去。后来给 Vision Sensor 加宽了视角并把安装位置稍微前移让车前下方更大范围被覆盖丢线问题就缓解了。5.4 后续可扩展方向这个项目跑通之后还可以在几个方向上继续扩展。一是把固定差速绕行换成动态窗口法DWA或者 VFH 局部规划避障路径会平滑很多二是给视觉识别加上车道弯曲度估计不只看质心还拟合车道线方程提前预判转向三是把三个超声波换成二维激光雷达用点云做更精确的障碍物边界检测绕行轨迹会更贴近真实路径四是可以加入多车协同让两台车各自跑不同车道在交叉口做优先级调度V-REP 支持多客户端连接完全可以实现。最后分享一个个人经验。做这个项目时最耗时其实不是算法本身而是各种小配置传感器没开、图像通道顺序反了、坐标系方向不对这些问题排查起来比写代码更费神。所以建议从场景搭建开始就养成好习惯每个传感器命名清楚、代码里都加注释、参数集中放在文件头部。把这些基础工作做扎实后面的算法调试会顺畅很多。
返回列表