diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 74a5b4b4..d87554b3 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -17,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/target_rectify_service.dart'; // NOUVEAU : redressement import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; @@ -37,8 +36,8 @@ class _CaptureScreenState extends State // NOUVEAU : Service IMU de parallélisme final ParallelismService _parallelismService = ParallelismService( - alignThreshold: 25.0, // Seuil pour passer au vert (permissif) - misalignThreshold: 32.0, // Seuil pour repasser à l'orange + alignThreshold: 15.0, // Seuil pour passer au vert + misalignThreshold: 22.0, // Seuil pour repasser à l'orange ); String? _selectedImagePath; @@ -54,9 +53,8 @@ class _CaptureScreenState extends State late AnimationController _scanAnimationController; late Animation _scanAnimation; - // Détection OpenCV (cible circulaire) — on garde le résultat COMPLET + // Détection OpenCV (inchangée) bool? _alignmentStatus; - TargetDetectionResult? _targetResult; // NOUVEAU : centre + rayon de la cible Timer? _detectionTimer; bool _isAnalyzingFrame = false; @@ -64,9 +62,6 @@ class _CaptureScreenState extends State ParallelismData? _parallelismData; StreamSubscription? _parallelismSubscription; - // Service de redressement de cible (warp perspective) - final TargetRectifyService _rectifyService = TargetRectifyService(); - @override void initState() { super.initState(); @@ -114,43 +109,25 @@ class _CaptureScreenState extends State } // ───────────────────────────────────────────────────────────────────────── - // Calcule la couleur et le message depuis l'IMU + la détection de cible + // NOUVEAU : Calcule la couleur et le message depuis les données IMU // ───────────────────────────────────────────────────────────────────────── - /// La cible circulaire est-elle détectée et raisonnablement cadrée ? - 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; - } - - /// Couleur du cadre : dépend UNIQUEMENT du parallélisme IMU. - /// (La détection de cible sert seulement à dessiner le cercle, elle ne - /// bloque jamais le passage au vert.) + /// Couleur du cadre selon le parallélisme détecté par l'IMU. Color get _frameColor { - final bool imuAligned = - _parallelismData == null || _parallelismData!.isAligned; - return imuAligned ? const Color(0xFF00FF00) : Colors.orange; + if (_parallelismData == null) return const Color(0xFF00FF00); + return _parallelismData!.isAligned ? const Color(0xFF00FF00) : Colors.orange; } - /// Message d'aide selon le parallélisme IMU. + /// Message d'aide selon le parallélisme détecté. String get _alignmentMessage { - if (_parallelismData == null || - _parallelismData!.status == ParallelismStatus.unknown) { + if (_parallelismData == null) { return 'ALIGNEZ LA CIBLE DANS LE CADRE'; } - - // Aligné → message de validation (avec bonus si la cible est détectée) - if (_parallelismData!.isAligned) { - return _targetReady - ? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT' - : 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER'; + if (_parallelismData!.status == ParallelismStatus.aligned) { + return 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER'; } - // Mal aligné → message directif selon l'axe le plus dévié + // Message directif selon l'axe le plus dévié final double pitch = _parallelismData!.pitchDegrees; final double roll = _parallelismData!.rollDegrees; @@ -202,7 +179,6 @@ class _CaptureScreenState extends State _isCameraInitialized = true; _showLiveCamera = true; _alignmentStatus = null; - _targetResult = null; // reset détection cible _parallelismData = null; // NOUVEAU : reset IMU }); @@ -228,9 +204,8 @@ class _CaptureScreenState extends State _cameraController!.startImageStream((CameraImage cameraImage) async { 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!).inMilliseconds < 1000) return; + now.difference(_lastAnalysis!).inSeconds < 3) return; _lastAnalysis = now; _isAnalyzingFrame = true; @@ -254,9 +229,6 @@ class _CaptureScreenState extends State 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 { @@ -286,7 +258,6 @@ class _CaptureScreenState extends State } } catch (_) {} _alignmentStatus = null; - _targetResult = null; } img.Image? _convertCameraImage(CameraImage cameraImage) { @@ -468,21 +439,6 @@ class _CaptureScreenState extends State ), ), - // 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: _TargetCirclePainter( - target: _targetResult!, - color: _targetReady ? frameColor : Colors.white, - highlighted: _targetReady, - ), - ), - ), - ), - // 2. Cadre de visée avec coins et mire centrale Center( child: Container( @@ -753,39 +709,8 @@ class _CaptureScreenState extends State final XFile photo = await _cameraController!.takePicture(); - // NOUVEAU : redressement automatique de la cible (warp perspective). - // On écrit le résultat dans un fichier dédié, à côté de l'original. - String finalPath = photo.path; - try { - final tempDir = await getTemporaryDirectory(); - final rectifiedPath = - '${tempDir.path}/rectified_${DateTime.now().millisecondsSinceEpoch}.jpg'; - - final result = await _rectifyService.rectify( - inputPath: photo.path, - outputPath: rectifiedPath, - ); - - finalPath = result.outputPath; - - if (mounted && result.rectified) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), - content: Text( - 'Cible redressée automatiquement ' - '(inclinaison ${result.estimatedTiltDegrees.toStringAsFixed(1)}° corrigée)', - ), - ), - ); - } - } catch (e) { - debugPrint('Redressement ignoré: $e'); - finalPath = photo.path; // on garde la photo originale en secours - } - setState(() { - _selectedImagePath = finalPath; + _selectedImagePath = photo.path; _showLiveCamera = false; }); @@ -891,69 +816,4 @@ class _CaptureScreenState extends State ), ); } -} - -// ───────────────────────────────────────────────────────────────────────── -// 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 _TargetCirclePainter extends CustomPainter { - final TargetDetectionResult target; - final Color color; - final bool highlighted; - - _TargetCirclePainter({ - required this.target, - required this.color, - required this.highlighted, - }); - - @override - void paint(Canvas canvas, Size size) { - // 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 = highlighted ? 3.5 : 2.0 - ..color = color; - - // Cercle principal autour de la cible - canvas.drawCircle(Offset(cx, cy), r, stroke); - - // 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(); - tp.paint(canvas, Offset(cx - tp.width / 2, (cy - r - 20).clamp(0.0, size.height))); - } - } - - @override - bool shouldRepaint(_TargetCirclePainter old) => - old.target != target || - old.color != color || - old.highlighted != highlighted; } \ No newline at end of file diff --git a/lib/services/image_crop_service.dart b/lib/services/image_crop_service.dart index cd2e34eb..743bb738 100644 --- a/lib/services/image_crop_service.dart +++ b/lib/services/image_crop_service.dart @@ -55,7 +55,37 @@ void _cropIsolateEntry(_CropParams params) { throw Exception('Impossible de décoder l\'image: ${params.sourcePath}'); } + // ── OPTIMISATION MAJEURE : pré-réduction avant rotation ─────────────────── + // La rotation (copyRotate) est l'opération la plus lourde du package `image` + // et son coût est proportionnel au nombre de pixels. Comme la sortie finale + // ne fait que `outputSize` px, on n'a aucun intérêt à faire pivoter une image + // de plusieurs dizaines de mégapixels. + // + // On réduit donc l'image source à une "taille de travail" juste suffisante + // AVANT toute rotation/crop. Le crop garde ~85% du cadre, donc on laisse une + // marge : workMax = outputSize / 0.7 couvre largement le besoin sans perte + // visible. Les coordonnées de crop étant relatives (0..1), elles restent + // valables quelle que soit l'échelle. + final int workMax = (params.outputSize / 0.7).round(); + final int longestSide = math.max(originalImage.width, originalImage.height); + if (longestSide > workMax) { + if (originalImage.width >= originalImage.height) { + originalImage = img.copyResize( + originalImage, + width: workMax, + interpolation: img.Interpolation.linear, + ); + } else { + originalImage = img.copyResize( + originalImage, + height: workMax, + interpolation: img.Interpolation.linear, + ); + } + } + // Rotation si nécessaire — LINEAR au lieu de CUBIC pour la vitesse + // (désormais appliquée sur une image déjà réduite → beaucoup plus rapide) if (params.rotationDegrees != 0.0) { originalImage = img.copyRotate( originalImage,