fix: on revire ml kit on essaie d ameliorer la detection des cercles

This commit is contained in:
qguillaume
2026-06-05 23:05:11 +02:00
parent 9f9f9beb84
commit f0b15941cf
2 changed files with 77 additions and 307 deletions

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'dart:math' as math;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.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/opencv_target_service.dart';
import '../../services/parallelism_service.dart'; // NOUVEAU
import '../../services/object_detection_service.dart'; // NOUVEAU : ML Kit
import '../../services/target_rectify_service.dart'; // NOUVEAU : redressement
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
@@ -56,8 +54,9 @@ class _CaptureScreenState extends State<CaptureScreen>
late AnimationController _scanAnimationController;
late Animation<double> _scanAnimation;
// Détection OpenCV (inchangée)
// Détection OpenCV (cible circulaire) — on garde le résultat COMPLET
bool? _alignmentStatus;
TargetDetectionResult? _targetResult; // NOUVEAU : centre + rayon de la cible
Timer? _detectionTimer;
bool _isAnalyzingFrame = false;
@@ -65,12 +64,7 @@ class _CaptureScreenState extends State<CaptureScreen>
ParallelismData? _parallelismData;
StreamSubscription<ParallelismData>? _parallelismSubscription;
// NOUVEAU : Détection d'objet ML Kit
final ObjectDetectionService _objectService = ObjectDetectionService();
StreamSubscription<List<DetectedObject2D>>? _objectSubscription;
List<DetectedObject2D> _detectedObjects = [];
// NOUVEAU : Service de redressement de cible (warp perspective)
// Service de redressement de cible (warp perspective)
final TargetRectifyService _rectifyService = TargetRectifyService();
@override
@@ -93,8 +87,6 @@ class _CaptureScreenState extends State<CaptureScreen>
_detectionTimer?.cancel();
_parallelismSubscription?.cancel(); // NOUVEAU
_parallelismService.dispose(); // NOUVEAU
_objectSubscription?.cancel(); // NOUVEAU
_objectService.dispose(); // NOUVEAU
super.dispose();
}
@@ -122,52 +114,21 @@ class _CaptureScreenState extends State<CaptureScreen>
}
// ─────────────────────────────────────────────────────────────────────────
// NOUVEAU : Démarre lcoute de la détection d'objet ML Kit
// ─────────────────────────────────────────────────────────────────────────
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
// Calcule la couleur et le message depuis l'IMU + la détection de cible
// ─────────────────────────────────────────────────────────────────────────
/// Y a-t-il un objet détecté suffisamment grand et globalement centré ?
DetectedObject2D? get _primaryObject {
if (_detectedObjects.isEmpty) return null;
// On prend l'objet le plus grand (le sujet principal le plus probable).
final sorted = [..._detectedObjects]
..sort((a, b) => b.area.compareTo(a.area));
final obj = sorted.first;
// Filtre : doit occuper une part raisonnable de l'image et être centré.
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;
/// La cible circulaire est-elle détectée et raisonnablement cade ?
bool get _targetReady {
final t = _targetResult;
if (t == null || !t.success) return false;
final bool centered =
t.centerX > 0.15 && t.centerX < 0.85 && t.centerY > 0.15 && t.centerY < 0.85;
final bool bigEnough = t.radius > 0.12;
return centered && bigEnough;
}
bool get _objectReady => _primaryObject != null;
/// 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.)
Color get _frameColor {
final bool imuAligned =
@@ -182,10 +143,10 @@ class _CaptureScreenState extends State<CaptureScreen>
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) {
return _objectReady
? 'PARFAIT — OBJET CADRÉ, PRÊT'
return _targetReady
? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT'
: 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER';
}
@@ -241,12 +202,12 @@ class _CaptureScreenState extends State<CaptureScreen>
_isCameraInitialized = true;
_showLiveCamera = true;
_alignmentStatus = null;
_targetResult = null; // reset détection cible
_parallelismData = null; // NOUVEAU : reset IMU
});
_startAlignmentDetection();
_startParallelismDetection(); // NOUVEAU
_startObjectDetection(); // NOUVEAU
}
} catch (e) {
debugPrint('Erreur caméra: $e');
@@ -265,18 +226,11 @@ class _CaptureScreenState extends State<CaptureScreen>
DateTime? _lastAnalysis;
_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;
final now = DateTime.now();
// Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
if (_lastAnalysis != null &&
now.difference(_lastAnalysis!).inSeconds < 3) return;
now.difference(_lastAnalysis!).inMilliseconds < 1000) return;
_lastAnalysis = now;
_isAnalyzingFrame = true;
@@ -300,6 +254,9 @@ class _CaptureScreenState extends State<CaptureScreen>
if (mounted && _showLiveCamera) {
setState(() {
// On garde le résultat complet pour dessiner le cercle.
_targetResult = result.success ? result : null;
if (!result.success) {
_alignmentStatus = null;
} else {
@@ -329,6 +286,7 @@ class _CaptureScreenState extends State<CaptureScreen>
}
} catch (_) {}
_alignmentStatus = null;
_targetResult = null;
}
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é
if (_detectedObjects.isNotEmpty)
// 1.bis Cercle dessiné autour de la cible circulaire détectée
if (_targetResult != null && _targetResult!.success)
Center(
child: AspectRatio(
aspectRatio: 3 / 4,
child: CustomPaint(
painter: _ObjectBoxPainter(
objects: _detectedObjects,
primary: _primaryObject,
color: frameColor,
painter: _TargetCirclePainter(
target: _targetResult!,
color: _targetReady ? frameColor : Colors.white,
highlighted: _targetReady,
),
),
),
@@ -584,7 +542,6 @@ class _CaptureScreenState extends State<CaptureScreen>
onPressed: () {
_stopAlignmentDetection();
_stopParallelismDetection(); // NOUVEAU
_stopObjectDetection(); // NOUVEAU
setState(() {
_showLiveCamera = false;
});
@@ -793,7 +750,6 @@ class _CaptureScreenState extends State<CaptureScreen>
_stopAlignmentDetection();
_stopParallelismDetection(); // NOUVEAU
_stopObjectDetection(); // NOUVEAU
final XFile photo = await _cameraController!.takePicture();
@@ -818,7 +774,7 @@ class _CaptureScreenState extends State<CaptureScreen>
duration: const Duration(seconds: 2),
content: Text(
'Cible redressée automatiquement '
'(inclinaison ${result.estimatedTiltDegrees.toStringAsFixed(1)}° corrigée)',
'(inclinaison ${result.estimatedTiltDegrees.toStringAsFixed(1)}° corrigée)',
),
),
);
@@ -938,66 +894,66 @@ class _CaptureScreenState extends State<CaptureScreen>
}
// ─────────────────────────────────────────────────────────────────────────
// Painter qui dessine la boîte englobante de CHAQUE objet détecté.
// L'objet principal (le plus grand et centré) est mis en évidence avec
// la couleur d'état (vert/orange) ; les autres en blanc translucide.
// Painter qui dessine un cercle autour de la cible circulaire détectée.
// Le centre et le rayon viennent d'OpenCV (coordonnées normalisées 0..1).
// Blanc tant que la cible n'est pas bien cadrée, couleur d'état sinon.
// ─────────────────────────────────────────────────────────────────────────
class _ObjectBoxPainter extends CustomPainter {
final List<DetectedObject2D> objects;
final DetectedObject2D? primary;
class _TargetCirclePainter extends CustomPainter {
final TargetDetectionResult target;
final Color color;
final bool highlighted;
_ObjectBoxPainter({
required this.objects,
required this.primary,
_TargetCirclePainter({
required this.target,
required this.color,
required this.highlighted,
});
@override
void paint(Canvas canvas, Size size) {
for (final obj in objects) {
final bool isPrimary = identical(obj, primary);
final Color boxColor =
isPrimary ? color : Colors.white.withValues(alpha: 0.6);
// Centre en pixels dans la zone de dessin
final double cx = target.centerX * size.width;
final double cy = target.centerY * size.height;
// 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()
..style = PaintingStyle.stroke
..strokeWidth = isPrimary ? 3.0 : 1.5
..color = boxColor;
final Paint stroke = Paint()
..style = PaintingStyle.stroke
..strokeWidth = highlighted ? 3.5 : 2.0
..color = color;
final Rect rect = Rect.fromLTRB(
obj.left * size.width,
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);
// Cercle principal autour de la cible
canvas.drawCircle(Offset(cx, cy), r, stroke);
if (obj.label.isNotEmpty) {
final textPainter = TextPainter(
text: TextSpan(
text: ' ${obj.label} '
'${(obj.confidence * 100).toStringAsFixed(0)}% ',
style: TextStyle(
color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.bold,
backgroundColor: boxColor,
),
// Petite croix au centre détecté
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: ' CIBLE ',
style: TextStyle(
color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.bold,
backgroundColor: color,
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(
canvas,
Offset(rect.left, (rect.top - 18).clamp(0.0, size.height)),
);
}
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(canvas, Offset(cx - tp.width / 2, (cy - r - 20).clamp(0.0, size.height)));
}
}
@override
bool shouldRepaint(_ObjectBoxPainter old) =>
old.objects != objects || old.color != color || old.primary != primary;
}
bool shouldRepaint(_TargetCirclePainter old) =>
old.target != target ||
old.color != color ||
old.highlighted != highlighted;
}