昇腾CANN elec-ops-inspection:电力巡检场景的 NPU 端侧部署实战
本文介绍了基于昇腾NPU的电力巡检端到端解决方案,通过无人机航拍图像自动检测绝缘子破损、导线断股等缺陷。系统采用混合架构:YOLO/UNet处理目标检测和分割,传统CV算子分析锈蚀等特征。关键技术包括:1)高分辨率图像分块处理;2)神经网络与传统CV融合的推理流程;3)针对UNet显存优化的降采样策略。该方案实现了从图像采集到报告生成的全流程NPU加速,解决了电力巡检场景特有的小样本、高精度检测难
之前 文章都在写 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 个仓库不只有基础设施——还有这些直接解决行业问题的垂直方案。
更多推荐




所有评论(0)