#UniAD

UniAD是围绕查询设计的,它的感知、预测、规划任务都使用交叉注意力来将前置任务的查询转换为当前任务的查询。每个任务的查询以及这些查询的组合,都用长度为256的向量表征。最初的表征(BEV元素)通过多个Transformer Decoder,每个Decoder的输出有不同任务的监督数据,这些任务引导了中间表示(dim=256的向量)的演化。

在这个过程中,UniAD对表征做了很多组合和变换,引入了冗余的参数。(与之相比,英伟达的ParaDrive则直接从BEV并行训练多任务,取得了更好的效果。)中间表征在演化过程中有多个名字,如下表中形状中有x256的变量,作为每个模块的输入和输出。在开始关注每个模块的代码前,需要牢记下表中每个查询变量的含义和形状。

如果大家对端到端自动驾驶技术栈还不是很熟悉,可以翻一翻我们前面的 端到端技术栈汇总!

对于感知部分,由于已经有很多文章对目标检测、跟踪和建图的论文和代码做过解读,因此我们不再重复。在这篇文章中,我们只对运动预测、占用预测和规划器进行解读。

运动预测模块

这个模块的结构可以在配置文件base_e2e.py中查看。我们忽略了参数部分,只关注模型的前向传播链。我们把写在模块代码的__init__中而不在配置文件中的部分也填充进去,构成完整的前向传播链:

# 配置文件中的前向传播链

motion_head=dict(
        type='MotionHead',
        
        transformerlayers=dict(
            type='MotionTransformerDecoder',

            transformerlayers=dict(
                type='MotionTransformerAttentionLayer',

                attn_cfgs=[
                    dict(
                        type='MotionDeformableAttention',
                    ),
                ],

                operation_order=('cross_attn', 'norm', 'ffn', 'norm')),
        ),
    ),

# 完整的前向传播链

motion_head=dict(
        type='MotionHead',
        
        transformerlayers=dict(
            // 解码器
            type='MotionTransformerDecoder',

            // 对象-目标交互
            intention_interaction_layers = IntentionInteraction()
            // 对象-对象交互
            track_agent_interaction_layers = nn.ModuleList(
            [TrackAgentInteraction() for i in range(self.num_layers)])
            // 对象-地图交互
            map_interaction_layers = nn.ModuleList(
            [MapInteraction() for i in range(self.num_layers)])
            // 对象-目标交互
            bev_interaction_layers = nn.ModuleList(
            [build_transformer_layer(transformerlayers) for i in range(self.num_layers)])

            // 一些对查询进行转换的MLP层
            static_dynamic_fuser = nn.Sequential(
                nn.Linear(self.embed_dims*2, self.embed_dims*2),
                nn.ReLU(),
                nn.Linear(self.embed_dims*2, self.embed_dims),
            )
            dynamic_embed_fuser = nn.Sequential(
                nn.Linear(self.embed_dims*3, self.embed_dims*2),
                nn.ReLU(),
                nn.Linear(self.embed_dims*2, self.embed_dims),
            )
            in_query_fuser = nn.Sequential(
                nn.Linear(self.embed_dims*2, self.embed_dims*2),
                nn.ReLU(),
                nn.Linear(self.embed_dims*2, self.embed_dims),
            )
            out_query_fuser = nn.Sequential(
                nn.Linear(self.embed_dims*4, self.embed_dims*2),
                nn.ReLU(),
                nn.Linear(self.embed_dims*2, self.embed_dims),
            )

            
            transformerlayers=dict(
                // 对象-目标交互
                type='MotionTransformerAttentionLayer',

                attn_cfgs=[
                    dict(
                        type='MotionDeformableAttention',
                    ),
                ],

                operation_order=('cross_attn', 'norm', 'ffn', 'norm')
            ),
        ),
    ),
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.

我们按照UniAD、MotionHead、MotionTransformerDecoder的顺序看前向传播链的forward函数。其中:

  1. UniAD的forward只是简单地串联起了五个模块
  2. MotionHead相当于MotionTransformerDecoder的预处理和后处理,它做了锚点和轨迹首尾点的嵌入,作为MotionTransformerDecoder的输入,之后又将MotionTransformerDecoder的输出查询转换为轨迹点列
  3. MotionTransformerDecoder是最核心的部分,它将锚点和轨迹的嵌入相加构造输入查询,通过三个交叉注意力模块吸收上游模块的查询的信息,再将它们的输出串联并压缩,得到输出查询。

我们对代码中的关键部分进行注释,并省略相对次要的部分。

