fix(crop): synchroniser les calculs avec le mode BoxFit.cover et activer la découpe physique
This commit is contained in:
@@ -162,8 +162,8 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect target and impacts
|
// CORRECTION PARALLÈLE : Changement de la méthode pour l'Isolate asynchrone
|
||||||
final result = _detectionService.detectTarget(imagePath, targetType);
|
final result = await _detectionService.detectTargetAsync(imagePath, targetType);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
_state = AnalysisState.error;
|
_state = AnalysisState.error;
|
||||||
|
|||||||
@@ -225,12 +225,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
return Transform(
|
return Transform(
|
||||||
transform: Matrix4.identity()
|
transform: Matrix4.identity()
|
||||||
..setTranslationRaw(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0, 0.0)
|
..setTranslationRaw(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0, 0.0)
|
||||||
..scale(1.0, 1.0) // Zoom ignoré au plotting pour rester en vue globale
|
..scale(1.0, 1.0)
|
||||||
..rotateZ((provider.cropRotation) * (math.pi / 180)), // FIX : Utilisation du provider dynamique
|
..rotateZ((provider.cropRotation) * (math.pi / 180)),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(provider.imagePath!),
|
File(provider.imagePath!),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover, // CORRECTION : BoxFit.cover pour s'aligner sur le Crop !
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
Image.file(
|
Image.file(
|
||||||
File(provider.imagePath!),
|
File(provider.imagePath!),
|
||||||
key: _imageKey,
|
key: _imageKey,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
TargetOverlay(
|
TargetOverlay(
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w600),
|
style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_rotation.toStringAsFixed(1)}°', // Affiche désormais le dixième de degré pour la précision
|
'${_rotation.toStringAsFixed(1)}°',
|
||||||
style: const TextStyle(color: Color(0xFF00FF00), fontWeight: FontWeight.bold),
|
style: const TextStyle(color: Color(0xFF00FF00), fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -167,9 +167,9 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: _rotation,
|
value: _rotation,
|
||||||
min: -15.0, // Pivot maximum à gauche bridé à 15°
|
min: -15.0,
|
||||||
max: 15.0, // Pivot maximum à droite bridé à 15°
|
max: 15.0,
|
||||||
divisions: 300, // 300 divisions pour obtenir un cran ultra-précis tous les 0,1°
|
divisions: 300,
|
||||||
label: '${_rotation.toStringAsFixed(1)}°',
|
label: '${_rotation.toStringAsFixed(1)}°',
|
||||||
activeColor: const Color(0xFF1A73E8),
|
activeColor: const Color(0xFF1A73E8),
|
||||||
inactiveColor: Colors.white12,
|
inactiveColor: Colors.white12,
|
||||||
@@ -182,7 +182,6 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
),
|
),
|
||||||
const Icon(Icons.rotate_right, color: Colors.white38, size: 20),
|
const Icon(Icons.rotate_right, color: Colors.white38, size: 20),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
// Petit bouton Reset rapide pour cette jauge
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.restart_alt, color: Colors.white54, size: 20),
|
icon: const Icon(Icons.restart_alt, color: Colors.white54, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -260,7 +259,7 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(widget.imagePath),
|
File(widget.imagePath),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
width: _viewportSize.width,
|
width: _viewportSize.width,
|
||||||
height: _viewportSize.height,
|
height: _viewportSize.height,
|
||||||
),
|
),
|
||||||
@@ -302,7 +301,6 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Zone visible : Rectangle parfait sans aucun BorderRadius
|
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: _cropSize,
|
width: _cropSize,
|
||||||
@@ -335,13 +333,14 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
final imageAspect = _imageSize!.width / _imageSize!.height;
|
final imageAspect = _imageSize!.width / _imageSize!.height;
|
||||||
final viewportAspect = _viewportSize.width / _viewportSize.height;
|
final viewportAspect = _viewportSize.width / _viewportSize.height;
|
||||||
|
|
||||||
|
// Initialisation calée sur la logique BoxFit.cover
|
||||||
double displayWidth, displayHeight;
|
double displayWidth, displayHeight;
|
||||||
if (imageAspect > viewportAspect) {
|
if (imageAspect > viewportAspect) {
|
||||||
displayWidth = _viewportSize.width;
|
|
||||||
displayHeight = _viewportSize.width / imageAspect;
|
|
||||||
} else {
|
|
||||||
displayHeight = _viewportSize.height;
|
displayHeight = _viewportSize.height;
|
||||||
displayWidth = _viewportSize.height * imageAspect;
|
displayWidth = _viewportSize.height * imageAspect;
|
||||||
|
} else {
|
||||||
|
displayWidth = _viewportSize.width;
|
||||||
|
displayHeight = _viewportSize.width / imageAspect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final minDisplayDim = math.min(displayWidth, displayHeight);
|
final minDisplayDim = math.min(displayWidth, displayHeight);
|
||||||
@@ -372,8 +371,17 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
final cropRect = _calculateCropRect();
|
final cropRect = _calculateCropRect();
|
||||||
final targetCenterX = cropRect.x + cropRect.width / 2;
|
|
||||||
final targetCenterY = cropRect.y + cropRect.height / 2;
|
// DECOUPE ET REDRESSEMENT PHYSIQUE DE L'IMAGE
|
||||||
|
final croppedImagePath = await _cropService.cropToSquare(
|
||||||
|
widget.imagePath,
|
||||||
|
cropRect,
|
||||||
|
rotationDegrees: _rotation,
|
||||||
|
);
|
||||||
|
|
||||||
|
// On recalcule le centre relatif de la cible pour l'image carrée finale (qui vaut 0.5, 0.5 par défaut)
|
||||||
|
const targetCenterX = 0.5;
|
||||||
|
const targetCenterY = 0.5;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -381,13 +389,13 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => AnalysisScreen(
|
builder: (_) => AnalysisScreen(
|
||||||
imagePath: widget.imagePath,
|
imagePath: croppedImagePath, // On passe le fichier carré proprement détouré !
|
||||||
targetType: widget.targetType,
|
targetType: widget.targetType,
|
||||||
initialCenterX: targetCenterX,
|
initialCenterX: targetCenterX,
|
||||||
initialCenterY: targetCenterY,
|
initialCenterY: targetCenterY,
|
||||||
cropScale: _scale,
|
cropScale: 1.0, // Réinitialisé car l'image physique est déjà à la bonne échelle
|
||||||
cropOffset: _offset,
|
cropOffset: Offset.zero, // Réinitialisé car l'image physique est déjà centrée
|
||||||
cropRotation: _rotation,
|
cropRotation: 0.0, // Réinitialisé car l'image physique est déjà redressée
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -395,25 +403,27 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
SnackBar(content: Text('Erreur de découpe: $e'), backgroundColor: AppTheme.errorColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calcule précisément le CropRect basé sur la géométrie BoxFit.cover
|
||||||
CropRect _calculateCropRect() {
|
CropRect _calculateCropRect() {
|
||||||
if (_imageSize == null) return const CropRect(x: 0, y: 0, width: 1, height: 1);
|
if (_imageSize == null) return const CropRect(x: 0, y: 0, width: 1, height: 1);
|
||||||
|
|
||||||
final imageAspect = _imageSize!.width / _imageSize!.height;
|
final imageAspect = _imageSize!.width / _imageSize!.height;
|
||||||
final viewportAspect = _viewportSize.width / _viewportSize.height;
|
final viewportAspect = _viewportSize.width / _viewportSize.height;
|
||||||
|
|
||||||
|
// Dimensions réelles de rendu en mode BoxFit.cover
|
||||||
double displayWidth, displayHeight;
|
double displayWidth, displayHeight;
|
||||||
if (imageAspect > viewportAspect) {
|
if (imageAspect > viewportAspect) {
|
||||||
displayWidth = _viewportSize.width;
|
|
||||||
displayHeight = _viewportSize.width / imageAspect;
|
|
||||||
} else {
|
|
||||||
displayHeight = _viewportSize.height;
|
displayHeight = _viewportSize.height;
|
||||||
displayWidth = _viewportSize.height * imageAspect;
|
displayWidth = _viewportSize.height * imageAspect;
|
||||||
|
} else {
|
||||||
|
displayWidth = _viewportSize.width;
|
||||||
|
displayHeight = _viewportSize.width / imageAspect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final scaledWidth = displayWidth * _scale;
|
final scaledWidth = displayWidth * _scale;
|
||||||
@@ -434,10 +444,10 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
final relCropHeight = _cropSize / scaledHeight;
|
final relCropHeight = _cropSize / scaledHeight;
|
||||||
|
|
||||||
return CropRect(
|
return CropRect(
|
||||||
x: relCropLeft,
|
x: relCropLeft.clamp(0.0, 1.0),
|
||||||
y: relCropTop,
|
y: relCropTop.clamp(0.0, 1.0),
|
||||||
width: relCropWidth,
|
width: relCropWidth.clamp(0.0, 1.0),
|
||||||
height: relCropHeight,
|
height: relCropHeight.clamp(0.0, 1.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ class _SessionDetailScreenState extends State<SessionDetailScreen> {
|
|||||||
if (File(currentAnalysis.imagePath).existsSync())
|
if (File(currentAnalysis.imagePath).existsSync())
|
||||||
Image.file(
|
Image.file(
|
||||||
File(currentAnalysis.imagePath),
|
File(currentAnalysis.imagePath),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
@@ -42,28 +42,40 @@ class ImageCropService {
|
|||||||
/// Taille de sortie maximale pour les images recadrées
|
/// Taille de sortie maximale pour les images recadrées
|
||||||
static const int maxOutputSize = 1024;
|
static const int maxOutputSize = 1024;
|
||||||
|
|
||||||
/// Recadre une image en format carré
|
/// Recadre une image en format carré en prenant en compte l'angle de rotation.
|
||||||
///
|
///
|
||||||
/// [sourcePath] - Chemin vers l'image source
|
/// [sourcePath] - Chemin vers l'image source
|
||||||
/// [cropRect] - Zone de recadrage normalisée (0.0 à 1.0)
|
/// [cropRect] - Zone de recadrage normalisée (0.0 à 1.0)
|
||||||
|
/// [rotationDegrees] - L'angle appliqué à la jauge haute précision du CropScreen
|
||||||
/// [outputSize] - Taille de sortie en pixels (carré)
|
/// [outputSize] - Taille de sortie en pixels (carré)
|
||||||
///
|
///
|
||||||
/// Retourne le chemin vers l'image recadrée dans le dossier temporaire
|
/// Retourne le chemin vers l'image recadrée dans le dossier temporaire
|
||||||
Future<String> cropToSquare(
|
Future<String> cropToSquare(
|
||||||
String sourcePath,
|
String sourcePath,
|
||||||
CropRect cropRect, {
|
CropRect cropRect, {
|
||||||
int outputSize = maxOutputSize,
|
double rotationDegrees = 0.0, // FIX : On intercepte la rotation ici !
|
||||||
}) async {
|
int outputSize = maxOutputSize,
|
||||||
|
}) async {
|
||||||
// Charger l'image source
|
// Charger l'image source
|
||||||
final file = File(sourcePath);
|
final file = File(sourcePath);
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
final originalImage = img.decodeImage(bytes);
|
img.Image? originalImage = img.decodeImage(bytes);
|
||||||
|
|
||||||
if (originalImage == null) {
|
if (originalImage == null) {
|
||||||
throw Exception('Impossible de décoder l\'image: $sourcePath');
|
throw Exception('Impossible de décoder l\'image: $sourcePath');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculer les coordonnées en pixels
|
// FIX CRITIQUE : Si l'utilisateur a pivoté l'image, on applique d'abord la rotation
|
||||||
|
// aux pixels bruts pour que le découpage rectiligne qui suit tape au bon endroit.
|
||||||
|
if (rotationDegrees != 0.0) {
|
||||||
|
originalImage = img.copyRotate(
|
||||||
|
originalImage,
|
||||||
|
angle: rotationDegrees,
|
||||||
|
interpolation: img.Interpolation.cubic,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer les coordonnées en pixels sur la géométrie de l'image redressée
|
||||||
final srcX = (cropRect.x * originalImage.width).round();
|
final srcX = (cropRect.x * originalImage.width).round();
|
||||||
final srcY = (cropRect.y * originalImage.height).round();
|
final srcY = (cropRect.y * originalImage.height).round();
|
||||||
final srcWidth = (cropRect.width * originalImage.width).round();
|
final srcWidth = (cropRect.width * originalImage.width).round();
|
||||||
@@ -75,7 +87,7 @@ class ImageCropService {
|
|||||||
final clampedWidth = math.min(srcWidth, originalImage.width - clampedX);
|
final clampedWidth = math.min(srcWidth, originalImage.width - clampedX);
|
||||||
final clampedHeight = math.min(srcHeight, originalImage.height - clampedY);
|
final clampedHeight = math.min(srcHeight, originalImage.height - clampedY);
|
||||||
|
|
||||||
// Recadrer l'image
|
// Recadrer l'image redressée
|
||||||
img.Image cropped = img.copyCrop(
|
img.Image cropped = img.copyCrop(
|
||||||
originalImage,
|
originalImage,
|
||||||
x: clampedX,
|
x: clampedX,
|
||||||
@@ -87,10 +99,10 @@ class ImageCropService {
|
|||||||
// Redimensionner à la taille de sortie si nécessaire
|
// Redimensionner à la taille de sortie si nécessaire
|
||||||
if (cropped.width != outputSize || cropped.height != outputSize) {
|
if (cropped.width != outputSize || cropped.height != outputSize) {
|
||||||
cropped = img.copyResize(
|
cropped = img.copyResize(
|
||||||
cropped,
|
cropped,
|
||||||
width: outputSize,
|
width: outputSize,
|
||||||
height: outputSize,
|
height: outputSize,
|
||||||
interpolation: img.Interpolation.cubic,
|
interpolation: img.Interpolation.cubic,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,26 +118,18 @@ class ImageCropService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calcule la zone de recadrage carrée maximale centrée sur l'image
|
/// Calcule la zone de recadrage carrée maximale centrée sur l'image
|
||||||
///
|
|
||||||
/// [imageWidth] - Largeur de l'image en pixels
|
|
||||||
/// [imageHeight] - Hauteur de l'image en pixels
|
|
||||||
///
|
|
||||||
/// Retourne un CropRect normalisé pour un carré centré
|
|
||||||
CropRect getDefaultSquareCrop(int imageWidth, int imageHeight) {
|
CropRect getDefaultSquareCrop(int imageWidth, int imageHeight) {
|
||||||
final aspectRatio = imageWidth / imageHeight;
|
final aspectRatio = imageWidth / imageHeight;
|
||||||
|
|
||||||
if (aspectRatio > 1) {
|
if (aspectRatio > 1) {
|
||||||
// Image plus large que haute - centrer horizontalement
|
|
||||||
final squareWidth = imageHeight / imageWidth;
|
final squareWidth = imageHeight / imageWidth;
|
||||||
final x = (1 - squareWidth) / 2;
|
final x = (1 - squareWidth) / 2;
|
||||||
return CropRect(x: x, y: 0, width: squareWidth, height: 1);
|
return CropRect(x: x, y: 0, width: squareWidth, height: 1);
|
||||||
} else if (aspectRatio < 1) {
|
} else if (aspectRatio < 1) {
|
||||||
// Image plus haute que large - centrer verticalement
|
|
||||||
final squareHeight = imageWidth / imageHeight;
|
final squareHeight = imageWidth / imageHeight;
|
||||||
final y = (1 - squareHeight) / 2;
|
final y = (1 - squareHeight) / 2;
|
||||||
return CropRect(x: 0, y: y, width: 1, height: squareHeight);
|
return CropRect(x: 0, y: y, width: 1, height: squareHeight);
|
||||||
} else {
|
} else {
|
||||||
// Déjà carré
|
|
||||||
return const CropRect(x: 0, y: 0, width: 1, height: 1);
|
return const CropRect(x: 0, y: 0, width: 1, height: 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +151,7 @@ class ImageCropService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignorer les erreurs de nettoyage
|
// Ignorer les erreurs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,110 @@ export 'image_processing_service.dart'
|
|||||||
export 'opencv_impact_detection_service.dart'
|
export 'opencv_impact_detection_service.dart'
|
||||||
show OpenCVDetectionSettings, OpenCVDetectedImpact;
|
show OpenCVDetectionSettings, OpenCVDetectedImpact;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STRUCT ET FONCTION GLOBALE POUR LE PARALLÉLISME (THREAD SECONDAIRE)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Conteneur de données pour envoyer les paramètres à l'Isolate d'arrière-plan.
|
||||||
|
class DetectionPayload {
|
||||||
|
final String imagePath;
|
||||||
|
final TargetType targetType;
|
||||||
|
|
||||||
|
// On recrée les instances à l'intérieur du thread isolé car les objets ne se partagent pas entre threads.
|
||||||
|
DetectionPayload({
|
||||||
|
required this.imagePath,
|
||||||
|
required this.targetType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FONCTION EXÉCUTÉE EN PARALLÈLE : Tourne sur un autre cœur du processeur.
|
||||||
|
/// L'interface graphique reste à 120 FPS et totalement fluide.
|
||||||
|
TargetDetectionResult runParallelTargetDetection(DetectionPayload payload) {
|
||||||
|
// 1. Initialisation locale des services dans le sous-thread
|
||||||
|
final imageProcessingService = ImageProcessingService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Détection de la cible principale (Calcul lourd)
|
||||||
|
final mainTarget = imageProcessingService.detectMainTarget(payload.imagePath);
|
||||||
|
|
||||||
|
double centerX = 0.5;
|
||||||
|
double centerY = 0.5;
|
||||||
|
double radius = 0.4;
|
||||||
|
|
||||||
|
if (mainTarget != null) {
|
||||||
|
centerX = mainTarget.centerX;
|
||||||
|
centerY = mainTarget.centerY;
|
||||||
|
radius = mainTarget.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Détection des impacts (Calcul lourd)
|
||||||
|
final impacts = imageProcessingService.detectImpacts(payload.imagePath);
|
||||||
|
|
||||||
|
// 4. Calcul mathématique des scores relatifs
|
||||||
|
final detectedImpacts = impacts.map((impact) {
|
||||||
|
final score = payload.targetType == TargetType.concentric
|
||||||
|
? _staticCalculateConcentricScore(
|
||||||
|
impact.x,
|
||||||
|
impact.y,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
)
|
||||||
|
: _staticCalculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
|
return DetectedImpactResult(
|
||||||
|
x: impact.x,
|
||||||
|
y: impact.y,
|
||||||
|
radius: impact.radius,
|
||||||
|
suggestedScore: score,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return TargetDetectionResult(
|
||||||
|
centerX: centerX,
|
||||||
|
centerY: centerY,
|
||||||
|
radius: radius,
|
||||||
|
impacts: detectedImpacts,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return TargetDetectionResult.error('Erreur de detection parallèle: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonctions mathématiques pures nécessaires à l'Isolate (statiques)
|
||||||
|
int _staticCalculateConcentricScore(double impactX, double impactY, double centerX, double centerY, double targetRadius) {
|
||||||
|
final dx = impactX - centerX;
|
||||||
|
final dy = impactY - centerY;
|
||||||
|
final distance = math.sqrt(dx * dx + dy * dy) / targetRadius;
|
||||||
|
if (distance <= 0.1) return 10;
|
||||||
|
if (distance <= 0.2) return 9;
|
||||||
|
if (distance <= 0.3) return 8;
|
||||||
|
if (distance <= 0.4) return 7;
|
||||||
|
if (distance <= 0.5) return 6;
|
||||||
|
if (distance <= 0.6) return 5;
|
||||||
|
if (distance <= 0.7) return 4;
|
||||||
|
if (distance <= 0.8) return 3;
|
||||||
|
if (distance <= 0.9) return 2;
|
||||||
|
if (distance <= 1.0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _staticCalculateSilhouetteScore(double impactX, double impactY, double centerX, double centerY) {
|
||||||
|
final dx = (impactX - centerX).abs();
|
||||||
|
final dy = impactY - centerY;
|
||||||
|
if (dx > 0.15) return 0;
|
||||||
|
if (dy < -0.25) return 5;
|
||||||
|
if (dy < 0.0) return 5;
|
||||||
|
if (dy < 0.15) return 4;
|
||||||
|
if (dy < 0.35) return 3;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FIN DU BLOC DE PARALLÉLISME
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TargetDetectionResult {
|
class TargetDetectionResult {
|
||||||
final double centerX; // Relative (0-1)
|
final double centerX; // Relative (0-1)
|
||||||
final double centerY; // Relative (0-1)
|
final double centerY; // Relative (0-1)
|
||||||
@@ -59,13 +163,20 @@ class TargetDetectionService {
|
|||||||
ImageProcessingService? imageProcessingService,
|
ImageProcessingService? imageProcessingService,
|
||||||
OpenCVImpactDetectionService? opencvService,
|
OpenCVImpactDetectionService? opencvService,
|
||||||
}) : _imageProcessingService =
|
}) : _imageProcessingService =
|
||||||
imageProcessingService ?? ImageProcessingService(),
|
imageProcessingService ?? ImageProcessingService(),
|
||||||
_opencvService = opencvService ?? OpenCVImpactDetectionService();
|
_opencvService = opencvService ?? OpenCVImpactDetectionService();
|
||||||
|
|
||||||
/// Detect target and impacts from an image file
|
/// Detect target and impacts from an image file ASYNCHRONOUSLY in a separate Thread.
|
||||||
|
/// CORRECTION : Utilise désormais 'compute' pour basculer en arrière-plan immédiat.
|
||||||
|
Future<TargetDetectionResult> detectTargetAsync(String imagePath, TargetType targetType) async {
|
||||||
|
final payload = DetectionPayload(imagePath: imagePath, targetType: targetType);
|
||||||
|
// Déclenche l'exécution isolée en tâche de fond
|
||||||
|
return await compute(runParallelTargetDetection, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gardée pour rétrocompatibilité synchrone si nécessaire
|
||||||
TargetDetectionResult detectTarget(String imagePath, TargetType targetType) {
|
TargetDetectionResult detectTarget(String imagePath, TargetType targetType) {
|
||||||
try {
|
try {
|
||||||
// Detect main target
|
|
||||||
final mainTarget = _imageProcessingService.detectMainTarget(imagePath);
|
final mainTarget = _imageProcessingService.detectMainTarget(imagePath);
|
||||||
|
|
||||||
double centerX = 0.5;
|
double centerX = 0.5;
|
||||||
@@ -78,19 +189,17 @@ class TargetDetectionService {
|
|||||||
radius = mainTarget.radius;
|
radius = mainTarget.radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect impacts
|
|
||||||
final impacts = _imageProcessingService.detectImpacts(imagePath);
|
final impacts = _imageProcessingService.detectImpacts(imagePath);
|
||||||
|
|
||||||
// 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(
|
? _calculateConcentricScore(
|
||||||
impact.x,
|
impact.x,
|
||||||
impact.y,
|
impact.y,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
radius,
|
radius,
|
||||||
)
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -113,18 +222,16 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int _calculateConcentricScore(
|
int _calculateConcentricScore(
|
||||||
double impactX,
|
double impactX,
|
||||||
double impactY,
|
double impactY,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double targetRadius,
|
double targetRadius,
|
||||||
) {
|
) {
|
||||||
// Calculate distance from center (normalized to target radius)
|
|
||||||
final dx = impactX - centerX;
|
final dx = impactX - centerX;
|
||||||
final dy = impactY - centerY;
|
final dy = impactY - centerY;
|
||||||
final distance = math.sqrt(dx * dx + dy * dy) / targetRadius;
|
final distance = math.sqrt(dx * dx + dy * dy) / targetRadius;
|
||||||
|
|
||||||
// Score zones (10 zones)
|
|
||||||
if (distance <= 0.1) return 10;
|
if (distance <= 0.1) return 10;
|
||||||
if (distance <= 0.2) return 9;
|
if (distance <= 0.2) return 9;
|
||||||
if (distance <= 0.3) return 8;
|
if (distance <= 0.3) return 8;
|
||||||
@@ -135,61 +242,52 @@ class TargetDetectionService {
|
|||||||
if (distance <= 0.8) return 3;
|
if (distance <= 0.8) return 3;
|
||||||
if (distance <= 0.9) return 2;
|
if (distance <= 0.9) return 2;
|
||||||
if (distance <= 1.0) return 1;
|
if (distance <= 1.0) return 1;
|
||||||
return 0; // Outside target
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _calculateSilhouetteScore(
|
int _calculateSilhouetteScore(
|
||||||
double impactX,
|
double impactX,
|
||||||
double impactY,
|
double impactY,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
) {
|
) {
|
||||||
// For silhouettes, scoring is typically based on zones
|
|
||||||
// Head and center mass = 5, body = 4, lower = 3
|
|
||||||
|
|
||||||
final dx = (impactX - centerX).abs();
|
final dx = (impactX - centerX).abs();
|
||||||
final dy = impactY - centerY;
|
final dy = impactY - centerY;
|
||||||
|
|
||||||
// Check if within silhouette bounds (approximate)
|
if (dx > 0.15) return 0;
|
||||||
if (dx > 0.15) return 0; // Too far left/right
|
if (dy < -0.25) return 5;
|
||||||
|
if (dy < 0.0) return 5;
|
||||||
|
if (dy < 0.15) return 4;
|
||||||
|
if (dy < 0.35) return 3;
|
||||||
|
|
||||||
// Vertical zones
|
return 0;
|
||||||
if (dy < -0.25) return 5; // Head zone (top)
|
|
||||||
if (dy < 0.0) return 5; // Center mass (upper body)
|
|
||||||
if (dy < 0.15) return 4; // Body
|
|
||||||
if (dy < 0.35) return 3; // Lower body
|
|
||||||
|
|
||||||
return 0; // Outside target
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect only impacts with custom settings (doesn't affect target position)
|
|
||||||
List<DetectedImpactResult> detectImpactsOnly(
|
List<DetectedImpactResult> detectImpactsOnly(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
TargetType targetType,
|
TargetType targetType,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double radius,
|
double radius,
|
||||||
int ringCount,
|
int ringCount,
|
||||||
ImpactDetectionSettings settings,
|
ImpactDetectionSettings settings,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Detect impacts with custom settings
|
|
||||||
final impacts = _imageProcessingService.detectImpactsWithSettings(
|
final impacts = _imageProcessingService.detectImpactsWithSettings(
|
||||||
imagePath,
|
imagePath,
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert impacts to relative coordinates and calculate scores
|
|
||||||
return impacts.map((impact) {
|
return impacts.map((impact) {
|
||||||
final score = targetType == TargetType.concentric
|
final score = targetType == TargetType.concentric
|
||||||
? _calculateConcentricScoreWithRings(
|
? _calculateConcentricScoreWithRings(
|
||||||
impact.x,
|
impact.x,
|
||||||
impact.y,
|
impact.y,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
radius,
|
radius,
|
||||||
ringCount,
|
ringCount,
|
||||||
)
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -205,19 +303,17 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int _calculateConcentricScoreWithRings(
|
int _calculateConcentricScoreWithRings(
|
||||||
double impactX,
|
double impactX,
|
||||||
double impactY,
|
double impactY,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double targetRadius,
|
double targetRadius,
|
||||||
int ringCount,
|
int ringCount,
|
||||||
) {
|
) {
|
||||||
// Calculate distance from center (normalized to target radius)
|
|
||||||
final dx = impactX - centerX;
|
final dx = impactX - centerX;
|
||||||
final dy = impactY - centerY;
|
final dy = impactY - centerY;
|
||||||
final distance = math.sqrt(dx * dx + dy * dy) / targetRadius;
|
final distance = math.sqrt(dx * dx + dy * dy) / targetRadius;
|
||||||
|
|
||||||
// Score zones based on ringCount
|
|
||||||
for (int i = 0; i < ringCount; i++) {
|
for (int i = 0; i < ringCount; i++) {
|
||||||
final zoneRadius = (i + 1) / ringCount;
|
final zoneRadius = (i + 1) / ringCount;
|
||||||
if (distance <= zoneRadius) {
|
if (distance <= zoneRadius) {
|
||||||
@@ -225,31 +321,29 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0; // Outside target
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Analyze reference impacts to learn their characteristics
|
|
||||||
ImpactCharacteristics? analyzeReferenceImpacts(
|
ImpactCharacteristics? analyzeReferenceImpacts(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
List<ReferenceImpact> references,
|
List<ReferenceImpact> references,
|
||||||
) {
|
) {
|
||||||
return _imageProcessingService.analyzeReferenceImpacts(
|
return _imageProcessingService.analyzeReferenceImpacts(
|
||||||
imagePath,
|
imagePath,
|
||||||
references,
|
references,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect impacts based on reference characteristics (calibrated detection)
|
|
||||||
List<DetectedImpactResult> detectImpactsFromReferences(
|
List<DetectedImpactResult> detectImpactsFromReferences(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
TargetType targetType,
|
TargetType targetType,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double radius,
|
double radius,
|
||||||
int ringCount,
|
int ringCount,
|
||||||
ImpactCharacteristics characteristics, {
|
ImpactCharacteristics characteristics, {
|
||||||
double tolerance = 2.0,
|
double tolerance = 2.0,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
final impacts = _imageProcessingService.detectImpactsFromReferences(
|
final impacts = _imageProcessingService.detectImpactsFromReferences(
|
||||||
imagePath,
|
imagePath,
|
||||||
@@ -260,13 +354,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.x,
|
||||||
impact.y,
|
impact.y,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
radius,
|
radius,
|
||||||
ringCount,
|
ringCount,
|
||||||
)
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -281,20 +375,15 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Détecte les impacts en utilisant OpenCV (Hough Circles + Contours)
|
|
||||||
///
|
|
||||||
/// Cette méthode utilise les algorithmes OpenCV pour une détection plus robuste:
|
|
||||||
/// - Transformation de Hough pour détecter les cercles
|
|
||||||
/// - Analyse de contours avec filtrage par circularité
|
|
||||||
List<DetectedImpactResult> detectImpactsWithOpenCV(
|
List<DetectedImpactResult> detectImpactsWithOpenCV(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
TargetType targetType,
|
TargetType targetType,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double radius,
|
double radius,
|
||||||
int ringCount, {
|
int ringCount, {
|
||||||
OpenCVDetectionSettings? settings,
|
OpenCVDetectionSettings? settings,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
final impacts = _opencvService.detectImpacts(
|
final impacts = _opencvService.detectImpacts(
|
||||||
imagePath,
|
imagePath,
|
||||||
@@ -304,13 +393,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.x,
|
||||||
impact.y,
|
impact.y,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
radius,
|
radius,
|
||||||
ringCount,
|
ringCount,
|
||||||
)
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -326,22 +415,17 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Détecte les impacts avec OpenCV en utilisant des références
|
|
||||||
///
|
|
||||||
/// Analyse les impacts de référence pour apprendre leurs caractéristiques
|
|
||||||
/// puis détecte les impacts similaires dans l'image.
|
|
||||||
List<DetectedImpactResult> detectImpactsWithOpenCVFromReferences(
|
List<DetectedImpactResult> detectImpactsWithOpenCVFromReferences(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
TargetType targetType,
|
TargetType targetType,
|
||||||
double centerX,
|
double centerX,
|
||||||
double centerY,
|
double centerY,
|
||||||
double radius,
|
double radius,
|
||||||
int ringCount,
|
int ringCount,
|
||||||
List<ReferenceImpact> references, {
|
List<ReferenceImpact> references, {
|
||||||
double tolerance = 2.0,
|
double tolerance = 2.0,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// Convertir les références au format OpenCV
|
|
||||||
final refPoints = references.map((r) => (x: r.x, y: r.y)).toList();
|
final refPoints = references.map((r) => (x: r.x, y: r.y)).toList();
|
||||||
|
|
||||||
final impacts = _opencvService.detectFromReferences(
|
final impacts = _opencvService.detectFromReferences(
|
||||||
@@ -353,13 +437,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.x,
|
||||||
impact.y,
|
impact.y,
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
radius,
|
radius,
|
||||||
ringCount,
|
ringCount,
|
||||||
)
|
)
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
||||||
|
|
||||||
return DetectedImpactResult(
|
return DetectedImpactResult(
|
||||||
@@ -374,5 +458,4 @@ class TargetDetectionService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user