diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..52fae0ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "avdmanager.executable": "c:\\Users\\streaper2\\AppData\\Local\\Android\\Sdk\\cmdline-tools\\latest\\bin\\avdmanager.bat", + "avdmanager.sdkManager": "c:\\Users\\streaper2\\AppData\\Local\\Android\\Sdk\\cmdline-tools\\latest\\bin\\sdkmanager.bat" +} \ No newline at end of file diff --git a/analyze_output.txt b/analyze_output.txt new file mode 100644 index 00000000..10abf256 Binary files /dev/null and b/analyze_output.txt differ diff --git a/analyze_results_final.txt b/analyze_results_final.txt new file mode 100644 index 00000000..bd9e85ee Binary files /dev/null and b/analyze_results_final.txt differ diff --git a/analyze_results_fixed.txt b/analyze_results_fixed.txt new file mode 100644 index 00000000..b7f2ab79 --- /dev/null +++ b/analyze_results_fixed.txt @@ -0,0 +1,37 @@ +Analyzing bully... + + info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:553:7 - avoid_print + info - The private field _selectedType could be 'final' - lib\features\capture\capture_screen.dart:27:14 - prefer_final_fields + info - 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead - lib\features\crop\crop_screen.dart:142:25 - deprecated_member_use + info - Statements in an if should be enclosed in a block - lib\services\distortion_correction_service.dart:566:11 - curly_braces_in_flow_control_structures + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:639:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:764:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:825:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:953:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1015:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1063:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:192:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:239:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:246:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:278:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:289:11 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:292:11 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:297:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:332:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:336:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:683:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:725:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:736:7 - avoid_print +warning - The declaration '_detectDarkSpotsAdaptive' isn't referenced - lib\services\image_processing_service.dart:780:15 - unused_element + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:328:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:377:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:414:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:23:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:29:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:31:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:67:7 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:4:3 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:5:3 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:6:3 - avoid_print + info - Don't invoke 'print' in production code - tests\test_homography.dart:4:3 - avoid_print + diff --git a/analyze_utf8.txt b/analyze_utf8.txt new file mode 100644 index 00000000..5167e7af --- /dev/null +++ b/analyze_utf8.txt @@ -0,0 +1,38 @@ +Analyzing bully... + + info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:392:7 - avoid_print + info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:596:7 - avoid_print + info - The private field _selectedType could be 'final' - lib\features\capture\capture_screen.dart:27:14 - prefer_final_fields + info - 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead - lib\features\crop\crop_screen.dart:142:25 - deprecated_member_use + info - Statements in an if should be enclosed in a block - lib\services\distortion_correction_service.dart:566:11 - curly_braces_in_flow_control_structures + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:639:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:764:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:825:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:953:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1015:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1063:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:192:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:239:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:246:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:278:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:289:11 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:292:11 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:297:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:332:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:336:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:683:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:725:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:736:7 - avoid_print +warning - The declaration '_detectDarkSpotsAdaptive' isn't referenced - lib\services\image_processing_service.dart:780:15 - unused_element + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:328:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:377:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:414:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:23:9 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:29:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:31:7 - avoid_print + info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:67:7 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:4:3 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:5:3 - avoid_print + info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:6:3 - avoid_print + info - Don't invoke 'print' in production code - tests\test_homography.dart:4:3 - avoid_print + diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6237c24e..3ac4a18e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -35,6 +35,7 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..c7abcf6f --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +# TensorFlow Lite +-keep class org.tensorflow.lite.** { *; } +-keep class com.google.android.gms.tflite.** { *; } +-dontwarn org.tensorflow.lite.** + +# Specifically for the GPU delegate error +-dontwarn org.tensorflow.lite.gpu.GpuDelegateFactory$Options diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index 0542169e..81031a9e 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -223,6 +223,16 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } + /// Update a shot's score manually + void updateShotScore(String shotId, int newScore) { + final index = _shots.indexWhere((shot) => shot.id == shotId); + if (index == -1) return; + + _shots[index] = _shots[index].copyWith(score: newScore); + _recalculateScores(); + notifyListeners(); + } + /// Auto-detect impacts using image processing Future autoDetectImpacts({ int darkThreshold = 80, @@ -293,6 +303,8 @@ class AnalysisProvider extends ChangeNotifier { 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, @@ -351,6 +363,47 @@ class AnalysisProvider extends ChangeNotifier { return detectedImpacts.length; } + /// Auto-detect impacts using YOLOv8 model + Future autoDetectImpactsWithYOLO({bool clearExisting = false}) async { + if (_imagePath == null || _targetType == null) return 0; + + try { + final detectedImpacts = await _detectionService.detectImpactsWithYOLO( + _imagePath!, + _targetType!, + _targetCenterX, + _targetCenterY, + _targetRadius, + _ringCount, + ); + + if (clearExisting) { + _shots.clear(); + } + + // Add detected impacts as shots + for (final impact in detectedImpacts) { + final shot = Shot( + id: _uuid.v4(), + x: impact.x, + y: impact.y, + score: impact.suggestedScore, + sessionId: '', + ); + _shots.add(shot); + } + + _recalculateScores(); + _recalculateGrouping(); + notifyListeners(); + + return detectedImpacts.length; + } catch (e) { + print('Error in YOLO auto-detection: $e'); + return 0; + } + } + /// Detect impacts with OpenCV using reference points Future detectFromReferencesWithOpenCV({ double tolerance = 2.0, diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index f9b22879..8cd72952 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -274,68 +274,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), child: Column( children: [ - // Auto-calibrate button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () async { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ), - SizedBox(width: 12), - Text('Auto-calibration en cours...'), - ], - ), - duration: Duration(seconds: 2), - ), - ); - final success = await provider - .autoCalibrateTarget(); - - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).hideCurrentSnackBar(); - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cible calibrée automatiquement', - ), - backgroundColor: AppTheme.successColor, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Échec de la calibration auto', - ), - backgroundColor: AppTheme.errorColor, - ), - ); - } - } - }, - icon: const Icon(Icons.auto_fix_high), - label: const Text('Auto-Calibrer la Cible'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepPurple, - foregroundColor: Colors.white, - ), - ), - ), - const SizedBox(height: 16), // Ring count slider Row( children: [ @@ -387,57 +326,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ], ), - // Target size slider - Row( - children: [ - const Icon( - Icons.zoom_out_map, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 8), - const Text( - 'Taille:', - style: TextStyle(color: Colors.white), - ), - Expanded( - child: Slider( - value: provider.targetRadius.clamp(0.05, 3.0), - min: 0.05, - max: 3.0, - label: - '${(provider.targetRadius * 100).toStringAsFixed(0)}%', - activeColor: AppTheme.warningColor, - onChanged: (value) { - provider.adjustTargetPosition( - provider.targetCenterX, - provider.targetCenterY, - provider.targetInnerRadius, - value, - ringCount: provider.ringCount, - ); - }, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: AppTheme.warningColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '${(provider.targetRadius * 100).toStringAsFixed(0)}%', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), const Divider(color: Colors.white24, height: 16), // Distortion correction row /*Row( @@ -793,6 +681,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), // Overlay qui se transforme avec l'image TargetOverlay( + showRings: true, shots: provider.shots, targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, @@ -879,6 +768,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), // Overlay qui se transforme avec l'image TargetOverlay( + showRings: true, shots: provider.shots, targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, @@ -991,107 +881,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { - return Column( - children: [ - // Reference-based detection section - if (_isSelectingReferences) ...[ - Card( - color: Colors.deepPurple.withValues(alpha: 0.1), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.touch_app, color: Colors.deepPurple), - const SizedBox(width: 8), - Text( - '${provider.referenceImpacts.length} reference(s) selectionnee(s)', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 8), - const Text( - 'Touchez l\'image pour marquer 3-4 impacts de reference. ' - 'L\'algorithme apprendra leurs caracteristiques pour detecter les autres.', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () { - setState(() => _isSelectingReferences = false); - provider.clearReferenceImpacts(); - }, - child: const Text('Annuler'), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton.icon( - onPressed: provider.referenceImpacts.length >= 2 - ? () => _showCalibratedDetectionDialog( - context, - provider, - ) - : null, - icon: const Icon(Icons.auto_fix_high), - label: const Text('Detecter'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepPurple, - foregroundColor: Colors.white, - ), - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 12), - ] else ...[ - // désactiver le temps de l'amelioration du scripts d'auto-detection - // Auto-detect buttons row - // Row( - // children: [ - // Expanded( - // child: ElevatedButton.icon( - // onPressed: () => _showAutoDetectDialog(context, provider), - // icon: const Icon(Icons.auto_fix_high), - // label: const Text('Auto-Detection'), - // style: ElevatedButton.styleFrom( - // backgroundColor: AppTheme.primaryColor, - // foregroundColor: Colors.white, - // padding: const EdgeInsets.symmetric(vertical: 12), - // ), - // ), - // ), - // const SizedBox(width: 12), - // Expanded( - // child: ElevatedButton.icon( - // onPressed: () => setState(() => _isSelectingReferences = true), - // icon: const Icon(Icons.touch_app), - // label: const Text('Par Reference'), - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.deepPurple, - // foregroundColor: Colors.white, - // padding: const EdgeInsets.symmetric(vertical: 12), - // ), - // ), - // ), - // ], - // ), - const SizedBox(height: 12), - ], - - // Manual actions - ], - ); + return const SizedBox.shrink(); } void _showHelpDialog(BuildContext context) { @@ -1129,20 +919,67 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { AnalysisProvider provider, String shotId, ) { + final shot = provider.shots.firstWhere((s) => s.id == shotId); + showModalBottomSheet( context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), builder: (context) => SafeArea( - child: Wrap( - children: [ - ListTile( - leading: const Icon(Icons.delete, color: AppTheme.errorColor), - title: const Text('Supprimer cet impact'), - onTap: () { - provider.removeShot(shotId); - Navigator.pop(context); - }, - ), - ], + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Modifier l\'impact', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 20), + // Dropdown for score + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Valeur de l\'impact : ', style: TextStyle(fontSize: 16)), + const SizedBox(width: 10), + DropdownButton( + value: shot.score.clamp(0, 10), + items: List.generate(11, (i) => i).map((i) { + return DropdownMenuItem( + value: i, + child: Text(i == 10 && provider.targetType == TargetType.concentric ? '10 (X)' : '$i'), + ); + }).toList(), + onChanged: (newScore) { + if (newScore != null) { + provider.updateShotScore(shotId, newScore); + Navigator.pop(context); + } + }, + ), + ], + ), + const SizedBox(height: 20), + // Delete button at the bottom + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + provider.removeShot(shotId); + Navigator.pop(context); + }, + icon: const Icon(Icons.delete), + label: const Text('SUPPRIMER L\'IMPACT'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.errorColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), ), ), ); @@ -1187,7 +1024,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - /* + void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) { // Detection settings bool clearExisting = true; @@ -1197,9 +1034,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { int maxImpactSize = 500; double minFillRatio = 0.5; - // NOTE: OpenCV désactivé - problèmes de build Windows - // Utilisation de la détection classique uniquement - showDialog( context: context, builder: (context) => StatefulBuilder( @@ -1216,11 +1050,78 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // YOLO option button + Card( + color: AppTheme.primaryColor.withAlpha(25), + child: ListTile( + leading: const Icon(Icons.psychology, color: AppTheme.primaryColor), + title: const Text('IA Detection (YOLOv8)', style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: const Text('Détection intelligente via modèle entraîné'), + onTap: () async { + Navigator.pop(context); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ), + SizedBox(width: 12), + Text('Détection IA en cours...'), + ], + ), + duration: Duration(seconds: 10), + ), + ); + + final count = await provider.autoDetectImpactsWithYOLO( + clearExisting: clearExisting, + ); + + if (context.mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + count > 0 + ? '$count impact(s) détecté(s) par l\'IA' + : 'Aucun impact détecté par l\'IA.', + ), + backgroundColor: count > 0 + ? AppTheme.successColor + : AppTheme.warningColor, + ), + ); + } + }, + ), + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Expanded(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text('OU', style: TextStyle(color: Colors.grey, fontSize: 12)), + ), + Expanded(child: Divider()), + ], + ), + ), + const Text( - 'Ajustez les parametres de detection:', + 'Détection Classique (Paramétrable):', style: TextStyle(fontWeight: FontWeight.bold), ), - const SizedBox(height: 16), + const SizedBox(height: 8), // Dark threshold slider Text('Seuil de detection (zones sombres): $darkThreshold'), @@ -1379,14 +1280,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } }, icon: const Icon(Icons.search), - label: const Text('Detecter'), + label: const Text('Détecter (Classique)'), ), ], ), ), ); } - */ void _showCalibratedDetectionDialog( BuildContext context, diff --git a/lib/features/analysis/widgets/target_overlay.dart b/lib/features/analysis/widgets/target_overlay.dart index 5c2d27e4..6ab23613 100644 --- a/lib/features/analysis/widgets/target_overlay.dart +++ b/lib/features/analysis/widgets/target_overlay.dart @@ -25,6 +25,7 @@ class TargetOverlay extends StatelessWidget { final double? groupingDiameter; final List? referenceImpacts; final double zoomScale; + final bool showRings; const TargetOverlay({ super.key, @@ -42,6 +43,7 @@ class TargetOverlay extends StatelessWidget { this.groupingDiameter, this.referenceImpacts, this.zoomScale = 1.0, + this.showRings = false, }); @override @@ -72,6 +74,7 @@ class TargetOverlay extends StatelessWidget { groupingDiameter: groupingDiameter, referenceImpacts: referenceImpacts, zoomScale: zoomScale, + showRings: showRings, ), child: Stack( children: shots.map((shot) { @@ -132,6 +135,7 @@ class _TargetOverlayPainter extends CustomPainter { final double? groupingDiameter; final List? referenceImpacts; final double zoomScale; + final bool showRings; _TargetOverlayPainter({ required this.shots, @@ -146,12 +150,15 @@ class _TargetOverlayPainter extends CustomPainter { this.groupingDiameter, this.referenceImpacts, this.zoomScale = 1.0, + this.showRings = false, }); @override void paint(Canvas canvas, Size size) { // Draw target center indicator - _drawTargetCenter(canvas, size); + if (showRings) { + _drawTargetCenter(canvas, size); + } // Draw grouping circle if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) { @@ -371,6 +378,7 @@ class _TargetOverlayPainter extends CustomPainter { groupingCenterY != oldDelegate.groupingCenterY || groupingDiameter != oldDelegate.groupingDiameter || referenceImpacts != oldDelegate.referenceImpacts || - zoomScale != oldDelegate.zoomScale; + zoomScale != oldDelegate.zoomScale || + showRings != oldDelegate.showRings; } } diff --git a/lib/services/target_detection_service.dart b/lib/services/target_detection_service.dart index 9c5db810..75541448 100644 --- a/lib/services/target_detection_service.dart +++ b/lib/services/target_detection_service.dart @@ -392,16 +392,21 @@ class TargetDetectionService { final impacts = await _yoloService.detectImpacts(imagePath); 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); + // Use YOLO-detected score (valeur) if available, otherwise calculate it + int score = impact.suggestedScore; + + if (score <= 0) { + 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, diff --git a/lib/services/yolo_impact_detection_service.dart b/lib/services/yolo_impact_detection_service.dart index d0a4d25f..ae24a7fa 100644 --- a/lib/services/yolo_impact_detection_service.dart +++ b/lib/services/yolo_impact_detection_service.dart @@ -7,25 +7,41 @@ import 'target_detection_service.dart'; class YOLOImpactDetectionService { Interpreter? _interpreter; + int _inputSize = 640; + List _outputShape = [1, 17, 8400]; + int _numClasses = 13; - static const String modelPath = 'assets/models/yolov11n_impact.tflite'; + static const String modelPath = 'assets/models/yolov8n_32.tflite'; static const String labelsPath = 'assets/models/labels.txt'; Future init() async { if (_interpreter != null) return; try { - // Try loading the specific YOLOv11 model first, fallback to v8 if not found - try { - _interpreter = await Interpreter.fromAsset(modelPath); - } catch (e) { - print('YOLOv11 model not found at $modelPath, trying YOLOv8 fallback'); - _interpreter = await Interpreter.fromAsset( - 'assets/models/yolov8n_impact.tflite', - ); + _interpreter = await Interpreter.fromAsset(modelPath); + + // Get model metadata + final inputTensors = _interpreter!.getInputTensors(); + if (inputTensors.isNotEmpty) { + // [1, 640, 640, 3] or [1, 3, 640, 640] + final shape = inputTensors[0].shape; + if (shape.length == 4) { + _inputSize = shape[1] == 3 ? shape[2] : shape[1]; + } + } + + final outputTensors = _interpreter!.getOutputTensors(); + if (outputTensors.isNotEmpty) { + _outputShape = outputTensors[0].shape; + // Output is usually [1, 4 + num_classes, num_boxes] + if (_outputShape.length == 3) { + _numClasses = _outputShape[1] - 4; + } } print('YOLO Interpreter loaded successfully'); + print('Model Input Size: $_inputSize'); + print('Model Output Shape: $_outputShape (Classes: $_numClasses)'); } catch (e) { print('Error loading YOLO model: $e'); } @@ -37,31 +53,35 @@ class YOLOImpactDetectionService { try { final bytes = File(imagePath).readAsBytesSync(); - final originalImage = img.decodeImage(bytes); - if (originalImage == null) return []; + final decodedImage = img.decodeImage(bytes); + if (decodedImage == null) return []; + + final originalImage = img.bakeOrientation(decodedImage); - // YOLOv8/v11 usually takes 640x640 - const int inputSize = 640; final resizedImage = img.copyResize( originalImage, - width: inputSize, - height: inputSize, + width: _inputSize, + height: _inputSize, ); - // Prepare input tensor - var input = _imageToByteListFloat32(resizedImage, inputSize); - - // Raw YOLO output shape usually [1, 4 + num_classes, 8400] - // For single class "impact", it's [1, 5, 8400] - var output = List.filled(1 * 5 * 8400, 0).reshape([1, 5, 8400]); + var input = _imageToByteListFloat32(resizedImage, _inputSize); + + // Allocate output buffer dynamically + var output = List.filled( + _outputShape.fold(1, (a, b) => a * b), + 0 + ).reshape(_outputShape); _interpreter!.run(input, output); - return _processOutput( + final results = _processOutput( output[0], originalImage.width, originalImage.height, ); + + print('YOLO Detection result count: ${results.length}'); + return results; } catch (e) { print('Error during YOLO inference: $e'); return []; @@ -76,33 +96,55 @@ class YOLOImpactDetectionService { final List<_Detection> candidates = []; const double threshold = 0.25; - // output is [5, 8400] -> [x, y, w, h, conf] - for (int i = 0; i < 8400; i++) { - final double confidence = output[4][i]; - if (confidence > threshold) { + // output is typically [4 + numClasses, numBoxes] + final int rows = output.length; + final int numBoxes = output[0].length; + + for (int i = 0; i < numBoxes; i++) { + double maxConfidence = 0; + int bestClass = 4; + + // Find best class among all classes (indices 4 to rows-1) + for (int c = 4; c < rows; c++) { + if (output[c][i] > maxConfidence) { + maxConfidence = output[c][i]; + bestClass = c; + } + } + + if (maxConfidence > threshold) { + final int classIndex = bestClass - 4; + candidates.add( _Detection( - x: output[0][i], - y: output[1][i], + // Use dynamic mapping: 0,1,2,3 are typically x,y,w,h + // We'll keep the current mapping for now as it matches user's previous model + y: output[0][i], + x: output[1][i], w: output[2][i], h: output[3][i], - confidence: confidence, + confidence: maxConfidence, + classIndex: classIndex, ), ); } } - // Apply Non-Max Suppression (NMS) final List<_Detection> suppressed = _nms(candidates); return suppressed .map( - (det) => DetectedImpactResult( - x: det.x / 640.0, - y: det.y / 640.0, - radius: 5.0, - suggestedScore: 0, - ), + (det) { + // Score = ClassIndex (0 to 10) for impact models + int score = det.classIndex; + + return DetectedImpactResult( + x: det.x, + y: det.y, + radius: 5.0, + suggestedScore: score, + ); + }, ) .toList(); } @@ -110,7 +152,6 @@ class YOLOImpactDetectionService { List<_Detection> _nms(List<_Detection> detections) { if (detections.isEmpty) return []; - // Sort by confidence descending detections.sort((a, b) => b.confidence.compareTo(a.confidence)); final List<_Detection> selected = []; @@ -164,11 +205,13 @@ class YOLOImpactDetectionService { class _Detection { final double x, y, w, h, confidence; + final int classIndex; _Detection({ required this.x, required this.y, required this.w, required this.h, required this.confidence, + required this.classIndex, }); } diff --git a/pubspec.lock b/pubspec.lock index 5cfae02f..1a439698 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -665,10 +665,10 @@ packages: dependency: "direct main" description: name: tflite_flutter - sha256: ffb8651fdb116ab0131d6dc47ff73883e0f634ad1ab12bb2852eef1bbeab4a6a + sha256: "48e6fde2ad97162bb66a16a142f4c4698add9e8cd397ce9d1cc7451b55537ac1" url: "https://pub.dev" source: hosted - version: "0.10.4" + version: "0.11.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f0c4edbf..b52ede12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: image: ^4.1.7 # Machine Learning for YOLOv8 - tflite_flutter: ^0.10.4 + tflite_flutter: ^0.11.0 dev_dependencies: flutter_test: @@ -90,7 +90,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: + assets: + - assets/models/yolov8n_32.tflite # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg