空间智能是 AI 的下一个前沿——不只是看懂图片里的内容,还要理解三维空间中的物体位置、空间关系、深度感知。cann-recipes-spatial-intelligence 覆盖三大应用:3D 目标检测(自动驾驶/机器人)、NeRF/3DGS(神经辐射场/高斯泼溅,新型三维重建)、SLAM(同步定位与地图构建)。三个方向的共性:输入是图像或视频,输出是空间中的几何信息。

三类空间智能任务

任务 输入 输出 模型 核心算子
3D 目标检测 多视角相机图像 目标 3D 边界框 BEVFormer / PointPillars 视角变换、3D 卷积
NeRF 神经辐射场 多视角图像 + 相机位姿 场景隐式表示 NeRF / 3D Gaussian Splatting 体密度查询、MipNeRF360
SLAM 连续帧图像 + IMU 相机轨迹 + 3D 地图 ORB-SLAM3 / Direct SLAM 光流、特征匹配、BA

三类任务在 NPU 上的算子需求完全不同——3D 检测依赖大量矩阵乘和 BEV 视角变换,NeRF 依赖体积渲染(Ray Marching),SLAM 依赖特征匹配和几何优化。

3D 目标检测:BEVFormer 的视角变换

BEV(Bird Eye View,鸟瞰图)感知是自动驾驶的核心——把前视相机的图像转成俯视图上的目标检测。

// cann-recipes-spatial-intelligence/kernels/bev_transform.cpp

// BEVFormer 的核心:Spatial Cross-Attention
// 把图像特征投影到 BEV 空间,然后做 self-attention

__aicore__ void SpatialCrossAttention(
    GlobalTensor<float>& bev_query,      // [B, H_bev, W_bev, C]
    GlobalTensor<float>& image_features,  // [B, N_cam, C, H_img, W_img]
    GlobalTensor<float>& camera_K,      // 内参矩阵 [B, N_cam, 3, 3]
    GlobalTensor<float>& camera_R,      // 外参矩阵 [B, N_cam, 3, 3]
    GlobalTensor<float>& camera_T,      // 外参平移向量 [B, N_cam, 3]
    GlobalTensor<float>& grid,          // BEV 网格坐标 [H_bev, W_bev, 3]
    int B, int N_cam, int C, int H_bev, int W_bev,
    int H_img, int W_img
) {
    // BEV 视角变换:从 BEV 坐标反投影到图像坐标
    // 逻辑:
    // BEV 空间的一个点 (x, y, z=0) → 3D 世界坐标 (x, y, 0)
    // 3D 世界坐标 → 相机坐标:T_cam = R^-1 × (P_world - T)
    // 相机坐标 → 像素坐标:p = K × T_cam

    for (int b = 0; b < B; b++) {
        for (int h = 0; h < H_bev; h++) {
            for (int w = 0; w < W_bev; w++) {
                // 取 BEV 网格点
                float bev_x = grid[h * W_bev * 3 + w * 3 + 0];
                float bev_y = grid[h * W_bev * 3 + w * 3 + 1];
                float bev_z = 0.0f;

                // 遍历所有相机
                LocalTensor<float> attended_features(N_cam * C);

                for (int cam = 0; cam < N_cam; cam++) {
                    // 3D 点(BEV → 世界 → 相机 → 像素)
                    // Cube 单元做矩阵乘
                    LocalTensor<float> world_pt(3);  // [x, y, 0]
                    world_pt[0] = bev_x;
                    world_pt[1] = bev_y;
                    world_pt[2] = bev_z;

                    // 逆外参变换:相机坐标 = R^T × (世界坐标 - T)
                    // R^T 是旋转矩阵的逆(因为 R 是正交的)
                    LocalTensor<float> cam_pt(3);
                    MatMul(cam_pt, camera_R[b * N_cam + cam], world_pt);  // R^T × P_world
                    cam_pt[0] -= camera_T[b * N_cam + cam][0];
                    cam_pt[1] -= camera_T[b * N_cam + cam][1];
                    cam_pt[2] -= camera_T[b * N_cam + cam][2];

                    // 检查深度(相机朝向方向)
                    if (cam_pt[2] <= 0.0f) continue;  // 深度为负,跳过

                    // 内参变换:像素坐标 = K × 相机坐标
                    LocalTensor<float> pixel_pt(3);
                    MatMul(pixel_pt, camera_K[b * N_cam + cam], cam_pt);

                    float u = pixel_pt[0] / pixel_pt[2];
                    float v = pixel_pt[1] / pixel_pt[2];

                    // 检查是否在图像范围内
                    if (u < 0 || u >= W_img || v < 0 || v >= H_img) continue;

                    // 双线性插值采样图像特征
                    float feat_val = BilinearSample(image_features[b][cam], u, v);
                    attended_features[cam * C : (cam+1) * C] = feat_val;
                }

                // 对所有相机的特征做加权平均
                bev_query[b * H_bev * W_bev * C + h * W_bev * C + w * C]
                    = Mean(attended_features, N_cam);
            }
        }
    }
}

