HarmonyOS鸿蒙Next中如何实现二维码扫描和图像识别功能?

HarmonyOS鸿蒙Next中如何实现二维码扫描和图像识别功能? 在HarmonyOS应用中如何调用相机拍照和录像?如何实现二维码扫描和图像识别功能?

5 回复

默认界面扫码

默认界面扫码能力提供系统级体验一致的扫码界面,包含相机预览流,相册扫码入口,暗光环境闪光灯开启提示。Scan Kit默认界面扫码对系统相机权限进行了预授权且调用期间处于安全访问状态,无需开发者再次申请相机权限。适用于不同扫码场景的应用开发。

说明

通过默认界面扫码可以实现应用内的扫码功能,为了应用更好的体验,推荐同时接入“扫码直达”服务,应用可以同时支持系统扫码入口(控制中心扫一扫)和应用内扫码两种方式跳转到指定服务页面。

场景介绍

默认界面扫码能力提供了系统级体验一致的扫码界面以及相册扫码入口,支持单码和多码识别,支持多种识码类型,请参见ScanType。无需使用三方库就可帮助开发者的应用快速处理各种扫码场景。

默认扫码界面UX:

默认扫码界面UX

业务流程

使用默认界面扫码的主要业务流程如下:

业务流程

  1. 用户向开发者的应用发起扫码请求。
  2. 开发者的应用通过调用Scan Kit的startScanForResult接口启动扫码界面。
  3. 系统首次使用默认界面扫码功能时,会向用户弹出隐私横幅提醒。
  4. 用户可以点击关闭隐私横幅,重新打开应用的扫码界面将不再显示隐私横幅提醒,显示安全访问提示,3s后消失。
  5. Scan Kit通过Callback回调函数或Promise方式返回扫码结果。
  6. 用户进行多码扫描时,需点击选择其中一个码图获取扫码结果返回。单码扫描则可直接返回扫码结果。
  7. 解析码值结果跳转应用服务页。

接口说明

接口返回值有两种返回形式:Callback和Promise回调。下表中为默认界面扫码Callback和Promise形式接口,Callback和Promise只是返回值方式不一样,功能相同。startScanForResult接口打开的是应用内呈现的扫码界面样式。具体API说明详见接口文档

接口名 描述
startScanForResult(context: common.Context, options?: ScanOptions): Promise<ScanResult> 启动默认界面扫码,通过ScanOptions进行扫码参数设置,使用Promise异步回调返回扫码结果。
startScanForResult(context: common.Context, options: ScanOptions, callback: AsyncCallback<ScanResult>): void 启动默认界面扫码,通过ScanOptions进行扫码参数设置,使用Callback异步回调返回扫码结果。
startScanForResult(context: common.Context, callback: AsyncCallback<ScanResult>): void 启动默认界面扫码,使用Callback异步回调返回扫码结果。

说明

startScanForResult接口需要在页面和组件的生命周期内调用。若需要设置扫码页面为全屏或沉浸式,请参见开发应用沉浸式效果

开发步骤

Scan Kit提供了默认界面扫码的能力,由扫码接口直接控制相机实现最优的相机放大控制、自适应的曝光调节、自适应对焦调节等操作,保障流畅的扫码体验,减少开发者的工作量。

为了方便开发者接入,我们提供了详细的样例工程供参考,推荐参考示例工程接入。

以下示例为调用Scan Kit的startScanForResult接口跳转扫码页面。

  1. 导入默认界面扫码模块,scanCore提供扫码类型定义,scanBarcode提供拉起默认界面扫码的方法和参数,导入方法如下。
  2. 调用startScanForResult方法拉起默认界面扫码。
  3. 通过Promise方式得到扫码结果。
  4. 通过Callback回调函数得到扫码结果。
