fix: legere baisse resolution pour augmenter fluidité de l appli
This commit is contained in:
@@ -17,7 +17,6 @@ import 'widgets/image_source_button.dart';
|
|||||||
import '../../services/image_crop_service.dart';
|
import '../../services/image_crop_service.dart';
|
||||||
import '../../services/opencv_target_service.dart';
|
import '../../services/opencv_target_service.dart';
|
||||||
import '../../services/parallelism_service.dart'; // NOUVEAU
|
import '../../services/parallelism_service.dart'; // NOUVEAU
|
||||||
import '../../services/target_rectify_service.dart'; // NOUVEAU : redressement
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -37,8 +36,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
|
|
||||||
// NOUVEAU : Service IMU de parallélisme
|
// NOUVEAU : Service IMU de parallélisme
|
||||||
final ParallelismService _parallelismService = ParallelismService(
|
final ParallelismService _parallelismService = ParallelismService(
|
||||||
alignThreshold: 25.0, // Seuil pour passer au vert (permissif)
|
alignThreshold: 15.0, // Seuil pour passer au vert
|
||||||
misalignThreshold: 32.0, // Seuil pour repasser à l'orange
|
misalignThreshold: 22.0, // Seuil pour repasser à l'orange
|
||||||
);
|
);
|
||||||
|
|
||||||
String? _selectedImagePath;
|
String? _selectedImagePath;
|
||||||
@@ -54,9 +53,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
late AnimationController _scanAnimationController;
|
late AnimationController _scanAnimationController;
|
||||||
late Animation<double> _scanAnimation;
|
late Animation<double> _scanAnimation;
|
||||||
|
|
||||||
// Détection OpenCV (cible circulaire) — on garde le résultat COMPLET
|
// Détection OpenCV (inchangée)
|
||||||
bool? _alignmentStatus;
|
bool? _alignmentStatus;
|
||||||
TargetDetectionResult? _targetResult; // NOUVEAU : centre + rayon de la cible
|
|
||||||
Timer? _detectionTimer;
|
Timer? _detectionTimer;
|
||||||
bool _isAnalyzingFrame = false;
|
bool _isAnalyzingFrame = false;
|
||||||
|
|
||||||
@@ -64,9 +62,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
ParallelismData? _parallelismData;
|
ParallelismData? _parallelismData;
|
||||||
StreamSubscription<ParallelismData>? _parallelismSubscription;
|
StreamSubscription<ParallelismData>? _parallelismSubscription;
|
||||||
|
|
||||||
// Service de redressement de cible (warp perspective)
|
|
||||||
final TargetRectifyService _rectifyService = TargetRectifyService();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -114,43 +109,25 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// 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 ?
|
/// Couleur du cadre selon le parallélisme détecté par l'IMU.
|
||||||
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.)
|
|
||||||
Color get _frameColor {
|
Color get _frameColor {
|
||||||
final bool imuAligned =
|
if (_parallelismData == null) return const Color(0xFF00FF00);
|
||||||
_parallelismData == null || _parallelismData!.isAligned;
|
return _parallelismData!.isAligned ? const Color(0xFF00FF00) : Colors.orange;
|
||||||
return imuAligned ? 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 {
|
String get _alignmentMessage {
|
||||||
if (_parallelismData == null ||
|
if (_parallelismData == null) {
|
||||||
_parallelismData!.status == ParallelismStatus.unknown) {
|
|
||||||
return 'ALIGNEZ LA CIBLE DANS LE CADRE';
|
return 'ALIGNEZ LA CIBLE DANS LE CADRE';
|
||||||
}
|
}
|
||||||
|
if (_parallelismData!.status == ParallelismStatus.aligned) {
|
||||||
// Aligné → message de validation (avec bonus si la cible est détectée)
|
return 'PARALLÈLE OK — PRÊT À PHOTOGRAPHIER';
|
||||||
if (_parallelismData!.isAligned) {
|
|
||||||
return _targetReady
|
|
||||||
? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT'
|
|
||||||
: '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 pitch = _parallelismData!.pitchDegrees;
|
||||||
final double roll = _parallelismData!.rollDegrees;
|
final double roll = _parallelismData!.rollDegrees;
|
||||||
|
|
||||||
@@ -202,7 +179,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
_isCameraInitialized = true;
|
_isCameraInitialized = true;
|
||||||
_showLiveCamera = true;
|
_showLiveCamera = true;
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
_targetResult = null; // reset détection cible
|
|
||||||
_parallelismData = null; // NOUVEAU : reset IMU
|
_parallelismData = null; // NOUVEAU : reset IMU
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,9 +204,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
_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.
|
|
||||||
if (_lastAnalysis != null &&
|
if (_lastAnalysis != null &&
|
||||||
now.difference(_lastAnalysis!).inMilliseconds < 1000) return;
|
now.difference(_lastAnalysis!).inSeconds < 3) return;
|
||||||
_lastAnalysis = now;
|
_lastAnalysis = now;
|
||||||
_isAnalyzingFrame = true;
|
_isAnalyzingFrame = true;
|
||||||
|
|
||||||
@@ -254,9 +229,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
|
|
||||||
if (mounted && _showLiveCamera) {
|
if (mounted && _showLiveCamera) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// On garde le résultat complet pour dessiner le cercle.
|
|
||||||
_targetResult = result.success ? result : null;
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -286,7 +258,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_alignmentStatus = null;
|
_alignmentStatus = null;
|
||||||
_targetResult = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img.Image? _convertCameraImage(CameraImage cameraImage) {
|
img.Image? _convertCameraImage(CameraImage cameraImage) {
|
||||||
@@ -468,21 +439,6 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 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
|
// 2. Cadre de visée avec coins et mire centrale
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -753,39 +709,8 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
|
|
||||||
final XFile photo = await _cameraController!.takePicture();
|
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(() {
|
setState(() {
|
||||||
_selectedImagePath = finalPath;
|
_selectedImagePath = photo.path;
|
||||||
_showLiveCamera = false;
|
_showLiveCamera = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -891,69 +816,4 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,37 @@ void _cropIsolateEntry(_CropParams params) {
|
|||||||
throw Exception('Impossible de décoder l\'image: ${params.sourcePath}');
|
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
|
// 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) {
|
if (params.rotationDegrees != 0.0) {
|
||||||
originalImage = img.copyRotate(
|
originalImage = img.copyRotate(
|
||||||
originalImage,
|
originalImage,
|
||||||
|
|||||||
Reference in New Issue
Block a user