HarmonyOS 6.1 全栈实战录 - 79 驶入视觉新纪元:实战 Camera Kit 全质量未压缩图调度、HDR动态照片与元数据实时增删中控舱

1、引言

在智能移动设备中,相机服务(Camera Kit)的性能与易用性直接决定了社交、扫码、AR 等核心场景的体验。随着影像技术的不断突破,开发者面临着更加极致的硬件调度挑战:如何在保障流畅度的同时获取高达上千万像素的未压缩 YUV 原图?如何让拍摄出来的动态照片(Live Photo)在亮暗对比上展现如临其境的高动态范围(HDR)?如何对复杂的实时图像识别流(元数据)进行动态的算法增减,而无需频繁重启沉重的相机硬件会话?
image.png

HarmonyOS NEXT 6.1 (API 23) 为相机的这些深水区难题,递交了全新的满分答卷:

  • 全质量与未压缩图统一监听:引入全新的回调直接回传物理照片,为高性能影像处理与定制滤镜提供了原生通道。
  • 全链路 HDR 动态照片:静态图与短视频全线高动态化,极致展现高光、暗部与层次感。
  • 元数据对象类型实时增删:支持零重建延迟的运行期对象动态调整。

本篇我们将通过构建一个「视觉新纪元中控舱」项目,全景式展示这三项相机底层更新,带你攻克先进摄影的核心逻辑。
image.png

2、效果展示与项目结构
2.1 运行效果展示

本节构建的控制舱——视觉新纪元中控舱,包含了三大核心验证区域:

  1. 全质量图与未压缩图调度舱:点击“注册监听”即可注入回调,模拟相机快门时接收 CapturePhoto 对象。当触发“模拟相机拍摄”后,控制舱迅速捕获并高亮显示已收到未压缩物理相片(image.Picture)。
  2. HDR 动态照片配置控制台:一键开启 HDR 开关,控制舱自动完成参数的相互约束性计算,将流格式切为 CAMERA_FORMAT_YCRCB_P010,将色彩空间升级为 BT2020_HLG 极宽色域,直观演示“全高动态范围”相片拍摄的自检及配置状态。
  3. 元数据检测流实时控制台:展示对人脸、条码等不同类型元数据的运行期动态注册与注销,实时显示已激活的监控列表。
2.2 示例项目物理结构

演示工程 CameraKitDemo 结构如下:

CameraKitDemo
├── AppScope
│   └── app.json5
├── entry
│   ├── oh-package.json5
│   └── src
│       └── main
│           ├── ets
│           │   ├── entryability
│           │   │   └── EntryAbility.ets
│           │   └── pages
│           │       ├── Index.ets           <-- Camera Kit 亮点展示与中控台导航
│           │       └── CameraDemo.ets      <-- 实战舱:三大新特性自检及流程仿真
│           ├── resources
│           │   └── base
│           │       └── profile
│           │           └── main_pages.json
│           └── module.json5
└── build-profile.json5
3、三项重磅相机新特性深度剖析
3.1 全质量图与未压缩图调度(PhotoOutput 回调革命)

在先前的 API 版本中,获取一张物理拍摄的图片,往往需要配合 imageReceiver 等复杂的媒体中间件进行数据通道转换。特别是在获取未压缩(YUV)的物理底片时,极其容易造成主线程卡顿或时序紊乱。

API 23 针对 PhotoOutput(照片输出)引入了全新的高画质及无损回调标准:

  • onCapturePhotoAvailable(callback: Callback<CapturePhoto>):用于注册监听全质量图和未压缩图。
  • 在触发此回调时,应用直接拿到了封装完好的 CapturePhoto 实例,其包含的主分量属性 main 会携带未压缩的 image.Pictureimage.Image
  • 开发约束:此接口目前不支持在回调方法内部调用 offCapturePhotoAvailable,即严禁“套娃式注销”;且如果拍摄未压缩图(YUV)格式图片,该接口是唯一合法的物理接收通路。
3.2 全链路双 HDR 动态照片拍摄

动态照片(Live Photo)的静态画面与附带的极短小视频极易产生色差与画质撕裂。API 23 引入的“HDR 动态照片”,要求组成动态照片的静态图和短视频全链路都是高动态范围(HDR)内容