import { scanCore, scanBarcode } from '@kit.ScanKit';
// 导入默认界面扫码需要的日志模块和错误码模块
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct ScanBarCodePage {
  build() {
    Column() {
      Row() {
        Button("Promise with options")
          .backgroundColor('#0D9FFB')
          .fontSize(20)
          .fontColor($r('sys.color.comp_background_list_card'))
          .fontWeight(FontWeight.Normal)
          .align(Alignment.Center)
          .type(ButtonType.Capsule)
          .width('90%')
          .height(40)
          .margin({ top: 5, bottom: 5 })
          .onClick(() => {
            // 定义扫码参数options
            let options: scanBarcode.ScanOptions = {
              scanTypes: [scanCore.ScanType.ALL],
              enableMultiMode: true,
              enableAlbum: true
            };
            try {
              // 可调用getHostContext接口获取当前页面关联的Context
              scanBarcode.startScanForResult(this.getUIContext().getHostContext(), options).then((result: scanBarcode.ScanResult) => {
                // 解析码值结果跳转应用服务页
                hilog.info(0x0001, '[Scan CPSample]', `Succeeded in getting ScanResult by promise with options, result is ${JSON.stringify(result)}`);
              }).catch((error: BusinessError) => {
                hilog.error(0x0001, '[Scan CPSample]',
                  `Failed to get ScanResult by promise with options. Code:${error.code}, message: ${error.message}`);
              });
            } catch (error) {
              hilog.error(0x0001, '[Scan CPSample]',
                `Failed to start the scanning service. Code:${error.code}, message: ${error.message}`);
            }
          })
      }
      .height('100%')
    }
    .width('100%')
  }
}
@Entry
@Component
struct ScanBarCodePage {
  build() {
    Column() {
      Row() {
        Button('Callback with options')
          .backgroundColor('#0D9FFB')
          .fontSize(20)
          .fontColor($r('sys.color.comp_background_list_card'))
          .fontWeight(FontWeight.Normal)
          .align(Alignment.Center)
          .type(ButtonType.Capsule)
          .width('90%')
          .height(40)
          .margin({ top: 5, bottom: 5 })
          .onClick(() => {
            // 定义扫码参数options
            let options: scanBarcode.ScanOptions = {
              scanTypes: [scanCore.ScanType.ALL],
              enableMultiMode: true,
              enableAlbum: true
            };
            try {
              // 可调用getHostContext接口获取当前页面关联的Context
              scanBarcode.startScanForResult(this.getUIContext().getHostContext(), options,
                (error: BusinessError, result: scanBarcode.ScanResult) => {
                  if (error) {
                    hilog.error(0x0001, '[Scan CPSample]',
                      `Failed to get ScanResult by callback with options. Code: ${error.code}, message: ${error.message}`);
                    return;
                  }
                  // 解析码值结果跳转应用服务页
                  hilog.info(0x0001, '[Scan CPSample]', `Succeeded in getting ScanResult by callback with options, result is ${JSON.stringify(result)}`);
                })
            } catch (error) {
              hilog.error(0x0001, '[Scan CPSample]',
                `Failed to start the scanning service. Code:${error.code}, message: ${error.message}`);
            }
          })
      }
      .height('100%')
    }
    .width('100%')
  }
}

更多关于HarmonyOS鸿蒙Next中如何实现二维码扫描和图像识别功能?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


解决方案

1. 使用Picker拍照

import picker from '@ohos.file.picker'
import fs from '@ohos.file.fs'
import { BusinessError } from '@ohos.base'

@Entry
@Component
struct CameraPicker {
  @State imageUri: string = ''

  build() {
    Column({ space: 16 }) {
      Text('拍照示例')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Button('打开相机拍照')
        .width('100%')
        .onClick(() => {
          this.takePhoto()
        })

      Button('从相册选择')
        .width('100%')
        .onClick(() => {
          this.selectPhoto()
        })

      if (this.imageUri) {
        Image(this.imageUri)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Contain)
          .margin({ top: 16 })
      }
    }
    .padding(16)
  }

  private async takePhoto() {
    try {
      const photoPicker = new picker.PhotoViewPicker()
      const result = await photoPicker.select({
        MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1,
        isPhotoTakingSupported: true // 支持拍照
      })

      if (result && result.photoUris.length > 0) {
        this.imageUri = result.photoUris[0]
        console.log('拍照成功:', this.imageUri)
      }
    } catch (error) {
      const err = error as BusinessError
      console.error('拍照失败:', err.message)
    }
  }

  private async selectPhoto() {
    try {
      const photoPicker = new picker.PhotoViewPicker()
      const result = await photoPicker.select({
        MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      })

      if (result && result.photoUris.length > 0) {
        this.imageUri = result.photoUris[0]
        console.log('选择照片:', this.imageUri)
      }
    } catch (error) {
      const err = error as BusinessError
      console.error('选择照片失败:', err.message)
    }
  }
}

2. 自定义相机拍照

import camera from '@ohos.multimedia.camera'
import image from '@ohos.multimedia.image'
import { BusinessError } from '@ohos.base'

@Entry
@Component
struct CustomCamera {
  private surfaceId: string = ''
  private xComponentController: XComponentController = new XComponentController()
  private cameraManager?: camera.CameraManager
  private cameraInput?: camera.CameraInput
  private previewOutput?: camera.PreviewOutput
  private photoOutput?: camera.PhotoOutput
  private session?: camera.PhotoSession
  @State capturedImage: string = ''

  async aboutToAppear() {
    await this.initCamera()
  }

  aboutToDisappear() {
    this.releaseCamera()
  }

  build() {
    Stack() {
      // 相机预览
      XComponent({
        id: 'camera_preview',
        type: 'surface',
        controller: this.xComponentController
      })
        .onLoad(() => {
          this.surfaceId = this.xComponentController.getXComponentSurfaceId()
          this.startPreview()
        })
        .width('100%')
        .height('100%')

      // 控制层
      Column() {
        Blank()

        Row() {
          Button('拍照')
            .type(ButtonType.Circle)
            .width(70)
            .height(70)
            .backgroundColor(Color.White)
            .onClick(() => {
              this.takePhoto()
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding(32)
      }
      .width('100%')
      .height('100%')

      // 预览拍摄的照片
      if (this.capturedImage) {
        Column() {
          Image(this.capturedImage)
            .width('100%')
            .height('80%')
            .objectFit(ImageFit.Contain)

          Row({ space: 16 }) {
            Button('重新拍照')
              .onClick(() => {
                this.capturedImage = ''
              })

            Button('保存')
              .onClick(() => {
                this.savePhoto()
              })
          }
          .padding(16)
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Black)
      }
    }
  }

  private async initCamera() {
    try {
      // 获取相机管理器
      this.cameraManager = camera.getCameraManager(getContext(this))

      // 获取相机列表
      const cameras = this.cameraManager.getSupportedCameras()
      if (cameras.length === 0) {
        console.error('没有可用相机')
        return
      }

      // 创建相机输入
      this.cameraInput = this.cameraManager.createCameraInput(cameras[0])

      // 打开相机
      await this.cameraInput.open()
    } catch (error) {
      const err = error as BusinessError
      console.error('初始化相机失败:', err.message)
    }
  }

  private async startPreview() {
    if (!this.cameraManager || !this.cameraInput || !this.surfaceId) return

    try {
      // 创建预览输出
      const profile = this.cameraManager.getSupportedOutputCapability(
        this.cameraInput.cameraDevice
      ).previewProfiles[0]
      this.previewOutput = this.cameraManager.createPreviewOutput(
        profile,
        this.surfaceId
      )

      // 创建拍照输出
      const photoProfile = this.cameraManager.getSupportedOutputCapability(
        this.cameraInput.cameraDevice
      ).photoProfiles[0]
      this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile)

      // 创建会话
      this.session = this.cameraManager.createPhotoSession() as camera.PhotoSession

      // 配置会话
      this.session.beginConfig()
      this.session.addInput(this.cameraInput)
      this.session.addOutput(this.previewOutput)
      this.session.addOutput(this.photoOutput)
      await this.session.commitConfig()

      // 开始预览
      await this.session.start()
      console.log('相机预览已启动')
    } catch (error) {
      const err = error as BusinessError
      console.error('启动预览失败:', err.message)
    }
  }

  private async takePhoto() {
    if (!this.photoOutput) return

    try {
      // 设置拍照回调
      this.photoOutput.on('photoAvailable', (err, photo) => {
        if (err) {
          console.error('拍照失败:', err)
          return
        }

        // 处理拍摄的照片
        this.processPhoto(photo)
      })

      // 拍照
      await this.photoOutput.capture()
    } catch (error) {
      const err = error as BusinessError
      console.error('拍照失败:', err.message)
    }
  }

  private async processPhoto(photo: camera.Photo) {
    try {
      // 获取图像数据
      const imageReceiver = image.createImageReceiver(
        photo.main.size.width,
        photo.main.size.height,
        image.ImageFormat.JPEG,
        1
      )

      // 读取图像
      const imageObj = await imageReceiver.readNextImage()
      const imageComponent = await imageObj.getComponent(image.ComponentType.JPEG)

      if (imageComponent && imageComponent.byteBuffer) {
        // 保存到临时文件
        const filesDir = getContext(this).cacheDir
        const filePath = `${filesDir}/photo_${Date.now()}.jpg`
        const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
        fs.writeSync(file.fd, imageComponent.byteBuffer)
        fs.closeSync(file.fd)

        this.capturedImage = `file://${filePath}`
        console.log('照片已保存:', filePath)
      }

      imageObj.release()
      imageReceiver.release()
    } catch (error) {
      console.error('处理照片失败:', error)
    }
  }

  private savePhoto() {
    console.log('保存照片:', this.capturedImage)
    // 实现保存到相册的逻辑
    this.capturedImage = ''
  }

  private async releaseCamera() {
    try {
      if (this.session) {
        await this.session.stop()
        await this.session.release()
      }
      if (this.photoOutput) {
        await this.photoOutput.release()
      }
      if (this.previewOutput) {
        await this.previewOutput.release()
      }
      if (this.cameraInput) {
        await this.cameraInput.close()
      }
    } catch (error) {
      console.error('释放相机资源失败:', error)
    }
  }
}

3. 二维码扫描

import scanBarcode from '@ohos.scanBarcode'
import { BusinessError } from '@ohos.base'

@Entry
@Component
struct QRCodeScanner {
  @State scanResult: string = ''

  build() {
    Column({ space: 16 }) {
      Text('二维码扫描')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Button('扫描二维码')
        .width('100%')
        .onClick(() => {
          this.startScan()
        })

      if (this.scanResult) {
        Column({ space: 8 }) {
          Text('扫描结果:')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)

          Text(this.scanResult)
            .fontSize(14)
            .fontColor('#666666')
            .padding(16)
            .backgroundColor('#f5f5f5')
            .borderRadius(8)
            .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
      }
    }
    .padding(16)
  }

  private async startScan() {
    try {
      const options: scanBarcode.ScanOptions = {
        scanTypes: [scanBarcode.ScanType.ALL],
        enableMultiMode: false,
        enableAlbum: true
      }

      const result = await scanBarcode.startScanForResult(getContext(this), options)
      
      if (result && result.originalValue) {
        this.scanResult = result.originalValue
        console.log('扫描结果:', this.scanResult)
        
        // 处理扫描结果
        this.handleScanResult(this.scanResult)
      }
    } catch (error) {
      const err = error as BusinessError
      console.error('扫描失败:', err.message)
    }
  }

  private handleScanResult(result: string) {
    // 判断结果类型
    if (result.startsWith('http://') || result.startsWith('https://')) {
      console.log('这是一个URL')
      // 可以打开浏览器或WebView
    } else if (result.match(/^[0-9]+$/)) {
      console.log('这是一个数字')
      // 处理数字类型
    } else {
      console.log('其他类型的内容')
    }
  }
}

4. 自定义扫码界面

import scanBarcode from '@ohos.scanBarcode'
import camera from '@ohos.multimedia.camera'

@Entry
@Component
struct CustomScanner {
  private surfaceId: string = ''
  private xComponentController: XComponentController = new XComponentController()
  private cameraManager?: camera.CameraManager
  private session?: camera.VideoSession
  @State scanResult: string = ''
  @State isScanning: boolean = false

  build() {
    Stack() {
      // 相机预览
      XComponent({
        id: 'scanner_preview',
        type: 'surface',
        controller: this.xComponentController
      })
        .onLoad(() => {
          this.surfaceId = this.xComponentController.getXComponentSurfaceId()
          this.startCamera()
        })
        .width('100%')
        .height('100%')

      // 扫描框
      Column() {
        Blank()

        // 扫描区域
        Stack() {
          Column()
            .width(280)
            .height(280)
            .border({
              width: 2,
              color: Color.White,
              radius: 12
            })

          // 扫描线动画
          if (this.isScanning) {
            Row()
              .width(260)
              .height(2)
              .backgroundColor(Color.Green)
              // 添加动画效果
          }
        }

        Blank()

        // 提示文字
        Text(this.scanResult || '请将二维码放入框内')
          .fontSize(16)
          .fontColor(Color.White)
          .padding(16)
          .backgroundColor('rgba(0,0,0,0.5)')
          .borderRadius(8)
          .margin({ bottom: 32 })
      }
      .width('100%')
      .height('100%')
    }
    .backgroundColor(Color.Black)
  }

  private async startCamera() {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this))
      const cameras = this.cameraManager.getSupportedCameras()
      
      if (cameras.length > 0) {
        // 初始化相机并开始扫描
        this.isScanning = true
        console.log('相机已启动,开始扫描')
      }
    } catch (error) {
      console.error('启动相机失败:', error)
    }
  }

  aboutToDisappear() {
    this.isScanning = false
    // 释放相机资源
  }
}

5. 生成二维码

import image from '@ohos.multimedia.image'
import scanBarcode from '@ohos.scanBarcode'

@Entry
@Component
struct QRCodeGenerator {
  @State inputText: string = 'https://www.example.com'
  @State qrCodeImage?: image.PixelMap

  build() {
    Column({ space: 16 }) {
      Text('二维码生成')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      TextInput({
        text: this.inputText,
        placeholder: '请输入要生成二维码的内容'
      })
        .width('100%')
        .onChange((value) => {
          this.inputText = value
        })

      Button('生成二维码')
        .width('100%')
        .onClick(() => {
          this.generateQRCode()
        })

      if (this.qrCodeImage) {
        Image(this.qrCodeImage)
          .width(280)
          .height(280)
          .margin({ top: 16 })
          .backgroundColor(Color.White)
          .padding(16)
      }
    }
    .padding(16)
  }

  private async generateQRCode() {
    try {
      const options: scanBarcode.GenerateOptions = {
        scanType: scanBarcode.ScanType.QR_CODE,
        height: 280,
        width: 280,
        margin: 1
      }

      this.qrCodeImage = await scanBarcode.generateBarcode(this.inputText, options)
      console.log('二维码生成成功')
    } catch (error) {
      const err = error as BusinessError
      console.error('生成二维码失败:', err.message)
    }
  }
}

