fix: filtre historique, fuite mémoire OpenCV, export IA, tests
- Historique : le filtre par type de cible est désormais appliqué au chargement ; correction du piège PopupMenuItem(value: null) qui empêchait l'option « Tous » de réinitialiser le filtre ; icône colorée quand un filtre est actif - OpenCV : libération des Mat natifs (img, gray, blurred, circles) dans un finally — detectTarget tourne toutes les secondes pendant l'aperçu caméra et faisait grimper la mémoire native en continu - Export IA : distance, arme et id de session réels transmis depuis SessionProvider au lieu des placeholders (25 m / "Unknown") - Tests : remplacement du test widget cassé (BullyApp sans providers) par 14 tests qui passent — calcul de score concentrique (centre, hors cible, ratio d'image, anneaux personnalisés), agrégation des scores, analyse de groupement, et rendu du widget StatsCard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -238,8 +238,14 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
_groupingResult = _groupingAnalyzerService.analyzeGrouping(_shots);
|
||||
}
|
||||
|
||||
/// Exporte l'image et le json vers le backend IA
|
||||
Future<bool> exportToAiBackend() async {
|
||||
/// Exporte l'image et le json vers le backend IA.
|
||||
/// [sessionId], [distance] et [weapon] proviennent du SessionProvider de
|
||||
/// l'écran appelant, pour que le dataset contienne les vraies métadonnées.
|
||||
Future<bool> exportToAiBackend({
|
||||
String? sessionId,
|
||||
int? distance,
|
||||
String? weapon,
|
||||
}) async {
|
||||
if (_imagePath == null || _targetType == null) {
|
||||
_errorMessage = "Impossible d'export : image ou type de cible manquant.";
|
||||
notifyListeners();
|
||||
@@ -253,12 +259,14 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
|
||||
final success = await service.exportData(
|
||||
imagePath: _imagePath!,
|
||||
sessionId: 'export',
|
||||
sessionId: sessionId ?? 'export',
|
||||
targetType: _targetType!,
|
||||
targetCenterX: _targetCenterX,
|
||||
targetCenterY: _targetCenterY,
|
||||
targetRadius: _targetRadius,
|
||||
shots: _shots,
|
||||
distanceMeters: distance ?? 25,
|
||||
weaponName: weapon ?? 'Unknown',
|
||||
);
|
||||
|
||||
_state = AnalysisState.success;
|
||||
|
||||
@@ -275,7 +275,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
const SnackBar(content: Text('Exportation en cours...')),
|
||||
);
|
||||
|
||||
final success = await p.exportToAiBackend();
|
||||
// Métadonnées réelles de la session en cours (distance,
|
||||
// arme) plutôt que les placeholders par défaut.
|
||||
final sp = context.read<SessionProvider>();
|
||||
final success = await p.exportToAiBackend(
|
||||
sessionId: sp.activeSessionId,
|
||||
distance: sp.distance,
|
||||
weapon: sp.currentWeapon,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
@@ -43,6 +43,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
setState(() {
|
||||
_sessions = sessions;
|
||||
|
||||
// --- FILTRAGE PAR TYPE DE CIBLE ---
|
||||
// Une session est retenue si au moins une de ses cibles est du type choisi.
|
||||
if (_filterType != null) {
|
||||
_sessions = _sessions
|
||||
.where((s) => s.analyses.any((a) => a.targetType == _filterType))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// --- LOGIQUE DE FILTRAGE PAR PÉRIODE ---
|
||||
if (_selectedDateRange != null) {
|
||||
_sessions = _sessions.where((s) {
|
||||
@@ -125,17 +133,29 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
appBar: AppBar(
|
||||
title: const Text('Historique'),
|
||||
actions: [
|
||||
PopupMenuButton<TargetType?>(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onSelected: (type) {
|
||||
setState(() => _filterType = type);
|
||||
// NOTE : on passe par un String sentinelle ('all') car un
|
||||
// PopupMenuItem avec value null ne déclenche jamais onSelected
|
||||
// (Flutter l'interprète comme une annulation du menu).
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(
|
||||
Icons.filter_list,
|
||||
// Icône colorée quand un filtre est actif, pour le rendre visible.
|
||||
color: _filterType != null ? AppTheme.primaryColor : null,
|
||||
),
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_filterType =
|
||||
value == 'all' ? null : TargetType.fromString(value);
|
||||
});
|
||||
_loadSessions();
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: null, child: Text('Tous')),
|
||||
const PopupMenuItem(value: 'all', child: Text('Tous')),
|
||||
...TargetType.values.map(
|
||||
(type) =>
|
||||
PopupMenuItem(value: type, child: Text(type.displayName)),
|
||||
(type) => PopupMenuItem(
|
||||
value: type.name,
|
||||
child: Text(type.displayName),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -53,6 +53,8 @@ class AiExportService {
|
||||
required double targetCenterY,
|
||||
required double targetRadius,
|
||||
required List<Shot> shots,
|
||||
int distanceMeters = 25,
|
||||
String weaponName = 'Unknown',
|
||||
String? apiUrl,
|
||||
}) async {
|
||||
try {
|
||||
@@ -109,9 +111,9 @@ class AiExportService {
|
||||
"device_info": deviceData,
|
||||
"target_metadata": {
|
||||
"type": targetType.name,
|
||||
"distance_meters": 25, // Default/placeholder
|
||||
"weapon": "Unknown", // Default/placeholder
|
||||
// The backend could extract exact width/height from the image.
|
||||
"distance_meters": distanceMeters,
|
||||
"weapon": weaponName,
|
||||
// The backend could extract exact width/height from the image.
|
||||
},
|
||||
"plotting": {
|
||||
"target_corners": corners,
|
||||
|
||||
@@ -26,23 +26,34 @@ class TargetDetectionResult {
|
||||
|
||||
class OpenCVTargetService {
|
||||
/// Detect the main target (center and radius) from an image file
|
||||
///
|
||||
/// IMPORTANT : les Mat OpenCV sont de la mémoire NATIVE, invisible pour le
|
||||
/// garbage collector Dart. Cette méthode est appelée en boucle (~1 s)
|
||||
/// pendant l'aperçu caméra : sans dispose() explicite dans le finally, la
|
||||
/// mémoire native grimpe en continu tant que l'utilisateur vise.
|
||||
Future<TargetDetectionResult> detectTarget(String imagePath) async {
|
||||
cv.Mat? img;
|
||||
cv.Mat? gray;
|
||||
cv.Mat? blurred;
|
||||
cv.Mat? circles;
|
||||
cv.Mat? looseCircles;
|
||||
|
||||
try {
|
||||
// Read image
|
||||
final img = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
|
||||
img = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
|
||||
if (img.isEmpty) {
|
||||
return TargetDetectionResult.failure();
|
||||
}
|
||||
|
||||
// Convert to grayscale
|
||||
final gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);
|
||||
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);
|
||||
|
||||
// Apply Gaussian blur to reduce noise
|
||||
final blurred = cv.gaussianBlur(gray, (9, 9), 2, sigmaY: 2);
|
||||
blurred = cv.gaussianBlur(gray, (9, 9), 2, sigmaY: 2);
|
||||
|
||||
// Detect circles using Hough Transform
|
||||
// Parameters need to be tuned for the specific target type
|
||||
final circles = cv.HoughCircles(
|
||||
// Detect circles using Hough Transform.
|
||||
// HoughCircles returns a Mat of shape (1, N) of Vec3f (x, y, r).
|
||||
circles = cv.HoughCircles(
|
||||
blurred,
|
||||
cv.HOUGH_GRADIENT,
|
||||
1, // dp
|
||||
@@ -55,26 +66,9 @@ class OpenCVTargetService {
|
||||
maxRadius: img.cols ~/ 2,
|
||||
);
|
||||
|
||||
// HoughCircles returns a Mat of shape (1, N, 3) where N is number of circles.
|
||||
// In opencv_dart, we cannot iterate easily.
|
||||
// However, we can access data via pointer if needed, or check if Vec3f is supported.
|
||||
// Given the user report, `at<Vec3f>` likely failed compilation or runtime.
|
||||
// Let's use a safer approach: assume standard memory layout (x, y, r, x, y, r...).
|
||||
// Or use `at<double>` carefully.
|
||||
|
||||
// Better yet: try to use `circles.data` if available, but it returns a Pointer.
|
||||
// Let's stick to `at` but use `double` and manual offset if Vec3f fails.
|
||||
// actually, let's try to trust `at<double>` for flattened access OR `at<Vec3f>`.
|
||||
// NOTE: `at<Vec3f>` was reported as "method at not defined for VecPoint2f" earlier, NOT for Mat.
|
||||
// The user error was for `VecPoint2f`. `Mat` definitely has `at`.
|
||||
// BUT `VecPoint2f` is a List-like structure in Dart wrapper.
|
||||
// usage of `at` on `VecPoint2f` was the error.
|
||||
// Here `circles` IS A MAT. So `at` IS defined.
|
||||
// However, to be safe and robust, and to implement clustering...
|
||||
|
||||
if (circles.isEmpty) {
|
||||
// Try with different parameters if first attempt fails (more lenient)
|
||||
final looseCircles = cv.HoughCircles(
|
||||
looseCircles = cv.HoughCircles(
|
||||
blurred,
|
||||
cv.HOUGH_GRADIENT,
|
||||
1,
|
||||
@@ -93,8 +87,15 @@ class OpenCVTargetService {
|
||||
|
||||
return _findBestConcentricCircles(circles, img.cols, img.rows);
|
||||
} catch (e) {
|
||||
// print('Error detecting target with OpenCV: $e');
|
||||
return TargetDetectionResult.failure();
|
||||
} finally {
|
||||
// _findBestConcentricCircles a déjà extrait les données dans des listes
|
||||
// Dart avant qu'on arrive ici : libérer les Mat est donc toujours sûr.
|
||||
img?.dispose();
|
||||
gray?.dispose();
|
||||
blurred?.dispose();
|
||||
circles?.dispose();
|
||||
looseCircles?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user