BEV 视角变换的计算量随相机数量线性增长——6 个相机就是 6 次矩阵乘和 6 次双线性采样。CUBE 单元并行处理多个相机的投影,Vector 单元处理双线性插值。

NeRF 的体积渲染

NeRF(Neural Radiance Fields)用隐式表示描述三维场景——给定一个 3D 坐标 (x, y, z) 和观察方向 (θ, φ),输出颜色 (R, G, B) 和体密度 σ。

体积渲染(Volumetric Rendering)沿着射线采样点,累积颜色和密度:

// cann-recipes-spatial-intelligence/kernels/nerf_rendering.cpp

// 体积渲染:沿射线采样 N_fine 个点,计算颜色和密度

__aicore__ void NerfVolumeRender(
    GlobalTensor<float>& rgb_map,         // [H, W, 3] 渲染图像
    GlobalTensor<float>& depth_map,        // [H, W] 深度图
    GlobalTensor<float>& ray_origins,     // [H, W, 3] 射线起点
    GlobalTensor<float>& ray_dirs,         // [H, W, 3] 射线方向(归一化)
    GlobalTensor<NeRFNetwork>& network,   // NeRF 网络权重
    int H, int W, int N_samples           // 采样点数
) {
    const float t_near = 0.1f;     // 近平面
    const float t_far = 10.0f;     // 远平面

    for (int y = 0; y < H; y++) {
        for (int x = 0; x < W; x++) {
            int ray_idx = y * W + x;

            float3 orig = LoadFloat3(ray_origins, ray_idx);
            float3 dir = LoadFloat3(ray_dirs, ray_idx);

            // 沿射线均匀采样 N 个点
            float t_vals[N_samples];
            for (int i = 0; i < N_samples; i++) {
                float t = t_near + (t_far - t_near) * float(i) / float(N_samples - 1);
                // 添加细粒度扰动(分层采样,降低方差)
                t += (hash(ray_idx * N_samples + i) - 0.5f) * (t_far - t_near) / float(N_samples);
                t_vals[i] = clamp(t, t_near, t_far);
            }

            // 累积颜色和密度
            float3 rgb = {0, 0, 0};
            float transmittance = 1.0f;
            float depth = 0.0f;

            for (int i = 0; i < N_samples; i++) {
                // 射线上的 3D 采样点
                float px = orig.x + dir.x * t_vals[i];
                float py = orig.y + dir.y * t_vals[i];
                float pz = orig.z + dir.z * t_vals[i];

                // NeRF MLP 查询:输入 (x, y, z, θ, φ),输出 (R, G, B, σ)
                float3 rgb_i;
                float sigma_i;
                network.Query(px, py, pz, dir, rgb_i, sigma_i);

                // Delta = t_{i+1} - t_i
                float delta = (i == N_samples - 1)
                    ? t_far - t_vals[i]
                    : t_vals[i+1] - t_vals[i];

                // 透射率衰减因子
                float alpha = 1.0f - expf(-sigma_i * delta);
                float weight = alpha * transmittance;

                // 累积颜色
                rgb.x += weight * rgb_i.x;
                rgb.y += weight * rgb_i.y;
                rgb.z += weight * rgb_i.z;

                // 累积深度(加权平均)
                depth += weight * t_vals[i];

                // 更新透射率
                transmittance *= (1.0f - alpha);

                if (transmittance < 1e-4f) break;  // 光线基本被遮挡,提前终止
            }

            StoreFloat3(rgb_map, ray_idx, rgb);
            depth_map[ray_idx] = depth;
        }
    }
}

