HarmonyOS开发中的相机HDR:HDR拍照原理、多帧合成、HDR预览、HDR配置、HDR与普通模式切换

核心要点:深入理解 HDR(高动态范围)拍照的完整技术链路,从多帧曝光原理到图像合成算法,从 HDR 实时预览到参数配置,以及 HDR 与普通模式的无缝切换,打造专业级 HDR 相机体验。

一、背景与动机

你有没有遇到过这种情况:站在窗前拍一张照片,窗外蓝天白云清晰可见,但室内的人却黑乎乎一片——或者反过来,人拍清楚了,窗外却白茫茫一片?

这就是动态范围不足的经典表现。人眼可以同时看清亮处和暗处的细节,但相机传感器的动态范围有限,一次曝光只能照顾到亮部或暗部其中之一。

HDR(High Dynamic Range,高动态范围)技术就是为了解决这个问题而生的。它的核心思想很简单:拍多张不同曝光的照片,然后把它们合成一张——暗的负责保留亮部细节,亮的负责保留暗部细节,合在一起就两全其美了。

听起来简单,但实际实现起来有很多技术挑战:多帧之间的对齐、鬼影消除、色调映射、实时预览……今天我们就来逐一攻克。


二、核心原理

2.1 HDR 拍照完整流程

flowchart TB
    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

    A[场景分析]:::primary --> B[曝光参数计算]:::primary
    B --> C[短曝光帧 EV-2]:::warning
    B --> D[正常曝光帧 EV0]:::info
    B --> E[长曝光帧 EV+2]:::warning
    C --> F[多帧对齐]:::purple
    D --> F
    E --> F
    F --> G[鬼影检测与消除]:::error
    G --> H[曝光融合]:::purple
    H --> I[色调映射 Tone Mapping]:::purple
    I --> J[色彩校正]:::info
    J --> K[HDR 输出图像]:::primary

2.2 多帧曝光原理

HDR 的核心是包围曝光(Bracketing):对同一场景以不同的曝光参数拍摄多张照片。

  • 短曝光(Under-exposed):曝光时间短,画面偏暗,但亮部细节保留完好(比如天空的云彩)
  • 正常曝光(Normal-exposed):标准曝光,中间调细节最丰富
  • 长曝光(Over-exposed):曝光时间长,画面偏亮,但暗部细节保留完好(比如阴影中的物体)

三帧(或更多帧)合成后,每个像素取最合适的那帧数据,就能得到一张亮暗细节都丰富的 HDR 照片。

2.3 曝光值与 EV

曝光值(EV,Exposure Value)是衡量曝光量的标准单位。每增加 1 个 EV,曝光量翻倍;每减少 1 个 EV,曝光量减半。

EV 偏移曝光时间变化用途
-2 EV1/4 倍保留亮部细节
-1 EV1/2 倍适度保留亮部
0 EV基准曝光中间调
+1 EV2 倍适度保留暗部
+2 EV4 倍保留暗部细节

2.4 多帧对齐

手持拍摄时,多帧之间不可避免地会有微小的位移。如果直接合成,画面会出现重影。因此需要对齐——通常使用特征点匹配或光流法来估计帧间运动,然后对齐到参考帧。

2.5 鬼影消除

即使对齐了,如果场景中有运动物体(比如走动的人),多帧中这个物体的位置不同,合成后会出现半透明的"鬼影"。鬼影消除算法会检测运动区域,在这些区域只使用参考帧的数据。

2.6 色调映射(Tone Mapping)

HDR 图像的动态范围远超显示设备的能力。色调映射就是将 HDR 数据压缩到标准动态范围(SDR)的过程,同时尽量保留视觉上的亮度对比和细节。


三、代码实战

3.1 HDR 拍照:多帧合成核心实现

这是 HDR 拍照最核心的部分——连续拍摄多帧不同曝光的照片,然后合成。

import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * HDR 曝光参数配置
 */
interface HdrExposureConfig {
  /** 曝光补偿值数组,单位 EV */
  evValues: number[];
  /** 是否使用自动包围曝光 */
  useAutoBracketing: boolean;
}

/**
 * HDR 拍照模式
 */
enum HdrMode {
  /** 关闭 HDR */
  OFF = 'off',
  /** 自动 HDR(系统判断是否需要) */
  AUTO = 'auto',
  /** 强制开启 HDR */
  ON = 'on',
}

