原生相机APK和原生模式相机APP,这两年在玩机圈和开发圈里被提得越来越频繁——一边是想给老机器换个干净拍照界面的普通用户,一边是拿着 Camera2 文档啃手动模式参数的开发者。它本质上指的是两类东西:一类是系统自带那套“素面朝天”的相机应用安装包,另一类是绕过厂商那套 AI 堆栈、把快门、ISO、对焦、白平衡全部交回人手的原生模式相机应用。前者解决“界面太花、启动太慢、拍出来太假”的问题,后者解决“我想自己控制曝光”的问题。这篇内容适合两类人看:想自己动手做一个能用的原生模式相机 APP 的 Android 开发者,以及手里握着几个移植包、不知道要不要装、装了闪退又不知道为啥的折腾党。下面全是一线踩出来的东西。
1. 先把“原生相机”这四个字拆开看清楚
1.1 三种完全不同的“原生相机”,别混为一谈
这个词在社区里的歧义大到离谱,我见过太多人拿着一个 A 机型的相机包往 B 机型上怼,然后抱怨“原生相机就是骗人的”。实际上大家嘴里的“原生相机”至少对应三种东西,能力和风险完全不同。
第一种是系统内置相机应用的原版安装包。它本质就是一个普通的相机 APP,但深度绑定了自家系统的私有图像处理服务和相机扩展接口,脱离原机型后大概率只能调起最基础的一路预览,滤镜、多帧合成、专属拍摄模式全部失效。
第二种是社区基于开源相机示例改出来的“纯净相机”,界面极简,功能只保留快门、切换镜头、闪光灯,没有美颜、没有场景识别,胜在启动快、成片没有过度锐化和涂抹。这类包的兼容性反而最好,因为它只依赖系统公开的相机接口。
第三种是“原生模式相机 APP”,也就是带专业/手动模式的相机。它不一定“原生”,但它的调参链路直接对接系统相机服务,不经过厂商那套自动策略。这才是技术含量最高、也最容易被误解的一类。很多人以为装了它就能拍出相机质感,实际上它只是把你从自动挡换到了手动挡,开得好不好全看你自己的判断。
把这三者分清,后面所有讨论才有意义。你要的是“干净的界面”,还是“手动控制权”,还是“某个特定机型的成片风格”?答案不同,走的路完全不同。
1.2 为什么有人非要绕开厂商相机
厂商相机在这十年里做了大量自动化工作:场景识别、多帧降噪、HDR 合成、人像抠图、夜景长曝。对 95% 的用户来说这是好事,按下快门就有一张观感不错的照片。但对另一部分人来说,这些处理带来了三个具体的痛点。
第一是不可预测。同一场景连拍三张,色彩、锐度、亮度可能都不一样,因为算法在做动态决策。你要是想拍一组统一风格的素材做后期,这种不确定性很要命。
第二是过度处理。锐化过度导致边缘出现白边,降噪过强导致皮肤失去质感,HDR 把该有的明暗关系压平。放大到 100% 看,细节是被“画”出来的,不是被“记录”下来的。
第三是控制权缺失。想固定一个曝光参数拍延时、想锁死白平衡拍产品图、想手动对焦到无限远拍星空,厂商相机的自动挡根本不给你入口。这时候原生模式相机就是刚需。
反过来说,我也要泼一盆冷水:绝大多数手机的手动模式是“半残”的。传感器太小、镜头光圈固定、缺少 ND 滤镜,你能调的空间远比想象中窄。真正决定画质的是传感器尺寸和光学,参数只是让你在既定硬件上少犯错误,不是让你突破物理极限。
1.3 能力边界:它能做什么,做不到什么
先把预期摆正。一个基于系统相机接口做的原生模式 APP,能做到的事情大致是这些:
- 手动锁定曝光时间与感光度,并实时看到曝光变化的反馈;
- 手动设定对焦距离(以屈光度为单位),实现固定焦点拍摄;
- 关闭自动白平衡,手动给红绿蓝增益;
- 输出未压缩的 RAW 数据,交给后期软件处理;
- 强制指定目标帧率范围,稳定预览流畅度;
- 关闭所有多帧合成,拿到单帧真实成像结果。
做不到的事情同样明确:你无法改变镜头物理光圈,无法让一颗小底传感器获得大底的高感表现,无法在关闭自动曝光的同时还享受多帧降噪(这两件事在系统层面是互斥的),也无法调用厂商私有的计算摄影管线。理解这条边界,能省掉你后面 80% 的困惑。
2. 架构拆解:一个原生模式相机是怎么跑起来的
2.1 从界面到传感器的完整链路
Android 的相机栈是一条相当长的链:应用层 → 相机框架(CameraService,跑在独立进程)→ 相机 HAL(厂商实现)→ 驱动 → 传感器。应用层能碰到的只有最上面这一层,但这层提供的抽象已经足够做大部分事情。
Camera2 的核心模型是“会话 + 请求”。你先打开设备拿到一个 CameraDevice,然后用若干个输出 Surface(预览、拍照、图像分析)建立一个 CaptureSession,之后每次需要画面就向会话提交一个 CaptureRequest,系统把请求排队下发给 HAL,HAL 返回对应的 CaptureResult 和图像数据。
这个模型的关键在于请求是可以带完整参数的。你在 CaptureRequest 里设置的每一个键,都会原样传递到 HAL 层。这也是手动模式能够存在的原因——它不是什么特殊通道,就是把自动算法平时填的那些参数,换成你填的值。
和它并列的还有一套更高层的封装,把会话管理、生命周期、用例组合都包好了,写业务代码快得多。代价是它对底层参数的控制是“白名单式”的,很多手动参数不开放。所以做专业模式,基本只能走底层那套接口,高层封装留给普通拍照功能。
2.2 硬件能力等级决定了你的上限
同样是 Android 手机,相机能力天差地别。系统用INFO_SUPPORTED_HARDWARE_LEVEL把设备分成几档,这个值直接决定你的 APP 能走多远。
| 能力等级 | 典型表现 | 手动模式可行性 |
|---|---|---|
| LEGACY | 旧设备兼容层,基本只能预览和拍照 | 手动参数基本无效,不建议做 |
| LIMITED | 支持基础会话与部分控制 | 可做自动曝光补偿,完整手动受限 |
| FULL | 支持每帧参数控制与 3A 手动关闭 | 手动模式可正常实现 |
| LEVEL_3 | 在 FULL 基础上增加更多输出组合与重处理 | 功能最完整,可做 RAW 与多流并发 |
除了等级,还要看能力列表里有没有这几项:MANUAL_SENSOR(能不能手动设曝光和 ISO)、RAW(能不能出 RAW)、MANUAL_POST_PROCESSING(能不能手动设色彩校正)、BURST_CAPTURE(连拍)、LOGICAL_MULTI_CAMERA(多摄逻辑相机)。缺了MANUAL_SENSOR,你界面做得再花哨,参数填进去也是白填——HAL 会直接忽略。
注意:不要用设备型号做判断。同一个型号的不同批次、不同系统版本,能力列表都可能不一样。所有功能入口都应该在运行时读取能力后再决定是否显示。
2.3 手动模式的参数闭环:关掉自动,才有手动
这是最多人踩的坑。你在请求里设了曝光时间和 ISO,结果画面一点变化都没有,为什么?因为自动曝光还开着。系统优先级是这样的:只要CONTROL_AE_MODE不是 OFF,你设的SENSOR_EXPOSURE_TIME和SENSOR_SENSITIVITY就会被自动算法覆盖。
所以正确的顺序是:先把CONTROL_MODE设为 OFF,再把对应的 3A 子模式逐个关掉。
- 曝光:
CONTROL_AE_MODE = OFF,然后设曝光时间与 ISO; - 对焦:
CONTROL_AF_MODE = OFF,然后设LENS_FOCUS_DISTANCE; - 白平衡:
CONTROL_AWB_MODE = OFF,然后设色彩校正增益。
对焦距离这个参数特别容易搞错。它用的单位是屈光度(1/米),不是米。也就是说,对焦到 0.5 米处,你要填 2.0;对焦到无限远,填 0。设备支持的最远对焦能力由LENS_INFO_MINIMUM_FOCUS_DISTANCE给出,比如它是 10.0,代表最近能对到 0.1 米。很多人填了个 0.5 以为是对焦到半米,实际是对焦到 2 米,怎么调都觉得对不上。
白平衡更绕。系统没有提供“直接设定色温”的接口,你要么用预设模式(日光、阴天、白炽灯那一套),要么走完整的手动路径:COLOR_CORRECTION_MODE = TRANSFORM_MATRIX,然后提供COLOR_CORRECTION_GAINS(一个四分量向量,分别对应 R、Gr、Gb、B 的增益)以及可选的色彩变换矩阵。这条路能做出真正的色温滑杆,但需要你自己把色温映射成增益,工作量不小。
3. 开发实操:从零搭一个能用的原生模式相机
3.1 第一步永远是探测能力,而不是画界面
我见过太多项目一上来就写 UI,滑杆全摆好,跑起来发现一半是灰的。正确做法是先做一次完整的能力探测,把结果缓存起来,界面按结果动态生成。
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val id = manager.cameraIdList.firstOrNull { camId -> val c = manager.getCameraCharacteristics(camId) c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK } ?: return val chars = manager.getCameraCharacteristics(id) val caps = chars.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: intArrayOf() val canManual = caps.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR) val canRaw = caps.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) val expRange = chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) // 纳秒 val isoRange = chars.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) // ISO 值 val minFocus = chars.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE) // 屈光度 val focal = chars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) val physSize = chars.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) // 毫米拿到这几个范围,你的滑杆取值范围就确定了。这里有个细节:SENSOR_INFO_EXPOSURE_TIME_RANGE返回的是纳秒,而界面上人们习惯看“1/500 秒”或者“2 秒”。这个换算必须在 UI 层做一次格式化,不要把纳秒直接显示出去,用户会一脸问号。
还有一个容易被忽略的项:SENSOR_INFO_TIMESTAMP_SOURCE。它告诉你时间戳是来自单调时钟还是实时时钟,做长曝光堆栈或者和外部设备同步时会用到。
3.2 预览链路:Surface 的生命周期比你想的脆
预览一般用TextureView或者SurfaceView。前者能参与视图变换、支持动画,后者性能更好但层级处理麻烦。做相机 APP 我一般选前者,因为要叠网格线、对焦框、直方图这些图层,前者方便得多。
关键是Surface 的生命周期和相机会话必须严格绑定。TextureView 的onSurfaceTextureDestroyed一旦触发,你手上那个 Surface 就失效了,此时如果会话还持有它,下一次提交请求就会抛异常或者直接黑屏。标准的处理是:Surface 销毁时关闭会话、关闭预览请求,Surface 重建时重新开会话。切后台同理,onPause里释放相机,onResume里重新打开。
fun openCamera(id: String) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) return manager.openCamera(id, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { this@Camera.openDevice = device; createSession() } override fun onDisconnected(device: CameraDevice) { device.close() } override fun onError(device: CameraDevice, error: Int) { device.close() } }, backgroundHandler) }backgroundHandler用一个单独的后台线程 Looper,别用主线程。会话回调、图像回调都走这个线程,主线程只负责 UI 更新。这一点在低端机上差别巨大,用主线程跑图像回调,预览帧率能掉一半。
3.3 拍照与 RAW 输出:两条路要分开走
普通拍照用ImageFormat.JPEG,简单直接,HAL 帮你编码好。RAW 输出要用ImageFormat.RAW_SENSOR,拿到的是未去马赛克的拜耳数据,需要自己处理。
RAW 有个硬性约束:输出尺寸必须是SENSOR_INFO_PIXEL_ARRAY_SIZE,不能随便缩放。所以你不能把预览和 RAW 塞进同一个 ImageReader。标准做法是开两个 ImageReader,一个 JPEG 用,一个 RAW 用,按需挂到会话上。
val rawSize = chars.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)!! val rawReader = ImageReader.newInstance( rawSize.width, rawSize.height, ImageFormat.RAW_SENSOR, 2 )拿到 Image 之后直接交给DngCreator封装成 DNG,这是最省事的做法,因为 DNG 里要写入大量元数据(色彩矩阵、噪声模型、黑电平、白电平),自己拼容易出错。
rawReader.setOnImageAvailableListener({ reader -> val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener val dng = DngCreator(chars, lastResult) dng.setOrientation(ExifInterface.ORIENTATION_NORMAL) FileOutputStream(file).use { dng.writeImage(it, image) } dng.close() image.close() }, backgroundHandler)lastResult是拍照那一刻的TotalCaptureResult,必须配套保存。没有它,DNG 里的元数据是空的,后期软件读进来色彩会完全不对。这个坑我踩过一次,导出的 DNG 在后期软件里打开是灰绿色的,查了半天才发现是结果对象没传。
提示:RAW 单帧的体积大约是像素数 × 2 字节,再叠加元数据。五千万像素的机器一张 DNG 接近 100MB,连续拍几张就会触发存储压力。做连拍前先算好剩余空间。
3.4 参数映射:把冷冰冰的数值变成能用的滑杆
曝光时间。范围通常是纳秒级的几千分之一秒到零点几秒。滑杆不要用线性映射,用对数。因为从 1/1000 到 1/500 的差别,人眼感知和从 1/60 到 1/30 的差别是一样的,都是差一档。用对数刻度,滑杆手感才对。
fun expToSlider(ns: Long, min: Long, max: Long): Float = (ln(ns.toDouble()) - ln(min.toDouble())) / (ln(max.toDouble()) - ln(min.toDouble())).toFloat() fun sliderToExp(p: Float, min: Long, max: Long): Long = exp(ln(min.toDouble()) + p * (ln(max.toDouble()) - ln(min.toDouble()))).toLong()ISO同样用对数,因为噪声随 ISO 的增长也是按比例感知的。范围一般在 50 到 3200 之间,部分机型能到 6400。要注意CONTROL_POST_RAW_SENSITIVITY_BOOST这个键,它可以在 RAW 域上再放大一档灵敏度,代价是噪点成倍增加,只在极暗场景下配合后期降噪才有意义。
对焦。上面说过单位是屈光度,所以滑杆应该反过来映射:滑杆最左边是最近对焦距离(对应LENS_INFO_MINIMUM_FOCUS_DISTANCE的最大值),最右边是无限远(对应 0)。中间千万不要线性分布,因为屈光度和实际距离是倒数关系——屈光度从 10 到 5,实际距离是从 0.1 米到 0.2 米;从 5 到 0,实际距离是从 0.2 米一路到无穷远。线性映射的结果就是滑杆前 10% 走完了 0.1 到 0.2 米,剩下的 90% 全在远处晃。
白平衡。如果只做预设,直接用CONTROL_AWB_MODE的几个枚举值最省事。要做色温滑杆,就得自己实现色温到 RGB 增益的换算,通常用一个黑体辐射近似公式算出目标白点的 XYZ,再转到传感器色彩空间求增益。这套东西网上有成熟的实现思路,但要注意每台设备的传感器色彩矩阵不同,从SENSOR_COLOR_TRANSFORM和SENSOR_CALIBRATION_TRANSFORM里读出来再算,不然不同机器上同一个色温值颜色会不一样。
3.5 界面与交互:手动模式最怕“看不见反馈”
专业模式的界面设计有一条第原则:任何一个参数的改动,都必须在画面上有即时可见的反馈。做不到这一点,用户就是在盲调。
具体做法是:每次滑动参数,立刻重建 CaptureRequest 并setRepeatingRequest。不要用一次性capture,那样预览是静止的。同时把当前的曝光值(EV)实时算出来显示在界面上,让用户知道自己在往哪个方向走。
EV 的计算方式:EV = log2(N² / t),其中 N 是光圈值,t 是曝光时间(秒)。举例,某机器固定光圈 f/1.8,你把快门设成 1/2000 秒,那么EV = log2(3.24 × 2000) = log2(6480) ≈ 12.7。这个数字的意义是:在同样的环境亮度下,正确曝光的 EV 大概是多少,你可以拿它当参考,避免参数飞出合理区间。
还有个实用的小设计:给每个参数加一个“锁定图标”,锁上之后这个参数在自动模式下也不被覆盖。这样用户可以只锁 ISO 让快门自动,或者只锁对焦让曝光自动,介于全自动和全手动之间,实际用起来比纯手动舒服得多。
4. 装别人做的原生相机 APK,坑在哪
4.1 为什么大部分移植包一装就闪退
先说结论:跨机型移植相机应用,成功率天然很低,这不是包做得差,是架构决定的。
系统相机应用通常依赖三类东西:系统私有的图像处理服务、厂商自定义的相机扩展接口、以及签名级的权限。第一类在你没装那个服务的机器上直接抛异常;第二类在接口版本不匹配时返回错误码;第三类最要命——某些权限只有和系统同签名的应用才拿得到,第三方安装的包申请了也批不下来。
所以你会看到这些典型现象:启动瞬间闪退(找不到服务类)、能启动但预览黑屏(权限或接口不匹配)、能预览但拍照无响应(HAL 请求被拒)、拍照成功但成片全黑(图像处理回调没走通)。
4.2 判断一个移植包值不值得装,看四件事
我总结了四个快速判断点,基本能在五分钟内给出结论。
| 判断项 | 怎么看 | 结论 |
|---|---|---|
| 目标系统版本跨度 | 包里声明的目标版本与你机器系统差几个大版本 | 跨三个大版本以上基本别抱期望 |
| 是否附带额外依赖 | 解包看有没有额外的服务或框架文件 | 需要额外依赖的,缺一个就是闪退 |
| 权限清单是否包含系统级权限 | 反编译看清单里的权限项 | 声明了签名级权限的,第三方装必失败 |
| 相机能力等级是否匹配 | 你机器的能力等级够不够它要求的功能 | 等级不够时功能会静默失效 |
第三条特别值得展开。很多人以为“我给了相机权限就能用”,实际上相机应用经常需要读取传感器信息、访问图像处理服务、写入特定目录,这些权限的授权主体是系统而不是用户,普通安装的包拿不到。这就是为什么同一个包在原机型上顺滑,换台机器就废。
4.3 更现实的选择:装“极简相机”而不是“移植原厂相机”
如果你的真实需求是“界面干净、成片不做作”,那我更推荐找那种基于公开接口开发的极简相机,而不是硬啃厂商移植包。极简相机只依赖系统公开的相机接口,兼容性覆盖几乎所有现代设备,功能少但稳定。
如果需求是“手动控制”,那更没必要装移植包——自己用底层接口写一个,或者用现成的开源示例改,比折腾移植包快得多。移植包最大的问题是不可维护,出问题你连日志都看不懂。
注意:安装来路不明的相机安装包存在数据风险。相机是权限最敏感的模块之一,能拿到画面就意味着能拿到画面里的一切。装之前至少确认来源并且检查权限清单,别为了一个滤镜把整个相册搭进去。
5. 常见问题与排查实录
5.1 预览黑屏、绿屏、花屏怎么定位
这三种现象的原因完全不同,别混着查。
黑屏:最常见是会话建立了但请求没提交,或者提交了但目标 Surface 不对。排查顺序是:确认 Surface 有效(isValid)、确认会话状态回调返回了成功、确认setRepeatingRequest真的被调用且没抛异常。还有一种情况是权限拿到了但设备被别的应用占用,此时onDisconnected会回调,一定要在这个回调里做资源释放和重试。
绿屏:通常是图像格式不匹配,或者色彩空间没设对。当你把预览输出配到某个不支持的格式上,HAL 会返回一帧未初始化的缓冲区,看起来就是一片绿。检查输出格式和尺寸是否在StreamConfigurationMap的可用列表里。
花屏、撕裂:多半是缓冲区竞争。ImageReader的maxImages给太小了(比如给了 1),上一帧还没处理完下一帧就来了。给 2 到 3 比较稳妥,同时记得每帧处理完必须close(),不关就会一直卡在等待缓冲区的状态。
5.2 对焦拉风箱和白平衡漂移
对焦拉风箱的根因是自动对焦在低对比场景下反复搜索。场景是纯色墙面、逆光剪影、玻璃幕墙的时候特别明显。解决思路有三条:切到手动对焦直接锁定距离;或者用区域对焦把对焦点限制在有纹理的位置;或者干脆用连续对焦但限制对焦区域大小。
白平衡漂移更烦人,表现为同一场景画面颜色缓慢来回变。原因是自动白平衡在灰色世界假设失效的场景(大面积单色、室内混合光源)下不断修正。做视频或者连拍时这个问题会毁掉素材。解决办法是拍之前先对着白纸或灰卡让自动白平衡收敛,然后锁定。手动模式里就是直接关掉自动白平衡,把增益固定住。
5.3 曝光闪烁与帧率抖动
手动模式下如果曝光时间设得比帧间隔还长,系统会通过降帧来凑曝光,表现出来就是预览卡顿。比如你设了 0.1 秒曝光,那帧率最高只能到 10 帧。这不是 bug,是物理限制。
要避免的话,把预览和拍照分开处理:预览用短曝光保证流畅,按下快门时再切换到长曝光一次性拍摄。这样体验和画质都能兼顾。
帧率抖动还有一个来源是CONTROL_AE_TARGET_FPS_RANGE没设。设成固定范围(比如 30,30)能显著提升预览稳定性,代价是在暗光下失去自动降帧提亮的能力。
5.4 问题速查表
| 现象 | 优先怀疑 | 快速验证 |
|---|---|---|
| 启动即崩溃 | 权限未授予或设备被占用 | 看异常堆栈是否指向 openCamera |
| 预览黑屏 | 会话未成功或 Surface 失效 | 打印会话状态回调 |
| 参数调了没反应 | 自动模式未关闭 | 检查 CONTROL_MODE 是否为 OFF |
| 对焦对不上 | 屈光度单位搞错 | 打印实际填入的值与范围 |
| 拍照全黑 | 请求未带拍照 Surface | 检查 addTarget 列表 |
| 成片偏色 | 白平衡增益错误 | 关掉手动白平衡对比 |
| 画面卡顿 | 曝光时间超过帧间隔 | 打印曝光值与帧率 |
6. 成像质量与性能,那些文档里不会写的细节
6.1 曝光参数的取舍要用算的,不要凭感觉
在固定光圈的手机上,你能调的只有快门和 ISO,而这两个参数是此消彼长的。同样一个 EV 值,可以用低 ISO 长曝光,也可以用高 ISO 短曝光,画质差别巨大。
具体的取舍逻辑是:先用安全快门兜底,再在剩下的空间里降 ISO。安全快门怎么算?先求等效焦距:
等效焦距 = 物理焦距 × (43.27 / 传感器对角线),对角线由SENSOR_INFO_PHYSICAL_SIZE的宽高平方和开根号得到。
举个例子,某机器传感器物理尺寸 7.6mm × 5.7mm,对角线约 9.5mm,等效系数约 4.55。镜头物理焦距 4.3mm,那等效焦距就是约 19.6mm。手持拍摄时,安全快门取等效焦距的倒数再乘一个保守系数,19.6mm 大概对应 1/20 秒,考虑到手机握持姿势和防抖水平,我一般再乘 2 到 3 倍,取 1/60 秒作为安全线。
有了这条线,剩下的就好办了:先用 1/60 秒,如果画面还是暗,就往上抬 ISO,直到 ISO 触到你个人的可接受噪声阈值(多数手机在 ISO 800 以内还能看,超过 1600 开始明显掉细节)。如果 ISO 顶格了还暗,才考虑放慢快门,同时想办法固定手机。
6.2 关掉自动,就等于关掉了多帧降噪
这是最容易被低估的一点。厂商相机的夜景之所以能看,靠的是连拍十几帧然后对齐叠加降噪。而在手动模式下,你把自动曝光关了,这套多帧管线通常也就停了,你拿到的是真正的单帧。
这意味着同样 ISO 1600,自动挡出片和手动挡出片,噪点水平可能差两三档。很多人第一次用手动模式拍夜景,看到满屏噪点就骂街,其实这就是传感器的真实水平,之前看到的是算法补出来的。
知道这一点之后,策略就变了:手动模式拍夜景,要么用三脚架配合低 ISO 长曝光,要么老老实实拍 RAW 交给后期降噪,别指望直出 JPEG 能看。
6.3 调试手段:日志之外还得看图
调相机最痛苦的是一堆参数看不见摸不着。我的做法是同时开三条线:
第一条是日志,把每次请求的关键参数(曝光、ISO、对焦、增益)打印出来,配合CaptureResult里返回的实际生效值对比。请求值和生效值不一致,说明有东西在覆盖你的设置。
第二条是直方图,实时显示在预览角落。手调曝光的时候,直方图比肉眼判断准得多,尤其是判断有没有过曝的时候。
第三条是截图对比,固定机位、固定参数,改一个变量拍一张,放一起比。参数调试是控制变量法的活,凭肉眼记是记不住的。
6.4 一个常被忽略的性能点:请求重建开销
每滑动一次滑杆就重建一次请求,听起来没什么,但在低端设备上这个操作是有成本的。滑杆快速拖动时如果每帧都重建,会导致请求队列堆积,预览反而更卡。
比较稳的做法是加一层节流:滑杆的连续事件合并到下一帧统一处理,用Choreographer或者一个简单的标志位都行。我实测下来,把更新频率压到 30Hz 左右,手感和性能的平衡最好——再高用户也感知不到,再低就觉得跟手性差了。
7. 关于 RAW 和后期,我的一些实际体会
RAW 不是万灵药。很多人以为拍了 RAW 就能救回一切,实际上 RAW 能救回来的动态范围大概只有一到两档,超过这个范围的信息在传感器采样阶段就已经丢了,后期再怎么拉也是拉出一堆噪声。
更现实的价值在于白平衡和锐化。RAW 没有经过锐化,边缘是干净的;白平衡可以在后期无损调整,不受拍摄时设定的影响。这两点对需要统一风格的一组片子来说非常有用。
但也有代价:DNG 文件大、处理慢、手机上看预览还得装专门的软件。如果只是发社交平台,直出 JPEG 完全够用。我现在的习惯是——出去认真拍用 RAW,随手记录用 JPEG,不纠结。
还有一个细节:不同机型写出来的 DNG,在后期软件里的初始色彩表现差别很大,有的偏灰、有的偏品红。这不是格式坏了,是内嵌的色彩矩阵和相机配置文件不同。想省事的话,可以在导出时一并写入相机配置文件,或者在后期软件里手动指定输入色彩空间。
最后分享一个我自己的习惯:每台新设备到手,第一件事不是装各种相机应用,而是写一个几十行的小程序,把它的能力列表、曝光范围、ISO 范围、对焦范围、支持的输出尺寸全部打印出来存一份。这份“设备画像”在后续所有调试里都是基准线——参数调不动的时候,先回头看看这台机器到底支不支持你想做的事,往往答案就在那张表里。