NeRF 的计算瓶颈在 MLP 查询(每个采样点一次)——64 个采样点 × 100 万条射线 = 6400 万次 MLP 调用。每条射线的采样点之间没有数据依赖,适合 NPU 的多核并行。

SLAM 的光流和特征匹配

SLAM(同步定位与地图构建)实时估计相机运动和三维环境结构。关键步骤:光流(跟踪相邻帧的像素运动)和特征匹配(找两张图之间的对应关系)。

// cann-recipes-spatial-intelligence/kernels/slam_optical_flow.cpp

// 光流计算:Lucas-Kanade 算法的 NPU 实现
// 假设邻域内所有像素有相同的运动向量

__aicore__ void OpticalFlowLK(
    GlobalTensor<float>& flow_u,         // 光流水平分量 [H, W]
    GlobalTensor<float>& flow_v,         // 光流垂直分量 [H, W]
    GlobalTensor<uchar>& img1,           // 第 1 帧图像
    GlobalTensor<uchar>& img2,           // 第 2 帧图像
    int H, int W,
    int window_size = 5                  // Lucas-Kanade 窗口大小
) {
    int half_win = window_size / 2;

    for (int y = half_win; y < H - half_win; y++) {
        for (int x = half_win; x < W - half_win; x++) {
            // 构建局部邻域的 Ix, Iy, It 矩阵
            // Ix: 图像 x 方向梯度
            // Iy: 图像 y 方向梯度
            // It: 帧间时间梯度

            float A[4] = {0, 0, 0, 0};  // [2x2] 矩阵 A^T A
            float B[2] = {0, 0};         // [2x1] 向量 A^T B

            for (int wy = -half_win; wy <= half_win; wy++) {
                for (int wx = -half_win; wx <= half_win; wx++) {
                    int x1 = x + wx, y1 = y + wy;
                    int x2 = x1 + 1, y2 = y1;  // x+1 方向
                    int x3 = x1, y3 = y1 + 1;  // y+1 方向

                    // Sobel 算子计算 Ix 和 Iy
                    float Ix = (float(img2[y1][x2]) - float(img2[y1][x1])) / 2.0f;
                    float Iy = (float(img2[y3][x1]) - float(img2[y1][x1])) / 2.0f;

                    // 时间梯度 It
                    float It = float(img2[y1][x1]) - float(img1[y1][x1]);

                    A[0] += Ix * Ix;
                    A[1] += Ix * Iy;
                    A[2] += Ix * Iy;
                    A[3] += Iy * Iy;
                    B[0] += -Ix * It;
                    B[1] += -Iy * It;
                }
            }

            // 解 2×2 线性方程组 A × [u; v] = B
            // [A[0]  A[1]] [u] = [B[0]]
            // [A[2]  A[3]] [v] = [B[1]]
            float det = A[0]*A[3] - A[1]*A[2];
            if (fabs(det) > 1e-6f) {
                flow_u[y * W + x] = (A[3]*B[0] - A[1]*B[1]) / det;
                flow_v[y * W + x] = (-A[2]*B[0] + A[0]*B[1]) / det;
            } else {
                flow_u[y * W + x] = 0.0f;
                flow_v[y * W + x] = 0.0f;
            }
        }
    }
}

光流计算的核心是 2×2 矩阵的求逆——Vector 单元的逐元素操作在这里非常高效。窗口内每个像素参与一次累加,窗口滑动过程可以复用中间结果。

踩坑一:NeRF 的射线采样在长序列上显存爆炸

NeRF 渲染一张图需要沿每条射线采样 N 个点。1920×1080 的图 × 64 个采样点 = 1.33 亿个采样点,每个采样点调用一次 MLP(查询颜色和密度)。

错误:一次性把所有采样点送进 MLP。

// 一次性处理所有射线和所有采样点
// 1.33 亿个采样点全部加载到 HBM
// MLP 查询需要同时保存输入和中间激活值
// HBM 占用 = 1.33亿 × (输入3D坐标3×float + MLP中间层~10×float) × 4B ≈ 69 GB
// 单卡 HBM 只有 64 GB,还要加载网络权重 → 爆显存

正确做法:按 batch 分批渲染,同 batch 的射线共享 MLP 的中间激活值。

// 分批渲染:每批处理 4096 条射线
// 4096 × 64 = 262144 个采样点
// HBM 占用 ≈ 262144 × 40 × 4B = 42 MB
// 完全适配单卡显存

