Compare commits
3 Commits
2e81f4b69e
...
screens
| Author | SHA1 | Date | |
|---|---|---|---|
| fba3b41f2f | |||
| e32833e366 | |||
| d4cb179fde |
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
BIN
analyze_output.txt
Normal file
BIN
analyze_output.txt
Normal file
Binary file not shown.
BIN
analyze_results_final.txt
Normal file
BIN
analyze_results_final.txt
Normal file
Binary file not shown.
37
analyze_results_fixed.txt
Normal file
37
analyze_results_fixed.txt
Normal file
@@ -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
|
||||||
|
|
||||||
38
analyze_utf8.txt
Normal file
38
analyze_utf8.txt
Normal file
@@ -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
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ android {
|
|||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
android/app/proguard-rules.pro
vendored
Normal file
7
android/app/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
@@ -53,6 +53,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
double _targetCenterX = 0.5;
|
double _targetCenterX = 0.5;
|
||||||
double _targetCenterY = 0.5;
|
double _targetCenterY = 0.5;
|
||||||
double _targetRadius = 0.4;
|
double _targetRadius = 0.4;
|
||||||
|
double _targetInnerRadius = 0.04;
|
||||||
int _ringCount = 10;
|
int _ringCount = 10;
|
||||||
List<double>? _ringRadii; // Individual ring radii multipliers
|
List<double>? _ringRadii; // Individual ring radii multipliers
|
||||||
double _imageAspectRatio = 1.0; // width / height
|
double _imageAspectRatio = 1.0; // width / height
|
||||||
@@ -83,6 +84,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
double get targetCenterX => _targetCenterX;
|
double get targetCenterX => _targetCenterX;
|
||||||
double get targetCenterY => _targetCenterY;
|
double get targetCenterY => _targetCenterY;
|
||||||
double get targetRadius => _targetRadius;
|
double get targetRadius => _targetRadius;
|
||||||
|
double get targetInnerRadius => _targetInnerRadius;
|
||||||
int get ringCount => _ringCount;
|
int get ringCount => _ringCount;
|
||||||
List<double>? get ringRadii =>
|
List<double>? get ringRadii =>
|
||||||
_ringRadii != null ? List.unmodifiable(_ringRadii!) : null;
|
_ringRadii != null ? List.unmodifiable(_ringRadii!) : null;
|
||||||
@@ -138,6 +140,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
_targetCenterX = 0.5;
|
_targetCenterX = 0.5;
|
||||||
_targetCenterY = 0.5;
|
_targetCenterY = 0.5;
|
||||||
_targetRadius = 0.4;
|
_targetRadius = 0.4;
|
||||||
|
_targetInnerRadius = 0.04;
|
||||||
|
|
||||||
// Initialize empty shots list
|
// Initialize empty shots list
|
||||||
_shots = [];
|
_shots = [];
|
||||||
@@ -160,6 +163,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
_targetCenterX = result.centerX;
|
_targetCenterX = result.centerX;
|
||||||
_targetCenterY = result.centerY;
|
_targetCenterY = result.centerY;
|
||||||
_targetRadius = result.radius;
|
_targetRadius = result.radius;
|
||||||
|
_targetInnerRadius = result.radius * 0.1;
|
||||||
|
|
||||||
// Create shots from detected impacts
|
// Create shots from detected impacts
|
||||||
_shots = result.impacts.map((impact) {
|
_shots = result.impacts.map((impact) {
|
||||||
@@ -219,6 +223,16 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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
|
/// Auto-detect impacts using image processing
|
||||||
Future<int> autoDetectImpacts({
|
Future<int> autoDetectImpacts({
|
||||||
int darkThreshold = 80,
|
int darkThreshold = 80,
|
||||||
@@ -289,6 +303,8 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
double param2 = 30,
|
double param2 = 30,
|
||||||
int minRadius = 5,
|
int minRadius = 5,
|
||||||
int maxRadius = 50,
|
int maxRadius = 50,
|
||||||
|
int minSize = 5,
|
||||||
|
int maxSize = 1000,
|
||||||
int blurSize = 5,
|
int blurSize = 5,
|
||||||
bool useContourDetection = true,
|
bool useContourDetection = true,
|
||||||
double minCircularity = 0.6,
|
double minCircularity = 0.6,
|
||||||
@@ -347,6 +363,47 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
return detectedImpacts.length;
|
return detectedImpacts.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Auto-detect impacts using YOLOv8 model
|
||||||
|
Future<int> 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
|
/// Detect impacts with OpenCV using reference points
|
||||||
Future<int> detectFromReferencesWithOpenCV({
|
Future<int> detectFromReferencesWithOpenCV({
|
||||||
double tolerance = 2.0,
|
double tolerance = 2.0,
|
||||||
@@ -488,12 +545,14 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
void adjustTargetPosition(
|
void adjustTargetPosition(
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
|
double innerRadius,
|
||||||
double radius, {
|
double radius, {
|
||||||
int? ringCount,
|
int? ringCount,
|
||||||
List<double>? ringRadii,
|
List<double>? ringRadii,
|
||||||
}) {
|
}) {
|
||||||
_targetCenterX = centerX;
|
_targetCenterX = centerX;
|
||||||
_targetCenterY = centerY;
|
_targetCenterY = centerY;
|
||||||
|
_targetInnerRadius = innerRadius;
|
||||||
_targetRadius = radius;
|
_targetRadius = radius;
|
||||||
if (ringCount != null) {
|
if (ringCount != null) {
|
||||||
_ringCount = ringCount;
|
_ringCount = ringCount;
|
||||||
@@ -517,10 +576,29 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
if (_imagePath == null) return false;
|
if (_imagePath == null) return false;
|
||||||
|
|
||||||
try {
|
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; // The corrected image is always square (side x side)
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Detect the target on the straight/corrected image
|
||||||
final result = await _opencvTargetService.detectTarget(_imagePath!);
|
final result = await _opencvTargetService.detectTarget(_imagePath!);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
adjustTargetPosition(result.centerX, result.centerY, result.radius);
|
adjustTargetPosition(
|
||||||
|
result.centerX,
|
||||||
|
result.centerY,
|
||||||
|
result.radius * 0.1,
|
||||||
|
result.radius,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -687,6 +765,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
_targetCenterX = 0.5;
|
_targetCenterX = 0.5;
|
||||||
_targetCenterY = 0.5;
|
_targetCenterY = 0.5;
|
||||||
_targetRadius = 0.4;
|
_targetRadius = 0.4;
|
||||||
|
_targetInnerRadius = 0.04;
|
||||||
_ringCount = 10;
|
_ringCount = 10;
|
||||||
_ringRadii = null;
|
_ringRadii = null;
|
||||||
_imageAspectRatio = 1.0;
|
_imageAspectRatio = 1.0;
|
||||||
|
|||||||
@@ -274,68 +274,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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
|
// Ring count slider
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -361,6 +300,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
provider.adjustTargetPosition(
|
provider.adjustTargetPosition(
|
||||||
provider.targetCenterX,
|
provider.targetCenterX,
|
||||||
provider.targetCenterY,
|
provider.targetCenterY,
|
||||||
|
provider.targetInnerRadius,
|
||||||
provider.targetRadius,
|
provider.targetRadius,
|
||||||
ringCount: value.round(),
|
ringCount: value.round(),
|
||||||
);
|
);
|
||||||
@@ -386,56 +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,
|
|
||||||
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),
|
const Divider(color: Colors.white24, height: 16),
|
||||||
// Distortion correction row
|
// Distortion correction row
|
||||||
/*Row(
|
/*Row(
|
||||||
@@ -535,6 +425,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
initialCenterX: provider.targetCenterX,
|
initialCenterX: provider.targetCenterX,
|
||||||
initialCenterY: provider.targetCenterY,
|
initialCenterY: provider.targetCenterY,
|
||||||
initialRadius: provider.targetRadius,
|
initialRadius: provider.targetRadius,
|
||||||
|
initialInnerRadius: provider.targetInnerRadius,
|
||||||
initialRingCount: provider.ringCount,
|
initialRingCount: provider.ringCount,
|
||||||
initialRingRadii: provider.ringRadii,
|
initialRingRadii: provider.ringRadii,
|
||||||
targetType: provider.targetType!,
|
targetType: provider.targetType!,
|
||||||
@@ -542,6 +433,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
(
|
(
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
innerRadius,
|
||||||
radius,
|
radius,
|
||||||
ringCount, {
|
ringCount, {
|
||||||
List<double>? ringRadii,
|
List<double>? ringRadii,
|
||||||
@@ -549,6 +441,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
provider.adjustTargetPosition(
|
provider.adjustTargetPosition(
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
|
innerRadius,
|
||||||
radius,
|
radius,
|
||||||
ringCount: ringCount,
|
ringCount: ringCount,
|
||||||
ringRadii: ringRadii,
|
ringRadii: ringRadii,
|
||||||
@@ -788,6 +681,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
// Overlay qui se transforme avec l'image
|
// Overlay qui se transforme avec l'image
|
||||||
TargetOverlay(
|
TargetOverlay(
|
||||||
|
showRings: true,
|
||||||
shots: provider.shots,
|
shots: provider.shots,
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
targetCenterY: provider.targetCenterY,
|
targetCenterY: provider.targetCenterY,
|
||||||
@@ -874,6 +768,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
// Overlay qui se transforme avec l'image
|
// Overlay qui se transforme avec l'image
|
||||||
TargetOverlay(
|
TargetOverlay(
|
||||||
|
showRings: true,
|
||||||
shots: provider.shots,
|
shots: provider.shots,
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
targetCenterY: provider.targetCenterY,
|
targetCenterY: provider.targetCenterY,
|
||||||
@@ -986,107 +881,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
||||||
return Column(
|
return const SizedBox.shrink();
|
||||||
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
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showHelpDialog(BuildContext context) {
|
void _showHelpDialog(BuildContext context) {
|
||||||
@@ -1124,21 +919,68 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
AnalysisProvider provider,
|
AnalysisProvider provider,
|
||||||
String shotId,
|
String shotId,
|
||||||
) {
|
) {
|
||||||
|
final shot = provider.shots.firstWhere((s) => s.id == shotId);
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) => SafeArea(
|
||||||
child: Wrap(
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
Text(
|
||||||
leading: const Icon(Icons.delete, color: AppTheme.errorColor),
|
'Modifier l\'impact',
|
||||||
title: const Text('Supprimer cet impact'),
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
onTap: () {
|
),
|
||||||
provider.removeShot(shotId);
|
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<int>(
|
||||||
|
value: shot.score.clamp(0, 10),
|
||||||
|
items: List.generate(11, (i) => i).map((i) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
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);
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1182,7 +1024,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) {
|
void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) {
|
||||||
// Detection settings
|
// Detection settings
|
||||||
bool clearExisting = true;
|
bool clearExisting = true;
|
||||||
@@ -1192,9 +1034,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
int maxImpactSize = 500;
|
int maxImpactSize = 500;
|
||||||
double minFillRatio = 0.5;
|
double minFillRatio = 0.5;
|
||||||
|
|
||||||
// NOTE: OpenCV désactivé - problèmes de build Windows
|
|
||||||
// Utilisation de la détection classique uniquement
|
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
@@ -1211,11 +1050,78 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
const Text(
|
||||||
'Ajustez les parametres de detection:',
|
'Détection Classique (Paramétrable):',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Dark threshold slider
|
// Dark threshold slider
|
||||||
Text('Seuil de detection (zones sombres): $darkThreshold'),
|
Text('Seuil de detection (zones sombres): $darkThreshold'),
|
||||||
@@ -1374,14 +1280,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
label: const Text('Detecter'),
|
label: const Text('Détecter (Classique)'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
void _showCalibratedDetectionDialog(
|
void _showCalibratedDetectionDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
|||||||
@@ -13,16 +13,26 @@ class TargetCalibration extends StatefulWidget {
|
|||||||
final double initialCenterX;
|
final double initialCenterX;
|
||||||
final double initialCenterY;
|
final double initialCenterY;
|
||||||
final double initialRadius;
|
final double initialRadius;
|
||||||
|
final double initialInnerRadius;
|
||||||
final int initialRingCount;
|
final int initialRingCount;
|
||||||
final TargetType targetType;
|
final TargetType targetType;
|
||||||
final List<double>? initialRingRadii;
|
final List<double>? initialRingRadii;
|
||||||
final Function(double centerX, double centerY, double radius, int ringCount, {List<double>? ringRadii}) onCalibrationChanged;
|
final Function(
|
||||||
|
double centerX,
|
||||||
|
double centerY,
|
||||||
|
double innerRadius,
|
||||||
|
double radius,
|
||||||
|
int ringCount, {
|
||||||
|
List<double>? ringRadii,
|
||||||
|
})
|
||||||
|
onCalibrationChanged;
|
||||||
|
|
||||||
const TargetCalibration({
|
const TargetCalibration({
|
||||||
super.key,
|
super.key,
|
||||||
required this.initialCenterX,
|
required this.initialCenterX,
|
||||||
required this.initialCenterY,
|
required this.initialCenterY,
|
||||||
required this.initialRadius,
|
required this.initialRadius,
|
||||||
|
required this.initialInnerRadius,
|
||||||
this.initialRingCount = 10,
|
this.initialRingCount = 10,
|
||||||
required this.targetType,
|
required this.targetType,
|
||||||
this.initialRingRadii,
|
this.initialRingRadii,
|
||||||
@@ -37,11 +47,13 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
late double _centerX;
|
late double _centerX;
|
||||||
late double _centerY;
|
late double _centerY;
|
||||||
late double _radius;
|
late double _radius;
|
||||||
|
late double _innerRadius;
|
||||||
late int _ringCount;
|
late int _ringCount;
|
||||||
late List<double> _ringRadii;
|
late List<double> _ringRadii;
|
||||||
|
|
||||||
bool _isDraggingCenter = false;
|
bool _isDraggingCenter = false;
|
||||||
bool _isDraggingRadius = false;
|
bool _isDraggingRadius = false;
|
||||||
|
bool _isDraggingInnerRadius = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -49,28 +61,57 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
_centerX = widget.initialCenterX;
|
_centerX = widget.initialCenterX;
|
||||||
_centerY = widget.initialCenterY;
|
_centerY = widget.initialCenterY;
|
||||||
_radius = widget.initialRadius;
|
_radius = widget.initialRadius;
|
||||||
|
_innerRadius = widget.initialInnerRadius;
|
||||||
_ringCount = widget.initialRingCount;
|
_ringCount = widget.initialRingCount;
|
||||||
_initRingRadii();
|
_initRingRadii();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initRingRadii() {
|
void _initRingRadii() {
|
||||||
if (widget.initialRingRadii != null && widget.initialRingRadii!.length == _ringCount) {
|
if (widget.initialRingRadii != null &&
|
||||||
|
widget.initialRingRadii!.length == _ringCount) {
|
||||||
_ringRadii = List.from(widget.initialRingRadii!);
|
_ringRadii = List.from(widget.initialRingRadii!);
|
||||||
} else {
|
} else {
|
||||||
// Initialize with default proportional radii
|
// Initialize with default proportional radii interpolated between inner and outer
|
||||||
_ringRadii = List.generate(_ringCount, (i) => (i + 1) / _ringCount);
|
_ringRadii = List.generate(_ringCount, (i) {
|
||||||
|
if (_ringCount <= 1) return 1.0;
|
||||||
|
final ratio = _innerRadius / _radius;
|
||||||
|
return ratio + (1.0 - ratio) * i / (_ringCount - 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(TargetCalibration oldWidget) {
|
void didUpdateWidget(TargetCalibration oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
bool shouldReinit = false;
|
||||||
|
|
||||||
|
if (widget.initialCenterX != oldWidget.initialCenterX &&
|
||||||
|
!_isDraggingCenter) {
|
||||||
|
_centerX = widget.initialCenterX;
|
||||||
|
}
|
||||||
|
if (widget.initialCenterY != oldWidget.initialCenterY &&
|
||||||
|
!_isDraggingCenter) {
|
||||||
|
_centerY = widget.initialCenterY;
|
||||||
|
}
|
||||||
if (widget.initialRingCount != oldWidget.initialRingCount) {
|
if (widget.initialRingCount != oldWidget.initialRingCount) {
|
||||||
_ringCount = widget.initialRingCount;
|
_ringCount = widget.initialRingCount;
|
||||||
_initRingRadii();
|
shouldReinit = true;
|
||||||
}
|
}
|
||||||
if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius) {
|
if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius) {
|
||||||
_radius = widget.initialRadius;
|
_radius = widget.initialRadius;
|
||||||
|
shouldReinit = true;
|
||||||
|
}
|
||||||
|
if (widget.initialInnerRadius != oldWidget.initialInnerRadius &&
|
||||||
|
!_isDraggingInnerRadius) {
|
||||||
|
_innerRadius = widget.initialInnerRadius;
|
||||||
|
shouldReinit = true;
|
||||||
|
}
|
||||||
|
if (widget.initialRingRadii != oldWidget.initialRingRadii) {
|
||||||
|
shouldReinit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldReinit) {
|
||||||
|
_initRingRadii();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +131,13 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
centerX: _centerX,
|
centerX: _centerX,
|
||||||
centerY: _centerY,
|
centerY: _centerY,
|
||||||
radius: _radius,
|
radius: _radius,
|
||||||
|
innerRadius: _innerRadius,
|
||||||
ringCount: _ringCount,
|
ringCount: _ringCount,
|
||||||
ringRadii: _ringRadii,
|
ringRadii: _ringRadii,
|
||||||
targetType: widget.targetType,
|
targetType: widget.targetType,
|
||||||
isDraggingCenter: _isDraggingCenter,
|
isDraggingCenter: _isDraggingCenter,
|
||||||
isDraggingRadius: _isDraggingRadius,
|
isDraggingRadius: _isDraggingRadius,
|
||||||
|
isDraggingInnerRadius: _isDraggingInnerRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -109,21 +152,42 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
// Check if tapping on center handle
|
// Check if tapping on center handle
|
||||||
final distToCenter = _distance(tapX, tapY, _centerX, _centerY);
|
final distToCenter = _distance(tapX, tapY, _centerX, _centerY);
|
||||||
|
|
||||||
// Check if tapping on radius handle (on the right edge of the outermost circle)
|
// Check if tapping on outer radius handle
|
||||||
final minDim = math.min(size.width, size.height);
|
final minDim = math.min(size.width, size.height);
|
||||||
final outerRadius = _radius * (_ringRadii.isNotEmpty ? _ringRadii.last : 1.0);
|
final outerRadius = _radius;
|
||||||
final radiusHandleX = _centerX + outerRadius * minDim / size.width;
|
final radiusHandleX = _centerX + outerRadius * minDim / size.width;
|
||||||
final radiusHandleY = _centerY;
|
final radiusHandleY = _centerY;
|
||||||
final distToRadiusHandle = _distance(tapX, tapY, radiusHandleX.clamp(0.0, 1.0), radiusHandleY.clamp(0.0, 1.0));
|
final distToOuterHandle = _distance(
|
||||||
|
tapX,
|
||||||
|
tapY,
|
||||||
|
radiusHandleX.clamp(0.0, 1.0),
|
||||||
|
radiusHandleY.clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if tapping on inner radius handle (top edge of innermost circle)
|
||||||
|
final actualInnerRadius = _innerRadius;
|
||||||
|
final innerHandleX = _centerX;
|
||||||
|
final innerHandleY = _centerY - actualInnerRadius * minDim / size.height;
|
||||||
|
final distToInnerHandle = _distance(
|
||||||
|
tapX,
|
||||||
|
tapY,
|
||||||
|
innerHandleX.clamp(0.0, 1.0),
|
||||||
|
innerHandleY.clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increase touch target size slightly for handles
|
||||||
if (distToCenter < 0.05) {
|
if (distToCenter < 0.05) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDraggingCenter = true;
|
_isDraggingCenter = true;
|
||||||
});
|
});
|
||||||
} else if (distToRadiusHandle < 0.05) {
|
} else if (distToOuterHandle < 0.05) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDraggingRadius = true;
|
_isDraggingRadius = true;
|
||||||
});
|
});
|
||||||
|
} else if (distToInnerHandle < 0.05) {
|
||||||
|
setState(() {
|
||||||
|
_isDraggingInnerRadius = true;
|
||||||
|
});
|
||||||
} else if (distToCenter < _radius + 0.02) {
|
} else if (distToCenter < _radius + 0.02) {
|
||||||
// Tapping inside the target - move center
|
// Tapping inside the target - move center
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -143,19 +207,36 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
_centerX = _centerX + deltaX;
|
_centerX = _centerX + deltaX;
|
||||||
_centerY = _centerY + deltaY;
|
_centerY = _centerY + deltaY;
|
||||||
} else if (_isDraggingRadius) {
|
} else if (_isDraggingRadius) {
|
||||||
// Adjust outer radius (scales all rings proportionally)
|
// Adjust outer radius
|
||||||
final newRadius = _radius + deltaX * (size.width / minDim);
|
final newRadius = _radius + deltaX * (size.width / minDim);
|
||||||
_radius = newRadius.clamp(0.05, 3.0);
|
_radius = newRadius.clamp(math.max(0.05, _innerRadius + 0.01), 3.0);
|
||||||
|
_initRingRadii(); // Recalculate linear separation
|
||||||
|
} else if (_isDraggingInnerRadius) {
|
||||||
|
// Adjust inner radius (sliding up reduces Y, so deltaY is negative when growing. Thus we subtract deltaY)
|
||||||
|
final newInnerRadius = _innerRadius - deltaY * (size.height / minDim);
|
||||||
|
_innerRadius = newInnerRadius.clamp(
|
||||||
|
0.01,
|
||||||
|
math.max(0.01, _radius - 0.01),
|
||||||
|
);
|
||||||
|
_initRingRadii(); // Recalculate linear separation
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
widget.onCalibrationChanged(_centerX, _centerY, _radius, _ringCount, ringRadii: _ringRadii);
|
widget.onCalibrationChanged(
|
||||||
|
_centerX,
|
||||||
|
_centerY,
|
||||||
|
_innerRadius,
|
||||||
|
_radius,
|
||||||
|
_ringCount,
|
||||||
|
ringRadii: _ringRadii,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanEnd() {
|
void _onPanEnd() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDraggingCenter = false;
|
_isDraggingCenter = false;
|
||||||
_isDraggingRadius = false;
|
_isDraggingRadius = false;
|
||||||
|
_isDraggingInnerRadius = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,21 +251,25 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
final double centerX;
|
final double centerX;
|
||||||
final double centerY;
|
final double centerY;
|
||||||
final double radius;
|
final double radius;
|
||||||
|
final double innerRadius;
|
||||||
final int ringCount;
|
final int ringCount;
|
||||||
final List<double> ringRadii;
|
final List<double> ringRadii;
|
||||||
final TargetType targetType;
|
final TargetType targetType;
|
||||||
final bool isDraggingCenter;
|
final bool isDraggingCenter;
|
||||||
final bool isDraggingRadius;
|
final bool isDraggingRadius;
|
||||||
|
final bool isDraggingInnerRadius;
|
||||||
|
|
||||||
_CalibrationPainter({
|
_CalibrationPainter({
|
||||||
required this.centerX,
|
required this.centerX,
|
||||||
required this.centerY,
|
required this.centerY,
|
||||||
required this.radius,
|
required this.radius,
|
||||||
|
required this.innerRadius,
|
||||||
required this.ringCount,
|
required this.ringCount,
|
||||||
required this.ringRadii,
|
required this.ringRadii,
|
||||||
required this.targetType,
|
required this.targetType,
|
||||||
required this.isDraggingCenter,
|
required this.isDraggingCenter,
|
||||||
required this.isDraggingRadius,
|
required this.isDraggingRadius,
|
||||||
|
required this.isDraggingInnerRadius,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -192,6 +277,7 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
final centerPx = Offset(centerX * size.width, centerY * size.height);
|
final centerPx = Offset(centerX * size.width, centerY * size.height);
|
||||||
final minDim = size.width < size.height ? size.width : size.height;
|
final minDim = size.width < size.height ? size.width : size.height;
|
||||||
final baseRadiusPx = radius * minDim;
|
final baseRadiusPx = radius * minDim;
|
||||||
|
final innerRadiusPx = innerRadius * minDim;
|
||||||
|
|
||||||
if (targetType == TargetType.concentric) {
|
if (targetType == TargetType.concentric) {
|
||||||
_drawConcentricZones(canvas, size, centerPx, baseRadiusPx);
|
_drawConcentricZones(canvas, size, centerPx, baseRadiusPx);
|
||||||
@@ -199,17 +285,42 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
_drawSilhouetteZones(canvas, size, centerPx, baseRadiusPx);
|
_drawSilhouetteZones(canvas, size, centerPx, baseRadiusPx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fullscreen crosshairs when dragging center
|
||||||
|
if (isDraggingCenter) {
|
||||||
|
final crosshairLinePaint = Paint()
|
||||||
|
..color = AppTheme.successColor.withValues(alpha: 0.5)
|
||||||
|
..strokeWidth = 1;
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(0, centerPx.dy),
|
||||||
|
Offset(size.width, centerPx.dy),
|
||||||
|
crosshairLinePaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(centerPx.dx, 0),
|
||||||
|
Offset(centerPx.dx, size.height),
|
||||||
|
crosshairLinePaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw center handle
|
// Draw center handle
|
||||||
_drawCenterHandle(canvas, centerPx);
|
_drawCenterHandle(canvas, centerPx);
|
||||||
|
|
||||||
// Draw radius handle (for outer ring)
|
// Draw radius handle (for outer ring)
|
||||||
_drawRadiusHandle(canvas, size, centerPx, baseRadiusPx);
|
_drawRadiusHandle(canvas, size, centerPx, baseRadiusPx);
|
||||||
|
|
||||||
|
// Draw inner radius handle
|
||||||
|
_drawInnerRadiusHandle(canvas, size, centerPx, innerRadiusPx);
|
||||||
|
|
||||||
// Draw instructions
|
// Draw instructions
|
||||||
_drawInstructions(canvas, size);
|
_drawInstructions(canvas, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawConcentricZones(Canvas canvas, Size size, Offset center, double baseRadius) {
|
void _drawConcentricZones(
|
||||||
|
Canvas canvas,
|
||||||
|
Size size,
|
||||||
|
Offset center,
|
||||||
|
double baseRadius,
|
||||||
|
) {
|
||||||
// Generate colors for zones
|
// Generate colors for zones
|
||||||
List<Color> zoneColors = [];
|
List<Color> zoneColors = [];
|
||||||
for (int i = 0; i < ringCount; i++) {
|
for (int i = 0; i < ringCount; i++) {
|
||||||
@@ -235,7 +346,9 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
|
|
||||||
// Draw from outside to inside
|
// Draw from outside to inside
|
||||||
for (int i = ringCount - 1; i >= 0; i--) {
|
for (int i = ringCount - 1; i >= 0; i--) {
|
||||||
final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount;
|
final ringRadius = ringRadii.length > i
|
||||||
|
? ringRadii[i]
|
||||||
|
: (i + 1) / ringCount;
|
||||||
final zoneRadius = baseRadius * ringRadius;
|
final zoneRadius = baseRadius * ringRadius;
|
||||||
|
|
||||||
zonePaint.color = zoneColors[i];
|
zonePaint.color = zoneColors[i];
|
||||||
@@ -244,12 +357,12 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw zone labels (only if within visible area)
|
// Draw zone labels (only if within visible area)
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int i = 0; i < ringCount; i++) {
|
for (int i = 0; i < ringCount; i++) {
|
||||||
final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount;
|
final ringRadius = ringRadii.length > i
|
||||||
|
? ringRadii[i]
|
||||||
|
: (i + 1) / ringCount;
|
||||||
final prevRingRadius = i > 0
|
final prevRingRadius = i > 0
|
||||||
? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount)
|
? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
@@ -268,9 +381,7 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
color: Colors.white.withValues(alpha: 0.9),
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
shadows: const [
|
shadows: const [Shadow(color: Colors.black, blurRadius: 2)],
|
||||||
Shadow(color: Colors.black, blurRadius: 2),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
textPainter.layout();
|
textPainter.layout();
|
||||||
@@ -278,14 +389,24 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
// Draw label on the right side of each zone
|
// Draw label on the right side of each zone
|
||||||
final labelY = center.dy - textPainter.height / 2;
|
final labelY = center.dy - textPainter.height / 2;
|
||||||
if (labelY >= 0 && labelY <= size.height) {
|
if (labelY >= 0 && labelY <= size.height) {
|
||||||
textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY));
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(labelX - textPainter.width / 2, labelY),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawSilhouetteZones(Canvas canvas, Size size, Offset center, double radius) {
|
void _drawSilhouetteZones(
|
||||||
|
Canvas canvas,
|
||||||
|
Size size,
|
||||||
|
Offset center,
|
||||||
|
double radius,
|
||||||
|
) {
|
||||||
// Simplified silhouette zones
|
// Simplified silhouette zones
|
||||||
final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2;
|
final paint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 2;
|
||||||
|
|
||||||
// Draw silhouette outline (simplified as rectangle for now)
|
// Draw silhouette outline (simplified as rectangle for now)
|
||||||
final silhouetteWidth = radius * 0.8;
|
final silhouetteWidth = radius * 0.8;
|
||||||
@@ -293,7 +414,11 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
|
|
||||||
paint.color = Colors.green.withValues(alpha: 0.5);
|
paint.color = Colors.green.withValues(alpha: 0.5);
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromCenter(center: center, width: silhouetteWidth, height: silhouetteHeight),
|
Rect.fromCenter(
|
||||||
|
center: center,
|
||||||
|
width: silhouetteWidth,
|
||||||
|
height: silhouetteHeight,
|
||||||
|
),
|
||||||
paint,
|
paint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -316,17 +441,36 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
final crossPaint = Paint()
|
final crossPaint = Paint()
|
||||||
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor
|
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor
|
||||||
..strokeWidth = 2;
|
..strokeWidth = 2;
|
||||||
canvas.drawLine(Offset(center.dx - 20, center.dy), Offset(center.dx - 8, center.dy), crossPaint);
|
canvas.drawLine(
|
||||||
canvas.drawLine(Offset(center.dx + 8, center.dy), Offset(center.dx + 20, center.dy), crossPaint);
|
Offset(center.dx - 20, center.dy),
|
||||||
canvas.drawLine(Offset(center.dx, center.dy - 20), Offset(center.dx, center.dy - 8), crossPaint);
|
Offset(center.dx - 8, center.dy),
|
||||||
canvas.drawLine(Offset(center.dx, center.dy + 8), Offset(center.dx, center.dy + 20), crossPaint);
|
crossPaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(center.dx + 8, center.dy),
|
||||||
|
Offset(center.dx + 20, center.dy),
|
||||||
|
crossPaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(center.dx, center.dy - 20),
|
||||||
|
Offset(center.dx, center.dy - 8),
|
||||||
|
crossPaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(center.dx, center.dy + 8),
|
||||||
|
Offset(center.dx, center.dy + 20),
|
||||||
|
crossPaint,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawRadiusHandle(Canvas canvas, Size size, Offset center, double baseRadius) {
|
void _drawRadiusHandle(
|
||||||
|
Canvas canvas,
|
||||||
|
Size size,
|
||||||
|
Offset center,
|
||||||
|
double baseRadius,
|
||||||
|
) {
|
||||||
// Radius handle on the right edge of the outermost ring
|
// Radius handle on the right edge of the outermost ring
|
||||||
final outerRingRadius = ringRadii.isNotEmpty ? ringRadii.last : 1.0;
|
final actualHandleX = center.dx + baseRadius;
|
||||||
final actualRadius = baseRadius * outerRingRadius;
|
|
||||||
final actualHandleX = center.dx + actualRadius;
|
|
||||||
final clampedHandleX = actualHandleX.clamp(20.0, size.width - 20);
|
final clampedHandleX = actualHandleX.clamp(20.0, size.width - 20);
|
||||||
final clampedHandleY = center.dy.clamp(20.0, size.height - 20);
|
final clampedHandleY = center.dy.clamp(20.0, size.height - 20);
|
||||||
final handlePos = Offset(clampedHandleX, clampedHandleY);
|
final handlePos = Offset(clampedHandleX, clampedHandleY);
|
||||||
@@ -376,7 +520,7 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
// Label
|
// Label
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
text: const TextSpan(
|
text: const TextSpan(
|
||||||
text: 'RAYON',
|
text: 'EXT.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
@@ -392,6 +536,78 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawInnerRadiusHandle(
|
||||||
|
Canvas canvas,
|
||||||
|
Size size,
|
||||||
|
Offset center,
|
||||||
|
double innerRadiusPx,
|
||||||
|
) {
|
||||||
|
// Inner radius handle on the top edge of the innermost ring
|
||||||
|
final actualHandleY = center.dy - innerRadiusPx;
|
||||||
|
final clampedHandleX = center.dx.clamp(20.0, size.width - 20);
|
||||||
|
final clampedHandleY = actualHandleY.clamp(20.0, size.height - 20);
|
||||||
|
final handlePos = Offset(clampedHandleX, clampedHandleY);
|
||||||
|
|
||||||
|
final isClamped = actualHandleY < 20.0;
|
||||||
|
|
||||||
|
final paint = Paint()
|
||||||
|
..color = isDraggingInnerRadius
|
||||||
|
? AppTheme.successColor
|
||||||
|
: (isClamped ? Colors.orange : Colors.purpleAccent)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
// Draw handle
|
||||||
|
canvas.drawCircle(handlePos, 14, paint);
|
||||||
|
|
||||||
|
// Up/Down arrow indicators
|
||||||
|
final arrowPaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..strokeWidth = 2
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
// Up arrow
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(handlePos.dx, handlePos.dy - 4),
|
||||||
|
Offset(handlePos.dx - 4, handlePos.dy - 8),
|
||||||
|
arrowPaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(handlePos.dx, handlePos.dy - 4),
|
||||||
|
Offset(handlePos.dx + 4, handlePos.dy - 8),
|
||||||
|
arrowPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Down arrow
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(handlePos.dx, handlePos.dy + 4),
|
||||||
|
Offset(handlePos.dx - 4, handlePos.dy + 8),
|
||||||
|
arrowPaint,
|
||||||
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(handlePos.dx, handlePos.dy + 4),
|
||||||
|
Offset(handlePos.dx + 4, handlePos.dy + 8),
|
||||||
|
arrowPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: const TextSpan(
|
||||||
|
text: 'INT.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(handlePos.dx - textPainter.width / 2, handlePos.dy - 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _drawInstructions(Canvas canvas, Size size) {
|
void _drawInstructions(Canvas canvas, Size size) {
|
||||||
const instruction = 'Deplacez le centre ou ajustez le rayon';
|
const instruction = 'Deplacez le centre ou ajustez le rayon';
|
||||||
|
|
||||||
@@ -418,9 +634,11 @@ class _CalibrationPainter extends CustomPainter {
|
|||||||
return centerX != oldDelegate.centerX ||
|
return centerX != oldDelegate.centerX ||
|
||||||
centerY != oldDelegate.centerY ||
|
centerY != oldDelegate.centerY ||
|
||||||
radius != oldDelegate.radius ||
|
radius != oldDelegate.radius ||
|
||||||
|
innerRadius != oldDelegate.innerRadius ||
|
||||||
ringCount != oldDelegate.ringCount ||
|
ringCount != oldDelegate.ringCount ||
|
||||||
isDraggingCenter != oldDelegate.isDraggingCenter ||
|
isDraggingCenter != oldDelegate.isDraggingCenter ||
|
||||||
isDraggingRadius != oldDelegate.isDraggingRadius ||
|
isDraggingRadius != oldDelegate.isDraggingRadius ||
|
||||||
|
isDraggingInnerRadius != oldDelegate.isDraggingInnerRadius ||
|
||||||
ringRadii != oldDelegate.ringRadii;
|
ringRadii != oldDelegate.ringRadii;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
final double? groupingDiameter;
|
final double? groupingDiameter;
|
||||||
final List<Shot>? referenceImpacts;
|
final List<Shot>? referenceImpacts;
|
||||||
final double zoomScale;
|
final double zoomScale;
|
||||||
|
final bool showRings;
|
||||||
|
|
||||||
const TargetOverlay({
|
const TargetOverlay({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,6 +43,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
this.groupingDiameter,
|
this.groupingDiameter,
|
||||||
this.referenceImpacts,
|
this.referenceImpacts,
|
||||||
this.zoomScale = 1.0,
|
this.zoomScale = 1.0,
|
||||||
|
this.showRings = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -72,6 +74,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
groupingDiameter: groupingDiameter,
|
groupingDiameter: groupingDiameter,
|
||||||
referenceImpacts: referenceImpacts,
|
referenceImpacts: referenceImpacts,
|
||||||
zoomScale: zoomScale,
|
zoomScale: zoomScale,
|
||||||
|
showRings: showRings,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: shots.map((shot) {
|
children: shots.map((shot) {
|
||||||
@@ -132,6 +135,7 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
final double? groupingDiameter;
|
final double? groupingDiameter;
|
||||||
final List<Shot>? referenceImpacts;
|
final List<Shot>? referenceImpacts;
|
||||||
final double zoomScale;
|
final double zoomScale;
|
||||||
|
final bool showRings;
|
||||||
|
|
||||||
_TargetOverlayPainter({
|
_TargetOverlayPainter({
|
||||||
required this.shots,
|
required this.shots,
|
||||||
@@ -146,12 +150,15 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
this.groupingDiameter,
|
this.groupingDiameter,
|
||||||
this.referenceImpacts,
|
this.referenceImpacts,
|
||||||
this.zoomScale = 1.0,
|
this.zoomScale = 1.0,
|
||||||
|
this.showRings = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
// Draw target center indicator
|
// Draw target center indicator
|
||||||
|
if (showRings) {
|
||||||
_drawTargetCenter(canvas, size);
|
_drawTargetCenter(canvas, size);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw grouping circle
|
// Draw grouping circle
|
||||||
if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) {
|
if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) {
|
||||||
@@ -371,6 +378,7 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
groupingCenterY != oldDelegate.groupingCenterY ||
|
groupingCenterY != oldDelegate.groupingCenterY ||
|
||||||
groupingDiameter != oldDelegate.groupingDiameter ||
|
groupingDiameter != oldDelegate.groupingDiameter ||
|
||||||
referenceImpacts != oldDelegate.referenceImpacts ||
|
referenceImpacts != oldDelegate.referenceImpacts ||
|
||||||
zoomScale != oldDelegate.zoomScale;
|
zoomScale != oldDelegate.zoomScale ||
|
||||||
|
showRings != oldDelegate.showRings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'services/target_detection_service.dart';
|
|||||||
import 'services/score_calculator_service.dart';
|
import 'services/score_calculator_service.dart';
|
||||||
import 'services/grouping_analyzer_service.dart';
|
import 'services/grouping_analyzer_service.dart';
|
||||||
import 'services/image_processing_service.dart';
|
import 'services/image_processing_service.dart';
|
||||||
|
import 'services/yolo_impact_detection_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -33,9 +34,13 @@ void main() async {
|
|||||||
Provider<ImageProcessingService>(
|
Provider<ImageProcessingService>(
|
||||||
create: (_) => ImageProcessingService(),
|
create: (_) => ImageProcessingService(),
|
||||||
),
|
),
|
||||||
|
Provider<YOLOImpactDetectionService>(
|
||||||
|
create: (_) => YOLOImpactDetectionService(),
|
||||||
|
),
|
||||||
Provider<TargetDetectionService>(
|
Provider<TargetDetectionService>(
|
||||||
create: (context) => TargetDetectionService(
|
create: (context) => TargetDetectionService(
|
||||||
imageProcessingService: context.read<ImageProcessingService>(),
|
imageProcessingService: context.read<ImageProcessingService>(),
|
||||||
|
yoloService: context.read<YOLOImpactDetectionService>(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Provider<ScoreCalculatorService>(
|
Provider<ScoreCalculatorService>(
|
||||||
@@ -44,9 +49,7 @@ void main() async {
|
|||||||
Provider<GroupingAnalyzerService>(
|
Provider<GroupingAnalyzerService>(
|
||||||
create: (_) => GroupingAnalyzerService(),
|
create: (_) => GroupingAnalyzerService(),
|
||||||
),
|
),
|
||||||
Provider<SessionRepository>(
|
Provider<SessionRepository>(create: (_) => SessionRepository()),
|
||||||
create: (_) => SessionRepository(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: const BullyApp(),
|
child: const BullyApp(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -676,4 +676,399 @@ class DistortionCorrectionService {
|
|||||||
points[2] = br;
|
points[2] = br;
|
||||||
points[3] = bl;
|
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<String> 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<cv.Point> 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<cv.Point> 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) {
|
||||||
|
print('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<String> 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<cv.RotatedRect> 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<cv.RotatedRect> 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) {
|
||||||
|
print(
|
||||||
|
"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<cv.Point2f> srcPointsList = [];
|
||||||
|
List<cv.Point2f> 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) {
|
||||||
|
print('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<String> 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) {
|
||||||
|
print(
|
||||||
|
"Aucun papier quadrilatère détecté, on utilise les cercles à la place.",
|
||||||
|
);
|
||||||
|
return await correctPerspectiveUsingCircles(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to List<cv.Point>
|
||||||
|
final List<cv.Point> 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<cv.Point> 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) {
|
||||||
|
print('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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class OpenCVImpactDetectionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final contours = contoursResult.$1;
|
final contours = contoursResult.$1;
|
||||||
// hierarchy is item2
|
// hierarchy is $2
|
||||||
|
|
||||||
for (int i = 0; i < contours.length; i++) {
|
for (int i = 0; i < contours.length; i++) {
|
||||||
final contour = contours[i];
|
final contour = contours[i];
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ class OpenCVTargetService {
|
|||||||
final circles = cv.HoughCircles(
|
final circles = cv.HoughCircles(
|
||||||
blurred,
|
blurred,
|
||||||
cv.HOUGH_GRADIENT,
|
cv.HOUGH_GRADIENT,
|
||||||
1, // dp: Inverse ratio of the accumulator resolution to the image resolution
|
1, // dp
|
||||||
(img.rows / 8)
|
(img.rows / 16)
|
||||||
.toDouble(), // minDist: Minimum distance between the centers of the detected circles
|
.toDouble(), // minDist decreased to allow more rings in same general area
|
||||||
param1: 100, // param1: Gradient value for Canny edge detection
|
param1: 100, // Canny edge detection
|
||||||
param2:
|
param2:
|
||||||
30, // param2: Accumulator threshold for the circle centers at the detection stage
|
60, // Accumulator threshold (higher = fewer false circles, more accurate)
|
||||||
minRadius: img.cols ~/ 20, // minRadius
|
minRadius: img.cols ~/ 20,
|
||||||
maxRadius: img.cols ~/ 2, // maxRadius
|
maxRadius: img.cols ~/ 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
// HoughCircles returns a Mat of shape (1, N, 3) where N is number of circles.
|
// HoughCircles returns a Mat of shape (1, N, 3) where N is number of circles.
|
||||||
@@ -79,8 +79,8 @@ class OpenCVTargetService {
|
|||||||
cv.HOUGH_GRADIENT,
|
cv.HOUGH_GRADIENT,
|
||||||
1,
|
1,
|
||||||
(img.rows / 8).toDouble(),
|
(img.rows / 8).toDouble(),
|
||||||
param1: 50,
|
param1: 100,
|
||||||
param2: 20,
|
param2: 40,
|
||||||
minRadius: img.cols ~/ 20,
|
minRadius: img.cols ~/ 20,
|
||||||
maxRadius: img.cols ~/ 2,
|
maxRadius: img.cols ~/ 2,
|
||||||
);
|
);
|
||||||
@@ -137,18 +137,22 @@ class OpenCVTargetService {
|
|||||||
for (final circle in detected) {
|
for (final circle in detected) {
|
||||||
bool added = false;
|
bool added = false;
|
||||||
for (final cluster in clusters) {
|
for (final cluster in clusters) {
|
||||||
// Check distance to cluster center (average of existing)
|
// Calculate the actual center of the cluster based on the smallest circle (the likely bullseye)
|
||||||
double clusterX = 0;
|
double clusterCenterX = cluster.first.x;
|
||||||
double clusterY = 0;
|
double clusterCenterY = cluster.first.y;
|
||||||
|
double minRadiusInCluster = cluster.first.r;
|
||||||
|
|
||||||
for (final c in cluster) {
|
for (final c in cluster) {
|
||||||
clusterX += c.x;
|
if (c.r < minRadiusInCluster) {
|
||||||
clusterY += c.y;
|
minRadiusInCluster = c.r;
|
||||||
|
clusterCenterX = c.x;
|
||||||
|
clusterCenterY = c.y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
clusterX /= cluster.length;
|
|
||||||
clusterY /= cluster.length;
|
|
||||||
|
|
||||||
final dist = math.sqrt(
|
final dist = math.sqrt(
|
||||||
math.pow(circle.x - clusterX, 2) + math.pow(circle.y - clusterY, 2),
|
math.pow(circle.x - clusterCenterX, 2) +
|
||||||
|
math.pow(circle.y - clusterCenterY, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (dist < tolerance) {
|
if (dist < tolerance) {
|
||||||
@@ -171,10 +175,10 @@ class OpenCVTargetService {
|
|||||||
|
|
||||||
for (final cluster in clusters) {
|
for (final cluster in clusters) {
|
||||||
// Score calculation
|
// Score calculation
|
||||||
// Base score = number of circles * 10
|
// Base score = number of circles squared (heavily favor concentric rings)
|
||||||
double score = cluster.length * 10.0;
|
double score = math.pow(cluster.length, 2).toDouble() * 10.0;
|
||||||
|
|
||||||
// Penalize distance from center
|
// Small penalty for distance from center (only as tie-breaker)
|
||||||
double cx = 0, cy = 0;
|
double cx = 0, cy = 0;
|
||||||
for (final c in cluster) {
|
for (final c in cluster) {
|
||||||
cx += c.x;
|
cx += c.x;
|
||||||
@@ -188,7 +192,8 @@ class OpenCVTargetService {
|
|||||||
);
|
);
|
||||||
final relDist = distFromCenter / math.min(width, height);
|
final relDist = distFromCenter / math.min(width, height);
|
||||||
|
|
||||||
score -= relDist * 5.0; // Moderate penalty for off-center
|
score -=
|
||||||
|
relDist * 2.0; // Very minor penalty so we don't snap to screen center
|
||||||
|
|
||||||
// Penalize very small clusters if they are just noise
|
// Penalize very small clusters if they are just noise
|
||||||
// (Optional: check if radii are somewhat distributed?)
|
// (Optional: check if radii are somewhat distributed?)
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import 'dart:math' as math;
|
|||||||
import '../data/models/target_type.dart';
|
import '../data/models/target_type.dart';
|
||||||
import 'image_processing_service.dart';
|
import 'image_processing_service.dart';
|
||||||
import 'opencv_impact_detection_service.dart';
|
import 'opencv_impact_detection_service.dart';
|
||||||
|
import 'yolo_impact_detection_service.dart';
|
||||||
|
|
||||||
export 'image_processing_service.dart' show ImpactDetectionSettings, ReferenceImpact, ImpactCharacteristics;
|
export 'image_processing_service.dart'
|
||||||
export 'opencv_impact_detection_service.dart' show OpenCVDetectionSettings, OpenCVDetectedImpact;
|
show ImpactDetectionSettings, ReferenceImpact, ImpactCharacteristics;
|
||||||
|
export 'opencv_impact_detection_service.dart'
|
||||||
|
show OpenCVDetectionSettings, OpenCVDetectedImpact;
|
||||||
|
|
||||||
class TargetDetectionResult {
|
class TargetDetectionResult {
|
||||||
final double centerX; // Relative (0-1)
|
final double centerX; // Relative (0-1)
|
||||||
@@ -52,18 +55,19 @@ class DetectedImpactResult {
|
|||||||
class TargetDetectionService {
|
class TargetDetectionService {
|
||||||
final ImageProcessingService _imageProcessingService;
|
final ImageProcessingService _imageProcessingService;
|
||||||
final OpenCVImpactDetectionService _opencvService;
|
final OpenCVImpactDetectionService _opencvService;
|
||||||
|
final YOLOImpactDetectionService _yoloService;
|
||||||
|
|
||||||
TargetDetectionService({
|
TargetDetectionService({
|
||||||
ImageProcessingService? imageProcessingService,
|
ImageProcessingService? imageProcessingService,
|
||||||
OpenCVImpactDetectionService? opencvService,
|
OpenCVImpactDetectionService? opencvService,
|
||||||
}) : _imageProcessingService = imageProcessingService ?? ImageProcessingService(),
|
YOLOImpactDetectionService? yoloService,
|
||||||
_opencvService = opencvService ?? OpenCVImpactDetectionService();
|
}) : _imageProcessingService =
|
||||||
|
imageProcessingService ?? ImageProcessingService(),
|
||||||
|
_opencvService = opencvService ?? OpenCVImpactDetectionService(),
|
||||||
|
_yoloService = yoloService ?? YOLOImpactDetectionService();
|
||||||
|
|
||||||
/// Detect target and impacts from an image file
|
/// Detect target and impacts from an image file
|
||||||
TargetDetectionResult detectTarget(
|
TargetDetectionResult detectTarget(String imagePath, TargetType targetType) {
|
||||||
String imagePath,
|
|
||||||
TargetType targetType,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
// Detect main target
|
// Detect main target
|
||||||
final mainTarget = _imageProcessingService.detectMainTarget(imagePath);
|
final mainTarget = _imageProcessingService.detectMainTarget(imagePath);
|
||||||
@@ -84,7 +88,13 @@ class TargetDetectionService {
|
|||||||
// Convert impacts to relative coordinates and calculate scores
|
// Convert impacts to relative coordinates and calculate scores
|
||||||
final detectedImpacts = impacts.map((impact) {
|
final detectedImpacts = impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScore(impact.x, impact.y, centerX, centerY, radius)
|
? _calculateConcentricScore(
|
||||||
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -177,7 +187,13 @@ class TargetDetectionService {
|
|||||||
return impacts.map((impact) {
|
return impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScoreWithRings(
|
? _calculateConcentricScoreWithRings(
|
||||||
impact.x, impact.y, centerX, centerY, radius, ringCount)
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
ringCount,
|
||||||
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -221,7 +237,10 @@ class TargetDetectionService {
|
|||||||
String imagePath,
|
String imagePath,
|
||||||
List<ReferenceImpact> references,
|
List<ReferenceImpact> references,
|
||||||
) {
|
) {
|
||||||
return _imageProcessingService.analyzeReferenceImpacts(imagePath, references);
|
return _imageProcessingService.analyzeReferenceImpacts(
|
||||||
|
imagePath,
|
||||||
|
references,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect impacts based on reference characteristics (calibrated detection)
|
/// Detect impacts based on reference characteristics (calibrated detection)
|
||||||
@@ -245,7 +264,13 @@ class TargetDetectionService {
|
|||||||
return impacts.map((impact) {
|
return impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScoreWithRings(
|
? _calculateConcentricScoreWithRings(
|
||||||
impact.x, impact.y, centerX, centerY, radius, ringCount)
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
ringCount,
|
||||||
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -283,7 +308,13 @@ class TargetDetectionService {
|
|||||||
return impacts.map((impact) {
|
return impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScoreWithRings(
|
? _calculateConcentricScoreWithRings(
|
||||||
impact.x, impact.y, centerX, centerY, radius, ringCount)
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
ringCount,
|
||||||
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -315,9 +346,7 @@ class TargetDetectionService {
|
|||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Convertir les références au format OpenCV
|
// Convertir les références au format OpenCV
|
||||||
final refPoints = references
|
final refPoints = references.map((r) => (x: r.x, y: r.y)).toList();
|
||||||
.map((r) => (x: r.x, y: r.y))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final impacts = _opencvService.detectFromReferences(
|
final impacts = _opencvService.detectFromReferences(
|
||||||
imagePath,
|
imagePath,
|
||||||
@@ -328,7 +357,13 @@ class TargetDetectionService {
|
|||||||
return impacts.map((impact) {
|
return impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScoreWithRings(
|
? _calculateConcentricScoreWithRings(
|
||||||
impact.x, impact.y, centerX, centerY, radius, ringCount)
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
ringCount,
|
||||||
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -343,4 +378,46 @@ class TargetDetectionService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Détecte les impacts en utilisant YOLOv8
|
||||||
|
Future<List<DetectedImpactResult>> detectImpactsWithYOLO(
|
||||||
|
String imagePath,
|
||||||
|
TargetType targetType,
|
||||||
|
double centerX,
|
||||||
|
double centerY,
|
||||||
|
double radius,
|
||||||
|
int ringCount,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final impacts = await _yoloService.detectImpacts(imagePath);
|
||||||
|
|
||||||
|
return impacts.map((impact) {
|
||||||
|
// 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,
|
||||||
|
y: impact.y,
|
||||||
|
radius: impact.radius,
|
||||||
|
suggestedScore: score,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('Erreur détection YOLOv8: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
lib/services/yolo_impact_detection_service.dart
Normal file
217
lib/services/yolo_impact_detection_service.dart
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:tflite_flutter/tflite_flutter.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'target_detection_service.dart';
|
||||||
|
|
||||||
|
class YOLOImpactDetectionService {
|
||||||
|
Interpreter? _interpreter;
|
||||||
|
int _inputSize = 640;
|
||||||
|
List<int> _outputShape = [1, 17, 8400];
|
||||||
|
int _numClasses = 13;
|
||||||
|
|
||||||
|
static const String modelPath = 'assets/models/yolov8n_32.tflite';
|
||||||
|
static const String labelsPath = 'assets/models/labels.txt';
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_interpreter != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
_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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<DetectedImpactResult>> detectImpacts(String imagePath) async {
|
||||||
|
if (_interpreter == null) await init();
|
||||||
|
if (_interpreter == null) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bytes = File(imagePath).readAsBytesSync();
|
||||||
|
final decodedImage = img.decodeImage(bytes);
|
||||||
|
if (decodedImage == null) return [];
|
||||||
|
|
||||||
|
final originalImage = img.bakeOrientation(decodedImage);
|
||||||
|
|
||||||
|
final resizedImage = img.copyResize(
|
||||||
|
originalImage,
|
||||||
|
width: _inputSize,
|
||||||
|
height: _inputSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
var input = _imageToByteListFloat32(resizedImage, _inputSize);
|
||||||
|
|
||||||
|
// Allocate output buffer dynamically
|
||||||
|
var output = List<double>.filled(
|
||||||
|
_outputShape.fold(1, (a, b) => a * b),
|
||||||
|
0
|
||||||
|
).reshape(_outputShape);
|
||||||
|
|
||||||
|
_interpreter!.run(input, output);
|
||||||
|
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DetectedImpactResult> _processOutput(
|
||||||
|
List<List<double>> output,
|
||||||
|
int imgWidth,
|
||||||
|
int imgHeight,
|
||||||
|
) {
|
||||||
|
final List<_Detection> candidates = [];
|
||||||
|
const double threshold = 0.25;
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
// 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: maxConfidence,
|
||||||
|
classIndex: classIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_Detection> suppressed = _nms(candidates);
|
||||||
|
|
||||||
|
return suppressed
|
||||||
|
.map(
|
||||||
|
(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_Detection> _nms(List<_Detection> detections) {
|
||||||
|
if (detections.isEmpty) return [];
|
||||||
|
|
||||||
|
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
|
||||||
|
|
||||||
|
final List<_Detection> selected = [];
|
||||||
|
final List<bool> active = List.filled(detections.length, true);
|
||||||
|
|
||||||
|
for (int i = 0; i < detections.length; i++) {
|
||||||
|
if (!active[i]) continue;
|
||||||
|
|
||||||
|
selected.add(detections[i]);
|
||||||
|
|
||||||
|
for (int j = i + 1; j < detections.length; j++) {
|
||||||
|
if (!active[j]) continue;
|
||||||
|
|
||||||
|
if (_iou(detections[i], detections[j]) > 0.45) {
|
||||||
|
active[j] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _iou(_Detection a, _Detection b) {
|
||||||
|
final double areaA = a.w * a.h;
|
||||||
|
final double areaB = b.w * b.h;
|
||||||
|
|
||||||
|
final double x1 = math.max(a.x - a.w / 2, b.x - b.w / 2);
|
||||||
|
final double y1 = math.max(a.y - a.h / 2, b.y - b.h / 2);
|
||||||
|
final double x2 = math.min(a.x + a.w / 2, b.x + b.w / 2);
|
||||||
|
final double y2 = math.min(a.y + a.h / 2, b.y + b.h / 2);
|
||||||
|
|
||||||
|
final double intersection = math.max(0.0, x2 - x1) * math.max(0.0, y2 - y1);
|
||||||
|
return intersection / (areaA + areaB - intersection);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _imageToByteListFloat32(img.Image image, int inputSize) {
|
||||||
|
var convertedBytes = Float32List(1 * inputSize * inputSize * 3);
|
||||||
|
var buffer = Float32List.view(convertedBytes.buffer);
|
||||||
|
int pixelIndex = 0;
|
||||||
|
for (int i = 0; i < inputSize; i++) {
|
||||||
|
for (int j = 0; j < inputSize; j++) {
|
||||||
|
var pixel = image.getPixel(j, i);
|
||||||
|
buffer[pixelIndex++] = (pixel.r / 255.0);
|
||||||
|
buffer[pixelIndex++] = (pixel.g / 255.0);
|
||||||
|
buffer[pixelIndex++] = (pixel.b / 255.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return convertedBytes.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
tflite_flutter
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -536,6 +536,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -653,6 +661,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.9"
|
||||||
|
tflite_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: tflite_flutter
|
||||||
|
sha256: "48e6fde2ad97162bb66a16a142f4c4698add9e8cd397ce9d1cc7451b55537ac1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ dependencies:
|
|||||||
# Image processing for impact detection
|
# Image processing for impact detection
|
||||||
image: ^4.1.7
|
image: ^4.1.7
|
||||||
|
|
||||||
|
# Machine Learning for YOLOv8
|
||||||
|
tflite_flutter: ^0.11.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -87,7 +90,8 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# 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_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
|||||||
12
tests/find_homography_test.dart
Normal file
12
tests/find_homography_test.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
7
tests/opencv_quad_test.dart
Normal file
7
tests/opencv_quad_test.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:opencv_dart/opencv_dart.dart' as cv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
print(cv.approxPolyDP);
|
||||||
|
print(cv.arcLength);
|
||||||
|
print(cv.contourArea);
|
||||||
|
}
|
||||||
5
tests/test_homography.dart
Normal file
5
tests/test_homography.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:opencv_dart/opencv_dart.dart' as cv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
print(cv.findHomography);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
tflite_flutter
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user