From 723900b8606500d9735d550e71c337616fce3b0a Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 15 Feb 2026 09:49:48 +0100 Subject: [PATCH] Autocalibration de la cible sur le centre de la cible ok + petite correction de la distortion mais pas tres fonctionnel --- lib/features/capture/capture_screen.dart | 20 +- lib/features/crop/widgets/crop_overlay.dart | 51 +++- .../distortion_correction_service.dart | 247 ++++++++++++++++-- pubspec.lock | 64 +---- pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 7 files changed, 288 insertions(+), 102 deletions(-) diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index a5f12fa..0fc969f 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -6,7 +6,7 @@ library; import 'dart:io'; -import 'package:cunning_document_scanner/cunning_document_scanner.dart'; +import 'package:google_mlkit_document_scanner/google_mlkit_document_scanner.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../core/constants/app_constants.dart'; @@ -84,8 +84,8 @@ class _CaptureScreenState extends State { floatingActionButton: _selectedImagePath != null ? FloatingActionButton.extended( onPressed: _analyzeImage, - icon: const Icon(Icons.analytics), - label: const Text('Analyser'), + icon: const Icon(Icons.arrow_forward), + label: const Text('Suivant'), ) : null, ); @@ -205,10 +205,18 @@ class _CaptureScreenState extends State { setState(() => _isLoading = true); try { - final List? pictures = await CunningDocumentScanner.getPictures(); + final options = DocumentScannerOptions( + documentFormat: DocumentFormat.jpeg, + mode: ScannerMode.base, + pageLimit: 1, + isGalleryImport: false, + ); - if (pictures != null && pictures.isNotEmpty) { - setState(() => _selectedImagePath = pictures.first); + final scanner = DocumentScanner(options: options); + final documents = await scanner.scanDocument(); + + if (documents.images.isNotEmpty) { + setState(() => _selectedImagePath = documents.images.first); } } catch (e) { if (mounted) { diff --git a/lib/features/crop/widgets/crop_overlay.dart b/lib/features/crop/widgets/crop_overlay.dart index 075e2e4..2794d09 100644 --- a/lib/features/crop/widgets/crop_overlay.dart +++ b/lib/features/crop/widgets/crop_overlay.dart @@ -13,20 +13,13 @@ class CropOverlay extends StatelessWidget { /// Afficher la grille des tiers final bool showGrid; - const CropOverlay({ - super.key, - required this.cropSize, - this.showGrid = true, - }); + const CropOverlay({super.key, required this.cropSize, this.showGrid = true}); @override Widget build(BuildContext context) { return CustomPaint( size: Size.infinite, - painter: _CropOverlayPainter( - cropSize: cropSize, - showGrid: showGrid, - ), + painter: _CropOverlayPainter(cropSize: cropSize, showGrid: showGrid), ); } } @@ -35,10 +28,7 @@ class _CropOverlayPainter extends CustomPainter { final double cropSize; final bool showGrid; - _CropOverlayPainter({ - required this.cropSize, - required this.showGrid, - }); + _CropOverlayPainter({required this.cropSize, required this.showGrid}); @override void paint(Canvas canvas, Size size) { @@ -77,6 +67,9 @@ class _CropOverlayPainter extends CustomPainter { if (showGrid) { _drawGrid(canvas, cropRect); } + + // Dessiner le point central (croix) + _drawCenterPoint(canvas, cropRect); } void _drawCorners(Canvas canvas, Rect rect) { @@ -171,6 +164,38 @@ class _CropOverlayPainter extends CustomPainter { ); } + void _drawCenterPoint(Canvas canvas, Rect rect) { + final centerPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.8) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + const size = 10.0; + final centerX = rect.center.dx; + final centerY = rect.center.dy; + + // Ligne horizontale + canvas.drawLine( + Offset(centerX - size, centerY), + Offset(centerX + size, centerY), + centerPaint, + ); + + // Ligne verticale + canvas.drawLine( + Offset(centerX, centerY - size), + Offset(centerX, centerY + size), + centerPaint, + ); + + // Petit cercle central pour précision (optionnel, mais aide à viser) + canvas.drawCircle( + rect.center, + 2, + Paint()..color = Colors.red.withValues(alpha: 0.6), + ); + } + @override bool shouldRepaint(covariant _CropOverlayPainter oldDelegate) { return cropSize != oldDelegate.cropSize || showGrid != oldDelegate.showGrid; diff --git a/lib/services/distortion_correction_service.dart b/lib/services/distortion_correction_service.dart index 223360e..6acc10a 100644 --- a/lib/services/distortion_correction_service.dart +++ b/lib/services/distortion_correction_service.dart @@ -8,6 +8,7 @@ library; import 'dart:io'; import 'dart:math' as math; import 'package:image/image.dart' as img; +import 'package:opencv_dart/opencv_dart.dart' as cv; import 'package:path_provider/path_provider.dart'; /// Paramètres de distorsion calculés à partir de la calibration @@ -281,16 +282,56 @@ class DistortionCorrectionService { final p11 = image.getPixel(x1, y1); // Interpoler chaque canal - final r = _lerp2D(p00.r.toDouble(), p10.r.toDouble(), p01.r.toDouble(), p11.r.toDouble(), wx, wy); - final g = _lerp2D(p00.g.toDouble(), p10.g.toDouble(), p01.g.toDouble(), p11.g.toDouble(), wx, wy); - final b = _lerp2D(p00.b.toDouble(), p10.b.toDouble(), p01.b.toDouble(), p11.b.toDouble(), wx, wy); - final a = _lerp2D(p00.a.toDouble(), p10.a.toDouble(), p01.a.toDouble(), p11.a.toDouble(), wx, wy); + final r = _lerp2D( + p00.r.toDouble(), + p10.r.toDouble(), + p01.r.toDouble(), + p11.r.toDouble(), + wx, + wy, + ); + final g = _lerp2D( + p00.g.toDouble(), + p10.g.toDouble(), + p01.g.toDouble(), + p11.g.toDouble(), + wx, + wy, + ); + final b = _lerp2D( + p00.b.toDouble(), + p10.b.toDouble(), + p01.b.toDouble(), + p11.b.toDouble(), + wx, + wy, + ); + final a = _lerp2D( + p00.a.toDouble(), + p10.a.toDouble(), + p01.a.toDouble(), + p11.a.toDouble(), + wx, + wy, + ); - return img.ColorRgba8(r.round().clamp(0, 255), g.round().clamp(0, 255), b.round().clamp(0, 255), a.round().clamp(0, 255)); + return img.ColorRgba8( + r.round().clamp(0, 255), + g.round().clamp(0, 255), + b.round().clamp(0, 255), + a.round().clamp(0, 255), + ); } /// Interpolation linéaire 2D - double _lerp2D(double v00, double v10, double v01, double v11, double wx, double wy) { + double _lerp2D( + double v00, + double v10, + double v01, + double v11, + double wx, + double wy, + ) { final top = v00 * (1 - wx) + v10 * wx; final bottom = v01 * (1 - wx) + v11 * wx; return top * (1 - wy) + bottom * wy; @@ -320,7 +361,9 @@ class DistortionCorrectionService { final height = image.height; // Convertir les coordonnées normalisées en pixels - final srcCorners = corners.map((c) => (x: c.x * width, y: c.y * height)).toList(); + final srcCorners = corners + .map((c) => (x: c.x * width, y: c.y * height)) + .toList(); // Calculer la taille du rectangle destination // On prend la moyenne des largeurs et hauteurs @@ -336,20 +379,21 @@ class DistortionCorrectionService { final result = img.Image(width: dstWidth, height: dstHeight); // Calculer la matrice de transformation perspective - final matrix = _computePerspectiveMatrix( - srcCorners, - [ - (x: 0.0, y: 0.0), - (x: dstWidth.toDouble(), y: 0.0), - (x: dstWidth.toDouble(), y: dstHeight.toDouble()), - (x: 0.0, y: dstHeight.toDouble()), - ], - ); + final matrix = _computePerspectiveMatrix(srcCorners, [ + (x: 0.0, y: 0.0), + (x: dstWidth.toDouble(), y: 0.0), + (x: dstWidth.toDouble(), y: dstHeight.toDouble()), + (x: 0.0, y: dstHeight.toDouble()), + ]); // Appliquer la transformation for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { - final src = _applyPerspectiveTransform(matrix, x.toDouble(), y.toDouble()); + final src = _applyPerspectiveTransform( + matrix, + x.toDouble(), + y.toDouble(), + ); if (src.x >= 0 && src.x < width && src.y >= 0 && src.y < height) { final pixel = _bilinearInterpolate(image, src.x, src.y); @@ -408,8 +452,11 @@ class DistortionCorrectionService { // Le système 'a' est de taille 8x9 (8 équations, 9 inconnues). // On fixe h8 = 1.0 pour résoudre le système, ce qui nous donne un système 8x8. final int n = 8; - final List> matrix = List.generate(n, (i) => List.from(a[i])); - + final List> matrix = List.generate( + n, + (i) => List.from(a[i]), + ); + // Vecteur B (les constantes de l'autre côté de l'égalité) // Dans DLT, -h8 * dx (ou dy) devient le terme constant. final List b = List.generate(n, (i) => -matrix[i][8]); @@ -428,7 +475,7 @@ class DistortionCorrectionService { final List tempRow = matrix[i]; matrix[i] = matrix[pivot]; matrix[pivot] = tempRow; - + final double tempB = b[i]; b[i] = b[pivot]; b[pivot] = tempB; @@ -462,7 +509,11 @@ class DistortionCorrectionService { return h; } - ({double x, double y}) _applyPerspectiveTransform(List h, double x, double y) { + ({double x, double y}) _applyPerspectiveTransform( + List h, + double x, + double y, + ) { final w = h[6] * x + h[7] * y + h[8]; if (w.abs() < 1e-10) { return (x: x, y: y); @@ -471,4 +522,158 @@ class DistortionCorrectionService { final ny = (h[3] * x + h[4] * y + h[5]) / w; return (x: nx, y: ny); } + + /// Corrige la perspective en se basant sur la détection de cercles (ellipses) + /// dans l'image. + /// + /// Cette méthode tente de détecter l'ellipse la plus proéminente (la cible) + /// et calcule une transformation pour la rendre parfaitement circulaire. + Future correctPerspectiveUsingCircles(String imagePath) async { + try { + // 1. Charger l'image avec OpenCV + final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR); + if (src.isEmpty) throw Exception("Impossible de charger l'image"); + + // 2. Prétraitement + final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY); + final blurred = cv.gaussianBlur(gray, (5, 5), 0); + + // Canny edge detector avec seuil adaptatif (Otsu) + final thresh = cv.threshold( + blurred, + 0, + 255, + cv.THRESH_BINARY | cv.THRESH_OTSU, + ); + final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1); + + // 3. Trouver les contours + final contoursResult = cv.findContours( + edges, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE, + ); + final contours = contoursResult.$1; + + if (contours.isEmpty) return imagePath; // Pas de contours trouvés + + // 4. Trouver le meilleur candidat ellipse + cv.RotatedRect? bestEllipse; + double maxArea = 0; + + for (final contour in contours) { + if (contour.length < 5) + continue; // fitEllipse nécessite au moins 5 points + + final area = cv.contourArea(contour); + if (area < 1000) continue; // Ignorer les trop petits bruits + + final ellipse = cv.fitEllipse(contour); + + // Critère de sélection: on cherche la plus grande ellipse qui est proche d'un cercle + // Mais comme on veut corriger la distorsion, elle PEUT être aplatie. + // Donc on prend juste la plus grande ellipse raisonnablement centrée. + if (area > maxArea) { + maxArea = area; + bestEllipse = ellipse; + } + } + + if (bestEllipse == null) return imagePath; + + // 5. Calculer la transformation perspective + // L'idée est de mapper les 4 sommets de l'ellipse détectée vers un cercle parfait. + // Ou plus simplement, mapper le rectangle englobant de l'ellipse vers un carré. + + // Points source: les 4 coins du rotated rect de l'ellipse + // Note: opencv_dart RotatedRect points() non dispo directement? + // On peut utiliser boxPoints(ellipse) + final boxPoints = cv.boxPoints(bestEllipse); + // boxPoints returns Mat (4x2 float32) + + // Extraire les 4 points + final List srcPoints = []; + + for (int i = 0; i < boxPoints.length; i++) { + // On accède directement au point à l'index i + final point2f = boxPoints[i]; + + // On convertit les coordonnées float en int pour cv.Point + srcPoints.add(cv.Point(point2f.x.toInt(), point2f.y.toInt())); + } + + // Trier les points pour avoir: TL, TR, BR, BL + _sortPoints(srcPoints); + + // Dimensions cibles + final side = math + .max(bestEllipse.size.width, bestEllipse.size.height) + .toInt(); + + final List dstPoints = [ + cv.Point(0, 0), + cv.Point(side, 0), + cv.Point(side, side), + cv.Point(0, side), + ]; + + // Matrice de perspective + final M = cv.getPerspectiveTransform( + cv.VecPoint.fromList(srcPoints), + cv.VecPoint.fromList(dstPoints), + ); + + // 6. Warper l'image + final corrected = cv.warpPerspective(src, M, (side, side)); + + // 7. Sauvegarder + final tempDir = await getTemporaryDirectory(); + final timestamp = DateTime.now().millisecondsSinceEpoch; + final outputPath = '${tempDir.path}/corrected_circle_$timestamp.jpg'; + + cv.imwrite(outputPath, corrected); + + return outputPath; + } catch (e) { + // En cas d'erreur, retourner l'image originale + print('Erreur correction perspective cercles: $e'); + return imagePath; + } + } + + /// Trie les points dans l'ordre: Top-Left, Top-Right, Bottom-Right, Bottom-Left + void _sortPoints(List points) { + // Calculer le centre de gravité + double cx = 0; + double cy = 0; + for (final p in points) { + cx += p.x; + cy += p.y; + } + cx /= points.length; + cy /= points.length; + + points.sort((a, b) { + // Trier par angle autour du centre + final angleA = math.atan2(a.y - cy, a.x - cx); + final angleB = math.atan2(b.y - cy, b.x - cx); + return angleA.compareTo(angleB); + }); + + // Re-trier pour être sûr: + points.sort((a, b) => (a.y + a.x).compareTo(b.y + b.x)); + final tl = points[0]; + final br = points[3]; + + // Reste tr et bl + final remaining = [points[1], points[2]]; + remaining.sort((a, b) => a.x.compareTo(b.x)); + final bl = remaining[0]; + final tr = remaining[1]; + + points[0] = tl; + points[1] = tr; + points[2] = br; + points[3] = bl; + } } diff --git a/pubspec.lock b/pubspec.lock index e91cbe6..a76930f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,14 +81,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - cunning_document_scanner: - dependency: "direct main" - description: - name: cunning_document_scanner - sha256: de0c0705799f7d5cc9b82b67bfb8b3e965a1fbff4afbd70ea10cd1dad4f3a98c - url: "https://pub.dev" - source: hosted - version: "1.4.0" cupertino_icons: dependency: "direct main" description: @@ -224,6 +216,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_mlkit_document_scanner: + dependency: "direct main" + description: + name: google_mlkit_document_scanner + sha256: "67428ddb853880c8185049a5834cd328e6420921a74786f6aadee0b76f8536bd" + url: "https://pub.dev" + source: hosted + version: "0.2.1" hooks: dependency: transitive description: @@ -488,54 +488,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - permission_handler: - dependency: transitive - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.dev" - source: hosted - version: "12.0.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" - source: hosted - version: "13.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.dev" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4b88926..e6bafe6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,8 +38,8 @@ dependencies: opencv_dart: ^2.1.0 # Image capture from camera/gallery - image_picker: ^1.0.7 - cunning_document_scanner: ^1.4.0 + image_picker: ^1.2.1 + google_mlkit_document_scanner: ^0.2.0 # Local database for history sqflite: ^2.3.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2c256bd..77ab7a0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,8 @@ #include "generated_plugin_registrant.h" #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - PermissionHandlerWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 230eabf..a423a02 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows - permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST