Flutter集成YOLOv8模型报错:TensorFlow Lite张量字节不匹配求助
问题解决:Flutter集成YOLOv8时输入张量尺寸不匹配错误
错误根源
你编写了预处理函数但完全未调用,且预处理函数本身逻辑错误,导致模型收到的输入张量尺寸与要求的float32[1,3,640,640]完全不符。错误提示中的23040字节是原图的原始字节大小,而模型需要的是4915200字节(136406404,float32每个元素占4字节)的张量。
预处理函数的核心错误
- 使用
img.encodePng()获取的是PNG压缩后的字节流,并非原始RGB像素值,导致后续处理完全偏离预期。 - 最后将float32张量转回Uint8List,等于作废了归一化和维度调整操作,模型需要的是float32格式输入。
- 维度扩展逻辑错误:
[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); } // ...其他原有代码保持不变 }
关键注意事项
- 通道顺序:YOLOv8要求输入为CHW格式(通道前置),需将普通图片的HWC格式转换为CHW。
- 归一化:float32模型需将像素值归一化到[0,1]范围,若为uint8量化模型则归一化到[0,255]。
- 坐标缩放:检测结果需从640x640缩放回原始图片尺寸,保证框的位置准确。
- NMS处理:必须运行非极大值抑制,去除重复的检测框。
内容的提问来源于stack exchange,提问作者Suryanshu
相关产品推荐
相关产品推荐

