YOLOv8实例分割模型转TFLite后在Flutter Android部署报错:输出张量形状不匹配
YOLOv8实例分割模型转TFLite后在Flutter Android部署报错:输出张量形状不匹配
兄弟,我一眼就看出问题出在哪了——你代码里对YOLOv8分割模型的TFLite输出格式完全理解错了!
为什么会报形状不匹配的错?
你当前的Flutter代码预设输出是[1,640,640,1]的单通道分割掩码图,但YOLOv8的实例分割模型转TFLite后,根本不会直接输出掩码图!它的输出是三个部分的组合(或被concat成一个张量,取决于导出参数):
- 检测头数据:包含每个检测框的坐标、置信度、类别
- 掩码系数:每个检测框对应的掩码生成系数
- 原型掩码图:固定尺寸的掩码原型,需要和系数计算才能得到最终的目标掩码
你报错里的[1,44,8400],就是模型实际输出的张量形状——44是每个检测结果的参数长度(比如4个框坐标+1个置信度+1个自定义正畸类别+38个掩码系数),8400是YOLOv8默认的候选框数量。
解决步骤一步步来
1. 重新导出正确的TFLite分割模型
你之前导出的时候可能没明确指定分割任务,导致模型输出格式不对。用Ultralytics官方的正确导出方式:
from ultralytics import YOLO # 加载训练好的PyTorch模型 model = YOLO('best.pt') # 导出TFLite,务必指定task为segment,imgsz和Flutter代码里的inputSize保持一致(比如640) model.export( format='tflite', imgsz=640, task='segment', optimize=True # 可选,优化模型大小和推理速度 )
2. 先在Python里确认模型输出结构
导出后,先在Python里跑一下,搞清楚模型到底输出什么:
import tensorflow as tf interpreter = tf.lite.Interpreter(model_path='best.tflite') interpreter.allocate_tensors() # 打印输入输出细节 print("输入形状:", interpreter.get_input_details()[0]['shape']) print("\n输出形状:") for idx, out in enumerate(interpreter.get_output_details()): print(f"输出{idx+1}形状:{out['shape']}")
正常的YOLOv8分割TFLite模型会输出3个张量:
- 输出1:
[1, 8400, 4+1+num_classes]→ 4个框坐标+1个置信度+类别概率 - 输出2:
[1, 8400, mask_dim]→ 每个框对应的掩码系数(比如32维) - 输出3:
[1, mask_dim, 160, 160]→ 固定尺寸的掩码原型图
3. 彻底修改Flutter代码的推理和后处理逻辑
你之前的代码完全是按照“直接输出掩码图”来写的,现在要改成处理YOLOv8的分割输出:
第一步:修正推理函数_runInference
不再预设输出形状,而是根据模型实际输出动态创建张量:
Future<List<Object>> _runInference(List<List<List<double>>> input) async { if (_interpreter == null) throw Exception('Model not loaded'); // 准备输入张量(添加batch维度) var inputTensor = [input]; // 根据模型输出细节创建对应形状的输出容器 List<Object> outputs = []; for (var outDetail in _interpreter!.getOutputDetails()) { List<int> shape = outDetail['shape']; dynamic outputTensor; // 处理不同维度的输出 if (shape.length == 3) { // 检测头/掩码系数:[batch, num_boxes, params] outputTensor = List.generate(shape[0], (_) => List.generate(shape[1], (_) => List.filled(shape[2], 0.0) ) ); } else if (shape.length == 4) { // 原型掩码图:[batch, mask_dim, h, w] outputTensor = List.generate(shape[0], (_) => List.generate(shape[1], (_) => List.generate(shape[2], (_) => List.filled(shape[3], 0.0) ) ) ); } outputs.add(outputTensor); } // 多输入多输出推理 _interpreter!.runForMultipleInputs([inputTensor], outputs); return outputs; }
第二步:重写后处理逻辑_postprocessResults
这部分是核心,需要从模型输出中计算出最终的分割掩码并叠加到原图:
Future<Uint8List> _postprocessResults(Uint8List originalImageBytes, List<Object> modelOutputs) async { img.Image? originalImage = img.decodeImage(originalImageBytes); if (originalImage == null) throw Exception('Failed to decode original image'); // 解析模型输出(根据你Python里看到的输出顺序调整) List<List<List<double>>> detectionOutput = modelOutputs[0] as List<List<List<double>>>; List<List<List<double>>> maskCoeffsOutput = modelOutputs[1] as List<List<List<double>>>; List<List<List<List<double>>>> protoMaskOutput = modelOutputs[2] as List<List<List<List<double>>>>; // 1. 过滤有效检测框(置信度>0.5) List<Map<String, dynamic>> validBoxes = []; for (int i = 0; i < detectionOutput[0].length; i++) { var boxData = detectionOutput[0][i]; double confidence = boxData[4]; if (confidence > 0.5) { // 把YOLO格式的坐标转成原图的x1,y1,x2,y2 double xc = boxData[0] * originalImage.width; double yc = boxData[1] * originalImage.height; double w = boxData[2] * originalImage.width; double h = boxData[3] * originalImage.height; validBoxes.add({ 'x1': xc - w/2, 'y1': yc - h/2, 'x2': xc + w/2, 'y2': yc + h/2, 'mask_coeffs': maskCoeffsOutput[0][i] }); } } // 2. 生成掩码并叠加到原图 img.Image resultImage = img.copyImage(originalImage); int protoH = protoMaskOutput[0][0].length; int protoW = protoMaskOutput[0][0][0].length; for (var box in validBoxes) { List<double> coeffs = box['mask_coeffs']; // 计算当前框的掩码:掩码系数 × 原型掩码 List<List<double>> mask = List.generate(protoH, (y) => List.generate(protoW, (x) { double val = 0.0; for (int i = 0; i < coeffs.length; i++) { val += coeffs[i] * protoMaskOutput[0][i][y][x]; } // Sigmoid激活转成0-1的掩码值 return 1.0 / (1.0 + exp(-val)); }) ); // 3. 把掩码转成图片并resize到原图大小 img.Image maskImage = img.Image(width: protoW, height: protoH); for (int y = 0; y < protoH; y++) { for (int x = 0; x < protoW; x++) { // 置信度大于0.5的区域画红色半透明掩码 int alpha = mask[y][x] > 0.5 ? 128 : 0; maskImage.setPixel(x, y, img.ColorRgba8(255, 0, 0, alpha)); } } img.Image resizedMask = img.copyResize(maskImage, width: originalImage.width, height: originalImage.height); // 4. 叠加掩码到原图 resultImage = img.compositeImage(resultImage, resizedMask); } // 编码为PNG返回 return Uint8List.fromList(img.encodePng(resultImage)); }
第三步:修正analyzeImage里的调用
把原来的_runInference调用改成接收多输出:
final modelOutputs = await _runInference(processedInput); final processedImage = await _postprocessResults(imageBytes, modelOutputs);
4. 额外注意事项
- 确保Flutter代码里的
inputSize和导出TFLite时的imgsz完全一致(比如都是640) - 预处理时的通道顺序:YOLOv8默认是RGB,你代码里的处理是对的,不要改成BGR
- 先在Python里用Ultralytics加载TFLite模型测试,确保模型本身能正确输出分割结果:
from ultralytics import YOLO model = YOLO('best.tflite') results = model('test_image.jpg') results[0].show()
最后再总结下
你之前的错误完全是对YOLOv8分割模型的输出格式理解偏差——它不会直接给你画好的掩码图,而是给你“零件”,需要你自己组装成最终的分割结果。先把模型导对,再把代码的推理和后处理逻辑改成对应YOLOv8的分割输出格式,问题就能解决了!
内容来源于stack exchange




