fix(crop): stabiliser le rectangle géométrique de découpe et l'enchaînement des zooms

This commit is contained in:
qguillaume
2026-05-27 15:36:56 +02:00
parent 4d2ca2c94f
commit 730629a031
5 changed files with 269 additions and 174 deletions

View File

@@ -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;

View File

@@ -225,8 +225,8 @@ 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!),

View File

@@ -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: () {
@@ -249,7 +248,7 @@ class _CropScreenState extends State<CropScreen> {
onScaleUpdate: _onScaleUpdate, onScaleUpdate: _onScaleUpdate,
child: Stack( child: Stack(
children: [ children: [
// 1. L'image brute avec ses transformations // 1. L'image brute avec ses transformations (Reste calé sur BoxFit.contain d'origine)
Positioned.fill( Positioned.fill(
child: Center( child: Center(
child: Transform( child: Transform(
@@ -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,
@@ -372,8 +370,18 @@ 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; // AJOUT DE LA DECOUPE ET DU REDRESSEMENT GÉOMÉTRIQUE PHYSIQUE
final croppedImagePath = await _cropService.cropToSquare(
widget.imagePath,
cropRect,
rotationDegrees: _rotation, // On transmet la rotation à la jauge haute précision
);
// L'image sortant du service étant désormais un carré physique parfait (1:1),
// le centre de la cible se trouve précisément au milieu (0.5, 0.5)
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 injecte le fichier carré nettoyé
targetType: widget.targetType, targetType: widget.targetType,
initialCenterX: targetCenterX, initialCenterX: targetCenterX,
initialCenterY: targetCenterY, initialCenterY: targetCenterY,
cropScale: _scale, cropScale: 1.0, // Réinitialisé car la texture est déjà recadrée à la bonne échelle
cropOffset: _offset, cropOffset: Offset.zero, // Réinitialisé car le fichier est déjà parfaitement centré
cropRotation: _rotation, cropRotation: 0.0, // Réinitialisé car les pixels ont déjà subi la rotation matricielle
), ),
), ),
); );
@@ -395,7 +403,7 @@ 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),
); );
} }
} }
@@ -434,10 +442,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),
); );
} }
} }

View File

@@ -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
} }
} }
} }

View File

@@ -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 [];
} }
} }
} }