
曝光控制,比你想象中复杂一点
很多人在用 Camera Kit 开发相机应用时,会卡在一个问题上:自动曝光在某些场景下表现不佳。
比如逆光拍人像,脸部过暗;夜景拍灯光,高光溢出。自动曝光算法再聪明,也没法懂你的构图意图。这时候就需要手动干预曝光参数。
但 HarmonyOS 的曝光控制 API 有个特点:看起来简单,实际用起来限制不少。官方文档只写了"支持设置曝光模式和补偿值",但没说清楚这几个 API 在什么设备上生效、什么场景下会失效。这篇文章就把这些边界情况讲清楚。
这篇文章主要解决三个场景:
- 手动控制曝光时间(比如长曝光拍夜景)
- 调整曝光补偿(适应不同光照环境)
- 锁定自动曝光(对焦完成后不让画面变亮变暗)
环境说明
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 中
}
最佳实践
-
先查能力再设置 :每次设置曝光参数前,调用
getSupportedExposureModes()、getSupportedExposureDurationRange()、getSupportedExposureBiasRange()获取设备能力。不同设备硬件差异大,硬编码范围会导致 API 调用异常。 -
曝光补偿值的步进对齐 :
setExposureBias传参必须是对齐 step 的值。比如 step=0.3 的设备,传入 1.0 会取整为 0.9。建议在 UI 中直接限制用户滑动到对齐值。 -
锁定状态与 UI 同步 :曝光锁定在 session 层面是一个一次性操作,没有 state 概念。UI 的锁定开关状态必须手动维护,不要在
lockAutoExposure或unlockAutoExposure之后直接读取 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 秒画面无变化,检查传入的值是否在设备支持的范围内。另一个可能性是当前处于手动曝光模式,补偿值不生效。