MotionHead:

这段代码主要实现了以下功能:

  1. 构造并归一化代理级别和场景级别的锚点。
  2. 使用嵌入层将锚点的位置信息转换为嵌入向量。
  3. 通过MotionFormer模型进行前向传播,获取中间状态和参考轨迹。
  4. 计算每个级别的轨迹分数和轨迹,并应用双变量高斯激活。
  5. 构造输出字典,包含轨迹分数、预测轨迹、轨迹查询的有效性掩码等信息,并返回
class MotionHead(BaseMotionHead):

    def forward(self, 
                bev_embed, 
                track_query, 
                lane_query, 
                lane_query_pos, 
                track_bbox_results):
        """
        该函数执行模型的前向传播,用于基于鸟瞰图(BEV)嵌入、轨迹查询、车道查询和轨迹边界框结果进行运动预测。

        参数:

        bev_embed (torch.Tensor):形状为 (h*w, B, D) 的张量,表示鸟瞰图嵌入。
        track_query (torch.Tensor):形状为 (B, num_dec, A_track, D) 的张量,表示轨迹查询。
        lane_query (torch.Tensor):形状为 (N, M_thing, D) 的张量,表示车道查询。
        lane_query_pos (torch.Tensor):形状为 (N, M_thing, D) 的张量,表示车道查询的位置。
        track_bbox_results (List[torch.Tensor]):包含批次中每个图像的跟踪边界框结果的张量列表。
        返回值:

        dict:包含以下键和值的字典:
        'all_traj_scores':形状为 (num_levels, B, A_track, num_points) 的张量,包含每个级别的轨迹分数。
        'all_traj_preds':形状为 (num_levels, B, A_track, num_points, num_future_steps, 2) 的张量,包含每个级别的预测轨迹。
        'valid_traj_masks':形状为 (B, A_track) 的张量,指示轨迹掩码的有效性。
        'traj_query':包含轨迹查询中间状态的张量。
        'track_query':包含输入轨迹查询的张量。s
        'track_query_pos':包含轨迹查询位置嵌入的张量。
        """
        
        ...
        

        # 构造代理级别/场景级别的查询位置嵌入  
        # (num_groups, num_anchor, 12, 2)  
        # 以融入不同组和坐标的信息,并嵌入方向和位置信息 
        agent_level_anchors = self.kmeans_anchors.to(dtype).to(device).view(num_groups, self.num_anchor, self.predict_steps, 2).detach()
        scene_level_ego_anchors = anchor_coordinate_transform(agent_level_anchors, track_bbox_results, with_translation_transform=True)  # B, A, G, P ,12 ,2
        scene_level_offset_anchors = anchor_coordinate_transform(agent_level_anchors, track_bbox_results, with_translation_transform=False)  # B, A, G, P ,12 ,2

        # 对锚点进行归一化
        agent_level_norm = norm_points(agent_level_anchors, self.pc_range)
        scene_level_ego_norm = norm_points(scene_level_ego_anchors, self.pc_range)
        scene_level_offset_norm = norm_points(scene_level_offset_anchors, self.pc_range)

        # 仅使用锚点的最后一个点
        agent_level_embedding = self.agent_level_embedding_layer(
            pos2posemb2d(agent_level_norm[..., -1, :]))  # G, P, D
        scene_level_ego_embedding = self.scene_level_ego_embedding_layer(
            pos2posemb2d(scene_level_ego_norm[..., -1, :]))  # B, A, G, P , D
        scene_level_offset_embedding = self.scene_level_offset_embedding_layer(
            pos2posemb2d(scene_level_offset_norm[..., -1, :]))  # B, A, G, P , D
        
        ...

        outputs_traj_scores = []
        outputs_trajs = []

        # 通过MotionFormer模型进行前向传播
        # 输入各种查询、位置、边界框结果、BEV嵌入、初始参考轨迹等  
        # 以及锚点嵌入和锚点位置嵌入层  
        inter_states, inter_references = self.motionformer(
            track_query,  # B, A_track, D
            lane_query,  # B, M, D
            track_query_pos=track_query_pos,
            lane_query_pos=lane_query_pos,
            track_bbox_results=track_bbox_results,
            bev_embed=bev_embed,
            reference_trajs=init_reference,
            traj_reg_branches=self.traj_reg_branches,
            traj_cls_branches=self.traj_cls_branches,
            # anchor embeddings 
            agent_level_embedding=agent_level_embedding,
            scene_level_ego_embedding=scene_level_ego_embedding,
            scene_level_offset_embedding=scene_level_offset_embedding,
            learnable_embed=learnable_embed,
            # anchor positional embeddings layers
            agent_level_embedding_layer=self.agent_level_embedding_layer,
            scene_level_ego_embedding_layer=self.scene_level_ego_embedding_layer,
            scene_level_offset_embedding_layer=self.scene_level_offset_embedding_layer,
            spatial_shapes=torch.tensor(
                [[self.bev_h, self.bev_w]], device=device),
            level_start_index=torch.tensor([0], device=device))

        # 遍历每个级别,计算轨迹分数和轨迹
        for lvl in range(inter_states.shape[0]):
            outputs_class = self.traj_cls_branches[lvl](inter_states[lvl])
            tmp = self.traj_reg_branches[lvl](inter_states[lvl])
            tmp = self.unflatten_traj(tmp)
            
            # 使用累积和技巧来获取轨迹
            tmp[..., :2] = torch.cumsum(tmp[..., :2], dim=3)

            outputs_class = self.log_softmax(outputs_class.squeeze(3))
            outputs_traj_scores.append(outputs_class)

            # 对每个批次应用双变量高斯激活
            for bs in range(tmp.shape[0]):
                tmp[bs] = bivariate_gaussian_activation(tmp[bs])
            outputs_trajs.append(tmp)

        # 堆叠并输出轨迹分数和轨迹
        outputs_traj_scores = torch.stack(outputs_traj_scores)
        outputs_trajs = torch.stack(outputs_trajs)

        # 获取轨迹查询的有效性掩码
        B, A_track, D = track_query.shape
        valid_traj_masks = track_query.new_ones((B, A_track)) > 0

        # 构造输出字典
        outs = {
            'all_traj_scores': outputs_traj_scores,
            'all_traj_preds': outputs_trajs,
            'valid_traj_masks': valid_traj_masks,
            'traj_query': inter_states,
            'track_query': track_query,
            'track_query_pos': track_query_pos,
        }

        return outs
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.

