You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动