HarmonyOS6.1 从图像分类到目标检测的扩展实现

前言

在前面的文章中,我们实现了图像分类应用。但在实际场景中,我们往往需要不仅识别物体类别,还要知道物体在图像中的具体位置。本文将介绍如何在 HarmonyOS 上实现目标检测功能,包括边界框检测、多对象识别和可视化展示。

目标检测 vs 图像分类

图像分类(Image Classification)

  • 输入: 一张图片

  • 输出: 类别标签 + 置信度

  • 应用: 图片标签、内容审核

    输入图片 → 模型 → "狗" (0.95)

目标检测(Object Detection)

  • 输入: 一张图片

  • 输出: 多个物体的类别、置信度、边界框坐标

  • 应用: 自动驾驶、安防监控、AR 应用

    输入图片 → 模型 → [
    { label: "狗", confidence: 0.95, box: [10, 20, 100, 150] },
    { label: "猫", confidence: 0.88, box: [200, 50, 150, 180] }
    ]

核心数据结构设计

边界框定义

typescript 复制代码
// DetectionTypes.ets

/**
 * 边界框坐标(归一化到 0-1)
 */
export interface BoundingBox {
  x: number      // 左上角 x 坐标
  y: number      // 左上角 y 坐标
  width: number  // 宽度
  height: number // 高度
}

/**
 * 检测结果
 */
export interface DetectionResult {
  label: string           // 类别标签
  confidence: number      // 置信度
  box: BoundingBox        // 边界框
  classId: number         // 类别 ID
}

/**
 * 检测配置
 */
export interface DetectionConfig {
  confidenceThreshold: number  // 置信度阈值
  iouThreshold: number         // NMS IoU 阈值
  maxDetections: number        // 最大检测数量
}

目标检测模型管理器

typescript 复制代码
// ObjectDetectionManager.ets
import { Context } from '@kit.AbilityKit'
import { DetectionResult, BoundingBox, DetectionConfig } from './DetectionTypes'

export class ObjectDetectionManager {
  private modelLoaded: boolean = false
  private modelName: string = 'yolov5s.ms'
  
  // COCO 数据集类别(简化版)
  private readonly 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'
  ]

  /**
   * 加载检测模型
   */
  async loadModel(context: Context): Promise<void> {
    console.info(`开始加载目标检测模型: ${this.modelName}`)
    const startTime = Date.now()

    try {
      const resMgr = context.resourceManager
      const descriptor = await resMgr.getRawFd(this.modelName)
      
      // 模拟模型加载
      await this.delay(2000)
      
      this.modelLoaded = true
      const loadTime = Date.now() - startTime
      console.info(`模型加载成功,耗时: ${loadTime}ms`)
    } catch (error) {
      const err = error as Error
      throw new Error(`模型加载失败: ${err.message}`)
    }
  }

  /**
   * 执行目标检测
   */
  async detect(
    imageUri: string,
    config: DetectionConfig = {
      confidenceThreshold: 0.5,
      iouThreshold: 0.45,
      maxDetections: 10
    }
  ): Promise<DetectionResult[]> {
    if (!this.modelLoaded) {
      throw new Error('模型未加载')
    }

    console.info('开始目标检测')
    const startTime = Date.now()

    try {
      // 1. 图像预处理
      await this.delay(300)

      // 2. 模型推理
      await this.delay(800)

      // 3. 后处理:NMS 等
      const detections = this.getMockDetections(config)

      const detectTime = Date.now() - startTime
      console.info(`检测完成,耗时: ${detectTime}ms,检测到 ${detections.length} 个对象`)

      return detections
    } catch (error) {
      const err = error as Error
      throw new Error(`检测失败: ${err.message}`)
    }
  }

  /**
   * 获取模拟检测结果
   */
  private getMockDetections(config: DetectionConfig): DetectionResult[] {
    const allDetections: DetectionResult[] = [
      {
        label: 'dog',
        confidence: 0.92,
        box: { x: 0.1, y: 0.2, width: 0.3, height: 0.4 },
        classId: 16
      },
      {
        label: 'cat',
        confidence: 0.85,
        box: { x: 0.5, y: 0.3, width: 0.25, height: 0.35 },
        classId: 15
      },
      {
        label: 'person',
        confidence: 0.78,
        box: { x: 0.7, y: 0.1, width: 0.2, height: 0.6 },
        classId: 0
      }
    ]

    // 应用置信度阈值过滤
    return allDetections
      .filter((det: DetectionResult): boolean => det.confidence >= config.confidenceThreshold)
      .slice(0, config.maxDetections)
  }

  /**
   * 格式化检测结果为文本
   */
  formatDetections(detections: DetectionResult[]): string {
    if (detections.length === 0) {
      return '未检测到任何对象'
    }

    let output = `🎯 检测到 ${detections.length} 个对象:\n\n`
    
    detections.forEach((det: DetectionResult, index: number): void => {
      output += `${index + 1}. ${det.label}\n`
      output += `   置信度: ${(det.confidence * 100).toFixed(1)}%\n`
      output += `   位置: (${(det.box.x * 100).toFixed(0)}%, ${(det.box.y * 100).toFixed(0)}%)\n`
      output += `   大小: ${(det.box.width * 100).toFixed(0)}% × ${(det.box.height * 100).toFixed(0)}%\n\n`
    })

    return output
  }

  private delay(ms: number): Promise<void> {
    return new Promise<void>((resolve: Function): void => {
      setTimeout((): void => resolve(), ms)
    })
  }
}

