From 0564bdbb484f4c7332721769fcfa00bfd274bebc Mon Sep 17 00:00:00 2001 From: qguillaume Date: Wed, 17 Jun 2026 20:00:14 +0200 Subject: [PATCH] fix: lag lors de la prise de photo --- lib/features/capture/capture_screen.dart | 163 +++++++++++++++-------- 1 file changed, 107 insertions(+), 56 deletions(-) diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index f885d72b..409012d7 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math' as math; import 'dart:async'; import 'dart:isolate'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -20,6 +21,72 @@ import '../../services/parallelism_service.dart'; // NOUVEAU import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; +/// Paramètres sérialisables pour convertir une frame caméra dans un Isolate. +class _FrameConvertParams { + final int width; + final int height; + final Uint8List yBytes; + final Uint8List uBytes; + final Uint8List vBytes; + final int yRowStride; + final int uvRowStride; + final int uvPixelStride; + final String outputPath; + + _FrameConvertParams({ + required this.width, + required this.height, + required this.yBytes, + required this.uBytes, + required this.vBytes, + required this.yRowStride, + required this.uvRowStride, + required this.uvPixelStride, + required this.outputPath, + }); +} + +/// Conversion YUV420 → RGB (sous-échantillonnée /2) + encodage JPEG, exécutée +/// dans un Isolate pour ne JAMAIS bloquer le thread UI pendant l'aperçu caméra +/// (c'était la cause principale du lag à la prise de photo). Retourne true si +/// le fichier d'analyse a bien été écrit. +bool _convertAndEncodeFrameIsolate(_FrameConvertParams p) { + try { + final int targetW = p.width ~/ 2; + final int targetH = p.height ~/ 2; + final image = img.Image(width: targetW, height: targetH); + + for (int y = 0; y < targetH; y++) { + for (int x = 0; x < targetW; x++) { + final int srcX = x * 2; + final int srcY = y * 2; + + final int yIndex = srcY * p.yRowStride + srcX; + final int uvIndex = + (srcY ~/ 2) * p.uvRowStride + (srcX ~/ 2) * p.uvPixelStride; + + final int yVal = p.yBytes[yIndex]; + final int uVal = p.uBytes[uvIndex] - 128; + final int vVal = p.vBytes[uvIndex] - 128; + + final int r = (yVal + 1.402 * vVal).clamp(0, 255).toInt(); + final int g = (yVal - 0.344136 * uVal - 0.714136 * vVal) + .clamp(0, 255) + .toInt(); + final int b = (yVal + 1.772 * uVal).clamp(0, 255).toInt(); + + image.setPixelRgb(x, y, r, g, b); + } + } + + // PERF : qualité 50 pour la frame d'analyse temps réel (jetable). + File(p.outputPath).writeAsBytesSync(img.encodeJpg(image, quality: 50)); + return true; + } catch (_) { + return false; + } +} + class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @@ -95,10 +162,23 @@ class _CaptureScreenState extends State _parallelismService.start(); _parallelismSubscription = _parallelismService.stream.listen((data) { - if (mounted && _showLiveCamera) { - setState(() { - _parallelismData = data; - }); + if (!mounted || !_showLiveCamera) return; + + // Le capteur émet à ~20 Hz, mais l'affichage ne dépend que du statut + // (couleur verte/orange) et des angles arrondis à 0,1° (message). On ne + // reconstruit donc QUE quand l'un d'eux change réellement → bien moins de + // rebuilds inutiles de la vue caméra. + final prev = _parallelismData; + final bool changed = prev == null || + prev.status != data.status || + prev.pitchDegrees.toStringAsFixed(1) != + data.pitchDegrees.toStringAsFixed(1) || + prev.rollDegrees.toStringAsFixed(1) != + data.rollDegrees.toStringAsFixed(1); + + _parallelismData = data; + if (changed) { + setState(() {}); } }); } @@ -223,30 +303,44 @@ class _CaptureScreenState extends State _detectionTimer?.cancel(); _detectionTimer = null; - DateTime? _lastAnalysis; + DateTime? lastAnalysis; _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; - _lastAnalysis = now; + if (lastAnalysis != null && + now.difference(lastAnalysis!).inMilliseconds < 1000) return; + lastAnalysis = now; _isAnalyzingFrame = true; try { final tempDir = await getTemporaryDirectory(); final tempPath = '${tempDir.path}/frame_analysis.jpg'; - final img.Image? converted = _convertCameraImage(cameraImage); - if (converted == null) { + // On copie les octets des plans MAINTENANT (le buffer caméra est + // recyclé dès la sortie du callback), puis on délègue TOUTE la + // conversion YUV→RGB + l'encodage JPEG à un Isolate → l'aperçu et le + // déclencheur restent fluides (plus de gel du thread UI). + final params = _FrameConvertParams( + width: cameraImage.width, + height: cameraImage.height, + yBytes: Uint8List.fromList(cameraImage.planes[0].bytes), + uBytes: Uint8List.fromList(cameraImage.planes[1].bytes), + vBytes: Uint8List.fromList(cameraImage.planes[2].bytes), + yRowStride: cameraImage.planes[0].bytesPerRow, + uvRowStride: cameraImage.planes[1].bytesPerRow, + uvPixelStride: cameraImage.planes[1].bytesPerPixel ?? 1, + outputPath: tempPath, + ); + + final bool ok = + await Isolate.run(() => _convertAndEncodeFrameIsolate(params)); + if (!ok) { _isAnalyzingFrame = false; return; } - // PERF : qualité 50 pour la frame d'analyse temps réel (jetable) - await File(tempPath).writeAsBytes(img.encodeJpg(converted, quality: 50)); - final result = await _opencvService.detectTarget(tempPath); try { @@ -279,49 +373,6 @@ class _CaptureScreenState extends State _targetResult = null; } - img.Image? _convertCameraImage(CameraImage cameraImage) { - try { - final int width = cameraImage.width; - final int height = cameraImage.height; - final int targetW = width ~/ 2; - final int targetH = height ~/ 2; - - final yPlane = cameraImage.planes[0]; - final uPlane = cameraImage.planes[1]; - final vPlane = cameraImage.planes[2]; - - final image = img.Image(width: targetW, height: targetH); - - for (int y = 0; y < targetH; y++) { - for (int x = 0; x < targetW; x++) { - final int srcX = x * 2; - final int srcY = y * 2; - - final int yIndex = srcY * yPlane.bytesPerRow + srcX; - final int uvIndex = (srcY ~/ 2) * uPlane.bytesPerRow + - (srcX ~/ 2) * uPlane.bytesPerPixel!; - - final int yVal = yPlane.bytes[yIndex]; - final int uVal = uPlane.bytes[uvIndex] - 128; - final int vVal = vPlane.bytes[uvIndex] - 128; - - final int r = (yVal + 1.402 * vVal).clamp(0, 255).toInt(); - final int g = (yVal - 0.344136 * uVal - 0.714136 * vVal) - .clamp(0, 255) - .toInt(); - final int b = (yVal + 1.772 * uVal).clamp(0, 255).toInt(); - - image.setPixelRgb(x, y, r, g, b); - } - } - - return image; - } catch (e) { - debugPrint('Erreur conversion frame: $e'); - return null; - } - } - // ───────────────────────────────────────────────────────────────────────── // Galerie (inchangée) // ─────────────────────────────────────────────────────────────────────────