Android 人脸识别实战:兼容不同厂商摄像头的 CameraX、Camera API1 与 RK3568 排障
2026/7/23 5:31:15 网站建设 项目流程

Android 人脸识别实战:兼容不同厂商摄像头的 CameraX、Camera API1 与 RK3568 排障

最近在做一个 Android 端离线人脸识别项目,业务场景是门口横屏设备实时识别人脸。手机上测试一切正常,但是放到 RK3568 这类定制 Android 硬件上后,最典型的问题就来了:

App 已经授权相机权限,但页面没有相机预览流,或者 CameraX 绑定成功后一直没有首帧。

这类问题很容易被误判成“SDK 不识别人脸”或者“权限没给”,但真正排查下来,相机链路通常才是第一道坎。本文记录一套比较稳的实战方案:优先使用 CameraX,失败或无首帧时降级到 Android Camera API1,然后统一把 NV21 画面交给人脸 SDK 分析。

本文不绑定某一个人脸 SDK,旷视、百度或其他本地人脸 SDK 都可以使用类似思路。核心目标只有一个:先稳定拿到不同厂商设备上的摄像头预览流。

1. 先搞清楚 Android 相机链路

很多人看到设备上有/dev/video0,就以为 Android App 一定能打开摄像头。实际不是这样。

Android App 常规访问摄像头的链路是:

App -> CameraX / Camera2 / Camera API1 -> CameraService -> Camera HAL / External Camera Provider -> Linux 驱动 / UVC / MIPI 摄像头 -> /dev/video*

所以要注意两点:

  1. Camera API1 不能绕过 Camera HAL。它只是旧版 Java 相机接口,底层仍然要经过系统 CameraService 和 HAL。
  2. /dev/video*存在,不代表 Android Framework 一定能枚举到摄像头。如果 CameraService 枚举结果是 0,App 通过 CameraX、Camera2、Camera API1 都可能拿不到画面。

在 RK3568、RK3588、全志、MTK 定制板等设备上,最容易出问题的是 HAL 或 External Camera Provider 没有适配好 USB 摄像头,或者系统固件只给某个厂商测试 App 开了私有通道。

2. 权限先确认,但不要只盯权限

Manifest 至少需要:

<uses-permissionandroid:name="android.permission.CAMERA"/><uses-featureandroid:name="android.hardware.camera"android:required="false"/><uses-featureandroid:name="android.hardware.camera.front"android:required="false"/>

运行时也要申请:

if(ContextCompat.checkSelfPermission(context,Manifest.permission.CAMERA)!=PackageManager.PERMISSION_GRANTED){requestPermissionLauncher.launch(Manifest.permission.CAMERA)}

如果设备上明明授权了,但仍然没有画面,可以继续检查:

adb shell pm list permissions-g-dadb shell dumpsys package your.package.name|grepCAMERA adb shell appops get your.package.name CAMERA

如果权限正常,就不要一直卡在权限层面了。下一步应该看系统到底有没有把摄像头暴露给 Android CameraService。

3. CameraX 优先:枚举所有相机,而不是写死前置

很多业务代码会直接写:

CameraSelector.DEFAULT_FRONT_CAMERA

这在手机上通常没问题,但在门口闸机、工控屏、RK 板子、外接 USB 摄像头上就不稳。更好的方式是先枚举 CameraX 能看到的所有相机,再按业务优先级选择。

我当前项目里的策略是:

FRONT -> EXTERNAL -> BACK -> ANY

也就是优先前置,其次外接,再后置,实在识别不出类型就拿第一个可用相机。

核心代码如下:

internalenumclassCameraKind{FRONT,EXTERNAL,BACK,UNKNOWN,ANY,}internalfunselectPreferredCameraKind(available:List<CameraKind>):CameraKind?=when{available.isEmpty()->nullCameraKind.FRONTinavailable->CameraKind.FRONT CameraKind.EXTERNALinavailable->CameraKind.EXTERNAL CameraKind.BACKinavailable->CameraKind.BACKelse->CameraKind.ANY}

