如何将USB摄像头回调的YUV ByteBuffer转换为android.media.Image/ImageProxy用于ML Kit分析
嘿,我来帮你搞定这个问题!你现在遇到的情况其实挺普遍的——USB摄像头回调给你的YUV格式ByteBuffer,没法直接用ML Kit的fromByteBuffer方法,那咱们得把它转换成Android的android.media.Image或者ImageProxy才行。下面给你两种实用的方案,你可以根据自己的项目场景来选:
方案一:直接创建android.media.Image(适配Android 8.0+)
Android的Image类是不可变的,所以咱们得借助ImageWriter来生成可写入的Image实例,再把YUV ByteBuffer的数据拆分到Image的对应平面里。这里假设你的ByteBuffer是常见的NV21格式(大部分USB摄像头默认输出这个格式),步骤如下:
1. 初始化ImageWriter
先在初始化环节(比如Activity的onCreate或者Fragment的onViewCreated)创建好ImageWriter,关联一个ImageReader来接收生成的Image:
private lateinit var imageWriter: ImageWriter // 替换成你的USB摄像头实际分辨率 private val cameraWidth = 1920 private val cameraHeight = 1080 private val yuvFormat = ImageFormat.YUV_420_888 fun initImageWriter() { val imageReader = ImageReader.newInstance(cameraWidth, cameraHeight, yuvFormat, 2) imageWriter = ImageWriter.newInstance(imageReader.surface, 2) }
2. 在onFrame回调中填充数据并生成Image
每次收到onFrame回调时,从ImageWriter获取一个可用的Image,把ByteBuffer里的Y、UV数据分别写入对应的平面:
fun onFrame(frame: ByteBuffer) { // 从ImageWriter获取可写入的Image实例 val image = imageWriter.dequeueInputImage() try { val planes = image.planes val yPlane = planes[0] val uPlane = planes[1] val vPlane = planes[2] // NV21格式的字节分布:Y区占width*height字节,后面是交替的V、U字节 val yByteCount = cameraWidth * cameraHeight val uvByteCount = yByteCount / 2 // 写入Y通道数据 frame.position(0) yPlane.buffer.put(frame, yByteCount) // 写入U、V通道数据(NV21是V在前,U在后,交替排列) frame.position(yByteCount) while (frame.hasRemaining()) { // 先写V平面 vPlane.buffer.put(frame.get()) // 再写U平面 uPlane.buffer.put(frame.get()) } // 现在就可以用这个Image创建ML Kit的InputImage了 // 第二个参数是图像旋转角度,根据你的摄像头安装方向调整(比如0、90、180、270) val inputImage = InputImage.fromMediaImage(image, 0) // 这里就可以执行你的ML Kit分析逻辑了 // 例如:faceDetector.process(inputImage)... } finally { // 务必释放Image,避免内存泄漏 imageWriter.queueInputImage(image) } }
方案二:转换为ImageProxy(适配Jetpack CameraX场景)
如果你用的是Jetpack CameraX,那ImageProxy是更常用的载体。其实ImageProxy本质是Image的包装类,咱们可以复用方案一的逻辑,把生成的Image导入到ImageReader,再通过ImageReader的监听器获取ImageProxy:
1. 初始化ImageReader并设置监听器
private lateinit var imageReader: ImageReader fun initImageReader() { imageReader = ImageReader.newInstance(cameraWidth, cameraHeight, yuvFormat, 2) // 设置ImageProxy可用的监听器 imageReader.setOnImageAvailableListener({ reader -> // 获取最新的ImageProxy val imageProxy = reader.acquireLatestImage() imageProxy?.use { // 直接用ImageProxy创建InputImage val inputImage = InputImage.fromImageProxy(it) // 执行ML Kit分析 } }, Handler(Looper.getMainLooper())) }
2. 关联ImageWriter与ImageReader
在初始化ImageWriter的时候,把ImageReader的Surface传进去,这样当你把Image交还给ImageWriter后,ImageReader就能自动拿到对应的ImageProxy:
fun initImageWriter() { imageWriter = ImageWriter.newInstance(imageReader.surface, 2) }
之后的onFrame回调逻辑就和方案一完全一样了,不需要额外修改。
关键注意事项
- YUV格式匹配:如果你的USB摄像头输出的是YV12或者其他YUV格式,需要调整字节拆分的逻辑,比如YV12的UV区是连续的U在前、V在后,和NV21不一样。
- 旋转角度:一定要根据摄像头的实际安装方向设置InputImage的旋转参数,否则ML Kit分析出来的结果会是颠倒的。
- 内存管理:
Image和ImageProxy都是占用Native内存的资源,务必在使用完后及时释放(比如用use语法或者手动调用close/queueInputImage),避免内存泄漏。
备注:内容来源于stack exchange,提问作者Mahmoud Ramzy

