HarmonyOS技术精讲-Camera Kit(相机服务)第8篇:曝光控制深入解析

曝光控制,比你想象中复杂一点

很多人在用 Camera Kit 开发相机应用时,会卡在一个问题上:自动曝光在某些场景下表现不佳

比如逆光拍人像,脸部过暗;夜景拍灯光,高光溢出。自动曝光算法再聪明,也没法懂你的构图意图。这时候就需要手动干预曝光参数。

但 HarmonyOS 的曝光控制 API 有个特点:看起来简单,实际用起来限制不少。官方文档只写了"支持设置曝光模式和补偿值",但没说清楚这几个 API 在什么设备上生效、什么场景下会失效。这篇文章就把这些边界情况讲清楚。

这篇文章主要解决三个场景:

  1. 手动控制曝光时间(比如长曝光拍夜景)
  2. 调整曝光补偿(适应不同光照环境)
  3. 锁定自动曝光(对焦完成后不让画面变亮变暗)

环境说明

复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(建议真机测试,模拟器在某些曝光 API 上行为不一致)

核心实现

1. 获取并配置曝光模式

相机服务提供三种曝光模式:自动曝光锁定曝光手动曝光

typescript 复制代码
import { camera } from '@kit.CameraKit';

// 在 session 创建完成后调用
function setupExposure(session: camera.PhotoSession): void {
  // 检查设备是否支持手动曝光
  const exposureModeRange: camera.ExposureMode[] = session.getSupportedExposureModes();
  
  if (exposureModeRange.includes(camera.ExposureMode.MANUAL)) {
    // 切换到手动曝光模式
    session.setExposureMode(camera.ExposureMode.MANUAL);
  } else {
    // 不支持手动曝光,回退到自动
    session.setExposureMode(camera.ExposureMode.AUTO);
    console.warn('当前设备不支持手动曝光模式');
  }
}

这段代码的核心逻辑很直接:先查能力,再设置模式 。很多人直接调用 setExposureMode(camera.ExposureMode.MANUAL),结果在某些机型上直接抛异常。

实际项目里,getSupportedExposureModes() 返回的数组长度在不同设备上可能不同。低端机型可能只支持自动曝光和锁定曝光两种模式。

2. 设置曝光时间

手动模式下,曝光时间的单位是纳秒。

typescript 复制代码
function setManualExposure(session: camera.PhotoSession, durationNs: number): void {
  // 获取支持的最小曝光时长和最大曝光时长
  const { min, max } = session.getSupportedExposureDurationRange();
  
  // 限制范围,防止设置超出硬件能力的值
  const clampedDuration = Math.min(Math.max(durationNs, min), max);
  
  // 设置曝光时长
  session.setExposure(clampedDuration);
  
  // 验证是否生效
  const actualDuration = session.getExposure();
  console.info(`已设置曝光时长:${actualDuration} ns`);
}

这里有个容易踩坑的地方:setExposure 只在手动曝光模式下生效。如果在自动模式下调用,API 不会报错,但不会产生任何效果。

曝光时长范围一般是 1/10000 秒(100,000 ns)到 30 秒(30,000,000,000 ns),但这个范围因设备而异。建议在 UI 初始化时读取这个范围,用来约束滑块的最大最小值。

3. 曝光补偿( Exposure bias)

曝光补偿用于在自动曝光的基础上微调整体亮度。

typescript 复制代码
function setExposureCompensation(session: camera.PhotoSession, compensation: number): void {
  // 获取支持的最小补偿值和最大补偿值
  const { min, max, step } = session.getSupportedExposureBiasRange();
  
  // 确保补偿值在有效范围内
  // 步进值一般为 0.1 或 0.3,需要对齐
  const alignedCompensation = Math.round(compensation / step) * step;
  const finalCompensation = Math.min(Math.max(alignedCompensation, min), max);
  
  // 单位是 EV(曝光值)
  session.setExposureBias(finalCompensation);
  
  // 建议配置完成后读取以验证
  const actualBias = session.getExposureBias();
  console.info(`曝光补偿值:${actualBias} EV`);
}

补偿值的范围通常是 -4 EV 到 +4 EV,步进 0.1 EV 或 0.3 EV。但不同设备硬件差异较大,不要硬编码补偿范围。

自动曝光模式下,曝光补偿才能生效。 如果在手动模式下设置 compensation,API 会直接返回错误码。

4. 曝光锁定( Auto Exposure Lock)

曝光锁定的作用是让相机保持在当前亮度水平,即使场景发生变化也不会重新调整。

