fix: centrage de la cible image pleine ecran respectant la prise origine au niveau des coins verts/orange

This commit is contained in:
qguillaume
2026-06-17 19:42:13 +02:00
parent c4880ccb68
commit 05356aaaea
2 changed files with 55 additions and 49 deletions

View File

@@ -884,16 +884,23 @@ class _CaptureScreenState extends State<CaptureScreen>
} }
} }
/// Réduit fortement la résolution de la photo pour accélérer tous les /// Recadre la photo sur la zone du viseur PUIS réduit sa résolution, en un
/// écrans suivants (crop, calibration, plotting). 1080 px de côté max /// seul décodage dans un Isolate (thread UI fluide).
/// + JPEG qualité 70 : largement suffisant pour la détection visuelle.
/// ///
/// Baisser `maxSide` (ex. 900) ou `quality` (ex. 60) rend le chargement /// 1) Recadrage viseur : carré centré à 85 % du petit côté de la photo. C'est
/// encore plus rapide au prix d'un peu de finesse à l'écran. /// EXACTEMENT le cadre des 4 coins verts/orange affiché à la capture
/// (`SizedBox` de `0.85 * largeur écran`, centré, l'aperçu caméra remplissant
/// la largeur). → l'écran de centrage ne reçoit QUE ce qui était visé entre
/// les coins, sans le fond capturé autour : plus d'impression de « dézoom ».
/// 2) Dégradation résolution : 1080 px de côté max + JPEG qualité 70, largement
/// suffisant pour la détection visuelle et bien plus rapide à recharger.
///
/// Baisser `maxSide` (ex. 900) ou `quality` (ex. 60) accélère encore au prix
/// d'un peu de finesse à l'écran.
Future<String> _downscaleForPipeline(String sourcePath) async { Future<String> _downscaleForPipeline(String sourcePath) async {
// On résout le chemin de sortie sur le thread UI (path_provider a besoin // On résout le chemin de sortie sur le thread UI (path_provider a besoin
// du canal de plateforme), puis tout le travail lourd (décodage, resize, // du canal de plateforme), puis tout le travail lourd (décodage, crop,
// réencodage JPEG) part dans un Isolate → aucun gel du thread UI. // resize, réencodage JPEG) part dans un Isolate → aucun gel du thread UI.
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final outPath = final outPath =
'${tempDir.path}/downscaled_${DateTime.now().millisecondsSinceEpoch}.jpg'; '${tempDir.path}/downscaled_${DateTime.now().millisecondsSinceEpoch}.jpg';
@@ -903,21 +910,34 @@ class _CaptureScreenState extends State<CaptureScreen>
final decoded = img.decodeImage(bytes); final decoded = img.decodeImage(bytes);
if (decoded == null) return sourcePath; if (decoded == null) return sourcePath;
const int maxSide = 1080; // 1) Recadrage sur la zone du viseur (carré centré à 85 % du petit côté).
final int longest = math.max(decoded.width, decoded.height); final int side = (math.min(decoded.width, decoded.height) * 0.85).round();
if (longest <= maxSide) { final int cropX =
return sourcePath; // déjà assez petite, rien à faire ((decoded.width - side) / 2).round().clamp(0, decoded.width - 1);
} final int cropY =
((decoded.height - side) / 2).round().clamp(0, decoded.height - 1);
final double ratio = maxSide / longest; img.Image result = img.copyCrop(
final resized = img.copyResize(
decoded, decoded,
width: (decoded.width * ratio).round(), x: cropX,
height: (decoded.height * ratio).round(), y: cropY,
interpolation: img.Interpolation.linear, // rapide width: math.min(side, decoded.width - cropX),
height: math.min(side, decoded.height - cropY),
); );
File(outPath).writeAsBytesSync(img.encodeJpg(resized, quality: 70)); // 2) Dégradation résolution si nécessaire.
const int maxSide = 1080;
final int longest = math.max(result.width, result.height);
if (longest > maxSide) {
final double ratio = maxSide / longest;
result = img.copyResize(
result,
width: (result.width * ratio).round(),
height: (result.height * ratio).round(),
interpolation: img.Interpolation.linear, // rapide
);
}
File(outPath).writeAsBytesSync(img.encodeJpg(result, quality: 70));
return outPath; return outPath;
}); });
} }