MotionTransformerDecoder:

这段代码融合了静态意图、动态意图、代理之间的交互、代理与地图的交互以及代理与目标的交互,最终生成了融合了多种信息的查询嵌入query_embed。这里意图(intention)代表目标位置。

class MotionTransformerDecoder(BaseModule):

    def forward(self,
                track_query,
                lane_query,
                track_query_pos=None,
                lane_query_pos=None,
                track_bbox_results=None,
                bev_embed=None,
                reference_trajs=None,
                traj_reg_branches=None,
                agent_level_embedding=None,
                scene_level_ego_embedding=None,
                scene_level_offset_embedding=None,
                learnable_embed=None,
                agent_level_embedding_layer=None,
                scene_level_ego_embedding_layer=None,
                scene_level_offset_embedding_layer=None,
                **kwargs):
        """Forward function for `MotionTransformerDecoder`.
        Args:
            agent_query (B, A, D):代理查询,其中 B 表示批次大小,A 表示代理(agent)的数量,D 表示特征维度。
            map_query (B, M, D):地图查询,其中 M 表示地图中的对象数量。
            map_query_pos (B, G, D):地图查询位置。
            static_intention_embed (B, A, P, D):静态意图嵌入,其中 P 表示意图的数量。代表每个代理的静态或固定的意图。
            offset_query_embed (B, A, P, D):偏移查询嵌入,与意图的偏移或变化有关。
            global_intention_embed (B, A, P, D):全局意图嵌入,代表每个代理的全局或整体意图。
            learnable_intention_embed (B, A, P, D):可学习的意图嵌入,是模型在训练过程中学习的意图表示。
            det_query_pos (B, A, D):检测查询位置,代表与检测任务相关的位置信息。
        Returns:
            None
        """
        intermediate = []    # 用于存储中间输出的列表  
        intermediate_reference_trajs = []    # 用于存储中间参考轨迹的列表

        # 对输入进行广播和扩展,以匹配所需的形状
        B, _, P, D = agent_level_embedding.shape
        track_query_bc = track_query.unsqueeze(2).expand(-1, -1, P, -1)  # (B, A, P, D)
        track_query_pos_bc = track_query_pos.unsqueeze(2).expand(-1, -1, P, -1)  # (B, A, P, D)

        # 计算静态意图嵌入,它在所有层中都是不变的
        agent_level_embedding = self.intention_interaction_layers(agent_level_embedding)
        static_intention_embed = agent_level_embedding + scene_level_offset_embedding + learnable_embed
        reference_trajs_input = reference_trajs.unsqueeze(4).detach()

        # 初始化查询嵌入,其形状与静态意图嵌入相同  
        query_embed = torch.zeros_like(static_intention_embed)
        for lid in range(self.num_layers):
            # 融合动态意图嵌入  
            # 动态意图嵌入是前一层的输出,初始化为锚点嵌入(anchor embedding)
            dynamic_query_embed = self.dynamic_embed_fuser(torch.cat(
                [agent_level_embedding, scene_level_offset_embedding, scene_level_ego_embedding], dim=-1))
            
            # 融合静态和动态意图嵌入  
            query_embed_intention = self.static_dynamic_fuser(torch.cat(
                [static_intention_embed, dynamic_query_embed], dim=-1))  # (B, A, P, D)
            
            # 将意图嵌入与查询嵌入融合
            query_embed = self.in_query_fuser(torch.cat([query_embed, query_embed_intention], dim=-1))
            
            # 代理之间的交互
            track_query_embed = self.track_agent_interaction_layers[lid](
                query_embed, track_query, query_pos=track_query_pos_bc, key_pos=track_query_pos)
            
            # 代理与地图之间的交互
            map_query_embed = self.map_interaction_layers[lid](
                query_embed, lane_query, query_pos=track_query_pos_bc, key_pos=lane_query_pos)
            
            # 代理与目标(BEV,即鸟瞰图)之间的交互,使用可变形Transformer实现  
            # implemented with deformable transformer
            bev_query_embed = self.bev_interaction_layers[lid](
                query_embed,
                value=bev_embed,
                query_pos=track_query_pos_bc,
                bbox_results=track_bbox_results,
                reference_trajs=reference_trajs_input,
                **kwargs)
            
            # 融合来自不同交互层的嵌入
            query_embed = [track_query_embed, map_query_embed, bev_query_embed, track_query_bc+track_query_pos_bc]
            query_embed = torch.cat(query_embed, dim=-1)
            query_embed = self.out_query_fuser(query_embed)
            ...
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.

