fix: lag lors de la prise de photo
This commit is contained in:
@@ -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<CaptureScreen>
|
||||
_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<CaptureScreen>
|
||||
_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<CaptureScreen>
|
||||
_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)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user