可视化组件:边界框绘制

typescript 复制代码
// BoundingBoxCanvas.ets
import { DetectionResult } from '../utils/DetectionTypes'

@Component
export struct BoundingBoxCanvas {
  @Prop detections: DetectionResult[] = []
  @Prop imageWidth: number = 300
  @Prop imageHeight: number = 300
  
  // 颜色映射
  private colors: string[] = [
    '#FF0000', '#00FF00', '#0000FF', '#FFFF00',
    '#FF00FF', '#00FFFF', '#FFA500', '#800080'
  ]

  build() {
    Stack() {
      // 边界框和标签
      ForEach(this.detections, (det: DetectionResult, index: number) => {
        this.buildBoundingBox(det, index)
      })
    }
    .width(this.imageWidth)
    .height(this.imageHeight)
  }

  @Builder
  buildBoundingBox(detection: DetectionResult, index: number): void {
    Stack() {
      // 边界框
      Column()
        .width(`${detection.box.width * 100}%`)
        .height(`${detection.box.height * 100}%`)
        .border({
          width: 3,
          color: this.colors[index % this.colors.length]
        })
        .borderRadius(4)

      // 标签背景
      Column() {
        Text(`${detection.label} ${(detection.confidence * 100).toFixed(0)}%`)
          .fontSize(12)
          .fontColor(Color.White)
          .padding({ left: 5, right: 5, top: 2, bottom: 2 })
      }
      .backgroundColor(this.colors[index % this.colors.length])
      .borderRadius({ topLeft: 4, topRight: 4 })
      .position({
        x: `${detection.box.x * 100}%`,
        y: `${detection.box.y * 100}%`
      })
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.TopStart)
  }
}

完整 UI 实现

typescript 复制代码
// DetectionPage.ets
import { ObjectDetectionManager } from '../utils/ObjectDetectionManager'
import { DetectionResult, DetectionConfig } from '../utils/DetectionTypes'
import { BoundingBoxCanvas } from '../components/BoundingBoxCanvas'
import promptAction from '@ohos.promptAction'
import picker from '@ohos.file.picker'

@Entry
@Component
struct DetectionPage {
  @State modelLoaded: boolean = false
  @State selectedImageUri: string = ''
  @State detections: DetectionResult[] = []
  @State resultText: string = ''
  @State isProcessing: boolean = false
  @State showBoxes: boolean = true

  private detectionManager: ObjectDetectionManager = new ObjectDetectionManager()

  build() {
    Scroll() {
      Column({ space: 15 }) {
        // 标题
        this.buildHeader()

        // 模型状态
        this.buildModelStatus()

        // 操作按钮
        this.buildActions()

        // 图像预览 + 边界框
        if (this.selectedImageUri) {
          this.buildImageWithBoxes()
        }

        // 检测结果文本
        this.buildResultText()
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildHeader(): void {
    Column() {
      Text('🎯 目标检测')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 10 })

      Text('多对象识别与定位')
        .fontSize(14)
        .fontColor('#999999')
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  buildModelStatus(): void {
    Row() {
      Column() {
        Text('模型状态')
          .fontSize(14)
          .fontColor('#666666')
        Text(this.modelLoaded ? '✅ YOLOv5s 已就绪' : '⏸ 未加载')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.modelLoaded ? '#00AA00' : '#FF6B6B')
          .margin({ top: 5 })
      }
      .alignItems(HorizontalAlign.Start)

      Blank()

      Column() {
        Text(`检测数: ${this.detections.length}`)
          .fontSize(14)
          .fontColor('#666666')
      }
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

  @Builder
  buildActions(): void {
    Column({ space: 10 }) {
      Button(this.isProcessing && !this.modelLoaded ? '加载中...' : '加载检测模型')
        .width('100%')
        .height(50)
        .enabled(!this.modelLoaded && !this.isProcessing)
        .onClick((): void => {
          this.loadModel()
        })

      Button('📷 选择图片')
        .width('100%')
        .height(50)
        .enabled(this.modelLoaded && !this.isProcessing)
        .backgroundColor('#4CAF50')
        .onClick((): void => {
          this.pickImage()
        })

      Button(this.isProcessing && this.modelLoaded ? '检测中...' : '🔍 开始检测')
        .width('100%')
        .height(50)
        .enabled(this.modelLoaded && !this.isProcessing && this.selectedImageUri.length > 0)
        .backgroundColor('#FF9800')
        .onClick((): void => {
          this.runDetection()
        })

      // 切换边界框显示
      Row() {
        Text('显示边界框')
          .fontSize(16)
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.showBoxes })
          .onChange((isOn: boolean): void => {
            this.showBoxes = isOn
          })
      }
      .width('100%')
      .padding(15)
      .backgroundColor(Color.White)
      .borderRadius(8)
    }
  }

  @Builder
  buildImageWithBoxes(): void {
    Column() {
      Text('🖼 检测结果可视化')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .alignSelf(ItemAlign.Start)
        .margin({ bottom: 10 })

      Stack() {
        // 原始图像
        Image(this.selectedImageUri)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Contain)
          .borderRadius(8)

        // 边界框叠加层
        if (this.showBoxes && this.detections.length > 0) {
          BoundingBoxCanvas({
            detections: this.detections,
            imageWidth: 300,
            imageHeight: 300
          })
        }
      }
      .width('100%')
      .height(300)
      .backgroundColor(Color.White)
      .borderRadius(8)
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

  @Builder
  buildResultText(): void {
    Column() {
      Text('📊 检测详情')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .alignSelf(ItemAlign.Start)
        .margin({ bottom: 10 })

      Text(this.resultText || '请选择图片并开始检测')
        .width('100%')
        .padding(15)
        .backgroundColor('#FAFAFA')
        .borderRadius(8)
        .fontSize(14)
        .lineHeight(22)
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

  private async loadModel(): Promise<void> {
    this.isProcessing = true
    try {
      await this.detectionManager.loadModel(getContext(this))
      this.modelLoaded = true
      promptAction.showToast({ message: '✅ 模型加载成功' })
    } catch (error) {
      const err = error as Error
      promptAction.showToast({ message: `❌ ${err.message}` })
    }
    this.isProcessing = false
  }

  private async pickImage(): Promise<void> {
    try {
      const photoPicker: picker.PhotoViewPicker = new picker.PhotoViewPicker()
      const result: picker.PhotoSelectResult = await photoPicker.select({
        MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      })
      if (result.photoUris.length > 0) {
        this.selectedImageUri = result.photoUris[0]
        this.detections = []
        this.resultText = ''
        promptAction.showToast({ message: '图片选择成功' })
      }
    } catch (error) {
      const err = error as Error
      promptAction.showToast({ message: `选择失败: ${err.message}` })
    }
  }

  private async runDetection(): Promise<void> {
    this.isProcessing = true
    this.resultText = '⏳ 正在检测...'

    try {
      const config: DetectionConfig = {
        confidenceThreshold: 0.5,
        iouThreshold: 0.45,
        maxDetections: 10
      }

      this.detections = await this.detectionManager.detect(this.selectedImageUri, config)
      this.resultText = this.detectionManager.formatDetections(this.detections)

      promptAction.showToast({ message: `✅ 检测到 ${this.detections.length} 个对象` })
    } catch (error) {
      const err = error as Error
      this.resultText = `❌ 检测失败: ${err.message}`
      promptAction.showToast({ message: '检测失败' })
    }
    this.isProcessing = false
  }
}

关键技术点

1. NMS (非极大值抑制)

目标检测模型通常会产生大量重叠的检测框,NMS 用于过滤冗余框:

typescript 复制代码
/**
 * 计算 IoU (Intersection over Union)
 */
function calculateIoU(box1: BoundingBox, box2: BoundingBox): number {
  const x1 = Math.max(box1.x, box2.x)
  const y1 = Math.max(box1.y, box2.y)
  const x2 = Math.min(box1.x + box1.width, box2.x + box2.width)
  const y2 = Math.min(box1.y + box1.height, box2.y + box2.height)

  const intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1)
  const box1Area = box1.width * box1.height
  const box2Area = box2.width * box2.height
  const unionArea = box1Area + box2Area - intersectionArea

  return intersectionArea / unionArea
}

/**
 * NMS 算法
 */
function applyNMS(
  detections: DetectionResult[],
  iouThreshold: number
): DetectionResult[] {
  // 按置信度降序排序
  const sorted = detections.sort((a, b) => b.confidence - a.confidence)
  const keep: DetectionResult[] = []

  while (sorted.length > 0) {
    const current = sorted.shift()!
    keep.push(current)

    // 移除与当前框 IoU 超过阈值的框
    for (let i = sorted.length - 1; i >= 0; i--) {
      if (calculateIoU(current.box, sorted[i].box) > iouThreshold) {
        sorted.splice(i, 1)
      }
    }
  }

  return keep
}

2. 坐标归一化

模型输出通常是归一化坐标 (0-1),需要转换为像素坐标:

typescript 复制代码
function denormalizeBox(
  box: BoundingBox,
  imageWidth: number,
  imageHeight: number
): BoundingBox {
  return {
    x: box.x * imageWidth,
    y: box.y * imageHeight,
    width: box.width * imageWidth,
    height: box.height * imageHeight
  }
}

3. Canvas 绘制优化

typescript 复制代码
// 使用 Stack 组件实现图层叠加
Stack() {
  Image(imageUri)  // 底层:原图
  Canvas()         // 顶层:边界框
}

实际应用场景

1. 积木识别系统

typescript 复制代码
interface BlockInfo {
  type: string        // 积木类型
  color: string       // 颜色
  position: { x: number, y: number }
  orientation: number // 方向角度
}

class BlockDetector {
  async detectBlocks(imageUri: string): Promise<BlockInfo[]> {
    const detections = await this.detector.detect(imageUri)
    return detections.map((det): BlockInfo => ({
      type: this.getBlockType(det.classId),
      color: this.getBlockColor(det),
      position: this.getCenter(det.box),
      orientation: 0
    }))
  }
}

2. 人脸检测

typescript 复制代码
interface FaceDetection {
  box: BoundingBox
  landmarks: { x: number, y: number }[]  // 关键点
  age?: number
  gender?: string
}

3. 车牌识别

typescript 复制代码
interface PlateDetection {
  box: BoundingBox
  plateNumber: string
  confidence: number
}

性能优化建议

1. 模型选择

模型 大小 速度 精度 适用场景
YOLOv5n 4MB 实时应用
YOLOv5s 14MB 较快 较高 通用场景
YOLOv5m 42MB 高精度需求

2. 输入分辨率

typescript 复制代码
// 降低输入分辨率提升速度
const configs = {
  fast: { width: 320, height: 320 },     // 快速模式
  balanced: { width: 640, height: 640 }, // 平衡模式
  accurate: { width: 1280, height: 1280 }  // 精确模式
}

3. 批处理

typescript 复制代码
// 批量处理多张图片
async detectBatch(imageUris: string[]): Promise<DetectionResult[][]> {
  return Promise.all(imageUris.map(uri => this.detect(uri)))
}

常见问题解决

Q1: 检测速度慢?

解决方案

  1. 使用更小的模型(YOLOv5n)
  2. 降低输入分辨率
  3. 启用硬件加速
  4. 减少检测频率

Q2: 小物体检测不到?

解决方案

  1. 提高输入分辨率
  2. 降低置信度阈值
  3. 使用专门的小目标检测模型

Q3: 边界框抖动?

解决方案

  1. 使用卡尔曼滤波平滑
  2. 增加时间一致性约束
  3. 提高 IoU 阈值

扩展功能

1. 视频流检测

typescript 复制代码
class VideoDetector {
  private frameSkip: number = 3  // 每3帧检测一次

  async processVideoFrame(frameData: ArrayBuffer): Promise<DetectionResult[]> {
    // 处理视频帧
    return []
  }
}

2. 实时追踪

typescript 复制代码
interface TrackedObject {
  id: number
  detections: DetectionResult[]
  trajectory: { x: number, y: number }[]
}

3. 3D 边界框

typescript 复制代码
interface BoundingBox3D extends BoundingBox {
  depth: number
  rotation: { x: number, y: number, z: number }
}

总结

本文详细介绍了在 HarmonyOS 上实现目标检测功能:

  1. ✅ 目标检测原理与数据结构
  2. ✅ 检测模型管理器实现
  3. ✅ 边界框可视化组件
  4. ✅ 完整 UI 交互实现
  5. ✅ NMS 算法详解
  6. ✅ 实际应用场景扩展
  7. ✅ 性能优化建议

通过本系列文章的学习,你已经掌握了:

  • HarmonyOS AI 应用的完整开发流程
  • 从图像分类到目标检测的技术演进
  • 架构设计和最佳实践
  • 实际项目中的优化技巧

系列文章回顾

  1. HarmonyOS6.1 端侧 AI 模型加载与推理入门
  2. HarmonyOS6.1 图像分类应用完整实战
  3. HarmonyOS6.1 AI 模型管理架构设计与最佳实践
  4. HarmonyOS6.1 从图像分类到目标检测的扩展实现(本文)

参考资料

  1. YOLOv5 官方文档
  2. MindSpore Lite 目标检测示例
  3. COCO 数据集

感谢阅读完整系列!点赞👍 收藏⭐ 关注👀 支持原创!

有问题欢迎在评论区讨论交流!

相关推荐
智联物联3 小时前
办公楼转型养老公寓,边缘计算网关实现全场景智慧监护
人工智能·边缘计算·物联网解决方案·工业网关·智慧养老·数采网关·边缘盒子
库拉大叔3 小时前
工具调用效率对比实测:GPT-5.5与Gemini 3.5 Flash性能评估
java·前端·人工智能
智讯天下3 小时前
专业的高端智能照明品牌哪家好?从光学技术、系统稳定性、设计认证、服务保障四个维度看
人工智能·智能手机
xiami_world3 小时前
2026年UI/UX设计工具私有化部署方案深度解析
人工智能·ui·ai·产品经理·ux
无忧智库4 小时前
基于C4ISR与数据链的智慧应急体系:从“透明战场”到“透明城市”的数字化指挥解决方案(170页PPT)
大数据·人工智能·智慧城市
罗小罗同学4 小时前
哈佛团队在Nat Med发表医学AI模型,可以在任务推理阶段实时调整推理方式,无需重新训练
人工智能·医学图像处理·医工交叉·医学ai
杭州默安科技4 小时前
AI挖掘0day漏洞常态化,企业网络防御该如何破局?
人工智能·网络安全
Rauser Mack4 小时前
不懂编程,但是vibe coding一个扫雷游戏
人工智能·python·游戏·html·prompt