typescript 复制代码
function toggleExposureLock(session: camera.PhotoSession, lock: boolean): void {
  if (lock) {
    // 锁定自动曝光
    session.lockAutoExposure();
    console.info('自动曝光已锁定');
  } else {
    // 解锁自动曝光
    session.unlockAutoExposure();
    console.info('自动曝光已解锁');
  }
}

// 检查当前锁定状态
function isExposureLocked(session: camera.PhotoSession): boolean {
  return session.isAutoExposureLocked();
}

曝光锁定适合在用户完成对焦和构图后使用。例如短视频录制过程中,场景内容在变化,但用户不希望画面忽明忽暗。

注意:曝光锁定只在自动曝光模式下有意义。如果已经在手动曝光模式下,锁定调用不会报错但不会改变行为。

5. 曝光调节滑块控件

把上面几个能力整合到一个 UI 组件中:

typescript 复制代码
@Component
struct ExposureControlPanel {
  @State private exposureMode: camera.ExposureMode = camera.ExposureMode.AUTO;
  @State private exposureValue: number = 0; // 曝光时间,单位纳秒
  @State private exposureBias: number = 0; // 曝光补偿,单位 EV
  @State private exposureLocked: boolean = false;
  
  private session: camera.PhotoSession | null = null;
  
  build() {
    Column({ space: 12 }) {
      // 曝光模式切换
      Row() {
        Text('曝光模式')
        Toggle({ type: ToggleType.Switch, isOn: this.exposureMode === camera.ExposureMode.MANUAL })
          .onChange((isOn: boolean) => {
            this.exposureMode = isOn ? camera.ExposureMode.MANUAL : camera.ExposureMode.AUTO;
            this.session?.setExposureMode(this.exposureMode);
          })
      }
      
      // 手动曝光时长滑块(仅手动模式下可用)
      if (this.exposureMode === camera.ExposureMode.MANUAL) {
        Slider({
          value: this.exposureValue,
          min: 100000, // 最小 1/10000 秒
          max: 30000000000, // 最大 30 秒
          step: 100000
        })
          .onChange((value: number) => {
            this.exposureValue = value;
            this.session?.setExposure(value);
          })
      } else {
        // 自动模式 + 曝光补偿
        Slider({
          value: this.exposureBias,
          min: -4,
          max: 4,
          step: 0.1
        })
          .onChange((value: number) => {
            this.exposureBias = value;
            this.session?.setExposureBias(value);
          })
        
        // 曝光锁定开关
        Toggle({ type: ToggleType.Switch, isOn: this.exposureLocked })
          .onChange((isOn: boolean) => {
            this.exposureLocked = isOn;
            if (isOn) {
              this.session?.lockAutoExposure();
            } else {
              this.session?.unlockAutoExposure();
            }
          })
      }
    }
    .padding(16)
    .backgroundColor('#1A000000')
    .borderRadius(12)
  }
}

这段代码的逻辑核心是:根据曝光模式动态切换 UI 控件

手动模式下只显示曝光时长滑块;自动模式下显示曝光补偿滑块和锁定开关。这种设计避免了用户配置无效参数。

常见问题

问题 1:setExposure 设置了值但画面没变化

现象 :在手动曝光模式下调用 session.setExposure(value),API 返回成功,但预览画面亮度没有变化。

原因 :这个 API 需要搭配正确的曝光模式才能生效。如果先调用了 setExposureMode 切换到手动模式,但在后续操作中因为某些原因回调又切换回了自动模式(比如 restart() 或重新配置 session),那么设置的曝光时长会被忽略。

解决方案:每次配置曝光参数前,先读取当前的曝光模式进行校验。

typescript 复制代码
function safeSetExposure(session: camera.PhotoSession, duration: number): void {
  const currentMode = session.getExposureMode();
  if (currentMode !== camera.ExposureMode.MANUAL) {
    console.warn('当前不是手动曝光模式,设置曝光时长无效');
    return;
  }
  session.setExposure(duration);
}

问题 2:曝光锁定在页面返回后失效

现象:用户锁定曝光后,按 Home 键返回桌面再回到应用,或者页面发生路由跳转,曝光锁定状态自动丢失。

原因:相机 session 在页面不可见时会被系统自动释放。页面重新可见后需要重新创建 session,之前的状态全部丢失。

解决方案:在页面的生命周期回调中保存和恢复曝光锁定状态。

typescript 复制代码
@State private lockState: boolean = false;
private session: camera.PhotoSession | null = null;

onPageShow(): void {
  // 页面显示时,尝试恢复锁定状态
  if (this.lockState && this.session) {
    this.session.lockAutoExposure();
  }
}