View File

@@ -101,17 +101,16 @@ class _CropScreenState extends State<CropScreen> {
? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8))) ? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8)))
: Column( : Column(
children: [ children: [
// Zone interactive de crop. Le cadre épouse le ratio réel de la photo // Zone interactive de crop, CARRÉE. La photo la remplit entièrement
// (AspectRatio) → la photo le remplit bord à bord, sans bande noire de // (BoxFit.cover) → aucun bord noir dans la zone. Le débord hors cadre
// letterbox. La fenêtre de visée carrée reste centrée à l'intérieur. // est récupérable en déplaçant/zoomant. La sortie d'analyse reste
// carrée, donc la cible n'est pas déformée.
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Center( child: Center(
child: AspectRatio( child: AspectRatio(
aspectRatio: (_imageSize != null && _imageSize!.height > 0) aspectRatio: 1.0,
? _imageSize!.width / _imageSize!.height
: 1.0,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -249,21 +248,9 @@ class _CropScreenState extends State<CropScreen> {
builder: (context, constraints) { builder: (context, constraints) {
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight); _viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
// FIX : On calcule d'abord la taille de la photo affichée avant de définir la taille du cadre vert ! // La photo remplit toute la zone carrée (BoxFit.cover). La fenêtre de
final imageAspect = _imageSize != null ? _imageSize!.width / _imageSize!.height : 1.0; // visée = toute la zone visible → aucun bord noir autour du cadre.
final viewportAspect = _viewportSize.width / _viewportSize.height; _cropSize = math.min(_viewportSize.width, _viewportSize.height);
double displayWidth, displayHeight;
if (imageAspect > viewportAspect) {
displayWidth = _viewportSize.width;
displayHeight = _viewportSize.width / imageAspect;
} else {
displayHeight = _viewportSize.height;
displayWidth = _viewportSize.height * imageAspect;
}
// On passe de 0.95 à 0.85 pour matcher parfaitement avec l'appareil photo !
_cropSize = math.min(displayWidth, displayHeight)* 0.85;
if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) { if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) {
_initializeImagePosition(); _initializeImagePosition();
@@ -285,7 +272,7 @@ class _CropScreenState extends State<CropScreen> {
alignment: Alignment.center, alignment: Alignment.center,
child: Image.file( child: Image.file(
File(widget.imagePath), File(widget.imagePath),
fit: BoxFit.contain, fit: BoxFit.cover,
width: _viewportSize.width, width: _viewportSize.width,
height: _viewportSize.height, height: _viewportSize.height,
), ),
@@ -385,14 +372,13 @@ class _CropScreenState extends State<CropScreen> {
Future<void> _onCropConfirm() async { Future<void> _onCropConfirm() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
// Facteur d'échelle affichage/source (BoxFit.contain) — identique à // Facteur d'échelle affichage/source (BoxFit.cover) — identique à celui
// celui utilisé pour afficher l'image dans l'aperçu. // utilisé pour afficher l'image dans l'aperçu : l'image remplit la zone,
final imageAspect = _imageSize!.width / _imageSize!.height; // le débord est rogné, donc l'échelle est le MAX des deux ratios d'axe.
final viewportAspect = _viewportSize.width / _viewportSize.height; final double displayPerSourcePx = math.max(
final double displayWidth = imageAspect > viewportAspect _viewportSize.width / _imageSize!.width,
? _viewportSize.width _viewportSize.height / _imageSize!.height,
: _viewportSize.height * imageAspect; );
final double displayPerSourcePx = displayWidth / _imageSize!.width;
// Découpe calée sur la fenêtre de visée : le DÉPLACEMENT (pan) et la // Découpe calée sur la fenêtre de visée : le DÉPLACEMENT (pan) et la
// ROTATION sont pris en compte, le ZOOM est ignoré, et les débordements // ROTATION sont pris en compte, le ZOOM est ignoré, et les débordements