之前 文章都在写 CANN 怎么支撑模型开发——算子、通信、编译、量化。elec-ops-inspection 是第一篇写「用 CANN 干什么」的文章。它是昇腾 NPU 在电力巡检场景的端到端解决方案:从无人机拍回的照片 → NPU 识别绝缘子破损、导线断股、杆塔倾斜 → 生成巡检报告。

电力巡检的算子需求

电力巡检跟通用 CV 不一样——不是 ImageNet 分类,是特定目标的缺陷检测:

检测目标 算子类型 输入 输出
绝缘子破损 目标检测 + 分割 无人机航拍图 (4000×3000) 破损 mask + 破损等级
导线断股 形态学 + 线检测 高分辨率灰度图 断股位置 + 断股数
杆塔倾斜 几何变换 + 角度计算 多视角图 倾斜角度 + 偏移量
鸟巢检测 目标检测 塔顶区域 crop 是否存在 + 边界框
锈蚀检测 颜色空间 + 纹理分析 金属部件 close-up 锈蚀面积百分比

前四类用神经网络(YOLO/UNet 变体),最后一类用传统 CV 算子(颜色检测 + 纹理分析)。elec-ops-inspection 把两套方法都打包进了一个统一的推理 pipeline。

推理 Pipeline

无人机航拍图 (4000×3000) 
  ↓
┌─────────────────┐
│ Tile分割        │  ← 高分辨率图像切成长 1024×1024 的瓦片
│ 4000×3000       │     ops-tensor (reshape + split)
│ → 12 tiles      │
└─────────────────┘
  ↓
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│ YOLO 推理        │  │ UNet 分割       │  │ 传统CV预处理     │
│ (鸟巢/破损粗检)   │  │ (绝缘子精分割)   │  │ (锈蚀/导线)      │
│ ops-cv + NN      │  │ ops-cv + UNet   │  │ ops-cv + sip     │
└─────────────────┘  └─────────────────┘  └─────────────────┘
  ↓                      ↓                    ↓
┌──────────────────────────────────────────────┐
│ 结果融合                                       │
│ ops-tensor: concat bbox + mask + rust score   │
└──────────────────────────────────────────────┘
  ↓
┌─────────────────┐
│ 巡检报告         │
│ JSON + 标注截图   │
└─────────────────┘

整个 pipeline 在 NPU 上跑完——Tile 分割、模型推理、结果融合全在 HBM 里流转,中间不返回 host 端。

核心代码:Tile 分割 + 批量推理

// elec-ops-inspection/pipeline/inspection_pipeline.cpp

__aicore__ void InspectionPipeline(
    GlobalTensor<float>& image,          // [4000, 3000, 3]
    GlobalTensor<float>& yolo_weights,   // YOLO 模型权重
    GlobalTensor<float>& unet_weights,   // UNet 模型权重
    GlobalTensor<Detection>& results     // 输出:检测结果
) {
    // 第一步:高分辨率图像切瓦片
    // 4000×3000 切成 4×3=12 个 1024×1024 瓦片
    const int tile_size = 1024;
    const int tiles_x = 4;
    const int tiles_y = 3;
    LocalTensor<float> tiles[tiles_x * tiles_y];

    // TileExtract 把大图均匀切成 tiles——一次性操作
    // ops-tensor 的 extract_tiles 算子:stride=1024, 自动处理边缘 padding
    ExtractTiles(image, tiles, tile_size, tiles_x, tiles_y);

    // 第二步:对 12 个瓦片并行做 YOLO 推理
    // batch=12,NPU 的 12 个批次图片同时进入模型
    LocalTensor<BBox> yolo_detections;
    BatchYOLOInfer(tiles, yolo_weights, yolo_detections, 12);

    // 第三步:对每个检测到的目标做 UNet 分割
    // 只在检测到目标的位置做分割——不在整图上做
    for (int d = 0; d < yolo_detections.num_detections; d++) {
        BBox box = yolo_detections[d];

        // crop 目标区域(从对应的 tile 里裁)
        LocalTensor<float> roi;
        CropROI(tiles[box.tile_id], roi, box.x, box.y, box.w, box.h);

        // UNet 分割:输出破损/正常 mask
        LocalTensor<float> mask;
        UNetSegmentation(roi, unet_weights, mask);

        // 汇总检测结果
        results[d] = {
            bbox: box,
            mask: mask,
            defect_type: ClassifyDefect(mask),  // 破损类型
            confidence: box.confidence
        };
    }
}

传统 CV 算子:锈蚀检测不用神经网络

锈蚀检测是一个特殊任务——训练数据太少,神经网络在小数据集上过拟合严重。用传统 CV 算子反而更稳定:

// elec-ops-inspection/ops/rust_detection.cpp

