手动控制感光度(ISO)是移动影像开发中提升画质掌控力的关键能力。在HarmonyOS NEXT 6.1.1(API 24)之前,Camera Kit中的ISO通常由自动曝光(AE)算法统一托管,开发者只能通过曝光补偿等间接手段影响最终成像。新版本中Camera Kit将ISO控制权完整开放给上层应用:无论是ArkTS接口还是Native C API,开发者在HarmonyOS 6.1.1(API 24)中都可以查询硬件原生的ISO支持阵列,并强制锁定传感器增益值。
低ISO(如50、100)提供极致纯净的画质,高ISO(如3200、6400)能在暗光环境下捕捉微弱光线,代价是画面噪点增加。手动ISO的价值在于按场景主动控制噪点与亮度的平衡,而不是依赖AE算法猜测业务意图。
一、ArkTS层接口速览
ISO控制能力挂载在Session(捕获会话)的继承体系中,由ManualIso / ManualIsoQuery两部分组成,核心接口包括:
getSupportedIsoRange():与对焦范围、持续时长这类连续区间不同,该接口返回的是number[]离散数组,即硬件的标准ISO档位,例如[50, 100, 200, 400, 800]——这符合摄影常识,因为传感器增益放大器通常具有固定的基础档位。
getIso() / setIso(iso: number):用于实时读取与设置当前感光度。
这里有一个容易踩的坑:直接调用setIso()会触发底层错误码报错。前置条件是当前会话的ExposureMode必须处于EXPOSURE_MODE_LOCKED(曝光锁定)状态。原因在于ISO、快门、光圈共同构成曝光三角,驱动层不允许开发者在自动曝光模式下单独修改ISO而破坏AE算法的内部一致性。
二、Demo代码:手动ISO控光舱
下面是一个完整的“手动ISO控光舱”示例,通过选择器直观体验“改变ISO即改变进光敏锐度”的效果。Demo中查询和设置部分采用模拟数据以展示调用流程,生产环境需将模拟部分替换为photoSession实例上的真实API调用。
- import { camera } from '@kit.CameraKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- import { router } from '@kit.ArkUI';
- @Entry
- @Component
- struct CameraISODemo {
- @State logs: string[] = [];
- @State supportedIsoList: number[] = [];
- @State currentIso: number = 0;
- private appendLog(msg: string): void {
- let now = new Date();
- let timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`;
- this.logs.unshift(`[${timeStr}] ${msg}`);
- }
- // 1. 获取硬件支持的 ISO 档位
- querySupportedIso(): void {
- this.appendLog('🔍 正在探测硬件原生 ISO 阵列...');
- try {
- // 实际项目应通过 photoSession.getSupportedIsoRange() 获取
- let mockIsoRange = [50, 100, 200, 400, 800, 1600, 3200, 6400];
- this.supportedIsoList = mockIsoRange;
- // 实际项目应通过 photoSession.getIso() 获取
- this.currentIso = 400;
- this.appendLog(`✅ 探测成功!可用档位: ${JSON.stringify(this.supportedIsoList)}`);
- this.appendLog(`📸 当前实际生效 ISO: ${this.currentIso}`);
- } catch (e) {
- this.appendLog(`❌ 探测失败: ${(e as BusinessError).code}`);
- }
- }
- // 2. 锁定并设置 ISO(前置:ExposureMode 必须为 EXPOSURE_MODE_LOCKED)
- changeIso(targetIso: number): void {
- this.appendLog(`⚙️ 准备锁定 ISO 为: ${targetIso} ...`);
- this.appendLog('⚠️ 前置检查:ExposureMode 必须为 EXPOSURE_MODE_LOCKED!');
- try {
- // 实际项目应先通过 photoSession.lockForExposure() 完成曝光锁定
- // 再调用 photoSession.setIso(targetIso)
- this.currentIso = targetIso;
- this.appendLog(`✅ ISO 锁定成功!感光度已切换至 ${this.currentIso}`);
- } catch (e) {
- this.appendLog(`❌ ISO 切换失败: ${(e as BusinessError).code}`);
- }
- }
- // 3. 模拟 C API 调用流
- testNativeIsoAPI(): void {
- this.appendLog('🛠️ [Native 模拟] 发送 C++ 底层 ISO 管控指令...');
- setTimeout(() => {
- this.appendLog(' ├─ OH_CaptureSession_GetSupportedISORange -> Min: 50, Max: 6400');
- this.appendLog(' ├─ OH_CaptureSession_GetIso -> 400');
- this.appendLog(' ├─ OH_CaptureSession_SetIso(3200) -> CAMERA_OK');
- this.appendLog('✅ [Native] C 层增益放大器已锁定。');
- }, 500);
- }
- build() {
- Column() {
- Row() {
- Image($r('app.media.startIcon')).width(24).height(24).onClick(() => router.back())
- Text('暗光之钥:手动 ISO 控制')
- .fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 10 })
- }
- .width('100%').padding(20).backgroundColor(Color.White)
- Column({ space: 15 }) {
- Button('获取支持的 ISO 数组', { type: ButtonType.Normal })
- .width('100%').height(45).borderRadius(8).backgroundColor('#3B82F6')
- .onClick(() => this.querySupportedIso())
- if (this.supportedIsoList.length > 0) {
- Text('快捷选择 ISO 档位:').fontSize(14).fontColor('#666').alignSelf(ItemAlign.Start)
- Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
- ForEach(this.supportedIsoList, (isoVal: number) => {
- Button(`ISO ${isoVal}`, { type: ButtonType.Normal })
- .height(35)
- .backgroundColor(this.currentIso === isoVal ? '#EC4899' : '#E2E8F0')
- .fontColor(this.currentIso === isoVal ? Color.White : '#333')
- .borderRadius(6)
- .onClick(() => this.changeIso(isoVal))
- })
- }
- }
- Button('触发 Native ISO 指令', { type: ButtonType.Normal })
- .width('100%').height(45).borderRadius(8).backgroundColor('#059669')
- .onClick(() => this.testNativeIsoAPI())
- }.padding(20)
- Column() {
- Text('控制台 Console')
- .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#666').margin({ bottom: 10 }).alignSelf(ItemAlign.Start)
- List({ space: 8 }) {
- ForEach(this.logs, (item: string) => {
- ListItem() { Text(item).fontSize(12).fontColor('#333').fontFamily('monospace').width('100%') }
- })
- }
- .width('100%').layoutWeight(1).backgroundColor('#F8FAFC').borderRadius(8).padding(10)
- }.padding({ left: 20, right: 20, bottom: 20 }).layoutWeight(1).width('100%')
- }
- .width('100%').height('100%').backgroundColor(Color.White)
- }
- }
复制代码
三、Native C API能力
针对游戏引擎(如Unity的虚拟现实背景流)或底层视觉算法场景,Camera Kit同样提供了遵循ISO 12232:2006标准的C API:
OH_CaptureSession_GetSupportedISORange(session, &minIso, &maxIso):注意此接口返回的是连续范围的min/max指针,而非离散数组,给底层线性插值留下了更灵活的计算空间。
OH_CaptureSession_GetIso(session, ¤tIso):获取当前ISO整数读数。
OH_CaptureSession_SetIso(session, iso):下发ISO设置,前提同样是必须先将曝光模式设置为锁定状态。
四、ArkTS与C API返回值差异的底层逻辑
为什么ArkTS层返回离散数组,而C API返回极值?因为C API直接面对更底层的Sensor驱动流。如果你在C层设置了一个处于Min和Max之间、但不在标准数组内的值(例如ISO 135),底层往往会根据ISP(图像信号处理器)的调校做取整逼近。为保证跨端一致性,建议在业务层始终通过预检将ISO对齐到标准档位。
五、混合控制场景的工程建议
手动ISO当前有一个严格的约束:一旦进入手动ISO流程,必须将曝光模式锁定。如果你想要“快门速度自动、ISO限定在100”这类混合控制,底层驱动并不直接支持,需要在应用层自己实现一套简易AE算法:实时监控曝光回调,在回调中反手锁定模式,然后设置ISO,再做快门补偿。这在高帧率或连续变帧场景下会增加一些设计复杂度,但对画质掌控力是最强的。
六、总结
ManualIso / ManualIsoQuery接口配合Native支持,补全了Camera Kit在专业光影操控上的关键能力,使得构建专业级相机、夜景视频或工业级AI质检平台时,开发者可以像操作单反一样精细控制传感器增益。建议在实际项目中,将“查询ISO档位、锁定曝光、设置ISO”三步封装为原子操作,作为独立的曝光管理模块,便于夜景模式或专业手动模式平滑演进。 |