Android NDK 相机开发实战:从搭建会话到定格那一帧
【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples
这篇文章写给想在 Android NDK 层做相机开发的人:当你的主业务在 C++ 里,或者想绕开 Java 直接操控曝光、感光度与图像格式时,就会用到 NDK Camera API。我们以 ndk-samples 仓库 camera 目录下的官方示例为蓝本,讲清楚整个链路:怎么建会话、预览挂到哪里、照片怎么定格,以及收尾时怎么不泄漏。
一、为什么值得在 NDK 层写相机:三个实在的理由
这一节回答"图什么"。理由只有三条,占任何一条你就该往下看。
第一条是性能上限。预览帧直接进 GPU 纹理或 native 窗口,不逐帧过 Java 对象,JNI 的开销基本被抹掉。第二条是控制粒度。NDK Camera API 允许你往 ACaptureRequest 里直接写 ACAMERA_SENSOR_EXPOSURE_TIME、ACAMERA_SENSOR_SENSITIVITY 这类 tag,和 Java 的 Camera2 能力对齐,但少了一层中间商。第三条是复用。如果你的视觉流水线、推理引擎本来就是一整个 C++ 技术栈,相机模块放在同一侧,数据就不用跨界序列化。
代价同样实在:API 是纯 C 接口,引用计数和资源回收全靠你自己。这是第六节要重点讲的事。
二、一条链路看懂 NDK 相机数据流:从传感器到后处理
这一节给你一张导航地图,后面所有章节都是对这条链路上某个节点的展开。
Android NDK 相机数据流:请求驱动一切,输出目标统一是 ANativeWindow
三个要点记牢:
- 请求是驱动力。不发请求就没有帧,
setRepeatingRequest管连续预览,capture管单次拍摄。 - 输出目标统一是 ANativeWindow。无论你是想进 TextureView 还是 AImageReader,到了 NDK 层都是一个窗口。
- 会话是输出目标的"容器"。先把窗口装进 ACaptureSessionOutputContainer,再围绕容器创建会话。
Java 的 Camera2 五个对象和 NDK 一一对应,方便你切换心智模型:
| Java Camera2 | NDK Camera API | 职责 |
|---|---|---|
| CameraManager | ACameraManager | 设备枚举、特性查询 |
| CameraDevice | ACameraDevice | 持设备、创建会话 |
| CaptureSession | ACameraCaptureSession | 输出容器、提交请求 |
| CaptureRequest | ACaptureRequest | 配输出目标与参数 |
| ImageReader | AImageReader | 帧队列(mediandk 提供) |
三、搭建相机会话:管理器、设备与请求三件套
这一节解决"从零到一个可用设备"的问题。完整封装可参考 NDKCamera 会话封装,下面是同样的流程骨架。
枚举与打开:找到一台能用的相机
// 管理器建一次、活到退出,避免反复枚举 ACameraManager* mgr = ACameraManager_create(); ACameraIdList* ids = nullptr; ACameraManager_getCameraIdList(mgr, &ids); // 逐个查 ACAMERA_LENS_FACING,优先选后置; // 找不到后置就退回第一台,保证逻辑不中断 const char* pickId = ...; ACameraDevice* dev = nullptr; // 状态回调在这里传入,设备断连怎么办见第六节 ACameraManager_openCamera(mgr, pickId, &dev);特性信息统一走 ACameraMetadata 拿。做手动曝光滑杆之前,务必先读 ACAMERA_SENSOR_INFO_EXPOSURE_TIME_RANGE 与 ACAMERA_SENSOR_INFO_SENSITIVITY_RANGE 两个范围,别写出设备支持之外的值。示例还把曝光钳在 1ms~250ms:不少设备上报的范围远大于实际可用,写到极值画面直接全黑。
会话配置:输出目标与重复请求
ACaptureSessionOutputContainer* box; ACaptureSessionOutputContainer_create(&box); ANativeWindow_acquire(win); // 接管引用计数,析构时成对 release ACaptureSessionOutput* out; ACaptureSessionOutput_create(win, &out); ACaptureSessionOutputContainer_add(box, out); ACameraOutputTarget* target; ACameraOutputTarget_create(win, &target); ACaptureRequest* req; ACameraDevice_createCaptureRequest(dev, TEMPLATE_PREVIEW, &req); ACaptureRequest_addTarget(req, target); ACameraCaptureSession* session; ACameraDevice_createCaptureSession(dev, box, sessionCb, &session);分辨率选择有一条经验法则:预览取与窗口宽高比相同的最小分辨率(减小后处理量),拍照取最大 JPEG 分辨率;找不到匹配的才退回 640x480。
四、NDK 相机预览实现:TextureView 与 ANativeWindow 怎么选
这一节解决"帧出来了往哪里挂"。
两条主流预览路径
| 方案 | 实现路径 | 旋转/适配 | CPU 开销 | 适用场景 |
|---|---|---|---|---|
| TextureView + Surface | Java 建 SurfaceTexture,包成 Surface 传给 native,ANativeWindow_fromSurface转窗口 | Java 侧 Matrix 处理 | 低,直连 GPU | 标准 UI 集成 |
| NativeActivity 的 app->window | ANativeWindow_lock/unlockAndPost,自己写 YUV→RGB | 自己实现四个方向的旋转 | 高,CPU 软转换 | 无 Java UI 依赖的纯 native 应用 |
| AImageReader 仅取帧 | 不做显示,只处理帧 | — | 中高 | 数据抓取、算法输入 |
TextureView 路线用得最多,绑定的核心就几行:
// Java 侧:权限授予后建引擎,surface 就绪才交 Surface 给 native @Override public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) { surface_ = new Surface(st); onPreviewSurfaceCreated(ndkCamera_, surface_); }// native 侧:Surface 转成 ANativeWindow,建会话后立刻发重复请求 extern "C" void JNICALL Java_..._onPreviewSurfaceCreated( JNIEnv* env, jobject, jlong engine, jobject surface) { ANativeWindow* win = ANativeWindow_fromSurface(env, surface); camera_->CreateSession(win); // 第三节的流程 camera_->StartPreview(true); }容易忽略的一点:缓冲尺寸和视图尺寸未必一致,要按选中的预览分辨率重算视图的布局参数,并用变换矩阵处理 90/270 度旋转。ViewActivity 里有完整实现,可直接对照。
五、AImageReader JPEG 捕获:定格那一帧与格式选型
这一节解决"照片怎么存、格式怎么选、写盘怎么不阻塞"。
AImageReader:回调驱动的帧队列
AImageReader 本质是一个定长帧队列加一个门铃:
// 建 4 槽的 JPEG 队列;getWindow 拿到的窗口就是第三节的输出目标 AImageReader* reader = nullptr; AImageReader_new(w, h, AIMAGE_FORMAT_JPEG, 4, &reader); AImageReader_ImageListener listener{ .context = this, .onImageAvailable = OnImageAvailable, // 门铃:有帧就绪时通知 }; AImageReader_setImageListener(reader, &listener); ANativeWindow* win; AImageReader_getWindow(reader, &win);Android NDK 相机 JPEG 捕获时序:暂停、单次、写盘、恢复,四步缺一不可
取帧有两个 API,语义要分清:AImageReader_acquireNextImage逐帧取,适合批处理;AImageReader_acquireLatestImage直接拿最新帧并丢弃旧帧,适合实时场景。JPEG 捕获时把写文件扔进独立线程,别把回调堵住,否则下一帧的 doorbell 会排队。
三种格式:JPEG、YUV、RAW
| 格式 | 数据形态 | 典型用途 | 注意事项 |
|---|---|---|---|
| AIMAGE_FORMAT_JPEG | 单平面压缩流,拿起来就能存 | 照片保存 | 先设 ACAMERA_JPEG_ORIENTATION,否则存出的图不旋转 |
| AIMAGE_FORMAT_YUV_420_888 | 3 平面 Y/U/V,4:2:0 子采样 | 实时处理、显示转换 | 必须看 rowStride / pixelStride,不能裸读像素 |
| AIMAGE_FORMAT_RAW16 | 16bit Bayer 原始数据 | 自定义 ISP、专业后期 | 数据量大,设备支持参差不齐,以实际设备为准 |
目的是显示或存档,JPEG 路线最省事;要喂模型或逐像素处理,就选 YUV。示例里的 YUV→RGB 整数转换加四方向旋转处理在 image_reader.cpp,可以当基线抄。
六、回调、错误与资源回收:生产级 NDK 相机代码清单
这一节解决"线上不崩、不泄漏、不沉默"。
三层回调都要注册
| 回调结构 | 注册位置 | 触发时机 | 处理建议 |
|---|---|---|---|
| ACameraDevice_stateCallbacks | openCamera 第 3 参 | 设备断连、设备错误 | 标记不可用并 ACameraDevice_close |
| ACameraCaptureSession_stateCallbacks | createCaptureSession 第 3 参 | 会话 ready / active / closed | 更新内部状态机 |
| ACameraCaptureSession_captureCallbacks | capture() 第 2 参 | 拍摄序列结束 / 中止 / 失败 | 恢复 repeating 预览、通知 UI |
// C 回调不带对象,用静态 trampoline + context 传回 this static void OnSeqEnd(void* ctx, ACameraCaptureSession* s, int seqId, int64_t frame) { reinterpret_cast<NDKCamera*>(ctx)->ResumePreviewIfJpg(seqId); } static ACameraCaptureSession_captureCallbacks cb{ .context = this, .onCaptureSequenceCompleted = OnSeqEnd, .onCaptureFailed = OnFailed, };拍照最容易栽的坑:序列完成回调里忘了恢复预览,拍完一张画面就冻住。示例的做法是比对 seqId 是不是拍照那条序列,是的话就把 repeating 请求换回预览。
错误映射与清理清单
别假设 camera_status_t 永远返回 OK。先把错误码映射成可读字符串再分支,完整的映射表在 camera_utils.cpp。高频的几个:ACAMERA_ERROR_NOT_ENOUGH_MEMORY(缓冲不够)、ACAMERA_ERROR_CAMERA_IN_USE(被别的进程占着)、ACAMERA_ERROR_STREAM_CONFIGURE_FAIL(输出目标组合不被支持)。
清理顺序与创建相反,可以逐项打勾:
- 先对活跃会话 stopRepeating,再 close
- 每个 ACaptureRequest:先 removeTarget,再 _free
- ACameraOutputTarget_free、ACaptureSessionOutput_free 逐条释放
- ANativeWindow 的 acquire / release 配对,建会话时接管的那个别漏
- 容器 _free、设备 close、管理器 _delete,按此顺序收尾
- debug 构建打印 tag / 错误字符串映射,排查效率差一个量级
七、NDK 相机避坑清单与进阶方向
这一节是高频问题的浓缩版,外加几个能继续挖的方向。
- ⚠️手动调曝光没反应:八成是忘了先把 ACAMERA_CONTROL_AE_MODE 设为 ACAMERA_CONTROL_AE_MODE_OFF,自动曝光算法会盖掉你写的值。
- 存出来的照片方向不对:预览旋转和 JPEG 旋转是两回事,预览靠显示端转,照片必须显式写 ACAMERA_JPEG_ORIENTATION;前置相机还要多做一次镜像换算。
- 曝光时间单位是 ns 不是 ms。你以为写了 33000 是 33ms,其实是 33μs,画面会糊成一片。
- 权限还是要走 Java 层。native 代码没有代申请权限的能力,只能回调 Activity 请求,再把结果送回 native,CameraActivity 演示了这套往返。
- 旋屏时别硬改会话。示例的策略是配置变化就删掉整个相机重建,简单可靠。
想再往深处走,有三个方向都有现成参照:
- 拉 RAW16 流,接自己的 demosaic 与白平衡模块;
- 在同一会话里加第二个输出目标——YUV 预览加 JPEG 捕获已经是两条流,可以再试第三条分析流;
- 把预览 YUV 帧直接喂给 NNAPI 推理管线,让相机模块和 nn-samples 模块串起来。
先做最小的一步:git clone https://gitcode.com/gh_mirrors/nd/ndk-samples,把 camera/texture-view 示例跑在一台 Camera2 级别的真机上,看到预览稳定输出后,再把输出目标从 Surface 换成 AImageReader,打印每帧的到达时间——第二到五节的数据流,到时就真正长在你脑子里了。
【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考