__aicore__ float DetectRust(
    GlobalTensor<uchar>& metal_image,  // 金属部件 close-up RGB
    int width, int height
) {
    // 第一步:RGB → HSV 颜色空间转换
    // sip 的颜色空间转换算子
    LocalTensor<float> hsv_image;
    RGBtoHSV(metal_image, hsv_image, width, height);

    // 第二步:提取 Hue 通道中的「橙色-棕色」范围
    // 锈蚀的颜色特征:Hue ∈ [10°, 40°]、Saturation > 0.3
    // 不是单纯阈值——有纹理变化(锈蚀有斑驳感)
    LocalTensor<bool> rust_mask;

    // 颜色阈值
    Threshold3Channel(
        rust_mask,
        hsv_image,
        h_min=10.0,  h_max=40.0,    // Hue 范围
        s_min=0.3,   s_max=1.0,     // Saturation 范围
        v_min=0.1,   v_max=1.0      // Value 范围(调暗金属反射)
    );

    // 第三步:形态学膨胀 + 纹理分析
    // 颜色检测有噪声(金属反光被误判为锈蚀)
    // 形态学膨胀把单像素噪声消掉
    Dilate(rust_mask, kernel_size=3);

    // 纹理分析:锈蚀区域有颗粒感(局部方差大)
    // 正常金属表面光滑(局部方差小)
    LocalTensor<float> texture_variance;
    LocalVariance(hsv_image, texture_variance, window=8);
    LocalTensor<bool> texture_mask;
    Threshold(texture_variance, texture_mask, thresh_min=50.0);

    // 合并:颜色+纹理都要满足
    rust_mask = rust_mask && texture_mask;

    // 第四步:计算锈蚀面积百分比
    int rust_pixels = CountNonZero(rust_mask);
    return float(rust_pixels) / (width * height) * 100.0;
}

传统 CV 的做法在数据不足时比神经网络更可靠:颜色和纹理特征是物理规律(铁锈的颜色就是橙色-棕色),不是从数据学来的统计规律。

踩坑一:UNet 分割在高分辨率下的显存爆炸

UNet 的标准输入是 512×512。航拍图的绝缘子区域 crop 可能是 2048×2048——UNet 照原分辨率跑,中间特征图(encoder path 的 256× feature map × 2048 × 2048 = 4GB)直接把 HBM 撑爆。

错误:crop 的 ROI 直接送 UNet。

// 绝缘子区域 crop 出来 1800×1500
// UNet 内部 5 个 encoder stage,每 stage 分辨率减半、通道数加倍
// Stage 1: 1800×1500   × 64  = 172 MB
// Stage 2:  900× 750   × 128 =  86 MB
// Stage 3:  450× 375   × 256 =  43 MB
// Stage 4:  225× 188   × 512 =  22 MB
// Stage 5:  113×  94   × 1024 =  11 MB
// 总计 ≈ 334 MB(只是 forward pass)
// 加上 decoder 和梯度(如果有)≈ 1 GB
// HBM 够用,但 172 MB 的第一层特征图太大了

正确做法:UNet 之前强制 resize 到 512×512。

// 第一步:crop ROI(保持原始 high resolution)
LocalTensor<float> roi_raw;
CropROI(source_tile, roi_raw, box.x, box.y, box.w, box.h);

// 第二步:resize 到 512×512(op
// 第二步:resize 到 512×512(ops-cv 的双线性插值算子)
LocalTensor<float> roi_512;
BilinearResize(roi_raw, roi_512, 512, 512);

// 第三步:分割
UNetSegmentation(roi_512, unet_weights, mask_512);

// 第四步:mask 缩放回原始尺寸(用于在原始图像上画标注)
LocalTensor<float> mask_original;
BilinearResize(mask_512, mask_original, box.w, box.h);

踩坑二:YOLO 后处理的 NMS 瓶颈

YOLO 检测出 1000+ 个候选框后,NMS(非极大值抑制)是瓶颈。NMS 需要两两比较 IoU——O(N²) 复杂度——1000 个框就是 50 万次 IoU 计算。信极端情况下超过 1 毫秒(算子延迟通常在 200ns 级别),变成整个 pipeline 的唯一瓶颈。

优化:NMS 可以按 tile 分组执行。

// 每个 tile 独立运行 NMS
// 1000 个框分布在 12 个 tile → 每 tile 平均 83 个框
// O(N²/t) = O(1000²/12) ≈ 8.3 万次比较(vs 50 万次)
// 延迟减少到原来的 1/6

for (int t = 0; t < tiles_x * tiles_y; t++) {
    BBox* tile_boxes = &yolo_detections[tile_offset[t]];
    int tile_count = tile_box_count[t];
    NMS(tile_boxes, tile_count, iou_thresh=0.5);
}

// 注意:tile 边界的框可能跨两个 tile
// 需要额外的跨 tile NMS 把边界框合并
CrossTileNMS(yolo_detections, tile_boundaries);

踩坑三:形态学算子在 GPU/NPU 上的语义差异

Dilate 算子(形态学膨胀)在 CPU 上的默认实现是 4-连通:膨胀到上下左右四个邻居。但 NPU 的 Vector 实现用的是 8-连通(加上四个对角线邻居)。同样的 kernel_size=3 参数在两种实现下结果不一致——NPU 膨胀的区域比 CPU 大了一圈。

根因:CPU 实现考虑内存连续性(只访问 ±1 和 ±stride),NPU Vector 单元有跨 lane 空间,对角线邻居也在一条指令里做完。

解决:统一用 8-连通语义(NPU 的实现),CPU 上的参考实现也改成 8-连通(加对角线邻居),确保两种平台的行为一致。


elec-ops-inspection 跟之前写过的所有仓库都不同——它不提供可复用的通用算子,而是把 CANN 的各个组件(ops-cv + sip + YOLO + UNet + 传统 CV)编排成一个端到端的解决方案。从无人机航拍到巡检报告,全套在 NPU 上跑完。CANN 的 55 个仓库不只有基础设施——还有这些直接解决行业问题的垂直方案。

Logo

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

更多推荐