fix: on revire ml kit on essaie d ameliorer la detection des cercles
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
@@ -18,7 +17,6 @@ import 'widgets/image_source_button.dart';
|
|||||||
import '../../services/image_crop_service.dart';
|
import '../../services/image_crop_service.dart';
|
||||||
import '../../services/opencv_target_service.dart';
|
import '../../services/opencv_target_service.dart';
|
||||||
import '../../services/parallelism_service.dart'; // NOUVEAU
|
import '../../services/parallelism_service.dart'; // NOUVEAU
|
||||||
import '../../services/object_detection_service.dart'; // NOUVEAU : ML Kit
|
|
||||||
import '../../services/target_rectify_service.dart'; // NOUVEAU : redressement
|
import '../../services/target_rectify_service.dart'; // NOUVEAU : redressement
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -56,8 +54,9 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
late AnimationController _scanAnimationController;
|
late AnimationController _scanAnimationController;
|
||||||
late Animation<double> _scanAnimation;
|
late Animation<double> _scanAnimation;
|
||||||
|
|
||||||
// Détection OpenCV (inchangée)
|
// Détection OpenCV (cible circulaire) — on garde le résultat COMPLET
|
||||||
bool? _alignmentStatus;
|
bool? _alignmentStatus;
|
||||||
|
TargetDetectionResult? _targetResult; // NOUVEAU : centre + rayon de la cible
|
||||||
Timer? _detectionTimer;
|
Timer? _detectionTimer;
|
||||||
bool _isAnalyzingFrame = false;
|
bool _isAnalyzingFrame = false;
|
||||||
|
|
||||||
@@ -65,12 +64,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
ParallelismData? _parallelismData;
|
ParallelismData? _parallelismData;
|
||||||
StreamSubscription<ParallelismData>? _parallelismSubscription;
|
StreamSubscription<ParallelismData>? _parallelismSubscription;
|
||||||
|
|
||||||
// NOUVEAU : Détection d'objet ML Kit
|
// Service de redressement de cible (warp perspective)
|
||||||
final ObjectDetectionService _objectService = ObjectDetectionService();
|
|
||||||
StreamSubscription<List<DetectedObject2D>>? _objectSubscription;
|
|
||||||
List<DetectedObject2D> _detectedObjects = [];
|
|
||||||
|
|
||||||
// NOUVEAU : Service de redressement de cible (warp perspective)
|
|
||||||
final TargetRectifyService _rectifyService = TargetRectifyService();
|
final TargetRectifyService _rectifyService = TargetRectifyService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -93,8 +87,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
_detectionTimer?.cancel();
|
_detectionTimer?.cancel();
|
||||||
_parallelismSubscription?.cancel(); // NOUVEAU
|
_parallelismSubscription?.cancel(); // NOUVEAU
|
||||||
_parallelismService.dispose(); // NOUVEAU
|
_parallelismService.dispose(); // NOUVEAU
|
||||||
_objectSubscription?.cancel(); // NOUVEAU
|
|
||||||
_objectService.dispose(); // NOUVEAU
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,52 +114,21 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// NOUVEAU : Démarre l'écoute de la détection d'objet ML Kit
|
// Calcule la couleur et le message depuis l'IMU + la détection de cible
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
void _startObjectDetection() {
|
|
||||||
_objectSubscription?.cancel();
|
|
||||||
_objectService.start();
|
|
||||||
|
|
||||||
_objectSubscription = _objectService.stream.listen((objects) {
|
|
||||||
if (mounted && _showLiveCamera) {
|
|
||||||
setState(() {
|
|
||||||
_detectedObjects = objects;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopObjectDetection() {
|
|
||||||
_objectSubscription?.cancel();
|
|
||||||
_objectSubscription = null;
|
|
||||||
_objectService.stop();
|
|
||||||
_detectedObjects = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// NOUVEAU : Calcule la couleur et le message depuis les données IMU
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Y a-t-il un objet détecté suffisamment grand et globalement centré ?
|
/// La cible circulaire est-elle détectée et raisonnablement cadrée ?
|
||||||
DetectedObject2D? get _primaryObject {
|
bool get _targetReady {
|
||||||
if (_detectedObjects.isEmpty) return null;
|
final t = _targetResult;
|
||||||
// On prend l'objet le plus grand (le sujet principal le plus probable).
|
if (t == null || !t.success) return false;
|
||||||
final sorted = [..._detectedObjects]
|
final bool centered =
|
||||||
..sort((a, b) => b.area.compareTo(a.area));
|
t.centerX > 0.15 && t.centerX < 0.85 && t.centerY > 0.15 && t.centerY < 0.85;
|
||||||
final obj = sorted.first;
|
final bool bigEnough = t.radius > 0.12;
|
||||||
// Filtre : doit occuper une part raisonnable de l'image et être centré.
|
return centered && bigEnough;
|
||||||
final bool bigEnough = obj.area > 0.04; // ~20% × 20%
|
|
||||||
final bool centered = obj.centerX > 0.2 &&
|
|
||||||
obj.centerX < 0.8 &&
|
|
||||||
obj.centerY > 0.2 &&
|
|
||||||
obj.centerY < 0.8;
|
|
||||||
return (bigEnough && centered) ? obj : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _objectReady => _primaryObject != null;
|
|
||||||
|
|
||||||
/// Couleur du cadre : dépend UNIQUEMENT du parallélisme IMU.
|
/// Couleur du cadre : dépend UNIQUEMENT du parallélisme IMU.
|
||||||
/// (La détection d'objet sert seulement à dessiner une boîte, elle ne
|
/// (La détection de cible sert seulement à dessiner le cercle, elle ne
|
||||||
/// bloque jamais le passage au vert.)
|
/// bloque jamais le passage au vert.)
|
||||||
Color get _frameColor {
|
Color get _frameColor {
|
||||||
final bool imuAligned =
|
final bool imuAligned =
|
||||||
@@ -182,10 +143,10 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
return 'ALIGNEZ LA CIBLE DANS LE CADRE';
|
return 'ALIGNEZ LA CIBLE DANS LE CADRE';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aligné → message de validation (avec bonus si un objet est cadré)
|
// Aligné → message de validation (avec bonus si la cible est détectée)
|
||||||
if (_parallelismData!.isAligned) {
|
if (_parallelismData!.isAligned) {
|
||||||
return _objectReady
|
return _targetReady
|
||||||
? 'PARFAIT — OBJET CADRÉ, PRÊT'
|
? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT'
|
||||||
: 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER';
|
: 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,12 +202,12 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
_isCameraInitialized = true;
|
_isCameraInitialized = true;
|
||||||
_showLiveCamera = true;
|
_showLiveCamera = true;
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
|
_targetResult = null; // reset détection cible
|
||||||
_parallelismData = null; // NOUVEAU : reset IMU
|
_parallelismData = null; // NOUVEAU : reset IMU
|
||||||
});
|
});
|
||||||
|
|
||||||
_startAlignmentDetection();
|
_startAlignmentDetection();
|
||||||
_startParallelismDetection(); // NOUVEAU
|
_startParallelismDetection(); // NOUVEAU
|
||||||
_startObjectDetection(); // NOUVEAU
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Erreur caméra: $e');
|
debugPrint('Erreur caméra: $e');
|
||||||
@@ -265,18 +226,11 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
DateTime? _lastAnalysis;
|
DateTime? _lastAnalysis;
|
||||||
|
|
||||||
_cameraController!.startImageStream((CameraImage cameraImage) async {
|
_cameraController!.startImageStream((CameraImage cameraImage) async {
|
||||||
// NOUVEAU : on alimente ML Kit à chaque frame (le service gère son propre
|
|
||||||
// throttle interne via _isBusy, donc pas de saturation).
|
|
||||||
_objectService.processCameraImage(
|
|
||||||
cameraImage,
|
|
||||||
_cameras![0],
|
|
||||||
DeviceOrientation.portraitUp,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_isAnalyzingFrame) return;
|
if (_isAnalyzingFrame) return;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
// Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
|
||||||
if (_lastAnalysis != null &&
|
if (_lastAnalysis != null &&
|
||||||
now.difference(_lastAnalysis!).inSeconds < 3) return;
|
now.difference(_lastAnalysis!).inMilliseconds < 1000) return;
|
||||||
_lastAnalysis = now;
|
_lastAnalysis = now;
|
||||||
_isAnalyzingFrame = true;
|
_isAnalyzingFrame = true;
|
||||||
|
|
||||||
@@ -300,6 +254,9 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
|
|
||||||
if (mounted && _showLiveCamera) {
|
if (mounted && _showLiveCamera) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
// On garde le résultat complet pour dessiner le cercle.
|
||||||
|
_targetResult = result.success ? result : null;
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -329,6 +286,7 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
|
_targetResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.Image? _convertCameraImage(CameraImage cameraImage) {
|
img.Image? _convertCameraImage(CameraImage cameraImage) {
|
||||||
@@ -510,16 +468,16 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// NOUVEAU : 1.bis Cadre dessiné autour de CHAQUE objet détecté
|
// 1.bis Cercle dessiné autour de la cible circulaire détectée
|
||||||
if (_detectedObjects.isNotEmpty)
|
if (_targetResult != null && _targetResult!.success)
|
||||||
Center(
|
Center(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 3 / 4,
|
aspectRatio: 3 / 4,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _ObjectBoxPainter(
|
painter: _TargetCirclePainter(
|
||||||
objects: _detectedObjects,
|
target: _targetResult!,
|
||||||
primary: _primaryObject,
|
color: _targetReady ? frameColor : Colors.white,
|
||||||
color: frameColor,
|
highlighted: _targetReady,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -584,7 +542,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
_stopAlignmentDetection();
|
_stopAlignmentDetection();
|
||||||
_stopParallelismDetection(); // NOUVEAU
|
_stopParallelismDetection(); // NOUVEAU
|
||||||
_stopObjectDetection(); // NOUVEAU
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_showLiveCamera = false;
|
_showLiveCamera = false;
|
||||||
});
|
});
|
||||||
@@ -793,7 +750,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
|
|
||||||
_stopAlignmentDetection();
|
_stopAlignmentDetection();
|
||||||
_stopParallelismDetection(); // NOUVEAU
|
_stopParallelismDetection(); // NOUVEAU
|
||||||
_stopObjectDetection(); // NOUVEAU
|
|
||||||
|
|
||||||
final XFile photo = await _cameraController!.takePicture();
|
final XFile photo = await _cameraController!.takePicture();
|
||||||
|
|
||||||
@@ -938,66 +894,66 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Painter qui dessine la boîte englobante de CHAQUE objet détecté.
|
// Painter qui dessine un cercle autour de la cible circulaire détectée.
|
||||||
// L'objet principal (le plus grand et centré) est mis en évidence avec
|
// Le centre et le rayon viennent d'OpenCV (coordonnées normalisées 0..1).
|
||||||
// la couleur d'état (vert/orange) ; les autres en blanc translucide.
|
// Blanc tant que la cible n'est pas bien cadrée, couleur d'état sinon.
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
class _ObjectBoxPainter extends CustomPainter {
|
class _TargetCirclePainter extends CustomPainter {
|
||||||
final List<DetectedObject2D> objects;
|
final TargetDetectionResult target;
|
||||||
final DetectedObject2D? primary;
|
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final bool highlighted;
|
||||||
|
|
||||||
_ObjectBoxPainter({
|
_TargetCirclePainter({
|
||||||
required this.objects,
|
required this.target,
|
||||||
required this.primary,
|
|
||||||
required this.color,
|
required this.color,
|
||||||
|
required this.highlighted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
for (final obj in objects) {
|
// Centre en pixels dans la zone de dessin
|
||||||
final bool isPrimary = identical(obj, primary);
|
final double cx = target.centerX * size.width;
|
||||||
final Color boxColor =
|
final double cy = target.centerY * size.height;
|
||||||
isPrimary ? color : Colors.white.withValues(alpha: 0.6);
|
// radius est normalisé par min(largeur, hauteur) côté OpenCV
|
||||||
|
final double r = target.radius * math.min(size.width, size.height);
|
||||||
|
|
||||||
final Paint stroke = Paint()
|
final Paint stroke = Paint()
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = isPrimary ? 3.0 : 1.5
|
..strokeWidth = highlighted ? 3.5 : 2.0
|
||||||
..color = boxColor;
|
..color = color;
|
||||||
|
|
||||||
final Rect rect = Rect.fromLTRB(
|
// Cercle principal autour de la cible
|
||||||
obj.left * size.width,
|
canvas.drawCircle(Offset(cx, cy), r, stroke);
|
||||||
obj.top * size.height,
|
|
||||||
obj.right * size.width,
|
|
||||||
obj.bottom * size.height,
|
|
||||||
);
|
|
||||||
final RRect rrect =
|
|
||||||
RRect.fromRectAndRadius(rect, const Radius.circular(8));
|
|
||||||
canvas.drawRRect(rrect, stroke);
|
|
||||||
|
|
||||||
if (obj.label.isNotEmpty) {
|
// Petite croix au centre détecté
|
||||||
final textPainter = TextPainter(
|
final Paint cross = Paint()
|
||||||
|
..strokeWidth = 2.0
|
||||||
|
..color = color;
|
||||||
|
const double k = 10;
|
||||||
|
canvas.drawLine(Offset(cx - k, cy), Offset(cx + k, cy), cross);
|
||||||
|
canvas.drawLine(Offset(cx, cy - k), Offset(cx, cy + k), cross);
|
||||||
|
|
||||||
|
// Étiquette "CIBLE" quand elle est bien cadrée
|
||||||
|
if (highlighted) {
|
||||||
|
final tp = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: ' ${obj.label} '
|
text: ' CIBLE ',
|
||||||
'${(obj.confidence * 100).toStringAsFixed(0)}% ',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
backgroundColor: boxColor,
|
backgroundColor: color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
)..layout();
|
)..layout();
|
||||||
textPainter.paint(
|
tp.paint(canvas, Offset(cx - tp.width / 2, (cy - r - 20).clamp(0.0, size.height)));
|
||||||
canvas,
|
|
||||||
Offset(rect.left, (rect.top - 18).clamp(0.0, size.height)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(_ObjectBoxPainter old) =>
|
bool shouldRepaint(_TargetCirclePainter old) =>
|
||||||
old.objects != objects || old.color != color || old.primary != primary;
|
old.target != target ||
|
||||||
|
old.color != color ||
|
||||||
|
old.highlighted != highlighted;
|
||||||
}
|
}
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user