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

Flutter集成YOLOv8模型报错:TensorFlow Lite张量字节不匹配求助

问题解决:Flutter集成YOLOv8时输入张量尺寸不匹配错误

错误根源

你编写了预处理函数但完全未调用,且预处理函数本身逻辑错误,导致模型收到的输入张量尺寸与要求的float32[1,3,640,640]完全不符。错误提示中的23040字节是原图的原始字节大小,而模型需要的是4915200字节(136406404,float32每个元素占4字节)的张量。

预处理函数的核心错误

  1. 使用img.encodePng()获取的是PNG压缩后的字节流,并非原始RGB像素值,导致后续处理完全偏离预期。
  2. 最后将float32张量转回Uint8List,等于作废了归一化和维度调整操作,模型需要的是float32格式输入。
  3. 维度扩展逻辑错误:[1.0] + inputTensor完全不符合[1,3,640,640]的张量形状要求。

修正方案

1. 正确实现预处理函数

修改preprocessImage,直接处理原始像素,生成符合模型要求的float32张量:

import 'package:image/image.dart' as img;

Future<Float32List> preprocessImage(File imageFile) async {
  final imageBytes = await imageFile.readAsBytes();
  final decodedImage = img.decodeImage(imageBytes);
  if (decodedImage == null) throw Exception("无法解码图片");

  // 将图片调整为640x640(YOLOv8默认输入尺寸)
  final resizedImage = img.copyResize(decodedImage, width: 640, height: 640);

  // 提取RGB像素(跳过ARGB格式中的Alpha通道)
  final pixels = resizedImage.getBytes();
  final List<double> normalizedPixels = [];

  for (int i = 0; i < pixels.length; i += 4) {
    normalizedPixels.add(pixels[i] / 255.0);     // R通道归一化到[0,1]
    normalizedPixels.add(pixels[i + 1] / 255.0); // G通道归一化到[0,1]
    normalizedPixels.add(pixels[i + 2] / 255.0); // B通道归一化到[0,1]
  }

  // 转换为CHW格式(通道前置)的float32张量:[1,3,640,640]
  final Float32List inputTensor = Float32List(1 * 3 * 640 * 640);
  int index = 0;

  // 填充R通道
  for (int i = 0; i < normalizedPixels.length; i += 3) {
    inputTensor[index++] = normalizedPixels[i];
  }
  // 填充G通道
  for (int i = 1; i < normalizedPixels.length; i += 3) {
    inputTensor[index++] = normalizedPixels[i];
  }
  // 填充B通道
  for (int i = 2; i < normalizedPixels.length; i += 3) {
    inputTensor[index++] = normalizedPixels[i];
  }

  return inputTensor;
}

2. 修改检测逻辑,直接调用TFLite解释器

由于fluttervision默认处理uint8格式的原始图片,若你的模型要求float32输入,建议直接使用tflite_flutter包运行模型:

步骤1:添加依赖

在pubspec.yaml中添加:

dependencies:
  tflite_flutter: ^0.10.1

步骤2:修改模型加载与检测代码

import 'package:tflite_flutter/tflite_flutter.dart';

class _YoloImageV8State extends State<YoloImageV8> {
  late Interpreter interpreter;
  late List<String> labels;
  // ...其他变量保持不变

  @override
  void initState() {
    super.initState();
    loadYoloModel().then((_) {
      setState(() {
        yoloResults = [];
        isLoaded = true;
      });
    });
  }

  Future<void> loadYoloModel() async {
    // 加载TFLite模型
    interpreter = await Interpreter.fromAsset('assets/task1.tflite');
    // 加载标签文件
    labels = await rootBundle.loadString('assets/task1.txt').then((content) => content.split('\n'));
  }

