Android NDK 相机开发实战:从搭建会话到定格那一帧
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 Camera2NDK Camera API职责CameraManagerACameraManager设备枚举、特性查询CameraDeviceACameraDevice持设备、创建会话CaptureSessionACameraCaptureSession输出容器、提交请求CaptureRequestACaptureRequest配输出目标与参数ImageReaderAImageReader帧队列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 SurfaceJava 建 SurfaceTexture包成 Surface 传给 nativeANativeWindow_fromSurface转窗口Java 侧 Matrix 处理低直连 GPU标准 UI 集成NativeActivity 的 app-windowANativeWindow_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_8883 平面 Y/U/V4:2:0 子采样实时处理、显示转换必须看 rowStride / pixelStride不能裸读像素AIMAGE_FORMAT_RAW1616bit Bayer 原始数据自定义 ISP、专业后期数据量大设备支持参差不齐以实际设备为准目的是显示或存档JPEG 路线最省事要喂模型或逐像素处理就选 YUV。示例里的 YUV→RGB 整数转换加四方向旋转处理在 image_reader.cpp可以当基线抄。六、回调、错误与资源回收生产级 NDK 相机代码清单这一节解决线上不崩、不泄漏、不沉默。三层回调都要注册回调结构注册位置触发时机处理建议ACameraDevice_stateCallbacksopenCamera 第 3 参设备断连、设备错误标记不可用并 ACameraDevice_closeACameraCaptureSession_stateCallbackscreateCaptureSession 第 3 参会话 ready / active / closed更新内部状态机ACameraCaptureSession_captureCallbackscapture() 第 2 参拍摄序列结束 / 中止 / 失败恢复 repeating 预览、通知 UI// C 回调不带对象用静态 trampoline context 传回 this static void OnSeqEnd(void* ctx, ACameraCaptureSession* s, int seqId, int64_t frame) { reinterpret_castNDKCamera*(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再 _freeACameraOutputTarget_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 请求再把结果送回 nativeCameraActivity 演示了这套往返。旋屏时别硬改会话。示例的策略是配置变化就删掉整个相机重建简单可靠。想再往深处走有三个方向都有现成参照拉 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),仅供参考