You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter插件中通过Event Channel向Dart传输Android图像的方案问询

解决方案

Event Channel仅支持传输基础数据类型(如整数、字节数组、字符串等),无法直接传递Image对象。核心解决思路是将图像数据转换为字节数组(效率优先)或Base64字符串,传输到Dart端后再还原为可渲染的图像。以下是两种常用实现方案:


方式一:转换为RGBA字节数组传输(推荐,Dart端处理简单)

将CameraX的ImageAnalysis输出格式改为OUTPUT_IMAGE_FORMAT_RGBA_8888,直接获取RGBA格式的字节数据,Dart端可直接解析为图像。

Android端代码修改

val imageAnalysis = ImageAnalysis.Builder()
    .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) // 指定RGBA输出格式
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy ->
    val mediaImage = imageProxy.image ?: run {
        imageProxy.close()
        return@Analyzer
    }

    // 提取RGBA平面的字节数据
    val plane = mediaImage.planes[0]
    val buffer = plane.buffer
    val bytes = ByteArray(buffer.remaining())
    buffer.get(bytes)

    // 封装图像元数据和字节数据,通过Map传输
    val imageData = mapOf(
        "width" to mediaImage.width,
        "height" to mediaImage.height,
        "bytes" to bytes,
        "rotation" to imageProxy.imageInfo.rotationDegrees
    )

    sink?.success(imageData) // 发送至Dart端
    imageProxy.close() // 必须释放图像资源
})

Dart端处理代码

import 'dart:typed_data';
import 'package:flutter/widgets.dart';

// 监听Event Channel数据流
eventChannel.receiveBroadcastStream().listen((data) {
  if (data is Map) {
    final width = data['width'] as int;
    final height = data['height'] as int;
    final bytes = data['bytes'] as Uint8List;
    final rotation = data['rotation'] as int;

    // 直接将字节数组转为Dart Image组件
    Image rawImage = Image.memory(
      bytes,
      width: width.toDouble(),
      height: height.toDouble(),
      fit: BoxFit.cover,
    );

    // 处理图像旋转(可选)
    final rotatedImage = Transform.rotate(
      angle: rotation * 3.1415926 / 180,
      child: rawImage,
    );

    // 后续可将rotatedImage用于UI渲染
  }
});

方式二:处理默认YUV_420_888格式(无需修改输出格式)

若保持CameraX默认的YUV输出格式,需在Android端将YUV数据转换为RGBA字节数组,再传输到Dart端。

Android端YUV转RGBA工具方法及调用

// YUV_420_888转RGBA字节数组工具方法
fun yuvToRgba(image: Image): ByteArray {
    val yPlane = image.planes[0]
    val uPlane = image.planes[1]
    val vPlane = image.planes[2]

    val yBuffer = yPlane.buffer
    val uBuffer = uPlane.buffer
    val vBuffer = vPlane.buffer

    val yBytes = ByteArray(yBuffer.remaining())
    val uBytes = ByteArray(uBuffer.remaining())
    val vBytes = ByteArray(vBuffer.remaining())

    yBuffer.get(yBytes)
    uBuffer.get(uBytes)
    vBuffer.get(vBytes)

    val rgbaBytes = ByteArray(image.width * image.height * 4)
    var rgbaIndex = 0

    for (y in 0 until image.height) {
        for (x in 0 until image.width) {
            val yIndex = y * yPlane.rowStride + x * yPlane.pixelStride
            val yValue = yBytes[yIndex].toInt() and 0xFF

            val uvRowIndex = (y / 2) * uPlane.rowStride
            val uvColIndex = (x / 2) * uPlane.pixelStride
            val uValue = uBytes[uvRowIndex + uvColIndex].toInt() and 0xFF
            val vValue = vBytes[uvRowIndex + uvColIndex].toInt() and 0xFF

            // YUV转RGB颜色空间公式
            val r = (yValue + 1.370705 * (vValue - 128)).toInt()
            val g = (yValue - 0.698001 * (vValue - 128) - 0.337633 * (uValue - 128)).toInt()
            val b = (yValue + 1.732446 * (uValue - 128)).toInt()

            // 裁剪颜色值到0-255范围
            rgbaBytes[rgbaIndex++] = r.coerceIn(0, 255).toByte()
            rgbaBytes[rgbaIndex++] = g.coerceIn(0, 255).toByte()
            rgbaBytes[rgbaIndex++] = b.coerceIn(0, 255).toByte()
            rgbaBytes[rgbaIndex++] = 255.toByte() // Alpha通道设为不透明
        }
    }
    return rgbaBytes
}

// 在ImageAnalysis Analyzer中调用转换方法
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy ->
    val mediaImage = imageProxy.image ?: run {
        imageProxy.close()
        return@Analyzer
    }

    val rgbaBytes = yuvToRgba(mediaImage)
    val imageData = mapOf(
        "width" to mediaImage.width,
        "height" to mediaImage.height,
        "bytes" to rgbaBytes,
        "rotation" to imageProxy.imageInfo.rotationDegrees
    )

    sink?.success(imageData)
    imageProxy.close()
})

Dart端处理逻辑与方式一完全一致,直接用Image.memory()解析即可。


注意事项

  • 性能优化:实时传输相机画面时,建议设置合适的分辨率(如1280x720),避免过大的字节数组导致UI卡顿。
  • 内存安全:Android端必须调用imageProxy.close(),否则会造成相机资源泄漏。
  • 线程安全:若Event Channel的sink是在主线程创建的,需通过Handler(Looper.getMainLooper()).post包裹sink?.success()操作,确保线程安全。

内容的提问来源于stack exchange,提问作者Bo Hellgren

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 01:09:57