  yoloOnImage() async {
    yoloResults.clear();
    if (imageFile == null) return;

    // 预处理得到输入张量
    final inputTensor = await preprocessImage(imageFile!);
    // 获取模型输出形状并准备输出张量
    final outputShape = interpreter.getOutputTensor(0).shape;
    final outputTensor = Float32List(outputShape.reduce((a, b) => a * b));

    // 运行模型推理
    interpreter.run(inputTensor, outputTensor);

    // 解析YOLOv8输出结果
    yoloResults = parseYoloOutput(outputTensor);
    // 应用非极大值抑制(NMS)去除重复检测框
    yoloResults = applyNMS(yoloResults);

    setState(() {});
  }

  // 解析YOLOv8输出
  List<Map<String, dynamic>> parseYoloOutput(Float32List output) {
    List<Map<String, dynamic>> results = [];
    final numClasses = labels.length;
    final numBoxes = 8400; // YOLOv8默认输出8400个候选框

    for (int i = 0; i < numBoxes; i++) {
      final baseIdx = i * (numClasses + 4);
      // 提取框坐标(x,y,w,h)
      final x = output[baseIdx];
      final y = output[baseIdx + 1];
      final w = output[baseIdx + 2];
      final h = output[baseIdx + 3];

      // 找到置信度最高的类别
      double maxConf = 0;
      int maxClassIdx = 0;
      for (int j = 0; j < numClasses; j++) {
        final conf = output[baseIdx + 4 + j];
        if (conf > maxConf) {
          maxConf = conf;
          maxClassIdx = j;
        }
      }

      // 过滤低置信度结果
      if (maxConf < 0.4) continue;

      // 将坐标转换为原始图片尺寸
      final scaleX = imageWidth / 640;
      final scaleY = imageHeight / 640;
      final left = (x - w / 2) * scaleX;
      final top = (y - h / 2) * scaleY;
      final right = (x + w / 2) * scaleX;
      final bottom = (y + h / 2) * scaleY;

      results.add({
        "tag": labels[maxClassIdx],
        "box": [left, top, right, bottom, maxConf]
      });
    }
    return results;
  }

  // 非极大值抑制(NMS)
  List<Map<String, dynamic>> applyNMS(List<Map<String, dynamic>> boxes, {double iouThreshold = 0.8}) {
    // 按置信度降序排序
    boxes.sort((a, b) => b["box"][4].compareTo(a["box"][4]));
    final List<Map<String, dynamic>> keptBoxes = [];

    while (boxes.isNotEmpty) {
      final currentBox = boxes.removeAt(0);
      keptBoxes.add(currentBox);

      // 移除与当前框IOU超过阈值的重复框
      boxes.removeWhere((box) {
        final iou = calculateIOU(currentBox["box"], box["box"]);
        return iou > iouThreshold;
      });
    }
    return keptBoxes;
  }

  // 计算IOU(交并比)
  double calculateIOU(List<double> boxA, List<double> boxB) {
    final x1 = boxA[0] > boxB[0] ? boxA[0] : boxB[0];
    final y1 = boxA[1] > boxB[1] ? boxA[1] : boxB[1];
    final x2 = boxA[2] < boxB[2] ? boxA[2] : boxB[2];
    final y2 = boxA[3] < boxB[3] ? boxA[3] : boxB[3];

    final intersectArea = (x2 - x1).clamp(0, double.infinity) * (y2 - y1).clamp(0, double.infinity);
    final areaA = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1]);
    final areaB = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1]);

    return intersectArea / (areaA + areaB - intersectArea);
  }

  // ...其他原有代码保持不变
}

关键注意事项

  1. 通道顺序:YOLOv8要求输入为CHW格式(通道前置),需将普通图片的HWC格式转换为CHW。
  2. 归一化:float32模型需将像素值归一化到[0,1]范围,若为uint8量化模型则归一化到[0,255]。
  3. 坐标缩放:检测结果需从640x640缩放回原始图片尺寸,保证框的位置准确。
  4. NMS处理:必须运行非极大值抑制,去除重复的检测框。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 22:47:33