import 'dart:math' as math; import 'package:opencv_dart/opencv_dart.dart' as cv; class TargetDetectionResult { final double centerX; final double centerY; final double radius; final bool success; TargetDetectionResult({ required this.centerX, required this.centerY, required this.radius, this.success = true, }); factory TargetDetectionResult.failure() { return TargetDetectionResult( centerX: 0.5, centerY: 0.5, radius: 0.4, success: false, ); } } class OpenCVTargetService { /// Detect the main target (center and radius) from an image file Future detectTarget(String imagePath) async { try { // Read image final 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); // Apply Gaussian blur to reduce noise final 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( 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 param2: 30, // param2: Accumulator threshold for the circle centers at the detection stage minRadius: img.cols ~/ 20, // minRadius maxRadius: img.cols ~/ 2, // maxRadius ); // 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` likely failed compilation or runtime. // Let's use a safer approach: assume standard memory layout (x, y, r, x, y, r...). // Or use `at` 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` for flattened access OR `at`. // NOTE: `at` 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( blurred, cv.HOUGH_GRADIENT, 1, (img.rows / 8).toDouble(), param1: 50, param2: 20, minRadius: img.cols ~/ 20, maxRadius: img.cols ~/ 2, ); if (looseCircles.isEmpty) { return TargetDetectionResult.failure(); } return _findBestConcentricCircles(looseCircles, img.cols, img.rows); } return _findBestConcentricCircles(circles, img.cols, img.rows); } catch (e) { // print('Error detecting target with OpenCV: $e'); return TargetDetectionResult.failure(); } } TargetDetectionResult _findBestConcentricCircles( cv.Mat circles, int width, int height, ) { if (circles.rows == 0 || circles.cols == 0) { return TargetDetectionResult.failure(); } final int numCircles = circles.cols; final List<({double x, double y, double r})> detected = []; // Extract circles safely // We'll use `at` assuming the Mat is (1, N, 3) float32 (CV_32FC3 usually) // Actually HoughCircles usually returns CV_32FC3. // So we can access `at(0, i)`. // If that fails, we can fall back. But since `Mat` has `at`, it should work unless generic is bad. // Let's assume it works for Mat but checking boundaries. // NOTE: If this throws "at not defined" (unlikely for Mat), we'd need another way. // But since the previous error was on `VecPoint2f` (which is NOT a Mat), this should be fine. for (int i = 0; i < numCircles; i++) { // Access using Vec3f if possible, or try to interpret memory // Using `at` is the standard way. final vec = circles.at(0, i); detected.add((x: vec.val1, y: vec.val2, r: vec.val3)); } if (detected.isEmpty) return TargetDetectionResult.failure(); // Cluster circles by center position // We consider circles "concentric" if their centers are within 5% of image min dimension final double tolerance = math.min(width, height) * 0.05; final List> clusters = []; 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; for (final c in cluster) { clusterX += c.x; clusterY += c.y; } clusterX /= cluster.length; clusterY /= cluster.length; final dist = math.sqrt( math.pow(circle.x - clusterX, 2) + math.pow(circle.y - clusterY, 2), ); if (dist < tolerance) { cluster.add(circle); added = true; break; } } if (!added) { clusters.add([circle]); } } // Find the best cluster // 1. Prefer clusters with more circles (concentric rings) // 2. Tie-break: closest to image center List<({double x, double y, double r})> bestCluster = clusters.first; double bestScore = -1.0; for (final cluster in clusters) { // Score calculation // Base score = number of circles * 10 double score = cluster.length * 10.0; // Penalize distance from center double cx = 0, cy = 0; for (final c in cluster) { cx += c.x; cy += c.y; } cx /= cluster.length; cy /= cluster.length; final distFromCenter = math.sqrt( math.pow(cx - width / 2, 2) + math.pow(cy - height / 2, 2), ); final relDist = distFromCenter / math.min(width, height); score -= relDist * 5.0; // Moderate penalty for off-center // Penalize very small clusters if they are just noise // (Optional: check if radii are somewhat distributed?) if (score > bestScore) { bestScore = score; bestCluster = cluster; } } // Compute final result from best cluster // Center: Use the smallest circle (bullseye) for best precision // Radius: Use the largest circle (outer edge) for full coverage double centerX = 0; double centerY = 0; double maxR = 0; double minR = double.infinity; for (final c in bestCluster) { if (c.r > maxR) { maxR = c.r; } if (c.r < minR) { minR = c.r; centerX = c.x; centerY = c.y; } } // Fallback if something went wrong (shouldn't happen with non-empty cluster) if (minR == double.infinity) { centerX = bestCluster.first.x; centerY = bestCluster.first.y; } return TargetDetectionResult( centerX: centerX / width, centerY: centerY / height, radius: maxR / math.min(width, height), success: true, ); } }