onPageHide(): void {
  // 页面隐藏时,状态保存在 @State 中
}

最佳实践

  1. 先查能力再设置 :每次设置曝光参数前,调用 getSupportedExposureModes()getSupportedExposureDurationRange()getSupportedExposureBiasRange() 获取设备能力。不同设备硬件差异大,硬编码范围会导致 API 调用异常。

  2. 曝光补偿值的步进对齐setExposureBias 传参必须是对齐 step 的值。比如 step=0.3 的设备,传入 1.0 会取整为 0.9。建议在 UI 中直接限制用户滑动到对齐值。

  3. 锁定状态与 UI 同步 :曝光锁定在 session 层面是一个一次性操作,没有 state 概念。UI 的锁定开关状态必须手动维护,不要在 lockAutoExposureunlockAutoExposure 之后直接读取 session 状态来更新 UI。

完整 Demo 入口

typescript 复制代码
// entry/src/main/ets/pages/ExposureDemo.ets

import { camera } from '@kit.CameraKit';
import { media } from '@kit.MediaKit';

@Entry
@Component
struct ExposureDemo {
  private session: camera.PhotoSession | null = null;
  private cameraManager: camera.CameraManager | null = null;
  
  aboutToAppear(): void {
    this.initCamera();
  }
  
  async initCamera(): Promise<void> {
    // 获取相机管理器实例
    this.cameraManager = camera.getCameraManager();
    
    // 获取相机设备
    const cameras: camera.CameraDevice[] = this.cameraManager.getSupportedCameras();
    if (cameras.length === 0) {
      console.error('没有找到摄像头');
      return;
    }
    
    // 创建会话
    this.session = await this.cameraManager.createCameraSession(camera.ControllerMode.PHOTO);
    await this.session.beginConfig();
    await this.session.addInput(cameras[0]);
    
    // 添加预览输出
    const previewOutput = await this.session.createPreviewOutput();
    await this.session.addOutput(previewOutput);
    
    // 提交配置
    await this.session.commitConfig();
    await this.session.start();
  }
  
  build() {
    Stack() {
      // 相机预览流(省略实际预览组件的实现)
      XComponent({
        id: 'preview',
        type: XComponentType.SURFACE
      })
        .width('100%')
        .height('100%')
      
      // 曝光控制面板
      ExposureControlPanel({ session: this.session })
        .position({ x: 0, y: '70%' })
    }
    .width('100%')
    .height('100%')
  }
  
  aboutToDisappear(): void {
    this.session?.release();
  }
}

FAQ

Q:为什么真机正常,模拟器上曝光补偿值设置后画面不变化?

A:模拟器对曝光硬件的模拟不完整,特别是补偿值响应。建议真机调试曝光相关功能。

Q:为什么页面返回后曝光锁定失效了?

A:相机 session 在页面不可见时被系统自动释放。重新创建 session 后,之前锁定的状态不会自动恢复。需要在页面 onPageShow 回调中根据 UI 状态重新锁定。

Q:设置曝光补偿后,预览画面亮度没有立即变化,需要关闭重新打开 camera 吗?

A:不需要。setExposureBias 是实时生效的,不需要重启 session。如果设置后 1-2 秒画面无变化,检查传入的值是否在设备支持的范围内。另一个可能性是当前处于手动曝光模式,补偿值不生效。

相关推荐
Helen_cai3 小时前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos
气概3 小时前
QT集成basler相机
开发语言·数码相机·qt
爸爸6193 小时前
鸿蒙应用开发实战【08】— AppStorage 全局状态与跨页面通信
ubuntu·华为·harmonyos·鸿蒙系统
ldsweet3 小时前
HarmonyOS技术精讲-剪贴板服务:进阶技巧与延迟粘贴
华为·harmonyos
一缕清烟在人间3 小时前
鸿蒙应用开发实战【27】— DatabaseService 数据库初始化
javascript·数据库·harmonyos·鸿蒙系统
拥抱太阳06163 小时前
HarmonyOS 应用开发《掌上英语》第13篇:面向对象 vs 函数式:ArkTS 中的编程范式选择
harmonyos
全堆鸿蒙3 小时前
鸿蒙应用开发实战【34】— 批量操作:batchUpdateStatus 与 deleteMany
华为·harmonyos·鸿蒙系统
解局易否结局4 小时前
鸿蒙 PC 应用开发实战:多显示器、显示密度与跨屏适配
华为·计算机外设·harmonyos
贾伟康4 小时前
【安心陪诊 Agent v1.1】首页可信设计实战:HTML/CSS 状态卡与急症分流
css·html·harmonyos·ai agent·首页设计