import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:camera/camera.dart'; import 'package:google_mlkit_object_detection/google_mlkit_object_detection.dart'; /// Un objet détecté, exprimé en coordonnées NORMALISÉES (0..1) par rapport /// à l'image source. Indépendant de la résolution réelle de la caméra. class DetectedObject2D { /// Boîte englobante normalisée (left, top, right, bottom dans [0..1]). final double left; final double top; final double right; final double bottom; /// Label le plus probable (ex: "Fruit", "Plant"...) — peut être vide. final String label; /// Confiance du label [0..1]. final double confidence; const DetectedObject2D({ required this.left, required this.top, required this.right, required this.bottom, required this.label, required this.confidence, }); double get centerX => (left + right) / 2; double get centerY => (top + bottom) / 2; double get width => right - left; double get height => bottom - top; double get area => width * height; } /// Service de détection d'objets en temps réel basé sur Google ML Kit. /// /// Traite directement les frames `CameraImage` du flux caméra (pas de /// passage par un fichier JPEG, donc beaucoup plus rapide qu'OpenCV ici). /// /// Usage : /// final svc = ObjectDetectionService(); /// svc.start(); /// cameraController.startImageStream((frame) => /// svc.processCameraImage(frame, cameraDescription, rotation)); /// svc.stream.listen((objects) { ... }); class ObjectDetectionService { ObjectDetector? _detector; final StreamController> _controller = StreamController>.broadcast(); bool _isBusy = false; bool _started = false; Stream> get stream => _controller.stream; void start() { if (_started) return; _started = true; // Mode STREAM : optimisé pour le flux vidéo temps réel. // classifyObjects: true => label grossier + confiance par objet. // multipleObjects: true => on détecte et encadre TOUS les objets visibles. final options = ObjectDetectorOptions( mode: DetectionMode.stream, classifyObjects: true, multipleObjects: true, ); _detector = ObjectDetector(options: options); } /// À appeler depuis `startImageStream`. Future processCameraImage( CameraImage image, CameraDescription camera, DeviceOrientation deviceOrientation, ) async { if (!_started || _detector == null) return; if (_isBusy) return; // On saute la frame si la précédente n'est pas finie _isBusy = true; try { final inputImage = _toInputImage(image, camera, deviceOrientation); if (inputImage == null) { _isBusy = false; return; } final objects = await _detector!.processImage(inputImage); final int imgW = image.width; final int imgH = image.height; final List results = objects.map((o) { final rect = o.boundingBox; String label = ''; double conf = 0; if (o.labels.isNotEmpty) { final best = o.labels.reduce( (a, b) => a.confidence >= b.confidence ? a : b, ); label = best.text; conf = best.confidence; } return DetectedObject2D( left: (rect.left / imgW).clamp(0.0, 1.0), top: (rect.top / imgH).clamp(0.0, 1.0), right: (rect.right / imgW).clamp(0.0, 1.0), bottom: (rect.bottom / imgH).clamp(0.0, 1.0), label: label, confidence: conf, ); }).toList(); if (!_controller.isClosed) _controller.add(results); } catch (e) { debugPrint('ObjectDetection erreur: $e'); } finally { _isBusy = false; } } /// Convertit une CameraImage en InputImage ML Kit. InputImage? _toInputImage( CameraImage image, CameraDescription camera, DeviceOrientation deviceOrientation, ) { // Rotation : combine l'orientation du capteur et celle de l'appareil. final sensorOrientation = camera.sensorOrientation; InputImageRotation? rotation; if (defaultTargetPlatform == TargetPlatform.iOS) { rotation = InputImageRotationValue.fromRawValue(sensorOrientation); } else { // Android : table d'orientation var rotationCompensation = _orientations[deviceOrientation] ?? 0; if (camera.lensDirection == CameraLensDirection.front) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } else { rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } rotation = InputImageRotationValue.fromRawValue(rotationCompensation); } if (rotation == null) return null; final format = InputImageFormatValue.fromRawValue(image.format.raw); if (format == null) return null; // ML Kit attend un seul plan contigu (NV21 sur Android, BGRA sur iOS). if (image.planes.isEmpty) return null; final plane = image.planes.first; return InputImage.fromBytes( bytes: plane.bytes, metadata: InputImageMetadata( size: Size(image.width.toDouble(), image.height.toDouble()), rotation: rotation, format: format, bytesPerRow: plane.bytesPerRow, ), ); } static const Map _orientations = { DeviceOrientation.portraitUp: 0, DeviceOrientation.landscapeLeft: 90, DeviceOrientation.portraitDown: 180, DeviceOrientation.landscapeRight: 270, }; void stop() { _started = false; } void dispose() { stop(); _detector?.close(); _detector = null; _controller.close(); } }