| import 'dart:io'; |
| import 'dart:typed_data'; |
| import 'dart:ui' as ui; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_pytorch_lite/flutter_pytorch_lite.dart'; |
|
|
| class PlantAnomalyDetector { |
| Module? _module; |
| static const double _threshold = 0.5687; |
| |
| |
| static const List<double> _mean = [0.4682, 0.4865, 0.3050]; |
| static const List<double> _std = [0.2064, 0.1995, 0.1961]; |
|
|
| |
| Future<void> loadModel() async { |
| try { |
| |
| final filePath = '${Directory.systemTemp.path}/plant_anomaly_detector.ptl'; |
| final modelBytes = await _getBuffer('assets/models/plant_anomaly_detector.ptl'); |
| File(filePath).writeAsBytesSync(modelBytes); |
| |
| _module = await FlutterPytorchLite.load(filePath); |
| print('Model loaded successfully'); |
| } catch (e) { |
| print('Error loading model: $e'); |
| rethrow; |
| } |
| } |
|
|
| |
| static Future<Uint8List> _getBuffer(String assetFileName) async { |
| ByteData rawAssetFile = await rootBundle.load(assetFileName); |
| final rawBytes = rawAssetFile.buffer.asUint8List(); |
| return rawBytes; |
| } |
|
|
| |
| List<double> _normalize(List<double> input) { |
| List<double> normalized = []; |
| int channels = 3; |
| int pixelsPerChannel = input.length ~/ channels; |
| |
| for (int c = 0; c < channels; c++) { |
| for (int i = 0; i < pixelsPerChannel; i++) { |
| int idx = c * pixelsPerChannel + i; |
| double normalizedValue = (input[idx] - _mean[c]) / _std[c]; |
| normalized.add(normalizedValue); |
| } |
| } |
| |
| return normalized; |
| } |
|
|
| |
| double _calculateReconstructionError(List<double> original, List<double> reconstructed) { |
| if (original.length != reconstructed.length) { |
| throw ArgumentError('Original and reconstructed tensors must have same length'); |
| } |
| |
| double sumSquaredError = 0.0; |
| for (int i = 0; i < original.length; i++) { |
| double diff = original[i] - reconstructed[i]; |
| sumSquaredError += diff * diff; |
| } |
| |
| return sumSquaredError / original.length; |
| } |
|
|
| |
| Future<PlantDetectionResult> detectPlant(ui.Image image) async { |
| if (_module == null) { |
| throw StateError('Model not loaded. Call loadModel() first.'); |
| } |
|
|
| try { |
| |
| final inputShape = Int64List.fromList([1, 3, 224, 224]); |
| Tensor inputTensor = await TensorImageUtils.imageToFloat32Tensor( |
| image, |
| width: 224, |
| height: 224, |
| ); |
|
|
| |
| List<double> originalValues = inputTensor.dataAsFloat32List; |
| List<double> normalizedOriginal = _normalize(originalValues); |
|
|
| |
| IValue input = IValue.from(inputTensor); |
| IValue output = await _module!.forward([input]); |
| |
| |
| Tensor reconstructionTensor = output.toTensor(); |
| List<double> reconstruction = reconstructionTensor.dataAsFloat32List; |
|
|
| |
| double reconstructionError = _calculateReconstructionError( |
| normalizedOriginal, |
| reconstruction |
| ); |
|
|
| |
| bool isAnomaly = reconstructionError > _threshold; |
| double confidence = (reconstructionError - _threshold).abs() / _threshold; |
|
|
| return PlantDetectionResult( |
| isPlant: !isAnomaly, |
| reconstructionError: reconstructionError, |
| threshold: _threshold, |
| confidence: confidence, |
| ); |
|
|
| } catch (e) { |
| print('Error during inference: $e'); |
| rethrow; |
| } |
| } |
|
|
| |
| Future<void> dispose() async { |
| if (_module != null) { |
| await _module!.destroy(); |
| _module = null; |
| } |
| } |
| } |
|
|
| |
| class PlantDetectionResult { |
| final bool isPlant; |
| final double reconstructionError; |
| final double threshold; |
| final double confidence; |
|
|
| PlantDetectionResult({ |
| required this.isPlant, |
| required this.reconstructionError, |
| required this.threshold, |
| required this.confidence, |
| }); |
|
|
| @override |
| String toString() { |
| return 'PlantDetectionResult(' |
| 'isPlant: $isPlant, ' |
| 'reconstructionError: ${reconstructionError.toStringAsFixed(4)}, ' |
| 'threshold: ${threshold.toStringAsFixed(4)}, ' |
| 'confidence: ${(confidence * 100).toStringAsFixed(2)}%' |
| ')'; |
| } |
| } |
|
|
| |
| class PlantDetectionWidget extends StatefulWidget { |
| @override |
| _PlantDetectionWidgetState createState() => _PlantDetectionWidgetState(); |
| } |
|
|
| class _PlantDetectionWidgetState extends State<PlantDetectionWidget> { |
| final PlantAnomalyDetector _detector = PlantAnomalyDetector(); |
| bool _isModelLoaded = false; |
|
|
| @override |
| void initState() { |
| super.initState(); |
| _loadModel(); |
| } |
|
|
| Future<void> _loadModel() async { |
| try { |
| await _detector.loadModel(); |
| setState(() { |
| _isModelLoaded = true; |
| }); |
| } catch (e) { |
| print('Failed to load model: $e'); |
| } |
| } |
|
|
| Future<void> _detectFromAsset(String assetPath) async { |
| if (!_isModelLoaded) return; |
|
|
| try { |
| |
| const assetImage = AssetImage('assets/images/test_plant.jpg'); |
| final image = await TensorImageUtils.imageProviderToImage(assetImage); |
| |
| |
| final result = await _detector.detectPlant(image); |
| |
| |
| print('Detection result: $result'); |
| |
| |
| showDialog( |
| context: context, |
| builder: (context) => AlertDialog( |
| title: Text(result.isPlant ? 'Plant Detected' : 'Anomaly Detected'), |
| content: Text( |
| 'Reconstruction Error: ${result.reconstructionError.toStringAsFixed(4)}\n' |
| 'Confidence: ${(result.confidence * 100).toStringAsFixed(2)}%' |
| ), |
| actions: [ |
| TextButton( |
| onPressed: () => Navigator.pop(context), |
| child: Text('OK'), |
| ), |
| ], |
| ), |
| ); |
| |
| } catch (e) { |
| print('Error during detection: $e'); |
| } |
| } |
|
|
| @override |
| void dispose() { |
| _detector.dispose(); |
| super.dispose(); |
| } |
|
|
| @override |
| Widget build(BuildContext context) { |
| return Scaffold( |
| appBar: AppBar(title: Text('Plant Anomaly Detection')), |
| body: Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: [ |
| if (!_isModelLoaded) |
| CircularProgressIndicator() |
| else |
| ElevatedButton( |
| onPressed: () => _detectFromAsset('assets/images/test_plant.jpg'), |
| child: Text('Detect Plant'), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |