
做自动驾驶仿真的老朋友应该都懂激光雷达给得了距离但给不了颜色和纹理。红灯到底是灯壳本身还是只是反光车道线是实线还是虚线很多关键判断最后都得靠摄像头来解。AWSIM这款开源模拟器在Autoware圈子里用得很顺不过默认场景里的相机数量确实有点抠门。我在本地把官方Demo跑通之后很快遇到一个实际问题想测红绿灯识别、想跑环视拼接算法默认那一两个相机根本覆盖不了。为了验证后续的感知方案我给AWSIM模拟器做了一次“新增多相机”的改造相机从1路增加到6路分辨率、FOV、外参全部做成可配置并且把数据完整地发到ROS 2里让Autoware算法直接用。整个过程差不多花了两周踩了无数渲染和同步的坑这次把完整思路和可以复现的操作都整理出来。说实话在不少自动驾驶项目里摄像头传感器仿真容易被当成“比较简单的那部分”因为本质上就是在Unity里挂一个Camera组件、渲染一张图发出去。但真正做成一个多相机传感器系统时你会发现问题远比想象得多相机布局、内参外参标定、时间同步、帧率控制、编码、内存带宽瓶颈任何一个环节没处理好下游算法都会给你各种诡异的表现。这篇文章就按完整流程来写从需求设计开始一直到在Rviz里看到6路图像正常输出整个过程里值得注意的细节我都会提。1. 想清楚再动手多相机到底要解决什么场景1.1 光有默认相机不够用AWSIM的公开Demo一般默认带一个类似驾驶员视角的主相机。这个相机用于可视化确实不错但如果你要做正儿八经的感知开发会发现它远远不够。举一个最简单的例子红绿灯检测。前向相机需要足够高的分辨率才能把一两百米外的信号灯状态拍清楚默认主相机往往会把视角开得很宽画面里远处的目标只有几个像素根本没法为感知模块提供稳定的输入。另一个例子是泊车和低速场景。前向相机覆盖不了车辆左右两侧侧向来车、路肩和车身的距离靠一个前向相机根本算不出来。在默认配置下做自动泊车验证你会发现图像连最基本的侧视信息都给不了。再说到感知模型的训练和回灌验证如果仿真里只有一路相机那训练出来的模型上车以后大概率不适应真实车辆的多相机布局。所以多相机支持不是锦上添花而是模拟器和实车之间的一层重要匹配。1.2 先看看主流的相机布局方案我在动手之前把常见自动驾驶项目中的传感器布局过了一遍。到今天主流的多相机配置大概有这几类组合方案典型用途常见布置前视单目最简单的L2辅助驾驶1个广角前视前视三目L2、高速NOA广角中焦长焦挡风玻璃后环视4相机泊车、AVP前后保险杠左右后视镜多前向后视L3/L4多感知融合前视2~3个、左右侧视、后视1个全景8~12相机城市NOA、Robotaxi车顶或车身四角重叠覆盖如果你的项目主要是结构化道路行驶前视广角加长焦的组合几乎必不可少如果有泊车需求左右后视镜位置的相机必须有如果要做完全无盲区验证那就要上环视甚至鱼眼。多相机仿真要做的就是先于硬件把感知算法的逻辑验证跑通。1.3 我最终定的传感器组合经过权衡我第一版定的配置是6路相机前向广角、前向长焦、左侧后视、右侧后视、后视、顶视。第六路顶视不是用来做感知的主要是给整个系统做俯瞰监控和合成数据采集。我没有盲目堆到十几个相机因为仿真里多一个相机就意味着多一份渲染负担和带宽开销先做到够用后期需要再往配置里加。设计时我遵循一个原则每路相机都要有明确的职责。广角管近距离和路口长焦管远处信号灯和交通标志左右后视管变道场景后视管泊车。这样配置背后的意义是每一路图像都不是摆设都能对接到真实算法模块里去跑一版评测。2. 多相机系统的关键设计参数2.1 位置、姿态与视野覆盖相机位置会直接影响图像内容尤其是自车遮挡问题。仿真里很多新手习惯直接把相机放在车辆中心轴线上但真实车辆上前视相机一般安装在内后视镜附近。如果安装位置太低发动机盖会占掉大量画面安装位置太高又会带来明显的俯视变形。通常前视广角相机安装高度在离地1.2到1.5米之间俯仰角向下倾斜2到5度让画面能看到车头前沿又不至于把地面占满。我给的配置里前视广角相机位于前挡风玻璃中央靠上坐标是相对于后轴中心的1.801.3米。长焦和广角装在同一个壳里位置完全一致只有FOV和分辨率不同。左右侧视相机参照外后视镜位置分别放在1.2正负0.91.2附近后视相机放在后备箱盖中部-1.601.1。这里的坐标单位都是米朝向按照ZYX欧拉角定义roll、pitch、yaw的基准是车辆坐标系。这样设定之后各相机的视野不但能覆盖主要盲区而且重合区域可以留出来做多相机联合验证。2.2 分辨率、FOV和帧率的取舍分辨率、FOV和帧率三者是一组互相拉扯的参数不是越大越好。FOV越宽相同分辨率下每个像素对应的物理尺度越大远处目标会很小FOV越窄视野丢失严重容易漏目标。在仿真里fov直接决定Unity相机的透视投影也决定ROS 2里CameraInfo的内参标定结果。我这边广角相机用的1920x1080FOV是70度很多通用感知模型都可以直接吃这个分辨率长焦相机用的1280x720FOV压到30度目的是把远处的信号灯放大到足够像素数。如果你暂时不需要长焦可以省掉这一路毕竟长焦相机在同样帧率下单位时间产生的数据量并不少。帧率我给的是15Hz没有直接拉到30Hz。视觉感知模块一般本来就是10到15Hz处理一次30Hz只会把数据管道爆掉。每个相机帧率的控制看起来简单但实际更新时要注意做节流不是在Unity的Update里每帧都发。准备好的图像应该按照相机的publishRate来发送否则五六个相机同时以每秒60帧的频率发射数据后面任何一个算法节点都扛不住。2.3 别忽略同步和快门的坑多相机同步是特别容易被忽视的问题。真实车辆上环视相机靠硬件触发保证同一时刻曝光。如果仿真里每路相机各过各的时间有的信息早10毫秒、有的晚30毫秒视觉感知做融合时就会出现目标位置不一致的“跳变感”。在ROS 2里最终表现就是多个图像Topic的时间戳对不上message_filters同步节点会开始丢数据。我的做法是全局统一用一个时钟每路相机在同一个仿真步长里采集图像并给消息打上相同的时间戳基准。由于Unity的渲染本身在主线程执行如果不做异步读取多相机每一帧都会把主线程堵到怀疑人生。这个后面性能部分会重点讲。另外快门方式也值得提。模拟器默认相当于全局快门这对仿真感知来说是好事不会出现果冻效应。如果你要模拟真实卷帘快门的拖影效果需要在图像后处理阶段加像素行延迟曝光逻辑这个不是多相机的基础工作但可以做成后期扩展的开关选项。3. 在AWSIM里落地多相机3.1 AWSIM里加传感器的基础套路AWSIM本身基于Unity开发传感器都可以作为组件挂接在车辆模型下面。它的ROS 2通信层封装得比较完整你只要把数据准备好发出去就可以被Autoware订阅。我扩展传感器时遵循了AWSIM本来就有的一套组件化思路每个Sensor是一个继承MonoBehaviour的组件负责采集数据和发布数据。多相机我拆成了两层。第一层是单个相机传感器组件负责渲染、读取像素、参数配置第二层是MultiCameraManager负责按照配置文件创建所有相机并把每个相机的数据路由到独立的Topic。这样做的好处是后续你想加第7路、第8路相机完全不用改代码加一条配置记录就能跑。单个相机传感器的核心逻辑大致是这样public class CameraSensor : MonoBehaviour { public string frameId ; public int width 1920; public int height 1080; public float fov 70f; public float publishRate 15f; private Camera cam; private RenderTexture rt; private Texture2D snapshot; private float timer; void Start() { rt new RenderTexture(width, height, 24); cam gameObject.AddComponentCamera(); cam.targetTexture rt; snapshot new Texture2D(width, height, TextureFormat.RGB24, false); } void Update() { timer Time.deltaTime; if (timer 1.0f / publishRate) return; timer 0f; RenderTexture.active rt; snapshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); snapshot.Apply(); RenderTexture.active null; PublishFrame(snapshot); } }这里关键的一步是把Unity相机渲染输出到指定的RenderTexture然后通过ReadPixels把像素数据拿回CPU。PublishFrame里通过ROS 2发布器发送sensor_msgs/Image。但要注意ReadPixels本身是一个同步且较耗时的操作后面性能优化里我会给出替代方案。3.2 用配置文件驱动整个相机系统为了让多相机可以灵活增减我把所有相机参数放在了外部JSON里。这样改相机位置、改FOV都不需要重新编译整个Unity工程。实际项目里让算法工程师或测试人员自己改配置文件比每次找你改代码要省太多时间。配置结构大致是这样{ cameras: [ { name: camera_front_wide, frame_id: camera_front_wide_optical, parent_link: base_link, position_m: [1.8, 0.0, 1.3], orientation_rpy: [0.0, 0.03, 0.0], width: 1920, height: 1080, fov_deg: 70.0, publish_rate_hz: 15.0 }, { name: camera_front_far, frame_id: camera_front_far_optical, parent_link: base_link, position_m: [1.8, 0.0, 1.3], orientation_rpy: [0.0, 0.03, 0.0], width: 1280, height: 720, fov_deg: 30.0, publish_rate_hz: 15.0 } ] }MultiCameraManager加载这个文件之后遍历每一条配置创建CameraSensor并挂到对应位置上。注意相机坐标是相对base_link定义的但相机坐标系要作为独立的tf frame在ROS 2里发布出来否则下游节点在Rviz里看到图像时会全部堆在原点。3.3 从相机到ROS 2消息的完整管线现在到了整个链路里最容易出乱子的环节。图像从相机渲染好是一回事进ROS 2是另一回事。每张图像的Header要规范填写尤其是时间戳。我在开发中发现不少人用Unity的Time.time作为时间戳但它通常是从场景开始计时跟ROS 2的Topic时间基准不一致会直接影响message_filters的同步精度。我最终是把外部仿真时间统一同步到一个时钟在消息里填上秒和纳秒。发布图像时编码要固定成“rgb8”。ROS 2的image_transport默认编码是rgb8如果你发成了RGBA或BGR而下游模型没有处理立刻就会出现颜色错乱。AWSIM的底层ROS 2接口对发布消息的封装已经比较成熟你只要在订阅端把传输格式和帧id对好就行。CameraInfo也一样FrameId要和图像一致时间戳和图像时间戳保持一致。一个常见错误是把相机光学坐标系和相机坐标系混用。ROS里图像坐标系是x向右、y向下、z向前而车辆坐标系是x向前、y向左、z向上这个变换我在做静态tf的时候花了不少时间才理清楚。3.4 内参外参别等算法跑挂了再补内参标定是视觉感知绕不开的硬骨头。真实车靠标定板仿真车理论上可以直接从相机参数手算。但很多人在这一步翻车了Unity相机的fov到底是垂直fov还是水平fov官方文档里其实写得不太醒目。我踩过坑后确认Unity中Camera.fieldOfView指的是垂直方向的FOV不是水平方向。你在配置里写“FOV70”Unity内部会按屏幕宽高比换算成水平FOV。因此写代码生成标定矩阵时fx要用宽度和水平FOV来算或者如果你只有垂直fov要先做换算。公式如下fx width / (2 * tan(hFov / 2)) fy height / (2 * tan(vFov / 2)) cx width / 2 cy height / 2其中hFov需要通过Unity的垂直fov和宽高比换算出来。把这个矩阵填进CameraInfo的K矩阵光心默认在图像正中心畸变项D可以全填0。在仿真里完全没问题因为我们没引入镜头畸变。如果后面要做更接近真实相机的效果可以在后处理Shader里加径向畸变那时D矩阵就要按实际畸变模型填。至于外参我会把每个相机的坐标写成静态tf发布到ROS 2。坐标关系是base_link到camera_xxx再到光学坐标系。很多人把translation和rotation写反或者把四元数顺序搞错结果在Rviz里看到的点云投影全部偏移。想确认外参是否正确直接在Rviz里把激光雷达点云投影到图像上测试头几帧就能发现问题比在算法里黑盒摸索省事得多。4. 调试实录多相机最容易出的一堆问题4.1 黑屏和空帧先从渲染管线查起先说我遇到最多的一个现象Topic里有消息但拿出来的图是一张黑图。黑屏问题的排查思路第一位是检查RenderTexture是否真的接收了渲染。如果Camera的targetTexture没设置或者你在Start里重新new了一个RenderTexture但相机没有引用它就必然黑屏。我当时把组件开起来后画面全黑追踪了半天才发现新增出来的相机组件没有真正enableUnity不会给停用的相机做渲染。还有一种黑屏发生在相机挂到了车辆隐藏层级下面。如果整个车辆模型在某段初始化里被SetActive(false)即使相机本身没关渲染管线也不会工作。那时候你检查相机enable没问题、RT也正确却依然是黑图。解决方式是在车辆模型底下单独保留一个传感器基座节点与车身显示节点分开确保相机的GameObject在任何时候都处于激活状态。4.2 CameraInfo和图像对不上我还踩过一个大坑图像能正常显示但是把CameraInfo里的K矩阵填到下游模型后整个视觉检测全乱了。这是因为我把Unity的垂直fov直接填进去算fx而实际渲染出来的图像是1920x1080的宽屏。垂直FOV是60度水平FOV要比60度大不少算出来的焦距自然偏小。后来我写了一个带宽高比的换算函数并自动化测试了每个相机得出的fx、fy、cx、cy是否跟Unity相机参数完全匹配才解决这个问题。另外图像可能存在上下翻转的问题。Unity纹理的uv原点和ROS图像原点不一致如果不做翻转导出的图像和点云投影会对不上。建议在发布图像前统一把纹理的v轴反转一次。要记住不只RGB图要翻转如果你加了深度相机深度图也要做同样的翻转否则深度和颜色数据互相矛盾。4.3 tf缺失、静态坐标和动态坐标多相机系统最常见的现象是Rviz里图像挂不对位置。图像显示在画面中央但摄像头坐标轴停留在世界原点。原因很简单静态tf没发布。这里推荐用robot_state_publisher或者tf2_ros里的static_transform_publisher把每个相机相对base_link的坐标发布出来。发布后在Rviz里选中相机可视锥应该能看到六个锥体从车体各方向伸出去那才说明坐标关系正确。如果tf发布后图像还是乱飘重点检查欧拉角定义。我使用的坐标系约定是严格按照ROS REP-103的标准x向前、y向左、z向上roll-pitch-yaw顺序。如果你从Unity的Transform直接抓rotation得到的是Unity四元数需要显式转换。我见过直接拿Unity的rotation字段发Rviz结果镜头全跑到地面下面去了。有一个经验多相机系统的坐标约定一定要在项目文档里固化否则每次换人接手都要重新猜一遍。5. 性能优化与后续扩展5.1 多相机的性能瓶颈到底在哪里一路相机和六路相机的区别不只是数量它会让模拟器主循环直接翻车。最大的性能瓶颈有两个一是渲染压力多一路相机就多一个视锥、多一整套绘制流程GPU开销直线上升二是像素读取和传输假设每路相机1920x1080RGB888一张图大概6.2MB6路相机15Hz就是每秒560MB以上的原始数据。如果你还用CPU端的ReadPixels同步读会在主线程上造成一长段卡顿。画质也不是越高越好。仿真里做感知验证尤其是要跑模型推理时图像至少保持720p到1080p之间。后期我把默认渲染质量调低关掉部分阴影和远距离动态物体因为感知算法关注的是图像内容分布不是AAA级光影效果。如果任务只是验证传感器布局和覆盖范围甚至可以临时降成640x480输出等要拿数据训练模型时再拉高分辨率和帧率。5.2 我用过的几种优化手段第一用AsyncGPUReadback代替ReadPixels是我改完收益最明显的一步。GPU渲染完成后你发起异步回读不阻塞主线程等数据准备好了再回调处理。核心代码大致是if (SystemInfo.supportsAsyncGPUReadback) { AsyncGPUReadback.RequestIntoNativeArray(ref nativeArray, rt, 0, OnReadbackComplete); } else { snapshot.ReadPixels(...); }这一改主循环的卡顿明显下降。第二控制发布频率。不要让每个相机的Update在每一帧都发数据而是累计时间到发布间隔再发。第三复用分配的内存。不要在每帧都new Texture2D和byte数组GC会频繁触发导致模拟器周期性卡帧。提前分配好缓冲定时复用。第四根据任务需要实时缩放。比如感知不需要颜色时可以发灰度图网络带宽直接降到三分之一。5.3 扩展深度相机、语义分割相机和事件相机多相机框架一旦搭好后面加新类型相机就变成填配置的事。我在这个基础上顺手加了深度相机和语义分割相机。深度相机可以通过Unity的depth纹理获取深度再转成16bit的深度图语义分割相机可以考虑用Tag或自定义Shader渲染语义ID。这类传感器在仿真闭环测试里尤其有用因为可以很方便地生成带有Ground Truth的多模态训练数据。再往上你甚至可以接事件相机模拟。事件相机不是传统逐帧图像而是输出亮度变化事件流适合用来做高速低延迟感知。多相机框架本身不变只是数据的组织方式和发布Topic不同。从长远看AWSIM这套传感器框架确实适合做自动驾驶仿真验证它开放、可扩展、又跟Autoware打通多相机铺好之后整个感知链路的验证效率能提升不少。最后说点实在的给AWSIM加多相机的这段时间我最深的体会是工程上大多数难点不是复杂而是细节。你差一个坐标系旋转图像就歪差一个纹理翻转深度就对不齐差一个时间同步融合结果就开始抽风。真正把多相机系统做扎实需要你对渲染管线、坐标变换、消息格式有足够的熟悉而这些事没有哪家厂商会手把手教你。如果你正在做类似的事情我的建议是先别急着写代码把相机布局和坐标表画清楚。拿纸画一张自车俯视图标出每一路相机的位置、朝向、视野范围然后再去写配置。这样至少省掉一半同步和标定问题。最后再多说一个细节把每一路的图像都在Rviz的2D视图里打开确认六路图像都不黑不歪之后再接入算法这样你的调试会非常顺畅。多相机有了后面的路还很长。下一个我想做的方向是利用这套多相机训练一个车端视觉模型把仿真里看到的每一帧真正吃透。到时候有了结果再回来更新。