187 lines
5.7 KiB
Dart
187 lines
5.7 KiB
Dart
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<List<DetectedObject2D>> _controller =
|
|
StreamController<List<DetectedObject2D>>.broadcast();
|
|
|
|
bool _isBusy = false;
|
|
bool _started = false;
|
|
|
|
Stream<List<DetectedObject2D>> 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<void> 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<DetectedObject2D> 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<DeviceOrientation, int> _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();
|
|
}
|
|
}
|