CameraX 枚举时可以把 cameraId 和 lensFacing 打出来:

@OptIn(ExperimentalCamera2Interop::class)privatefunCameraInfo.toCameraInventoryItem():CameraInventoryItem{valkind=when(lensFacing){CameraSelector.LENS_FACING_FRONT->CameraKind.FRONT CameraSelector.LENS_FACING_EXTERNAL->CameraKind.EXTERNAL CameraSelector.LENS_FACING_BACK->CameraKind.BACKelse->CameraKind.UNKNOWN}valcameraId=runCatching{Camera2CameraInfo.from(this).cameraId}.getOrDefault("unknown")returnCameraInventoryItem(info=this,id=cameraId,kind=kind,)}

这一步非常关键。因为不同厂商设备可能把 USB 摄像头标成EXTERNAL,也可能标成BACK,甚至是UNKNOWN。不要按设备型号写死,尽量按系统能力列表动态选择。

4. CameraX 绑定成功,不代表真的有画面

有些设备更“迷惑”:CameraX 没有抛异常,bindToLifecycle也执行成功了,但预览就是黑的,分析器也没有收到帧。

我的处理方式是加一个首帧超时。如果 3 秒内没有收到ImageAnalysis第一帧,就认为 CameraX 在当前设备上不可用,自动降级到 Camera API1。

privateconstvalCAMERAX_FIRST_FRAME_TIMEOUT_MS=3_000LvalfirstFrameReceived=AtomicBoolean(false)valfallbackStarted=AtomicBoolean(false)valmainHandler=Handler(Looper.getMainLooper())funfallbackToCamera1(error:Throwable){if(!active||!fallbackStarted.compareAndSet(false,true))returnfirstFrameTimeout?.let{timeout->mainHandler.removeCallbacks(timeout)}imageAnalysis?.clearAnalyzer()runCatching{cameraProvider?.unbindAll()}Log.e("FaceCamera","CameraX failed; falling back to Camera API1",error)onFallbackToCamera1(error)}firstFrameTimeout=Runnable{if(active&&!firstFrameReceived.get()){fallbackToCamera1(IllegalStateException("CameraX produced no frames within${CAMERAX_FIRST_FRAME_TIMEOUT_MS}ms",),)}}

分析器收到第一帧时取消超时:

useCase.setAnalyzer(analysisExecutor){image->if(firstFrameReceived.compareAndSet(false,true)){firstFrameTimeout?.let{timeout->mainHandler.removeCallbacks(timeout)}Log.i("FaceCamera","CameraX delivered the first frame")}analyzer.analyze(image)}

绑定完成后启动超时检测:

provider.bindToLifecycle(lifecycleOwner,selectedCamera.info.cameraSelector,preview,analysis,)if(!firstFrameReceived.get()){firstFrameTimeout?.let{timeout->mainHandler.postDelayed(timeout,CAMERAX_FIRST_FRAME_TIMEOUT_MS)}}

这个机制在定制硬件上很实用。因为它不只判断“能不能 open”,还判断“业务到底有没有拿到帧”。

5. Camera API1 兜底:拿 NV21 预览流

CameraX 不稳定时,可以用 Camera API1 做兜底。虽然 API1 已废弃,但很多工控 Android 系统对它的兼容反而更直接。

注意:这里不是说 API1 比 CameraX 高级,而是在定制设备上多一条兼容路径。

一个 Compose 页面里可以这样封装:

@ComposablefunCameraPreview(analyzer:CameraXFaceAnalyzer,onCameraError:(String)->Unit,modifier:Modifier=Modifier,){varuseCamera1byremember(analyzer){mutableStateOf(false)}if(useCamera1){Camera1Preview(analyzer=analyzer,onCameraError=onCameraError,modifier=modifier,)}else{CameraXPreview(analyzer=analyzer,onFallbackToCamera1={useCamera1=true},modifier=modifier,)}}

API1 侧使用TextureView承载预览,并通过setPreviewCallbackWithBuffer复用缓冲区,避免频繁分配内存:

