51c自动驾驶~合集28
我自己的原文哦~ https://blog.51cto.com/whaosoft/12030824
#自动驾驶建图的统一矢量先验地图编码
高德地图&西交 | 先验驾驶
论文链接:https://arxiv.org/pdf/2409.05352
写在前面&笔者的个人理解
最近出现了很多先验地图的论文,高德地图和西交的这篇工作PriorDrive也给出了他们的方法。使用车载传感器来在线构建高精地图已经成为一种很有前途的解决方案;然而,由于遮挡和恶劣天气等,这些方法可能会受到数据不完整的阻碍。本文提出了PriorDrive框架,通过利用先验地图来解决这些限制,显著提高了在线高精地图构建的鲁棒性和准确性。PriorDrive集成了各种先验地图,例如OpenStreetMap的SDMap,图商提供的过时高精地图,以及根据历史车辆数据预测的在线局部地图。为了有效地将这些先验信息编码到在线建图模型中,本文引入了一种混合先验表示(HPQuery)来表示所有地图元素。PriorDrive的核心是统一矢量编码器(UVE),采用双重编码机制来处理任意的矢量数据。此外,本文提出了一种片段级和点级的矢量预训练策略,以提高UVE编码器的泛化性。通过对nuScenes数据集的广泛测试,证明PriorDrive与各种在线建图模型高度兼容,并大大提高了地图预测能力。通过PriorDrive框架整合先验地图,为单次感知的挑战提供了强大的解决方案,为更可靠的自动驾驶汽车导航铺平了道路。读完发现这篇文章提出的范式可以即插即用到其他矢量任务,特别是提出的UVE和预训练。
总的来说,本文的贡献如下:
1、本文介绍了一种统一的矢量编码器(UVE),它通过双重编码机制有效地编码各种矢量数据:矢量内编码器捕获精细的局部特征,而矢量间编码器融合全局上下文信息。
2、本文提出了一种向量数据的预训练范式,通过在片段级和点级别添加高斯噪声或掩码来学习向量数据的先验分布,并重建整个矢量地图。
3、本文提出了一个混合先验表示(HPQuery)来表示所有元素,并提出了一个带有矢量先验地图的PriorDrive框架来解决单次感知的局限性。对nuScenes数据集的综合评估表明, PriorDrive显着提高了在线地图模型的性能。
相关工作回顾
在线矢量高精地图构建。与基于SLAM的方法离线构建HDMaps不同,最近的研究直接使用车载传感器实现在线构建HDMaps,以降低成本并提供最新的道路。一些方法将HDMaps构建视为分割任务来预测像素级的光栅化地图,但是需要复杂的后处理以进行矢量化构建。所以后续的工作试图构建一个端到端的矢量化地图学习框架。VectorMapNet探索了关键点表示和从粗到细的两阶段网络。MapTR提出了点集的置换等效建模和类似DETR的一阶段网络。然而这些方法仅依赖于车载传感器的单次感知,在处理具有挑战性的场景(如遮挡或不利的天气)存在局限性。
基于先验地图的在线建图。最近的研究开始结合先验地图来提升模型建图性能。P-MapNet将SDMap编码为额外的条件分支,并利用masked autoencoder来捕捉HDMap的先验分布。NeuralMapPrior维护和更新全局神经地图先验,这种表示会自动更新自身并提高局部地图推理的性能。MapEX设计了三种合理类型的现有地图并改进基于查询的地图估计模型的匹配算法。现有的研究经常忽略了在线历史预测地图,本文将其作为先验地图为当前感知提供所需信息。并且本文直接以矢量的形式对现有的各种先验地图进行了广泛的研究。
基于掩码的预训练方法。在自然语言处理和计算机视觉领域,掩码预训练已经成为自监督表示学习的一种有效策略。BERT使用掩码语言建模来预测具有双向上下文的随机掩码token。Masked AutoEncoder (MAE)会屏蔽掉输入图像的随机patches,并根据未屏蔽的patches进行重建。与以前主要为文本和图像设计的工作不同,本文提出了面向矢量数据的预训练范式。
概述PriorDrive
集成方法
先验地图特征的提取。本文提出的UVE作为一个统一的矢量编码器,可以直接编码各种矢量数据信息,如位置、方向和点的类型(c)。使用表示点,向量通常由多个有序的点集合组成, ,其中n变化。先验地图由多个向量组成,其中m是可变的。用表示UVE模型,对先验地图特征的提取过程,记为,可表示为:
集成方法。本文总共提出了多种方法将先验地图特征整合到在线建图模型中。对于没有可学习查询Q的方法,如HDMapNet,本文直接reshape先验特征,并使用Deconv上采样来匹配BEV特征的形状,然后将其与BEV特性连接,并通过Conv对齐:
对于基于可学习查询Q的方法,如矢量化模型MapTR系列,本文提出了包含add/replace/concat三种操作的HPQuery。具体来说,Q通常由两部分组成, , 和分别表示实例级和点级的可学习查询。先验特征也由实例级特征和点级特征组成,。因此,本文分别在实例级和点级与查询Q交互。add/replace/concat三种操作的HPQuery可以分别表示为:
UVE模型结构
受BERT在文本相关任务上的启发,本文提出了一个统一的矢量编码器(UVE),将矢量点类比为单词,矢量地图元素类比为句子。
Hybrid Prior Embedding. 对于给定的先验地图,本文首先构造UVE的输入,称为混合先验嵌入(Hybrid Prior Embedding,HPE)。本文使用正弦嵌入获得(x, y)和(v_x, v_y)的点位置嵌入和方向嵌入。本文将这两个嵌入拼接为点级嵌入。为了获得整个矢量的特征表示,本文在每个矢量的开头添加一个特殊的[VEC]标记,并使用该标记的嵌入作为实例级嵌入。此外,本文引入了可学习的实例嵌入和类型嵌入来区分矢量实例。为了保证点的顺序,本文还引入了二维可学习位置嵌入。最后,这些嵌入被聚合成HPE,然后作为UVE的输入嵌入。
Intra and Inter-Vector Encoder. 由于矢量点本身的信息量有限,学习矢量实例的概念对模型来说具有挑战性,需要矢量内的点之间进行更多的交互,以增强每个点对同一矢量中其他点的感知能力。因此,UVE编码器使用M层矢量内注意编码器和N层矢量间注意力编码器,使用注意掩蔽机制促进不同矢量实例内部和之间的特征交互。通过这种双重编码机制,UVE可以深度理解和表示矢量数据,为在线高精地图建设等关键任务提供有力支持。
预训练:位置建模
由于在线建图模型的推理能力有限,历史预测地图存在一定的误差。因此,本文使用位置建模对UVE进行预训练,以提高其编码和降噪能力。
Noise & Mask Generator. 噪声主要分为片段级噪声和点级噪声。在点级噪声中,随机噪声被添加到整个地图上5%矢量点。对于段级噪声,在随机选择10\%的地图元素后,将随机噪声添加到子矢量段内的所有矢量点。使用表示原始地图元素,加入高斯噪声的过程可以用以下公式表示:
其中是一个随机高斯噪声,它只被添加到每个点的横纵坐标上。
与添加噪声类似,添加掩码选择也分为点级和片段级。选择要添加掩码的点后,这些点的坐标将被掩码为-1。
Loss Function. 本文将矢量地图经过噪声掩码生成器后送入UVE编码,然后使用MLP对所有矢量点的坐标进行解码,并计算到真实坐标的平均欧几里德距离作为监督:
其中代表中所有的矢量点。
实验分析
主要结果
为了评估本文的方法在不同模型架构、评估指标和矢量先验图中的有效性,本文将uve编码的先验地图集成到HDMapNet和矢量化模型MapTRv2中。SD地图提供的先验信息主要由简单中心线骨架组成,导致了最小的改进,分别提升3.0 mAP和1.9 mIoU。对于基于查询的模型,本文的方法有更大的改进,因为本文提出的HPQuery更好地利用了先验信息。提供最新道路状况的在线局部地图带来了更显著的改进分别提升 4.2 mAP和2.2 mIoU的增强中。这些结果表明局部地图通过提供更全面和最新的信息有助于提高性能。下图说明了在线局部地图有效地恢复遮挡的车道分隔线、人行横道和道路边界。虽然离线高精地图的精度很高,但由于缺乏完整的先验信息,分别提升了3.6 mAP和3.2 mIoU。此外,本文使用更严格的阈值0.5m来评估结果,SD地图、在线局部地图和过时高精地图分别提升了4.1 map、6.1 map和5.7 map。这些结果强调了本文的方法在更严格的评价条件下提供了更实质性的改进。总的来说,本文的方法在所有不同先验建图的基线模型中展示了一致的性能改进,突出了其通用性和对其他建图框架的潜在适用性。
消融实验
结论
在本文中介绍了一个新的框架PriorDrive,它有效地利用各种类型的先验地图来提高自动驾驶汽车在线高精地图构建的准确性和鲁棒性。本文的方法的核心是UVE,本文设计它来有效地编码各种矢量数据。通过综合实验,本文证明了UVE与本文提出的预训练策略相结合,显著提高了最先进的映射模型的性能。本文的方法不仅解决了动态和复杂环境中实时高精地图构建相关的挑战,而且还提供了一个可扩展的解决方案,随着时间的推移不断提高地图精度。迭代使用历史预测地图作为先验,导致逐步细化的地图输出,从而确保最新的道路信息可用于自主导航。
#小鹏也要做增程车了
恶心死了 宗教品牌 又开始了~~ 垃圾车的扯皮之路
首款车明年量产
激烈围斗中,车企们除了拼杀价格、智能化,刺激销量的筹码已然不多。增加车辆能源形态,正成为车企的另一种选择。
36氪长期跟进获悉,小鹏汽车的增程车项目已经明朗,今年上半年已经完成核心零部件定点,首款增程车正在全力开发当中,预计2025年下半年量产。
“不会晚于明年四季度”,消息人士透露,届时小鹏将形成纯电与增程两大车型产品阵列。
据36氪了解,小鹏首款增程车将是一款大型SUV,内部项目代号G01,“是以G9为原型车开发,主要针对20万元以上价格带”,首款车将在小鹏汽车广州黄埔第二工厂量产下线。
36氪从供应链处获悉,小鹏汽车首款增程器车已经定点东安动力。东安动力此前曾为理想汽车提供增程器部件。
对于上述消息,36氪向小鹏汽车求证,官方表示“更多信息将在1024科技日和朋友们分享”。
不只小鹏汽车,今年以来,极氪汽车、阿维塔、智己、埃安等也都相继宣布推出增程车产品,小米汽车也多次被曝出计划推出增程SUV车型。纯电和大电池插电混动(包括增程)两种动力形态并举,逐渐成为车企共识。
国内汽车公司中,几乎只有蔚来还在坚守纯电阵地,这家公司依靠换电等补能设施,今年维持住了月销2万辆的成绩。公司计划继续加码换电等补能服务,预计在2025年底全国建成5000座换电站。
但是在当下资源稀缺、竞争激烈的行业时期,大量车企已经没有资源或时间去高强度建设类似的补能体系。选择增程或者混动,自然成为扩大销量的关键途径。
「小鹏提速造增程」
增程车一直是小鹏汽车官方鲜少谈起的话题。有小鹏汽车员工告诉36氪,内部其实有相关团队一直在调研增程车型方案,“有些调研2年前就开始做了,但可能高层一直没有下定决心。”
但今年以来,小鹏汽车对增程车的态度逐渐明朗。
CEO何小鹏曾数次公开谈到增程车。今年一季度财报电话会议上何小鹏称,客户对混动有真实需求。技术角度上,从增程到纯电的难度要远高于从纯电到增程,当下的增程方案已经遇到体验挑战,小鹏也在思考下一代增程车是什么样的。
加上理想、问界等同行增程车型销量表现一路攀升,极氪、阿维塔等纯电品牌也在陆续推出增程车型。有内部人士表示,综合评估后,小鹏管理层已经达成了共识:“增程产品市场天花板还很高,投入回报率没有问题。”
还有知情人士透露,何小鹏对内部表示,不要过渡性的增程技术方案,对增程产品的技术提出了很高要求,“希望推出时领先行业”。
今年正式定点东安动力增程器后,小鹏加快了增程车的开发节奏。
近日,CEO何小鹏汽车15万元级新车MONA M03的上市庆功宴上称,小鹏未来5个季度都有新车发布。有内部人士透露,5个季度的新车发布中,就包含增程车型。
正常情况下,车企开发一款新能源车型需要18个月-24个月。小鹏的增程车正在加急开发,力争明年底的量产节点。
在产品开发之外,小鹏也在同步筹备增程车型的生产与制造。小鹏汽车广州工厂二期项目环保公示文件显示,该基地每年能生产30万套新能源车车身零部件,其中包含2款纯电车型与1款插电混动车型的产能,3款车零部件产能均为10万套。
该文件显示,小鹏广州工厂二期基地投资总额高达12亿元,整个项目建设周期为18个月。
「增程,撬动销量的另一个支点」
尽管增程路线过往被吐槽为“落后路线”,但其媲美纯电车的驾驶体验、以及无里程焦虑等优点,还是让越来越多用户看到了增程车的长板。
但车企做增程的理由不止于此。
从成本角度看,由于电池厂商低价抢单,电池电芯成本已经跌至 0.4元/Wh以下,但一个60度电的电池包仍需要4万元左右;而一个相对好的增程器成本不过万元左右,加上30多度的电池包,仍然比纯电车有成本优势。这也是中高端车型市场纯电车普遍比增程车贵几万元的原因。
除成本优势外,增程车也朝着大电池、超快充方向发展,解决了过往充电慢等痛点。如宁德时代就推出了专门的“神行超级增混电池”,纯电续航达300公里,并且支持3C快充;亿纬锂能、蜂巢能源等电池厂商也在跟进。“大电池+小增程”的配置正得到车企青睐。
市场方面,用户已经给出了最直接的反馈。今年上半年,国内新能源汽车销量494.4万辆,纯电动车卖出301.9万辆,同比增长11.6%;插电混动车(含增程)卖出192.2万辆,同比增长85.2%。
从全球视角来看,欧美市场对电车的热情也在回退,更趋向于混动车型。而小鹏希望未来10年其海外销量占到销量50%,如果仅依靠纯电车型来达成目标,挑战不小。
“新能源车渗透率提升背后是大量燃油车用户转化,他们直接去买纯电车,还是有不少难度。”一位行业人士向36氪分析,而增程或者大电池混动产品,是一个很好的选择,“相比燃油车,增程车的体验几乎都是升级。”
对小鹏而言,押注单一纯电产品确实存在较高风险。今年上半年小鹏汽车月均销量在1.5万辆以下。
而有增程器或油箱的同行们攫取了更多销量份额。凭借纯电和增程双重布局,华为问界和理想汽车当前每月整体销量都在4万辆以上;号称“小理想”的零跑汽车,今年8月销量已经突破3万辆。
随着增程车型到来,小鹏无疑拿到了另一个撬动销量的支点。“小鹏汽车的使命是汽车AI与智能化的普及,最终实现自动驾驶,这个时候,在动力选择上,就是以扩大销量为目标了。”有小鹏内部人士表示。
加速增程车落地,尽早攫取市场份额与利润,或许是小鹏汽车们当下的核心任务之一。特别奇怪的是这几个垃圾国产品牌ceo都统一的面目可憎
#PPAD
用于端到端自动驾驶的预测与规划迭代交互
PPAD: Iterative Interactions of Prediction and Planning for End-to-end Autonomous Driving,Zhili Chen1† , Maosheng Ye1 , Shuangjie Xu1 , Tongyi Cao2 , and Qifeng Chen1B。文章最大的创新点在于采用了iterative的prediction and planning的网络架构,而不是UniAD或者VAD这类one shot motion planning一次出结果。类似的prediction and planning interleaved 架构的方案还有DTPP, QCNet和GameFormer, 这俩方法的思路也是“recurrently model the trajectory prediction task. Given motion planning is a computational problem that finds a sequence of valid trajectories”。从直觉上讲这种多次交互的过程更符合agents and ego相互影响相互引导的过程。
我对当前learning based的方案在处理以上问题中的理解:
对于prediction and planning任务,他们之间在时序上具有很强的交互性和因果关系,直接卷ego query and motion query做个cross attention就能很好解决prediction and planning问题,那说明learning based 方案确实很强,也说明不依赖人类知识的引导,网络能学到一切。但看来事实并非如此。特别是UniAD, bev query传给所有模块,实现了全面东北乱炖,相比之下VAD至少信息传递上更加清晰,另外Para drive告诉我们这种结构还可以更简单。
另一方面,当前的各个端到端方案非常依赖imitation loss,在Fighting copycat agents in behavioral cloning from observation histories, NIPS 2020这个里面,说到copy cat现象,里面说到当满足以下两个条件时,模仿策略在访问过去的观察结果时会出现“copycat”问题:(i)专家行为随时间的变化具有很强的相关性,以及(ii)过去的专家行为很容易从观察历史中恢复。完美解释了我们当前端到端遇到的问题,第一个我们正在解决的问题具有极强的时序交互性,第二个我们依赖开环检测,ego status让我们学到了捷径,开环检测隐藏了潜在问题。
如果说真的网络能学一切,我提几个问题:
- 如果数据足够多,以及分布足够广泛,质量足够高,是不是在这个前提下,网络确实能学到一切?
- 如果由于prediction and planning交互关系和因果关系非常复杂, 当我们把网络设计的足够复杂的时候是不是能学到该学的东西?如果网络做的这么复杂,decoder真的能decode出来吗?
- 有一个流派diss人类知识的引导在深度学习中会让网络产生偏差, 认为应该让网络自己去学该学的,按照这个思路我们解决任何问题就是所有信息丢给网络啥也不管出一个结果即可?
关于以上个人观点,大家可以充分讨论讨论,文章的思路其实很清晰,就简单记录一下,abstract如下:也用whaosoft开发板商城的设备进行测试~
架构如下图所示:
核心思想就是不要一次性把ego and agents以及环境的信息全部塞给encoder decoder,寄希望于网络全给你学会了。本文的做法是将这个prediction-planning的模块变成互相多次迭代计算的过程。也就是原文中说的“the Prediction-Planning Module interleaves the processes of the agent motion prediction and the ego planning for N times.”
framework
Iterative Interactions of Prediction and Planning
这里的思路就是“ From a more global perspective, the agents at a larger range provide more extensive information on traffic flow, which is essential in long-term trajectory planning”,所以我们一定得看尽量远的agents, 不过由于看的太远可能会让我们失去关键信息,所以这里还通过了一个分级的mask起到信息聚焦的作用。此外可以看到上面公式中agents query中的k说明我们是针对每个模态的agents分别和ego query进行cross attention的。最后把ego query组合到一起:
卷完ego query就轮到map query, 作者强调了uniad, vad对map信息使用过于粗暴:“. They overlook the complexity of the evolving motion dynamic and overrate that the ego can plan precisely in the longer term by a single interaction with the map information.”就是说自车的long term planning是很难通过简单的和map query的一个cross attention学的到的,这很难让网络真的学到地图环境的复杂信息。所以一样的, 我们再次采用刚才卷ego query的方法,分级信息聚焦:
接下来就是卷bev query. 作者先指出了uniad的在使用bev feature上面的问题:“There are several drawbacks in UniAD: 1) occupancy grids consume large memory considering the whole scene's range. 2) UniAD failed to build the explicit interactions with the grid map. Therefore, we propose the BEV interactions that dynamically query the surrounding environment for each possible future step.”第一个occ grid非常占用内存,此外uniad没有显式的构建出map和ego and agents之间的interaction. 本文的做法通过一次卷的方式,每次只去聚焦一类信息,直觉上感觉更好,具体是不是这个意思就得看实验结果了。然后作者这里提到,虽然前面的信息卷到足够多的东西了,但是我们为了获得更细粒度的东西,还是要卷一次bev feature:
上面提到的分级的attention机制,就是在做cross attention的时候只对本车附近的区域位置进行cross attention从而达到信息聚焦的作用:
最后是loss的设计,和VAD几乎一致,关于环境的loss:
关于constraint loss:
以及imitation loss:
total loss是上面的求和,这里还有一个noisy loss, 因为前面还有一个noisy trajectory的设计,训练中引入了noisy trajectory:
这里要强调一点的是,在collision loss中,因为agents是多模态的,所以这个loss考虑了所有agents的带概率的可能模态"considers the potential collision of all the agents' motion modalities instead of only computing the loss on the agents' most confident mode"。这种对uncertainty的处理还是略显简单。问几个简单的问题:
考虑所有可能性,不就只能出一个over conservative的结果了吗?搞深度学习决策规划的,是怎么处理环境,agents的uncertainty,以及如何让plan做到risk aware的?深度学习中的risk and uncertainty awareness是如何建模的?
这个total loss中的参数设计,collision loss并没有占到主导地位,是不是意味着规划出来的轨迹连基本的collision avoidance都做不到?咱先不聊轨迹像不像人,深度学习决策规划是如何保证轨迹至少是安全的?
#ToC3D
速度完爆&性能不输!华科&百度提出,3D检测直接起飞!
自动驾驶环视3D目标检测的推理速度对于量产部署至关重要。尽管许多基于稀疏查询的方法已经尝试提高3D检测器的效率,但它们忽略了对主干网络的优化,尤其是当使用视觉transformer(ViT)来提升性能时。为了解决这一问题,作者探索了通过token压缩技术来构建高效的ViT骨干网络,用于环视3D检测,并提出了一个简单而有效的方法,称为TokenCompression3D(ToC3D)。通过利用历史目标查询作为高质量的前景先验,对它们中的3D运动信息进行建模,并通过注意力机制与图像token进行交互,ToC3D能够有效地确定图像token的信息密度,并区分出明显的前景目标token。引入的动态路由器设计使得ToC3D能够在压缩信息损失的同时,将更多的计算资源倾斜于重要的前景token,从而实现更高效的基于ViT的环视3D检测器。在大规模的nuScenes数据集上的广泛实验结果表明,作者的方法在几乎保持了近期最佳性能的同时,推理速度暴涨30%!并且在大的ViT主干和输入分辨率后,这种改进仍然保持一致。
项目链接:https://github.com/DYZhang09/ToC3D
相关工作总结环视3D目标检测
环视3D目标检测因其低成本和简单的传感器配置,在现实世界的应用中具有显著优势,例如在自动驾驶领域。现有的环视3D目标检测方法主要分为两大类:基于稠密鸟瞰图(BEV)的方法和基于稀疏查询的方法。
稠密BEV方法通过显式的视图变换将图像特征转换为稠密的BEV特征。BEVDet是这类方法的先驱。BEVDepth利用显式的深度监督来提高深度估计的准确性,而SOLOFusion结合了长期和短期的双目视觉信息以改善深度估计,这些方法都显著提升了检测性能。与显式视图变换不同,BEVFormer通过预定义的网格状BEV查询和注意力机制隐式地聚合稠密的BEV特征。PolarFormer探索使用极坐标系统替代传统的网格状坐标系统。由于需要提取稠密的BEV特征,这些方法在计算和内存成本上相对较高。
另一方面,稀疏查询方法通过直接使用稀疏的目标查询与图像特征交互,跳过了稠密BEV特征提取的过程。DETR3D通过将3D查询投影到2D图像平面来聚合特征。PETR将3D坐标的位置信息编码到图像特征中,消除了对3D查询投影的需求。CAPE和3DPPE进一步改善了3D位置信息的质量。SparseBEV在BEV和图像空间中引入了适应性。对于时序3D检测,Sparse4D提出了稀疏4D采样方法,用于从环视/尺度/时间戳聚合特征。StreamPETR引入了内存队列以存储历史目标查询,实现长期时间信息的传播。这些方法通过直接将图像特征传递给3D解码器进行检测,因此高质量的图像特征对于它们是有益的。随着预训练的ViT的发展,基于稀疏查询的环视3D检测方法已经实现了SOTA,并几乎主导了排行榜。然而,作者发现,由于ViT的计算负担,推理速度主要受到主干网络的限制,这激发了作者对ViT主干进行优化以提高效率的想法。
视觉Transformer中的Token压缩
视觉Transformer(ViT)因其强大的特征提取能力,在多种计算机视觉任务中变得流行。训练后的ViT的可视化显示了稀疏的注意力图,这意味着最终预测仅依赖于一小部分显著的token。基于这一观察,许多研究工作尝试通过移除不重要的token来加速ViT,这一过程被称为token压缩。例如,DynamicViT引入了一个轻量级的预测模块来估计每个token的重要性分数,然后逐步动态地剪枝不重要的token。A-ViT进一步提出了动态停止机制。EViT利用类token的注意力来识别token的重要性,然后保留注意力集中的图像token并将不集中的token融合。AdaViT在注意力头和块级别进一步进行剪枝。Si等人联合考虑了token的重要性和多样性。Evo-ViT提出了一种自车激励的慢速-快速token演化方法,保持了空间结构和信息流。所有这些方法最初都是为2D视觉任务设计的,并且在执行token压缩时没有考虑3D感知先验。
本文则借鉴了[11,33,40]中的方法,通过利用历史目标查询和建模3D运动信息,将token压缩从2D领域扩展到3D领域,实现了为3D目标检测量身定制的3D运动感知token压缩。这种方法允许作者进一步将稀疏查询方法的设计哲学从3D解码器扩展到整个处理流程,从而实现更高效的环视3D目标检测。
ToC3D方法详解概述
基于稀疏查询的方法通过主要对稀疏的目标中心查询进行建模,而非对整个3D场景进行建模,从而提高了3D检测器的效率。然而,作者认为,现有的稀疏查询方法在主干网络中对前景和背景进行了同等处理,这限制了效率的进一步提升。使用视觉Transformer(ViT)来实现卓越的性能时,主干网络成为了推理速度的瓶颈。
为了解决这一问题,作者提出了TokenCompression3D(ToC3D),它通过token压缩将稀疏查询方法的设计哲学扩展到ViT主干网络中。如图2(a)所示,ToC3D主要包括两个设计:运动查询引导的token选择策略(MQTS)和动态路由器。每个块中的token压缩过程如下:1)首先,MQTS以图像token和历史目标查询作为输入,通过图像token和历史查询之间的注意力机制计算每个图像token的重要性分数,将图像token划分为显著和冗余的部分。2)然后,使用动态路由器对不同组的token进行高效特征提取。显著的token通过常规路径传递,该路径由多个注意力块组成。对于冗余token,使用自由路径(在本文中使用恒等层)以节省计算成本。为了保持显著和冗余token在注意力块中的交互,作者将冗余token合并为一个桥接token,并将其与显著token一起在常规路径前传递。3)最后,在获得显著和冗余token的特征后,作者重新排列显著和冗余token,以满足典型3D目标检测器的兼容性。
通过堆叠赋能token压缩的block,计算资源被动态且更加密集地分配给前景提议,消除了不必要的消耗,并显著加速了推理。最终作者有效地修剪了ViT主干网络,开发了一个更高效的基于稀疏查询的环视3D检测器,配备了3D稀疏解码器。
运动查询引导的Token选择策略
运动查询引导的Token选择策略(MQTS)旨在测量每个图像token的重要性分数,并将token划分为显著/冗余token。通常,显著和冗余token分别包含有关前景目标和背景信息。基于此,MQTS本质上在图像中分割前景token,而具有3D运动信息的历史目标查询可以作为高质量的前景先验,这导致了运动查询引导的token选择设计,如图2(b)所示。
具体来说,MQTS首先采用历史查询内容 、历史查询参考点 (在齐次坐标中)和当前帧的图像token 作为输入,其中 、 分别是历史查询和图像token的数量,、 分别是历史查询和图像token的通道数。然后,MQTS使用以下过程准确且高效地分割token:
运动查询准备。由于当前帧和历史帧之间存在空间变换,作者引入时间对齐过程以将 与当前自车坐标系统对齐。与[36]一样,作者使用运动感知的层归一化。首先,作者将所有目标视为静止的,并使用自车变换矩阵将 对齐到当前帧:
其中 是与当前自车坐标系统对齐的历史查询参考点, 是从历史帧到当前帧的自车变换矩阵。然后,作者通过条件层归一化建模可移动目标的运动,计算仿射变换系数如下:
其中 是编码的运动向量,、 是用于层归一化的仿射变换系数, 是连接运算符, 是查询的速度, 是历史查询和当前帧之间的时间差。 是位置编码函数,作者采用了NeRF中使用的正弦余弦编码。最后,作者通过条件层归一化编码运动信息:
其中 , 是时间对齐后的历史查询内容和参考点嵌入,MLP(·)是用于将参考点转换为嵌入的多层感知机。
重要性分数计算。在获得时间对齐的历史查询后,作者分割显著的前景token。为了更好地提取前景先验,作者利用注意力机制计算每个图像token的重要性分数。作者首先将时间对齐后的历史查询内容和参考点嵌入相加得到新的查询嵌入,然后使用线性层对齐图像token和查询嵌入的维度:
其中 , 分别是维度对齐的查询嵌入和图像token,, 分别是图像token和历史查询的数量。接下来,作者通过有效的矩阵乘法获得注意力图:
本质上,注意力图 模拟了图像token和历史查询之间的相关性,因此可以表示每个token的前景信息密度,因为历史查询包含前景先验。通过简单的线性变换和sigmoid激活,作者确定每个图像token的重要性分数 :
最后,作者根据重要性分数选择前 个图像token作为显著token,以便更容易地进行批量处理:
其中 是显著token, 是冗余token。, 分别是显著和冗余token的数量。 是预设的常数,保持每个块中显著token的比率。增加 将增加显著token的数量;否则,增加冗余token的数量,从而作者可以通过调整 控制推理速度。
尽管重要性分数计算是轻量级的,但它仍然带来了一些开销。作者发现,更新每个层的重要性分数并不会带来显著的改进,因此作者只更新特定Transformer层之前的重要性分数,中间的层将重用最新的重要性分数。
历史查询采样。尽管历史目标查询携带高质量的前景先验,作者经验性地发现,并非所有历史目标查询都是有价值的。这是因为目标查询的数量大于典型DETR风格检测器中感兴趣目标的数量,许多目标查询并不对应于前景目标,而是背景事物。如果作者直接在MQTS中使用所有这些历史查询,重要性分数计算将会受到背景信息的偏见。由于目标查询的置信度测量,作者可以通过根据它们的置信度分数对历史查询进行采样来简单地解决这个问题。具体来说,作者根据它们在历史帧中预测的置信度分数对历史查询进行排序,并选择前 个查询作为MQTS的输入。
动态路由器
在将token划分为显著和冗余两类之后,作者设计了动态路由器以加快推理速度,同时尽可能地减少信息损失。作者意识到,并非所有的冗余token都与背景相对应,它们中的一部分可能包含了对目标检测有用的信息,而这些信息可以通过注意力机制传递给显著token。为了实现这一目标,作者将所有冗余token合并成一个桥接token,并将其与显著token一起传递,以便在显著和冗余token之间通过桥接token进行信息交互。在这一过程中,作者对显著token使用更多的神经网络层(即常规路径)来提取丰富的语义和几何特征,而对于冗余token,则使用较浅的网络层(本文中使用的是恒等层)来保留其信息。
具体来说,作者利用重要性分数对冗余token进行加权平均,以生成桥接token:
其中, 表示桥接token, 是第 个冗余token的重要性分数, 是对应的冗余token。
之后将桥接token附加到显著token之后,并将它们一起送入常规路径,以便提取更丰富的特征:
在这里, 分别代表经过更新的显著token和桥接token,Blocks指的是Transformer编码块,通常包括多个窗口注意力层和全局注意力层,用于处理环视3D目标检测任务。
对于冗余token,作者通过自由路径(即恒等层)进行处理,并与更新后的桥接token进行合并:
其中, 表示更新后的冗余token, 函数将 重复 次。
最终,作者将更新后的显著token和冗余token重新组合,形成更新后的图像token。得益于动态路由器的设计,作者能够在保持与典型环视3D检测器兼容性的同时,更高效地提炼图像token中的信息。
实验结果和分析 数据集与评估指标
本研究选取了大规模的nuScenes数据集进行方法评估,该数据集涵盖了700个场景用于训练、150个场景用于验证以及另外150个场景用于测试。每个场景的数据由六个摄像头以10Hz的频率捕获,实现了360°的全视场覆盖。在本研究中,作者关注10个类别的标注:轿车、卡车、施工车辆、巴士、拖车、障碍物、摩托车、自行车、行人和交通锥。评估模型性能时,作者采用了nuScenes的官方评估指标,包括nuScenes检测得分(NDS)、平均精度均值(mAP)、平均平移误差(ATE)、平均尺度误差(ASE)、平均方向误差(AOE)、平均速度误差(AVE)和平均属性误差(AAE)。
实现细节
本研究选取了性能优异的StreamPETR作为基准流水线。在主干网络方面,作者采用了ViT-B和ViT-L模型,并在这些模型上实施了token压缩策略。为了指导运动查询引导的token选择策略(MQTS),作者采用了高斯焦点损失,并以投影边界框作为真值。模型训练使用了8块NVIDIA V100显卡,总批量大小设置为16,经过了24个周期的训练。推理速度的测试在单块RTX3090显卡上进行。优化器选择了AdamW。数据增强策略遵循了StreamPETR的官方设置,且未采用CBGS。具体的配置细节参见表1。
主要结果
作者将ToC3D方法与StreamPETR及其他几种流行的环视3D检测方法在nuScenes验证集上进行了比较。主要结果汇总在表2中。结果显示,当以ViT-B作为主干网络时,ToC3D-Fast在保持与StreamPETR相当NDS和mAP的同时,推理速度提升了近20%。若能接受轻微的性能下降(0.5% NDS和mAP),ToC3D-Faster方案可以将推理速度进一步提高30%,整体流水线速度提升26%。值得一提的是,ToC3D-Faster在性能上与StreamPETR相当,但其推理速度却与使用R50主干网络的SOLOFusion相当,显示出作者方法的有效性。在采用ViT-L作为主干网络时,ToC3D-Fast在几乎无损性能的情况下,将整体流水线推理速度提升了36ms。此外,ToC3D-Faster在性能损失不超过0.9%的情况下,实现了25%的推理速度提升,与Sparse4D的速度相当,同时在性能上保持了显著的优势(即NDS提升了超过6.4%,mAP提升了超过7.7%)。进一步地,当输入图像分辨率提升至800×1600时,ToC3D-Fast和ToC3D-Faster配置下,推理时间分别减少了258ms和431ms,显著节省了计算资源。这些结果证明了作者方法在效率提升上的优势。
分析
本研究使用ViT-L作为主干网络,对提出的方法进行了深入分析。所有模型仅训练了12个周期,并在验证集上进行了评估。
为了证明运动查询引导的token选择策略(MQTS)的有效性,作者将其与典型的2D token压缩方法DynamicViT和SparseDETR进行了比较。为了公平比较,作者将MQTS替换为这两种方法,保持动态路由不变,并调整这些方法以达到最佳性能。作者还与随机token压缩进行了比较。
如表3所示,随机token压缩导致了显著的性能下降,特别是当保持比例较低时。这表明随机压缩无法有效捕捉图像token的重要性,从而导致大量有用信息的丢失。相比之下,基于分数的2D token压缩方法由于能更好地识别重要的前景token,因此在性能下降上要小得多。然而,这些2D方法仅将图像token作为输入,在没有3D感知特征或先验的情况下进行token压缩,导致性能严重受损(即mAP和NDS分别下降了约2%)。
作者的方法由于MQTS输入了历史目标查询,能够模拟目标的3D运动信息,并聚合高质量的3D前景先验,从而显著优于2D方法(mAP和NDS分别提高了超过2%)。值得注意的是,借助高效的MQTS,作者的方法能够在与2D token压缩方法相当的推理速度水平上几乎保持基本流水线的性能,显示了MQTS的优越性。
组件有效性分析。在验证了历史预测中的目标查询可以作为高质量的前景先验这一关键见解之后,作者进一步研究了是什么使得这一见解有效。作者将更快设置下作者的方法作为基线,并逐个移除一个组件以测量其有效性,如表4所示。值得注意的是,移除任何组件只会略微减少推理时间,显示了每个组件的高效率。
对于设置(a),作者用轻量级模块替换了等式5中的注意力机制,这导致了0.8% mAP和0.4% NDS的下降。这表明注意力图自然模拟了图像token和历史查询之间的相关性,从而更明确地表示了每个token的前景信息密度,实现了更好的重要性度量。
对于设置(b),作者丢弃了等式1至3中的过程。性能下降(1.1% mAP和0.6% NDS)表明了运动信息的重要性,它适应性地处理可移动目标,并抑制了历史目标与当前坐标系统之间的错位带来的噪声。对于设置(c),作者移除了历史查询采样,并使用所有历史查询。这一选择降低了mAP 0.8%和NDS 0.4%,显示了历史查询采样的必要性,因为它去除了对应于背景事物的目标查询,并防止了重要性分数计算的偏见。对于设置(d),作者没有在动态路由器中使用桥接token。由于缺少显著和冗余token之间的交互,冗余token中包含的潜在信息不能传递给前景token,导致信息丢失,并最终反映在mAP和NDS上。
历史查询数量 的影响。由于作者将 个历史查询作为MQTS的输入,作者在本节研究了不同 的影响,如表5所示。当 时,作者使用了所有历史查询。否则,作者根据历史预测中的置信度分数采样顶部 个历史查询。结果表明,采样历史查询总是有益的,因为许多历史查询对应于背景事物。历史查询采样有助于防止重要性分数受到背景的偏见,从而提高了性能。然而,如果 太小,它将丢失许多前景查询,遭受信息丢失,并损害性能。作者经验性地发现 可以获得更好的性能。
保持比例 的影响。保持比例 控制显著token的数量,并决定了准确性和速度之间的权衡。在本节中,作者进行了实验,以了解保持比例如何影响作者方法的效率。从表6中,作者可以得到以下现象:(1)作者可以通过几乎不损失性能的情况下将流水线加速近15%,并且以边际NDS损失(0.9%)加速25%。(2)在一定范围内,性能下降几乎与推理时间下降成线性关系(大约每20ms推理时间下降0.2% NDS)。(3)太小的保持比例(例如,0.3, 0.2, 0.1)将导致显著的性能下降。这是因为前景token通常占图像token的10%以上,而太小的保持比例不可避免地丢弃了前景token,导致信息丢失。考虑到实际应用的需求,作者将模型的保持比例设置为0.7, 0.5, 0.5作为ToC3D-Fast版本,以及0.5, 0.4, 0.3作为ToC3D-Faster版本,因为这些模型具有相对较好的权衡。
泛化性分析。作者选择StreamPETR作为作者的基础流水线,但这并不意味着作者方法的应用受到限制。事实上,ToC3D可以作为一个即插即用的方法,作者通过将其应用于另一个强大的流水线Sparse4Dv2来展示作者方法的泛化能力。结果如表7所示。它表明作者方法的行为在不同的基线方法中是一致的。与在StreamPETR上获得的加速比相同,作者的方法将性能损失保持在0.8%以内,并且令人惊讶地保持了与Sparse4Dv2完全相同的NDS,证明了作者方法在其他流水线上的可行性和有效性。
定性结果
为了更好地理解MQTS的行为,作者在图3中可视化了注意力图和显著/冗余token。注意力图清楚地表明,作者的方法能够精确地关注前景目标,无论是大尺寸目标(例如样本(a)中的汽车和卡车)还是小尺寸目标(例如样本(b)和(c)中的行人)。这是对作者第3.2节中声明的直观证明,即注意力图模拟了图像token和历史查询之间的相关性,因此可以表示每个token的前景信息密度,因为历史查询包含前景先验。借助3D前景目标感知的注意力图,当保持比例 降低时,整个模型可以更加集中于前景token,从而提高效率。
局限性讨论
作者的方法利用历史目标查询作为高质量的前景先验,这要求输入数据必须是连续的时间图像序列。虽然这一要求在一定程度上限制了方法的适用范围,但在自动驾驶等实际应用中,感知系统通常是连续运行的,因此这一假设是合理的。该方法的另一个限制是,如果需要调整保持比例 ,则必须重新训练 token 压缩模型。为了克服这一限制,未来的工作可以探索使用动态保持比例,并在训练过程中采用一些技术手段来确保模型的稳定性。
结论
在本研究中,作者提出了一个观点,即在当前的基于稀疏查询的环视3D检测器中,直接采用视觉变换器(ViT)会导致不必要的计算负担,并且显著地减慢了检测速度。为了克服这一问题,作者提出了一种新颖的方法,名为TokenCompression3D(ToC3D)。该方法通过利用历史目标查询作为高质量的前景先验,对这些查询中的3D运动信息进行建模,并通过注意力机制将更多的计算资源导向重要的前景token,同时尽量减少信息的损失。通过这种方式,作者将稀疏查询方法的设计哲学从3D解码器扩展到了整个处理流程中。在大规模的nuScenes数据集上进行的实验结果表明,作者的方法能够在几乎不损失性能的前提下,显著提升推理速度,并且利用历史目标查询可以带来更好的结果。作者期望本文的工作能够激励未来高效环视3D检测器的研究,并为该领域提供一个强有力的基准。
#RenderWorld
爆拉OccWorld!提升纯视觉端到端上限,4D Occ和运动规划最新SOTA!
纯视觉端到端自动驾驶不仅比激光雷达与视觉融合的方法更具成本效益,而且比传统方法更可靠。为了实现经济且鲁棒的纯视觉自动驾驶系统,这里提出了RenderWorld,一个仅基于视觉的端到端自动驾驶框架,它使用自监督的高斯-based Img2Occ模块生成3D占用标签,然后通过AM-VAE对标签进行编码,并利用世界模型进行预测和规划。RenderWorld采用高斯溅射(Gaussian Splatting)来表示3D场景,并渲染2D图像,与基于NeRF的方法相比,这大大提高了分割精度并降低了GPU内存消耗。通过将AM-VAE应用于分别编码空气和非空气部分,RenderWorld实现了更细粒度的场景元素表示,从而在基于自回归世界模型的4D占用预测和运动规划方面达到了最先进的性能。
行业背景介绍
随着自动驾驶技术的广泛应用,研究人员逐渐将重点放在了更好的感知和预测方法上,这些方法与系统的决策能力和鲁棒性密切相关。目前大多数框架都是将感知、预测和规划分开进行的。最常用的感知方法是使用视觉和激光雷达融合进行3D目标检测,这使得模型能够更好地预测未来场景并进行运动规划。然而,由于大多数3D目标检测方法无法获得环境中的细粒度信息,它们在后续模型中的规划中表现出非鲁棒性,这影响了系统的安全性。当前的感知方法主要依赖于激光雷达和camera,但激光雷达的高成本和多模态融合的计算需求对自动驾驶系统的实时性能和鲁棒性提出了挑战。
这里介绍了RenderWorld,这是一个用于预测和运动规划的自动驾驶框架,它基于高斯-based Img2Occ模块生成的3D占用标签进行训练。RenderWorld提出了一个自监督的gaussian splatting Img2Occ模块,该模块通过训练2D多视图深度图和语义图像来生成世界模型所需的3D占用标签。为了使世界模型更好地理解由3D占用表示的场景,在向量量化变分自编码器(VQ-VAE)的基础上提出了空气掩码变分自编码器(AM-VAE)。这通过增强场景表示的粒度来提高了我们世界模型的推理能力。
为了验证RenderWorld的有效性和可靠性,分别在NuScenes数据集上对3D占用生成和运动规划进行了评估。综上所述,主要贡献如下:
1)提出了RenderWorld,这是一个纯2D自动驾驶框架,它使用tokens 的2D图像来训练基于高斯的占用预测模块(Img2Occ),以生成世界模型所需的3D标签。2)为了提高空间表示能力,引入了AM-VAE,它通过分别编码空气体素和非空气体素来改进世界模型中的预测和规划,同时减少内存消耗。
相关工作介绍
3D占用率正在成为激光雷达感知的一种可行替代方案。大多数先前的工作都利用3D占用率真实值进行监督,但这在标注上是一个挑战。随着神经辐射场(NeRF)的广泛采用,一些方法试图使用2D深度和语义标签进行训练。然而,使用连续的隐式神经场来预测占用概率和语义信息往往会导致高昂的内存成本。最近,GaussianFormer利用稀疏高斯点作为减少GPU消耗的手段来描述3D场景,而GaussianOcc则利用一个6D姿态网络来消除对真实姿态的依赖,但两者都存在整体分割精度大幅下降的问题。在提出的工作中,采用了一种基于锚点的高斯初始化方法来对体素特征进行高斯化,并使用更密集的高斯点来表示3D场景,从而在避免NeRF基方法中光线采样导致的过度内存消耗的同时,实现了更高的分割精度。
世界模型常用于未来帧预测并辅助机器人做出决策。随着端到端自动驾驶的逐渐发展,世界模型也被应用于预测未来场景和制定决策。与传统自动驾驶方法不同,世界模型方法集成了感知、预测和决策制定。许多当前的方法将相机-激光雷达数据进行融合,并将其输入到世界模型中,用于预测和制定运动规划。其中,OccWorld提出利用3D占用率作为世界模型的输入。然而,OccWorld在利用纯2D输入方面效率较低,且在编码过程中由于信息丢失而难以准确预测未来场景。因此,我们设计了一个Img2Occ模块,将2D标签转换为3D占用标签,以增强世界建模能力。
RenderWorld方法介绍
本节中将描述RenderWorld的总体实现。首先,提出了一个Img2Occ模块,用于占用率预测和3D占用标签的生成。随后,介绍了一个基于空气mask变分自编码器(AM-VAE)的模块,以优化占用率表示并增强数据压缩效率。最后,详细阐述了如何集成世界模型以准确预测4D场景演变。
1)使用多帧2D标签进行3D占用率预测
为了实现3D语义占用率预测和未来3D占用标签的生成,这里设计了一个Img2Occ模块,如图2所示。使用来自多个相机的图像作为输入,首先使用预训练的BEVStereo4D主干和Swin Transformer提取2D图像特征。然后,利用已知的固有参数(i=1到N)和外参,将这些2D信息插值到3D空间中以生成体积特征。为了将3D占用体素投影到多相机语义图上,这里应用了高斯splatting,一种先进的实时渲染pipeline。
在每个体素的中心以可学习的尺度初始化锚点,以近似场景占用率。每个锚点的属性是根据相机与锚点之间的相对距离和观察方向来确定的。然后,这个锚点集被用来初始化一个带有语义标签的高斯集。每个高斯点x在世界空间中由一个完整的3D协方差矩阵Σ和其中心位置µ表示,并且每个点的颜色由该点的语义标签决定。
直接优化Σ可能会导致不可行的矩阵,因为它必须是正半定的。为了确保Σ的有效性,我们将其分解为缩放矩阵S和旋转矩阵R,以表征3D高斯椭球体的几何形状:
然后,通过计算相机空间协方差矩阵Σ',将3D高斯体投影到2D以进行渲染:
其中J是投影变换的仿射近似的雅可比矩阵,W是视图变换。然后,可以通过对排序后的高斯体应用alpha混合来计算每个像素的语义/深度:
为了计算真实深度与渲染深度之间的差异,利用皮尔逊相关系数,它可以测量两个2D深度图之间的分布差异,遵循以下函数:
最后,我们构建了损失函数,其中包括用于监督语义分割的交叉熵损失和用于深度监督的,整体损失可以计算如下:
2)空气mask变分自编码器(AM-VAE)
传统的变分自编码器(VAEs)无法对非空气体素的独特特征进行编码,这阻碍了模型以细粒度级别表示场景元素。为了解决这个问题,这里引入了空气掩码变分自编码器(AM-VAE),这是一种新颖的VAE,它涉及训练两个独立的向量量化变分自编码器(VQVAE),以分别编码和解码空气和非空气占用体素。假设o代表输入占用表示,而和分别代表空气和非空气体素。
首先利用一个3D卷积神经网络对占用数据进行编码,输出是一个连续的潜在空间表示,记为f。编码器qϕ(s|o)将输入f映射到潜在空间s。然后使用两个潜在变量和来分别表示空气和非空气体素:
每个编码后的潜在变量或使用可学习的码本或来获得离散tokens ,然后在输入到解码器之前,用与该tokens 最相似的codebook替换它。这个过程可以表示为:
然后,解码器pθ(o|s)从量化的潜在变量和中重建输入占用:
为了促进占用表示中空气和非空气元素的分离,用M表示非空气类别的集合。然后,在修改后的占用中,空气和非空气的指示函数可以定义如下:
修改后的空气占用和非空气占用由以下等式给出:
然后,为训练AM-VAE构建了损失函数,它包含重建损失和commitment损失LReg:
AM-VAE在统一的编码器-解码器设置中,为空气和非空气体素分别使用了不同的码本。这种方法有效地捕获了每种体素类型的独特特征,从而提高了重建准确性和泛化潜力。
3)世界模型
通过在自动驾驶中应用世界模型,将3D场景编码为高级tokens ,我们的框架可以有效地捕获环境的复杂性,从而实现对未来场景和车辆决策的准确自回归预测。
受OccWorld的启发,使用3D占用率来表示场景,并采用自监督的分词器来推导高级场景tokens T,并通过聚合车辆tokens z0来编码车辆的空间位置。世界模型w是根据当前时间戳T和历史帧数t来定义的,然后使用以下公式建立预测:
同时,采用了一种时间生成式Transformer架构来有效预测未来场景。它首先通过空间聚合和下采样处理场景tokens ,然后生成一个分层的tokens 集合{T0, · · · , TK}。为了在不同空间尺度上预测未来,采用多个子世界模型w = {w0, · · · , wK}来实现,并且每个子模型wi使用以下公式对每个位置j的tokens 应用时间注意力:
在预测模块中,首先利用自监督的分词器e将3D场景转换为高级场景tokens T,并使用车辆tokens z0来编码车辆的空间位置。在预测了未来的场景tokens后,应用一个场景解码器d来解码预测的3D占用率,并学习一个车辆解码器,用于生成相对于当前帧的车辆位移。预测模块通过生成未来车辆位移和场景变化的连续预测,为自动驾驶系统的轨迹优化提供决策支持,确保安全和自适应的路径规划。
这里实现了一个两阶段训练策略来有效地训练预测模块。在第一阶段,使用3D占用率损失来训练场景分词器e和解码器d:
然后,使用学习到的场景分词器e来获取所有帧的场景tokens z,对于车辆tokens ,同时学习车辆解码器,并在预测的位移和真实位移p上应用L2损失。第二阶段的总体损失可以表示为:
实验对比
#智能驾驶全链路加速剂,让远程控制大展身手
还是一个国产破电池车的 吹b之路~~~
新能源汽车产业上下游涉及诸多环节,包括动力电池、智能座舱、整车研发组装、自动驾驶、场景应用解决方案、配套设备及市场运营、后期的充电适配和电池回收等等...其中任何一个环节实现降本增效,都能够帮助整个产业产业向前发展。
如今,远程控制作为一种在IT互联网等领域已经广泛应用的成熟技术,在新能源汽车行业也能够发挥重要作用,赋能全产业链,帮助新能源汽车产业链上的企业提升竞争力,今天我们就来具体了解一下。
远程控制解决方案
赋能新能源汽车产业的哪些环节?
新能源汽车领域,上下游企业普遍存在广泛的远程控制需求,我们简单盘点其中几个主流的远程需求:
● 车辆&自动驾驶方案产研阶段的远程调试与监控
在车辆的产研测试,以及自动驾驶方案的研发阶段,会存在大量的多路仿真测试,甚至是户外实地测试。
在这一过程中,工程师需要频繁的访问车机或者车载产研设备,进行实时的调试与监控,确保相关工作顺利开展。
此类远程需求面临的主要痛点主要包括:
● 提升远程控制的稳定性与速度
● 对于不同车机系统的兼容性
● 在车载状态下,远控网络如何接入
● 充换电基础设施的运维与技术支持
充电桩与换电站是新能源汽车产业的重要周边配套,地位相当于传统能源汽车的加油站;目前,此类周边配套基础设施主要还是由车企主导建设,并提供运维和技术支持服务。
该场景的远程需求与连锁门店、加油站等场景类似,需要企业对分布广泛的一线门店智能设备提供服务.
其主要痛点包括:
● 对设备的管理与监控
● 运维支持体系的搭建
● 如何在运维环节实现降本增效,进而使得整个服务模式变得可持续
● 上下游汽配厂商产线运维
新能源汽车领域属于广义制造业,上下游涉及的产业众多,其中就包括与车企合作的相关汽配厂商。
这类厂商也需要引入远程控制技术,对其产线设备实施运维管理,实现降本增效,提升其在整个产业链中的话语权和竞争力。
这类远控场景的主要痛点包括:
● 较高的安全合规门槛
● 高级技术人才资源的稀缺性
● 运维时效性与成本如何平衡等问题
● 产线设备处于内网,常规远程手段无法接入
● 车企多分支机构异地协同办公与内部IT运维
造车是件大事,客观地说中小企业是很难单独玩转这个领域的,事实上,国内新能源车企也多为有一定规模的企业;同时又由于车企的业务往往遍布全球,因此拥有多个业务分支机构是非常普遍的现象。
正因如此,车企也同样拥有大型企业多分支机构异地协同办公与内部IT运维需求,这类需求最好的承接方式之一,就是远程控制。
应对此类远程需求,企业面对的痛点往往包括:
● IT设备如何授权管理
● 企业信息安全如何保障等等
● 跨境远程控制的质量如何保证
● 不同岗位如何设置对应的远控权限
● 远程方案与原有企业内部管理体系的对接与融合
贝锐向日葵远程控制
新能源行业场景案例解析
作为国民级远程控制品牌,向日葵新能源汽车领域已经有了诸多落地方案,覆盖行业上下游的诸多应用场景。
场景案例一
某造车“新势力”整车厂商
核心场景:企业内部复合型远程需求,全球远程控制。
该企业处于高速发展期,结合当前企业内部技术支持、远程设计、路试车远程调试等需求,希望能够打造以远程运维为核心的稳定的远程平台。
为此,该企业引入“向日葵企业+”服务,搭建依托公有云的远程服务体系,即开即用,部署速度快,且与个人版向日葵天然隔离。
同时,在欧洲和亚太独立部署向日葵转发服务器辐射海外远程需求。
引入向日葵方案后,该企业主要获得了以下收益:
● 大幅降低原有工程师随车调试的频率,提高检验、测试的效率。
● 服务分区管理,管理权限只开放至国内,国内数据不出海,保障企业安全。
● 欧洲&亚太部署转发服务器,提升海外远程效果,助力企业国际化发展。
● 支持免安装、SOS多版本被控端配置,满足多种办公场景下的远程使用。
场景案例二
车联网解决方案供应商
核心场景:自动驾驶远程研发调试。
该企业作为国内一线车联网解决方案供应商,各地均有分公司。开发和测试人员分布在北京,上海,南京,杭州,深圳等地。
在自动驾驶方案的研发阶段,该企业需要多地的开发和测试人员日常能同时满足多人远程同一台设备的场景,远程时间调试时间较多,不出现卡顿或掉线的问题,且支持快速传输文件。
为此,该企业引入“向日葵企业+”方案,依托公有云的远程控制保障开发、测试人员采用独享专线开展工作。
与此同时,由于车载系统为Ubuntu系统,向日葵为其适配了专用的linux版本,实现跨设备跨兼容访问。
在远控性能方面,向日葵方案为该企业提供了单独线路,同时结合包括向日葵BBR拥塞控制算法在内的一系列自研算法和策略,进一步优化远控体验,大幅提升了车辆测试过程中弱网环境下的远控体验。
引入向日葵方案后,该企业主要获得了以下收益:
● 解决了多终端、多系统相互远程访问问题,极大减少了差旅成本。
● 保障了多人访问不卡顿、延迟等访问难题。
● 远控管理自主可控,确保每次访问记录有迹可循,保障企业数据安全。
● 账号分配统一下发,不同人员仅允许访问指定终端,提供了便捷的设备管理。
场景案例三
某汽车配件供应商
核心场景:汽配产线终端系统运维。
对于制造业企业来说,数字化产线无论引入何种服务,安全都是大前提。
为此,产线端设备的网络隔离要求非常严格,无法与公网直连。远程运维调试方案也多会选择直接部署在内网环境的私有化方案。
为了保证产线设备运维方案的安全合规,该企业选择向日葵私有化部署方案,将远程控制体系搭建在产线内网环境。
运维人员通过访问堡垒机,再通过堡垒机权限发起远程控制,安全的连接至内网产线端设备。
在向日葵方案的加持下,该企业的主要收益包括:
● 大幅降低现场运维频次比例50%以上,节省了大量人工成本。
● 集中管控各个工厂高敏感设备,科技手段赋能工厂,加速工厂信息化进程。
● 系统采用自主研发协议,以最小带宽达到最流畅的远程操作体验,节省资源。
● 有效解决高级技术人员前往现场定位排查费时费精力、效率低的问题。
除了上述案例,贝锐向日葵还成功助力了新能源汽车产业的更多企业,嫩狗广泛覆盖各行业的远程需求,满足了绝大多数企业的多样化需求。
#OPUS
南开&卡尔动力 : 抛弃笨重OCC!速度提升明显,性能SOTA!
原标题:OPUS: Occupancy Prediction Using a Sparse Set
论文链接:https://arxiv.org/pdf/2409.09350
代码链接:https://github.com/jbwang1997/OPUS
作者单位:南开大学 NKIARI 上海交通大学 卡尔动力
论文思路:
占用预测旨在预测体素化的三维环境中的占用状态,正在自动驾驶领域迅速获得关注。主流的占用预测方法首先将三维环境离散化为体素,然后在这些密集网格上进行分类。然而,样本数据的检查显示,绝大多数体素是未被占用的。在这些空体素上进行分类需要次优的计算资源分配,而减少这些空体素则需要复杂的算法设计。为此,本文提出了一种针对占用预测任务的新颖视角:将其表述为一种简化的集合预测范式,而无需显式的空间建模或复杂的稀疏化过程。本文提出的框架称为OPUS,利用transformer编码器-解码器架构,通过一组可学习的查询同时预测占用位置和类别。首先,本文采用Chamfer距离损失将集合到集合的比较问题扩展到前所未有的规模,使得端到端训练此类模型成为现实。随后,基于学习到的位置,语义类别通过最近邻搜索自适应地分配。此外,OPUS结合了一系列非平凡策略来提升模型性能,包括粗细结合学习、一致的点采样、自适应重加权等。最终,与当前最先进的方法相比,本文最轻量的模型在Occ3D-nuScenes数据集上以接近2倍的FPS实现了更优的RayIoU,而本文最重的模型则在RayIoU上超越了之前的最佳结果6.1个百分点。
主要贡献:
据本文所知,这是首次将占用预测视为直接的集合预测问题,从而促进了稀疏框架的端到端训练。
本文进一步引入了几种非平凡策略,包括粗细结合学习、一致的点采样和自适应重加权,以提升OPUS的性能。
在Occ3D-nuScenes数据集上进行的大量实验表明,OPUS在RayIoU结果上可以超越最先进的方法,同时保持实时推理速度。
论文设计:
与成熟的框表示方法[7, 22, 19, 35, 28, 44]相比,基于体素的占用预测[15, 33, 9, 34]能够为周围场景提供更细致的几何和语义信息。例如,使用边界框来描述车门打开的车辆或支腿展开的起重机并不直观,而占用预测可以自然地描述这些不常见的形状。因此,占用预测在自动驾驶领域迅速获得了关注。
近期的研究方法[3, 42, 8, 26, 15]主要依赖于密集数据表示,特征点与物理体素之间存在直接的一对一对应关系。然而,本文注意到,绝大多数物理体素是空的。例如,在SemanticKITTI [1]中,约67%的体素是空的,而在Occ3D-nuScenes [34]中,这一比例超过了90%。这种占用数据的稀疏特性使得直接的密集表示无疑是低效的,因为大部分计算资源都分配到了空体素上。为缓解这种低效性,已经探索了替代的稀疏潜在表示方法,例如三视图表示[33, 8]或减少解空间[20, 9],从而显著减少了计算成本。然而,这些方法仍然将占用预测视为特定位置的分类问题,需要复杂的中间设计和显式的三维空间建模。
在本研究中,本文将任务表述为一个直接的集合预测问题,通过并行回归占用位置和分类相应的语义标签。本文提出的框架称为OPUS,利用transformer编码器-解码器架构,特点如下:(1) 一个图像编码器从多视图图像中提取二维特征;(2) 一组可学习的查询,用于预测占用位置和语义类别;(3) 一个稀疏解码器,用相关的图像特征更新查询特征。本文的OPUS消除了对显式空间建模或复杂稀疏化过程的需求,提供了一种简化且优雅的端到端解决方案。然而,一个关键挑战在于将预测结果与真实值匹配,特别是考虑到预测结果的无序性质。本文认为,尽管Hungarian算法 [11] 在DETR系列 [4, 43, 27, 21, 12, 38] 中被广泛采用,但并不适用于这个任务。Hungarian算法的时间复杂度为O(n³),空间复杂度为O(n²),无法处理大量的体素。在本文的实验中,将两个各含10K点的集合进行关联时,Hungarian算法在一块80G A100 GPU上消耗了大约24秒和2,304MB的GPU内存。而在实际情况中,体素数量在Occ3D-nuScenes [34] 数据集中可以达到约70K。因此,直接应用Hungarian算法进行集合到集合的匹配在占用预测任务中是不可行的。
但是,占用预测任务是否真的需要精确的一对一匹配呢?本文认识到,预测结果与真实标注之间的一对一对应的目的是获得监督信号,具体来说是完整、精确的点位置和准确的点类别。如果本文能够从其他地方获得这些监督信号,那么就可以完全避免一对一匹配的繁重工作。因此,本文提出将占用预测任务解耦为两个并行的子任务,如图1所示。第一个任务通过将预测点分布与真实值对齐来获得点位置的监督,这可以通过Chamfer距离损失来实现,Chamfer距离损失是一种在点云处理中已经成熟的技术[5, 29]。第二个任务通过为预测点分配语义标签来获得点类别的监督。这是通过将每个点分配其在真实值中的最近邻的类别来完成的。值得注意的是,所有涉及的操作都可以并行执行,并且在GPU设备上非常高效。因此,在Occ3D-nuScenes数据集中,单次匹配可以在毫秒内处理完毕,且内存消耗可以忽略不计。本文的方案具有O(n²)的时间复杂度和O(n)的空间复杂度,为大规模训练占用预测模型开辟了新天地。
此外,本文提出了几种策略来进一步提升本文端到端稀疏框架中占用预测的性能,包括粗细结合学习、一致的点采样和自适应损失重加权。在Occ3D-nuScenes数据集上,本文的所有模型变体都轻松超越了所有先前的工作,验证了所提方法的有效性和效率。特别地,本文最轻量的模型在RayIoU上相比SparseOcc [20] 提升了3.3个百分点,同时运行速度超过了2倍。最重配置的模型最终实现了41.2的RayIoU,建立了新的上限,具有14%的优势。
图1:将占用预测视为一个集合预测问题。对于每个场景,本文预测一组点位置 和相应的语义类别 。在拥有真实占用体素位置集合 和类别集合 的情况下,本文将集合到集合的匹配任务解耦为两个独立的部分:(a) 使用Chamfer距离强制 和 的点分布相似。(b) 将预测的类别 与真实类别 对齐,其中 基于最近的真实点为点
图2:OPUS利用transformer编码器-解码器架构,包括:(1) 一个图像编码器,用于从多视图图像中提取二维特征。(2) 一系列解码器,通过一致的点采样模块与图像特征相关联,以细化查询。(3) 一组可学习的查询,用于预测占用点的位置和类别。每个查询遵循粗到细的规则,逐步增加预测点的数量。最终,整个模型通过本文自适应重加权的集合到集合损失进行端到端训练。
实验结果:
图3:占用预测的可视化结果。
图4:粗细预测的可视化结果。
图5:单个查询中点的标准差分布。
图6:不同查询生成的点的可视化结果。
总结:
本文通过将其表述为一个直接的集合预测问题,提出了对占用预测的新颖视角。利用transformer编码器-解码器架构,所提出的OPUS通过一组可学习的查询并行地直接预测占用位置和类别。预测结果与真实值之间的匹配通过两个高效的并行任务完成,从而在该应用中实现了大量点的端到端训练。此外,通过一系列非平凡设计(如粗细结合学习、一致的点采样和损失重加权)增强了查询特征,从而提升了预测性能。本文的实验在Occ3D-nuScenes基准上表明,得益于框架中的稀疏设计,OPUS在准确性和效率方面均超越了所有现有的技术。
然而,提出的OPUS也带来了新的挑战,特别是在收敛速度方面。收敛缓慢的问题可能可以通过借鉴DETR后续工作的经验来缓解,这些工作在很大程度上解决了原始DETR的收敛问题。另一个挑战是,虽然稀疏方法通常在RayIoU上比密集方法表现更好,但它们在mIoU指标上往往表现较差。如何在保持优越的RayIoU结果的同时提高mIoU性能,是未来研究的一个有前途的方向。此外,尽管本文在仅基于视觉的数据集上进行了实验,但本文的核心方法直接适用于多模态任务。本文将多模态占用预测作为未来的研究工作。
#2024年底将至,各家端到端进展如何?
说起端到端,每个从业者可能都觉得会是下一代自动驾驶量产方案绕不开的点!特斯拉率先吹响了方案更新的号角,无论是完全端到端,还是专注于planner的模型,各家公司基本都投入较大人力去研发,小鹏、蔚来、理想、华为都对外展示了其端到端自动驾驶方案,效果着实不错,非常有研究价值。
为什么需要端到端?
首先我们聊一下当前的主流自动驾驶方案,主要核心部分包括:感知模块、预测模块、规控模块。每个模块相对独立,感知模块给预测模块提供动静态障碍物信息;预测模块为规控模块提供规划的参考,规划再转换为控制指令。从传感器端到控制端,需要多个功能支持,这就不可避免导致了累积误差,一旦碰到问题,需要整个pipeline做分析。而且每个模块的优化,并不能保证整个系统达成最优解。
这个时候,就希望有一种模型能够完成感知信息的无损传递,即从传感器端到输出控制策略端,这也是端到端自动驾驶提出的原因。传统定义上感知和规划模块的对接一般是通过白名单(比如机动车、行人、甚至occ输出的非通用几何障碍物)的检测与预测来完成,是人为定义的规则和抽象。随着产品的迭代,每一次都需要添加各类case,设计各种博弈的策略,从模型训练到工程部署再到逻辑设计,时间和人力成本高昂。
而且这种方式无法罗列所有情况,那么是否可以通过对整个场景的学习抽象,无损的将所有信息传递给PnC部分?这就是我们期望的端到端。端到端核心是优化最终目标且全局可导,作为一个完整的优化任务来看,直接求最优解,而不是先求感知再求规控的最优解。
端到端效果怎么样?
今年各大自动驾驶公司都在预研和落地相关端到端方案,小鹏、蔚来、华为、理想也都对外展示了其端到端方案。由于端到端模型的优势明显,各大自动驾驶公司都在拼命布局揽人,对应岗位薪资水涨船高,某想甚至开出了七位数给到该岗位。
那么各家的端到端自动驾驶效果怎么样呢?先来看看国外的特斯拉:
,时长04:56
再来看看国内的UniAD效果:
,时长04:01
不得不说,端到端是一个更简约的方法,更具有全场景的优化能力。
端到端有哪些技术栈?
行业里面的端到端主要分为完全端到端方案、专注于planner的端到端方案(包括某鹏的XPlanner)。顾名思义,完全端到端是从传感器直接到规控;而专注于planner的端到端以感知模块的输出作为先验,替换原来以规则作为主要形式的PnC模块。
从传感器到控制策略的(如果把条件再放松下也可以到轨迹输出)完全端到端方案更为简约,但同样面临一个问题,可解释性差。UniAD用分阶段监督的方法逐步提高了可解释性,但训练仍然是个难题。在足够体量和质量的数据群下,效果能够得到保证,泛化性能也不错。
而专注于planner的端到端方案,如果深究的话,只能算狭义上的端到端,但更贴合当下的量产方案和任务,而且可解释性也较高,是目前主机厂和自动驾驶公司优先推行和落地的。
如果从信息输入的角度上来看,又可以分为纯视觉方案(UAD、UniAD这类)和多模态方案(FusionAD这类),传感器成本不断在下降,多模态方案也一直是行业里面都在关注的点。
#身处相机内外参之间(EG3D/NeRF/3D Gaussian Splatting)
“相机系统是一个非常令我头疼的事情,我真的很不擅长这个。”
这篇blog是为了“解析”我遇到的一些仓库中的关于相机系统的代码,这么说可能会很奇怪,因为对于比较专业的人来说,这些根本不能算是问题;但由于我是一个只知道PyTorch的文盲,所以这对我来说,是问题,而且很大。
哪怕在这里语境下的“相机”只是最最简单的理想模型,但即使这样,想修改与其相关的代码对没有受过相关训练的人来说还是有些困难的。尤其当需要对相机系统做一些更“定制化”的操作时候,浅显且不够直接的理解就不太能支持继续推进下去了。
我曾将我的窘况诉说给一位学长,他听完非常震惊“不是吧这都不会”(此处自动脑补“虾头座椅电脑”表情包)。他建议我去回炉重造,去看GAMES101或者洋人的公开课。但我没看几分钟就睡着了,睡醒以后我不禁陷入思考:在那个田园的时代,人们或许是通过看一些关于3DV的课或者书从而掌握了其理论基础,然后可能在OpenCV,OpenGL,Unity等库里进行实践,由于那些库写的都很严谨,所以很快就可以将理论与实际匹配上,然后熟悉这一套东西。
但在2024年,在新时代图形学的背景下,我如果再这么做可能有点缘木求鱼了。所以有没有一种顺应新时代的掌握这些的方法呢?大部分DL+3D项目都是缝来缝去,作者可能自己都不知道我的coordinates convention是什么。所以我觉得更适合新时代的方法是头铁的去硬看那些代码,把这些都看明白了,自然就会了。
这篇blog以这样的组织进行:
- 会先从EG3D的相机系统出发,这里提供了一种构造相机位姿矩阵(下文简称为c2w)的方法,同时可以让我们对NeRF-based的方法有个可视化的认识。在这个部分我们可以对c2w有一个直观的认识。
- 然后我们会返回去解决初学NeRF-PyTorch时不太关心的那些相机代码。相当于对上一部分的练习,同时温习一下透视投影。
- 之后我们会阅读PanoHead的预处理部分,其基于3DDFA_V2。这个预处理本质上和EG3D的预处理做的事情是一样的,只是这个基于3DDFA的预处理被PanoHead的作者整理的更自洽。在这套代码里,我们会正好遇到相机外参(下文简称为w2c)和c2w的关系。这一套预处理比较复杂,涉及正交投影和一些琐碎的数字图像处理。相当于某种程度的过关考核。
- 再然后我们会解读3D Gaussian Splatting的代码。这个如今大热的显式表示可以让我们复习一下在NeRF里常常被忽略的透视投影。
- 最后,我们会从更深入的角度下讨论在代码中经常出现的旋转操作,用李群和李代数的视角进行一些浅显的讨论,来从某种意义上“升华”一下我们的理解。
所以这篇blog至少需要读者大概了解上述工作,可能咿呀学语般的玩过其中一些的代码,大概能稀里糊涂的说出什么相机内外参,刚体变换之类的名词,对情况有大致的了解。(天哪这简直是我.jpg)
EG3D
在EG3D那一套代码里,我们经常可以看到c2w是这么被召唤出来的,先是有一个地方定义了一个:
camera_lookat_point = torch.tensor([0, 0, 0.2], device=self.device)
然后在需要c2w的地方,会有:
cam2world_pose = LookAtPoseSampler.sample(3.14/2 + 2 * 3.14 * frame_idx / num_frames, 3.14/2,
camera_lookat_point, radius=2.75, device=self.device)
类似这样的操作,然后就神奇的得到一个有效的4×4矩阵了。例如:
>>> cam2world = LookAtPoseSampler.sample(math.pi/4, math.pi/4, torch.tensor([0, 0, 0]), radius=2.5)
>>> tensor([[[ 0.7071, -0.3536, 0.6124, -1.5309],
[ 0.0000, -0.8660, -0.5000, 1.2500],
[ 0.7071, 0.3536, -0.6124, 1.5309],
[ 0.0000, 0.0000, 0.0000, 1.0000]]])
现在我们关心LookAtPoseSampler.sample
的实现,这个实现展示了一个很经典的用look, at, up构造相机外参的方法。EG3D中,整套代码是在这样的一个settings下:
这里用的是左手系,不过影响不大。整套代码的实现是:
class LookAtPoseSampler:
"""
Same as GaussianCameraPoseSampler, except the
camera is specified as looking at 'lookat_position', a 3-vector.
Example:
For a camera pose looking at the origin with the camera at position [0, 0, 1]:
cam2world = LookAtPoseSampler.sample(math.pi/2, math.pi/2, torch.tensor([0, 0, 0]), radius=1)
"""
@staticmethod
def sample(horizontal_mean, vertical_mean, lookat_position, horizontal_stddev=0, vertical_stddev=0, radius=1, batch_size=1, device='cpu'):
h = torch.randn((batch_size, 1), device=device) * horizontal_stddev + horizontal_mean
v = torch.randn((batch_size, 1), device=device) * vertical_stddev + vertical_mean
v = torch.clamp(v, 1e-5, math.pi - 1e-5)
theta = h
v = v / math.pi
phi = torch.arccos(1 - 2*v)
camera_origins = torch.zeros((batch_size, 3), device=device)
camera_origins[:, 0:1] = radius*torch.sin(phi) * torch.cos(math.pi-theta)
camera_origins[:, 2:3] = radius*torch.sin(phi) * torch.sin(math.pi-theta)
camera_origins[:, 1:2] = radius*torch.cos(phi)
# forward_vectors = math_utils.normalize_vecs(-camera_origins)
forward_vectors = math_utils.normalize_vecs(lookat_position - camera_origins)
return create_cam2world_matrix(forward_vectors, camera_origins)
最开始,是先构造出球坐标系下的和。然后,我们会根据半径r给出相机的位置camera_origins
:
由于和与标准的右手系下的球坐标系不太一样,所以这里变成了-。之后,我们会计算一个forward_vector
,这个其实就是LookAt。通过将输入的lookat_position
和camera_origins
的差归一化,我们会得到一个从相机位置指向look at位置的一个方向。然后就进入了create_cam2world_matrix(forward_vectors, camera_origins)
:
def create_cam2world_matrix(forward_vector, origin):
"""
Takes in the direction the camera is pointing and the camera origin and returns a cam2world matrix.
Works on batches of forward_vectors, origins. Assumes y-axis is up and that there is no camera roll.
"""
forward_vector = math_utils.normalize_vecs(forward_vector)
up_vector = torch.tensor([0, 1, 0], dtype=torch.float, device=origin.device).expand_as(forward_vector)
right_vector = -math_utils.normalize_vecs(torch.cross(up_vector, forward_vector, dim=-1))
up_vector = math_utils.normalize_vecs(torch.cross(forward_vector, right_vector, dim=-1))
rotation_matrix = torch.eye(4, device=origin.device).unsqueeze(0).repeat(forward_vector.shape[0], 1, 1)
rotation_matrix[:, :3, :3] = torch.stack((right_vector, up_vector, forward_vector), axis=-1)
translation_matrix = torch.eye(4, device=origin.device).unsqueeze(0).repeat(forward_vector.shape[0], 1, 1)
translation_matrix[:, :3, 3] = origin
cam2world = (translation_matrix @ rotation_matrix)[:, :, :]
assert(cam2world.shape[1:] == (4, 4))
return cam2world
这个矩阵很直接的描述了如何从相机坐标系下的某个点,变换回世界坐标系。我们可以带入[0.0.0.1],[1.0.0.1]这两个特殊的点:
相机外参实际上是这个矩阵的逆,即w2c。从坐标系变换来理解c2w和w2c,总是有些困难的。更倾向于用这种方法理解了c2w,然后将相机外参当作这个矩阵的逆就好了。可以做一些简单的可视化来加深印象,下图画一个对于人头前半部分和整个部分的相机:
上述脚本中设置了lookat_position
为[0, 0, 0.2],所以可以看到这些相机的延长线汇聚到了z轴上方的一个点。
然后是关于相机内参的事情,在EG3D里相机内参是在预处理阶段直接给定的。其本身没有什么特殊的,只是选取了一个视场角比较小的内参,在EG3D的代码中,可以看到:
intrinsics = torch.tensor([[4.2647, 0, 0.5], [0, 4.2647, 0.5], [0, 0, 1]], device=device)
上面的4.2647
是归一化后的内参:
所以:
内参在这套代码里的目的,是用来决定发射光线的方向的。具体来说是起到“视口变换”的逆变换,即从归一化平面坐标系转移到相机坐标系:
def raysampler(cam2world_matrix, intrinsics, resolutinotallow=16):
"""
Create batches of rays and return origins and directions.
cam2world_matrix: (N, 4, 4)
intrinsics: (N, 3, 3)
resolution: int
ray_origins: (N, M, 3)
ray_dirs: (N, M, 2)
"""
N, M = cam2world_matrix.shape[0], resolution**2
cam_locs_world = cam2world_matrix[:, :3, 3]
fx = intrinsics[:, 0, 0]
fy = intrinsics[:, 1, 1]
cx = intrinsics[:, 0, 2]
cy = intrinsics[:, 1, 2]
sk = intrinsics[:, 0, 1]
uv = torch.stack(torch.meshgrid(torch.arange(resolution, dtype=torch.float32, device=cam2world_matrix.device), torch.arange(resolution, dtype=torch.float32, device=cam2world_matrix.device), indexing='ij')) * (1./resolution) + (0.5/resolution)
uv = uv.flip(0).reshape(2, -1).transpose(1, 0)
uv = uv.unsqueeze(0).repeat(cam2world_matrix.shape[0], 1, 1)
x_cam = uv[:, :, 0].view(N, -1)
y_cam = uv[:, :, 1].view(N, -1)
z_cam = torch.ones((N, M), device=cam2world_matrix.device)
x_lift = (x_cam - cx.unsqueeze(-1) + cy.unsqueeze(-1)*sk.unsqueeze(-1)/fy.unsqueeze(-1) - sk.unsqueeze(-1)*y_cam/fy.unsqueeze(-1)) / fx.unsqueeze(-1) * z_cam
y_lift = (y_cam - cy.unsqueeze(-1)) / fy.unsqueeze(-1) * z_cam
cam_rel_points = torch.stack((x_lift, y_lift, z_cam, torch.ones_like(z_cam)), dim=-1)
world_rel_points = torch.bmm(cam2world_matrix, cam_rel_points.permute(0, 2, 1)).permute(0, 2, 1)[:, :, :3]
ray_dirs = world_rel_points - cam_locs_world[:, None, :]
ray_dirs = torch.nn.functional.normalize(ray_dirs, dim=2)
ray_origins = cam_locs_world.unsqueeze(1).repeat(1, ray_dirs.shape[1], 1)
return ray_origins, ray_dirs
sk
是一个用于校正x和y轴不垂直的系数,我们可以认为其是0。我们可以看出整个代码的逻辑是创建一个[0, 1]的uv
,实际上就是一个1×1的grid上的坐标。他们现在需要被换算到相机坐标系:
大多数时候,平面z都是1。然后这些相机坐标系下的点左乘刚才的c2w
,就可以变换到世界坐标系。继而求出ray_dirs
。
求出每个像素上光线的方向后,下面就是要考虑沿光线方向采样。在EG3D中,其对于FFHQ数据集,经验性质的选择了ray_start=2.25
,ray_end=3.3
。不同的数据集这个采样的上下限不一样,我推测是试+大概齐估计的。所以对于一个相机,它发出的光线大概就是下面这样:
我们可以看到采样出的那个光锥,以及视场角确实很小。给定一个预训练模型后,这里能改动的地方其实不多。首先更改ray_start
和ray_end
大概率会有一些无效的值,感觉能做的操作只剩下修改采样点数,和换成那个“倒数线性插值”。
以及ray_start=2.25
,ray_end=3.3
也和相机半径r在EG3D训练FFHQ被指定为2.75有关,可以看到图中的坐标基本是在-0.5~0.5
,因为后续需要在triplane里去插值。
最后画一个多个相机发出光线的样子:
这个动图还是挺有意思的。所有EG3D生成出来的图,都来自于中心那蓝色区域,蓝色区域会由不同的z所导引出的不同的特征图f按照triplane的方法来产生不同的响应,仿佛一个在涌现着什么的水晶球,从某个角度看过去就会有一张又一张的人脸。
“视锥之内,即是NeRF。”
所以在这个settings下,“透视变换”的概念就比较弱,相机内参给定焦距,焦距给定光线方向,除此以外就没有“透视投影”的事情了。
NeRF-PyTorch
上述EG3D的管线中,并没有使用“透视变换”。但实际上在原版NeRF实现中,我们不应忘记,是有NDC空间的使用的。而从正常的欧式空间变换到NDC空间用的就是透视变换。
对于拍摄的facing-forward的数据集,我们应该在训练时变换到NDC空间,这样深度就被缩放到 [-1.1]了,有利于NN去拟合。同时在此基础上对z进行均匀采样,在NDC空间里自动变成了对近处多采一些对远处少采一些的倒数线性采样。
在这篇博客中,详细阐述了NeRF之中的NDC和lindisp。我们在这里不作赘述,重点放在解析其他的一些相机工具代码。
我们主要讨论load_llff.py
中的一些函数,这个版本的NeRF代码中传递的poses一般为[N, 3, 5],N为数量,5那儿多出来的那一维是cat了图像的宽高,估计的焦距。中间是3而不是4,是因为存储时舍去了[0.0.0.1]这一行。这里我们使用的poses是由LLFF中的imgs2poses.py导出的,其中储存的就是c2w。
一个常用的函数是求取这些位姿的平均姿态pose_avg:
def viewmatrix(z, up, pos):
vec2 = normalize(z)
vec1_avg = up
vec0 = normalize(np.cross(vec1_avg, vec2))
vec1 = normalize(np.cross(vec2, vec0))
m = np.stack([vec0, vec1, vec2, pos], 1)
return m
def poses_avg(poses):
hwf = poses[0, :3, -1:]
center = poses[:, :3, 3].mean(0)
vec2 = normalize(poses[:, :3, 2].sum(0))
up = poses[:, :3, 1].sum(0)
c2w = np.concatenate([viewmatrix(vec2, up, center), hwf], 1)
return c2w
这个是很直接的,我们求取了每个轴方向向量的平均,相机位置的平均,最后得到一个表示相机平均位置的c2w,如下图所示:
一个在处理facing-forward的数据时的一个很有用的操作是recenter_poses,通过对全体姿态左乘一个平均姿态的逆,可以让任意settings下的相机都转变到一个“跟世界坐标系一致”的样子下:
def recenter_poses(poses):
poses_ = poses+0
bottom = np.reshape([0,0,0,1.], [1,4])
c2w = poses_avg(poses)
c2w = np.concatenate([c2w[:3,:4], bottom], -2)
bottom = np.tile(np.reshape(bottom, [1,1,4]), [poses.shape[0],1,1])
poses = np.concatenate([poses[:,:3,:4], bottom], -2)
poses = np.linalg.inv(c2w) @ poses
poses_[:,:3,:4] = poses[:,:3,:4]
poses = poses_
return poses
这么说可能有点抽象,下面是图示:
我们可以很轻松的看出哪些是recenter后的相机,可以看到,recenter消除了后面做NDC时的歧义,同时方便了后面生成螺旋轨迹的相机轨迹。
另一个比较复杂的就是spherify_poses,它分为两个部分,先对相机轨迹进行“球化”,然后再其基础上构造新的相机轨迹(用于可视化):
def spherify_poses(poses, bds):
p34_to_44 = lambda p : np.concatenate([p, np.tile(np.reshape(np.eye(4)[-1,:], [1,1,4]), [p.shape[0], 1,1])], 1)
rays_d = poses[:,:3,2:3]
rays_o = poses[:,:3,3:4]
def min_line_dist(rays_o, rays_d):
A_i = np.eye(3) - rays_d * np.transpose(rays_d, [0,2,1])
b_i = -A_i @ rays_o
pt_mindist = np.squeeze(-np.linalg.inv((np.transpose(A_i, [0,2,1]) @ A_i).mean(0)) @ (b_i).mean(0))
return pt_mindist
pt_mindist = min_line_dist(rays_o, rays_d)
center = pt_mindist
up = (poses[:,:3,3] - center).mean(0)
vec0 = normalize(up)
vec1 = normalize(np.cross([.1,.2,.3], vec0))
vec2 = normalize(np.cross(vec0, vec1))
pos = center
c2w = np.stack([vec1, vec2, vec0, pos], 1)
poses_reset = np.linalg.inv(p34_to_44(c2w[None])) @ p34_to_44(poses[:,:3,:4])
rad = np.sqrt(np.mean(np.sum(np.square(poses_reset[:,:3,3]), -1)))
sc = 1./rad
poses_reset[:,:3,3] *= sc
bds *= sc
rad *= sc
centroid = np.mean(poses_reset[:,:3,3], 0)
zh = centroid[2]
radcircle = np.sqrt(rad**2-zh**2)
new_poses = []
for th in np.linspace(0.,2.*np.pi, 120):
camorigin = np.array([radcircle * np.cos(th), radcircle * np.sin(th), zh])
up = np.array([0,0,-1.])
vec2 = normalize(camorigin)
vec0 = normalize(np.cross(vec2, up))
vec1 = normalize(np.cross(vec2, vec0))
pos = camorigin
p = np.stack([vec0, vec1, vec2, pos], 1)
new_poses.append(p)
new_poses = np.stack(new_poses, 0)
new_poses = np.concatenate([new_poses, np.broadcast_to(poses[0,:3,-1:], new_poses[:,:3,-1:].shape)], -1)
poses_reset = np.concatenate([poses_reset[:,:3,:4], np.broadcast_to(poses[0,:3,-1:], poses_reset[:,:3,-1:].shape)], -1)
return poses_reset, new_poses, bds
代码的前半部分是一个有趣的线性代数问题(min_line_dist),给定N条直线,求出一个点到这些直线的距离和最小。这个问题可以直接直接得到解析解。考虑光线原点o,光线方向d,直线上的点可以表述为o+td,考虑直线外一点p,其点到直线距离为(均为列向量):
最后一个等号是因为方向向量d的内积d是1,所以I-d长成了一个幂等矩阵。
考虑有N条光线时,有:
这其实就是一个线性方程组:
于是就有了上面代码里的子函数。接下来,这个函数用不同的叉乘顺序计算了一个类似c2w的矩阵,其位置是刚才估计的直线中心。然后对所有位姿左乘其逆阵,这样就让位姿的lookat对准原点了。同时将相机位置的半径缩放为1,完成“球化”:
(但这个“球化”并不意味着相机姿态真的处理成了一个球面上均匀分布的样子,不要混淆。)
最后一个需要解读的函数是render_path_spiral,这个函数可以生成一个螺旋型的轨迹,来可视化出NeRF在facing-forward时的novel pose:
def render_path_spiral(c2w, up, rads, focal, zdelta, zrate, rots, N):
render_poses = []
rads = np.array(list(rads) + [1.])
hwf = c2w[:,4:5]
for theta in np.linspace(0., 2. * np.pi * rots, N+1)[:-1]:
c = np.dot(c2w[:3,:4], np.array([np.cos(theta), -np.sin(theta), -np.sin(theta*zrate), 1.]) * rads)
z = normalize(c - np.dot(c2w[:3,:4], np.array([0,0,-focal, 1.])))
render_poses.append(np.concatenate([viewmatrix(z, up, c), hwf], 1))
return render_poses
可以看出是先创建这样的螺旋线:
与解析几何中熟悉的螺旋线不同,这里的z是用sin(.)来有确保有界的。这个函数的其他输入来自于load_llff_data函数的一个分支:
else:
c2w = poses_avg(poses)
print('recentered', c2w.shape)
print(c2w[:3,:4])
## Get spiral
# Get average pose
up = normalize(poses[:, :3, 1].sum(0))
# Find a reasonable "focus depth" for this dataset
close_depth, inf_depth = bds.min()*.9, bds.max()*5.
dt = .75
mean_dz = 1./(((1.-dt)/close_depth + dt/inf_depth))
focal = mean_dz
# Get radii for spiral path
shrink_factor = .8
zdelta = close_depth * .2
tt = poses[:,:3,3] # ptstocam(poses[:3,3,:].T, c2w).T
rads = np.percentile(np.abs(tt), 90, 0)
c2w_path = c2w
N_views = 120
N_rots = 2
if path_zflat:
# zloc = np.percentile(tt, 10, 0)[2]
zloc = -close_depth * .1
c2w_path[:3,3] = c2w_path[:3,3] + zloc * c2w_path[:3,2]
rads[2] = 0.
N_rots = 1
N_views/=2
# Generate poses for spiral path
render_poses = render_path_spiral(c2w_path, up, rads, focal, zdelta, zrate=.5, rots=N_rots, N=N_views)
通过场景的界限bds,我们可以知道一个类似近远平面的度量,将两者进行倒数线性插值,作为一个估计,来给出focal,focal在def render_path_spiral用于计算lookat的那个固定点。当path_zflat为真时,计算出的rads的z值会归0,于是刚才的螺旋线将退化为圆。
这就是NeRF中的一些关于相机的工具函数了。
3DDFA
在这个部分,我们其实关心的是EG3D中对人脸图片的预处理。这个预处理在处理2D人脸数据时常用的“FFHQ alignment”上更进了一步。但由于EG3D给的预处理代码写的比较分散,不太适合作为“教具”。而基于EG3D的工作PanoHead里提供了一个更自洽和规整的脚本,来让我们可以体会对齐这一步。
PanoHead的预处理用到了3DDFA,这篇工作的全称是Face Alignment in Full Pose Range: A 3D Total Solution,它的本意是为了为了提升大角度偏转情况下人脸对齐的效果,在这个预处理中用到它的目的其实和它的本意有点出入,但这个问题不大,我们也可以把这个当作一个熟悉陌生工作中相机settings的例子。我这里用一张3DDFA GitHub仓库里的teaser来图示一下这个工作具体做了什么事情。
括号里的那一项是一个3DMM重建出的人脸,然后这个人脸上的每一个点,都被R旋转,然后被Pr做正交投影,这个概念非常的简单,正交投影矩阵只是:
你可以看出它直接忽略掉了z,等价于一个焦距非常大的相机,没有近大远小的性质,在这个任务的情景下是非常合适的简化。而它又乘了一个缩放系数f,那么实际上f* Pr在做的操作有一个专门的名字:弱透视投影。
在一个比较滑稽的情境下,我被指出分不清强投影和弱投影。实际上这个概念非常简单,只是我过于土鳖,没听过。弱投影应该就是上面的“弱透视投影”,我后来又去搜了一下,发现并没有“强投影”这个专有名词,大概率这个强投影只是指普通的透视投影。
这里的f,你可以把它按照弱透视投影里的那个scaling来理解,或者更简单地,单纯的认为它只是将标准3DMM里那些顶点的量级(大约几百)调整为更符合图像中的量级(可能-1~1)。
所以这里就有了一个很有意思的比较,再看刚才的式子:
这个过程被总结为MVP变换,但其实GAMES101上讲的MVP变换描述了一个更正规情况下的事情。比如在GAMES101中的视图变换,其变换矩阵就是用上面说的lookat的事情构造的,并不是单纯的只有旋转R,透视变换时其作的也是正常的透视变换而非简化后的弱透视变换。
根源是我们这里在单纯的描述“从一张图片中重建出一个人脸mesh”时,我们不需要那么详细的描述。我们可以假定焦距无限远,然后不考虑近大远小,因为人头本身就不是一个很长的东西。
只不过在这里,我们需要“意识到”投影变换的存在。正如前文所说,一个滑稽的事情就是:假如你入门3DV只是跟我一样通过NeRF入门的,那么你大概率“意识不到”投影变换的存在,你会把它弱化,结构为初二物理里平面镜成像规律中就能学到的“近大远小”。你虽然知道有这么个东西,但你大概率很难对这个有什么“intuition”。因为在NeRF里每个像素都是ray casting出来的,想象成是某种“投影”并不直接。因为从体渲染的角度来看,NeRF是backward mapping, 是发出光线去查点,你并不需要像forward mapping般的主动的去投影。
意识到这些以后,我们可以看一下代码了。
在recrop_images.py之前,其实每张输入的人脸图片是通过dlib库检测了一波人脸关键点的。如下图所示:
从FFHQ数据集的对齐开始,就一直沿用一种方法:从这些关键点中,提取左眼,右眼,嘴部的关键点,然后整合出一个quad,其包含4个顶点,在图上可以连成一个类似平行四边形的图案,如上图中的红色圆点(灰色区域表示他们在原图外围)。FFHQ对齐的精髓在于以这4个点对图片进行仿射变换,从而将图片拉正。这样可以保证GAN学习到一个更规整的人脸分布,减少一些掉san的伪影。
但在这里,我们不仅要对齐这个人头,我们还想用3DDFA估计出这个人头此时的位姿,这要求我们对quad进行进一步的处理。同时我们希望位姿符合EG3D的相机系统,所以接下来会有更复杂的一些处理。首先,利用已有的,通过关键点计算出的quad对图片进行仿射变换:
bound = np.array([[0, 0], [0, size-1], [size-1, size-1], [size-1, 0]], dtype=np.float32)
mat = cv2.getAffineTransform(quad[:3], bound[:3])
img = crop_image(img_orig, mat, size, size)
然后用3DDFA扫一下仿射变换后的图片img:
param_lst, roi_box_lst = tddfa(img, boxes)
box_idx = find_center_bbox(roi_box_lst, w, h)
这里find_center_bbox就是单纯的在所有roi_box_lst里,最接近图片中心的那个。但大部分时候图片里只有一张人脸,我们可以认为box_idx就是0。接下来我们会利用第box_idx个param_lst和roi_box_lst来进行处理:
param = param_lst[box_idx]
P = param[:12].reshape(3, -1) # camera matrix
s_relative, R, t3d = P2sRt(P)
我们可以打印一个P出来,会发现:
(Pdb) print(P)
[[ 4.7336455e-04 3.7309933e-06 1.8318256e-05 5.8912811e+01]
[ 1.1534430e-06 4.8227943e-04 1.3704226e-05 6.9054771e+01]
[-1.9893454e-05 -1.7727274e-05 4.7678972e-04 -6.6671005e+01]]
def P2sRt(P):
""" decompositing camera matrix P.
Args:
P: (3, 4). Affine Camera Matrix.
Returns:
s: scale factor.
R: (3, 3). rotation matrix.
t2d: (2,). 2d translation.
"""
t3d = P[:, 3]
R1 = P[0:1, :3]
R2 = P[1:2, :3]
s = (np.linalg.norm(R1) + np.linalg.norm(R2)) / 2.0
r1 = R1 / np.linalg.norm(R1)
r2 = R2 / np.linalg.norm(R2)
r3 = np.cross(r1, r2)
R = np.concatenate((r1, r2, r3), 0)
return s, R, t3d
注意t3d中,实际上有效的只有前两个数,在tddfa中t3d的最后一位永远是-66.67,因为这里没有任何深度之类的事情(焦距无限远)。然后我们提取第一行和第二行,将其归一化,叉乘出第三行,得到新的正交阵作为旋转R。同时计算第一行和第二行模场的平均值,我们就可以估计出1/f,代码中记成了s(scale)。根据3DDFA用的BFM model的量级,其实f可以估计出大致是2000。
(Pdb) print(s_relative)
0.0004781045136041939
(Pdb) print(R)
[[ 0.9992211 0.00787572 0.03866785]
[ 0.00239068 0.9995937 0.02840398]
[-0.03842843 -0.02828942 0.9987962 ]]
(Pdb) print(t3d)
[ 58.91281 69.05477 -66.671005]
然后,需要调整一下t3d,在代码中称为"Adjust z-translation in object space",我推测这么做的原因是3DDFA估计出的结果,是隐含在BFM标准人头的那个坐标系下的:
如上图所示,蓝色的人脸是标准的BFM,橙色的是将z轴减去0.5mean(z)以后的BFM。因为我们想要那个人头(是某种程度上)在原点附近的,0.5mean(z)作为一个经验值就被作者采用了。可以看到右侧的橙色人脸,如果我们脑补一个完整的脑壳出来,那么差不多就是一个在原点处的人头了。
这样做虽然是调整"z-translation",但我们这里其实是没有真正的深度的,所以这样做的目的其实是修正t3d中的t2d,即:
R_ = param[:12].reshape(3, -1)[:, :3]
u = tddfa.bfm.u.reshape(3, -1, order='F')
trans_z = np.array([ 0, 0, 0.5*u[2].mean() ]) # Adjust the object center
trans = np.matmul(R_, trans_z.reshape(3,1))
t3d += trans.reshape(3)
在正脸的时候,trans大概是:
[[ 0.6798876 ]
[ 0.50863648]
[17.69619383]]
由于是正脸,所以修正项很小。
在一些侧着头的情况下,大概是:
[[-11.60984414]
[ -0.08104343]
[ 14.03402536]]
这时候修正项就不能忽略了。
接下来,我们需要对t3d进行归一化,这一步基本算是数字图像处理课程里的习题,t3d本身是在3DDFA的尺寸下的,其中tddfa.size一般是120,我们需要通过之前Face_Boxes得到的roi_box_lst把t3d先缩放回原始图像上:
sx, sy, ex, ey = roi_box_lst[0]
scale_x = (ex - sx) / tddfa.size
scale_y = (ey - sy) / tddfa.size
由于t3d的坐标系和图像坐标系的y轴是反的(如下图):
所以进一步换算为:
t3d[0] = (t3d[0]-1) * scale_x + sx
t3d[1] = (tddfa.size-t3d[1]) * scale_y + sy
然后再根据图片的w和h进行归一化:
t3d[0] = (t3d[0] - 0.5*(w-1)) / (0.5*(w-1)) # Normalize to [-1,1]
t3d[1] = (t3d[1] - 0.5*(h-1)) / (0.5*(h-1)) # Normalize to [-1,1], y is flipped for image space
同时要对缩放系数s_relative也进行“重缩放”:
s_relative = s_relative * 2000
scale_x = (ex - sx) / (w-1)
scale_y = (ey - sy) / (h-1)
s = (scale_x + scale_y) / 2 * s_relative
完成这些以后,我们终于可以对quad进行加工了:
quad_c = quad_c + quad_x * t3d[0]
quad_c = quad_c - quad_y * t3d[1]
quad_x = quad_x * s
quad_y = quad_y * s
c, x, y = quad_c, quad_x, quad_y
quad = np.stack([c - x - y, c - x + y, c + x + y, c + x - y]).astype(np.float32)
我们可以看出,对quad进行修改的目的,是为了让quad圈定的图片中的人头的中心更贴近(TDDFA认为的)图像中心。即争取让每一张图片在对齐后,输入给TDDFA后都能输出近似为[60, 60, -66.7]的t3d(tddfa.size为120)。
最后一步就是要得到c2w矩阵来做训练,由于现在的图像已经被裁剪成没有“translation”了,我们只需要拿来旋转R,就可以产生一个符合PanoHead的c2w了。
这里有一个比较不自然的事情,TDDFA输出的R其实是将BFM从canonical变换成图像中的姿态。在坐标系规定合适的情况下,它是等价于w2c中的R的,但许多时候不一定合适,这样可能x或y方向就会差个负号。我们下面的讨论里就当这里的R是等价于w2c里的R的。
但与之前不同,我们并不是很清楚相机的位置,不能直接用角度定义。我们可以先给出相机外参w2c,然后求逆。当谈及相机外参时,我们往往会提到"translation vector",即 。一个关于t的表述是:世界坐标系下的原点在相机坐标系下的表示。因为当你将[0.0.0.1]左乘w2c后,你会得到[t.1]。
考虑旋转R和平移t构成的w2c,我们可以构造出其逆阵:
所以考虑c2w中的旋转和相机位置C,我们有:
那么直接代入t=-RC就会得到(注意向量r和向量u对于向量c都是正交的):
最后得到的结果即是相机位置的模长,由于相机位置是在球坐标系下采样得到的,所以就是半径。
于是我们直接取t=[0,0,-radius],即可得到w2c,继而求逆得到c2w:
def eg3dcamparams(R_in):
camera_dist = 2.7
intrinsics = np.array([[4.2647, 0, 0.5], [0, 4.2647, 0.5], [0, 0, 1]])
# assume inputs are rotation matrices for world2cam projection
R = np.array(R_in).astype(np.float32).reshape(4,4)
# add camera translation
t = np.eye(4, dtype=np.float32)
t[2, 3] = - camera_dist
# convert to OpenCV camera
convert = np.array([
[1, 0, 0, 0],
[0, -1, 0, 0],
[0, 0, -1, 0],
[0, 0, 0, 1],
]).astype(np.float32)
# world2cam -> cam2world
P = convert @ t @ R
cam2world = np.linalg.inv(P)
# add intrinsics
label_new = np.concatenate([cam2world.reshape(16), intrinsics.reshape(9)], -1)
return label_new
到这里,才完成了全部。我们终于从一张wild-image出发,得到了对齐后的图像和对应的符合EG3D格式的相机位姿。
3D Gaussian Splatting
在3DGS中,有着更加“practical”的相机系统。由于3DGS是一种显式的表达方法,其跟NeRF在相机实现上的最大不同就是因为其是forward mapping,所以必须手动实现投影的过程。除了这一点以外,由于3DGS的官方代码开发周期比较长,所以里面很多令人困惑的地方,下面我们一并过一下。
3DGS中将相机的一些属性组织进一个Camera类中:
class Camera(nn.Module):
def __init__(self, colmap_id, R, T, FoVx, FoVy, image, gt_alpha_mask,
image_name, uid,
trans=np.array([0.0, 0.0, 0.0]), scale=1.0, data_device = "cuda"
):
super(Camera, self).__init__()
self.uid = uid
self.colmap_id = colmap_id
self.R = R
self.T = T
self.FoVx = FoVx
self.FoVy = FoVy
self.image_name = image_name
try:
self.data_device = torch.device(data_device)
except Exception as e:
print(e)
print(f"[Warning] Custom device {data_device} failed, fallback to default cuda device" )
self.data_device = torch.device("cuda")
self.original_image = image.clamp(0.0, 1.0).to(self.data_device)
self.image_width = self.original_image.shape[2]
self.image_height = self.original_image.shape[1]
if gt_alpha_mask is not None:
self.original_image *= gt_alpha_mask.to(self.data_device)
else:
self.original_image *= torch.ones((1, self.image_height, self.image_width), device=self.data_device)
self.zfar = 100.0
self.znear = 0.01
self.trans = trans
self.scale = scale
self.world_view_transform = torch.tensor(getWorld2View2(R, T, trans, scale)).transpose(0, 1).cuda()
self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0,1).cuda()
self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0)
self.camera_center = self.world_view_transform.inverse()[3, :3]
具体的逻辑是,当在Scene中加载数据集中的每一张图片时,旋即读取相应的COLMAP或synthesis数据集的transform,对应实例化一个Camera出来。然后会来到scene/dataset_readers.py里,可以看到其为这两种形式的数据集layout提供了两种回调函数:
sceneLoadTypeCallbacks = {
"Colmap": readColmapSceneInfo,
"Blender" : readNerfSyntheticInfo
}
但无论是哪种,我们都会发现,程序赋值的R,都是相机外参的旋转的转置:
def readColmapCameras(cam_extrinsics, cam_intrinsics, images_folder):
...
R = np.transpose(qvec2rotmat(extr.qvec))
...
def readCamerasFromTransforms(path, transformsfile, white_background, extensinotallow=".png"):
...
# get the world-to-camera transform and set R, T
w2c = np.linalg.inv(c2w)
R = np.transpose(w2c[:3,:3]) # R is stored transposed due to 'glm' in CUDA code
...
这个其实是历史遗留问题,我们返回来看Camera实现里关于变换的计算:
self.world_view_transform = torch.tensor(getWorld2View2(R, T, trans, scale)).transpose(0, 1).cuda()
self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0,1).cuda()
self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0)
self.camera_center = self.world_view_transform.inverse()[3, :3]
查看getWorld2View2:
def getWorld2View2(R, t, translate=np.array([.0, .0, .0]), scale=1.0):
Rt = np.zeros((4, 4))
Rt[:3, :3] = R.transpose()
Rt[:3, 3] = t
Rt[3, 3] = 1.0
C2W = np.linalg.inv(Rt)
cam_center = C2W[:3, 3]
cam_center = (cam_center + translate) * scale
C2W[:3, 3] = cam_center
Rt = np.linalg.inv(C2W)
return np.float32(Rt)
也就是说输入进Camera的R本身就是w2c转置过的,也就是c2w里的旋转,然后这里又取个转置得到Rt,那么说明Rt应该是w2c,然后这里留了一个修正姿态的废案,给Rt取逆得到c2w。然后再给c2w求逆得到w2c,所以这个函数返回的是相机外参/w2c,这没什么问题。
但在给self.world_view_transform赋值的时候,刚传出来的w2c会再转置一下。所以我们得到的其实是:
同样的,在创建透视投影矩阵时,最后也是将结果转置一下,得到。这里如果我们检视getProjectionMatrix(),会发现其和我们熟悉的透视投影矩阵不太一样,代码中实现的是:
而我们更熟悉的投影矩阵是:
于是投影矩阵P即:
你或许发现第一行的第三个元素和第二行的第三个元素,跟代码实现不符。这应该是代码实现的BUG,但由于函数内定义的视锥是对称的(r=-l,t=-b),所以这一项为0,不会影响结果。
为了对这个过程有更加透彻的理解,这里展示了一个透视矩阵视场角,近平面,远平面变化时,视锥体(黑)和canonical(红)的样子:
注意这里z轴是被缩放到[0.1],所以其看起来是个长方体而非正方体。
我们继续回到代码,之后的self.full_proj_transform是,刚才的self.world_view_transform是,在render时我们传入CUDA侧的变换都是转置过的。这其实是因为CUDA代码中是按照glm的规范,即列主序实现的矩阵乘。例如在cuda_rasterizer/auxiliary.h中:
__forceinline__ __device__ float3 transformPoint4x3(const float3& p, const float* matrix)
{
float3 transformed = {
matrix[0] * p.x + matrix[4] * p.y + matrix[8] * p.z + matrix[12],
matrix[1] * p.x + matrix[5] * p.y + matrix[9] * p.z + matrix[13],
matrix[2] * p.x + matrix[6] * p.y + matrix[10] * p.z + matrix[14],
};
return transformed;
}
__forceinline__ __device__ float4 transformPoint4x4(const float3& p, const float* matrix)
{
float4 transformed = {
matrix[0] * p.x + matrix[4] * p.y + matrix[8] * p.z + matrix[12],
matrix[1] * p.x + matrix[5] * p.y + matrix[9] * p.z + matrix[13],
matrix[2] * p.x + matrix[6] * p.y + matrix[10] * p.z + matrix[14],
matrix[3] * p.x + matrix[7] * p.y + matrix[11] * p.z + matrix[15]
};
return transformed;
}
可以看到这些索引并不是按照我们以为的:
...
matrix[0] * p.x + matrix[1] * p.y + matrix[2] * p.z + matrix[3],
...
索引排列。
接下来我们简要讨论下为什么要将这两个变换传入CUDA侧。传入CUDA的两个变换矩阵viewmatrix和projmatrix有不同的作用,首先通过这两个变换,可以判断每个线程处理的高斯核是否在关心的视锥内,如果不在可以直接结束该线程:
__forceinline__ __device__ bool in_frustum(int idx,
const float* orig_points,
const float* viewmatrix,
const float* projmatrix,
bool prefiltered,
float3& p_view)
{
float3 p_orig = { orig_points[3 * idx], orig_points[3 * idx + 1], orig_points[3 * idx + 2] };
// Bring points to screen space
float4 p_hom = transformPoint4x4(p_orig, projmatrix);
float p_w = 1.0f / (p_hom.w + 0.0000001f);
float3 p_proj = { p_hom.x * p_w, p_hom.y * p_w, p_hom.z * p_w };
p_view = transformPoint4x3(p_orig, viewmatrix);
if (p_view.z <= 0.2f)// || ((p_proj.x < -1.3 || p_proj.x > 1.3 || p_proj.y < -1.3 || p_proj.y > 1.3)))
{
if (prefiltered)
{
printf("Point is filtered although prefiltered is set. This shouldn't happen!");
__trap();
}
return false;
}
return true;
}
projmatrix计算出的结果p_proj,会联合cov2d,用于计算radii,从而得到该高斯核影响那些像素:
// Compute extent in screen space (by finding eigenvalues of
// 2D covariance matrix). Use extent to compute a bounding rectangle
// of screen-space tiles that this Gaussian overlaps with. Quit if
// rectangle covers 0 tiles.
float mid = 0.5f * (cov.x + cov.z);
float lambda1 = mid + sqrt(max(0.1f, mid * mid - det));
float lambda2 = mid - sqrt(max(0.1f, mid * mid - det));
float my_radius = ceil(3.f * sqrt(max(lambda1, lambda2)));
float2 point_image = { ndc2Pix(p_proj.x, W), ndc2Pix(p_proj.y, H) };
uint2 rect_min, rect_max;
getRect(point_image, my_radius, rect_min, rect_max, grid);
if ((rect_max.x - rect_min.x) * (rect_max.y - rect_min.y) == 0)
return;
计算cov2d时,需要用viewmatrix得到在相机下高斯点的位置,然后用EWA sampling中仿射变换的一阶近似来得到J,才能得到cov2d:
// The following models the steps outlined by equations 29
// and 31 in "EWA Splatting" (Zwicker et al., 2002).
// Additionally considers aspect / scaling of viewport.
// Transposes used to account for row-/column-major conventions.
float3 t = transformPoint4x3(mean, viewmatrix);
const float limx = 1.3f * tan_fovx;
const float limy = 1.3f * tan_fovy;
const float txtz = t.x / t.z;
const float tytz = t.y / t.z;
t.x = min(limx, max(-limx, txtz)) * t.z;
t.y = min(limy, max(-limy, tytz)) * t.z;
glm::mat3 J = glm::mat3(
focal_x / t.z, 0.0f, -(focal_x * t.x) / (t.z * t.z),
0.0f, focal_y / t.z, -(focal_y * t.y) / (t.z * t.z),
0, 0, 0);
glm::mat3 W = glm::mat3(
viewmatrix[0], viewmatrix[4], viewmatrix[8],
viewmatrix[1], viewmatrix[5], viewmatrix[9],
viewmatrix[2], viewmatrix[6], viewmatrix[10]);
glm::mat3 T = W * J;
glm::mat3 Vrk = glm::mat3(
cov3D[0], cov3D[1], cov3D[2],
cov3D[1], cov3D[3], cov3D[4],
cov3D[2], cov3D[4], cov3D[5]);
glm::mat3 cov = glm::transpose(T) * glm::transpose(Vrk) * T;
这就是传入这两个变换的逻辑,如果你对这其中的一些推导感兴趣,可以查阅这篇博客。
在讨论中我们忽略了一个传入CUDA侧的参数:相机位置。它的作用是计算点和相机的方向,从而计算球谐系数。在Python侧,相机位置通过计算:
self.camera_center = self.world_view_transform.inverse()[3, :3]
来实现,我们在先前已经讨论过为什么对w2c求逆可以得到相机位置了。
Lie theory of rotations
我们现在已经结合许多实际的项目代码分析并可视化了一些内容,在最后我们需要将目光放在一直以来都被认为习以为常的旋转R上。
了解到这一层对于搬运代码来说完全足够,但这会萌生更多新的问题,比如罗德里格斯公式除了可以几何意义上推导,还可以怎么来?为什么有时候这个公式左侧不叫R而叫exp(.)?为什么实际的旋转是四元数表示的旋转的两倍?以及四元数这一堆是什么?
我们需要用一个更抽象但合适的理论来概括一下,即需要借助李群(Lie Group)和李代数(Lie algebra)。由于我不是数学或物理专业出身,所以下面的叙述以建立直觉和认识为主。对于像我这样没有学过数学的人来说,阅读《视觉SLAM十四讲》的第四章是最合适的切入方式,但仅仅阅读那一段还是太过于管中窥豹了,接下来我们先以《视觉SLAM》十四讲的部分引入,然后再此基础上追加一些内容。
在大一的时候,我们学过如何计算向量叉乘(外积):
右边的结果可以进一步拆成矩阵与向量的乘法:
接下来我们这里按SLAM十四讲里的方法来引入“李代数”,考虑旋转矩阵R,我们假设其代表某个相机的旋转,即随时间变化。那么根据旋转矩阵的正交性:
非正式地,我们对两边关于时间求导,得到:
不失一般性的,我们取=0且R(0)=I,于是右边可以写成:
我们看到,向量(t0)某种程度上反应了R的变化。除此以外,由于:
我们假设在t0附近(t0)=,那么上式就是一个普通的常微分方程,初始值为R(0)=I:
这里t只是我们为了进行上述推理所虚设的一个值,他其实没必要是0,只要在R(0)=I,那么在的邻域内就可以有这样的关系。
我们其实还没有定义这个情景下的指数运算。但我们能感觉到,冥冥之中,好像我如果有一个向量,然后把这个向量写成反对称阵,然后做一下这个“exp(.)”就可以得到R了。除此以外,我们其实也并不了解“为什么上面要这么引入”,为什么要构造一个似是而非的R(t)”和凑个微分方程,感觉很生硬。
但其实,李群、李代数的初衷好像就是为了求解微分方程。不过好像不是上面这样。
下面我们先引入群:我们刚才是习惯上想用R(t)来描述一组R,我们最好引入另一个结构来描述一组R,即群。群的定义是:
则称(G,.)为一个群。
所以我们一直在处理的旋转R,它和矩阵乘法就组成了一个群。显然旋转变换的复合还是旋转变换,且矩阵乘法满足结合律,旋转变换存在幺元即单位旋转,每个旋转变换都存在逆阵。我们将行列式为1的这些旋转矩阵,与矩阵乘法组成的群,称为特殊正交群(Special Orthogonal Group),记作SO(n):
我们关心的往往是二维和三维上的旋转,即SO(2)SO(3)。更重要的是,旋转是可以连续变化的。这种具有连续(光滑)性质的群,称为李群。我们可以简单的认为,由于李群的连续性,使得我们比较熟悉的微积分可以作为分析李群的工具。
这个指数映射是具有一般性的,我们考虑幺元g(0)=1附近的无穷小的群:
那么根据重要极限,我们就有:
所以刚才的例子就是生成元取J=i时的情况。所以从生成元和幺元出发,我们可以得到整个李群。这就是为什么,在刚才微分方程背景下的引入中,我们有“只要在R(t0)=I,那么在的邻域内就可以有这样的关系。”。
有了对李群和李代数的更直观的了解,现在我们引入李代数的定义:
其中二元运算[,]称为李括号(Lie bracket),这个运算只要求自反,不要求结合律,用于表达两个元素的差异。
形式上即泰勒展开。对于指数映射出的矩阵exp(A),这里有一个比较重要的性质:
这是由于任何复方阵A都可以作相似变换,得到一个三角阵:
那么两边作指数映射:
于是我们就得到了罗德里格斯公式,这个公式在SMPL和FLAME都用到了。这事实上说明, 里的so(3)其实就是几何意义上的旋转向量。相应地,我们也可以定义对数映射,来将SO(3)中的元素对应到so(3)上:
这个式子比较难处理,更明智的做法是通过指数映射的结论两边取迹:
现在我们从指数映射上解答了罗德里格斯公式,下面我们开始讨论一下四元数,我们知道四元数是所谓:
我们会发现,这个结构可以用矩阵和矩阵乘法来等价描述,下面我们定义这样的复矩阵:
这样,通过用a,b,c,d线性组合这四个矩阵,我们也可以得到一个复矩阵U:
这也是个特殊的群,我们定义:
称之为二阶特殊幺正(unitary)群,实际上,单位四元数以及其乘法构成了一个群,而2X2的幺正矩阵以及矩阵乘法也构成了一个群,并且每个单位四元数都可以对应到某个幺正矩阵中。这种关系正是群的同构,我们可以通过研究SU(2)来研究四元数。
现在,我们将SO(3)和SU(2)放在一起,考察他们的生成元,在SO(3)的定义中,我们有:
如果我们用矩阵来表达生成元,那么满足上面两个条件的一组基矩阵可以是:
以及,我们发现,他们之间的李括号存在这样的关系:
我们再考虑SU(2)上的生成元:
由于此时是复矩阵,所以:
可以验证,如下三个基矩阵可以用来表达生成元:
所以我们其实也得到了su(2),即:
此时,这几个基矩阵之间的李括号为:
我们将变换作用上去:
用二项式定理展开,并整理,可以得到:
这里:
结合先前的式子,这实际上给出了一种对旋转后的球谐函数进行计算的方法:
上式给出的复球谐,而我们实际用的其实是实球谐,这里的推导只是为了让我们对球谐函数是怎么来的有一个除开调和分析以外的概念,实际上指导意义不大。
写这一部分的时候还是很艰难的,因为我一直以来代数就不好。以及这一部分讨论的这些理论内容,大部分出自数学或物理教材,他们用的符号标记和说法工科出身很难适应,我在图书馆中找了很久,一个很“elementary”的教材是《李群》(邵丹 邵亮 郭紫 著),这本书帮助很大。
至于为什么要坚持写这个部分,这些推导肯定都是不严谨而且以后也用不到的。但通过学习没学过的东西,然后能有选择性的学和跳过哪些内容,从而构建一个逻辑自洽的roadmap,是很重要的一个能力。这个部分只是为了这碟醋包的饺子。我有时就在想,如果当年高等代数课不逃课,事情会不会有所不同。所以这也算是克服心魔了吧。
End
说书人或许会留恋,但故事毕竟有终点。最好的惊堂木是时间, 就让我合上这书卷。
这篇blog选取了若干先进的与相机系统有关的深度学习项目进行了分析,最后从理论角度讨论了代码中常用的旋转操作的一些性质。
#RetNet与ViT完美结合
超越SWin5个点!
作者将RetNet和Transformer结合起来,提出了RMT。在所有模型中,当模型的大小相似并采用相同的策略进行训练时,RMT在Top1准确率方面表现最佳。此外,RMT在目标检测、实例分割和语义分割等下游任务中明显优于现有的视觉Backbone模型。
Transformer首次出现在自然语言处理领域,后来迁移到计算机视觉领域,在视觉任务中表现出出色的性能。然而,最近,Retentive Network(RetNet)作为一种有可能取代Transformer的架构出现,引起了自然语言处理社区的广泛关注。因此,作者提出了一个问题,即将RetNet的思想迁移到视觉领域是否也能为视觉任务带来出色的性能。为了解决这个问题,作者将RetNet和Transformer结合起来,提出了RMT。受RetNet启发,RMT在视觉Backbone中引入了显式衰减,将与空间距离相关的先验知识引入到视觉模型中。这种与距离相关的空间先验允许显式控制每个Token可以关注的Token范围。此外,为了降低全局建模的计算成本,作者沿图像的两个坐标轴分解了这个建模过程。
大量的实验表明,RMT在各种计算机视觉任务中表现出色。例如,RMT仅使用4.5G FLOPs在ImageNet-1k上实现了84.1%的Top1准确率。据作者所知,在所有模型中,当模型的大小相似并采用相同的策略进行训练时,RMT在Top1准确率方面表现最佳。此外,RMT在目标检测、实例分割和语义分割等下游任务中明显优于现有的视觉Backbone模型。
1. 引言
自从Transformer在自然语言处理领域提出以来,它在许多下游任务中取得了出色的表现。尽管计算机视觉和自然语言处理之间存在模态差异,研究人员成功地将这种架构迁移到了视觉任务中,再次给作者带来了与以前的自然语言处理任务一样的巨大惊喜。
最近,自然语言处理领域出现了一些强大的架构,然而还没有任何工作尝试将这些自然语言处理架构转移到视觉任务中。与Transformer相比,RetNet在多个自然语言处理任务中表现出更强的性能。因此,作者希望能够将这个强大的自然语言处理架构RetNet转移到视觉领域。
RetNet中的基本运算符是Retention。Retention和Transformer中的基本的自注意力运算符之间的一个重要区别是引入了衰减系数,这些系数明确地控制每个Token相对于其相邻Token的注意力权重,确保注意力权重随着Token之间的距离增加而衰减。这种衰减有效地将一维距离的先验知识引入到模型中,从而提高了性能。
基于RetNet的研究结果,作者尝试进一步改进Retention机制,使其适应二维形式,并将其引入到视觉任务中。具体来说,原始版本的Retention机制经历了单向衰减,适用于自然语言处理的因果属性。然而,对于没有因果属性的图像,这种单向衰减是不适用的。
因此,作者首先将Retention机制从单向扩展到双向。此外,原始版本的Retention机制设计用于一维顺序信息,不适合在二维空间中使用。因此,考虑到二维空间的空间特性,作者设计了基于二维距离的衰减矩阵。最后,为了解决视觉Backbone早期阶段大量Token带来的高计算负荷问题,作者将二维计算过程分解为沿图像的两个坐标轴分别进行。作者将适用于图像的这种机制命名为Retention自注意力(ReSA)机制。基于ReSA机制,作者构建了RMT系列。
作者通过广泛的实验来证明所提出的方法的有效性。如图1所示,作者的RMT在图像分类任务上明显优于最先进的模型(SOTA)。此外,与其他模型相比,作者的模型在目标检测和实例分割等任务中具有更明显的优势。作者的贡献可以总结如下:
- 作者将Retentive Network的核心机制Retention扩展到了二维情景,引入了与距离相关的空间先验知识到视觉模型中。新的机制被称为Retention自注意力(ReSA)。
- 作者将ReSA沿着两个图像轴进行分解,降低了计算复杂性。这种分解方法有效地减小了计算负担,同时对模型性能的影响最小。
- 广泛的实验证明了RMT的出色性能。特别是在目标检测和实例分割等下游任务中,RMT表现出明显的优势。
2. 相关工作Transformer
Transformer的提出旨在解决递归模型的训练限制,并在许多自然语言处理任务中取得了巨大成功。通过将图像分割为小的、不重叠的Patch序列,视觉Transformer(ViTs)也引起了广泛关注,并在视觉任务中得到了广泛应用。
与过去不同,RNN和CNN分别在自然语言处理和计算机视觉领域占主导地位,而Transformer架构在各种模态和领域中都表现出色。
Transformer中的先验知识
为了增强Transformer模型的性能,已经进行了大量尝试,将先验知识引入其中。最初的Transformer使用三角函数位置编码为每个Token提供位置信息。Swin Transformer提出了使用相对位置编码作为原始的绝对位置编码的替代方法。[Conditional positional encodings]指出卷积层中的零填充也可以为Transformer提供位置感知能力,而这种位置编码方法非常高效。
在许多研究中,ConvFFN已被用来进一步丰富Transformer中的位置信息。此外,在最近的Retentive Network中,引入了显式衰减,为模型提供了基于距离变化的先验知识。
Retentive Network
RetNet提出了用于序列建模的Retention机制。与传统的基于Transformer的模型相比,RetNet中提出的Retention使用显式衰减来建模一维距离的先验知识,这是一种重要区别。它包括三种计算范式,即并行、递归和分块递归。在Retention中,它使用一个衰减矩阵乘以一个权重矩阵,以根据距离先验控制每个Token看到其周围Token的比例。作者也尝试将这一思想扩展到二维空间。
3. 方法
3.1. 初步的Retentive Network
RetNet是一种强大的语言模型架构。本工作提出了用于序列建模的Retention机制。Retention引入了显式衰减到语言模型中,而Transformer没有这个特性。Retention首先以递归方式考虑序列建模问题。它可以写成如下的等式(式1):
在训练过程中,对于并行训练过程,等式(1)可以写成如下的等式(2):
其中是的复共轭,包含了因果Masking和指数衰减,代表了一维序列中的相对距离,这带来了先验知识。基于Retention中的一维显式衰减,作者尝试将其扩展到二维,并将空间先验知识引入视觉模型中。
3.2. Retention自注意力
从单向到双向
由于语言任务的因果性质,RetNet中的Retention是单向的,意味着每个Token只能关注其前面的Token,而不能关注其后的Token。这不适用于没有因果属性的任务,例如图像识别任务。因此,作者首先将Retention扩展到二维,在这种情况下,对于每个Token,其输出变为如下的等式(3):
其中N是Token的数量。该等式可以重排为并行形式,表示为等式(4):
其中BiRetention表示具有双向建模能力的Retention。
从一维到二维
尽管Retention现在具有双向建模的能力,但这种建模能力仍然局限于一维水平,仍然不适用于二维图像。因此,作者进一步将一维Retention扩展到二维。
对于图像,每个Token在平面内具有唯一的二维坐标。对于第n个Token,作者使用来表示其二维坐标。基于每个Token的二维坐标,作者修改矩阵D中的每个元素,使其成为相应位置的Token对之间的曼哈顿距离,完成了从一维到二维衰减系数的转换。矩阵D转化为等式(5):
在Retention中,放弃了Softmax,并用一个门控函数来增加运算符的非线性性。然而,根据作者的实验,这种方法对于视觉模型并没有产生更好的结果。相反,它引入了额外的参数和计算复杂性。因此,作者仍然使用Softmax来引入非线性到作者的模型中。
基于上述步骤,作者的Retention自注意力可以表示为等式(6):
早期阶段的分解式ReSA
目前的ReSA并不完全适用于图像识别任务。这是因为在视觉Backbone网络的早期阶段,存在大量的Token,导致了Attention的计算成本过高。这也是大多数视觉Transformer变种都努力解决的问题。作者的ReSA也遇到了这个问题。因此,作者将ReSA分解为图像的两个轴,具体过程如等式(7)所示:
基于这种ReSA的分解,每个Token的感受野形状如图3所示,与完整ReSA的感受野形状相同。
为了进一步增强ReSA的局部表达能力,作者还引入了一个使用DWConv的局部增强模块:
3.3. 整体架构
作者整个模型的架构如图2所示。与传统的Backbone网络类似,它分为4个阶段。前3个阶段使用了分解的ReSA,而最后一个阶段使用了原始的ReSA。与许多先前的Backbone网络一样,作者在模型中引入了CPE。
- 实验4.1. 图像分类
作者在表1中将RMT与许多最先进的模型进行了比较。表中的结果表明,RMT在所有设置下始终优于先前的模型。具体而言,RMT-S仅使用4.5 GFLOPs就实现了84.1%的Top1准确率。RMT-B也在类似的FLOPs下超越了iFormer 0.4%。
此外,作者的RMT-L模型在使用更少的FLOPs的情况下,将Top1准确率提高了0.6%,超过了MaxViT-B。作者的RMT-T模型也在性能上超越了许多轻量级模型。
4.2. 目标检测和实例分割
表2和表3显示了在RetinaNet和Mask R-CNN中的结果。结果表明,作者的RMT在所有比较中表现最好。对于RetinaNet框架,作者的RMT-T超越了FAT-B2 +1.1 AP,而S/B/L也优于其他方法。
对于具有“1×”schedule的Mask R-CNN,RMT-L超越了最近的InternImage-B +1.8框AP和+1.9maskAP。对于“3× +MS”schedule,RMT-S超越了InternImage-T +1.6框AP和+1.2maskAP。所有以上结果表明,RMT明显优于其同类算法。
4.3. 语义分割
语义分割的结果可以在表4和表5中找到。所有的FLOPs都是以512×2048的分辨率测量的。作者所有的模型在所有比较中都取得了最佳性能。
具体而言,作者的RMT-S在Semantic FPN的情况下超越了Shunted-S +1.2 mIoU。此外,作者的RMT-B在最近的InternImage-S +1.3 mIoU。所有以上结果证明了作者模型在密集预测方面的优越性。
4.4. 消融研究
γ衰减。
作者验证了明确衰减对模型的影响,如表6所示。明确衰减提高了模型的性能。
5. 结论
作者提出了RMT,这是一个集成了Retentive网络和Vision Transformer的视觉Backbone网络。RMT引入了与距离相关的明确衰减,为视觉模型带来了空间先验知识。新机制称为Retentive自注意力(ReSA)。为了降低模型的复杂性,RMT还采用了将ReSA分解为两个轴的方法。大量实验证实了RMT的有效性,特别是在目标检测等下游任务中,RMT表现出明显的优势。
#ALOcc
自适应再出山,精度与速度的完美均衡!
基于视觉的语义占用和流量预测在为自动驾驶等现实世界任务提供时空线索方面发挥着至关重要的作用。现有方法优先考虑更高的精度,以满足这些任务的需求。在这项工作中,通过引入一系列针对3D语义占用预测和流量估计的有针对性的改进来提高性能。首先引入了一种具有深度去噪技术的遮挡感知自适应提升机制,以提高二维到三维特征变换的鲁棒性,减少对深度先验的依赖。其次通过利用共享的语义原型来联合约束2D和3D特征,加强了3D特征与其原始2D模态之间的语义一致性。这与基于置信度和类别的采样策略相辅相成,以应对3D空间中的长尾挑战。为了减轻语义和流量联合预测中的特征编码负担,我们提出了一种基于BEV成本量的预测方法,该方法通过成本量将流量和语义特征联系起来,并采用分类回归监督方案来解决动态场景中不同的流量标度。本文的纯卷积架构框架名为ALOcc,在速度和精度之间实现了最佳权衡,在多个基准测试中取得了最先进的结果。在Occ3D和没有相机可见mask的训练中,我们的ALOcc在RayIoU方面实现了2.5%的绝对增益,同时使用相同的输入大小(256×704)和ResNet-50,以与SOTA速度相当的速度运行。ALOcc在CVPR24占用率和流量预测竞赛中也获得了第二名。
- 开源链接:https://github.com/cdb342/ALOcc
总结来说,本文的主要贡献如下:
- 介绍了一种二维到三维的自适应提升方法,该方法通过自适应权重调整将二维信号转换为遮挡和稀疏区域,同时结合深度去噪以防止收敛到局部最优值。
- 提出了一种基于BEV的成本量方法用于占用流量预测,减轻了多任务设置中的特征编码负担,并通过组合分类和回归增强了流量预测。
- 提出了共享语义原型,将类间关系从2D转移到3D,通过选择性原型训练和不确定性感知采样来缓解类不平衡问题。
- 对多个语义占用和流量预测基准的综合评估表明,与当前的SOTA相比,它有了持续的改进。我们提供多种模型变体,我们的实时版本优于现有的实时方法。
相关工作回顾
语义场景完成(SSC)致力于从给定的输入中重建和语义完成3D场景。早期的研究主要集中在室内场景,预测有限场景中的占用率和语义标签。随后的研究逐渐将注意力转移到复杂的户外环境,尤其是在驾驶环境中。SSC的本质是它能够感知未被观察到的事物,用精确的语义洞察力填补部分观察中的空白。最近的方法,如VoxFormer,采用了一种两阶段的方法,首先预测占用率,然后对占用的片段进行语义预测。OccFormer引入了一个用于3D编码的双路径Transformer子模块,并采用了一种基于查询的分割方法。
基于视觉的3D占用预测领域与SSC密切相关,但强调多视角联合感知,这对自主导航至关重要。占用率预测要求将复杂的3D场景描绘成细粒度的元素,以便熟练地导航动态驾驶环境。TPVFormer等最初的工作采用稀疏点云进行监督,利用空间注意力进行粒度预测。后续的工作,如OccNet、SurroundOcc、Occ3D和Openccupancy,基于时间信息和实例级标签构建了更密集的占用标注。一些研究借鉴了显式几何预测方法来促进二维到三维视图的转换。此外,最近的研究引入了3D占用流预测,该预测侧重于每体素动力学,扩展了3D场景理解的能力。然而,之前的研究缺乏一个连贯的评估框架,通常在孤立的基准(如Occ3D或Opencc)上进行实验,或者使用单一指标(如mIoU或RayIoU)比较性能。在这项工作中,我们提出了一种统一的方法,该方法在语义占用预测和占用流预测任务中都表现出色,并通过综合评估指标进行了验证。
ALOcc方法介绍
Revisiting Depth-based LSS
作为基于视觉的占用预测的核心模块,2D到3D视图转换过程可表述为:
Occlusion-Aware Adaptive Lifting
我们通过引入从表面到遮挡区域的概率转移来增强基于深度的LSS。我们首先用基于概率的软填充方法取代了基于硬舍入的填充策略。如图3所示,我们使用三线性插值将点(x,y,z)的概率扩散到VCS中的八个相邻点上。插值计算八个概率值:
基于深度的LSS方法通过显式深度概率建模指导2D到3D特征转换,表现出色。然而,深度估计的目标遵循分布,这导致大多数权重集中在表面点上。因此,遮挡区域的权重显著降低,导致归纳偏差,限制了模型从这些区域捕获信息的能力。我们的目标是在视图转换过程中弥合可见部分和遮挡部分之间的差距。具体来说,我们解决了两种类型的遮挡:1)同一目标内的遮挡,2)不同目标之间的遮挡。
对于这两种情况,我们构建了一个从可见部分到遮挡部分的概率转移矩阵。对于前一种遮挡,我们设计了条件概率,将表面概率转化为遮挡长度概率。给定像素点x的离散深度概率,我们采用相同的bin划分进行离散遮挡长度预测。我们引入贝叶斯条件概率将离散深度概率转换为离散遮挡长度概率:
因此,我们只需要估计深度di到较大深度位置的条件概率。
深度概率在2D到3D语义转换中的主要作用可能会导致由于初始深度估计不准确而导致的次优模型收敛。为了减轻这种情况,我们引入了一种类似于目标检测算法中查询去噪的去噪操作。该方法利用GT深度概率来指导早期模型训练。我们采用GT和预测深度的加权平均值来指导训练中的自适应lifting过程。地面真实深度的权重初始化为1,并按照余弦退火策略逐渐减小到0。这可以表述为:
Semantic Prototype-based Occupancy Head
在2D到3D特征转换之后,我们使用共享原型增强了2D和3D特征之间的语义对齐。如图4所示,我们为每个类初始化一个语义原型,该原型同时作为2D和3D特征损失计算中的类权重。这种共享原型方法创建了一个链接2D和3D语义的快捷方式,促进了跨维度的一致特征表示。
给定每个类的原型,解码语义占用的直观方法是计算体素特征和原型之间的相似性,然后进行交叉熵监督。然而,由于驾驶场景中语义类别的高度倾斜分布,这种方法不是最优的。为了解决这种不平衡,我们提出了一种原型独立损失,该损失仅考虑每个场景的地面真实占用图中存在的类。
我们通过基础事实提取所有现有语义类别的原型(包括空类的嵌入),并计算这些原型和3D特征之间的内积以生成类掩码。为了进一步加强尾部类别的训练,我们采用了一种基于不确定性和类先验的采样技术。我们使用从内积导出的每原型logit图作为每体素不确定性的度量。这种不确定性和类先验形成多项式分布。然后,我们根据此分布从整个占用图中采样K个体素。损失仅在采样点上计算。最终的3D感知损失被表述为二进制交叉熵和diceA损失的组合:
BEV Cost Volume-based Occupancy Flow Prediction
如图2所示,我们也从编码的体积特征中解码了占用流。基本上,流量网络将占用流量预测为:
具体来说,我们对高度为0到4米的体积特征进行平均,以创建一个以前景为中心的BEV特征,如图5所示。我们对BEV特征进行降采样,以增加感受野,同时减少计算开销。然后,我们使用相机参数将前一帧的BEV特征包裹到当前帧的坐标系中,并在每个点周围的多个假设点处将其与当前帧特征进行匹配。通过计算每对特征之间的余弦相似度来构建成本量:
为了提高我们的模型预测不同尺度流量值的能力,我们提出了一种结合回归和分类的混合方法。我们根据从训练集中得出的最大和最小流量值将流量值划分为多个区间。我们的流量水头预测了流入每个料仓的流量的可能性。连续流量预测公式为bin中心值的加权和:
实验结果
结论
本文探讨了基于视觉的3D语义占用和流量预测的挑战。我们提出了一种基于遮挡感知的自适应提升方法,辅以深度去噪,以提高二维到三维视图转换过程的适应性和鲁棒性。为了进一步改进语义占用学习,我们引入了一种基于语义原型的占用头,该占用头将2D和3D语义对齐,并结合硬样本挖掘技术来缓解长尾问题。此外,我们提出了一种基于BEV成本量的方法来促进占用流学习,减少了同时表示语义和流的特征负担。对Occ3D和Opencc数据集进行的评估表明,我们的方法优于当前的SOTA解决方案。受益于我们方法的轻量级特性,我们提供了多种模型版本:我们性能最高的模型比其他性能相当的方法更快,而我们速度最快的模型比速度相似的方法具有更优的性能。