using OpenCvSharp; using System; using System.Collections.Generic; using System.Linq; namespace CircleDetectionApi.Helpers { public static class WheelDetector { /// /// Detector especializado para rodas de veículos em imagens laterais /// public static List<(Point Center, int Radius)> DetectWheelsInVehicle(Mat image) { var results = new List<(Point Center, int Radius)>(); // 1. Pré-processamento: converter para escala de cinza using var grayImage = new Mat(); Cv2.CvtColor(image, grayImage, ColorConversionCodes.BGR2GRAY); // 2. Aplicar equalização de histograma para melhorar o contraste using var equalizedImage = new Mat(); Cv2.EqualizeHist(grayImage, equalizedImage); // 3. Suavizar a imagem com blur para reduzir ruído using var blurredImage = new Mat(); Cv2.GaussianBlur(equalizedImage, blurredImage, new Size(5, 5), 0); // 4. Extrair bordas com Canny using var edges = new Mat(); Cv2.Canny(blurredImage, edges, 30, 150); // 5. Aplicar operações morfológicas para conectar bordas e remover ruído var structElement = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5)); using var dilatedEdges = new Mat(); Cv2.Dilate(edges, dilatedEdges, structElement); Cv2.Erode(dilatedEdges, dilatedEdges, structElement); // 6. Encontrar contornos Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(dilatedEdges, out contours, out hierarchy, RetrievalModes.List, ContourApproximationModes.ApproxSimple); // 7. Criar uma máscara para mostrar apenas a parte inferior da imagem (onde as rodas estão) int lowerRegionHeight = image.Height / 3; using var lowerRegionMask = new Mat(dilatedEdges.Size(), MatType.CV_8UC1, Scalar.Black); Rect lowerRect = new Rect(0, image.Height - lowerRegionHeight, image.Width, lowerRegionHeight); lowerRegionMask[lowerRect].SetTo(Scalar.White); // 8. Encontrar círculos usando HoughCircles com múltiplos parâmetros List allCircles = new List(); // Tentar com diferentes parâmetros para maior robustez DetectCirclesWithParams(blurredImage, allCircles, 1, 200, 120, 25, 20, 150); DetectCirclesWithParams(blurredImage, allCircles, 1, 150, 100, 20, 30, 200); DetectCirclesWithParams(blurredImage, allCircles, 1, 100, 80, 15, 40, 250); // 9. Filtrar círculos encontrados var filteredCircles = FilterCircles(allCircles, lowerRect, image.Size()); // 10. Procurar contornos circulares considerando proporções e posição na imagem var circularContours = FindCircularContours(contours, lowerRect); // 11. Combinar resultados de diferentes métodos results.AddRange(filteredCircles.Select(c => (new Point((int)c.Center.X, (int)c.Center.Y), (int)c.Radius))); results.AddRange(circularContours); // 12. Remover duplicatas (círculos muito próximos) results = RemoveDuplicateCircles(results); // 13. Analisar a distribuição horizontal e selecionar rodas mais prováveis if (results.Count > 0) { results = SelectMostProbableWheels(results, image.Size()); } return results; } // Detectar círculos com diferentes parâmetros private static void DetectCirclesWithParams( Mat image, List circles, double dp, double minDist, double param1, double param2, int minRadius, int maxRadius) { CircleSegment[] detectedCircles = Cv2.HoughCircles( image, HoughModes.Gradient, dp, minDist, param1: param1, param2: param2, minRadius: minRadius, maxRadius: maxRadius ); if (detectedCircles != null && detectedCircles.Length > 0) { circles.AddRange(detectedCircles); } } // Filtrar círculos fora da região de interesse e aqueles que são improváveis serem rodas private static List FilterCircles(List circles, Rect lowerRegion, Size imageSize) { var filtered = new List(); foreach (var circle in circles) { // Verificar se o círculo está próximo à parte inferior da imagem (onde as rodas estariam) bool isInLowerRegion = circle.Center.Y >= lowerRegion.Y; // Verificar se o raio é razoável (não muito pequeno nem muito grande) bool isReasonableSize = circle.Radius >= 20 && circle.Radius <= Math.Min(imageSize.Width, imageSize.Height) / 5; if (isInLowerRegion && isReasonableSize) { filtered.Add(circle); } } return filtered; } // Encontrar contornos que parecem circulares e têm características de rodas private static List<(Point Center, int Radius)> FindCircularContours(Point[][] contours, Rect lowerRegion) { var results = new List<(Point Center, int Radius)>(); foreach (var contour in contours) { // Ignorar contornos muito pequenos double area = Cv2.ContourArea(contour); if (area < 300) continue; // Calcular perímetro e circularidade double perimeter = Cv2.ArcLength(contour, true); double circularity = (4 * Math.PI * area) / (perimeter * perimeter); // Obter bounding rect e características var boundingRect = Cv2.BoundingRect(contour); // Verificar se está na parte inferior da imagem bool isInLowerRegion = boundingRect.Y + boundingRect.Height / 2 >= lowerRegion.Y; // Verificar se é relativamente circular (valor entre 0 e 1, onde 1 é um círculo perfeito) bool isCircular = circularity > 0.6; // Verificar se a proporção altura/largura está próxima de 1 (como um círculo) double aspectRatio = (double)boundingRect.Width / boundingRect.Height; bool hasGoodAspectRatio = aspectRatio >= 0.8 && aspectRatio <= 1.2; if (isInLowerRegion && isCircular && hasGoodAspectRatio) { // Calcular centro e raio aproximado Point center = new Point( boundingRect.X + boundingRect.Width / 2, boundingRect.Y + boundingRect.Height / 2 ); int radius = Math.Max(boundingRect.Width, boundingRect.Height) / 2; results.Add((center, radius)); } } return results; } // Remover círculos duplicados (círculos muito próximos um do outro) private static List<(Point Center, int Radius)> RemoveDuplicateCircles(List<(Point Center, int Radius)> circles) { var result = new List<(Point Center, int Radius)>(); // Ordenar círculos por tamanho (do maior para o menor) var sortedCircles = circles.OrderByDescending(c => c.Radius).ToList(); foreach (var circle in sortedCircles) { bool isDuplicate = false; foreach (var existingCircle in result) { double distance = Math.Sqrt( Math.Pow(existingCircle.Center.X - circle.Center.X, 2) + Math.Pow(existingCircle.Center.Y - circle.Center.Y, 2) ); // Se a distância for menor que o raio do maior círculo, considerar como duplicata if (distance < Math.Max(existingCircle.Radius, circle.Radius)) { isDuplicate = true; break; } } if (!isDuplicate) { result.Add(circle); } } return result; } // Selecionar as rodas mais prováveis com base na distribuição horizontal private static List<(Point Center, int Radius)> SelectMostProbableWheels(List<(Point Center, int Radius)> circles, Size imageSize) { // Se temos menos de 3 círculos, retornar todos if (circles.Count <= 3) return circles; // Ordenar por posição X (da esquerda para a direita) var sortedCircles = circles.OrderBy(c => c.Center.X).ToList(); // Calcular distâncias entre centros consecutivos var distances = new List(); for (int i = 0; i < sortedCircles.Count - 1; i++) { double distance = sortedCircles[i + 1].Center.X - sortedCircles[i].Center.X; distances.Add(distance); } // Ordenar as distâncias var sortedDistances = distances.OrderByDescending(d => d).ToList(); // Se houver uma diferença significativa entre as distâncias, isso pode indicar // eixos separados em um caminhão (como no caso do de 3 rodas visíveis) bool hasLargeGap = false; if (sortedDistances.Count >= 2) { double largestGap = sortedDistances[0]; double secondLargestGap = sortedDistances[1]; // Se a maior distância for significativamente maior que a segunda maior, // considerar que há uma separação clara entre grupos de rodas if (largestGap > secondLargestGap * 1.8) { hasLargeGap = true; } } // Tentar identificar rodas baseado na análise espacial if (hasLargeGap) { // Identificar o índice onde ocorre a maior distância int gapIndex = 0; double maxDistance = 0; for (int i = 0; i < distances.Count; i++) { if (distances[i] > maxDistance) { maxDistance = distances[i]; gapIndex = i; } } // Separar em grupos antes e depois da maior distância var leftGroup = sortedCircles.Take(gapIndex + 1).ToList(); var rightGroup = sortedCircles.Skip(gapIndex + 1).ToList(); // Selecionar rodas representativas de cada grupo var selectedCircles = new List<(Point Center, int Radius)>(); // Se houver apenas uma roda no grupo da esquerda, adicioná-la if (leftGroup.Count == 1) { selectedCircles.Add(leftGroup[0]); } // Caso contrário, pegar as duas rodas mais extremas (se houver mais de uma) else if (leftGroup.Count > 1) { selectedCircles.Add(leftGroup.First()); // Roda mais à esquerda selectedCircles.Add(leftGroup.Last()); // Roda mais à direita } // O mesmo para o grupo da direita if (rightGroup.Count == 1) { selectedCircles.Add(rightGroup[0]); } else if (rightGroup.Count > 1) { selectedCircles.Add(rightGroup.First()); selectedCircles.Add(rightGroup.Last()); } return selectedCircles; } else { // Se não houver uma distância claramente maior, selecionar com base no tamanho // e espaçamento, tipicamente para veículos menores com 2 rodas // Para casos com muitos círculos detectados, filtramos para ficar com os maiores // e mais bem espaçados if (circles.Count > 4) { // Ordenar por tamanho e pegar os maiores var largestCircles = circles.OrderByDescending(c => c.Radius) .Take(4) .OrderBy(c => c.Center.X) .ToList(); // Se ainda temos mais de 2 círculos, tentar agrupar if (largestCircles.Count > 2) { // Olhar para a distribuição espacial e pegar os mais separados var selected = new List<(Point Center, int Radius)>(); selected.Add(largestCircles.First()); // O mais à esquerda selected.Add(largestCircles.Last()); // O mais à direita return selected; } return largestCircles; } return circles; } } /// /// Estima a quantidade de eixos baseado nas rodas detectadas /// public static int EstimateAxles(List<(Point Center, int Radius)> wheels, Size imageSize) { if (wheels.Count == 0) return 0; // Para um único eixo (normalmente 2 rodas lado a lado) if (wheels.Count <= 2) return 1; // Para mais rodas, analisar a distribuição no eixo X var sortedByX = wheels.OrderBy(w => w.Center.X).ToList(); // Calcular distâncias entre rodas consecutivas var distances = new List(); for (int i = 0; i < sortedByX.Count - 1; i++) { distances.Add(sortedByX[i + 1].Center.X - sortedByX[i].Center.X); } // Se temos apenas 3 rodas, temos que decidir se é um veículo com 2 eixos // onde uma roda está oculta, ou um veículo com 3 eixos if (wheels.Count == 3) { // Se as distâncias entre as rodas são similares, provavelmente são 3 eixos individuais if (Math.Abs(distances[0] - distances[1]) < Math.Min(distances[0], distances[1]) * 0.3) return 3; // Se uma distância é significativamente menor, provavelmente são 2 eixos com uma roda oculta return 2; } // Para 4 rodas, verificar se estão agrupadas (2 eixos) ou igualmente espaçadas (4 eixos) if (wheels.Count == 4) { // Ordenar distâncias distances.Sort(); // Se a menor distância é significativamente menor que as outras, // provavelmente são 2 eixos com 2 rodas cada if (distances[0] < distances[2] * 0.5) return 2; // Se as distâncias são similares, podem ser 3 ou 4 eixos // Vamos considerar a proporção da imagem double aspectRatio = (double)imageSize.Width / imageSize.Height; // Veículos longos como caminhões provavelmente têm mais eixos if (aspectRatio > 2.0) return 4; return 3; } // Para 5 ou mais rodas, tentamos estimar pelo espaçamento // Ordenar distâncias e procurar gaps significativos distances.Sort(); // Se há uma diferença significativa entre distâncias, isso pode indicar grupos de eixos double maxDistance = distances.Last(); double averageDistance = distances.Average(); if (maxDistance > averageDistance * 2) { // Provavelmente temos agrupamentos de rodas // Estimar como metade do número de rodas arredondado para cima return (int)Math.Ceiling(wheels.Count / 2.0); } // Se as rodas estão uniformemente espaçadas, cada uma provavelmente representa um eixo return wheels.Count; } } }