diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index 10519b6..bb91d86 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -53,6 +53,7 @@ class AnalysisProvider extends ChangeNotifier { double _targetCenterX = 0.5; double _targetCenterY = 0.5; double _targetRadius = 0.4; + double _targetInnerRadius = 0.04; int _ringCount = 10; List? _ringRadii; // Individual ring radii multipliers double _imageAspectRatio = 1.0; // width / height @@ -83,6 +84,7 @@ class AnalysisProvider extends ChangeNotifier { double get targetCenterX => _targetCenterX; double get targetCenterY => _targetCenterY; double get targetRadius => _targetRadius; + double get targetInnerRadius => _targetInnerRadius; int get ringCount => _ringCount; List? get ringRadii => _ringRadii != null ? List.unmodifiable(_ringRadii!) : null; @@ -138,6 +140,7 @@ class AnalysisProvider extends ChangeNotifier { _targetCenterX = 0.5; _targetCenterY = 0.5; _targetRadius = 0.4; + _targetInnerRadius = 0.04; // Initialize empty shots list _shots = []; @@ -160,6 +163,7 @@ class AnalysisProvider extends ChangeNotifier { _targetCenterX = result.centerX; _targetCenterY = result.centerY; _targetRadius = result.radius; + _targetInnerRadius = result.radius * 0.1; // Create shots from detected impacts _shots = result.impacts.map((impact) { @@ -488,12 +492,14 @@ class AnalysisProvider extends ChangeNotifier { void adjustTargetPosition( double centerX, double centerY, + double innerRadius, double radius, { int? ringCount, List? ringRadii, }) { _targetCenterX = centerX; _targetCenterY = centerY; + _targetInnerRadius = innerRadius; _targetRadius = radius; if (ringCount != null) { _ringCount = ringCount; @@ -520,7 +526,12 @@ class AnalysisProvider extends ChangeNotifier { final result = await _opencvTargetService.detectTarget(_imagePath!); if (result.success) { - adjustTargetPosition(result.centerX, result.centerY, result.radius); + adjustTargetPosition( + result.centerX, + result.centerY, + result.radius * 0.1, + result.radius, + ); return true; } return false; @@ -687,6 +698,7 @@ class AnalysisProvider extends ChangeNotifier { _targetCenterX = 0.5; _targetCenterY = 0.5; _targetRadius = 0.4; + _targetInnerRadius = 0.04; _ringCount = 10; _ringRadii = null; _imageAspectRatio = 1.0; diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index c5c4197..f9b2287 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -275,7 +275,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { child: Column( children: [ // Auto-calibrate button - /*SizedBox( + SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () async { @@ -334,7 +334,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { foregroundColor: Colors.white, ), ), - ),*/ + ), const SizedBox(height: 16), // Ring count slider Row( @@ -361,6 +361,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { provider.adjustTargetPosition( provider.targetCenterX, provider.targetCenterY, + provider.targetInnerRadius, provider.targetRadius, ringCount: value.round(), ); @@ -411,6 +412,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { provider.adjustTargetPosition( provider.targetCenterX, provider.targetCenterY, + provider.targetInnerRadius, value, ringCount: provider.ringCount, ); @@ -535,6 +537,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { initialCenterX: provider.targetCenterX, initialCenterY: provider.targetCenterY, initialRadius: provider.targetRadius, + initialInnerRadius: provider.targetInnerRadius, initialRingCount: provider.ringCount, initialRingRadii: provider.ringRadii, targetType: provider.targetType!, @@ -542,6 +545,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ( centerX, centerY, + innerRadius, radius, ringCount, { List? ringRadii, @@ -549,6 +553,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { provider.adjustTargetPosition( centerX, centerY, + innerRadius, radius, ringCount: ringCount, ringRadii: ringRadii, diff --git a/lib/features/analysis/widgets/target_calibration.dart b/lib/features/analysis/widgets/target_calibration.dart index a68d6aa..b78e2eb 100644 --- a/lib/features/analysis/widgets/target_calibration.dart +++ b/lib/features/analysis/widgets/target_calibration.dart @@ -13,16 +13,26 @@ class TargetCalibration extends StatefulWidget { final double initialCenterX; final double initialCenterY; final double initialRadius; + final double initialInnerRadius; final int initialRingCount; final TargetType targetType; final List? initialRingRadii; - final Function(double centerX, double centerY, double radius, int ringCount, {List? ringRadii}) onCalibrationChanged; + final Function( + double centerX, + double centerY, + double innerRadius, + double radius, + int ringCount, { + List? ringRadii, + }) + onCalibrationChanged; const TargetCalibration({ super.key, required this.initialCenterX, required this.initialCenterY, required this.initialRadius, + required this.initialInnerRadius, this.initialRingCount = 10, required this.targetType, this.initialRingRadii, @@ -37,11 +47,13 @@ class _TargetCalibrationState extends State { late double _centerX; late double _centerY; late double _radius; + late double _innerRadius; late int _ringCount; late List _ringRadii; bool _isDraggingCenter = false; bool _isDraggingRadius = false; + bool _isDraggingInnerRadius = false; @override void initState() { @@ -49,28 +61,57 @@ class _TargetCalibrationState extends State { _centerX = widget.initialCenterX; _centerY = widget.initialCenterY; _radius = widget.initialRadius; + _innerRadius = widget.initialInnerRadius; _ringCount = widget.initialRingCount; _initRingRadii(); } void _initRingRadii() { - if (widget.initialRingRadii != null && widget.initialRingRadii!.length == _ringCount) { + if (widget.initialRingRadii != null && + widget.initialRingRadii!.length == _ringCount) { _ringRadii = List.from(widget.initialRingRadii!); } else { - // Initialize with default proportional radii - _ringRadii = List.generate(_ringCount, (i) => (i + 1) / _ringCount); + // Initialize with default proportional radii interpolated between inner and outer + _ringRadii = List.generate(_ringCount, (i) { + if (_ringCount <= 1) return 1.0; + final ratio = _innerRadius / _radius; + return ratio + (1.0 - ratio) * i / (_ringCount - 1); + }); } } @override void didUpdateWidget(TargetCalibration 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) { _ringCount = widget.initialRingCount; - _initRingRadii(); + shouldReinit = true; } if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius) { _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 { centerX: _centerX, centerY: _centerY, radius: _radius, + innerRadius: _innerRadius, ringCount: _ringCount, ringRadii: _ringRadii, targetType: widget.targetType, isDraggingCenter: _isDraggingCenter, isDraggingRadius: _isDraggingRadius, + isDraggingInnerRadius: _isDraggingInnerRadius, ), ), ); @@ -109,21 +152,42 @@ class _TargetCalibrationState extends State { // Check if tapping on center handle 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 outerRadius = _radius * (_ringRadii.isNotEmpty ? _ringRadii.last : 1.0); + final outerRadius = _radius; final radiusHandleX = _centerX + outerRadius * minDim / size.width; 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) { setState(() { _isDraggingCenter = true; }); - } else if (distToRadiusHandle < 0.05) { + } else if (distToOuterHandle < 0.05) { setState(() { _isDraggingRadius = true; }); + } else if (distToInnerHandle < 0.05) { + setState(() { + _isDraggingInnerRadius = true; + }); } else if (distToCenter < _radius + 0.02) { // Tapping inside the target - move center setState(() { @@ -143,19 +207,36 @@ class _TargetCalibrationState extends State { _centerX = _centerX + deltaX; _centerY = _centerY + deltaY; } else if (_isDraggingRadius) { - // Adjust outer radius (scales all rings proportionally) + // Adjust outer radius 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() { setState(() { _isDraggingCenter = false; _isDraggingRadius = false; + _isDraggingInnerRadius = false; }); } @@ -170,21 +251,25 @@ class _CalibrationPainter extends CustomPainter { final double centerX; final double centerY; final double radius; + final double innerRadius; final int ringCount; final List ringRadii; final TargetType targetType; final bool isDraggingCenter; final bool isDraggingRadius; + final bool isDraggingInnerRadius; _CalibrationPainter({ required this.centerX, required this.centerY, required this.radius, + required this.innerRadius, required this.ringCount, required this.ringRadii, required this.targetType, required this.isDraggingCenter, required this.isDraggingRadius, + required this.isDraggingInnerRadius, }); @override @@ -192,6 +277,7 @@ class _CalibrationPainter extends CustomPainter { final centerPx = Offset(centerX * size.width, centerY * size.height); final minDim = size.width < size.height ? size.width : size.height; final baseRadiusPx = radius * minDim; + final innerRadiusPx = innerRadius * minDim; if (targetType == TargetType.concentric) { _drawConcentricZones(canvas, size, centerPx, baseRadiusPx); @@ -199,17 +285,42 @@ class _CalibrationPainter extends CustomPainter { _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 _drawCenterHandle(canvas, centerPx); // Draw radius handle (for outer ring) _drawRadiusHandle(canvas, size, centerPx, baseRadiusPx); + // Draw inner radius handle + _drawInnerRadiusHandle(canvas, size, centerPx, innerRadiusPx); + // Draw instructions _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 List zoneColors = []; for (int i = 0; i < ringCount; i++) { @@ -235,7 +346,9 @@ class _CalibrationPainter extends CustomPainter { // Draw from outside to inside 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; zonePaint.color = zoneColors[i]; @@ -244,12 +357,12 @@ class _CalibrationPainter extends CustomPainter { } // Draw zone labels (only if within visible area) - final textPainter = TextPainter( - textDirection: TextDirection.ltr, - ); + final textPainter = TextPainter(textDirection: TextDirection.ltr); 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 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0; @@ -268,9 +381,7 @@ class _CalibrationPainter extends CustomPainter { color: Colors.white.withValues(alpha: 0.9), fontSize: 12, fontWeight: FontWeight.bold, - shadows: const [ - Shadow(color: Colors.black, blurRadius: 2), - ], + shadows: const [Shadow(color: Colors.black, blurRadius: 2)], ), ); textPainter.layout(); @@ -278,14 +389,24 @@ class _CalibrationPainter extends CustomPainter { // Draw label on the right side of each zone final labelY = center.dy - textPainter.height / 2; 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 - final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2; + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2; // Draw silhouette outline (simplified as rectangle for now) final silhouetteWidth = radius * 0.8; @@ -293,7 +414,11 @@ class _CalibrationPainter extends CustomPainter { paint.color = Colors.green.withValues(alpha: 0.5); canvas.drawRect( - Rect.fromCenter(center: center, width: silhouetteWidth, height: silhouetteHeight), + Rect.fromCenter( + center: center, + width: silhouetteWidth, + height: silhouetteHeight, + ), paint, ); } @@ -316,17 +441,36 @@ class _CalibrationPainter extends CustomPainter { final crossPaint = Paint() ..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..strokeWidth = 2; - canvas.drawLine(Offset(center.dx - 20, center.dy), Offset(center.dx - 8, center.dy), 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); + canvas.drawLine( + Offset(center.dx - 20, center.dy), + Offset(center.dx - 8, center.dy), + 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 - final outerRingRadius = ringRadii.isNotEmpty ? ringRadii.last : 1.0; - final actualRadius = baseRadius * outerRingRadius; - final actualHandleX = center.dx + actualRadius; + final actualHandleX = center.dx + baseRadius; final clampedHandleX = actualHandleX.clamp(20.0, size.width - 20); final clampedHandleY = center.dy.clamp(20.0, size.height - 20); final handlePos = Offset(clampedHandleX, clampedHandleY); @@ -376,7 +520,7 @@ class _CalibrationPainter extends CustomPainter { // Label final textPainter = TextPainter( text: const TextSpan( - text: 'RAYON', + text: 'EXT.', style: TextStyle( color: Colors.white, 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) { const instruction = 'Deplacez le centre ou ajustez le rayon'; @@ -418,9 +634,11 @@ class _CalibrationPainter extends CustomPainter { return centerX != oldDelegate.centerX || centerY != oldDelegate.centerY || radius != oldDelegate.radius || + innerRadius != oldDelegate.innerRadius || ringCount != oldDelegate.ringCount || isDraggingCenter != oldDelegate.isDraggingCenter || isDraggingRadius != oldDelegate.isDraggingRadius || + isDraggingInnerRadius != oldDelegate.isDraggingInnerRadius || ringRadii != oldDelegate.ringRadii; } } diff --git a/lib/services/opencv_target_service.dart b/lib/services/opencv_target_service.dart index 6b2f9c0..9719724 100644 --- a/lib/services/opencv_target_service.dart +++ b/lib/services/opencv_target_service.dart @@ -45,14 +45,14 @@ class OpenCVTargetService { final circles = cv.HoughCircles( blurred, cv.HOUGH_GRADIENT, - 1, // dp: Inverse ratio of the accumulator resolution to the image resolution - (img.rows / 8) - .toDouble(), // minDist: Minimum distance between the centers of the detected circles - param1: 100, // param1: Gradient value for Canny edge detection + 1, // dp + (img.rows / 16) + .toDouble(), // minDist decreased to allow more rings in same general area + param1: 100, // Canny edge detection param2: - 30, // param2: Accumulator threshold for the circle centers at the detection stage - minRadius: img.cols ~/ 20, // minRadius - maxRadius: img.cols ~/ 2, // maxRadius + 60, // Accumulator threshold (higher = fewer false circles, more accurate) + minRadius: img.cols ~/ 20, + maxRadius: img.cols ~/ 2, ); // HoughCircles returns a Mat of shape (1, N, 3) where N is number of circles. @@ -79,8 +79,8 @@ class OpenCVTargetService { cv.HOUGH_GRADIENT, 1, (img.rows / 8).toDouble(), - param1: 50, - param2: 20, + param1: 100, + param2: 40, minRadius: img.cols ~/ 20, maxRadius: img.cols ~/ 2, ); @@ -137,18 +137,22 @@ class OpenCVTargetService { for (final circle in detected) { bool added = false; for (final cluster in clusters) { - // Check distance to cluster center (average of existing) - double clusterX = 0; - double clusterY = 0; + // Calculate the actual center of the cluster based on the smallest circle (the likely bullseye) + double clusterCenterX = cluster.first.x; + double clusterCenterY = cluster.first.y; + double minRadiusInCluster = cluster.first.r; + for (final c in cluster) { - clusterX += c.x; - clusterY += c.y; + if (c.r < minRadiusInCluster) { + minRadiusInCluster = c.r; + clusterCenterX = c.x; + clusterCenterY = c.y; + } } - clusterX /= cluster.length; - clusterY /= cluster.length; 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) { @@ -171,10 +175,10 @@ class OpenCVTargetService { for (final cluster in clusters) { // Score calculation - // Base score = number of circles * 10 - double score = cluster.length * 10.0; + // Base score = number of circles squared (heavily favor concentric rings) + 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; for (final c in cluster) { cx += c.x; @@ -188,7 +192,8 @@ class OpenCVTargetService { ); 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 // (Optional: check if radii are somewhat distributed?)