开启 HDR 动态照片,需要系统平台进行特定的能力校验(先查后用):

  1. 格式核验:通过 getSupportedFullOutputCapability 查询预览输出能力,必须从中选择 P010 深度色彩输出流(如 CAMERA_FORMAT_YCRCB_P010CAMERA_FORMAT_YCBCR_P010)。
  2. 色彩空间自检:通过 getSupportedColorSpaces 查询设备支持的色彩空间列表,并显式调用 setColorSpace 强制约束为 BT2020_HLG

只有当流格式和色彩空间双项 HDR 标志位均正确咬合时,生成的动态照片才能具备极其通透的高光、深邃的暗部与完美的色彩渐变层次。

3.3 实时元数据检测流(MetadataOutput)零延迟切换

在开发高集成度的相机应用时,同一个拍照界面可能需要兼顾“条码识别”与“人脸自动对焦”。如果因为用户从扫码切换到人脸拍摄,就去销毁现有的相机配置流、重建 CameraSession,会造成极为明显的视觉闪烁与耗电激增。

API 23 针对 MetadataOutput 新增了以下方法:

  • addMetadataObjectTypes(types: Array<MetadataObjectType>):在线增加需要上报的检测对象类型。
  • removeMetadataObjectTypes(types: Array<MetadataObjectType>):在线移除已上报的检测对象类型。

这两个方法允许我们在不需要暂停 CameraSession、不需要重建硬件实例的情况下,实时以非阻塞式控制检测对象的增减。

4、视觉新纪元中控舱源码实战
4.1 Index.ets — 导向页

首页负责概述 API 23 Camera Kit 的三个重磅变化,并开启控制舱:

