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: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(() {
|
|
||||||
_parallelismData = data;
|
// 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?.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)
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user