fix: lag lors de la prise de photo

This commit is contained in:
qguillaume
2026-06-17 20:00:14 +02:00
parent 05356aaaea
commit 0564bdbb48

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:async'; import 'dart:async';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@@ -20,6 +21,72 @@ import '../../services/parallelism_service.dart'; // NOUVEAU
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';
/// 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 { class CaptureScreen extends StatefulWidget {
const CaptureScreen({super.key}); const CaptureScreen({super.key});
@@ -95,10 +162,23 @@ class _CaptureScreenState extends State<CaptureScreen>
_parallelismService.start(); _parallelismService.start();
_parallelismSubscription = _parallelismService.stream.listen((data) { _parallelismSubscription = _parallelismService.stream.listen((data) {
if (mounted && _showLiveCamera) { if (!mounted || !_showLiveCamera) return;
setState(() {
// 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; _parallelismData = data;
}); if (changed) {
setState(() {});
} }
}); });
} }
@@ -223,30 +303,44 @@ class _CaptureScreenState extends State<CaptureScreen>
_detectionTimer?.cancel(); _detectionTimer?.cancel();
_detectionTimer = null; _detectionTimer = null;
DateTime? _lastAnalysis; DateTime? lastAnalysis;
_cameraController!.startImageStream((CameraImage cameraImage) async { _cameraController!.startImageStream((CameraImage cameraImage) async {
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. // Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
if (_lastAnalysis != null && if (lastAnalysis != null &&
now.difference(_lastAnalysis!).inMilliseconds < 1000) return; now.difference(lastAnalysis!).inMilliseconds < 1000) return;
_lastAnalysis = now; lastAnalysis = now;
_isAnalyzingFrame = true; _isAnalyzingFrame = true;
try { try {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final tempPath = '${tempDir.path}/frame_analysis.jpg'; final tempPath = '${tempDir.path}/frame_analysis.jpg';
final img.Image? converted = _convertCameraImage(cameraImage); // On copie les octets des plans MAINTENANT (le buffer caméra est
if (converted == null) { // 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; _isAnalyzingFrame = false;
return; 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); final result = await _opencvService.detectTarget(tempPath);
try { try {
@@ -279,49 +373,6 @@ class _CaptureScreenState extends State<CaptureScreen>
_targetResult = null; _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) // Galerie (inchangée)
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────