import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  build() {
    Column() {
      Column() {
        Image($r('app.media.startIcon'))
          .width(72)
          .height(72)
          .margin({ bottom: 16 })
          .shadow({ radius: 30, color: '#38BDF8', offsetY: 8 })

        Text('Camera Kit')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#38BDF8')
          .margin({ bottom: 4 })

        Text('视觉新纪元中控舱 · API 23 新增全画质未压缩图与 HDR 动态摄影')
          .fontSize(12)
          .fontColor('#64748B')
      }
      .width('100%')
      .padding({ top: 60, bottom: 40, left: 24, right: 24 })
      .backgroundColor('#0B0F19')
      .border({ width: { bottom: 1 }, color: '#1E293B' })

      Column({ space: 16 }) {
        Column({ space: 12 }) {
          Text('📸 Camera Kit API 23 摄影新篇章')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#38BDF8')
            .alignSelf(ItemAlign.Start)

          Text('1. 【全质量与未压缩图】新增 onCapturePhotoAvailable 监听,回调直接递交 CapturePhoto 主分量,大幅简化 YUV 未压缩图的开发流程。')
            .fontSize(12)
            .fontColor('#94A3B8')
            .lineHeight(18)
            .alignSelf(ItemAlign.Start)

          Text('2. 【HDR 动态照片】支持拍摄静态图像与动态短视频均为高动态范围(HDR)内容的动态相片,通过 P010 输出格式与 BT2020_HLG 色彩空间搭配配置。')
            .fontSize(12)
            .fontColor('#94A3B8')
            .lineHeight(18)
            .alignSelf(ItemAlign.Start)

          Text('3. 【实时检测流控制】新增 addMetadataObjectTypes / removeMetadataObjectTypes,支持对人脸、条码等元数据检测类型的实时动态控制。')
            .fontSize(12)
            .fontColor('#94A3B8')
            .lineHeight(18)
            .alignSelf(ItemAlign.Start)
        }
        .padding(20)
        .backgroundColor('#0F172A')
        .borderRadius(14)
        .border({ width: 1, color: '#1E293B' })

        Blank()

        Button('📺 激活视觉新纪元中控舱', { type: ButtonType.Normal })
          .width('100%')
          .height(50)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .backgroundColor('#38BDF8')
          .borderRadius(10)
          .shadow({ radius: 12, color: '#38BDF8', offsetY: 4 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/CameraDemo' });
          })
      }
      .layoutWeight(1)
      .padding(24)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#070A13')
  }
}
4.2 CameraDemo.ets — 核心交互验证舱
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct CameraDemo {
  @State log: string = '等待中控台指令...\n';
  @State isHDRPhotoEnabled: boolean = false;
  @State selectedColorSpace: string = 'SRGB';
  @State selectedFormat: string = 'CAMERA_FORMAT_YUV_420_SP';
  
  // 元数据对象检测类型列表
  @State activeMetadataTypes: string[] = ['FACE_DETECTION'];
  @State availableMetadataTypes: string[] = ['FACE_DETECTION', 'BARCODE_DETECTION', 'PORTRAIT_DETECTION'];
  
  // 模拟的捕获图照片基本状态信息
  @State photoCapturedCount: number = 0;
  @State lastPhotoInfo: string = '暂无拍摄记录';

  // 1. 注册监听全质量图和未压缩图回调
  private onPhotoAvailableCallback = (capturePhoto: camera.CapturePhoto): void => {
    this.photoCapturedCount++;
    // 模拟接收到的全质量图或未压缩图(YUV)对象
    let picture: image.Image | image.Picture = capturePhoto.main;
    let sizeStr = picture ? '已成功载入物理图主分量数据(image.Picture)' : '空数据';
    this.lastPhotoInfo = `已捕获全质量未压缩图 (No. ${this.photoCapturedCount}) - ${sizeStr}`;
    this.appendLog(`[回调触发] onCapturePhotoAvailable 监听到新相片,主分量: ${sizeStr}`);
  };

  private registerPhotoAvailableListener(): void {
    this.appendLog('[指令] 正在注册全质量与未压缩图监听...');
    try {
      // 实际开发中调用:photoOutput.onCapturePhotoAvailable(this.onPhotoAvailableCallback)
      this.appendLog('✅ 成功调用: photoOutput.onCapturePhotoAvailable(callback)');
      promptAction.showToast({ message: '全质量相片监听注册成功' });
    } catch (error) {
      let err = error as BusinessError;
      this.appendLog(`❌ 注册监听失败,错误码: ${err.code}`);
    }
  }

  private unregisterPhotoAvailableListener(): void {
    this.appendLog('[指令] 正在注销全质量与未压缩图监听...');
    try {
      // 实际开发中调用:photoOutput.offCapturePhotoAvailable(this.onPhotoAvailableCallback)
      this.appendLog('✅ 成功调用: photoOutput.offCapturePhotoAvailable(callback)');
      promptAction.showToast({ message: '全质量相片监听已注销' });
    } catch (error) {
      let err = error as BusinessError;
      this.appendLog(`❌ 注销监听失败,错误码: ${err.code}`);
    }
  }

  // 模拟主动拍摄动作以触发回调
  private triggerCaptureSimulation(): void {
    this.appendLog('[拍照] 触发相机拍摄动作...');
    setTimeout(() => {
      // 模拟构造一个 CapturePhoto 传入回调
      const mockPicture: image.Picture = {} as image.Picture; 
      const mockCapturePhoto: camera.CapturePhoto = {
        main: mockPicture
      };
      this.onPhotoAvailableCallback(mockCapturePhoto);
    }, 500);
  }

  // 2. HDR动态照片配置逻辑与状态切换
  private toggleHDRPhoto(enabled: boolean): void {
    this.isHDRPhotoEnabled = enabled;
    if (enabled) {
      this.selectedFormat = 'CAMERA_FORMAT_YCRCB_P010';
      this.selectedColorSpace = 'BT2020_HLG';
      this.appendLog('[配置] 切换至 HDR 动态照片模式:静态图片与动态短视频均配置为高动态范围。');
      this.appendLog('  - 预览输出格式设置为: P010 (YCRCB_P010)');
      this.appendLog('  - 色彩空间设置为: BT2020_HLG (setColorSpace)');
      promptAction.showToast({ message: 'HDR 动态照片模式已开启' });
    } else {
      this.selectedFormat = 'CAMERA_FORMAT_YUV_420_SP';
      this.selectedColorSpace = 'SRGB';
      this.appendLog('[配置] 切换至标准 SDR 模式:');
      this.appendLog('  - 预览输出格式设置为: YUV_420_SP');
      this.appendLog('  - 色彩空间设置为: SRGB');
      promptAction.showToast({ message: '已回退至标准 SDR 模式' });
    }
  }

  // 3. 元数据检测类型实时增删逻辑
  private toggleMetadataType(type: string): void {
    const index = this.activeMetadataTypes.indexOf(type);
    if (index > -1) {
      // 删除该类型
      let temp = [...this.activeMetadataTypes];
      temp.splice(index, 1);
      this.activeMetadataTypes = temp;
      
      this.appendLog(`[元数据] 正在注销检测类型: ${type}`);
      try {
        // 实际调用为 metadataOutput.removeMetadataObjectTypes([camera.MetadataObjectType[type]])
        this.appendLog(`✅ 成功调用: metadataOutput.removeMetadataObjectTypes(['${type}'])`);
      } catch (error) {
        let err = error as BusinessError;
        this.appendLog(`❌ 移除元数据检测失败: ${err.code}`);
      }
    } else {
      // 新增该类型
      this.activeMetadataTypes.push(type);
      this.appendLog(`[元数据] 正在注册检测类型: ${type}`);
      try {
        // 实际调用为 metadataOutput.addMetadataObjectTypes([camera.MetadataObjectType[type]])
        this.appendLog(`✅ 成功调用: metadataOutput.addMetadataObjectTypes(['${type}'])`);
      } catch (error) {
        let err = error as BusinessError;
        this.appendLog(`❌ 添加元数据检测失败: ${err.code}`);
      }
    }
  }

  private appendLog(line: string): void {
    this.log = `${line}\n${this.log}`;
  }

  build() {
    Column() {
      Row() {
        Text('视觉新纪元中控舱 · Camera Kit')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#38BDF8')
      }
      .width('100%')
      .height(56)
      .padding({ left: 20 })
      .backgroundColor('#0B0F19')

      Scroll() {
        Column({ space: 16 }) {
          // 模块一:全质量与未压缩图自检舱
          Column({ space: 12 }) {
            Row() {
              Text('📷 全质量图与未压缩图调度舱 (API 23)')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#38BDF8')
            }
            .width('100%')

            Text('从 API version 23 开始,PhotoOutput 提供 onCapturePhotoAvailable 回调用于监听全质量和未压缩物理相片,回调可直接获取完整 Picture 物理数据。')
              .fontSize(11)
              .fontColor('#94A3B8')
              .lineHeight(16)

            Row({ space: 10 }) {
              Button('注册 PhotoAvailable 监听')
                .fontSize(12)
                .layoutWeight(1)
                .height(40)
                .backgroundColor('#0369A1')
                .onClick(() => this.registerPhotoAvailableListener())

              Button('注销监听')
                .fontSize(12)
                .layoutWeight(1)
                .height(40)
                .backgroundColor('#334155')
                .onClick(() => this.unregisterPhotoAvailableListener())
            }

            Button('⚡️ 模拟相机拍摄捕获')
              .width('100%')
              .height(40)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .backgroundColor('#0284C7')
              .onClick(() => this.triggerCaptureSimulation())

            Column() {
              Text(`捕获统计: 已收到 ${this.photoCapturedCount} 张全画质相片`)
                .fontSize(12)
                .fontColor('#E2E8F0')
                .margin({ bottom: 4 })
              Text(this.lastPhotoInfo)
                .fontSize(11)
                .fontColor('#F472B6')
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#1E293B')
            .borderRadius(8)
          }
          .padding(16)
          .backgroundColor('#0F172A')
          .borderRadius(14)
          .border({ width: 1, color: '#1E293B' })

          // 模块二:HDR 动态照片配置舱
          Column({ space: 12 }) {
            Row() {
              Text('🌅 HDR 动态照片配置控制台')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#38BDF8')
            }
            .width('100%')

            Text('静态图片与动态短视频均配置为高动态范围(HDR)内容,在高光与暗部细节、色彩层次和整体质感方面优于SDR成片。P010 输出流格式与 HLG 色彩空间需搭配使用。')
              .fontSize(11)
              .fontColor('#94A3B8')
              .lineHeight(16)

            Row() {
              Text('HDR 动态照片模式')
                .fontSize(13)
                .fontColor('#F8FAFC')
              Toggle({ type: ToggleType.Switch, isOn: this.isHDRPhotoEnabled })
                .onChange((isOn: boolean) => this.toggleHDRPhoto(isOn))
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

            Column({ space: 6 }) {
              Row() {
                Text('流格式 (Format): ')
                  .fontSize(12)
                  .fontColor('#64748B')
                Text(this.selectedFormat)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.isHDRPhotoEnabled ? '#34D399' : '#94A3B8')
              }
              .width('100%')

              Row() {
                Text('色彩空间 (ColorSpace): ')
                  .fontSize(12)
                  .fontColor('#64748B')
                Text(this.selectedColorSpace)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.isHDRPhotoEnabled ? '#34D399' : '#94A3B8')
              }
              .width('100%')
            }
            .padding(12)
            .backgroundColor('#1E293B')
            .borderRadius(8)
          }
          .padding(16)
          .backgroundColor('#0F172A')
          .borderRadius(14)
          .border({ width: 1, color: '#1E293B' })

          // 模块三:元数据实时检测增删舱
          Column({ space: 12 }) {
            Row() {
              Text('🔍 元数据检测流实时控制台')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#38BDF8')
            }
            .width('100%')

            Text('API 23 支持 addMetadataObjectTypes/removeMetadataObjectTypes 对检测类型进行实时增减,无需重建会话或重启 MetadataOutput。')
              .fontSize(11)
              .fontColor('#94A3B8')
              .lineHeight(16)

            Column({ space: 8 }) {
              ForEach(this.availableMetadataTypes, (type: string) => {
                Row() {
                  Text(type)
                    .fontSize(12)
                    .fontColor('#F8FAFC')
                  Checkbox({ name: type, group: 'metadataGroup' })
                    .select(this.activeMetadataTypes.indexOf(type) > -1)
                    .onChange(() => this.toggleMetadataType(type))
                }
                .width('100%')
                .justifyContent(FlexAlign.SpaceBetween)
                .padding({ top: 4, bottom: 4 })
              })
            }

            Row() {
              Text('当前激活的检测类型:')
                .fontSize(12)
                .fontColor('#64748B')
              Text(this.activeMetadataTypes.join(' , ') || '无')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#F472B6')
            }
            .width('100%')
            .padding(10)
            .backgroundColor('#1E293B')
            .borderRadius(8)
          }
          .padding(16)
          .backgroundColor('#0F172A')
          .borderRadius(14)
          .border({ width: 1, color: '#1E293B' })

          // 实时日志面板
          Column() {
            Text('控制台诊断日志')
              .fontSize(12)
              .fontColor('#64748B')
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 8 })

            Text(this.log)
              .fontSize(11)
              .fontColor('#CBD5E1')
              .fontFamily('monospace')
              .lineHeight(18)
              .alignSelf(ItemAlign.Start)
              .width('100%')
          }
          .padding(16)
          .backgroundColor('#0F172A')
          .borderRadius(12)
          .border({ width: 1, color: '#1E293B' })
        }
        .padding(16)
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#070A13')
  }
}
5、元数据 C API 高阶应用参考