关键要点

  1. 权限申请: 使用相机需要申请ohos.permission.CAMERA权限
  2. Picker方式: 使用PhotoViewPicker是最简单的拍照方式
  3. 自定义相机: 使用Camera API可以完全控制拍照流程
  4. 二维码扫描: 使用scanBarcode模块实现扫码功能
  5. 资源释放: 使用完相机后必须释放资源

最佳实践

  1. 用户体验: 提供清晰的拍照引导和扫描提示
  2. 性能优化: 及时释放相机和图像资源
  3. 错误处理: 处理权限拒绝、设备不支持等异常
  4. 图像质量: 根据场景选择合适的分辨率和质量
  5. 隐私保护: 明确告知用户相机使用目的

鸿蒙Next中二维码扫描和图像识别功能主要通过ArkUI框架和系统API实现。使用@ohos.zbar库进行二维码扫描,调用scanCode()方法可获取结果。图像识别需集成AI能力,通过@ohos.ai.image相关接口处理图像数据,支持物体检测和分类。开发时需在module.json5中声明ohos.permission.CAMERA等权限。具体实现代码可参考官方文档中的多媒体和AI服务章节。

在HarmonyOS Next中,实现二维码扫描和图像识别功能主要依赖于ArkUI框架和系统提供的相机、图像处理能力。以下是核心实现路径:

1. 调用相机拍照和录像

使用@ohos.multimedia.camera@ohos.multimedia.image等系统API。

  • 权限声明:在module.json5中声明ohos.permission.CAMERA等必要权限。
  • 相机管理:通过CameraManager获取相机设备列表,创建CameraInputPreviewOutput(预览)、PhotoOutput(拍照)或VideoOutput(录像)等会话。
  • 会话控制:在CaptureSession中配置输出流,启动预览、拍照或录像。

2. 二维码扫描

基于相机捕获的图像数据,使用@ohos.zbar(二维码扫描库)或@ohos.mlkit(ML Kit提供二维码识别)进行解析。

  • 流程:相机预览 → 获取图像帧 → 调用识别库解析二维码数据。
  • 关键APIzbar.scan()mlkit.barcode.scan()

3. 图像识别

利用@ohos.ai.image(图像分析)或@ohos.mlkit(机器学习套件)实现。

  • 能力范围:可进行物体检测、图像分类、文字识别等。
  • 基本步骤:获取图像源(相机或图库) → 转换为PixelMap → 调用识别模型(如ImageClassifier) → 获取识别结果。

代码示例(简化版)

// 1. 相机预览和捕获
import camera from '@ohos.multimedia.camera';
// 初始化相机,配置输出流...

// 2. 二维码扫描
import zbar from '@ohos.zbar';
let result = zbar.scan(imageData); // imageData来自相机帧

// 3. 图像识别(以分类为例)
import image from '@ohos.ai.image';
let classifier = new image.ImageClassifier();
let classificationResult = classifier.classify(pixelMap);

注意事项

  • 需在真机或支持相机模拟的模拟器上测试。
  • 图像识别功能可能依赖模型文件,需按规范部署。
  • 关注API版本兼容性,确保使用的接口在目标SDK中可用。

通过组合相机控制、图像捕获和AI分析API,可高效实现扫码与识别功能。建议参考官方文档中的完整示例和API详细说明进行开发。

回到顶部