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

Flutter图像分类应用中TensorFlow维度错误排查求助

Flutter图像分类应用中TensorFlow维度错误排查求助

大家好,我在开发一个基于Flutter的垃圾分类应用时,使用自定义分类器模型处理拍摄图像时遇到了维度不匹配的问题,导致分类流程失败。下面是我的扫描页面完整代码,以及目前的问题情况,希望能得到大家的帮助:

扫描页面完整代码

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:waste_pal/widgets/processing-image.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:waste_pal/services/classifier_service.dart';

class ScanScreen extends StatefulWidget {
  @override
  _ScanScreenState createState() => _ScanScreenState();
}

class _ScanScreenState extends State<ScanScreen> with WidgetsBindingObserver {
  CameraController? _controller;
  List<CameraDescription>? cameras;
  bool _isCameraInitialized = false;
  bool _isProcessing = false;
  XFile? _capturedImage;
  ClassificationResult? _classificationResult;
  ClassifierService _classifierService = ClassifierService();
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initializeEverything();
  }

  Future<void> _initializeEverything() async {
    try {
      await _initCamera();
      await _initializeClassifier();
    } catch (e) {
      _showError("Initialization error: $e");
    }
  }

  Future<void> _initCamera() async {
    try {
      cameras = await availableCameras();
      if (cameras == null || cameras!.isEmpty) {
        _showError("No cameras found");
        return;
      }
      _controller = CameraController(cameras![0], ResolutionPreset.medium);
      await _controller!.initialize();
      await _controller!.setFlashMode(FlashMode.off);
      if (!mounted) return;
      setState(() {
        _isCameraInitialized = true;
      });
    } catch (e) {
      _showError("Camera initialization failed: $e");
    }
  }

  Future<void> _initializeClassifier() async {
    try {
      await _classifierService.init();
    } catch (e) {
      _showError("Classifier initialization failed: $e");
    }
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // Handle app lifecycle changes - important for camera resources
    if (_controller == null || !_controller!.value.isInitialized) return;
    if (state == AppLifecycleState.inactive) {
      _controller?.dispose();
    } else if (state == AppLifecycleState.resumed) {
      _initCamera();
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    _classifierService.dispose();
    super.dispose();
  }

  void _captureImage() async {
    if (_controller == null || !_controller!.value.isInitialized) {
      _showError("Camera not ready");
      return;
    }
    setState(() {
      _isProcessing = true;
      _errorMessage = null;
    });
    try {
      _showProcessingModal();
      // Capture image
      final XFile image = await _controller!.takePicture();
      if (!mounted) return;
      Navigator.pop(context); // Close processing modal
      setState(() {
        _capturedImage = image;
      });
      // Classify the captured image
      try {
        final result = await _classifierService.classifyImage(File(_capturedImage!.path));
        if (mounted) {
          setState(() {
            _classificationResult = result;
            _isProcessing = false;
          });
        }
      } catch (e) {
        _showError("Classification failed: $e");
      }
    } catch (e) {
      if (mounted) {
        Navigator.pop(context); // Ensure dialog is closed
        _showError("Failed to capture image: $e");
      }
    }
  }

  void _showError(String message) {
    print("ERROR: $message");
    if (mounted) {
      setState(() {
        _errorMessage = message;
        _isProcessing = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(message),
          backgroundColor: Colors.red,
          duration: Duration(seconds: 3),
        ),
      );
    }
  }

  void _retakePhoto() {
    if (_capturedImage != null) {
      try {
        File(_capturedImage!.path).delete();
      } catch (e) {
        print("Error deleting image: $e");
      }
    }
    setState(() {
      _capturedImage = null;
      _classificationResult = null;
      _errorMessage = null;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        actionsPadding: EdgeInsets.only(right: 16.w),
        automaticallyImplyLeading: false,
        actions: [
          IconButton(
            icon: Icon(Icons.menu, size: 35.sp, color: Color.fromARGB(255, 35, 62, 46)),
            onPressed: () {
              Scaffold.of(context).openEndDrawer();
            },
          ),
        ],
        title: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Image.asset('assets/logo.png', height: 48.h),
            Text(
              'WASTE PAL',
              style: TextStyle(
                  fontSize: 28.sp,
                  fontFamily: 'Montserrat',
                  fontWeight: FontWeight.w900,
                  color: Color.fromARGB(255, 35, 62, 46)),
            ),
          ],
        ),
      ),
      backgroundColor: Colors.white,
      body: _capturedImage == null ? _buildCameraView() : _buildResultView(),
    );
  }

  Widget _buildCameraView() {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(10.r),
          child: Container(
            padding: EdgeInsets.symmetric(vertical: 18.r),
            width: double.infinity,
            decoration: BoxDecoration(
              color: const Color.fromARGB(255, 9, 93, 64),
              borderRadius: BorderRadius.circular(18.r),
            ),
            child: Center(
              child: Text(
                'Take a photo of the item',
                style: TextStyle(
                    fontSize: 18.sp,
                    fontWeight: FontWeight.bold,
                    fontFamily: 'Montserrat',
                    color: Colors.white),
              ),
            ),
          ),
        ),
        SizedBox(height: 10.h),
        // Camera preview or error message
        _isCameraInitialized
            ? Expanded(
                child: CameraPreview(_controller!),
              )
            : Center(
                child: _errorMessage != null
                    ? Text(_errorMessage!, style: TextStyle(color: Colors.red))
                    : CircularProgressIndicator(),
              ),
        SizedBox(height: 20.h),
        if (!_isProcessing)
          ElevatedButton(
            onPressed: _captureImage,
            child: Text('Capture & Classify'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Color.fromARGB(255, 9, 93, 64),
              foregroundColor: Colors.white,
              padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 15.h),
              textStyle: TextStyle(fontSize: 20.sp, fontFamily: 'Montserrat'),
            ),
          ),
        if (_isProcessing)
          CircularProgressIndicator(),
      ],
    );
  }

  Widget _buildResultView() {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(10.r),
          child: Image.file(File(_capturedImage!.path), height: 300.h),
        ),
        SizedBox(height: 20.h),
        if (_classificationResult != null)
          Column(
            children: [
              Text(
                'Classification Result',
                style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 10.h),
              Text(
                'Category: ${_classificationResult!.category}',
                style: TextStyle(fontSize: 20.sp),
              ),
              Text(
                'Confidence: ${(_classificationResult!.confidence * 100).toStringAsFixed(2)}%',
                style: TextStyle(fontSize: 18.sp),
              ),
            ],
          ),
        SizedBox(height: 20.h),
        ElevatedButton(
          onPressed: _retakePhoto,
          child: Text('Retake Photo'),
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.grey,
            foregroundColor: Colors.white,
          ),
        ),
      ],
    );
  }

  void _showProcessingModal() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) {
        return AlertDialog(
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 10.h),
              Text('Processing image...'),
            ],
          ),
        );
      },
    );
  }
}

目前遇到的问题

  1. 相机拍摄功能正常,能成功获取到图像文件,但调用classifyImage时抛出张量维度不匹配的异常,示例错误信息:
    Classification failed: Invalid argument(s): Model expects input shape [1, 224, 224, 3] but received tensor with shape [1, 480, 640, 3]
    
  2. 分类器初始化阶段没有报错,模型文件已正确加载
  3. 已经确认拍摄的图像文件存在且路径正确

我怀疑的排查方向(尚未解决)

  • 图像预处理环节是否没有将拍摄的图像缩放到模型要求的224x224尺寸?
  • ClassifierService中的classifyImage方法是否在转换图像为张量时,没有正确处理维度顺序(比如NHWC和NCHW的区别)?
  • 模型的输入通道数是否与拍摄图像的RGB三通道不匹配?

有没有朋友遇到过类似的问题,或者能帮我检查代码中可能存在的问题?非常感谢!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:53:12