const int RAYS_PER_BATCH = 4096;
int num_rays = H * W;
int num_batches = (num_rays + RAYS_PER_BATCH - 1) / RAYS_PER_BATCH;

for (int batch = 0; batch < num_batches; batch++) {
    int start = batch * RAYS_PER_BATCH;
    int end = min(start + RAYS_PER_BATCH, num_rays);

    // 只加载当前 batch 的射线数据
    LoadRayBatch(ray_origins, ray_dirs, start, end);

    // 渲染当前 batch
    RenderBatch(output_rgb, output_depth, start, end);
}

踩坑二:SLAM 的光流在低纹理区域失效

Lucas-Kanade 光流假设窗口内所有像素有相同运动——这个假设在低纹理区域(白墙、天空)完全失效:图像梯度为零(Ix = Iy = 0),矩阵 A 是奇异的,方程无解。

修复:在低纹理区域跳过光流计算。

// 先检测图像的梯度幅值
// 梯度幅值小于阈值 → 低纹理区域 → 光流不可靠

float grad_mag = sqrt(A[0] + A[3]);  // = Ix² + Iy²

if (grad_mag < 1e-4f) {
    // 低纹理区域:设置光流为 0 或用附近像素的外插值
    flow_u[y * W + x] = 0.0f;
    flow_v[y * W + x] = 0.0f;
    continue;
}

// 只在纹理丰富的区域计算光流
// 非极大值抑制:保留响应最强的点

踩坑三:3D 检测的相机外参在线标定误差

BEVFormer 的性能依赖相机外参(R, T)的标定精度。实际车载环境中,相机安装位置随时间漂移(热胀冷缩、震动),旧的外参逐渐失效。

症状:BEV 检测的远处目标出现系统性偏移——不是随机误差,而是固定方向的偏差。

修复:在线外参优化(Online Extrinsic Calibration)。在每帧图像中提取车道线或地面特征,用这些特征和上一帧的外参联合优化 R 和 T。

// 在线外参优化(简化为单参数偏移)
// 假设外参只有平移偏移:T_real = T_initial + ΔT
// ΔT 通过最小化重投影误差得到

__aicore__ void OnlineExtrinsicCalib(
    GlobalTensor<float>& delta_T,         // 外参平移偏移 [3]
    GlobalTensor<float>& lane_3d,        // 检测到的 3D 车道线点
    GlobalTensor<float>& lane_2d_proj,  // 车道线在图像上的投影
    GlobalTensor<float>& camera_K,       // 内参
    GlobalTensor<float>& camera_R,      // 外参旋转(已知固定)
    int num_lanes
) {
    // 重投影误差:车道线 3D 点投影到图像,和检测到的车道线像素的差异
    // e_i = ||project(lane_3d[i], K, R, T_initial + ΔT) - lane_2d_proj[i]||²

    // 高斯-牛顿迭代优化 ΔT
    for (int iter = 0; iter < 10; iter++) {
        float2 total_error = {0, 0};

        for (int i = 0; i < num_lanes; i++) {
            float3 p_3d = lane_3d[i];
            float2 p_proj;

            // 用当前 ΔT 做投影
            float3 T_curr = T_initial + delta_T;
            ProjectToImage(p_3d, camera_K, camera_R, T_curr, p_proj);

            // 误差
            float2 e = p_proj - lane_2d_proj[i];
            total_error += e;
        }

        // 更新 ΔT(梯度下降方向)
        // 简化:ΔT_new = ΔT - lr × total_error / num_lanes
        float lr = 0.01f;
        delta_T -= lr * total_error / float(num_lanes);

        // 收敛判断
        if (Length(total_error) < 0.1f) break;
    }
}

在线标定的收敛速度快(3-5 次迭代),但会消耗一部分计算资源。在资源紧张时可以每 N 帧做一次标定,而不是每帧都做。


空间智能三条线——3D 检测、NeRF、SLAM——都是自动驾驶和机器人感知的基础能力。三个场景在算子层面的需求各不相同:BEVFormer 依赖高效矩阵乘和视角变换,NeRF 依赖体渲染(Ray Marching),SLAM 依赖光流和特征匹配。但它们的共同点是实时性要求高——自动驾驶的前视感知需要在 50ms 内完成,SLAM 需要在 30fps 下跑通。cann-recipes-spatial-intelligence 把这三类场景的优化路径都整理成菜谱。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