439 lines
12 KiB
Dart
439 lines
12 KiB
Dart
/// Gestionnaire d'état pour l'analyse des cibles (ChangeNotifier).
|
|
///
|
|
/// Gère le workflow complet d'analyse : chargement d'image, détection de cible,
|
|
/// gestion des impacts (manuels et automatiques), calcul des scores,
|
|
/// analyse de groupement et sauvegarde des sessions.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import '../../data/models/session.dart';
|
|
import '../../data/models/shot.dart';
|
|
import '../../data/models/target_type.dart';
|
|
import '../../data/repositories/session_repository.dart';
|
|
import '../../services/target_detection_service.dart';
|
|
import '../../services/score_calculator_service.dart';
|
|
import '../../services/grouping_analyzer_service.dart';
|
|
|
|
enum AnalysisState { initial, loading, success, error }
|
|
|
|
class AnalysisProvider extends ChangeNotifier {
|
|
final TargetDetectionService _detectionService;
|
|
final ScoreCalculatorService _scoreCalculatorService;
|
|
final GroupingAnalyzerService _groupingAnalyzerService;
|
|
final SessionRepository _sessionRepository;
|
|
final Uuid _uuid = const Uuid();
|
|
|
|
AnalysisProvider({
|
|
required TargetDetectionService detectionService,
|
|
required ScoreCalculatorService scoreCalculatorService,
|
|
required GroupingAnalyzerService groupingAnalyzerService,
|
|
required SessionRepository sessionRepository,
|
|
}) : _detectionService = detectionService,
|
|
_scoreCalculatorService = scoreCalculatorService,
|
|
_groupingAnalyzerService = groupingAnalyzerService,
|
|
_sessionRepository = sessionRepository;
|
|
|
|
AnalysisState _state = AnalysisState.initial;
|
|
String? _errorMessage;
|
|
String? _imagePath;
|
|
TargetType? _targetType;
|
|
|
|
// Target detection results
|
|
double _targetCenterX = 0.5;
|
|
double _targetCenterY = 0.5;
|
|
double _targetRadius = 0.4;
|
|
int _ringCount = 10;
|
|
List<double>? _ringRadii; // Individual ring radii multipliers
|
|
double _imageAspectRatio = 1.0; // width / height
|
|
|
|
// Shots
|
|
List<Shot> _shots = [];
|
|
|
|
// Score results
|
|
ScoreResult? _scoreResult;
|
|
|
|
// Grouping results
|
|
GroupingResult? _groupingResult;
|
|
|
|
// Reference-based detection
|
|
List<Shot> _referenceImpacts = [];
|
|
ImpactCharacteristics? _learnedCharacteristics;
|
|
|
|
// Getters
|
|
AnalysisState get state => _state;
|
|
String? get errorMessage => _errorMessage;
|
|
String? get imagePath => _imagePath;
|
|
TargetType? get targetType => _targetType;
|
|
double get targetCenterX => _targetCenterX;
|
|
double get targetCenterY => _targetCenterY;
|
|
double get targetRadius => _targetRadius;
|
|
int get ringCount => _ringCount;
|
|
List<double>? get ringRadii => _ringRadii != null ? List.unmodifiable(_ringRadii!) : null;
|
|
double get imageAspectRatio => _imageAspectRatio;
|
|
List<Shot> get shots => List.unmodifiable(_shots);
|
|
ScoreResult? get scoreResult => _scoreResult;
|
|
GroupingResult? get groupingResult => _groupingResult;
|
|
|
|
int get totalScore => _scoreResult?.totalScore ?? 0;
|
|
int get shotCount => _shots.length;
|
|
List<Shot> get referenceImpacts => List.unmodifiable(_referenceImpacts);
|
|
ImpactCharacteristics? get learnedCharacteristics => _learnedCharacteristics;
|
|
bool get hasLearnedCharacteristics => _learnedCharacteristics != null;
|
|
|
|
/// Analyze an image
|
|
Future<void> analyzeImage(String imagePath, TargetType targetType) async {
|
|
_state = AnalysisState.loading;
|
|
_imagePath = imagePath;
|
|
_targetType = targetType;
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// Load image to get dimensions
|
|
final file = File(imagePath);
|
|
final bytes = await file.readAsBytes();
|
|
final codec = await ui.instantiateImageCodec(bytes);
|
|
final frame = await codec.getNextFrame();
|
|
_imageAspectRatio = frame.image.width / frame.image.height;
|
|
frame.image.dispose();
|
|
|
|
// Detect target and impacts
|
|
final result = _detectionService.detectTarget(imagePath, targetType);
|
|
|
|
if (!result.success) {
|
|
_state = AnalysisState.error;
|
|
_errorMessage = result.errorMessage;
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
_targetCenterX = result.centerX;
|
|
_targetCenterY = result.centerY;
|
|
_targetRadius = result.radius;
|
|
|
|
// Create shots from detected impacts
|
|
_shots = result.impacts.map((impact) {
|
|
return Shot(
|
|
id: _uuid.v4(),
|
|
x: impact.x,
|
|
y: impact.y,
|
|
score: impact.suggestedScore,
|
|
sessionId: '', // Will be set when saving
|
|
);
|
|
}).toList();
|
|
|
|
// Calculate scores
|
|
_recalculateScores();
|
|
|
|
// Calculate grouping
|
|
_recalculateGrouping();
|
|
|
|
_state = AnalysisState.success;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
_state = AnalysisState.error;
|
|
_errorMessage = 'Erreur d\'analyse: $e';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Add a manual shot
|
|
void addShot(double x, double y) {
|
|
final score = _calculateShotScore(x, y);
|
|
final shot = Shot(
|
|
id: _uuid.v4(),
|
|
x: x,
|
|
y: y,
|
|
score: score,
|
|
sessionId: '',
|
|
);
|
|
|
|
_shots.add(shot);
|
|
_recalculateScores();
|
|
_recalculateGrouping();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Remove a shot
|
|
void removeShot(String shotId) {
|
|
_shots.removeWhere((shot) => shot.id == shotId);
|
|
_recalculateScores();
|
|
_recalculateGrouping();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Move a shot to a new position
|
|
void moveShot(String shotId, double newX, double newY) {
|
|
final index = _shots.indexWhere((shot) => shot.id == shotId);
|
|
if (index == -1) return;
|
|
|
|
final newScore = _calculateShotScore(newX, newY);
|
|
_shots[index] = _shots[index].copyWith(
|
|
x: newX,
|
|
y: newY,
|
|
score: newScore,
|
|
);
|
|
|
|
_recalculateScores();
|
|
_recalculateGrouping();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Auto-detect impacts using image processing
|
|
Future<int> autoDetectImpacts({
|
|
int darkThreshold = 80,
|
|
int minImpactSize = 20,
|
|
int maxImpactSize = 500,
|
|
double minCircularity = 0.6,
|
|
double minFillRatio = 0.5,
|
|
bool clearExisting = false,
|
|
}) async {
|
|
if (_imagePath == null || _targetType == null) return 0;
|
|
|
|
final settings = ImpactDetectionSettings(
|
|
darkThreshold: darkThreshold,
|
|
minImpactSize: minImpactSize,
|
|
maxImpactSize: maxImpactSize,
|
|
minCircularity: minCircularity,
|
|
minFillRatio: minFillRatio,
|
|
);
|
|
|
|
final detectedImpacts = _detectionService.detectImpactsOnly(
|
|
_imagePath!,
|
|
_targetType!,
|
|
_targetCenterX,
|
|
_targetCenterY,
|
|
_targetRadius,
|
|
_ringCount,
|
|
settings,
|
|
);
|
|
|
|
if (clearExisting) {
|
|
_shots.clear();
|
|
}
|
|
|
|
// Add detected impacts as shots
|
|
for (final impact in detectedImpacts) {
|
|
final score = _calculateShotScore(impact.x, impact.y);
|
|
final shot = Shot(
|
|
id: _uuid.v4(),
|
|
x: impact.x,
|
|
y: impact.y,
|
|
score: score,
|
|
sessionId: '',
|
|
);
|
|
_shots.add(shot);
|
|
}
|
|
|
|
_recalculateScores();
|
|
_recalculateGrouping();
|
|
notifyListeners();
|
|
|
|
return detectedImpacts.length;
|
|
}
|
|
|
|
/// Add a reference impact for calibrated detection
|
|
void addReferenceImpact(double x, double y) {
|
|
final score = _calculateShotScore(x, y);
|
|
final shot = Shot(
|
|
id: _uuid.v4(),
|
|
x: x,
|
|
y: y,
|
|
score: score,
|
|
sessionId: '',
|
|
);
|
|
_referenceImpacts.add(shot);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Remove a reference impact
|
|
void removeReferenceImpact(String shotId) {
|
|
_referenceImpacts.removeWhere((shot) => shot.id == shotId);
|
|
_learnedCharacteristics = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Clear all reference impacts
|
|
void clearReferenceImpacts() {
|
|
_referenceImpacts.clear();
|
|
_learnedCharacteristics = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Learn characteristics from reference impacts
|
|
bool learnFromReferences() {
|
|
if (_imagePath == null || _referenceImpacts.length < 2) return false;
|
|
|
|
final references = _referenceImpacts
|
|
.map((shot) => ReferenceImpact(x: shot.x, y: shot.y))
|
|
.toList();
|
|
|
|
_learnedCharacteristics = _detectionService.analyzeReferenceImpacts(
|
|
_imagePath!,
|
|
references,
|
|
);
|
|
|
|
notifyListeners();
|
|
return _learnedCharacteristics != null;
|
|
}
|
|
|
|
/// Auto-detect impacts using learned reference characteristics
|
|
Future<int> detectFromReferences({
|
|
double tolerance = 2.0,
|
|
bool clearExisting = false,
|
|
}) async {
|
|
if (_imagePath == null || _targetType == null || _learnedCharacteristics == null) {
|
|
return 0;
|
|
}
|
|
|
|
final detectedImpacts = _detectionService.detectImpactsFromReferences(
|
|
_imagePath!,
|
|
_targetType!,
|
|
_targetCenterX,
|
|
_targetCenterY,
|
|
_targetRadius,
|
|
_ringCount,
|
|
_learnedCharacteristics!,
|
|
tolerance: tolerance,
|
|
);
|
|
|
|
if (clearExisting) {
|
|
_shots.clear();
|
|
}
|
|
|
|
// Add detected impacts as shots
|
|
for (final impact in detectedImpacts) {
|
|
final score = _calculateShotScore(impact.x, impact.y);
|
|
final shot = Shot(
|
|
id: _uuid.v4(),
|
|
x: impact.x,
|
|
y: impact.y,
|
|
score: score,
|
|
sessionId: '',
|
|
);
|
|
_shots.add(shot);
|
|
}
|
|
|
|
_recalculateScores();
|
|
_recalculateGrouping();
|
|
notifyListeners();
|
|
|
|
return detectedImpacts.length;
|
|
}
|
|
|
|
/// Adjust target position
|
|
void adjustTargetPosition(double centerX, double centerY, double radius, {int? ringCount, List<double>? ringRadii}) {
|
|
_targetCenterX = centerX;
|
|
_targetCenterY = centerY;
|
|
_targetRadius = radius;
|
|
if (ringCount != null) {
|
|
_ringCount = ringCount;
|
|
}
|
|
if (ringRadii != null) {
|
|
_ringRadii = ringRadii;
|
|
}
|
|
|
|
// Recalculate all shot scores based on new target position
|
|
_shots = _shots.map((shot) {
|
|
final newScore = _calculateShotScore(shot.x, shot.y);
|
|
return shot.copyWith(score: newScore);
|
|
}).toList();
|
|
|
|
_recalculateScores();
|
|
notifyListeners();
|
|
}
|
|
|
|
int _calculateShotScore(double x, double y) {
|
|
if (_targetType == TargetType.concentric) {
|
|
return _scoreCalculatorService.calculateConcentricScore(
|
|
shotX: x,
|
|
shotY: y,
|
|
targetCenterX: _targetCenterX,
|
|
targetCenterY: _targetCenterY,
|
|
targetRadius: _targetRadius,
|
|
ringCount: _ringCount,
|
|
imageAspectRatio: _imageAspectRatio,
|
|
ringRadii: _ringRadii,
|
|
);
|
|
} else {
|
|
return _scoreCalculatorService.calculateSilhouetteScore(
|
|
shotX: x,
|
|
shotY: y,
|
|
targetCenterX: _targetCenterX,
|
|
targetCenterY: _targetCenterY,
|
|
targetWidth: _targetRadius * 0.8,
|
|
targetHeight: _targetRadius * 2,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _recalculateScores() {
|
|
if (_targetType == null) return;
|
|
|
|
_scoreResult = _scoreCalculatorService.calculateScores(
|
|
shots: _shots,
|
|
targetType: _targetType!,
|
|
targetCenterX: _targetCenterX,
|
|
targetCenterY: _targetCenterY,
|
|
targetRadius: _targetRadius,
|
|
ringCount: _ringCount,
|
|
imageAspectRatio: _imageAspectRatio,
|
|
ringRadii: _ringRadii,
|
|
);
|
|
}
|
|
|
|
void _recalculateGrouping() {
|
|
_groupingResult = _groupingAnalyzerService.analyzeGrouping(_shots);
|
|
}
|
|
|
|
/// Save the session
|
|
Future<Session> saveSession({String? notes}) async {
|
|
if (_imagePath == null || _targetType == null) {
|
|
throw Exception('Cannot save: missing image or target type');
|
|
}
|
|
|
|
final session = await _sessionRepository.createSession(
|
|
targetType: _targetType!,
|
|
imagePath: _imagePath!,
|
|
shots: _shots.map((s) => s.copyWith(sessionId: '')).toList(),
|
|
totalScore: totalScore,
|
|
groupingDiameter: _groupingResult?.diameter,
|
|
groupingCenterX: _groupingResult?.centerX,
|
|
groupingCenterY: _groupingResult?.centerY,
|
|
notes: notes,
|
|
targetCenterX: _targetCenterX,
|
|
targetCenterY: _targetCenterY,
|
|
targetRadius: _targetRadius,
|
|
);
|
|
|
|
// Update shots with session ID
|
|
_shots = session.shots;
|
|
notifyListeners();
|
|
|
|
return session;
|
|
}
|
|
|
|
/// Reset the provider
|
|
void reset() {
|
|
_state = AnalysisState.initial;
|
|
_errorMessage = null;
|
|
_imagePath = null;
|
|
_targetType = null;
|
|
_targetCenterX = 0.5;
|
|
_targetCenterY = 0.5;
|
|
_targetRadius = 0.4;
|
|
_ringCount = 10;
|
|
_ringRadii = null;
|
|
_imageAspectRatio = 1.0;
|
|
_shots = [];
|
|
_scoreResult = null;
|
|
_groupingResult = null;
|
|
_referenceImpacts = [];
|
|
_learnedCharacteristics = null;
|
|
notifyListeners();
|
|
}
|
|
}
|