/**
 * HDR 拍照管理器
 * 实现多帧包围曝光拍摄和合成
 */
class HdrCaptureManager {
  private cameraManager: camera.CameraManager | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private photoOutput: camera.PhotoOutput | null = null;
  private cameraInput: camera.CameraInput | null = null;
  private hdrMode: HdrMode = HdrMode.OFF;

  /** 曝光配置:默认 -2EV, 0EV, +2EV 三帧 */
  private exposureConfig: HdrExposureConfig = {
    evValues: [-2, 0, 2],
    useAutoBracketing: true,
  };

  /** 拍照回调 */
  onCaptureComplete?: (pixelMap: image.PixelMap) => void;
  onHdrProgress?: (progress: number) => void;

  /**
   * 初始化 HDR 相机会话
   */
  async init(surfaceId: string): Promise<void> {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      const cameras = this.cameraManager.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (!backCamera) {
        console.error('[HdrCapture] 找不到后置摄像头');
        return;
      }

      // 创建相机输入
      this.cameraInput = this.cameraManager.createCameraInput(backCamera);
      await this.cameraInput.open();

      // 获取输出能力
      const capability = this.cameraManager.getSupportedOutputCapability(backCamera);

      // 创建预览输出
      const previewProfile = capability.previewProfiles[0];
      const previewOutput = this.cameraManager.createPreviewOutput(previewProfile, surfaceId);

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

      // 创建捕获会话
      this.captureSession = this.cameraManager.createCaptureSession();
      this.captureSession.beginConfig();
      this.captureSession.addInput(this.cameraInput);
      this.captureSession.addOutput(previewOutput);
      this.captureSession.addOutput(this.photoOutput);

      await this.captureSession.commitConfig();
      await this.captureSession.start();

      console.info('[HdrCapture] 相机会话初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[HdrCapture] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 检查设备是否支持 HDR 拍照
   */
  async checkHdrSupport(): Promise<boolean> {
    if (!this.cameraManager) return false;

    try {
      const cameras = this.cameraManager.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (!backCamera) return false;

      const capability = this.cameraManager.getSupportedOutputCapability(backCamera);

      // 检查是否支持 HDR 照片格式
      const hasHdrProfile = capability.photoProfiles.some(
        profile => profile.format === camera.CameraFormat.CAMERA_FORMAT_JPEG
      );

      // 检查是否支持曝光补偿
      // 曝光补偿范围不为 0 说明支持手动曝光控制
      const exposureRange = this.captureSession?.getExposureBiasRange();
      const hasExposureControl = exposureRange && (exposureRange[0] !== 0 || exposureRange[1] !== 0);

      console.info(`[HdrCapture] HDR支持: 格式=${hasHdrProfile}, 曝光控制=${hasExposureControl}`);
      return hasHdrProfile && !!hasExposureControl;
    } catch (error) {
      console.error('[HdrCapture] HDR 支持检查失败');
      return false;
    }
  }

  /**
   * 设置 HDR 模式
   */
  setHdrMode(mode: HdrMode): void {
    this.hdrMode = mode;
    console.info(`[HdrCapture] HDR 模式设置为: ${mode}`);
  }

  /**
   * 执行 HDR 拍照
   * 按包围曝光参数连续拍摄多帧,然后合成
   */
  async captureHdr(): Promise<image.PixelMap | null> {
    if (!this.captureSession || !this.photoOutput) {
      console.error('[HdrCapture] 相机未初始化');
      return null;
    }

    if (this.hdrMode === HdrMode.OFF) {
      console.info('[HdrCapture] HDR 已关闭,执行普通拍照');
      return this.captureNormal();
    }

    try {
      const frames: image.PixelMap[] = [];
      const evValues = this.exposureConfig.evValues;

      console.info(`[HdrCapture] 开始 HDR 拍照,共 ${evValues.length} 帧`);

      // 逐帧拍摄不同曝光的照片
      for (let i = 0; i < evValues.length; i++) {
        const ev = evValues[i];
        this.onHdrProgress?.((i / evValues.length) * 0.5); // 前 50% 进度是拍摄

        // 设置曝光补偿
        await this.setExposureBias(ev);
        console.info(`[HdrCapture] 拍摄第 ${i + 1} 帧,EV: ${ev}`);

        // 等待曝光稳定
        await this.waitForExposureStable();

        // 拍照
        const frame = await this.takePhoto();
        if (frame) {
          frames.push(frame);
        }
      }

      // 恢复自动曝光
      await this.setExposureBias(0);

      // 多帧合成
      this.onHdrProgress?.(0.6);
      const hdrImage = await this.mergeFrames(frames);
      this.onHdrProgress?.(1.0);

      // 释放中间帧
      frames.forEach(f => f.release());

      console.info('[HdrCapture] HDR 拍照完成');
      this.onCaptureComplete?.(hdrImage);
      return hdrImage;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[HdrCapture] HDR 拍照失败: ${err.code} - ${err.message}`);
      return null;
    }
  }

  /**
   * 设置曝光补偿
   * @param ev 曝光补偿值,单位 EV
   */
  private async setExposureBias(ev: number): Promise<void> {
    if (!this.captureSession) return;

    try {
      // 将 EV 值转换为曝光补偿步数
      // HarmonyOS 曝光补偿以 1/3 EV 为步长
      const biasSteps = Math.round(ev * 3);
      this.captureSession.setExposureBias(biasSteps);
    } catch (error) {
      console.error(`[HdrCapture] 设置曝光补偿失败: EV=${ev}`);
    }
  }

  /**
   * 等待曝光稳定
   * 切换曝光参数后需要等待几帧让 AE 收敛
   */
  private waitForExposureStable(): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 150)); // 等待 150ms
  }

  /**
   * 单次拍照
   */
  private async takePhoto(): Promise<image.PixelMap | null> {
    if (!this.photoOutput) return null;

    return new Promise((resolve) => {
      this.photoOutput!.on('photoAvailable', (err: BusinessError, photo: camera.Photo) => {
        if (err) {
          console.error(`[HdrCapture] 拍照回调错误: ${err.code}`);
          resolve(null);
          return;
        }

        // 获取主照片
        const mainImage = photo.main;
        if (mainImage) {
          const pixelMap = mainImage;
          resolve(pixelMap);
        } else {
          resolve(null);
        }
      });

      // 触发拍照
      const captureSetting: camera.PhotoCaptureSetting = {
        quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
        rotation: camera.ImageRotation.ROTATION_0,
      };

      this.photoOutput!.capture(captureSetting);
    });
  }

  /**
   * 普通拍照(非 HDR)
   */
  private async captureNormal(): Promise<image.PixelMap | null> {
    return this.takePhoto();
  }

  /**
   * 多帧合成核心算法
   * 使用曝光融合(Exposure Fusion)方法
   * 不生成 HDR 中间结果,直接输出 LDR 图像
   */
  private async mergeFrames(frames: image.PixelMap[]): Promise<image.PixelMap> {
    if (frames.length === 0) {
      throw new Error('没有可合成的帧');
    }

    if (frames.length === 1) {
      return frames[0]; // 只有一帧,直接返回
    }

    // 使用第一帧作为参考帧(正常曝光)
    const referenceFrame = frames[Math.floor(frames.length / 2)];
    const refInfo = await referenceFrame.getImageInfo();
    const width = refInfo.size.width;
    const height = refInfo.size.height;

    // 创建输出图像
    const outBuffer = new ArrayBuffer(width * height * 4);
    const outData = new Uint8Array(outBuffer);

    // 读取所有帧的像素数据
    const frameBuffers: Uint8Array[] = [];
    for (const frame of frames) {
      const buffer = new ArrayBuffer(width * height * 4);
      await frame.readPixelsToBuffer(buffer);
      frameBuffers.push(new Uint8Array(buffer));
    }

    // 曝光融合:对每个像素计算加权平均
    // 权重基于对比度、饱和度和良好曝光度
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const idx = (y * width + x) * 4;
        let sumR = 0, sumG = 0, sumB = 0, totalWeight = 0;

        for (let f = 0; f < frameBuffers.length; f++) {
          const data = frameBuffers[f];
          const r = data[idx];
          const g = data[idx + 1];
          const b = data[idx + 2];

          // 计算该像素的权重
          const weight = this.calculatePixelWeight(r, g, b);

          sumR += r * weight;
          sumG += g * weight;
          sumB += b * weight;
          totalWeight += weight;
        }

        // 加权平均
        if (totalWeight > 0) {
          outData[idx] = Math.round(sumR / totalWeight);
          outData[idx + 1] = Math.round(sumG / totalWeight);
          outData[idx + 2] = Math.round(sumB / totalWeight);
        } else {
          // 降级为参考帧
          const refData = frameBuffers[Math.floor(frames.length / 2)];
          outData[idx] = refData[idx];
          outData[idx + 1] = refData[idx + 1];
          outData[idx + 2] = refData[idx + 2];
        }
        outData[idx + 3] = 255; // alpha
      }
    }

    // 创建输出 PixelMap
    const outputPixelMap = await image.createPixelMap(outBuffer, {
      size: { width, height },
      pixelFormat: image.PixelFormat.RGBA_8888,
    });

    return outputPixelMap;
  }

  /**
   * 计算像素权重
   * 基于 Mertens 曝光融合算法的权重函数
   * 考虑三个因素:对比度、饱和度、良好曝光度
   */
  private calculatePixelWeight(r: number, g: number, b: number): number {
    // 1. 对比度权重(拉普拉斯算子的近似)
    const contrast = Math.abs(r - g) + Math.abs(g - b) + Math.abs(b - r);
    const contrastWeight = Math.max(contrast / 255, 0.01);

    // 2. 饱和度权重
    const maxC = Math.max(r, g, b);
    const minC = Math.min(r, g, b);
    const saturation = maxC > 0 ? (maxC - minC) / maxC : 0;
    const saturationWeight = Math.max(saturation, 0.01);

    // 3. 良好曝光度权重
    // 像素值接近 128(中间调)的权重最高
    const wellExposed = Math.exp(-0.5 * Math.pow((r / 255 - 0.5) / 0.2, 2))
                      * Math.exp(-0.5 * Math.pow((g / 255 - 0.5) / 0.2, 2))
                      * Math.exp(-0.5 * Math.pow((b / 255 - 0.5) / 0.2, 2));
    const exposureWeight = Math.max(wellExposed, 0.01);

    // 综合权重
    return contrastWeight * saturationWeight * exposureWeight;
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      if (this.captureSession) {
        await this.captureSession.stop();
        this.captureSession.release();
      }
      if (this.cameraInput) {
        this.cameraInput.close();
      }
      console.info('[HdrCapture] 资源已释放');
    } catch (error) {
      console.error('[HdrCapture] 释放资源失败');
    }
  }
}

3.2 HDR 预览:实时 HDR 效果

在 HDR 模式下,预览画面也应该体现 HDR 效果,让用户在拍照前就能看到大致的 HDR 效果。

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * HDR 预览管理器
 * 在预览流中实时应用 HDR 效果
 */
class HdrPreviewManager {
  private cameraManager: camera.CameraManager | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private isHdrPreviewEnabled: boolean = false;

  /**
   * 初始化 HDR 预览
   */
  async init(surfaceId: string): Promise<void> {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      const cameras = this.cameraManager.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (!backCamera) return;

      const cameraInput = this.cameraManager.createCameraInput(backCamera);
      await cameraInput.open();

      const capability = this.cameraManager.getSupportedOutputCapability(backCamera);
      const previewProfile = capability.previewProfiles[0];
      this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, surfaceId);

      this.captureSession = this.cameraManager.createCaptureSession();
      this.captureSession.beginConfig();
      this.captureSession.addInput(cameraInput);
      this.captureSession.addOutput(this.previewOutput);

      await this.captureSession.commitConfig();
      await this.captureSession.start();

      console.info('[HdrPreview] 预览已启动');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[HdrPreview] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 启用 HDR 预览效果
   * 通过调整曝光参数和色调映射来模拟 HDR 效果
   */
  async enableHdrPreview(): Promise<void> {
    if (!this.captureSession) return;

    try {
      // 设置 HDR 预览参数
      // 1. 启用自动曝光,但调整目标亮度
      this.captureSession.setExposureMode(camera.ExposureMode.EXPOSURE_MODE_CONTINUOUS_AUTO);

      // 2. 调整对比度增强(如果设备支持)
      // 通过调整曝光补偿来模拟 HDR 效果
      // 略微增加曝光,让暗部更亮
      this.captureSession.setExposureBias(1); // +1/3 EV

      // 3. 设置对焦模式为连续自动对焦
      this.captureSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);

      this.isHdrPreviewEnabled = true;
      console.info('[HdrPreview] HDR 预览已启用');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[HdrPreview] 启用 HDR 预览失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 禁用 HDR 预览,恢复普通模式
   */
  async disableHdrPreview(): Promise<void> {
    if (!this.captureSession) return;

    try {
      // 恢复标准曝光
      this.captureSession.setExposureBias(0);

      this.isHdrPreviewEnabled = false;
      console.info('[HdrPreview] HDR 预览已禁用');
    } catch (error) {
      console.error('[HdrPreview] 禁用 HDR 预览失败');
    }
  }

  /**
   * 切换 HDR 预览状态
   */
  async toggleHdrPreview(): Promise<boolean> {
    if (this.isHdrPreviewEnabled) {
      await this.disableHdrPreview();
    } else {
      await this.enableHdrPreview();
    }
    return this.isHdrPreviewEnabled;
  }

  /**
   * 获取 HDR 预览状态
   */
  isHdrEnabled(): boolean {
    return this.isHdrPreviewEnabled;
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      if (this.captureSession) {
        await this.captureSession.stop();
        this.captureSession.release();
      }
      console.info('[HdrPreview] 资源已释放');
    } catch (error) {
      console.error('[HdrPreview] 释放资源失败');
    }
  }
}

3.3 HDR 配置与模式切换 UI

完整的 HDR 相机界面,支持 HDR 模式切换、参数配置、实时预览。

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct HdrCameraPage {
  private surfaceId: string = '';
  private hdrCaptureManager: HdrCaptureManager | null = null;
  private hdrPreviewManager: HdrPreviewManager | null = null;

  @State hdrMode: string = 'off'; // off / auto / on
  @State isCapturing: boolean = false;
  @State captureProgress: number = 0;
  @State showSettings: boolean = false;
  @State evRange: number = 2; // 包围曝光 EV 范围
  @State frameCount: number = 3; // 拍摄帧数

  aboutToAppear(): void {
    this.hdrCaptureManager = new HdrCaptureManager();
    this.hdrPreviewManager = new HdrPreviewManager();
  }

  /**
   * 初始化相机
   */
  async initCamera(): Promise<void> {
    if (this.hdrPreviewManager) {
      await this.hdrPreviewManager.init(this.surfaceId);
    }
    if (this.hdrCaptureManager) {
      await this.hdrCaptureManager.init(this.surfaceId);
    }
  }

  build() {
    Column() {
      // 顶部工具栏
      Row() {
        // HDR 模式切换按钮
        Row() {
          Text('HDR')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.hdrMode !== 'off' ? '#4FC3F7' : '#AAAAAA')
        }
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .borderRadius(16)
        .backgroundColor(this.hdrMode !== 'off' ? '#1A4FC3F7' : '#333333')
        .onClick(() => {
          // 循环切换 HDR 模式
          const modes = ['off', 'auto', 'on'];
          const currentIndex = modes.indexOf(this.hdrMode);
          this.hdrMode = modes[(currentIndex + 1) % modes.length];
          this.hdrCaptureManager?.setHdrMode(this.hdrMode as HdrMode);

          if (this.hdrMode !== 'off') {
            this.hdrPreviewManager?.enableHdrPreview();
          } else {
            this.hdrPreviewManager?.disableHdrPreview();
          }
        })

        // HDR 模式标签
        Text(this.getHdrModeLabel())
          .fontSize(12)
          .fontColor('#AAAAAA')
          .margin({ left: 8 })

        Blank()

        // 设置按钮
        Image($r('sys.media.ohos_ic_public_settings'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
          .onClick(() => {
            this.showSettings = !this.showSettings;
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })

      // HDR 设置面板
      if (this.showSettings) {
        Column() {
          // EV 范围滑块
          Row() {
            Text('包围范围')
              .fontSize(14)
              .fontColor('#CCCCCC')
              .width(80)
            Slider({
              value: this.evRange,
              min: 1,
              max: 4,
              step: 1,
            })
              .width('60%')
              .onChange((value: number) => {
                this.evRange = value;
              })
            Text(`±${this.evRange} EV`)
              .fontSize(14)
              .fontColor('#4FC3F7')
              .width(60)
          }
          .width('100%')
          .padding({ left: 16, right: 16 })

          // 帧数选择
          Row() {
            Text('拍摄帧数')
              .fontSize(14)
              .fontColor('#CCCCCC')
              .width(80)
            Row() {
              ForEach([3, 5, 7], (count: number) => {
                Text(`${count}`)
                  .fontSize(14)
                  .fontColor(this.frameCount === count ? '#4FC3F7' : '#AAAAAA')
                  .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                  .borderRadius(8)
                  .backgroundColor(this.frameCount === count ? '#1A4FC3F7' : 'transparent')
                  .onClick(() => {
                    this.frameCount = count;
                  })
              })
            }
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#1A1A1A')
        .borderRadius(12)
        .margin({ left: 16, right: 16, top: 4 })
      }

      // 相机预览区域
      Stack() {
        XComponent({
          id: 'hdrCameraPreview',
          type: XComponentType.SURFACE,
          libraryname: ''
        })
          .width('100%')
          .layoutWeight(1)
          .onLoad(() => {
            // 获取 surfaceId 并初始化相机
            console.info('[HdrCamera] XComponent 加载完成');
          })

        // HDR 拍照进度指示
        if (this.isCapturing) {
          Column() {
            LoadingProgress()
              .width(48)
              .height(48)
              .color('#4FC3F7')
            Text(`HDR 合成中 ${Math.round(this.captureProgress * 100)}%`)
              .fontSize(14)
              .fontColor('#FFFFFF')
              .margin({ top: 8 })
          }
          .padding(20)
          .borderRadius(16)
          .backgroundColor('#99000000')
        }

        // HDR 标识
        if (this.hdrMode !== 'off') {
          Text('HDR')
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4FC3F7')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(4)
            .backgroundColor('#334FC3F7')
            .position({ x: 16, y: 16 })
        }
      }
      .width('100%')
      .layoutWeight(1)

      // 底部操作栏
      Row() {
        // 相册入口
        Image($r('sys.media.ohos_ic_public_album'))
          .width(40)
          .height(40)
          .fillColor('#FFFFFF')
          .borderRadius(8)

        Blank()

        // 拍照按钮
        Stack() {
          Circle()
            .width(72)
            .height(72)
            .fill('#FFFFFF')
          Circle()
            .width(64)
            .height(64)
            .fill(this.isCapturing ? '#FFB74D' : '#FFFFFF')
            .stroke(this.hdrMode !== 'off' ? '#4FC3F7' : '#000000')
            .strokeWidth(this.hdrMode !== 'off' ? 3 : 0)
        }
        .onClick(async () => {
          if (this.isCapturing) return;
          this.isCapturing = true;

          try {
            const result = await this.hdrCaptureManager?.captureHdr();
            if (result) {
              console.info('[HdrCamera] HDR 拍照成功');
            }
          } finally {
            this.isCapturing = false;
            this.captureProgress = 0;
          }
        })

        Blank()

        // 前后切换
        Image($r('sys.media.ohos_ic_public_flip'))
          .width(40)
          .height(40)
          .fillColor('#FFFFFF')
      }
      .width('100%')
      .padding({ left: 32, right: 32, top: 16, bottom: 32 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }

  /** 获取 HDR 模式标签 */
  private getHdrModeLabel(): string {
    const labels: Record<string, string> = {
      'off': '已关闭',
      'auto': '自动',
      'on': '始终开启',
    };
    return labels[this.hdrMode] ?? '';
  }
}

四、踩坑与注意事项

4.1 多帧拍摄的手抖问题

问题:手持拍摄 HDR 时,多帧之间会有位移,导致合成后出现重影。

解决方案

  • 尽量缩短帧间间隔,使用连拍模式
  • 实现帧对齐算法(基于特征点匹配或光流法)
  • 在对齐失败的区域,只使用参考帧数据
  • 提示用户保持稳定,或在检测到抖动时自动放弃 HDR

4.2 运动物体的鬼影

问题:场景中有运动物体时,多帧合成会产生半透明的鬼影。

解决方案

  • 实现鬼影检测:比较多帧之间的差异,差异过大的区域标记为运动区域
  • 运动区域只使用参考帧数据
  • 对于快速运动场景,自动降级为普通拍照

4.3 曝光稳定延迟

问题:调整曝光补偿后,相机需要几帧时间才能稳定到新的曝光值。

解决方案

  • setExposureBias() 后等待 100-200ms
  • 监听曝光状态回调,确认 AE 收敛后再拍照
  • 使用 EXPOSURE_MODE_LOCKED 锁定曝光,避免 AE 自动调整

4.4 HDR 拍照耗时

问题:HDR 拍照需要拍摄多帧并合成,总耗时可能超过 1 秒。

解决方案

  • 显示进度指示,让用户知道正在处理
  • 合成算法使用 Worker 线程,不阻塞 UI
  • 考虑使用 NDK 实现高性能合成算法
  • 降低合成分辨率(预览用小图,保存用大图)

4.5 色调映射参数

问题:不同的色调映射参数会产生截然不同的 HDR 效果——有的自然,有的夸张。

解决方案

  • 提供多种预设风格(自然、鲜艳、戏剧等)
  • 允许用户手动调整 HDR 强度
  • 自动分析场景动态范围,选择合适的映射参数

五、HarmonyOS 6 适配

5.1 API 变更

功能HarmonyOS 5HarmonyOS 6
HDR 拍照手动多帧拍摄+合成新增HdrCaptureSession,系统自动处理多帧
曝光控制setExposureBias()新增setExposureCompensation(),精度更高
色调映射需手动实现新增ToneMapper 类,内置多种映射算法
HDR 预览需手动模拟新增HdrPreviewOutput,原生 HDR 预览

5.2 新增特性

  • 系统级 HDR 拍照:HarmonyOS 6 新增 HdrCaptureSession,只需设置 HDR 模式,系统自动完成多帧拍摄、对齐、合成,开发者无需手动处理
  • HEIF HDR 格式:支持保存 10-bit HEIF 格式的 HDR 照片,保留更多动态范围信息
  • 实时 HDR 取景器:新增 HdrPreviewOutput,预览画面直接显示 HDR 效果

5.3 迁移指南

// HarmonyOS 5 写法:手动多帧 HDR
const frames: PixelMap[] = [];
for (const ev of [-2, 0, 2]) {
  captureSession.setExposureBias(ev * 3);
  await delay(150);
  frames.push(await takePhoto());
}
const hdrImage = await mergeFrames(frames);

// HarmonyOS 6 推荐写法:系统 HDR
const hdrSession = camera.createHdrCaptureSession(backCamera);
hdrSession.setHdrMode(camera.HdrMode.HDR_ON);
hdrSession.setHdrStyle(camera.HdrStyle.NATURAL); // 自然风格
const hdrPhoto = await hdrSession.capture();

六、总结

mindmap
  root((相机HDR开发))
    HDR拍照原理
      包围曝光
      多帧不同EV
      亮部暗部兼顾
      动态范围扩展
    多帧合成
      帧对齐
      鬼影消除
      曝光融合
      Mertens算法
      像素权重计算
    HDR预览
      实时HDR效果
      曝光参数调整
      色调映射预览
      模式切换
    HDR配置
      EV范围设置
      帧数选择
      HDR模式切换
      风格预设
    模式切换
      OFF/AUTO/ON
      预览效果同步
      参数恢复
      降级处理

核心知识点回顾

  1. HDR 的本质是多帧合成:通过包围曝光拍摄不同亮度的照片,合成后获得更大的动态范围
  2. 曝光融合优于 HDR 重建:Mertens 曝光融合直接输出 LDR 图像,不需要生成 HDR 中间结果,更实用
  3. 帧对齐和鬼影消除是难点:手持拍摄不可避免有位移和运动物体,必须处理
  4. 色调映射决定最终观感:不同的映射算法和参数会产生截然不同的效果
  5. HarmonyOS 6 大幅简化了 HDR 开发:系统级 HDR 拍照 API 让开发者无需手动处理多帧

HDR 拍照是相机应用中最具技术含量的功能之一。理解了这些原理和实现,你就能开发出效果媲美系统相机的 HDR 功能!


蓝胖子样样好
79 声望702 粉丝

Never give up,and you will be successful