对于极致追求实时性的算法团队(如 AR 渲染、人脸物理追踪),元数据除了能通过 ArkTS 进行上层监听外,API 23 还额外在 NDK 侧新增了元数据流的专属 C API:

#include <multimedia/camera_framework/camera.h>
#include <multimedia/camera_framework/camera_manager.h>

void SetupCMetadataOutput(Camera_Manager* cameraManager) {
    // 1. 定义我们所关心的元数据对象类型数组
    Camera_MetadataObjectType types[] = {
        CAMERA_METADATA_OBJECT_TYPE_FACE,
        CAMERA_METADATA_OBJECT_TYPE_BARCODE
    };
    uint32_t size = sizeof(types) / sizeof(types[0]);

    Camera_MetadataOutput* metadataOutput = nullptr;

    // 2. 调用 API 23 专属的元数据对象类型数组创建接口
    Camera_ErrorCode ret = OH_CameraManager_CreateMetadataOutputWithObjectTypes(
        cameraManager, 
        types, 
        size, 
        &metadataOutput
    );

    if (ret == CAMERA_OK && metadataOutput != nullptr) {
        // 创建元数据实例成功,接下来可在 NDK 侧绑定数据回调进行深度帧处理
    }
}

使用这种方式,元数据对象可以直接流向 Native 层的检测模型,避免了频繁跨 JNI 的像素或坐标序列化消耗,提供了极致的响应速度。

运行效果图如下:
image.png
image.png

6、总结

在 HarmonyOS 摄影与影像进化的新阶段,相机的底座调度走向了更深层、更敏捷的设计:

  • onCapturePhotoAvailable 让应用摆脱了繁冗的数据链路转换,让 YUV 未压缩图的开发链路平滑而标准;
  • 全链路 HDR 动态照片不仅能实现更高饱和度与明暗细节的瞬间抓拍,更确保了附带短视频的色彩一致性;
  • 实时元数据增删(add/removeMetadataObjectTypes)则为复杂的视觉多算法共存场景提供了低延迟、零重闪的极速硬件切换底座。

这三项演进共同奠定了鸿蒙系统原生相机生态的全新视觉格局。


轻口味
39.5k 声望5.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei