diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index 92270c79..fd021bcb 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -1,8 +1,8 @@ /// Gestionnaire d'état pour l'analyse des cibles (ChangeNotifier). /// -/// Gère le workflow complet d'analyse : chargement d'image, détection de cible, -/// gestion des impacts (manuels et automatiques), calcul des scores, -/// analyse de groupement et sauvegarde des sessions. +/// Gère le workflow complet d'analyse : chargement d'image, gestion des +/// impacts placés manuellement, calcul des scores, analyse de groupement +/// et sauvegarde des sessions. library; import 'dart:io'; @@ -13,37 +13,25 @@ import '../../data/models/target_analysis.dart'; import '../../data/models/shot.dart'; import '../../data/models/target_type.dart'; import '../../data/repositories/session_repository.dart'; -import '../../services/target_detection_service.dart'; import '../../services/score_calculator_service.dart'; import '../../services/grouping_analyzer_service.dart'; -import '../../services/distortion_correction_service.dart'; -import '../../services/opencv_target_service.dart'; import '../../services/ai_export_service.dart'; enum AnalysisState { initial, loading, success, error } class AnalysisProvider extends ChangeNotifier { - final TargetDetectionService _detectionService; final ScoreCalculatorService _scoreCalculatorService; final GroupingAnalyzerService _groupingAnalyzerService; final SessionRepository _sessionRepository; - final DistortionCorrectionService _distortionService; - final OpenCVTargetService _opencvTargetService; final Uuid _uuid = const Uuid(); AnalysisProvider({ - required TargetDetectionService detectionService, required ScoreCalculatorService scoreCalculatorService, required GroupingAnalyzerService groupingAnalyzerService, required SessionRepository sessionRepository, - DistortionCorrectionService? distortionService, - OpenCVTargetService? opencvTargetService, - }) : _detectionService = detectionService, - _scoreCalculatorService = scoreCalculatorService, + }) : _scoreCalculatorService = scoreCalculatorService, _groupingAnalyzerService = groupingAnalyzerService, - _sessionRepository = sessionRepository, - _distortionService = distortionService ?? DistortionCorrectionService(), - _opencvTargetService = opencvTargetService ?? OpenCVTargetService(); + _sessionRepository = sessionRepository; AnalysisState _state = AnalysisState.initial; String? _errorMessage; @@ -53,7 +41,7 @@ class AnalysisProvider extends ChangeNotifier { // AJOUT PROTECTION DU PLOTTING : Stockage permanent de la rotation du Crop double _cropRotation = 0.0; - // Target detection results + // Target calibration double _targetCenterX = 0.5; double _targetCenterY = 0.5; double _targetRadius = 0.4; @@ -71,15 +59,6 @@ class AnalysisProvider extends ChangeNotifier { // Grouping results GroupingResult? _groupingResult; - // Reference-based detection - List _referenceImpacts = []; - ImpactCharacteristics? _learnedCharacteristics; - - // Distortion correction - bool _distortionCorrectionEnabled = false; - DistortionParameters? _distortionParams; - String? _correctedImagePath; - // Getters AnalysisState get state => _state; String? get errorMessage => _errorMessage; @@ -100,21 +79,6 @@ class AnalysisProvider extends ChangeNotifier { int get totalScore => _scoreResult?.totalScore ?? 0; int get shotCount => _shots.length; - List get referenceImpacts => List.unmodifiable(_referenceImpacts); - ImpactCharacteristics? get learnedCharacteristics => _learnedCharacteristics; - bool get hasLearnedCharacteristics => _learnedCharacteristics != null; - - // Distortion correction getters - bool get distortionCorrectionEnabled => _distortionCorrectionEnabled; - DistortionParameters? get distortionParams => _distortionParams; - String? get correctedImagePath => _correctedImagePath; - bool get hasDistortion => _distortionParams?.needsCorrection ?? false; - - /// Retourne le chemin de l'image à afficher (corrigée si activée, originale sinon) - String? get displayImagePath => - _distortionCorrectionEnabled && _correctedImagePath != null - ? _correctedImagePath - : _imagePath; /// Modifie et mémorise la rotation de l'image pour le Plotting void setCropRotation(double rotation) { @@ -122,16 +86,13 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } - /// Analyze an image - /// - /// [autoAnalyze] determines if we should run automatic detection immediately. - /// If false, only the image is loaded and default target parameters are set. + /// Charge l'image et initialise les paramètres de cible par défaut. + /// Le placement des impacts et la calibration se font ensuite manuellement. Future analyzeImage( - String imagePath, - TargetType targetType, { - bool autoAnalyze = true, - Offset? manualCenter, - }) async { + String imagePath, + TargetType targetType, { + Offset? manualCenter, + }) async { _state = AnalysisState.loading; _imagePath = imagePath; _targetType = targetType; @@ -147,54 +108,12 @@ class AnalysisProvider extends ChangeNotifier { _imageAspectRatio = frame.image.width / frame.image.height; frame.image.dispose(); - if (!autoAnalyze) { - // Just setup default values without running detection - _targetCenterX = manualCenter?.dx ?? 0.5; - _targetCenterY = manualCenter?.dy ?? 0.5; - _targetRadius = 0.4; - _targetInnerRadius = 0.04; + _targetCenterX = manualCenter?.dx ?? 0.5; + _targetCenterY = manualCenter?.dy ?? 0.5; + _targetRadius = 0.4; + _targetInnerRadius = 0.04; - // Initialize empty shots list - _shots = []; - - _state = AnalysisState.success; - notifyListeners(); - return; - } - - final result = await _detectionService.detectTargetAsync( - imagePath, - targetType, - ); - - if (!result.success) { - _state = AnalysisState.error; - _errorMessage = result.errorMessage; - notifyListeners(); - return; - } - - _targetCenterX = result.centerX; - _targetCenterY = result.centerY; - _targetRadius = result.radius; - _targetInnerRadius = result.radius * 0.1; - - // Create shots from detected impacts - _shots = result.impacts.map((impact) { - return Shot( - id: _uuid.v4(), - x: impact.x, - y: impact.y, - score: impact.suggestedScore, - analysisId: '', - ); - }).toList(); - - // Calculate scores - _recalculateScores(); - - // Calculate grouping - _recalculateGrouping(); + _shots = []; _state = AnalysisState.success; notifyListeners(); @@ -216,22 +135,18 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } - /// Remove a shot - void removeShot(String shotId) { - _shots.removeWhere((shot) => shot.id == shotId); + /// Efface tous les impacts en un clic (bouton ↻ de l'écran Plotting). + /// La calibration (centre, rayon, anneaux) n'est pas touchée. + void clearShots() { + _shots.clear(); _recalculateScores(); _recalculateGrouping(); notifyListeners(); } - /// Move a shot to a new position - void moveShot(String shotId, double newX, double newY) { - final index = _shots.indexWhere((shot) => shot.id == shotId); - if (index == -1) return; - - final newScore = _calculateShotScore(newX, newY); - _shots[index] = _shots[index].copyWith(x: newX, y: newY, score: newScore); - + /// Remove a shot + void removeShot(String shotId) { + _shots.removeWhere((shot) => shot.id == shotId); _recalculateScores(); _recalculateGrouping(); notifyListeners(); @@ -247,276 +162,17 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } - /// Auto-detect impacts using image processing - Future autoDetectImpacts({ - int darkThreshold = 80, - int minImpactSize = 20, - int maxImpactSize = 500, - double minCircularity = 0.6, - double minFillRatio = 0.5, - bool clearExisting = false, - }) async { - if (_imagePath == null || _targetType == null) return 0; - - final settings = ImpactDetectionSettings( - darkThreshold: darkThreshold, - minImpactSize: minImpactSize, - maxImpactSize: maxImpactSize, - minCircularity: minCircularity, - minFillRatio: minFillRatio, - ); - - final detectedImpacts = _detectionService.detectImpactsOnly( - _imagePath!, - _targetType!, - _targetCenterX, - _targetCenterY, - _targetRadius, - _ringCount, - settings, - ); - - if (clearExisting) { - _shots.clear(); - } - - // Add detected impacts as shots - for (final impact in detectedImpacts) { - final score = _calculateShotScore(impact.x, impact.y); - final shot = Shot( - id: _uuid.v4(), - x: impact.x, - y: impact.y, - score: score, - analysisId: '', - ); - _shots.add(shot); - } - - _recalculateScores(); - _recalculateGrouping(); - notifyListeners(); - - return detectedImpacts.length; - } - - /// Auto-detect impacts using OpenCV (Hough Circles + Contours) - Future autoDetectImpactsWithOpenCV({ - double cannyThreshold1 = 50, - double cannyThreshold2 = 150, - double minDist = 20, - double param1 = 100, - double param2 = 30, - int minRadius = 5, - int maxRadius = 50, - int minSize = 5, - int maxSize = 1000, - int blurSize = 5, - bool useContourDetection = true, - double minCircularity = 0.6, - double minContourArea = 50, - double maxContourArea = 5000, - bool clearExisting = false, - }) async { - if (_imagePath == null || _targetType == null) return 0; - - final settings = OpenCVDetectionSettings( - cannyThreshold1: cannyThreshold1, - cannyThreshold2: cannyThreshold2, - minDist: minDist, - param1: param1, - param2: param2, - minRadius: minRadius, - maxRadius: maxRadius, - blurSize: blurSize, - useContourDetection: useContourDetection, - minCircularity: minCircularity, - minContourArea: minContourArea, - maxContourArea: maxContourArea, - ); - - final detectedImpacts = _detectionService.detectImpactsWithOpenCV( - _imagePath!, - _targetType!, - _targetCenterX, - _targetCenterY, - _targetRadius, - _ringCount, - settings: settings, - ); - - if (clearExisting) { - _shots.clear(); - } - - // Add detected impacts as shots - for (final impact in detectedImpacts) { - final score = _calculateShotScore(impact.x, impact.y); - final shot = Shot( - id: _uuid.v4(), - x: impact.x, - y: impact.y, - score: score, - analysisId: '', - ); - _shots.add(shot); - } - - _recalculateScores(); - _recalculateGrouping(); - notifyListeners(); - - return detectedImpacts.length; - } - - /// Detect impacts with OpenCV using reference points - Future detectFromReferencesWithOpenCV({ - double tolerance = 2.0, - bool clearExisting = false, - }) async { - if (_imagePath == null || - _targetType == null || - _referenceImpacts.length < 2) { - return 0; - } - - // Convertir les références - final references = _referenceImpacts - .map((shot) => ReferenceImpact(x: shot.x, y: shot.y)) - .toList(); - - final detectedImpacts = _detectionService - .detectImpactsWithOpenCVFromReferences( - _imagePath!, - _targetType!, - _targetCenterX, - _targetCenterY, - _targetRadius, - _ringCount, - references, - tolerance: tolerance, - ); - - if (clearExisting) { - _shots.clear(); - } - - // Add detected impacts as shots - for (final impact in detectedImpacts) { - final score = _calculateShotScore(impact.x, impact.y); - final shot = Shot( - id: _uuid.v4(), - x: impact.x, - y: impact.y, - score: score, - analysisId: '', - ); - _shots.add(shot); - } - - _recalculateScores(); - _recalculateGrouping(); - notifyListeners(); - - return detectedImpacts.length; - } - - /// Add a reference impact for calibrated detection - void addReferenceImpact(double x, double y) { - final score = _calculateShotScore(x, y); - final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: ''); - _referenceImpacts.add(shot); - notifyListeners(); - } - - /// Remove a reference impact - void removeReferenceImpact(String shotId) { - _referenceImpacts.removeWhere((shot) => shot.id == shotId); - _learnedCharacteristics = null; - notifyListeners(); - } - - /// Clear all reference impacts - void clearReferenceImpacts() { - _referenceImpacts.clear(); - _learnedCharacteristics = null; - notifyListeners(); - } - - /// Learn characteristics from reference impacts - bool learnFromReferences() { - if (_imagePath == null || _referenceImpacts.length < 2) return false; - - final references = _referenceImpacts - .map((shot) => ReferenceImpact(x: shot.x, y: shot.y)) - .toList(); - - _learnedCharacteristics = _detectionService.analyzeReferenceImpacts( - _imagePath!, - references, - ); - - notifyListeners(); - return _learnedCharacteristics != null; - } - - /// Auto-detect impacts using learned reference characteristics - Future detectFromReferences({ - double tolerance = 2.0, - bool clearExisting = false, - }) async { - if (_imagePath == null || - _targetType == null || - _learnedCharacteristics == null) { - return 0; - } - - final detectedImpacts = _detectionService.detectImpactsFromReferences( - _imagePath!, - _targetType!, - _targetCenterX, - _targetCenterY, - _targetRadius, - _ringCount, - _learnedCharacteristics!, - tolerance: tolerance, - ); - - if (clearExisting) { - _shots.clear(); - } - - // Add detected impacts as shots - for (final impact in detectedImpacts) { - final score = _calculateShotScore(impact.x, impact.y); - final shot = Shot( - id: _uuid.v4(), - x: impact.x, - y: impact.y, - score: score, - analysisId: '', - ); - _shots.add(shot); - } - - _recalculateScores(); - _recalculateGrouping(); - notifyListeners(); - - return detectedImpacts.length; - } - /// Adjust target position void adjustTargetPosition( - double centerX, - double centerY, - double innerRadius, - double radius, { - int? ringCount, - List? ringRadii, - double zoomScale = 1.0, - Offset offset = Offset.zero, - }) { + double centerX, + double centerY, + double innerRadius, + double radius, { + int? ringCount, + List? ringRadii, + double zoomScale = 1.0, + Offset offset = Offset.zero, + }) { _targetCenterX = (centerX - offset.dx) / zoomScale; _targetCenterY = (centerY - offset.dy) / zoomScale; _targetRadius = radius / zoomScale; @@ -539,118 +195,6 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } - /// Auto-calibrate target using OpenCV - Future autoCalibrateTarget() async { - if (_imagePath == null) return false; - - try { - // 1. Attempt to correct perspective/distortion first - final correctedPath = await _distortionService - .correctPerspectiveWithConcentricMesh(_imagePath!); - - if (correctedPath != _imagePath) { - _imagePath = correctedPath; - _correctedImagePath = correctedPath; - _distortionCorrectionEnabled = true; - _imageAspectRatio = 1.0; - notifyListeners(); - } - - // 2. Detect the target on the straight/corrected image - final result = await _opencvTargetService.detectTarget(_imagePath!); - - if (result.success) { - adjustTargetPosition( - result.centerX, - result.centerY, - result.radius * 0.1, - result.radius, - ); - return true; - } - return false; - } catch (e) { - debugPrint('Auto-calibration error: $e'); - return false; - } - } - - /// Calcule les paramètres de distorsion basés sur la calibration actuelle - void calculateDistortion() { - _distortionParams = _distortionService.calculateDistortionFromCalibration( - targetCenterX: _targetCenterX, - targetCenterY: _targetCenterY, - targetRadius: _targetRadius, - imageAspectRatio: _imageAspectRatio, - ); - notifyListeners(); - } - - /// Applique la correction de distorsion à l'image - /// Crée une nouvelle image corrigée et la sauvegarde - Future applyDistortionCorrection() async { - if (_imagePath == null || _distortionParams == null) return; - - try { - _correctedImagePath = await _distortionService.applyCorrection( - _imagePath!, - _distortionParams!, - ); - _distortionCorrectionEnabled = true; - notifyListeners(); - } catch (e) { - _errorMessage = 'Erreur lors de la correction: $e'; - notifyListeners(); - } - } - - /// Active ou désactive l'affichage de l'image corrigée - void setDistortionCorrectionEnabled(bool enabled) { - if (enabled && _correctedImagePath == null && _distortionParams != null) { - // Si on active mais pas encore d'image corrigée, la créer - applyDistortionCorrection(); - } else { - _distortionCorrectionEnabled = enabled; - notifyListeners(); - } - } - - /// Calcule ET applique la correction pour un feedback immédiat - Future calculateAndApplyDistortion() async { - // 1. Calcul des paramètres (votre code actuel) - _distortionParams = _distortionService.calculateDistortionFromCalibration( - targetCenterX: _targetCenterX, - targetCenterY: _targetCenterY, - targetRadius: _targetRadius, - imageAspectRatio: _imageAspectRatio, - ); - - // 2. Vérification si une correction est réellement nécessaire - if (_distortionParams != null && _distortionParams!.needsCorrection) { - // 3. Application immédiate de la transformation (méthode asynchrone) - await applyDistortionCorrection(); - } else { - notifyListeners(); - } - } - - Future runFullDistortionWorkflow() async { - _state = AnalysisState.loading; - notifyListeners(); - - try { - calculateDistortion(); - await applyDistortionCorrection(); - _distortionCorrectionEnabled = true; - _state = AnalysisState.success; - } catch (e) { - _errorMessage = "Erreur de rendu : $e"; - _state = AnalysisState.error; - } finally { - notifyListeners(); - } - } - int _calculateShotScore(double x, double y) { if (_targetType == TargetType.concentric) { return _scoreCalculatorService.calculateConcentricScore( @@ -807,11 +351,6 @@ class AnalysisProvider extends ChangeNotifier { _shots = []; _scoreResult = null; _groupingResult = null; - _referenceImpacts = []; - _learnedCharacteristics = null; - _distortionCorrectionEnabled = false; - _distortionParams = null; - _correctedImagePath = null; notifyListeners(); } @@ -833,4 +372,4 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } } -} \ No newline at end of file +} diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 71d00b71..97550a6e 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -14,7 +14,6 @@ import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; import '../../data/models/shot.dart'; import '../../data/repositories/session_repository.dart'; -import '../../services/target_detection_service.dart'; import '../../services/score_calculator_service.dart'; import '../../services/grouping_analyzer_service.dart'; import '../../services/wallet_identity_service.dart'; @@ -61,7 +60,6 @@ class AnalysisScreen extends StatelessWidget { return ChangeNotifierProvider( create: (context) { final p = AnalysisProvider( - detectionService: context.read(), scoreCalculatorService: context.read(), groupingAnalyzerService: context.read(), sessionRepository: context.read(), @@ -73,7 +71,6 @@ class AnalysisScreen extends StatelessWidget { p.analyzeImage( imagePath, targetType, - autoAnalyze: false, manualCenter: manualCenterOffset, ); return p; @@ -111,7 +108,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { // Forcé à TRUE pour démarrer sur l'ajustement des cercles bool _isCalibrating = true; - bool _isSelectingReferences = false; bool _isAtBottom = false; final ScrollController _scrollController = ScrollController(); @@ -192,7 +188,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { if (validated == true) { setState(() { _isCalibrating = false; - _isSelectingReferences = false; }); } else { _enterCalibration(); @@ -250,17 +245,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { }, ), actions: [ - if (!_isCalibrating && !_isSelectingReferences) + // Remise à zéro des impacts : efface tous les impacts en un clic, + // sans modifier la calibration (centre, rayon, anneaux). + if (!_isCalibrating) IconButton( icon: const Icon(Icons.refresh), - onPressed: () => provider.analyzeImage( - context.read().imagePath!, - context.read().targetType!, - ), + tooltip: 'Effacer tous les impacts', + onPressed: () => provider.clearShots(), ), // Nuage d'export vers le backend IA : visible uniquement si l'analyse // a réussi ET que l'utilisateur a activé l'option dans les Paramètres. - if (!_isCalibrating && !_isSelectingReferences) + if (!_isCalibrating) FutureBuilder( future: WalletIdentityService().isUploadEnabled(), builder: (context, snapshot) { @@ -470,8 +465,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, ), - const SizedBox(height: 12), - _buildActionButtons(context, provider), const SizedBox(height: 50), ], ), @@ -647,10 +640,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { - return const Column(children: [Row(children: [])]); - } - void _showShotDetails( BuildContext context, AnalysisProvider provider, diff --git a/lib/features/analysis/widgets/target_overlay.dart b/lib/features/analysis/widgets/target_overlay.dart index bcd760cf..734f7446 100644 --- a/lib/features/analysis/widgets/target_overlay.dart +++ b/lib/features/analysis/widgets/target_overlay.dart @@ -1,9 +1,9 @@ /// Overlay visuel de la cible. /// -/// Dessine les anneaux de la cible, les impacts détectés, le cercle de groupement -/// et les impacts de référence. Gère uniquement la SÉLECTION d'impacts existants -/// (tap sur un impact). L'AJOUT d'un impact est délégué à l'écran parent pour -/// éviter tout conflit de gestes avec le zoom/pan de l'InteractiveViewer. +/// Dessine les anneaux de la cible, les impacts et le cercle de groupement. +/// Gère uniquement la SÉLECTION d'impacts existants (tap sur un impact). +/// L'AJOUT d'un impact est délégué à l'écran parent pour éviter tout conflit +/// de gestes avec le zoom/pan de l'InteractiveViewer. library; import 'package:flutter/material.dart'; @@ -20,11 +20,9 @@ class TargetOverlay extends StatelessWidget { final int ringCount; final List? ringRadii; final void Function(Shot shot)? onShotTapped; - final void Function(double x, double y)? onAddShot; final double? groupingCenterX; final double? groupingCenterY; final double? groupingDiameter; - final List? referenceImpacts; final double zoomScale; final bool showRings; @@ -38,11 +36,9 @@ class TargetOverlay extends StatelessWidget { this.ringCount = 10, this.ringRadii, this.onShotTapped, - this.onAddShot, this.groupingCenterX, this.groupingCenterY, this.groupingDiameter, - this.referenceImpacts, this.zoomScale = 1.0, this.showRings = false, }); @@ -72,7 +68,6 @@ class TargetOverlay extends StatelessWidget { groupingCenterX: groupingCenterX, groupingCenterY: groupingCenterY, groupingDiameter: groupingDiameter, - referenceImpacts: referenceImpacts, zoomScale: zoomScale, showRings: showRings, ), @@ -127,7 +122,6 @@ class _TargetOverlayPainter extends CustomPainter { final double? groupingCenterX; final double? groupingCenterY; final double? groupingDiameter; - final List? referenceImpacts; final double zoomScale; final bool showRings; @@ -142,7 +136,6 @@ class _TargetOverlayPainter extends CustomPainter { this.groupingCenterX, this.groupingCenterY, this.groupingDiameter, - this.referenceImpacts, this.zoomScale = 1.0, this.showRings = false, }); @@ -163,13 +156,6 @@ class _TargetOverlayPainter extends CustomPainter { for (final shot in shots) { _drawImpact(canvas, size, shot); } - - // Draw reference impacts (with different color) - if (referenceImpacts != null) { - for (final ref in referenceImpacts!) { - _drawReferenceImpact(canvas, size, ref); - } - } } void _drawTargetCenter(Canvas canvas, Size size) { @@ -319,48 +305,6 @@ class _TargetOverlayPainter extends CustomPainter { ); } - void _drawReferenceImpact(Canvas canvas, Size size, Shot ref) { - final x = ref.x * size.width; - final y = ref.y * size.height; - - // Tailles fixes divisées par le zoom pour rester constantes à l'écran - final outerRadius = 12 / zoomScale; - final innerRadius = 10 / zoomScale; - final strokeWidth = 3 / zoomScale; - final fontSize = 12 / zoomScale; - - // Draw outer circle (white outline for visibility) - final outlinePaint = Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = strokeWidth; - canvas.drawCircle(Offset(x, y), outerRadius, outlinePaint); - - // Draw reference marker (purple) - final refPaint = Paint() - ..color = Colors.deepPurple - ..style = PaintingStyle.fill; - canvas.drawCircle(Offset(x, y), innerRadius, refPaint); - - // Draw "R" to indicate reference - final textPainter = TextPainter( - text: TextSpan( - text: 'R', - style: TextStyle( - color: Colors.white, - fontSize: fontSize, - fontWeight: FontWeight.bold, - ), - ), - textDirection: TextDirection.ltr, - ); - textPainter.layout(); - textPainter.paint( - canvas, - Offset(x - textPainter.width / 2, y - textPainter.height / 2), - ); - } - @override bool shouldRepaint(covariant _TargetOverlayPainter oldDelegate) { return shots != oldDelegate.shots || @@ -372,7 +316,6 @@ class _TargetOverlayPainter extends CustomPainter { groupingCenterX != oldDelegate.groupingCenterX || groupingCenterY != oldDelegate.groupingCenterY || groupingDiameter != oldDelegate.groupingDiameter || - referenceImpacts != oldDelegate.referenceImpacts || zoomScale != oldDelegate.zoomScale || showRings != oldDelegate.showRings; } diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 409012d7..c7c71f84 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -128,7 +128,6 @@ class _CaptureScreenState extends State // Détection OpenCV (cible circulaire) — on garde le résultat COMPLET TargetDetectionResult? _targetResult; // NOUVEAU : centre + rayon de la cible - Timer? _detectionTimer; bool _isAnalyzingFrame = false; // NOUVEAU : Données IMU en temps réel @@ -148,7 +147,6 @@ class _CaptureScreenState extends State void dispose() { _cameraController?.dispose(); _scanAnimationController.dispose(); - _detectionTimer?.cancel(); _parallelismSubscription?.cancel(); // NOUVEAU _parallelismService.dispose(); // NOUVEAU super.dispose(); @@ -300,9 +298,6 @@ class _CaptureScreenState extends State // Détection OpenCV périodique (inchangée) // ───────────────────────────────────────────────────────────────────────── void _startAlignmentDetection() { - _detectionTimer?.cancel(); - _detectionTimer = null; - DateTime? lastAnalysis; _cameraController!.startImageStream((CameraImage cameraImage) async { @@ -362,8 +357,6 @@ class _CaptureScreenState extends State } void _stopAlignmentDetection() { - _detectionTimer?.cancel(); - _detectionTimer = null; try { if (_cameraController != null && _cameraController!.value.isStreamingImages) { diff --git a/lib/main.dart b/lib/main.dart index 746588b3..967aee2c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,10 +7,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'app.dart'; import 'core/theme/theme_provider.dart'; import 'data/repositories/session_repository.dart'; -import 'services/target_detection_service.dart'; import 'services/score_calculator_service.dart'; import 'services/grouping_analyzer_service.dart'; -import 'services/image_processing_service.dart'; import 'features/session/session_provider.dart'; void main() async { @@ -32,14 +30,6 @@ void main() async { runApp( MultiProvider( providers: [ - Provider( - create: (_) => ImageProcessingService(), - ), - Provider( - create: (context) => TargetDetectionService( - imageProcessingService: context.read(), - ), - ), Provider( create: (_) => ScoreCalculatorService(), ), diff --git a/lib/services/distortion_correction_service.dart b/lib/services/distortion_correction_service.dart deleted file mode 100644 index 6caa3a27..00000000 --- a/lib/services/distortion_correction_service.dart +++ /dev/null @@ -1,1076 +0,0 @@ -/// Service de correction de distorsion d'objectif. -/// -/// Utilise la calibration des cercles de la cible pour calculer et appliquer -/// une transformation qui corrige la distorsion de l'objectif. L'image est -/// transformée pour que les cercles calibrés deviennent parfaitement circulaires. -library; - -import 'dart:io'; -import 'dart:math' as math; -import 'package:image/image.dart' as img; -import 'package:flutter/foundation.dart'; -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 -class DistortionParameters { - /// Ratio d'aplatissement horizontal (1.0 = pas de correction) - final double scaleX; - - /// Ratio d'aplatissement vertical (1.0 = pas de correction) - final double scaleY; - - /// Angle de rotation de l'axe principal (en radians) - final double rotation; - - /// Centre de la distorsion en coordonnées normalisées (0-1) - final double centerX; - final double centerY; - - /// Ratio de circularité détecté (1.0 = cercle parfait) - final double circularityRatio; - - const DistortionParameters({ - required this.scaleX, - required this.scaleY, - required this.rotation, - required this.centerX, - required this.centerY, - required this.circularityRatio, - }); - - /// Paramètres par défaut (pas de correction) - static const identity = DistortionParameters( - scaleX: 1.0, - scaleY: 1.0, - rotation: 0.0, - centerX: 0.5, - centerY: 0.5, - circularityRatio: 1.0, - ); - - /// La correction est-elle nécessaire ? - bool get needsCorrection => circularityRatio < 0.95; - - @override - String toString() { - return 'DistortionParameters(scaleX: ${scaleX.toStringAsFixed(3)}, ' - 'scaleY: ${scaleY.toStringAsFixed(3)}, ' - 'rotation: ${(rotation * 180 / math.pi).toStringAsFixed(1)}°, ' - 'circularity: ${(circularityRatio * 100).toStringAsFixed(1)}%)'; - } -} - -/// Service pour détecter et corriger la distorsion d'objectif -class DistortionCorrectionService { - /// Calcule les paramètres de distorsion à partir de la calibration des cercles - /// - /// [targetCenterX], [targetCenterY] : Centre de la cible calibré (0-1) - /// [targetRadius] : Rayon de la cible calibré - /// [imageAspectRatio] : Ratio largeur/hauteur de l'image - /// - /// Cette méthode analyse la forme attendue (cercle) vs la forme observée - /// pour déterminer les paramètres de correction. - DistortionParameters calculateDistortionFromCalibration({ - required double targetCenterX, - required double targetCenterY, - required double targetRadius, - required double imageAspectRatio, - }) { - // En théorie, si la cible est un cercle parfait et que l'utilisateur - // a calibré les anneaux pour qu'ils correspondent à ce qu'il voit, - // on peut déduire la distorsion. - - // Pour l'instant, on utilise une approche simplifiée basée sur l'aspect ratio - // et la position du centre par rapport au centre de l'image. - - // Calcul de l'excentricité basée sur la position du centre - final offsetX = targetCenterX - 0.5; - final offsetY = targetCenterY - 0.5; - final offsetDistance = math.sqrt(offsetX * offsetX + offsetY * offsetY); - - // Plus le centre est éloigné du centre de l'image, plus la distorsion est probable - // Estimation simplifiée de la distorsion radiale - final distortionFactor = 1.0 + offsetDistance * 0.2; - - // Calculer l'angle de l'axe principal de déformation - final angle = math.atan2(offsetY, offsetX); - - // Si l'image n'est pas carrée, tenir compte de l'aspect ratio - double scaleX = 1.0; - double scaleY = 1.0; - - if (imageAspectRatio > 1.0) { - // Image plus large que haute - scaleY = distortionFactor; - } else if (imageAspectRatio < 1.0) { - // Image plus haute que large - scaleX = distortionFactor; - } - - final circularityRatio = 1.0 / distortionFactor; - - return DistortionParameters( - scaleX: scaleX, - scaleY: scaleY, - rotation: angle, - centerX: targetCenterX, - centerY: targetCenterY, - circularityRatio: circularityRatio, - ); - } - - /// Calcule les paramètres de distorsion en comparant un cercle théorique - /// avec les points de calibration fournis par l'utilisateur - /// - /// [calibrationPoints] : Points sur l'ellipse visible (coordonnées 0-1) - /// [expectedRadius] : Rayon du cercle théorique - /// [centerX], [centerY] : Centre du cercle théorique - DistortionParameters calculateDistortionFromPoints({ - required List<({double x, double y})> calibrationPoints, - required double expectedRadius, - required double centerX, - required double centerY, - }) { - if (calibrationPoints.length < 4) { - return DistortionParameters.identity; - } - - // Calculer les distances de chaque point au centre - final distances = []; - final angles = []; - - for (final point in calibrationPoints) { - final dx = point.x - centerX; - final dy = point.y - centerY; - distances.add(math.sqrt(dx * dx + dy * dy)); - angles.add(math.atan2(dy, dx)); - } - - // Trouver les axes majeur et mineur de l'ellipse - double maxDist = 0, minDist = double.infinity; - double maxAngle = 0; - - for (int i = 0; i < distances.length; i++) { - if (distances[i] > maxDist) { - maxDist = distances[i]; - maxAngle = angles[i]; - } - if (distances[i] < minDist) { - minDist = distances[i]; - } - } - - // Calculer les facteurs de correction - // On veut que maxDist et minDist deviennent égaux à expectedRadius - final scaleX = expectedRadius / maxDist; - final scaleY = expectedRadius / minDist; - final circularityRatio = minDist / maxDist; - - return DistortionParameters( - scaleX: scaleX.clamp(0.5, 2.0), - scaleY: scaleY.clamp(0.5, 2.0), - rotation: maxAngle, - centerX: centerX, - centerY: centerY, - circularityRatio: circularityRatio, - ); - } - - /// Applique la correction de distorsion à une image - /// - /// [imagePath] : Chemin de l'image source - /// [params] : Paramètres de distorsion calculés - /// - /// Retourne le chemin de l'image corrigée - Future applyCorrection( - String imagePath, - DistortionParameters params, - ) async { - final file = File(imagePath); - final bytes = await file.readAsBytes(); - final image = img.decodeImage(bytes); - - if (image == null) { - throw Exception('Impossible de décoder l\'image'); - } - - final correctedImage = _transformImage(image, params); - - // Sauvegarder l'image corrigée - final tempDir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final outputPath = '${tempDir.path}/corrected_$timestamp.jpg'; - - final outputFile = File(outputPath); - await outputFile.writeAsBytes(img.encodeJpg(correctedImage, quality: 95)); - - return outputPath; - } - - /// Transforme l'image pour corriger la distorsion - img.Image _transformImage(img.Image source, DistortionParameters params) { - final width = source.width; - final height = source.height; - - // Créer une nouvelle image de même taille - final result = img.Image(width: width, height: height); - - // Centre de transformation en pixels - final cx = params.centerX * width; - final cy = params.centerY * height; - - // Précalculer cos/sin de la rotation - final cosR = math.cos(params.rotation); - final sinR = math.sin(params.rotation); - - // Pour chaque pixel de l'image destination, trouver le pixel source correspondant - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // Coordonnées relatives au centre - final dx = x - cx; - final dy = y - cy; - - // Rotation inverse - final rx = dx * cosR + dy * sinR; - final ry = -dx * sinR + dy * cosR; - - // Mise à l'échelle inverse (pour trouver d'où vient le pixel) - final sx = rx / params.scaleX; - final sy = ry / params.scaleY; - - // Rotation dans l'autre sens - final fx = sx * cosR - sy * sinR; - final fy = sx * sinR + sy * cosR; - - // Coordonnées source - final srcX = fx + cx; - final srcY = fy + cy; - - // Interpolation bilinéaire - final pixel = _bilinearInterpolate(source, srcX, srcY); - result.setPixel(x, y, pixel); - } - } - - return result; - } - - /// Interpolation bilinéaire pour un échantillonnage de qualité - img.Color _bilinearInterpolate(img.Image image, double x, double y) { - final x0 = x.floor(); - final y0 = y.floor(); - final x1 = x0 + 1; - final y1 = y0 + 1; - - // Vérifier les limites - if (x0 < 0 || y0 < 0 || x1 >= image.width || y1 >= image.height) { - // Retourner le pixel le plus proche pour les zones hors limites - return image.getPixel( - x.round().clamp(0, image.width - 1), - y.round().clamp(0, image.height - 1), - ); - } - - // Poids pour l'interpolation - final wx = x - x0; - final wy = y - y0; - - // Récupérer les 4 pixels voisins - final p00 = image.getPixel(x0, y0); - final p10 = image.getPixel(x1, y0); - final p01 = image.getPixel(x0, y1); - 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, - ); - - 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, - ) { - final top = v00 * (1 - wx) + v10 * wx; - final bottom = v01 * (1 - wx) + v11 * wx; - return top * (1 - wy) + bottom * wy; - } - - /// Applique une correction de perspective simple basée sur 4 points - /// - /// Cette méthode est utile quand la photo est prise en angle. - /// [corners] : Les 4 coins de la cible dans l'ordre (haut-gauche, haut-droite, bas-droite, bas-gauche) - Future applyPerspectiveCorrection( - String imagePath, - List<({double x, double y})> corners, - ) async { - if (corners.length != 4) { - throw ArgumentError('4 points de coin sont requis'); - } - - final file = File(imagePath); - final bytes = await file.readAsBytes(); - final image = img.decodeImage(bytes); - - if (image == null) { - throw Exception('Impossible de décoder l\'image'); - } - - final width = image.width; - 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(); - - // Calculer la taille du rectangle destination - // On prend la moyenne des largeurs et hauteurs - final topWidth = _distance(srcCorners[0], srcCorners[1]); - final bottomWidth = _distance(srcCorners[3], srcCorners[2]); - final leftHeight = _distance(srcCorners[0], srcCorners[3]); - final rightHeight = _distance(srcCorners[1], srcCorners[2]); - - final dstWidth = ((topWidth + bottomWidth) / 2).round(); - final dstHeight = ((leftHeight + rightHeight) / 2).round(); - - // Créer l'image destination - 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()), - ]); - - // 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(), - ); - - if (src.x >= 0 && src.x < width && src.y >= 0 && src.y < height) { - final pixel = _bilinearInterpolate(image, src.x, src.y); - result.setPixel(x, y, pixel); - } - } - } - - // Sauvegarder - final tempDir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final outputPath = '${tempDir.path}/perspective_$timestamp.jpg'; - - final outputFile = File(outputPath); - await outputFile.writeAsBytes(img.encodeJpg(result, quality: 95)); - - return outputPath; - } - - double _distance(({double x, double y}) p1, ({double x, double y}) p2) { - final dx = p2.x - p1.x; - final dy = p2.y - p1.y; - return math.sqrt(dx * dx + dy * dy); - } - - /// Calcule la matrice de transformation perspective (homographie) - List _computePerspectiveMatrix( - List<({double x, double y})> src, - List<({double x, double y})> dst, - ) { - // Résolution du système linéaire pour trouver la matrice 3x3 - // Utilisation de la méthode DLT (Direct Linear Transform) - - final a = List>.generate(8, (_) => List.filled(9, 0.0)); - - for (int i = 0; i < 4; i++) { - final sx = src[i].x; - final sy = src[i].y; - final dx = dst[i].x; - final dy = dst[i].y; - - a[i * 2] = [-sx, -sy, -1, 0, 0, 0, dx * sx, dx * sy, dx]; - a[i * 2 + 1] = [0, 0, 0, -sx, -sy, -1, dy * sx, dy * sy, dy]; - } - - // Résolution par SVD simplifiée (on utilise une approximation) - // Pour une implémentation complète, il faudrait une vraie décomposition SVD - final h = _solveHomography(a); - - return h; - } - - /// Résout le système linéaire pour trouver la matrice d'homographie 3x3. - /// Utilise l'élimination de Gauss-Jordan avec pivot partiel pour la stabilité. - List _solveHomography(List> a) { - // 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]), - ); - - // 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]); - - // Élimination de Gauss-Jordan - for (int i = 0; i < n; i++) { - // Recherche du pivot (valeur maximale dans la colonne pour limiter les erreurs) - int pivot = i; - for (int j = i + 1; j < n; j++) { - if (matrix[j][i].abs() > matrix[pivot][i].abs()) { - pivot = j; - } - } - - // Échange des lignes (si nécessaire) - final List tempRow = matrix[i]; - matrix[i] = matrix[pivot]; - matrix[pivot] = tempRow; - - final double tempB = b[i]; - b[i] = b[pivot]; - b[pivot] = tempB; - - // Vérification de la singularité (division par zéro impossible) - if (matrix[i][i].abs() < 1e-10) { - return [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Retourne identité si échec - } - - // Normalisation de la ligne pivot - for (int j = i + 1; j < n; j++) { - final double factor = matrix[j][i] / matrix[i][i]; - b[j] -= factor * b[i]; - for (int k = i; k < n; k++) { - matrix[j][k] -= factor * matrix[i][k]; - } - } - } - - // Substitution arrière - final List h = List.filled(9, 0.0); - for (int i = n - 1; i >= 0; i--) { - double sum = 0.0; - for (int j = i + 1; j < n; j++) { - sum += matrix[i][j] * h[j]; - } - h[i] = (b[i] - sum) / matrix[i][i]; - } - - h[8] = 1.0; // Normalisation finale - return h; - } - - ({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); - } - final nx = (h[0] * x + h[1] * y + h[2]) / w; - 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 - debugPrint('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; - } - - /// Corrige la perspective en reformant le plus grand ovale (ellipse) en un cercle parfait, - /// sans recadrer agressivement l'image entière. - Future correctPerspectiveUsingOvals(String imagePath) async { - try { - final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR); - if (src.isEmpty) throw Exception("Impossible de charger l'image"); - - final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY); - final blurred = cv.gaussianBlur(gray, (5, 5), 0); - - final thresh = cv.threshold( - blurred, - 0, - 255, - cv.THRESH_BINARY | cv.THRESH_OTSU, - ); - final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1); - - final contoursResult = cv.findContours( - edges, - cv.RETR_EXTERNAL, - cv.CHAIN_APPROX_SIMPLE, - ); - final contours = contoursResult.$1; - - if (contours.isEmpty) return imagePath; - - cv.RotatedRect? bestEllipse; - double maxArea = 0; - - for (final contour in contours) { - if (contour.length < 5) continue; - final area = cv.contourArea(contour); - if (area < 1000) continue; - - final ellipse = cv.fitEllipse(contour); - if (area > maxArea) { - maxArea = area; - bestEllipse = ellipse; - } - } - - if (bestEllipse == null) return imagePath; - - // The goal here is to morph the bestEllipse into a perfect circle, while - // keeping the image the same size and the center of the ellipse in the same place. - // We'll use the average of the width and height (or max) to define the target circle - final targetRadius = - math.max(bestEllipse.size.width, bestEllipse.size.height) / 2.0; - - // Extract the 4 bounding box points of the ellipse - final boxPoints = cv.boxPoints(bestEllipse); - final List srcPoints = []; - for (int i = 0; i < boxPoints.length; i++) { - srcPoints.add(cv.Point(boxPoints[i].x.toInt(), boxPoints[i].y.toInt())); - } - _sortPoints(srcPoints); - - // Calculate the size of the perfectly squared output image - final int side = (targetRadius * 2).toInt(); - - final List dstPoints = [ - cv.Point(0, 0), // Top-Left - cv.Point(side, 0), // Top-Right - cv.Point(side, side), // Bottom-Right - cv.Point(0, side), // Bottom-Left - ]; - - // Morph the target region into a perfect square, cropping the rest of the image - final M = cv.getPerspectiveTransform( - cv.VecPoint.fromList(srcPoints), - cv.VecPoint.fromList(dstPoints), - ); - - final corrected = cv.warpPerspective(src, M, (side, side)); - - final tempDir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final outputPath = '${tempDir.path}/corrected_oval_$timestamp.jpg'; - - cv.imwrite(outputPath, corrected); - - return outputPath; - } catch (e) { - debugPrint('Erreur correction perspective ovales: $e'); - return imagePath; - } - } - - /// Corrige la distorsion et la profondeur (perspective) en créant un maillage - /// basé sur la concentricité des différents cercles de la cible pour trouver le meilleur plan. - Future correctPerspectiveWithConcentricMesh(String imagePath) async { - try { - final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR); - if (src.isEmpty) throw Exception("Impossible de charger l'image"); - - final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY); - final blurred = cv.gaussianBlur(gray, (5, 5), 0); - final thresh = cv.threshold( - blurred, - 0, - 255, - cv.THRESH_BINARY | cv.THRESH_OTSU, - ); - final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1); - - final contoursResult = cv.findContours( - edges, - cv.RETR_LIST, - cv.CHAIN_APPROX_SIMPLE, - ); - final contours = contoursResult.$1; - if (contours.isEmpty) return imagePath; - - List ellipses = []; - for (final contour in contours) { - if (contour.length < 5) continue; - if (cv.contourArea(contour) < 500) continue; - ellipses.add(cv.fitEllipse(contour)); - } - - if (ellipses.isEmpty) return imagePath; - - // Find the largest ellipse to serve as our central reference - ellipses.sort( - (a, b) => (b.size.width * b.size.height).compareTo( - a.size.width * a.size.height, - ), - ); - final largestEllipse = ellipses.first; - final maxDist = - math.max(largestEllipse.size.width, largestEllipse.size.height) * - 0.15; - - // Group all ellipses that are roughly concentric with the largest one - List concentricGroup = []; - for (final e in ellipses) { - final dx = e.center.x - largestEllipse.center.x; - final dy = e.center.y - largestEllipse.center.y; - if (math.sqrt(dx * dx + dy * dy) < maxDist) { - concentricGroup.add(e); - } - } - - if (concentricGroup.length < 2) { - debugPrint( - "Pas assez de cercles concentriques pour le maillage, utilisation de la méthode simple.", - ); - return await correctPerspectiveUsingOvals(imagePath); - } - - final targetRadius = - math.max(largestEllipse.size.width, largestEllipse.size.height) / 2.0; - final int side = (targetRadius * 2.4).toInt(); // Add padding - final double cx = side / 2.0; - final double cy = side / 2.0; - - List srcPointsList = []; - List dstPointsList = []; - - for (final ellipse in concentricGroup) { - final box = cv.boxPoints(ellipse); - final m0 = cv.Point2f( - (box[0].x + box[1].x) / 2, - (box[0].y + box[1].y) / 2, - ); - final m1 = cv.Point2f( - (box[1].x + box[2].x) / 2, - (box[1].y + box[2].y) / 2, - ); - final m2 = cv.Point2f( - (box[2].x + box[3].x) / 2, - (box[2].y + box[3].y) / 2, - ); - final m3 = cv.Point2f( - (box[3].x + box[0].x) / 2, - (box[3].y + box[0].y) / 2, - ); - - final d02 = math.sqrt( - math.pow(m0.x - m2.x, 2) + math.pow(m0.y - m2.y, 2), - ); - final d13 = math.sqrt( - math.pow(m1.x - m3.x, 2) + math.pow(m1.y - m3.y, 2), - ); - - cv.Point2f maj1, maj2, min1, min2; - double r; - - if (d02 > d13) { - maj1 = m0; - maj2 = m2; - min1 = m1; - min2 = m3; - r = d02 / 2.0; - } else { - maj1 = m1; - maj2 = m3; - min1 = m0; - min2 = m2; - r = d13 / 2.0; - } - - // Sort maj1 and maj2 so maj1 is left/top - if ((maj1.x - maj2.x).abs() > (maj1.y - maj2.y).abs()) { - if (maj1.x > maj2.x) { - final t = maj1; - maj1 = maj2; - maj2 = t; - } - } else { - if (maj1.y > maj2.y) { - final t = maj1; - maj1 = maj2; - maj2 = t; - } - } - - // Sort min1 and min2 so min1 is top/left - if ((min1.y - min2.y).abs() > (min1.x - min2.x).abs()) { - if (min1.y > min2.y) { - final t = min1; - min1 = min2; - min2 = t; - } - } else { - if (min1.x > min2.x) { - final t = min1; - min1 = min2; - min2 = t; - } - } - - srcPointsList.addAll([maj1, maj2, min1, min2]); - dstPointsList.addAll([ - cv.Point2f(cx - r, cy), - cv.Point2f(cx + r, cy), - cv.Point2f(cx, cy - r), - cv.Point2f(cx, cy + r), - ]); - - // Add ellipse centers mapping perfectly to the origin to force concentric depth alignment - srcPointsList.add(cv.Point2f(ellipse.center.x, ellipse.center.y)); - dstPointsList.add(cv.Point2f(cx, cy)); - } - - // We explicitly convert points to VecPoint to use findHomography standard binding - final srcVec = cv.VecPoint.fromList( - srcPointsList.map((p) => cv.Point(p.x.toInt(), p.y.toInt())).toList(), - ); - final dstVec = cv.VecPoint.fromList( - dstPointsList.map((p) => cv.Point(p.x.toInt(), p.y.toInt())).toList(), - ); - - final M = cv.findHomography( - cv.Mat.fromVec(srcVec), - cv.Mat.fromVec(dstVec), - method: cv.RANSAC, - ); - - if (M.isEmpty) { - return await correctPerspectiveUsingOvals(imagePath); - } - - final corrected = cv.warpPerspective(src, M, (side, side)); - - final tempDir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final outputPath = '${tempDir.path}/corrected_mesh_$timestamp.jpg'; - cv.imwrite(outputPath, corrected); - - return outputPath; - } catch (e) { - debugPrint('Erreur correction perspective maillage concentrique: $e'); - return imagePath; - } - } - - /// Corrige la perspective en détectant les 4 coins de la feuille (quadrilatère) - /// - /// Cette méthode cherche le plus grand polygone à 4 côtés (le bord du papier) - /// et le déforme pour en faire un carré parfait. - Future correctPerspectiveUsingQuadrilateral(String imagePath) async { - try { - final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR); - if (src.isEmpty) throw Exception("Impossible de charger l'image"); - - final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY); - // Flou plus important pour ignorer les détails internes (cercles, trous) - final blurred = cv.gaussianBlur(gray, (9, 9), 0); - - // Canny edge detector - final thresh = cv.threshold( - blurred, - 0, - 255, - cv.THRESH_BINARY | cv.THRESH_OTSU, - ); - final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1); - - // Pour la détection de la feuille (les bords peuvent être discontinus à cause de l'éclairage) - final kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)); - final closedEdges = cv.morphologyEx(edges, cv.MORPH_CLOSE, kernel); - - // Find contours - final contoursResult = cv.findContours( - closedEdges, - cv.RETR_EXTERNAL, - cv.CHAIN_APPROX_SIMPLE, - ); - final contours = contoursResult.$1; - - cv.VecPoint? bestQuad; - double maxArea = 0; - - final minArea = src.rows * src.cols * 0.1; // Au moins 10% de l'image - - for (final contour in contours) { - final area = cv.contourArea(contour); - if (area < minArea) continue; - - final peri = cv.arcLength(contour, true); - // Approximation polygonale (tolérance = 2% à 5% du périmètre) - final approx = cv.approxPolyDP(contour, 0.04 * peri, true); - - if (approx.length == 4) { - if (area > maxArea) { - maxArea = area; - bestQuad = approx; - } - } - } - - // Fallback - if (bestQuad == null) { - debugPrint( - "Aucun papier quadrilatère détecté, on utilise les cercles à la place.", - ); - return await correctPerspectiveUsingCircles(imagePath); - } - - // Convert to List - final List srcPoints = []; - for (int i = 0; i < bestQuad.length; i++) { - srcPoints.add(bestQuad[i]); - } - - _sortPoints(srcPoints); - - // Calculate max width and height - double widthA = _distanceCV(srcPoints[2], srcPoints[3]); - double widthB = _distanceCV(srcPoints[1], srcPoints[0]); - int dstWidth = math.max(widthA, widthB).toInt(); - - double heightA = _distanceCV(srcPoints[1], srcPoints[2]); - double heightB = _distanceCV(srcPoints[0], srcPoints[3]); - int dstHeight = math.max(heightA, heightB).toInt(); - - // Since standard target paper forms a square, we force the resulting warp to be a perfect square. - int side = math.max(dstWidth, dstHeight); - - final List dstPoints = [ - cv.Point(0, 0), - cv.Point(side, 0), - cv.Point(side, side), - cv.Point(0, side), - ]; - - final M = cv.getPerspectiveTransform( - cv.VecPoint.fromList(srcPoints), - cv.VecPoint.fromList(dstPoints), - ); - - final corrected = cv.warpPerspective(src, M, (side, side)); - - final tempDir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final outputPath = '${tempDir.path}/corrected_quad_$timestamp.jpg'; - - cv.imwrite(outputPath, corrected); - - return outputPath; - } catch (e) { - debugPrint('Erreur correction perspective quadrilatère: $e'); - // Fallback - return await correctPerspectiveUsingCircles(imagePath); - } - } - - double _distanceCV(cv.Point p1, cv.Point p2) { - final dx = p2.x - p1.x; - final dy = p2.y - p1.y; - return math.sqrt(dx * dx + dy * dy); - } -} diff --git a/lib/services/image_processing_service.dart b/lib/services/image_processing_service.dart deleted file mode 100644 index 146c83aa..00000000 --- a/lib/services/image_processing_service.dart +++ /dev/null @@ -1,1010 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'dart:math' as math; -import 'package:image/image.dart' as img; - -class DetectedCircle { - final double centerX; - final double centerY; - final double radius; - - DetectedCircle({ - required this.centerX, - required this.centerY, - required this.radius, - }); -} - -class DetectedImpact { - final double x; - final double y; - final double radius; - - DetectedImpact({ - required this.x, - required this.y, - required this.radius, - }); -} - -/// Image processing settings for impact detection -class ImpactDetectionSettings { - /// Threshold for dark spot detection (0-255, lower = darker) - final int darkThreshold; - - /// Minimum impact size in pixels - final int minImpactSize; - - /// Maximum impact size in pixels - final int maxImpactSize; - - /// Blur radius for noise reduction - final int blurRadius; - - /// Contrast enhancement factor - final double contrastFactor; - - /// Minimum circularity (0-1, 1 = perfect circle) - /// Used to filter out non-circular shapes like numbers - final double minCircularity; - - /// Maximum aspect ratio (width/height or height/width) - /// Used to filter out elongated shapes like numbers - final double maxAspectRatio; - - /// Minimum fill ratio (0-1, ~0.7 for filled circle, lower for rings/hollow shapes) - /// A bullet hole is FILLED, numbers on target are hollow rings - final double minFillRatio; - - const ImpactDetectionSettings({ - this.darkThreshold = 80, - this.minImpactSize = 20, - this.maxImpactSize = 500, - this.blurRadius = 2, - this.contrastFactor = 1.2, - this.minCircularity = 0.6, - this.maxAspectRatio = 2.0, - this.minFillRatio = 0.5, // Filled circles should have ratio > 0.5 - }); -} - -/// Reference impact for calibrated detection -class ReferenceImpact { - final double x; // Normalized 0-1 - final double y; // Normalized 0-1 - - const ReferenceImpact({required this.x, required this.y}); -} - -/// Characteristics learned from reference impacts -class ImpactCharacteristics { - final double avgLuminance; - final double luminanceStdDev; - final double avgSize; - final double sizeStdDev; - final double avgCircularity; - final double avgFillRatio; // How filled is the blob vs its bounding circle - final double avgDarkThreshold; // The threshold used to detect the blob - - const ImpactCharacteristics({ - required this.avgLuminance, - required this.luminanceStdDev, - required this.avgSize, - required this.sizeStdDev, - required this.avgCircularity, - required this.avgFillRatio, - required this.avgDarkThreshold, - }); - - @override - String toString() { - return 'ImpactCharacteristics(lum: ${avgLuminance.toStringAsFixed(1)} ± ${luminanceStdDev.toStringAsFixed(1)}, ' - 'size: ${avgSize.toStringAsFixed(1)} ± ${sizeStdDev.toStringAsFixed(1)}, ' - 'circ: ${avgCircularity.toStringAsFixed(2)}, fill: ${avgFillRatio.toStringAsFixed(2)})'; - } -} - -/// Service for image processing and impact detection -class ImageProcessingService { - /// Detect the main target circle from an image - DetectedCircle? detectMainTarget(String imagePath) { - // Return center of image as target (basic implementation) - // Could be enhanced with circle detection algorithm - return DetectedCircle( - centerX: 0.5, - centerY: 0.5, - radius: 0.4, - ); - } - - /// Detect impacts (bullet holes) from an image file - List detectImpacts(String imagePath) { - return detectImpactsWithSettings( - imagePath, - const ImpactDetectionSettings(), - ); - } - - /// Detect impacts with custom settings - List detectImpactsWithSettings( - String imagePath, - ImpactDetectionSettings settings, - ) { - try { - // Load the image - final file = File(imagePath); - final bytes = file.readAsBytesSync(); - final originalImage = img.decodeImage(bytes); - - if (originalImage == null) { - return []; - } - - // Resize for faster processing if image is too large - img.Image image; - final maxDimension = 1000; - if (originalImage.width > maxDimension || originalImage.height > maxDimension) { - final scale = maxDimension / math.max(originalImage.width, originalImage.height); - image = img.copyResize( - originalImage, - width: (originalImage.width * scale).round(), - height: (originalImage.height * scale).round(), - ); - } else { - image = originalImage; - } - - // Convert to grayscale - final grayscale = img.grayscale(image); - - // Apply gaussian blur to reduce noise - final blurred = img.gaussianBlur(grayscale, radius: settings.blurRadius); - - // Enhance contrast - final enhanced = img.adjustColor( - blurred, - contrast: settings.contrastFactor, - ); - - // Detect dark spots (potential impacts) - // Filter by circularity and fill ratio to avoid detecting numbers (hollow rings) - final impacts = _detectDarkSpots( - enhanced, - settings.darkThreshold, - settings.minImpactSize, - settings.maxImpactSize, - minCircularity: settings.minCircularity, - maxAspectRatio: settings.maxAspectRatio, - minFillRatio: settings.minFillRatio, - ); - - // Convert to relative coordinates - final width = originalImage.width.toDouble(); - final height = originalImage.height.toDouble(); - - return impacts.map((impact) { - return DetectedImpact( - x: impact.x / width, - y: impact.y / height, - radius: impact.radius, - ); - }).toList(); - } catch (e) { - debugPrint('Error detecting impacts: $e'); - return []; - } - } - - /// Analyze reference impacts to learn their characteristics - /// This actually finds the blob at each reference point and extracts its real properties - /// AMÉLIORÉ : Recherche plus large et analyse plus robuste - ImpactCharacteristics? analyzeReferenceImpacts( - String imagePath, - List references, { - int searchRadius = 50, // Augmenté de 30 à 50 - }) { - if (references.length < 2) return null; - - try { - final file = File(imagePath); - final bytes = file.readAsBytesSync(); - final originalImage = img.decodeImage(bytes); - if (originalImage == null) return null; - - // Resize for faster processing - taille augmentée - img.Image image; - double scale = 1.0; - final maxDimension = 1200; // Augmenté pour plus de précision - if (originalImage.width > maxDimension || originalImage.height > maxDimension) { - scale = maxDimension / math.max(originalImage.width, originalImage.height); - image = img.copyResize( - originalImage, - width: (originalImage.width * scale).round(), - height: (originalImage.height * scale).round(), - ); - } else { - image = originalImage; - } - - final grayscale = img.grayscale(image); - final blurred = img.gaussianBlur(grayscale, radius: 2); - final width = image.width; - final height = image.height; - - final luminances = []; - final sizes = []; - final circularities = []; - final fillRatios = []; - final thresholds = []; - - debugPrint('Analyzing ${references.length} reference impacts...'); - - for (int refIndex = 0; refIndex < references.length; refIndex++) { - final ref = references[refIndex]; - final centerX = (ref.x * width).round().clamp(0, width - 1); - final centerY = (ref.y * height).round().clamp(0, height - 1); - - debugPrint('Reference $refIndex at ($centerX, $centerY)'); - - // AMÉLIORATION : Recherche du point le plus sombre dans une zone plus large - int darkestX = centerX; - int darkestY = centerY; - double darkestLum = 255; - - // Recherche en spirale du point le plus sombre - for (int r = 0; r <= searchRadius; r++) { - for (int dy = -r; dy <= r; dy++) { - for (int dx = -r; dx <= r; dx++) { - // Seulement le périmètre du carré pour éviter les doublons - if (r > 0 && math.max(dx.abs(), dy.abs()) < r) continue; - - final px = centerX + dx; - final py = centerY + dy; - if (px < 0 || px >= width || py < 0 || py >= height) continue; - - final pixel = blurred.getPixel(px, py); - final lum = img.getLuminance(pixel).toDouble(); - if (lum < darkestLum) { - darkestLum = lum; - darkestX = px; - darkestY = py; - } - } - } - - // Si on a trouvé un point très sombre, on peut s'arrêter - if (darkestLum < 50 && r > 5) break; - } - - debugPrint(' Darkest point at ($darkestX, $darkestY), lum=$darkestLum'); - - // Now find the blob at the darkest point using adaptive threshold - final blobResult = _findBlobAtPoint(blurred, darkestX, darkestY, width, height); - - if (blobResult != null && blobResult.size >= 10) { // Au moins 10 pixels - luminances.add(blobResult.avgLuminance); - sizes.add(blobResult.size.toDouble()); - circularities.add(blobResult.circularity); - fillRatios.add(blobResult.fillRatio); - thresholds.add(blobResult.threshold); - debugPrint(' Found blob: size=${blobResult.size}, circ=${blobResult.circularity.toStringAsFixed(2)}, ' - 'fill=${blobResult.fillRatio.toStringAsFixed(2)}, threshold=${blobResult.threshold.toStringAsFixed(0)}'); - } else { - debugPrint(' No valid blob found at this reference'); - } - } - - if (luminances.isEmpty) { - debugPrint('ERROR: No valid blobs found from any reference!'); - return null; - } - - // Calculate statistics - final avgLum = luminances.reduce((a, b) => a + b) / luminances.length; - final avgSize = sizes.reduce((a, b) => a + b) / sizes.length; - final avgCirc = circularities.reduce((a, b) => a + b) / circularities.length; - final avgFill = fillRatios.reduce((a, b) => a + b) / fillRatios.length; - final avgThreshold = thresholds.reduce((a, b) => a + b) / thresholds.length; - - // Calculate standard deviations - double lumVariance = 0; - double sizeVariance = 0; - for (int i = 0; i < luminances.length; i++) { - lumVariance += math.pow(luminances[i] - avgLum, 2); - sizeVariance += math.pow(sizes[i] - avgSize, 2); - } - final lumStdDev = math.sqrt(lumVariance / luminances.length); - // AMÉLIORATION : Écart-type minimum pour éviter des plages trop étroites - final sizeStdDev = math.max( - math.sqrt(sizeVariance / sizes.length), - avgSize * 0.3, // Au moins 30% de variance - ); - - final result = ImpactCharacteristics( - avgLuminance: avgLum, - luminanceStdDev: math.max(lumStdDev, 10), // Minimum 10 de variance - avgSize: avgSize, - sizeStdDev: sizeStdDev, - avgCircularity: avgCirc, - avgFillRatio: avgFill, - avgDarkThreshold: avgThreshold, - ); - - debugPrint('Learned characteristics: $result'); - - return result; - } catch (e) { - debugPrint('Error analyzing reference impacts: $e'); - return null; - } - } - - /// Find a blob at a specific point and extract its characteristics - /// AMÉLIORÉ : Utilise plusieurs méthodes de détection et retourne le meilleur résultat - _BlobAnalysis? _findBlobAtPoint(img.Image image, int startX, int startY, int width, int height) { - // Get the luminance at the center point - final centerPixel = image.getPixel(startX, startY); - final centerLum = img.getLuminance(centerPixel).toDouble(); - - // MÉTHODE 1 : Expansion radiale pour trouver le bord - double sumLum = centerLum; - int pixelCount = 1; - double maxRadius = 0; - - // Collecter les luminances à différents rayons pour une analyse plus robuste - final radialLuminances = []; - - // Sample at different radii to find the edge - LIMITE RAISONNABLE pour impacts de balle - final maxSearchRadius = 60; // Un impact de balle ne fait pas plus de 60 pixels de rayon - for (int r = 1; r <= maxSearchRadius; r++) { - double ringSum = 0; - int ringCount = 0; - - // Sample points on a ring - final numSamples = math.max(12, r ~/ 2); - for (int i = 0; i < numSamples; i++) { - final angle = (i / numSamples) * 2 * math.pi; - final px = startX + (r * math.cos(angle)).round(); - final py = startY + (r * math.sin(angle)).round(); - if (px < 0 || px >= width || py < 0 || py >= height) continue; - - final pixel = image.getPixel(px, py); - final lum = img.getLuminance(pixel).toDouble(); - ringSum += lum; - ringCount++; - } - - if (ringCount > 0) { - final avgRingLum = ringSum / ringCount; - radialLuminances.add(avgRingLum); - - // Détection du bord : gradient de luminosité significatif - // Seuil adaptatif basé sur la différence avec le centre - final luminanceDiff = avgRingLum - centerLum; - - // Le bord est trouvé quand on a une augmentation significative de luminosité - if (luminanceDiff > 30 && maxRadius == 0) { - maxRadius = r.toDouble(); - break; // Arrêter dès qu'on trouve le bord - } - - if (maxRadius == 0) { - sumLum += ringSum; - pixelCount += ringCount; - } - } - } - - // Si aucun bord trouvé, chercher le gradient maximum - if (maxRadius < 2 && radialLuminances.length > 3) { - double maxGradient = 0; - int maxGradientIndex = 0; - for (int i = 1; i < radialLuminances.length; i++) { - final gradient = radialLuminances[i] - radialLuminances[i - 1]; - if (gradient > maxGradient) { - maxGradient = gradient; - maxGradientIndex = i; - } - } - if (maxGradient > 10) { - maxRadius = (maxGradientIndex + 1).toDouble(); - } - } - - // Rayon minimum de 3 pixels, maximum de 50 pour un impact de balle - if (maxRadius < 3) maxRadius = 3; - if (maxRadius > 50) maxRadius = 50; - - // Calculate threshold as weighted average between center and edge luminance - final edgeRadius = math.min((maxRadius * 1.2).round(), maxSearchRadius - 1); - double edgeLum = 0; - int edgeCount = 0; - for (int i = 0; i < 16; i++) { - final angle = (i / 16) * 2 * math.pi; - final px = startX + (edgeRadius * math.cos(angle)).round(); - final py = startY + (edgeRadius * math.sin(angle)).round(); - if (px < 0 || px >= width || py < 0 || py >= height) continue; - final pixel = image.getPixel(px, py); - edgeLum += img.getLuminance(pixel).toDouble(); - edgeCount++; - } - if (edgeCount > 0) { - edgeLum /= edgeCount; - } else { - edgeLum = centerLum + 50; - } - - // Calculer le seuil optimal - final threshold = ((centerLum + edgeLum) / 2).round().clamp(20, 200); - - // Utiliser une zone de recherche locale limitée autour du point - final analysis = _tryFindBlobWithThresholdLocal( - image, startX, startY, width, height, threshold, sumLum / pixelCount, - maxRadius.round() + 10, // Zone de recherche légèrement plus grande que le rayon détecté - ); - - return analysis; - } - - /// Trouve un blob avec un seuil dans une zone locale limitée - _BlobAnalysis? _tryFindBlobWithThresholdLocal( - img.Image image, - int startX, - int startY, - int width, - int height, - int threshold, - double avgLuminance, - int maxSearchRadius, - ) { - // Limiter la zone de recherche - final minX = math.max(0, startX - maxSearchRadius); - final maxX = math.min(width - 1, startX + maxSearchRadius); - final minY = math.max(0, startY - maxSearchRadius); - final maxY = math.min(height - 1, startY + maxSearchRadius); - - final localWidth = maxX - minX + 1; - final localHeight = maxY - minY + 1; - - // Create binary mask ONLY for the local region - final mask = List.generate(localHeight, (_) => List.filled(localWidth, false)); - for (int y = 0; y < localHeight; y++) { - for (int x = 0; x < localWidth; x++) { - final globalX = minX + x; - final globalY = minY + y; - final pixel = image.getPixel(globalX, globalY); - final lum = img.getLuminance(pixel); - mask[y][x] = lum < threshold; - } - } - - final visited = List.generate(localHeight, (_) => List.filled(localWidth, false)); - - // Find the blob containing the start point (in local coordinates) - final localStartX = startX - minX; - final localStartY = startY - minY; - - int searchX = localStartX; - int searchY = localStartY; - - if (!mask[localStartY][localStartX]) { - // Start point might not be in mask, find nearest point that is - bool found = false; - for (int r = 1; r <= 15 && !found; r++) { - for (int dy = -r; dy <= r && !found; dy++) { - for (int dx = -r; dx <= r && !found; dx++) { - final px = localStartX + dx; - final py = localStartY + dy; - if (px >= 0 && px < localWidth && py >= 0 && py < localHeight && mask[py][px]) { - searchX = px; - searchY = py; - found = true; - } - } - } - } - if (!found) return null; - } - - final blob = _floodFillLocal(mask, visited, searchX, searchY, localWidth, localHeight); - - // Vérifier que le blob est valide - taille raisonnable pour un impact - if (blob.size < 10 || blob.size > 5000) return null; // Entre 10 et 5000 pixels - - // Calculate fill ratio: actual pixels / bounding circle area - final boundingRadius = math.max(blob.radius, 1); - final boundingCircleArea = math.pi * boundingRadius * boundingRadius; - final fillRatio = (blob.size / boundingCircleArea).clamp(0.0, 1.0); - - return _BlobAnalysis( - avgLuminance: avgLuminance, - size: blob.size, - circularity: blob.circularity, - fillRatio: fillRatio, - threshold: threshold.toDouble(), - ); - } - - /// Flood fill pour une zone locale - _Blob _floodFillLocal( - List> mask, - List> visited, - int startX, - int startY, - int width, - int height, - ) { - final stack = <_Point>[_Point(startX, startY)]; - final points = <_Point>[]; - - int minX = startX, maxX = startX; - int minY = startY, maxY = startY; - int perimeterCount = 0; - - while (stack.isNotEmpty) { - final point = stack.removeLast(); - final x = point.x; - final y = point.y; - - if (x < 0 || x >= width || y < 0 || y >= height) continue; - if (visited[y][x] || !mask[y][x]) continue; - - visited[y][x] = true; - points.add(point); - - minX = math.min(minX, x); - maxX = math.max(maxX, x); - minY = math.min(minY, y); - maxY = math.max(maxY, y); - - // Check if this is a perimeter pixel - bool isPerimeter = false; - for (final delta in [[-1, 0], [1, 0], [0, -1], [0, 1]]) { - final nx = x + delta[0]; - final ny = y + delta[1]; - if (nx < 0 || nx >= width || ny < 0 || ny >= height || !mask[ny][nx]) { - isPerimeter = true; - break; - } - } - if (isPerimeter) perimeterCount++; - - // Add neighbors (4-connectivity) - stack.add(_Point(x + 1, y)); - stack.add(_Point(x - 1, y)); - stack.add(_Point(x, y + 1)); - stack.add(_Point(x, y - 1)); - } - - // Calculate centroid - double sumX = 0, sumY = 0; - for (final p in points) { - sumX += p.x; - sumY += p.y; - } - - final centerX = points.isNotEmpty ? sumX / points.length : startX.toDouble(); - final centerY = points.isNotEmpty ? sumY / points.length : startY.toDouble(); - - // Calculate bounding box dimensions - final blobWidth = (maxX - minX + 1).toDouble(); - final blobHeight = (maxY - minY + 1).toDouble(); - - // Calculate approximate radius based on bounding box - final radius = math.max(blobWidth, blobHeight) / 2.0; - - // Calculate circularity - final area = points.length.toDouble(); - final perimeter = perimeterCount.toDouble(); - final circularity = perimeter > 0 - ? (4 * math.pi * area) / (perimeter * perimeter) - : 0.0; - - // Calculate aspect ratio - final aspectRatio = blobWidth > blobHeight - ? blobWidth / blobHeight - : blobHeight / blobWidth; - - // Calculate fill ratio - final boundingCircleArea = math.pi * radius * radius; - final fillRatio = boundingCircleArea > 0 ? (area / boundingCircleArea).clamp(0.0, 1.0) : 0.0; - - return _Blob( - x: centerX, - y: centerY, - radius: radius, - size: points.length, - circularity: circularity.clamp(0.0, 1.0), - aspectRatio: aspectRatio, - fillRatio: fillRatio, - ); - } - - - /// Detect impacts based on reference characteristics with tolerance - /// - /// Utilise une approche multi-seuils adaptative pour une meilleure détection - List detectImpactsFromReferences( - String imagePath, - ImpactCharacteristics characteristics, { - double tolerance = 2.0, // Number of standard deviations - double minCircularity = 0.3, - }) { - try { - final file = File(imagePath); - final bytes = file.readAsBytesSync(); - final originalImage = img.decodeImage(bytes); - if (originalImage == null) return []; - - // Resize for faster processing - img.Image image; - double scale = 1.0; - final maxDimension = 1200; // Augmenté pour plus de précision - if (originalImage.width > maxDimension || originalImage.height > maxDimension) { - scale = maxDimension / math.max(originalImage.width, originalImage.height); - image = img.copyResize( - originalImage, - width: (originalImage.width * scale).round(), - height: (originalImage.height * scale).round(), - ); - } else { - image = originalImage; - } - - final grayscale = img.grayscale(image); - final blurred = img.gaussianBlur(grayscale, radius: 2); - - // AMÉLIORATION : Utiliser plusieurs seuils autour du seuil appris - final baseThreshold = characteristics.avgDarkThreshold.round(); - - // Générer une plage de seuils plus ciblée - final thresholds = []; - final thresholdRange = (15 * tolerance).round(); // Plage modérée - for (int offset = -thresholdRange; offset <= thresholdRange; offset += 8) { - final t = (baseThreshold + offset).clamp(30, 150); - if (!thresholds.contains(t)) thresholds.add(t); - } - - // Calculate size range based on learned characteristics - // Utiliser la variance mais avec des limites raisonnables - final sizeVariance = math.max(characteristics.sizeStdDev * tolerance, characteristics.avgSize * 0.4); - final minSize = math.max(20, (characteristics.avgSize - sizeVariance).round()); // Minimum 20 pixels - final maxSize = math.min(3000, (characteristics.avgSize + sizeVariance * 2).round()); // Maximum 3000 pixels - - // Calculate minimum circularity - équilibré - final circularityTolerance = 0.2 * tolerance; - final effectiveMinCircularity = math.max( - characteristics.avgCircularity - circularityTolerance, - minCircularity, - ).clamp(0.35, 0.85); - - // Calculate minimum fill ratio - impacts pleins - final minFillRatio = (characteristics.avgFillRatio - 0.2).clamp(0.35, 0.85); - - debugPrint('Detection params: thresholds=$thresholds, size=$minSize-$maxSize, ' - 'circ>=$effectiveMinCircularity, fill>=$minFillRatio'); - - // Détecter avec plusieurs seuils et combiner les résultats - final allBlobs = <_Blob>[]; - - for (final threshold in thresholds) { - final blobs = _detectDarkSpots( - blurred, - threshold, - minSize, - maxSize, - minCircularity: effectiveMinCircularity, - maxAspectRatio: 2.5, // Plus permissif - minFillRatio: minFillRatio, - ); - allBlobs.addAll(blobs); - } - - // Fusionner les blobs qui se chevauchent (même impact détecté à différents seuils) - final mergedBlobs = _mergeOverlappingBlobs(allBlobs); - - // FILTRE POST-DÉTECTION : Garder seulement les blobs similaires aux références - // Le filtre est plus ou moins strict selon la tolérance - final sizeToleranceFactor = 0.3 + (tolerance - 1) * 0.3; // 0.3 à 1.5 selon tolérance - final minSizeRatio = math.max(0.15, 1 / (1 + sizeToleranceFactor * 3)); - final maxSizeRatio = 1 + sizeToleranceFactor * 4; - - final filteredBlobs = mergedBlobs.where((blob) { - // Vérifier la taille par rapport aux caractéristiques apprises - final sizeRatio = blob.size / characteristics.avgSize; - if (sizeRatio < minSizeRatio || sizeRatio > maxSizeRatio) return false; - - // Vérifier la circularité (légèrement relaxée) - if (blob.circularity < effectiveMinCircularity * 0.85) return false; - - // Vérifier le fill ratio - if (blob.fillRatio < minFillRatio * 0.9) return false; - - return true; - }).toList(); - - debugPrint('Found ${filteredBlobs.length} impacts after filtering (from ${mergedBlobs.length} merged)'); - - // Convert to relative coordinates - return filteredBlobs.map((blob) { - return DetectedImpact( - x: blob.x / image.width, - y: blob.y / image.height, - radius: blob.radius / scale, - ); - }).toList(); - } catch (e) { - debugPrint('Error detecting impacts from references: $e'); - return []; - } - } - - /// Fusionne les blobs qui se chevauchent en gardant le meilleur représentant - List<_Blob> _mergeOverlappingBlobs(List<_Blob> blobs) { - if (blobs.isEmpty) return []; - - // Trier par score de qualité (circularité * fillRatio) - final sortedBlobs = List<_Blob>.from(blobs); - sortedBlobs.sort((a, b) { - final scoreA = a.circularity * a.fillRatio * a.size; - final scoreB = b.circularity * b.fillRatio * b.size; - return scoreB.compareTo(scoreA); - }); - - final merged = <_Blob>[]; - - for (final blob in sortedBlobs) { - bool shouldAdd = true; - - for (final existing in merged) { - final dx = blob.x - existing.x; - final dy = blob.y - existing.y; - final distance = math.sqrt(dx * dx + dy * dy); - final minDist = math.min(blob.radius, existing.radius); - - // Si les centres sont proches, c'est le même impact - if (distance < minDist * 1.5) { - shouldAdd = false; - break; - } - } - - if (shouldAdd) { - merged.add(blob); - } - } - - return merged; - } - - - /// Detect dark spots in a grayscale image using blob detection - List<_Blob> _detectDarkSpots( - img.Image image, - int threshold, - int minSize, - int maxSize, { - double minCircularity = 0.6, - double maxAspectRatio = 2.0, - double minFillRatio = 0.5, - }) { - final width = image.width; - final height = image.height; - - // Create binary mask of dark pixels - final mask = List.generate(height, (_) => List.filled(width, false)); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - final pixel = image.getPixel(x, y); - final luminance = img.getLuminance(pixel); - mask[y][x] = luminance < threshold; - } - } - - // Find connected components (blobs) - final visited = List.generate(height, (_) => List.filled(width, false)); - final blobs = <_Blob>[]; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - if (mask[y][x] && !visited[y][x]) { - final blob = _floodFill(mask, visited, x, y, width, height); - - // Filter by size - if (blob.size < minSize || blob.size > maxSize) continue; - - // Filter by circularity (reject non-circular shapes like numbers) - if (blob.circularity < minCircularity) continue; - - // Filter by aspect ratio (reject elongated shapes) - if (blob.aspectRatio > maxAspectRatio) continue; - - // Filter by fill ratio (reject hollow rings - numbers on target) - // A filled bullet hole should have fill ratio > 0.5 - // A hollow ring (like number "0" or "8") has a much lower fill ratio - if (blob.fillRatio < minFillRatio) continue; - - blobs.add(blob); - } - } - } - - // Filter overlapping blobs (keep larger ones) - final filteredBlobs = _filterOverlappingBlobs(blobs); - - return filteredBlobs; - } - - /// Flood fill to find connected component - _Blob _floodFill( - List> mask, - List> visited, - int startX, - int startY, - int width, - int height, - ) { - final stack = <_Point>[_Point(startX, startY)]; - final points = <_Point>[]; - - int minX = startX, maxX = startX; - int minY = startY, maxY = startY; - int perimeterCount = 0; - - while (stack.isNotEmpty) { - final point = stack.removeLast(); - final x = point.x; - final y = point.y; - - if (x < 0 || x >= width || y < 0 || y >= height) continue; - if (visited[y][x] || !mask[y][x]) continue; - - visited[y][x] = true; - points.add(point); - - minX = math.min(minX, x); - maxX = math.max(maxX, x); - minY = math.min(minY, y); - maxY = math.max(maxY, y); - - // Check if this is a perimeter pixel (has at least one non-blob neighbor) - bool isPerimeter = false; - for (final delta in [[-1, 0], [1, 0], [0, -1], [0, 1]]) { - final nx = x + delta[0]; - final ny = y + delta[1]; - if (nx < 0 || nx >= width || ny < 0 || ny >= height || !mask[ny][nx]) { - isPerimeter = true; - break; - } - } - if (isPerimeter) perimeterCount++; - - // Add neighbors (4-connectivity) - stack.add(_Point(x + 1, y)); - stack.add(_Point(x - 1, y)); - stack.add(_Point(x, y + 1)); - stack.add(_Point(x, y - 1)); - } - - // Calculate centroid - double sumX = 0, sumY = 0; - for (final p in points) { - sumX += p.x; - sumY += p.y; - } - - final centerX = points.isNotEmpty ? sumX / points.length : startX.toDouble(); - final centerY = points.isNotEmpty ? sumY / points.length : startY.toDouble(); - - // Calculate bounding box dimensions - final blobWidth = (maxX - minX + 1).toDouble(); - final blobHeight = (maxY - minY + 1).toDouble(); - - // Calculate approximate radius based on bounding box - final radius = math.max(blobWidth, blobHeight) / 2.0; - - // Calculate circularity: 4 * pi * area / perimeter^2 - // For a perfect circle, this equals 1 - final area = points.length.toDouble(); - final perimeter = perimeterCount.toDouble(); - final circularity = perimeter > 0 - ? (4 * math.pi * area) / (perimeter * perimeter) - : 0.0; - - // Calculate aspect ratio (always >= 1) - final aspectRatio = blobWidth > blobHeight - ? blobWidth / blobHeight - : blobHeight / blobWidth; - - // Calculate fill ratio: actual area vs bounding circle area - // A filled circle has fill ratio ~0.78 (pi/4), a ring/hollow circle has much lower - final boundingCircleArea = math.pi * radius * radius; - final fillRatio = boundingCircleArea > 0 ? (area / boundingCircleArea).clamp(0.0, 1.0) : 0.0; - - return _Blob( - x: centerX, - y: centerY, - radius: radius, - size: points.length, - circularity: circularity.clamp(0.0, 1.0), - aspectRatio: aspectRatio, - fillRatio: fillRatio, - ); - } - - /// Filter overlapping blobs, keeping the larger ones - List<_Blob> _filterOverlappingBlobs(List<_Blob> blobs) { - if (blobs.isEmpty) return []; - - // Sort by size (largest first) - blobs.sort((a, b) => b.size.compareTo(a.size)); - - final filtered = <_Blob>[]; - - for (final blob in blobs) { - bool overlaps = false; - - for (final existing in filtered) { - final dx = blob.x - existing.x; - final dy = blob.y - existing.y; - final distance = math.sqrt(dx * dx + dy * dy); - - // Check if blobs overlap - if (distance < (blob.radius + existing.radius) * 0.8) { - overlaps = true; - break; - } - } - - if (!overlaps) { - filtered.add(blob); - } - } - - return filtered; - } -} - -class _Point { - final int x; - final int y; - - _Point(this.x, this.y); -} - -class _Blob { - final double x; - final double y; - final double radius; - final int size; - final double circularity; // 0-1, 1 = perfect circle - final double aspectRatio; // width/height ratio - final double fillRatio; // How filled vs hollow the blob is - - _Blob({ - required this.x, - required this.y, - required this.radius, - required this.size, - required this.circularity, - required this.aspectRatio, - this.fillRatio = 1.0, - }); -} - -class _BlobAnalysis { - final double avgLuminance; - final int size; - final double circularity; - final double fillRatio; - final double threshold; - - _BlobAnalysis({ - required this.avgLuminance, - required this.size, - required this.circularity, - required this.fillRatio, - required this.threshold, - }); -} diff --git a/lib/services/opencv_impact_detection_service.dart b/lib/services/opencv_impact_detection_service.dart deleted file mode 100644 index 94c83771..00000000 --- a/lib/services/opencv_impact_detection_service.dart +++ /dev/null @@ -1,228 +0,0 @@ -/// Service de détection d'impacts utilisant OpenCV. -library; - -import 'dart:math' as math; -import 'package:opencv_dart/opencv_dart.dart' as cv; - -/// Paramètres de détection d'impacts OpenCV -class OpenCVDetectionSettings { - /// Seuil Canny bas pour la détection de contours - final double cannyThreshold1; - - /// Seuil Canny haut pour la détection de contours - final double cannyThreshold2; - - /// Distance minimale entre les centres des cercles détectés - final double minDist; - - /// Paramètre 1 de HoughCircles (seuil Canny interne) - final double param1; - - /// Paramètre 2 de HoughCircles (seuil d'accumulation) - final double param2; - - /// Rayon minimum des cercles en pixels - final int minRadius; - - /// Rayon maximum des cercles en pixels - final int maxRadius; - - /// Taille du flou gaussien (doit être impair) - final int blurSize; - - /// Utiliser la détection de contours en plus de Hough - final bool useContourDetection; - - /// Circularité minimale pour la détection par contours (0-1) - final double minCircularity; - - /// Surface minimale des contours - final double minContourArea; - - /// Surface maximale des contours - final double maxContourArea; - - const OpenCVDetectionSettings({ - this.cannyThreshold1 = 50, - this.cannyThreshold2 = 150, - this.minDist = 20, - this.param1 = 100, - this.param2 = 30, - this.minRadius = 5, - this.maxRadius = 50, - this.blurSize = 5, - this.useContourDetection = true, - this.minCircularity = 0.6, - this.minContourArea = 50, - this.maxContourArea = 5000, - }); -} - -/// Résultat de détection d'impact -class OpenCVDetectedImpact { - /// Position X normalisée (0-1) - final double x; - - /// Position Y normalisée (0-1) - final double y; - - /// Rayon en pixels - final double radius; - - /// Score de confiance (0-1) - final double confidence; - - /// Méthode de détection utilisée - final String method; - - const OpenCVDetectedImpact({ - required this.x, - required this.y, - required this.radius, - this.confidence = 1.0, - this.method = 'unknown', - }); -} - -/// Service de détection d'impacts utilisant OpenCV -class OpenCVImpactDetectionService { - /// Détecte les impacts dans une image en utilisant OpenCV - List detectImpacts( - String imagePath, { - OpenCVDetectionSettings settings = const OpenCVDetectionSettings(), - }) { - try { - final img = cv.imread(imagePath, flags: cv.IMREAD_COLOR); - if (img.isEmpty) return []; - - final gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY); - - // Apply blur to reduce noise - final blurKSize = (settings.blurSize, settings.blurSize); - final blurred = cv.gaussianBlur(gray, blurKSize, 2, sigmaY: 2); - - final List detectedImpacts = []; - - final circles = cv.HoughCircles( - blurred, - cv.HOUGH_GRADIENT, - 1, - settings.minDist, - param1: settings.param1, - param2: settings.param2, - minRadius: settings.minRadius, - maxRadius: settings.maxRadius, - ); - - if (circles.rows > 0 && circles.cols > 0) { - // Mat shape: (1, N, 3) usually for HoughCircles (CV_32FC3) - // We use at directly. - - for (int i = 0; i < circles.cols; i++) { - final vec = circles.at(0, i); - final x = vec.val1; - final y = vec.val2; - final r = vec.val3; - - detectedImpacts.add( - OpenCVDetectedImpact( - x: x / img.cols, - y: y / img.rows, - radius: r, - confidence: 0.8, - method: 'hough', - ), - ); - } - } - - // 2. Contour Detection (if enabled) - if (settings.useContourDetection) { - // Canny edge detection - final edges = cv.canny( - blurred, - settings.cannyThreshold1, - settings.cannyThreshold2, - ); - - // Find contours - final contoursResult = cv.findContours( - edges, - cv.RETR_EXTERNAL, - cv.CHAIN_APPROX_SIMPLE, - ); - - final contours = contoursResult.$1; - // hierarchy is $2 - - for (int i = 0; i < contours.length; i++) { - final contour = contours[i]; - - // Filter by area - final area = cv.contourArea(contour); - if (area < settings.minContourArea || - area > settings.maxContourArea) { - continue; - } - - // Filter by circularity - final perimeter = cv.arcLength(contour, true); - if (perimeter == 0) continue; - final circularity = 4 * math.pi * area / (perimeter * perimeter); - - if (circularity < settings.minCircularity) continue; - - // Get bounding circle - final enclosingCircle = cv.minEnclosingCircle(contour); - final center = enclosingCircle.$1; - final radius = enclosingCircle.$2; - - // Avoid duplicates (simple distance check against Hough results) - bool isDuplicate = false; - for (final existing in detectedImpacts) { - final dx = existing.x * img.cols - center.x; - final dy = existing.y * img.rows - center.y; - final dist = math.sqrt(dx * dx + dy * dy); - if (dist < radius) { - isDuplicate = true; - break; - } - } - - if (!isDuplicate) { - detectedImpacts.add( - OpenCVDetectedImpact( - x: center.x / img.cols, - y: center.y / img.rows, - radius: radius, - confidence: circularity, // Use circularity as confidence - method: 'contour', - ), - ); - } - } - } - - return detectedImpacts; - } catch (e) { - // print('OpenCV Error: $e'); - return []; - } - } - - /// Détecte les impacts en utilisant une image de référence - List detectFromReferences( - String imagePath, - List<({double x, double y})> referencePoints, { - double tolerance = 2.0, - }) { - // Basic implementation: use average color/brightness of reference points - // This is a placeholder for a more complex template matching or feature matching - - // For now, we can just run the standard detection but filter results - // based on properties of the reference points (e.g. size/radius if we had it). - - // Returning standard detection for now to enable the feature. - return detectImpacts(imagePath); - } -} diff --git a/lib/services/target_detection_service.dart b/lib/services/target_detection_service.dart deleted file mode 100644 index ecc6578d..00000000 --- a/lib/services/target_detection_service.dart +++ /dev/null @@ -1,461 +0,0 @@ -import 'dart:math' as math; -import 'package:flutter/foundation.dart'; -import '../data/models/target_type.dart'; -import 'image_processing_service.dart'; -import 'opencv_impact_detection_service.dart'; -export 'image_processing_service.dart' - show ImpactDetectionSettings, ReferenceImpact, ImpactCharacteristics; -export 'opencv_impact_detection_service.dart' - show OpenCVDetectionSettings, OpenCVDetectedImpact; - -// ============================================================================ -// STRUCT ET FONCTION GLOBALE POUR LE PARALLÉLISME (THREAD SECONDAIRE) -// ============================================================================ - -/// Conteneur de données pour envoyer les paramètres à l'Isolate d'arrière-plan. -class DetectionPayload { - final String imagePath; - final TargetType targetType; - - // On recrée les instances à l'intérieur du thread isolé car les objets ne se partagent pas entre threads. - DetectionPayload({ - required this.imagePath, - required this.targetType, - }); -} - -/// FONCTION EXÉCUTÉE EN PARALLÈLE : Tourne sur un autre cœur du processeur. -/// L'interface graphique reste à 120 FPS et totalement fluide. -TargetDetectionResult runParallelTargetDetection(DetectionPayload payload) { - // 1. Initialisation locale des services dans le sous-thread - final imageProcessingService = ImageProcessingService(); - - try { - // 2. Détection de la cible principale (Calcul lourd) - final mainTarget = imageProcessingService.detectMainTarget(payload.imagePath); - - double centerX = 0.5; - double centerY = 0.5; - double radius = 0.4; - - if (mainTarget != null) { - centerX = mainTarget.centerX; - centerY = mainTarget.centerY; - radius = mainTarget.radius; - } - - // 3. Détection des impacts (Calcul lourd) - final impacts = imageProcessingService.detectImpacts(payload.imagePath); - - // 4. Calcul mathématique des scores relatifs - final detectedImpacts = impacts.map((impact) { - final score = payload.targetType == TargetType.concentric - ? _staticCalculateConcentricScore( - impact.x, - impact.y, - centerX, - centerY, - radius, - ) - : _staticCalculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - - return TargetDetectionResult( - centerX: centerX, - centerY: centerY, - radius: radius, - impacts: detectedImpacts, - ); - } catch (e) { - return TargetDetectionResult.error('Erreur de detection parallèle: $e'); - } -} - -// Fonctions mathématiques pures nécessaires à l'Isolate (statiques) -int _staticCalculateConcentricScore(double impactX, double impactY, double centerX, double centerY, double targetRadius) { - final dx = impactX - centerX; - final dy = impactY - centerY; - final distance = math.sqrt(dx * dx + dy * dy) / targetRadius; - if (distance <= 0.1) return 10; - if (distance <= 0.2) return 9; - if (distance <= 0.3) return 8; - if (distance <= 0.4) return 7; - if (distance <= 0.5) return 6; - if (distance <= 0.6) return 5; - if (distance <= 0.7) return 4; - if (distance <= 0.8) return 3; - if (distance <= 0.9) return 2; - if (distance <= 1.0) return 1; - return 0; -} - -int _staticCalculateSilhouetteScore(double impactX, double impactY, double centerX, double centerY) { - final dx = (impactX - centerX).abs(); - final dy = impactY - centerY; - if (dx > 0.15) return 0; - if (dy < -0.25) return 5; - if (dy < 0.0) return 5; - if (dy < 0.15) return 4; - if (dy < 0.35) return 3; - return 0; -} - -// ============================================================================ -// FIN DU BLOC DE PARALLÉLISME -// ============================================================================ - - -class TargetDetectionResult { - final double centerX; // Relative (0-1) - final double centerY; // Relative (0-1) - final double radius; // Relative (0-1) - final List impacts; - final bool success; - final String? errorMessage; - - TargetDetectionResult({ - required this.centerX, - required this.centerY, - required this.radius, - required this.impacts, - this.success = true, - this.errorMessage, - }); - - factory TargetDetectionResult.error(String message) { - return TargetDetectionResult( - centerX: 0.5, - centerY: 0.5, - radius: 0.4, - impacts: [], - success: false, - errorMessage: message, - ); - } -} - -class DetectedImpactResult { - final double x; // Relative (0-1) - final double y; // Relative (0-1) - final double radius; // Absolute pixels - final int suggestedScore; - - DetectedImpactResult({ - required this.x, - required this.y, - required this.radius, - required this.suggestedScore, - }); -} - -class TargetDetectionService { - final ImageProcessingService _imageProcessingService; - final OpenCVImpactDetectionService _opencvService; - - TargetDetectionService({ - ImageProcessingService? imageProcessingService, - OpenCVImpactDetectionService? opencvService, - }) : _imageProcessingService = - imageProcessingService ?? ImageProcessingService(), - _opencvService = opencvService ?? OpenCVImpactDetectionService(); - - /// Detect target and impacts from an image file ASYNCHRONOUSLY in a separate Thread. - /// CORRECTION : Utilise désormais 'compute' pour basculer en arrière-plan immédiat. - Future detectTargetAsync(String imagePath, TargetType targetType) async { - final payload = DetectionPayload(imagePath: imagePath, targetType: targetType); - // Déclenche l'exécution isolée en tâche de fond - return await compute(runParallelTargetDetection, payload); - } - - /// Gardée pour rétrocompatibilité synchrone si nécessaire - TargetDetectionResult detectTarget(String imagePath, TargetType targetType) { - try { - final mainTarget = _imageProcessingService.detectMainTarget(imagePath); - - double centerX = 0.5; - double centerY = 0.5; - double radius = 0.4; - - if (mainTarget != null) { - centerX = mainTarget.centerX; - centerY = mainTarget.centerY; - radius = mainTarget.radius; - } - - final impacts = _imageProcessingService.detectImpacts(imagePath); - - final detectedImpacts = impacts.map((impact) { - final score = targetType == TargetType.concentric - ? _calculateConcentricScore( - impact.x, - impact.y, - centerX, - centerY, - radius, - ) - : _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - - return TargetDetectionResult( - centerX: centerX, - centerY: centerY, - radius: radius, - impacts: detectedImpacts, - ); - } catch (e) { - return TargetDetectionResult.error('Erreur de detection: $e'); - } - } - - int _calculateConcentricScore( - double impactX, - double impactY, - double centerX, - double centerY, - double targetRadius, - ) { - final dx = impactX - centerX; - final dy = impactY - centerY; - final distance = math.sqrt(dx * dx + dy * dy) / targetRadius; - - if (distance <= 0.1) return 10; - if (distance <= 0.2) return 9; - if (distance <= 0.3) return 8; - if (distance <= 0.4) return 7; - if (distance <= 0.5) return 6; - if (distance <= 0.6) return 5; - if (distance <= 0.7) return 4; - if (distance <= 0.8) return 3; - if (distance <= 0.9) return 2; - if (distance <= 1.0) return 1; - return 0; - } - - int _calculateSilhouetteScore( - double impactX, - double impactY, - double centerX, - double centerY, - ) { - final dx = (impactX - centerX).abs(); - final dy = impactY - centerY; - - if (dx > 0.15) return 0; - if (dy < -0.25) return 5; - if (dy < 0.0) return 5; - if (dy < 0.15) return 4; - if (dy < 0.35) return 3; - - return 0; - } - - List detectImpactsOnly( - String imagePath, - TargetType targetType, - double centerX, - double centerY, - double radius, - int ringCount, - ImpactDetectionSettings settings, - ) { - try { - final impacts = _imageProcessingService.detectImpactsWithSettings( - imagePath, - settings, - ); - - return impacts.map((impact) { - final score = targetType == TargetType.concentric - ? _calculateConcentricScoreWithRings( - impact.x, - impact.y, - centerX, - centerY, - radius, - ringCount, - ) - : _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - } catch (e) { - return []; - } - } - - int _calculateConcentricScoreWithRings( - double impactX, - double impactY, - double centerX, - double centerY, - double targetRadius, - int ringCount, - ) { - final dx = impactX - centerX; - final dy = impactY - centerY; - final distance = math.sqrt(dx * dx + dy * dy) / targetRadius; - - for (int i = 0; i < ringCount; i++) { - final zoneRadius = (i + 1) / ringCount; - if (distance <= zoneRadius) { - return 10 - i; - } - } - - return 0; - } - - ImpactCharacteristics? analyzeReferenceImpacts( - String imagePath, - List references, - ) { - return _imageProcessingService.analyzeReferenceImpacts( - imagePath, - references, - ); - } - - List detectImpactsFromReferences( - String imagePath, - TargetType targetType, - double centerX, - double centerY, - double radius, - int ringCount, - ImpactCharacteristics characteristics, { - double tolerance = 2.0, - }) { - try { - final impacts = _imageProcessingService.detectImpactsFromReferences( - imagePath, - characteristics, - tolerance: tolerance, - ); - - return impacts.map((impact) { - final score = targetType == TargetType.concentric - ? _calculateConcentricScoreWithRings( - impact.x, - impact.y, - centerX, - centerY, - radius, - ringCount, - ) - : _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - } catch (e) { - return []; - } - } - - List detectImpactsWithOpenCV( - String imagePath, - TargetType targetType, - double centerX, - double centerY, - double radius, - int ringCount, { - OpenCVDetectionSettings? settings, - }) { - try { - final impacts = _opencvService.detectImpacts( - imagePath, - settings: settings ?? const OpenCVDetectionSettings(), - ); - - return impacts.map((impact) { - final score = targetType == TargetType.concentric - ? _calculateConcentricScoreWithRings( - impact.x, - impact.y, - centerX, - centerY, - radius, - ringCount, - ) - : _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - } catch (e) { - debugPrint('Erreur détection OpenCV: $e'); - return []; - } - } - - List detectImpactsWithOpenCVFromReferences( - String imagePath, - TargetType targetType, - double centerX, - double centerY, - double radius, - int ringCount, - List references, { - double tolerance = 2.0, - }) { - try { - final refPoints = references.map((r) => (x: r.x, y: r.y)).toList(); - - final impacts = _opencvService.detectFromReferences( - imagePath, - refPoints, - tolerance: tolerance, - ); - - return impacts.map((impact) { - final score = targetType == TargetType.concentric - ? _calculateConcentricScoreWithRings( - impact.x, - impact.y, - centerX, - centerY, - radius, - ringCount, - ) - : _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY); - - return DetectedImpactResult( - x: impact.x, - y: impact.y, - radius: impact.radius, - suggestedScore: score, - ); - }).toList(); - } catch (e) { - debugPrint('Erreur détection OpenCV depuis références: $e'); - return []; - } - } -} \ No newline at end of file diff --git a/lib/services/target_rectify_service.dart b/lib/services/target_rectify_service.dart deleted file mode 100644 index b7660e7d..00000000 --- a/lib/services/target_rectify_service.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'dart:async'; -import 'dart:math' as math; -import 'package:flutter/foundation.dart'; -import 'package:opencv_dart/opencv_dart.dart' as cv; - -/// Résultat d'une tentative de redressement de cible. -class RectifyResult { - /// Chemin du fichier image redressé (ou original si échec). - final String outputPath; - - /// true si une cible a été détectée et redressée, false si on a renvoyé - /// l'image d'origine sans transformation. - final bool rectified; - - /// Angle d'inclinaison estimé de la cible AVANT redressement, en degrés. - /// (0 = déjà de face). Utile pour informer l'utilisateur. - final double estimatedTiltDegrees; - - /// Message de diagnostic (utile pour debug / affichage). - final String message; - - const RectifyResult({ - required this.outputPath, - required this.rectified, - required this.estimatedTiltDegrees, - required this.message, - }); -} - -/// Service qui redresse une cible RONDE (cercles concentriques) photographiée -/// de biais, en la ramenant parfaitement de face. -/// -/// Principe : -/// 1. Détecter le plus grand contour ~circulaire de l'image. -/// 2. Ajuster une ELLIPSE sur ce contour (fitEllipse). -/// 3. Un cercle vu en perspective devient une ellipse : on calcule la -/// transformation de perspective qui remappe cette ellipse vers un -/// CERCLE parfait, et on l'applique à toute l'image. -/// 4. La cible apparaît alors de face. -/// -/// L'image résultat est carrée et centrée sur la cible. -class TargetRectifyService { - /// Taille (en pixels) du côté de l'image carrée de sortie. - final int outputSize; - - /// Marge autour de la cible dans l'image de sortie (1.0 = cible pile au bord, - /// 1.3 = 30 % de marge autour). Garde un peu de contexte. - final double marginFactor; - - /// En dessous de cet écart d'axes (ratio petit/grand axe proche de 1), - /// la cible est considérée déjà de face → pas de warp inutile. - final double minTiltRatioToRectify; - - TargetRectifyService({ - this.outputSize = 1024, - this.marginFactor = 1.25, - this.minTiltRatioToRectify = 0.985, - }); - - /// Redresse l'image située à [inputPath]. Écrit le résultat dans - /// [outputPath] et renvoie un [RectifyResult]. - /// - /// Ne bloque jamais : en cas d'échec de détection, renvoie l'image - /// d'origine (rectified = false) pour ne pas perdre la photo du tireur. - Future rectify({ - required String inputPath, - required String outputPath, - }) async { - cv.Mat? src; - cv.Mat? gray; - cv.Mat? blurred; - cv.Mat? edges; - try { - src = cv.imread(inputPath, flags: cv.IMREAD_COLOR); - if (src.isEmpty) { - return RectifyResult( - outputPath: inputPath, - rectified: false, - estimatedTiltDegrees: 0, - message: 'Image illisible', - ); - } - - // ── 1. Prétraitement ──────────────────────────────────────────────── - gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY); - blurred = cv.gaussianBlur(gray, (5, 5), 2, sigmaY: 2); - edges = cv.canny(blurred, 60, 160); - - // Dilatation légère pour fermer les contours brisés - final cv.Mat kernel = cv.getStructuringElement( - cv.MORPH_ELLIPSE, - (3, 3), - ); - final cv.Mat dilated = cv.dilate(edges, kernel); - - // ── 2. Recherche du meilleur contour elliptique ────────────────────── - final (contours, _) = cv.findContours( - dilated, - cv.RETR_EXTERNAL, - cv.CHAIN_APPROX_SIMPLE, - ); - - final double imgArea = (src.width * src.height).toDouble(); - cv.RotatedRect? bestEllipse; - double bestScore = 0; - - for (int i = 0; i < contours.length; i++) { - final c = contours[i]; - if (c.length < 5) continue; // fitEllipse exige >= 5 points - - final double area = cv.contourArea(c); - // On ignore les contours minuscules et ceux qui couvrent presque tout - if (area < imgArea * 0.03 || area > imgArea * 0.97) continue; - - final cv.RotatedRect e = cv.fitEllipse(c); - final ep = e.points; // 4 sommets - if (ep.length < 4) continue; - final double ecx = (ep[0].x + ep[1].x + ep[2].x + ep[3].x) / 4.0; - final double ecy = (ep[0].y + ep[1].y + ep[2].y + ep[3].y) / 4.0; - final double mAx = (ep[0].x + ep[1].x) / 2.0; - final double mAy = (ep[0].y + ep[1].y) / 2.0; - final double mBx = (ep[1].x + ep[2].x) / 2.0; - final double mBy = (ep[1].y + ep[2].y) / 2.0; - final double w = - 2 * math.sqrt(math.pow(mAx - ecx, 2) + math.pow(mAy - ecy, 2)); - final double h = - 2 * math.sqrt(math.pow(mBx - ecx, 2) + math.pow(mBy - ecy, 2)); - if (w <= 1 || h <= 1) continue; - - // À quel point le contour ressemble-t-il vraiment à son ellipse ? - // On compare l'aire du contour à l'aire de l'ellipse ajustée. - final double ellipseArea = math.pi * (w / 2) * (h / 2); - if (ellipseArea <= 0) continue; - final double fitRatio = area / ellipseArea; // ~1 si bon ajustement - if (fitRatio < 0.7 || fitRatio > 1.3) continue; - - // Score = taille de l'ellipse (on veut la cible la plus grande) - final double score = ellipseArea; - if (score > bestScore) { - bestScore = score; - bestEllipse = e; - } - } - - if (bestEllipse == null) { - return RectifyResult( - outputPath: inputPath, - rectified: false, - estimatedTiltDegrees: 0, - message: 'Aucune cible circulaire détectée', - ); - } - - // ── 3. Extraire les 4 sommets de l'ellipse (robuste à la version d'API) ─ - // RotatedRect.points renvoie les 4 coins de la boîte englobant l'ellipse. - // On en dérive nous-mêmes le centre, les demi-axes et l'orientation, ce - // qui évite de dépendre de la forme exacte de `.size` / `.center` - // (record vs objet) qui varie selon les versions d'opencv_dart. - final pts = bestEllipse.points; // List de 4 sommets - - double px(int i) => pts[i].x; - double py(int i) => pts[i].y; - - // Centre = moyenne des 4 sommets - final double cx = (px(0) + px(1) + px(2) + px(3)) / 4.0; - final double cy = (py(0) + py(1) + py(2) + py(3)) / 4.0; - - // Milieux de deux côtés adjacents → extrémités des deux demi-axes - // Côté 0-1 et côté 1-2 (ordre des sommets d'un RotatedRect) - final double m01x = (px(0) + px(1)) / 2.0; - final double m01y = (py(0) + py(1)) / 2.0; - final double m12x = (px(1) + px(2)) / 2.0; - final double m12y = (py(1) + py(2)) / 2.0; - - // Demi-axes = distance centre → milieu de chaque côté - final double axisA = - math.sqrt(math.pow(m01x - cx, 2) + math.pow(m01y - cy, 2)); - final double axisB = - math.sqrt(math.pow(m12x - cx, 2) + math.pow(m12y - cy, 2)); - - final double majorAxis = math.max(axisA, axisB); - final double minorAxis = math.min(axisA, axisB); - if (majorAxis <= 1) { - return RectifyResult( - outputPath: inputPath, - rectified: false, - estimatedTiltDegrees: 0, - message: 'Ellipse dégénérée', - ); - } - - final double axisRatio = minorAxis / majorAxis; // 1 = cercle parfait - final double tiltDeg = - math.acos(axisRatio.clamp(0.0, 1.0)) * (180.0 / math.pi); - - // Déjà quasiment de face → on ne touche pas (évite le flou inutile) - if (axisRatio >= minTiltRatioToRectify) { - cv.imwrite(outputPath, src); - return RectifyResult( - outputPath: outputPath, - rectified: false, - estimatedTiltDegrees: tiltDeg, - message: 'Cible déjà de face', - ); - } - - // ── 4. Construire la transformation de perspective ──────────────────── - // Source : extrémités des deux axes de l'ellipse (4 points). - // Destination : extrémités des axes d'un cercle parfait centré. - // On obtient les extrémités en prolongeant centre→milieu-de-côté. - final srcPts = cv.VecPoint.fromList([ - cv.Point((cx + (m01x - cx)).round(), (cy + (m01y - cy)).round()), - cv.Point((cx - (m01x - cx)).round(), (cy - (m01y - cy)).round()), - cv.Point((cx + (m12x - cx)).round(), (cy + (m12y - cy)).round()), - cv.Point((cx - (m12x - cx)).round(), (cy - (m12y - cy)).round()), - ]); - - // Cible : cercle parfait centré, rayon R, dans une image carrée. - // L'axe "A" (m01) devient l'axe horizontal, l'axe "B" (m12) le vertical. - final double out = outputSize.toDouble(); - final double center = out / 2; - final double radius = (out / 2) / marginFactor; - final dstPts = cv.VecPoint.fromList([ - cv.Point((center + radius).round(), center.round()), - cv.Point((center - radius).round(), center.round()), - cv.Point(center.round(), (center + radius).round()), - cv.Point(center.round(), (center - radius).round()), - ]); - - final cv.Mat transform = cv.getPerspectiveTransform(srcPts, dstPts); - - final cv.Mat warped = cv.warpPerspective( - src, - transform, - (outputSize, outputSize), - flags: cv.INTER_LINEAR, - borderMode: cv.BORDER_CONSTANT, - ); - - cv.imwrite(outputPath, warped); - - return RectifyResult( - outputPath: outputPath, - rectified: true, - estimatedTiltDegrees: tiltDeg, - message: 'Cible redressée (inclinaison ${tiltDeg.toStringAsFixed(1)}°)', - ); - } catch (e) { - debugPrint('TargetRectify erreur: $e'); - // En cas de pépin, on renvoie l'original pour ne jamais perdre la photo - return RectifyResult( - outputPath: inputPath, - rectified: false, - estimatedTiltDegrees: 0, - message: 'Erreur de traitement: $e', - ); - } - } -} diff --git a/pubspec.lock b/pubspec.lock index 9cc0f496..094bf834 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -293,30 +293,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - google_mlkit_commons: - dependency: transitive - description: - name: google_mlkit_commons - sha256: "3e69fea4211727732cc385104e675ad1e40b29f12edd492ee52fa108423a6124" - url: "https://pub.dev" - source: hosted - version: "0.11.1" - 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" - google_mlkit_object_detection: - dependency: "direct main" - description: - name: google_mlkit_object_detection - sha256: "9dd35886972e18747e22098f8ebee78d30716a99a789bb2e3a65a24229e031e7" - url: "https://pub.dev" - source: hosted - version: "0.15.1" hooks: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a482fb2d..2fd58a6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,11 +38,9 @@ dependencies: cupertino_icons: ^1.0.8 sensors_plus: ^4.0.2 opencv_dart: ^2.1.0 - google_mlkit_object_detection: ^0.15.0 # Image capture from camera/gallery image_picker: ^1.2.1 - google_mlkit_document_scanner: ^0.2.0 # Local database for history sqflite: ^2.3.2 @@ -73,9 +71,6 @@ dependencies: crypto: ^3.0.7 camera: ^0.12.0+1 - # Machine Learning for YOLOv8 - # tflite_flutter: ^0.11.0 - dev_dependencies: flutter_test: sdk: flutter @@ -100,7 +95,6 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - assets/models/yolov8n_32.tflite # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/tests/find_homography_test.dart b/tests/find_homography_test.dart deleted file mode 100644 index 1202ea44..00000000 --- a/tests/find_homography_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:opencv_dart/opencv_dart.dart' as cv; - -void main() { - var p1 = cv.VecPoint.fromList([cv.Point(0, 0), cv.Point(1, 1)]); - var p2 = cv.VecPoint2f.fromList([cv.Point2f(0, 0), cv.Point2f(1, 1)]); - - // Is it p1.mat ? - // Or is it cv.findHomography(p1, p1) but actually needs specific types ? - cv.Mat mat1 = cv.Mat.fromVec(p1); - cv.Mat mat2 = cv.Mat.fromVec(p2); - cv.findHomography(mat1, mat2); -} diff --git a/tests/opencv_quad_test.dart b/tests/opencv_quad_test.dart deleted file mode 100644 index 11705c7d..00000000 --- a/tests/opencv_quad_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:opencv_dart/opencv_dart.dart' as cv; - -void main() { - print(cv.approxPolyDP); - print(cv.arcLength); - print(cv.contourArea); -} diff --git a/tests/test_homography.dart b/tests/test_homography.dart deleted file mode 100644 index e907c9b5..00000000 --- a/tests/test_homography.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:opencv_dart/opencv_dart.dart' as cv; - -void main() { - print(cv.findHomography); -}