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

Flutter 3.10.5下CameraImage转JPEG失败,求适配安卓/iOS的方案

Flutter 3.10.5 下 NV21 格式 CameraImage 转 JPEG 方案

针对你遇到的旧转换方案失效问题,以下是两种适配当前版本的可行实现:

方案一:纯 Dart 实现(基于 image 库)

步骤1:添加依赖

在 pubspec.yaml 中引入 image 库:

dependencies:
  image: ^4.0.17
  path_provider: ^2.0.15 # 可选,用于保存文件

步骤2:转换代码实现

import 'dart:typed_data';
import 'dart:io';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';

// NV21 转 RGB Image 对象
img.Image convertNV21ToImage(CameraImage image) {
  final int width = image.width;
  final int height = image.height;
  final Uint8List yPlane = image.planes[0].bytes;
  final Uint8List uvPlane = image.planes[1].bytes;

  final img.Image rgbImage = img.Image(width, height);
  int uvIndex = 0;

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      final int yValue = yPlane[y * width + x];
      
      // NV21 为 4:2:0 采样,每4个Y共用一组UV
      if (y % 2 == 0 && x % 2 == 0) {
        final int uValue = uvPlane[uvIndex];
        final int vValue = uvPlane[uvIndex + 1];
        uvIndex += 2;

        // BT.601 标准 YUV 转 RGB 公式
        int r = (yValue + 1.370705 * (vValue - 128)).round();
        int g = (yValue - 0.698001 * (vValue - 128) - 0.337633 * (uValue - 128)).round();
        int b = (yValue + 1.732446 * (uValue - 128)).round();

        // 限制 RGB 值在 0-255 区间
        r = r.clamp(0, 255);
        g = g.clamp(0, 255);
        b = b.clamp(0, 255);

        rgbImage.setPixelRgba(x, y, r, g, b, 255);
      } else {
        // 复用相邻像素的 UV 值
        final int neighborX = x % 2 == 0 ? x : x - 1;
        final int neighborY = y % 2 == 0 ? y : y - 1;
        final int r = rgbImage.getRed(neighborY * width + neighborX);
        final int g = rgbImage.getGreen(neighborY * width + neighborX);
        final int b = rgbImage.getBlue(neighborY * width + neighborX);
        rgbImage.setPixelRgba(x, y, r, g, b, 255);
      }
    }
  }
  return rgbImage;
}

// 处理 CameraImage 并转成 JPEG
void processImage(CameraImage image) async {
  try {
    final img.Image rgbImage = convertNV21ToImage(image);
    final Uint8List jpegBytes = img.encodeJpg(rgbImage, quality: 80);

    // 示例:保存到临时文件
    final Directory tempDir = await getTemporaryDirectory();
    final File file = File('${tempDir.path}/camera_jpeg.jpg');
    await file.writeAsBytes(jpegBytes);
    print("JPEG 文件已保存:${file.path}");
  } catch (e) {
    print("转换失败:$e");
  }
}

方案二:安卓原生实现(性能更优)

适合实时高帧率场景,利用安卓原生 API 直接转换 NV21 为 JPEG。

Flutter 端代码

import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

final MethodChannel _imageConverterChannel = MethodChannel('com.example/camera_converter');

// 调用原生转换
Future<Uint8List?> convertNV21ToJpegNative(Uint8List nv21Bytes, int width, int height) async {
  try {
    return await _imageConverterChannel.invokeMethod('convertNV21ToJpeg', {
      'nv21Bytes': nv21Bytes,
      'width': width,
      'height': height,
      'quality': 80,
    });
  } catch (e) {
    print("原生转换出错:$e");
    return null;
  }
}

void processImage(CameraImage image) async {
  try {
    // 合并 Y 和 UV 平面为完整 NV21 字节数组
    final Uint8List nv21Bytes = Uint8List(image.width * image.height * 3 ~/ 2);
    nv21Bytes.setRange(0, image.planes[0].bytes.length, image.planes[0].bytes);
    nv21Bytes.setRange(image.planes[0].bytes.length, nv21Bytes.length, image.planes[1].bytes);

    final Uint8List? jpegBytes = await convertNV21ToJpegNative(nv21Bytes, image.width, image.height);
    if (jpegBytes != null) {
      final Directory tempDir = await getTemporaryDirectory();
      final File file = File('${tempDir.path}/native_camera_jpeg.jpg');
      await file.writeAsBytes(jpegBytes);
      print("原生转换 JPEG 文件已保存:${file.path}");
    }
  } catch (e) {
    print("处理失败:$e");
  }
}

安卓端代码(MainActivity)

package com.example.your_app_package;

import android.graphics.YuvImage;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import java.io.ByteArrayOutputStream;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "com.example/camera_converter";

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
                .setMethodCallHandler((call, result) -> {
                    if (call.method.equals("convertNV21ToJpeg")) {
                        byte[] nv21Bytes = call.argument("nv21Bytes");
                        int width = call.argument("width");
                        int height = call.argument("height");
                        int quality = call.argument("quality");

                        if (nv21Bytes == null || width <= 0 || height <= 0) {
                            result.error("INVALID_ARGS", "参数无效", null);
                            return;
                        }

                        try {
                            YuvImage yuvImage = new YuvImage(nv21Bytes, ImageFormat.NV21, width, height, null);
                            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                            yuvImage.compressToJpeg(new Rect(0, 0, width, height), quality, outputStream);
                            result.success(outputStream.toByteArray());
                        } catch (Exception e) {
                            result.error("CONVERT_FAILED", "转换失败:" + e.getMessage(), null);
                        }
                    } else {
                        result.notImplemented();
                    }
                });
    }
}

权限配置

在安卓 AndroidManifest.xml 中添加必要权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 07:48:18