HarmonyOS开发:目标检测YOLO模型端侧部署
HarmonyOS开发:目标检测YOLO模型端侧部署
核心要点:本文系统讲解YOLO目标检测模型在HarmonyOS端侧的完整部署方案,包括YOLOv8模型转换、NMS后处理实现、多目标实时检测UI搭建,以及端侧检测的性能调优策略。
一、背景与动机
你打开手机相机对准街景,屏幕上立刻出现一个个方框——“汽车”、“行人”、“交通灯”——每个方框旁边还标注了置信度。这就是目标检测,它和图像分类最大的不同在于:分类只告诉你"图里有什么",检测则告诉你"在哪里、有多大"。
目标检测的应用场景太广了。智能驾驶需要检测行人和车辆,安防监控需要检测异常行为,零售行业需要检测货架商品,甚至你手机上的"智慧识屏"功能,底层也是目标检测。YOLO(You Only Look Once)系列是目标检测领域最流行的算法之一,它的核心思想是"一次前向传播搞定所有检测",速度快、精度高,特别适合端侧部署。
但YOLO模型比分类模型复杂得多——输出不是简单的概率向量,而是包含边界框、类别、置信度的多维张量,还需要NMS(非极大值抑制)后处理。这些在端侧怎么高效实现?怎么在保证帧率的同时维持检测精度?这就是本文要回答的问题。
二、核心原理
2.1 YOLO检测流程
YOLO的核心思想是将输入图像划分为S×S的网格,每个网格预测B个边界框,每个边界框包含5个值(x, y, w, h, confidence),再加上C个类别概率。
flowchart TB
A[输入图像<br/>640×640×3] --> B[YOLO Backbone<br/>特征提取]
B --> C[Neck<br/>特征融合FPN+PAN]
C --> D[检测头<br/>3个尺度输出]
D --> E[原始预测张量<br/>8400×85]
E --> F[置信度过滤<br/>threshold > 0.25]
F --> G[NMS非极大值抑制<br/>IoU阈值0.45]
G --> H[最终检测结果<br/>N×6: x,y,w,h,conf,cls]
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef info fill:#81C784,stroke:#388E3C,color:#000
classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
class A,B primary
class C,D warning
class E,F purple
class G info
class H error
2.2 YOLOv8输出格式
YOLOv8的输出格式与早期版本有所不同。以YOLOv8n(COCO 80类)为例:
- 输入:1×3×640×640(NCHW格式)
- 输出:1×84×8400
- 84 = 4(边界框坐标xyxy)+ 80(类别概率)
- 8400 = 80×80 + 40×40 + 20×20(三个检测头的网格总数)
输出张量需要转置为8400×84的格式,然后进行后处理。
2.3 NMS非极大值抑制
NMS是目标检测中不可或缺的后处理步骤。当多个检测框重叠指向同一个目标时,NMS保留置信度最高的框,抑制其余重叠框:
NMS算法流程:
1. 按置信度降序排列所有检测框
2. 取置信度最高的框A,加入结果列表
3. 计算A与其余所有框的IoU
4. 删除IoU > 阈值的框(它们与A指向同一目标)
5. 重复步骤2-4,直到所有框处理完毕
三、代码实战
3.1 YOLO检测数据结构定义
首先定义目标检测所需的核心数据结构:
// YOLOTypes.ets - YOLO检测数据结构定义
/**
* 边界框(左上角+右下角坐标格式)
*/
export interface BoundingBox {
x1: number; // 左上角x坐标
y1: number; // 左上角y坐标
x2: number; // 右下角x坐标
y2: number; // 右下角y坐标
}
/**
* 单个检测结果
*/
export interface Detection {
bbox: BoundingBox; // 边界框坐标
confidence: number; // 置信度
classId: number; // 类别ID
className: string; // 类别名称
}
/**
* YOLO模型配置参数
*/
export interface YOLOConfig {
modelName: string; // 模型名称
modelPath: string; // 模型文件路径
inputWidth: number; // 输入宽度
inputHeight: number; // 输入高度
numClasses: number; // 类别数量(COCO=80)
confidenceThreshold: number; // 置信度阈值
nmsThreshold: number; // NMS的IoU阈值
maxDetections: number; // 最大检测数量
labelPath: string; // 标签文件路径
}
/**
* COCO数据集80类标签
*/
export const COCO_LABELS: string[] = [
'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train',
'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep',
'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella',
'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard',
'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard',
'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork',
'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange',
'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair',
'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv',
'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave',
'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase',
'scissors', 'teddy bear', 'hair drier', 'toothbrush'
];
/**
* 不同类别的配色方案(用于可视化)
*/
export const CLASS_COLORS: string[] = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
'#F8C471', '#82E0AA', '#F1948A', '#AED6F1', '#D7BDE2',
'#A3E4D7', '#FAD7A0', '#A9CCE3', '#D5F5E3', '#FADBD8'
];
3.2 YOLO后处理引擎(含NMS)
这是整个目标检测最核心的部分——后处理逻辑,包括置信度过滤、NMS和坐标映射:
// YOLOPostProcessor.ets - YOLO后处理引擎
import { Detection, BoundingBox, YOLOConfig, COCO_LABELS, CLASS_COLORS } from './YOLOTypes';
/**
* YOLO后处理器
* 负责将模型原始输出转换为可用的检测结果
*/
export class YOLOPostProcessor {
private config: YOLOConfig;
constructor(config: YOLOConfig) {
this.config = config;
}
/**
* 主处理入口:原始输出 → 检测结果列表
* @param outputData 模型原始输出张量(84×8400格式)
* @param imageWidth 原始图像宽度
* @param imageHeight 原始图像高度
* @returns 检测结果数组
*/
process(outputData: Float32Array, imageWidth: number, imageHeight: number): Detection[] {
const { numClasses, confidenceThreshold, nmsThreshold, maxDetections, inputWidth, inputHeight } = this.config;
// YOLOv8输出格式:84×8400,需要转置为8400×84
const numDetections = 8400;
const numValues = 4 + numClasses; // 4个坐标 + 80个类别
// 第一步:解析所有候选框
const candidates: Detection[] = [];
for (let i = 0; i < numDetections; i++) {
// 提取边界框坐标(归一化的中心坐标格式)
const cx = outputData[0 * numDetections + i];
const cy = outputData[1 * numDetections + i];
const w = outputData[2 * numDetections + i];
const h = outputData[3 * numDetections + i];
// 找到最大类别概率作为置信度
let maxClassProb = 0;
let classId = 0;
for (let c = 0; c < numClasses; c++) {
const prob = outputData[(4 + c) * numDetections + i];
if (prob > maxClassProb) {
maxClassProb = prob;
classId = c;
}
}
// 置信度过滤
if (maxClassProb < confidenceThreshold) {
continue;
}
// 将中心坐标转换为左上角+右下角格式
// 同时从模型输入空间映射到原始图像空间
const scaleX = imageWidth / inputWidth;
const scaleY = imageHeight / inputHeight;
const x1 = (cx - w / 2) * scaleX;
const y1 = (cy - h / 2) * scaleY;
const x2 = (cx + w / 2) * scaleX;
const y2 = (cy + h / 2) * scaleY;
candidates.push({
bbox: { x1, y1, x2, y2 },
confidence: maxClassProb,
classId: classId,
className: classId < COCO_LABELS.length ? COCO_LABELS[classId] : `class_${classId}`
});
}
// 第二步:按类别分组执行NMS
const results: Detection[] = [];
const classGroups = new Map<number, Detection[]>();
// 按类别ID分组
for (const det of candidates) {
if (!classGroups.has(det.classId)) {
classGroups.set(det.classId, []);
}
classGroups.get(det.classId)!.push(det);
}
// 对每个类别分别执行NMS
for (const [, dets] of classGroups) {
// 按置信度降序排列
dets.sort((a, b) => b.confidence - a.confidence);
const nmsResults = this.nms(dets, nmsThreshold);
results.push(...nmsResults);
}
// 第三步:按置信度排序,取Top-K
results.sort((a, b) => b.confidence - a.confidence);
return results.slice(0, maxDetections);
}
/**
* NMS非极大值抑制算法
* @param detections 同一类别的检测结果(已按置信度降序排列)
* @param iouThreshold IoU阈值
* @returns 抑制后的检测结果
*/
private nms(detections: Detection[], iouThreshold: number): Detection[] {
const results: Detection[] = [];
const suppressed = new Set<number>();
for (let i = 0; i < detections.length; i++) {
if (suppressed.has(i)) continue;
// 保留当前最高置信度的检测框
results.push(detections[i]);
// 与后续所有框计算IoU
for (let j = i + 1; j < detections.length; j++) {
if (suppressed.has(j)) continue;
const iou = this.calculateIoU(detections[i].bbox, detections[j].bbox);
if (iou > iouThreshold) {
suppressed.add(j); // 抑制重叠框
}
}
}
return results;
}
/**
* 计算两个边界框的IoU(交并比)
*/
private calculateIoU(box1: BoundingBox, box2: BoundingBox): number {
// 计算交集区域
const interX1 = Math.max(box1.x1, box2.x1);
const interY1 = Math.max(box1.y1, box2.y1);
const interX2 = Math.min(box1.x2, box2.x2);
const interY2 = Math.min(box1.y2, box2.y2);
const interWidth = Math.max(0, interX2 - interX1);
const interHeight = Math.max(0, interY2 - interY1);
const interArea = interWidth * interHeight;
// 计算并集区域
const area1 = (box1.x2 - box1.x1) * (box1.y2 - box1.y1);
const area2 = (box2.x2 - box2.x1) * (box2.y2 - box2.y1);
const unionArea = area1 + area2 - interArea;
return unionArea > 0 ? interArea / unionArea : 0;
}
/**
* 获取类别对应的可视化颜色
*/
getClassColor(classId: number): string {
return CLASS_COLORS[classId % CLASS_COLORS.length];
}
}
3.3 YOLO检测器完整封装与Canvas绘制
将推理引擎、后处理和可视化整合到一起:
// YOLODetector.ets - YOLO目标检测器完整封装
import { mindspore } from '@kit.MindSporeLiteKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { fs } from '@kit.CoreFileKit';
import { YOLOConfig, Detection, COCO_LABELS } from './YOLOTypes';
import { YOLOPostProcessor } from './YOLOPostProcessor';
/**
* YOLO目标检测器
* 封装完整的检测流程:模型加载 → 预处理 → 推理 → 后处理
*/
export class YOLODetector {
private context: common.Context;
private config: YOLOConfig;
private session: mindspore.Session | null = null;
private model: mindspore.Model | null = null;
private postProcessor: YOLOPostProcessor;
private isInitialized: boolean = false;
constructor(context: common.Context, config: YOLOConfig) {
this.context = context;
this.config = config;
this.postProcessor = new YOLOPostProcessor(config);
}
/**
* 初始化检测器
*/
async initialize(): Promise<boolean> {
try {
// 拷贝模型到沙箱
const modelPath = await this.copyModelToSandbox();
// 创建Context
const msContext: mindspore.Context = {};
const npuDevice: mindspore.DeviceInfo = {
deviceType: mindspore.DeviceType.kNPU,
enableFloat16: true
};
const cpuDevice: mindspore.DeviceInfo = {
deviceType: mindspore.DeviceType.kCPU,
enableFloat16: true,
cpuCores: [0, 1, 2, 3]
};
msContext.deviceInfos = [npuDevice, cpuDevice];
// 加载模型
this.model = new mindspore.Model();
const loadResult = this.model.loadModelFromFile(modelPath, msContext);
if (loadResult !== mindspore.kMSStatusSuccess) {
// 回退到CPU
msContext.deviceInfos = [cpuDevice];
const retryResult = this.model.loadModelFromFile(modelPath, msContext);
if (retryResult !== mindspore.kMSStatusSuccess) {
console.error('[YOLODetector] 模型加载失败');
return false;
}
}
// 创建Session
this.session = this.model.createSession(msContext);
if (this.session === null) {
console.error('[YOLODetector] Session创建失败');
return false;
}
this.isInitialized = true;
console.info('[YOLODetector] 初始化成功');
return true;
} catch (error) {
console.error(`[YOLODetector] 初始化异常: ${error}`);
return false;
}
}
/**
* 拷贝模型文件到沙箱目录
*/
private async copyModelToSandbox(): Promise<string> {
const sandboxPath = `${this.context.filesDir}/${this.config.modelName}.ms`;
if (fs.accessSync(sandboxPath)) {
return sandboxPath;
}
const srcPath = `models/${this.config.modelName}.ms`;
const content = this.context.resourceMgr.getRawFileContentSync(srcPath);
const file = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
fs.writeSync(file.fd, content.buffer);
fs.closeSync(file);
return sandboxPath;
}
/**
* 图像预处理:Resize + LetterBox + Normalize
* LetterBox保持宽高比,避免目标变形
*/
private preprocess(pixelMap: image.PixelMap): { inputData: Float32Array; scaleInfo: ScaleInfo } {
const { inputWidth, inputHeight } = this.config;
const imageInfo = pixelMap.getImageInfo();
// 计算LetterBox缩放参数
const scale = Math.min(inputWidth / imageInfo.size.width, inputHeight / imageInfo.size.height);
const newWidth = Math.floor(imageInfo.size.width * scale);
const newHeight = Math.floor(imageInfo.size.height * scale);
const padX = (inputWidth - newWidth) / 2;
const padY = (inputHeight - newHeight) / 2;
const scaleInfo: ScaleInfo = { scale, padX, padY, newWidth, newHeight };
// 读取像素数据
const pixelBytes = new Uint8Array(imageInfo.size.width * imageInfo.size.height * 4);
pixelMap.readPixelsToBufferSync(pixelBytes.buffer);
// 构建输入张量(NCHW格式,LetterBox填充灰色128/255=0.502)
const totalSize = 3 * inputWidth * inputHeight;
const inputData = new Float32Array(totalSize);
// 初始化为灰色填充值
const padValue = (128 / 255.0 - 0.485) / 0.229; // 归一化后的灰色值
inputData.fill(padValue);
// 填充有效区域
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const srcX = Math.floor(x / scale);
const srcY = Math.floor(y / scale);
const srcIdx = (srcY * imageInfo.size.width + srcX) * 4;
const r = pixelBytes[srcIdx] / 255.0;
const g = pixelBytes[srcIdx + 1] / 255.0;
const b = pixelBytes[srcIdx + 2] / 255.0;
const dstIdx = (y + Math.floor(padY)) * inputWidth + (x + Math.floor(padX));
// NCHW格式
inputData[0 * inputWidth * inputHeight + dstIdx] = (r - 0.485) / 0.229;
inputData[1 * inputWidth * inputHeight + dstIdx] = (g - 0.456) / 0.224;
inputData[2 * inputWidth * inputHeight + dstIdx] = (b - 0.406) / 0.225;
}
}
return { inputData, scaleInfo };
}
/**
* 执行目标检测
*/
async detect(pixelMap: image.PixelMap): Promise<Detection[]> {
if (!this.isInitialized || this.session === null) {
return [];
}
try {
const imageInfo = pixelMap.getImageInfo();
const startTime = Date.now();
// 预处理
const { inputData, scaleInfo } = this.preprocess(pixelMap);
// 设置输入
const inputs = this.session.getInputs();
inputs[0].setData(inputData.buffer);
// 推理
this.session.run(inputs);
// 获取输出
const outputs = this.session.getOutputs();
const outputData = new Float32Array(outputs[0].getData());
// 后处理
const detections = this.postProcessor.process(
outputData,
imageInfo.size.width,
imageInfo.size.height
);
const totalTime = Date.now() - startTime;
console.info(`[YOLODetector] 检测完成: ${detections.length}个目标, 耗时${totalTime}ms`);
return detections;
} catch (error) {
console.error(`[YOLODetector] 检测异常: ${error}`);
return [];
}
}
/**
* 释放资源
*/
release(): void {
if (this.session !== null) {
this.model?.freeSession(this.session);
this.session = null;
}
if (this.model !== null) {
this.model.freeModel();
this.model = null;
}
this.isInitialized = false;
}
}
/**
* LetterBox缩放信息
*/
interface ScaleInfo {
scale: number; // 缩放比例
padX: number; // X方向填充
padY: number; // Y方向填充
newWidth: number; // 缩放后宽度
newHeight: number; // 缩放后高度
}
3.4 检测结果Canvas可视化绘制
在Canvas上绘制检测框和标签:
// YOLOVisualizer.ets - 检测结果Canvas可视化组件
import { Detection, CLASS_COLORS } from './YOLOTypes';
@Component
export struct YOLOVisualizer {
// 检测结果
@Prop detections: Detection[] = [];
// Canvas宽度
@Prop canvasWidth: number = 360;
// Canvas高度
@Prop canvasHeight: number = 360;
// 图像原始宽度
@Prop imageWidth: number = 640;
// 图像原始高度
@Prop imageHeight: number = 640;
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Canvas(this.context)
.width(this.canvasWidth)
.height(this.canvasHeight)
.onReady(() => {
this.drawDetections();
})
}
/**
* 绘制所有检测结果
*/
private drawDetections() {
// 清空画布
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 计算坐标映射比例
const scaleX = this.canvasWidth / this.imageWidth;
const scaleY = this.canvasHeight / this.imageHeight;
for (const det of this.detections) {
const color = CLASS_COLORS[det.classId % CLASS_COLORS.length];
const x1 = det.bbox.x1 * scaleX;
const y1 = det.bbox.y1 * scaleY;
const x2 = det.bbox.x2 * scaleX;
const y2 = det.bbox.y2 * scaleY;
const boxWidth = x2 - x1;
const boxHeight = y2 - y1;
// 绘制边界框
this.context.strokeStyle = color;
this.context.lineWidth = 2.5;
this.context.strokeRect(x1, y1, boxWidth, boxHeight);
// 绘制半透明填充
this.context.fillStyle = color + '20'; // 12%透明度
this.context.fillRect(x1, y1, boxWidth, boxHeight);
// 绘制标签背景
const labelText = `${det.className} ${(det.confidence * 100).toFixed(0)}%`;
this.context.font = '12px sans-serif';
const textMetrics = this.context.measureText(labelText);
const labelHeight = 20;
const labelWidth = textMetrics.width + 12;
this.context.fillStyle = color;
this.context.fillRect(x1, y1 - labelHeight, labelWidth, labelHeight);
// 绘制标签文字
this.context.fillStyle = '#FFFFFF';
this.context.fillText(labelText, x1 + 6, y1 - 5);
}
}
}
3.5 完整的目标检测页面
// ObjectDetectionPage.ets - 目标检测完整页面
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { YOLOConfig, Detection, COCO_LABELS } from './YOLOTypes';
import { YOLODetector } from './YOLODetector';
import { YOLOVisualizer } from './YOLOVisualizer';
@Entry
@Component
struct ObjectDetectionPage {
@State detections: Detection[] = [];
@State previewUri: string = '';
@State isLoading: boolean = false;
@State isEngineReady: boolean = false;
@State imageWidth: number = 640;
@State imageHeight: number = 640;
@State detectInfo: string = '';
private detector: YOLODetector | null = null;
aboutToAppear() {
this.initDetector();
}
aboutToDisappear() {
this.detector?.release();
}
async initDetector() {
const context = getContext(this) as common.Context;
const config: YOLOConfig = {
modelName: 'yolov8n',
modelPath: 'yolov8n.ms',
inputWidth: 640,
inputHeight: 640,
numClasses: 80,
confidenceThreshold: 0.25,
nmsThreshold: 0.45,
maxDetections: 100,
labelPath: 'coco_labels.txt'
};
this.detector = new YOLODetector(context, config);
this.isEngineReady = await this.detector.initialize();
}
async pickAndDetect() {
if (!this.isEngineReady || this.detector === null) return;
try {
this.isLoading = true;
this.detections = [];
const photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
const photoViewPicker = new picker.PhotoViewPicker();
const result = await photoViewPicker.select(photoSelectOptions);
if (result.photoUris.length === 0) {
this.isLoading = false;
return;
}
this.previewUri = result.photoUris[0];
// 解码图像
const imageSource = image.createImageSource(result.photoUris[0]);
const pixelMap = await imageSource.createPixelMap();
const imageInfo = pixelMap.getImageInfo();
this.imageWidth = imageInfo.size.width;
this.imageHeight = imageInfo.size.height;
// 执行检测
const startTime = Date.now();
const dets = await this.detector.detect(pixelMap);
this.detections = dets;
const totalTime = Date.now() - startTime;
this.detectInfo = `检测到 ${dets.length} 个目标,耗时 ${totalTime}ms`;
this.isLoading = false;
} catch (error) {
console.error(`[Page] 检测失败: ${error}`);
this.isLoading = false;
}
}
build() {
Scroll() {
Column() {
// 标题栏
Row() {
Text('目标检测')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.backgroundColor('#1A1A2E')
// 图像预览 + 检测框叠加
Stack() {
if (this.previewUri) {
Image(this.previewUri)
.width(360)
.height(360)
.objectFit(ImageFit.Contain)
}
// Canvas绘制检测框
YOLOVisualizer({
detections: this.detections,
canvasWidth: 360,
canvasHeight: 360,
imageWidth: this.imageWidth,
imageHeight: this.imageHeight
})
.width(360)
.height(360)
}
.width(360)
.height(360)
.margin({ top: 16 })
.borderRadius(12)
// 操作按钮
Button('选择图片检测')
.width(200)
.height(48)
.fontSize(16)
.backgroundColor('#4FC3F7')
.fontColor('#1A1A2E')
.borderRadius(24)
.enabled(this.isEngineReady && !this.isLoading)
.onClick(() => this.pickAndDetect())
.margin({ top: 16 })
// 检测信息
if (this.detectInfo) {
Text(this.detectInfo)
.fontSize(14)
.fontColor('#4FC3F7')
.margin({ top: 12 })
}
// 检测结果列表
if (this.detections.length > 0) {
Column() {
Text('检测目标列表')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ bottom: 8 })
ForEach(this.detections, (det: Detection, index: number) => {
Row() {
Circle({ width: 10, height: 10 })
.fill(CLASS_COLORS[det.classId % CLASS_COLORS.length])
Text(det.className)
.fontSize(14)
.fontColor('#FFFFFF')
.margin({ left: 8 })
.layoutWeight(1)
Text(`${(det.confidence * 100).toFixed(1)}%`)
.fontSize(14)
.fontColor('#4FC3F7')
Text(`[${Math.round(det.bbox.x1)},${Math.round(det.bbox.y1)}]-[${Math.round(det.bbox.x2)},${Math.round(det.bbox.y2)}]`)
.fontSize(11)
.fontColor('#888888')
.margin({ left: 8 })
}
.width('100%')
.height(36)
.alignItems(VerticalAlign.Center)
.padding({ left: 16, right: 16 })
})
}
.width('92%')
.padding(16)
.borderRadius(12)
.backgroundColor('#16213E')
.margin({ top: 16 })
}
if (this.isLoading) {
LoadingProgress()
.width(48)
.height(48)
.color('#4FC3F7')
.margin({ top: 20 })
}
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#0F0F23')
}
}
// 需要引入CLASS_COLORS
import { CLASS_COLORS } from './YOLOTypes';
四、踩坑与注意事项
4.1 YOLO输出格式差异
坑:不同版本的YOLO输出格式完全不同,直接套用会得到错误结果。
| 版本 | 输出形状 | 坐标格式 | 类别概率 |
|---|---|---|---|
| YOLOv5 | 1×(5+C)×N | xywh | 单独objectness |
| YOLOv8 | 1×(4+C)×N | xyxy | 无objectness |
| YOLOv10 | 1×(4+C)×N | xyxy | 无NMS |
解:转换模型前务必确认YOLO版本,并对照输出格式编写后处理代码。建议在Python端用model.predict()验证输出形状后再移植。
4.2 LetterBox与坐标映射
坑:检测框位置偏移严重,或者框的大小不对。
原因:预处理使用了LetterBox(保持宽高比的缩放+填充),但后处理时没有正确映射回原始图像坐标。
解:后处理时需要反向映射:
// LetterBox坐标反映射
// 模型输出坐标 → 原始图像坐标
const origX = (modelX - padX) / scale;
const origY = (modelY - padY) / scale;
4.3 NMS性能问题
坑:NMS后处理耗时过长(>100ms),拖慢整体检测速度。
原因:纯ArkTS实现的NMS循环效率不高,尤其是候选框数量多时。
解:
- 提高置信度阈值,减少进入NMS的候选框数量
- 使用
Float32Array代替普通数组,减少GC压力 - HarmonyOS 6中可使用NPU内置NMS算子(如果模型支持)
4.4 模型尺寸选择
坑:直接部署YOLOv8x(最大模型),推理速度只有1-2FPS,完全不可用。
解:端侧推荐使用轻量级模型:
| 模型 | 参数量 | .ms文件大小 | 端侧推理速度 | mAP@50 |
|---|---|---|---|---|
| YOLOv8n | 3.2M | ~6MB | 30-50ms | 37.3 |
| YOLOv8s | 11.2M | ~22MB | 80-120ms | 44.9 |
| YOLOv8m | 25.9M | ~52MB | 200-300ms | 50.2 |
端侧首选YOLOv8n,在精度和速度之间取得最佳平衡。
五、HarmonyOS 6适配
5.1 新增特性
| 特性 | 说明 |
|---|---|
| NPU内置NMS | 模型可包含NMS算子,无需ArkTS后处理 |
| 动态输入尺寸 | 支持非固定尺寸输入,无需LetterBox |
| 检测模型专用API | 新增ObjectDetectionSession,简化调用 |
| 多帧聚合检测 | 支持视频流连续检测,自动跟踪 |
5.2 迁移指南
- NPU内置NMS:将NMS作为模型的一部分导出,推理后直接得到最终结果:
# Python端导出时包含NMS
from ultralytics import YOLO
model = YOLO('yolov8n.pt')
model.export(format='onnx', nms=True) # 导出时包含NMS
- 检测专用API(HarmonyOS 6新增):
// HarmonyOS 6 新增的检测专用接口
const detectionSession = model.createObjectDetectionSession(context);
detectionSession.setConfidenceThreshold(0.25);
detectionSession.setNMSThreshold(0.45);
const results = detectionSession.detect(pixelMap); // 直接返回Detection[]
- 视频流检测:
// HarmonyOS 6 支持连续帧检测
const streamDetector = model.createStreamDetector(context);
streamDetector.on('detection', (results: Detection[]) => {
// 每帧检测结果回调
this.updateDetections(results);
});
streamDetector.start(cameraStream); // 传入相机流
六、总结
本文完整讲解了YOLO目标检测模型在HarmonyOS端侧的部署方案,核心知识点回顾:
YOLO端侧部署
├── 模型选择
│ ├── YOLOv8n:端侧首选,6MB,30-50ms
│ ├── 输出格式:84×8400(4坐标+80类别)
│ └── INT8量化后体积再减4倍
├── 预处理
│ ├── LetterBox:保持宽高比缩放+灰色填充
│ ├── HWC→NCHW格式转换
│ └── ImageNet归一化
├── 后处理(核心难点)
│ ├── 输出转置:84×8400 → 8400×84
│ ├── 置信度过滤:threshold > 0.25
│ ├── NMS非极大值抑制:IoU阈值0.45
│ └── 坐标反映射:LetterBox逆变换
├── 可视化
│ ├── Canvas绘制检测框
│ ├── 类别颜色编码
│ └── 置信度标签显示
├── 性能优化
│ ├── 提高置信度阈值减少NMS负担
│ ├── Float32Array减少GC
│ └── NPU内置NMS(HarmonyOS 6)
└── 踩坑要点
├── YOLO版本间输出格式差异
├── LetterBox坐标映射
├── NMS性能瓶颈
└── 模型尺寸选择
一句话总结:YOLO端侧部署的难点不在推理,而在后处理——正确解析输出格式、高效实现NMS、精确映射坐标,这三者缺一不可。选对模型尺寸(YOLOv8n),做好LetterBox预处理和坐标反映射,你的端侧检测应用就能又快又准。
- 点赞
- 收藏
- 关注作者
评论(0)