Bird's Eye View Generation via Lift-Splat-Shoot in Autonomous Driving

Introduction

Bird's Eye View (BEV) representation is critical for autonomous driving perception tasks. Two primary approaches exist: explicit depth estimation methods (top-down) and transformer-based BEV query techniques (bottom-up). The Lift-Splat-Shoot (LSS) framework provides an end-to-end solutoin for multi-sensor fusion by eliminating post-processing steps and enabling direct feature transformation.

Lift Operation

Key perception parameters define the operational space:

img_height = 128
img_width = 352
x_range = [-50.0, 50.0, 0.5]
y_range = [-50.0, 50.0, 0.5]
z_range = [-10.0, 10.0, 20.0]
depth_bins = [4.0, 45.0, 1.0]
feat_height = img_height // 16
feat_width = img_width // 16

Frustum Construction

A 3D viewing volume is created using discrete depth values:

def generate_view_volume(self):
    depth_values = torch.arange(*self.config['depth_bins'], dtype=torch.float).view(-1, 1, 1)
    depth_values = depth_values.expand(-1, self.feat_h, self.feat_w)
    
    x_coords = torch.linspace(0, self.orig_width-1, self.feat_w, dtype=torch.float)
    x_coords = x_coords.view(1, 1, -1).expand(depth_values.shape[0], self.feat_h, self.feat_w)
    
    y_coords = torch.linspace(0, self.orig_height-1, self.feat_h, dtype=torch.float)
    y_coords = y_coords.view(1, -1, 1).expand(depth_values.shape[0], self.feat_h, self.feat_w)
    
    volume = torch.stack((x_coords, y_coords, depth_values), -1)
    return volume

Camera Feature Encoding

Image features are extracted and combined with depth distributions:

class CameraEncoder(nn.Module):
    def __init__(self, depth_bins, feat_channels, reduction_factor):
        super().__init__()
        self.depth_bins = depth_bins
        self.feat_channels = feat_channels
        
        self.backbone = EfficientNet.from_pretrained("efficientnet-b0")
        self.upsample = Up(320+112, 512)
        self.feature_net = nn.Conv2d(512, depth_bins + feat_channels, kernel_size=1)
    
    def get_depth_weights(self, x):
        return x.softmax(dim=1)
    
    def forward(self, x):
        base_features = self.backbone(x)
        features = self.upsample(base_features)
        output = self.feature_net(features)
        
        depth_weights = self.get_depth_weights(output[:, :self.depth_bins])
        spatial_features = depth_weights.unsqueeze(1) * output[:, self.depth_bins:].unsqueeze(2)
        return spatial_features

Splat Operation

Coordinate Transformation

Image coordinates are projected into 3D ego space:

def project_to_ego(self, rotations, translations, intrinsics, post_rots, post_trans):
    batch_size, num_cams, _ = translations.shape
    adjusted_points = self.view_volume - post_trans.view(batch_size, num_cams, 1, 1, 1, 3)
    
    inverse_rots = torch.inverse(post_rots).view(batch_size, num_cams, 1, 1, 1, 3, 3)
    adjusted_points = inverse_rots.matmul(adjusted_points.unsqueeze(-1)).squeeze(-1)
    
    combined_transform = rotations.matmul(torch.inverse(intrinsics))
    ego_points = combined_transform.view(batch_size, num_cams, 1, 1, 1, 3, 3).matmul(adjusted_points.unsqueeze(-1))
    ego_points = ego_points.squeeze(-1) + translations.view(batch_size, num_cams, 1, 1, 1, 3)
    
    return ego_points

Feature Pooling

Features are aggregated into voxel grids:

def pool_features(self, ego_coords, features):
    batch_size, num_cams, depth_bins, feat_h, feat_w, channels = features.shape
    total_points = batch_size * num_cams * depth_bins * feat_h * feat_w
    
    flat_features = features.view(total_points, channels)
    voxel_coords = ((ego_coords - (self.voxel_origin - self.voxel_size/2)) / self.voxel_size).long()
    flat_coords = voxel_coords.view(total_points, 3)
    
    batch_indices = torch.cat([torch.full([total_points//batch_size, 1], i) for i in range(batch_size)])
    combined_coords = torch.cat((flat_coords, batch_indices), 1)
    
    valid_mask = (combined_coords[:, 0] >= 0) & (combined_coords[:, 0] < self.grid_size[0]) & \
                 (combined_coords[:, 1] >= 0) & (combined_coords[:, 1] < self.grid_size[1]) & \
                 (combined_coords[:, 2] >= 0) & (combined_coords[:, 2] < self.grid_size[2])
    
    filtered_features = flat_features[valid_mask]
    filtered_coords = combined_coords[valid_mask]
    
    sorting_key = filtered_coords[:, 0] * (self.grid_size[1] * self.grid_size[2] * batch_size) + \
                  filtered_coords[:, 1] * (self.grid_size[2] * batch_size) + \
                  filtered_coords[:, 2] * batch_size + \
                  filtered_coords[:, 3]
    
    sorted_indices = sorting_key.argsort()
    sorted_features = filtered_features[sorted_indices]
    sorted_coords = filtered_coords[sorted_indices]
    
    pooled_voxels = torch.zeros((batch_size, channels, self.grid_size[2], self.grid_size[0], self.grid_size[1]))
    pooled_voxels[sorted_coords[:, 3], :, sorted_coords[:, 2], sorted_coords[:, 0], sorted_coords[:, 1]] = sorted_features
    
    return pooled_voxels

BEV Feature Processing

The final BEV features are processed through an encoder network:

class BEVEncoder(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(64, 64, kernel_size=7, stride=2, padding=3)
        self.norm1 = nn.BatchNorm2d(64)
        self.activation = nn.ReLU()
        
        self.block1 = ResidualBlock(64, 64)
        self.block2 = ResidualBlock(64, 128, stride=2)
        self.block3 = ResidualBlock(128, 256, stride=2)
        
        self.upsample1 = UpsampleBlock(256, 64)
        self.upsample2 = UpsampleBlock(64, 1)
    
    def forward(self, x):
        x = self.activation(self.norm1(self.conv1(x)))
        x1 = self.block1(x)
        x = self.block2(x1)
        x = self.block3(x)
        x = self.upsample1(x, x1)
        return self.upsample2(x)

Advantages and Limitations

LSS provides a unified approach for multi-sensor BEV feature extraction but has notable dependencies:

  • Advantages: Enables integrated perception tasks in BEV space and supports multi-modal sensor fusion
  • Limitations: Requires accurate depth estimation and has significant computational requirements

Tags: BEV Lift-Splat-Shoot autonomous-driving depth-estimation voxel-pooling

Posted on Mon, 07 Sep 2026 16:28:05 +0000 by wannalearnit