占用预测:

占用预测模块由5层Transformer构成,每层有一个自注意力和一个交叉注意力模块:

这部分的代码十分简单,因为DetrTransformerDecoder和DetrTransformerDecoderLayer是mmdet3d实现的,这里只是简单调用这个解码器和解码器层。这个模块的配置和前向传播如下,它主要实现了占用预测的任务,通过Transformer解码器对输入的特征图进行处理,生成未来的状态、掩码预测和占用逻辑。

occ_head=dict(
        type='OccHead',

        # Transformer
        transformer_decoder=dict(
            type='DetrTransformerDecoder',
            
            num_layers=5,
            transformerlayers=dict(
                type='DetrTransformerDecoderLayer',
                attn_cfgs=dict(
                    type='MultiheadAttention',
                    ),

                operation_order=('self_attn', 'norm', 'cross_attn', 'norm',
                                 'ffn', 'norm')),
            ),

    ),



class OccHead(BaseModule):
    def forward(self, x, ins_query):
        # 重新排列输入特征图 
        base_state = rearrange(x, '(h w) b d -> b d h w', h=self.bev_size[0])

        # 对特征图进行采样 
        base_state = self.bev_sampler(base_state)
        # 对特征图进行轻量级投影
        base_state = self.bev_light_proj(base_state)
        # 对特征图进行下采样
        base_state = self.base_downscale(base_state)
        base_ins_query = ins_query

        # 初始化最后的状态和查询
        last_state = base_state
        last_ins_query = base_ins_query
        future_states = []    # 存储未来的状态
        mask_preds = []        # 存储掩码预测
        temporal_query = []    # 存储时间查询
        temporal_embed_for_mask_attn = []    # 存储用于掩码注意力的时间嵌入

        # 计算每个块的Transformer层数
        n_trans_layer_each_block = self.num_trans_layers // self.n_future_blocks
        assert n_trans_layer_each_block >= 1
        
        # 遍历未来的块
        for i in range(self.n_future_blocks):
            # 下采样
            cur_state = self.downscale_convs[i](last_state)  # /4 -> /8

            # 注意力
            # 时间感知的ins_query
            cur_ins_query = self.temporal_mlps[i](last_ins_query)  # [b, q, d]
            temporal_query.append(cur_ins_query)

            # 生成注意力掩码 
            attn_mask, mask_pred, cur_ins_emb_for_mask_attn = self.get_attn_mask(cur_state, cur_ins_query)
            attn_masks = [None, attn_mask] 

            mask_preds.append(mask_pred)  # /1
            temporal_embed_for_mask_attn.append(cur_ins_emb_for_mask_attn)

            # 重新排列状态和查询
            cur_state = rearrange(cur_state, 'b c h w -> (h w) b c')
            cur_ins_query = rearrange(cur_ins_query, 'b q c -> q b c')

            # 遍历Transformer层
            for j in range(n_trans_layer_each_block):
                trans_layer_ind = i * n_trans_layer_each_block + j
                trans_layer = self.transformer_decoder.layers[trans_layer_ind]
                cur_state = trans_layer(
                    query=cur_state,  # [h'*w', b, c]
                    key=cur_ins_query,  # [nq, b, c]
                    value=cur_ins_query,  # [nq, b, c]
                    query_pos=None,  
                    key_pos=None,
                    attn_masks=attn_masks,
                    query_key_padding_mask=None,
                    key_padding_mask=None
                )  # out size: [h'*w', b, c]

            # 重新排列状态
            cur_state = rearrange(cur_state, '(h w) b c -> b c h w', h=self.bev_size[0]//8)
            
            # 上采样到/4
            cur_state = self.upsample_adds[i](cur_state, last_state)

            # 输出
            future_states.append(cur_state)  # [b, d, h/4, w/4]
            last_state = cur_state

        # 堆叠未来的状态、时间查询、掩码预测和查询嵌入
        future_states = torch.stack(future_states, dim=1)  # [b, t, d, h/4, w/4]
        temporal_query = torch.stack(temporal_query, dim=1)  # [b, t, q, d]
        mask_preds = torch.stack(mask_preds, dim=2)  # [b, q, t, h, w]
        ins_query = torch.stack(temporal_embed_for_mask_attn, dim=1)  # [b, t, q, d]

        # 将未来状态解码到更大的分辨率
        future_states = self.dense_decoder(future_states)
        ins_occ_query = self.query_to_occ_feat(ins_query)    # [b, t, q, query_out_dim]
        
        # 生成最终输出
        ins_occ_logits = torch.einsum("btqc,btchw->bqthw", ins_occ_query, future_states)
        
        return mask_preds, ins_occ_logits
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.

规划模块

规划模块是3层解码器,在图中由下到上依次为:

  1. 构造查询Q:由跟踪和运动预测模块的输出查询和导航信息的嵌入相加,经过MLP,选择概率最大的单个轨迹(从6个轨迹中),最后加上位置编码
  2. 通过N层解码器融合BEV信息,得到轨迹
  3. 使用占用预测做碰撞优化

代码也比较简单,这个模块的配置和前向传播如下。它实现了基于BEV特征嵌入、占用掩码、驾驶命令等输入,生成SDC(自动驾驶车辆)轨迹的过程。

planning_head=dict(
        type='PlanningHeadSingleMode',
        embed_dims=256,

        planning_steps=planning_steps,
        loss_planning=dict(type='PlanningLoss'),
        loss_collisinotallow=[dict(type='CollisionLoss', delta=0.0, weight=2.5),
                        dict(type='CollisionLoss', delta=0.5, weight=1.0),
                        dict(type='CollisionLoss', delta=1.0, weight=0.25)],
        use_col_optim=use_col_optim,
        planning_eval=True,
        with_adapter=True,
    ),


class PlanningHeadSingleMode(nn.Module):  
    def forward(self,   
                bev_embed,  # BEV(鸟瞰图)特征嵌入  
                occ_mask,   # 占用实例掩码  
                bev_pos,    # BEV位置  
                sdc_traj_query, # SDC轨迹查询  
                sdc_track_query, # SDC轨迹追踪查询  
                command):   # 驾驶命令  
        """  
        前向传播过程。  
  
        参数:  
            bev_embed (torch.Tensor): 鸟瞰图特征嵌入。  
            occ_mask (torch.Tensor): 占用实例掩码。  
            bev_pos (torch.Tensor): BEV位置。  
            sdc_traj_query (torch.Tensor): SDC轨迹查询。  
            sdc_track_query (torch.Tensor): SDC轨迹追踪查询。  
            command (int): 驾驶命令。  
  
        返回:  
            dict: 包含SDC轨迹和所有SDC轨迹的字典。  
        """  
  
        ...  
  
        # 根据驾驶命令获取导航嵌入  
        navi_embed = self.navi_embed.weight[command]  
        navi_embed = navi_embed[None].expand(-1, P, -1)  
        # 融合SDC轨迹查询、SDC轨迹追踪查询和导航嵌入  
        plan_query = torch.cat([sdc_traj_query, sdc_track_query, navi_embed], dim=-1)  
  
        # 使用多层感知机(MLP)融合查询,并取最大值  
        plan_query = self.mlp_fuser(plan_query).max(1, keepdim=True)[0]  
        # 重排plan_query的形状  
        plan_query = rearrange(plan_query, 'b p c -> p b c')  
          
        # 重排bev_pos的形状  
        bev_pos = rearrange(bev_pos, 'b c h w -> (h w) b c')  
        bev_feat = bev_embed + bev_pos  
          
        # 插件适配器  
        if self.with_adapter:  
            bev_feat = rearrange(bev_feat, '(h w) b c -> b c h w', h=self.bev_h, w=self.bev_w)  
            bev_feat = bev_feat + self.bev_adapter(bev_feat)  # 残差连接  
            bev_feat = rearrange(bev_feat, 'b c h w -> (h w) b c')  
          
        # 添加位置嵌入  
        pos_embed = self.pos_embed.weight  
        plan_query = plan_query + pos_embed[None]  
          
        # 使用注意力模块处理plan_query和bev_feat  
        plan_query = self.attn_module(plan_query, bev_feat)  
          
        # 回归分支,生成SDC轨迹  
        sdc_traj_all = self.reg_branch(plan_query).view((-1, self.planning_steps, 2))  
        # 累计求和,生成轨迹点  
        sdc_traj_all[...,:2] = torch.cumsum(sdc_traj_all[...,:2], dim=1)  
        # 对第一条轨迹应用双变量高斯激活  
        sdc_traj_all[0] = bivariate_gaussian_activation(sdc_traj_all[0])  
        # 如果使用碰撞优化且非训练模式,进行后处理  
        if self.use_col_optim and not self.training:  
            assert occ_mask is not None  
            sdc_traj_all = self.collision_optimization(sdc_traj_all, occ_mask)  
          
        # 返回SDC轨迹和所有SDC轨迹  
        return dict(  
            sdc_traj=sdc_traj_all,  
            sdc_traj_all=sdc_traj_all,  
        )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.

写在最后的话:

UniAD在自动驾驶的每个子任务中使用交叉注意力,显式指定了信息流向,且只能利用nuScenes类的封闭数据集。与之相对的,基于多模态基础模型的自动驾驶架构只使用自注意力层,将视觉信息映射为tokens,和多模态prompt的tokens一起输入自注意力层,多模态prompt隐式指定了信息流向,基础模型也已经用开放数据集进行了预训练。这是一种与UniAD互补的思路。




#Depth Anything v2

论文标题:

Depth Anything V2

论文作者:

Lihe Yang, Bingyi Kang, Zilong Huang, Zhen Zhao, Xiaogang Xu, Jiashi Feng, Hengshuang Zhao

项目地址: https://depth-anything-v2.github.io/

导读:


在单目深度估计研究中,广泛使用的标记真实图像具有很多局限性,因此需要借助合成图像来确保精度。为了解决合成图像引起的泛化问题,作者团队采用了数据驱动(大规模伪标记真实图像)和模型驱动(扩大教师模型)的策略。同时在一个现实世界的应用场景中,展示了未标记真实图像的不可或缺的作用,证明“精确合成数据+伪标记真实数据”比标记的真实数据更有前景。最后,研究团队将可转移经验从教师模型中提炼到更小的模型中,这类似于知识蒸馏的核心精神,证明了伪标签蒸馏更加容易和安全。©️【深蓝AI】编译

这项工作展示了Depth Anything V2, 在不追求技巧的情况下,该项研究的目标是为建立一个强大的单目深度估计模型奠定基础。值得注意的是,与V1相比,这个版本通过三个关键实践产生了更精细,更强大的深度预测:

●用合成图像替换所有标记的真实图像;


●扩大教师模型的能力;


●通过大规模伪标记真实图像的桥梁教授学生模型。

与建立在Stable Diffusion上最新的模型相比,Depth Anything v2的模型效率更高更准确。作者提供不同规模的模型(从25M到1.3B参数),以支持广泛的场景。得益于强大的泛化能力,研究团队使用度量标签对模型进行微调,以获得度量深度模型。除了模型本身之外,考虑到当前测试集的有限多样性和频繁的噪声,研究团队构建了一个具有精确注释和多样化场景的多功能评估基准,以方便未来的研究。

单目深度估计(Monocular Depth Estimation,MDE)因其在广泛的下游任务中的重要作用而受到越来越多的关注。精确的深度信息不仅在经典应用中是有利的,例如3D重建,导航和自动驾驶,而且在其他生成场景中也是可应用的。

从模型建构方面来看,已有的MDE模型可以分为两类,一类基于判别模型,另一类基于生成模型,从图1的比较结果,Depthing Anything是更高效轻巧的。根据表1可得,Depth Anything V2可以实现复杂场景的可靠预测,包括且不局限于复杂布局、透明对象、反射表面等;在预测的深度图中包含精细的细节,包括但不限于薄物体、小孔等;提供不同的模型规模和推理效率,以支持广泛的应用;具有足够的可推广性,可以转移到下游任务。从Depth Anything v1出发,研究团队推出v2,认为最关键的部分仍然是数据,它利用大规模未标记的数据来加速数据扩展并增加数据覆盖率。研究团队进一步构建了一个具有精确注释和多样化场景的多功能评估基准。

51c自动驾驶~合集3_配置文件

▲图1|Depthing Anything v2与其他模型比较

51c自动驾驶~合集3_锚点_02

▲表1|强大的单目深度估计模型的优选特性

重新审视Depth Anything V1标记数据的设计,如此大量的标记图像真的有利吗?真实标记的数据有2个缺点:一个是标签噪声,即深度图中的标签不准确。由于各种收集程序固有的局限性,真实标记数据不可避免地包含不准确的估计,例如无法捕捉透明物体的深度,立体匹配算法以及SFM算法在处理动态物体或异常值时受到的影响。另一个是细节忽略,一些真实数据通常会忽略深度图中的某些细节,例如树和椅子的深度往往表示非常粗糙。为了克服这些问题,研究者决定改变训练数据,寻找具有最好注释的图像,专门利用具有深度信息的合成图像进行训练,广泛检查合成图像的标签质量。

合成图像具有以下优势:

●所有精细细节都会得到正确标记,如图2所示;


●可以获得具有挑战性的透明物体和反射表面的实际深度,如图2中的花瓶。

51c自动驾驶~合集3_2d_03

▲图2|合成数据的深度

但是合成数据仍然也具有以下局限性:

●合成图像与真实图像之间存在分布偏差。尽管当前的图像引擎力求达到照片级逼真的效果,但其风格和颜色分布与真实图像仍存在明显差异。合成图像的颜色过于“干净”,布局过于“有序”,而真实图像则包含更多随机性;


●合成图像的场景覆盖范围有限。它们是从具有预定义固定场景类型的图形引擎迭代采样的,例如“客厅”和“街景”。

因此在MDE中,从合成图像到真实图像的迁移并非易事。为了缓解泛化问题,一些工作使用真实图像和合成图像的组合训练集,但是真实图像的粗深度图对细粒度预测具有破坏性。另一个潜在的解决方案是收集更多的合成图像,但是这是不可持续的。因此,在本文中,研究者提出一个路线图可以在不进行任何权衡的情况下解决精确性和鲁棒性困境,并且适用于任何模型规模。

51c自动驾驶~合集3_锚点_04

▲图3|对不同视觉编码器在合成到真实转换方面的定性比较

研究团队提出的解决方案是整合未标记的真实图像。团队最强大的MDE模型基于DINOV2-G,最初仅使用高质量合成图像进行训练,然后它在未标记的真实图像上分配伪深度标签,最后仅使用大规模且精确的伪标记图像进行训练。Depth Anything v1凸显了大规模无标记真实数据的重要性。针对合成标记图像的缺点,阐述整合未标记真实图像的作用:

●弥补差距:由于分布偏移,直接从合成训练图像转移到真实测试图像具有挑战性。但是如果可以利用额外的真实图像作为中间学习目标,这个过程将更加可靠。直观地讲,在对伪标记真实图像进行明确训练后,模型可以更熟悉真实世界的数据分布。与手动注释的图像相比,自动生成的伪标签细粒度和完整度更高。

●增强场景覆盖率:合成图像的多样性有限,没有包含足够的真实场景。然而可以通过合并来自公共数据集的大规模未标记图像轻松覆盖大量不同的场景。此外,由于合成图像是从预定义视频中重复采样的,因此确实非常冗余。相比之下,未标记的真实图像清晰可辨,信息量丰富。通过在足够的图像和场景上训练,模型不仅表现出更强的零样本MDE能力,而且还可以作为下游相关任务更好的训练源。

●将经验从最强大的模型转移到较小的模型:如图5所示,较小的模型本身无法直接从合成到真实的迁移中受益。然而,有了大规模未标记的真实图像,可以学习模仿更强大的模型的高质量预测,类似于知识蒸馏。


51c自动驾驶~合集3_2d_05

▲图4|Depth Anything v2

■3.1 整体框架

基于以上分析,训练Depth Anything v2的流程如下:

●基于高质量合成图像训练基于DINOv2-G的可靠教师模型;


●在大规模未标记的真实图像上产生精确的伪深度;


●在伪标记的真实图像上训练最终的学生模型,实现稳健的泛化。

研究团队发布4种学生模型,分别基于DINOv2的小型,基础,大型和巨型模型。

■3.2 细节

如表2所示,使用5个精确合成的数据集和8个大规模伪标记真实数据集进行训练。与V1相同,对于每个伪标记样本,忽略top-n-largest-loss最大区域,n设为10%。同时,模型可以产生仿射不变的逆深度,因为模型使用2个损失项对标记图像进行优化,分别是平移不变损失和梯度匹配损失。其中梯度匹配损失在使用合成图像时,对深度清晰度优化非常有效。在伪标记图像上,遵循V1添加额外的特征对齐损失,以保留来自预训练的DINOv2编码器的信息语义。

51c自动驾驶~合集3_配置文件_06

▲表2|训练数据集

■3.3 DA-2K

考虑到已有噪声数据的限制,该研究的目标是构建一个通用的相对单目深度估计评估基准。该基准可以:

●提供精确的深度关系;


●覆盖广泛的场景;


●包含大多数适合现代使用的高分辨率图像。

事实上,人类很难标注每个像素的深度,尤其是对于自然图像,因此研究员为每个图像标注稀疏深度。通常,给定一幅图像,可以选择其中的2个像素,并确定它们之间的相对深度。

51c自动驾驶~合集3_2d_07

▲图5|DA-2K

具体来说,可以采用2个不同的管道来选择像素对。在第一个管道中,如图5(a)所示,使用SAM自动预测对象掩码。但是可能存在模型预测的情况,引入第二个管道,仔细分析图像并手动识别具有挑战性的像素对。DA-2K并不能取代当前的基准,它只是作为准确密集深度的先决条件。

与Depth Anything v1一样,使用DPT作为深度解码器,并且基于DINO v2编码器构造。所有图像均裁剪到518进行训练,在合成图像上训练教师模型时,使用64的批处理大小进行160k次迭代。在伪标记真实图像上训练的第三阶段,该模型使用192的批处理大小进行480k次迭代。使用Adam优化器,分别将编码器和解码器的学习率设置为5e-5和5e-6。

51c自动驾驶~合集3_配置文件_08

▲表3|零样本深度估计

51c自动驾驶~合集3_配置文件_09

▲表4|DA-2K评估基准上的性能

如表3所示,结果优于MiDaS,稍逊于V1。然而,v2本身是针对薄结构进行细粒度预测,对复杂场景和透明物体进行稳健预测。这些维度的改进无法正确反映在当前的基准测试中。而在DA-2K的测试上,即使是最小的模型也明显优于其他基于SD的大模型。提出的最大模型在相对深度辨别方面的准确率比Margold高出10.6%.

51c自动驾驶~合集3_配置文件_10

▲表5|将Depth Anything V2预训练编码器微调至域内度量深度估计,即训练和测试图像共享同一域。所有比较方法都使用接近ViT-L的编码器大小

如表5所示,将编码器转移到下游的度量深度估计任务上,在NYU-D和KITTI数据集上都比之前的方法取得了显著改进,值得注意的是,即使是最轻量级的基于ViT-S的模型。

51c自动驾驶~合集3_锚点_11

▲表6|伪标记真实图像上的重要性

如表6所示,消融实验证明了大规模伪标记真实图像的重要性。与仅使用合成图像进行训练相比,模型通过结合伪标记真实图像得到了极大的增强。

在本研究中,作者提出了Depth Anything v2,一种更强大的单目深度估计基础模型。它能够:

●提供稳健且细粒度更大的深度预测;


●支持具有各种模型大小(从25M到1.3B参数)的广泛应用;


●可轻松微调到下游任务,可以作为有效的模型初始化。


研究团队揭示了这项关键发现,此外,考虑到现有测试集中多样性弱,噪声强的特点,团队构建了一个多功能评估基准DA-2K,涵盖具有精确且具有挑战性的稀疏深度标签的各种高分辨率图像。



欢迎大家一起 扣 ~裙研究~~ 64104075

whaosoft aiot 天皓智联