candidate.setPreviewTexture(surface)candidate.setDisplayOrientation(config.displayOrientation)candidate.setPreviewCallbackWithBuffer(this)repeat(PREVIEW_BUFFER_COUNT){candidate.addCallbackBuffer(ByteArray(config.bufferSize))}candidate.startPreview()

回调里把 NV21 数据交给统一的人脸分析器:

overridefunonPreviewFrame(data:ByteArray?,sourceCamera:Camera?){if(data==null||sourceCamera==null||disposed.get())returnvalconfig=cameraConfigtry{if(config!=null){currentAnalyzer.analyzeNv21(data=data,width=config.width,height=config.height,rotation=config.frameRotation,isBackCamera=config.isBackCamera,)}}finally{runCatching{sourceCamera.addCallbackBuffer(data)}}}

这里一定要在finally里把 buffer 还回去,否则跑一会儿就可能没有预览回调了。

6. API1 不要写死尺寸、FPS、格式

不同厂商摄像头支持的预览尺寸和 FPS 差异很大。不要直接写死1920x108030fps,应该读取能力列表后选择最接近业务目标的配置。

privatefunconfigureCamera(sourceCamera:Camera,descriptor:Camera1Descriptor,):Camera1Config{valparameters=sourceCamera.parametersvalsupportedFormats=parameters.supportedPreviewFormats.orEmpty()if(supportedFormats.isNotEmpty()&&ImageFormat.NV21!insupportedFormats){throwIllegalStateException("Camera API1 不支持 NV21 预览格式")}valsupportedSizes=parameters.supportedPreviewSizes.orEmpty()valpreviewSize=choosePreviewSize(supportedSizes)?:parameters.previewSizevalsupportedFpsRanges=parameters.supportedPreviewFpsRange.orEmpty()valfpsRange=choosePreviewFpsRange(supportedFpsRanges)Log.i("FaceCamera1","supportedFormats=$supportedFormats, "+"supportedSizes=${supportedSizes.joinToString{"${it.width}x${it.height}"}},"+"supportedFps=${supportedFpsRanges.joinToString{it.contentToString()}}",)parameters.previewFormat=ImageFormat.NV21 parameters.setPreviewSize(previewSize.width,previewSize.height)fpsRange?.let{range->parameters.setPreviewFpsRange(range[0],range[1])}if(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEOinparameters.supportedFocusModes.orEmpty()){parameters.focusMode=Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO}sourceCamera.parameters=parameters// 省略 rotation、bufferSize 等计算}

尺寸选择可以按比例和面积接近度来选:

privateconstvalTARGET_PREVIEW_AREA=1280L*720LprivateconstvalTARGET_ASPECT_RATIO=16.0/9.0privatefunchoosePreviewSize(sizes:List<Camera.Size>):Camera.Size?=sizes.minWithOrNull(compareBy<Camera.Size>({size->abs(size.width.toDouble()/size.height-TARGET_ASPECT_RATIO)},{size->abs(size.width.toLong()*size.height-TARGET_PREVIEW_AREA)},),)

FPS 也一样:

privateconstvalTARGET_MIN_FPS=15_000privateconstvalTARGET_MAX_FPS=30_000privatefunchoosePreviewFpsRange(ranges:List<IntArray>):IntArray?=ranges.filter{range->range.size>=2}.minWithOrNull(compareBy<IntArray>({range->abs(range[1]-TARGET_MAX_FPS)},{range->abs(range[0]-TARGET_MIN_FPS)},),)

Android Camera API1 里的 FPS 单位是乘以 1000 的,例如30000表示 30fps。

7. 横屏门口设备要处理旋转和填充

门口设备常见是横屏安装,有些摄像头方向还会反着来。如果只把预览画面铺满,可能出现人脸方向不对、预览被拉伸、识别框和人脸位置对不上的问题。

API1 需要计算两套角度:

  1. displayOrientation:给预览显示用。
  2. frameRotation:给人脸 SDK 分析用。
valdisplayDegrees=displayRotationDegrees()valisFrontCamera=descriptor.info.facing==Camera.CameraInfo.CAMERA_FACING_FRONTvalisBackCamera=descriptor.info.facing==Camera.CameraInfo.CAMERA_FACING_BACKvalframeRotation=if(isFrontCamera){(descriptor.info.orientation+displayDegrees)%360}else{(descriptor.info.orientation-displayDegrees+360)%360}valdisplayOrientation=if(isFrontCamera){(360-(descriptor.info.orientation+displayDegrees)%360)%360}else{frameRotation}

TextureView预览建议做fill center,保持比例填满容器:

privatefunapplyFillCenterTransform(config:Camera1Config){if(width<=0||height<=0)returnvalswapsDimensions=config.displayOrientation==90||config.displayOrientation==270valbufferWidth=if(swapsDimensions)config.height.toFloat()elseconfig.width.toFloat()valbufferHeight=if(swapsDimensions)config.width.toFloat()elseconfig.height.toFloat()valviewWidth=width.toFloat()valviewHeight=height.toFloat()valdefaultScaleX=viewWidth/bufferWidthvaldefaultScaleY=viewHeight/bufferHeightvalfillScale=max(defaultScaleX,defaultScaleY)valmatrix=Matrix().apply{setScale(fillScale/defaultScaleX,fillScale/defaultScaleY,viewWidth/2f,viewHeight/2f,)}setTransform(matrix)}

如果是门口人脸识别场景,通常建议:

  1. 预览区域按 16:9 或设备实际屏幕比例设计。
  2. 人脸检测取最大人脸,减少多人经过时误识别。
  3. SDK 分析帧可以节流,例如 200ms 到 300ms 分析一次,不需要每一帧都做人脸比对。

8. CameraX 和 API1 统一到 NV21 分析入口

为了后续换 SDK 或集成到其他应用,建议把相机层和人脸 SDK 层解耦。相机只负责产出帧,人脸模块只关心统一格式。

可以定义一个统一的 NV21 帧对象:

dataclassNv21Frame(valdata:ByteArray,valwidth:Int,valheight:Int,valrotation:Int,valisBackCamera:Boolean,)

CameraX 的ImageProxyYUV_420_888,可以转成 NV21:

privatefunImageProxy.toNv21Frame(isBackCamera:Boolean):Nv21Frame{valnv21=Yuv420888ToNv21Converter.convert(width=width,height=height,y=planes[0].toPlaneData(),u=planes[1].toPlaneData(),v=planes[2].toPlaneData(),)returnNv21Frame(data=nv21,width=width,height=height,rotation=imageInfo.rotationDegrees,isBackCamera=isBackCamera,)}

API1 本身就可以配置为 NV21,所以直接提交:

funanalyzeNv21(data:ByteArray,width:Int,height:Int,rotation:Int,isBackCamera:Boolean,){valnormalizedRotation=((rotation%360)+360)%360require(normalizedRotation%90==0){"frame rotation must be a multiple of 90"}valframe=Nv21Frame(data=data,width=width,height=height,rotation=normalizedRotation,isBackCamera=isBackCamera,)submit(frame)}

这样 CameraX 和 API1 都走同一个identify(frame),后续无论接旷视、百度还是其他算法,只要适配一个输入层即可。

9. RK3568 设备排障思路

如果手机正常,RK3568 上没有预览流,可以按下面顺序排查。

第一步,看 Android Framework 能不能枚举到相机:

adb shell dumpsys media.camera

重点看:

Number of camera devices: 0

或者有没有对应的Camera IDFacingOrientationDevice version等信息。

如果这里就是 0,那 App 侧怎么改 CameraX、Camera2、API1 都可能打不开。因为系统 CameraService 没有相机可用。

第二步,看 Linux 设备节点:

adb shellls-l/dev/video*

如果/dev/video*存在,但dumpsys media.camera枚举为 0,说明摄像头可能只在 Linux 驱动层存在,还没有接入 Android Camera HAL。

这种情况常见解决方向是:

  1. 找硬件厂商确认固件是否启用了 External Camera Provider。
  2. 确认 USB 摄像头是否是标准 UVC。
  3. 检查系统是否有 camera provider 进程。
  4. 让固件厂商把/dev/video*映射到 Android CameraService。
  5. 如果系统无法改,只能考虑 App 自己通过 UVC/libusb 直接取流,但这就不是标准 Camera API 方案了。

第三步,看日志:

adb logcat|grep-icamera

常见关键词:

CameraService CameraProvider CameraDevice open camera failed No camera available Permission denied

如果是Permission denied,再回头看权限、SELinux、设备节点访问策略。如果是 provider/HAL 报错,就要找系统固件层。

10. 常见问题对照表

现象可能原因建议处理
手机有画面,RK3568 没画面固件 HAL 没暴露摄像头先看dumpsys media.camera
/dev/video0存在,但 App 枚举不到Linux 驱动有,Android CameraService 没接上找厂商适配 External Camera Provider
CameraX 绑定成功但无首帧CameraX/Camera2 在当前系统兼容性差加首帧超时,自动切 Camera API1
API1getNumberOfCameras()为 0Framework 没相机App 侧无法通过标准 Camera API 解决
有预览但识别方向错rotation 传错区分显示旋转和算法帧旋转
预览拉伸TextureView 没按比例缩放使用 fill center 矩阵
跑一会儿没回调API1 buffer 没归还finally里调用addCallbackBuffer
识别卡顿每帧都做人脸比对对分析帧节流,只取最大人脸

11. 性能和生命周期建议

实时人脸识别不是单纯“拿到帧”就完了,还要避免设备长时间运行后发热、卡顿、内存抖动。

建议做到:

  1. ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST,永远处理最新帧。
  2. 人脸分析加节流,例如 250ms 一次。
  3. API1 使用setPreviewCallbackWithBuffer复用内存。
  4. 页面销毁时释放相机、清空 analyzer、关闭线程。
  5. 不在主线程做人脸检测和 1:N 比对。
  6. 人员库保存人脸特征向量,不要每次拿原图重新提特征。

一个简单的节流逻辑:

privatefuntryAcquireFrameSlot():Boolean{if(closed.get())returnfalsevalnow=SystemClock.elapsedRealtime()valprevious=lastSubmittedAt.get()if(previous!=0L&&now-previous<frameIntervalMs)returnfalseif(!processing.compareAndSet(false,true))returnfalselastSubmittedAt.set(now)returntrue}

12. 完整降级流程总结

最终我的推荐流程是:

申请 CAMERA 权限 -> CameraX 枚举相机 -> 按 FRONT / EXTERNAL / BACK / ANY 选择相机 -> 绑定 Preview + ImageAnalysis -> 3 秒内收到首帧 -> YUV_420_888 转 NV21 -> 人脸 SDK 分析 -> 3 秒内没有首帧或绑定失败 -> 切换 Camera API1 -> 枚举 API1 相机 -> 选择尺寸 / FPS / NV21 -> setPreviewCallbackWithBuffer -> NV21 交给同一个人脸分析器

如果 CameraX 和 Camera API1 都枚举不到摄像头,那就不是 App 层普通兼容代码能解决的问题了,优先检查系统固件、Camera HAL、External Camera Provider、UVC 驱动和 SELinux 策略。

结语

做人脸识别项目时,人脸 SDK 往往不是第一个难点。真正影响落地的,通常是不同 Android 硬件上的摄像头兼容性。

我的经验是:不要按厂商型号硬编码,不要只判断 open 成功,不要只盯权限。更稳的方式是围绕系统能力列表做适配,CameraX 拿不到真实帧就及时降级 Camera API1,同时把两套相机输入统一成 NV21,再交给人脸识别模块。

这样做的好处是,业务层只关心“识别成功、未注册、识别失败”等结果,相机兼容细节都收敛在独立模块里。后续无论换设备、换摄像头,还是把人脸 SDK 模块集成到其他 App,维护成本都会低很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询