using OpenCvSharp; using OpenCvSharp.Dnn; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace CircleDetectionApi.Helpers { public class VehicleDetectionHelper { private readonly Net _net; private readonly string[] _objectClasses; private readonly string[] _vehicleClasses = new[] { "car", "motorbike", "bus", "truck", "bicycle" }; public VehicleDetectionHelper(string modelPath, string configPath, string classesPath) { // Carregar o modelo YOLO para detecção de objetos _net = CvDnn.ReadNetFromDarknet(configPath, modelPath); // Usar CPU ou GPU dependendo da disponibilidade _net.SetPreferableBackend(Backend.DEFAULT); _net.SetPreferableTarget(Target.CPU); // Carregar as classes do COCO dataset _objectClasses = File.ReadAllLines(classesPath); } public (bool isVehicle, string vehicleClass, Rect vehicleBox) DetectVehicle(Mat image) { // Obter as dimensões da imagem int height = image.Rows; int width = image.Cols; // Criar um blob a partir da imagem para alimentar o modelo using var blob = CvDnn.BlobFromImage( image, 1 / 255.0, new Size(416, 416), new Scalar(0, 0, 0), swapRB: true, crop: false); // Definir a entrada para a rede _net.SetInput(blob); // Obter os nomes das camadas de saída var outNames = _net.GetUnconnectedOutLayersNames(); // Executar a inferência var outs = new List(); _net.Forward(outs, outNames); // Listas para armazenar os resultados da detecção var boxes = new List(); var confidences = new List(); var classIds = new List(); // Para cada camada de saída foreach (var mat in outs) { // Para cada detecção for (int i = 0; i < mat.Rows; i++) { // Extrair as pontuações de confiança para cada classe var scores = Mat.FromPixelData(1, _objectClasses.Length, MatType.CV_32F, mat.Ptr(i, 5)); // Obter o índice da classe com maior pontuação Point classIdPoint = new Point(); double confidence = 0; Cv2.MinMaxLoc(scores, out _, out confidence, out _, out classIdPoint); // Usar um limiar de confiança mais baixo para veículos if (confidence > 0.3 && _vehicleClasses.Contains(_objectClasses[classIdPoint.X])) { // Calcular as coordenadas da caixa delimitadora int centerX = (int)(mat.At(i, 0) * width); int centerY = (int)(mat.At(i, 1) * height); int boxWidth = (int)(mat.At(i, 2) * width); int boxHeight = (int)(mat.At(i, 3) * height); int left = centerX - boxWidth / 2; int top = centerY - boxHeight / 2; // Armazenar os resultados boxes.Add(new Rect(left, top, boxWidth, boxHeight)); confidences.Add((float)confidence); classIds.Add(classIdPoint.X); } } } // Se nenhum veículo foi detectado pelo YOLO, tentar métodos alternativos if (boxes.Count == 0) { // Método alternativo: se a imagem tiver uma proporção típica de veículo e ocupar // a maior parte do quadro, consideramos como um possível veículo double aspectRatio = (double)width / height; // Veículos geralmente têm proporção entre ~0.5 (caminhão) e ~2.5 (carro lateral) if (aspectRatio >= 0.5 && aspectRatio <= 3.0) { // Criar uma caixa com margens de 10% em cada lado int marginX = (int)(width * 0.1); int marginY = (int)(height * 0.1); // Se a proporção for mais próxima de caminhões (~1.5-3.0 horizontalmente) string vehicleGuess = (aspectRatio >= 1.5 && aspectRatio <= 3.0) ? "truck" : "car"; return (true, vehicleGuess, new Rect( marginX, marginY, width - 2 * marginX, height - 2 * marginY )); } return (false, "Não detectado", new Rect()); } // Aplicar supressão não-máxima para eliminar detecções redundantes int[] indices; CvDnn.NMSBoxes(boxes, confidences, 0.3f, 0.4f, out indices); // Se nenhuma caixa sobreviver após NMS, usar o método alternativo if (indices.Length == 0) { // Mesmo método alternativo descrito acima double aspectRatio = (double)width / height; if (aspectRatio >= 0.5 && aspectRatio <= 3.0) { int marginX = (int)(width * 0.1); int marginY = (int)(height * 0.1); string vehicleGuess = (aspectRatio >= 1.5 && aspectRatio <= 3.0) ? "truck" : "car"; return (true, vehicleGuess, new Rect( marginX, marginY, width - 2 * marginX, height - 2 * marginY )); } return (false, "Não detectado", new Rect()); } // Obter a caixa com maior confiança int maxConfidenceIndex = 0; float maxConfidence = 0; for (int i = 0; i < indices.Length; i++) { int idx = indices[i]; if (confidences[idx] > maxConfidence) { maxConfidence = confidences[idx]; maxConfidenceIndex = idx; } } int bestClassId = classIds[indices[maxConfidenceIndex]]; string vehicleClass = _objectClasses[bestClassId]; // Obter a caixa delimitadora do veículo com maior confiança var vehicleBox = boxes[indices[maxConfidenceIndex]]; // Ajustar a caixa para garantir que esteja dentro dos limites da imagem vehicleBox.X = Math.Max(0, vehicleBox.X); vehicleBox.Y = Math.Max(0, vehicleBox.Y); vehicleBox.Width = Math.Min(width - vehicleBox.X, vehicleBox.Width); vehicleBox.Height = Math.Min(height - vehicleBox.Y, vehicleBox.Height); return (true, vehicleClass, vehicleBox); } // Método para desenhar a caixa delimitadora e informações do veículo na imagem public void DrawVehicleBox(Mat image, Rect box, string vehicleClass) { if (box.Width <= 0 || box.Height <= 0) return; // Desenhar a caixa delimitadora Cv2.Rectangle(image, box, new Scalar(0, 255, 0), 2); // Adicionar o texto da classe var textPoint = new Point(box.X, box.Y - 10); Cv2.PutText(image, vehicleClass, textPoint, HersheyFonts.HersheySimplex, 0.7, new Scalar(0, 255, 0), 2); } // Método para detectar eixos em um veículo (não apenas rodas individuais) public static List<(Point Center, int Radius)> DetectWheelsAndAxles(Mat image) { // Converter para escala de cinza using var grayMat = new Mat(); Cv2.CvtColor(image, grayMat, ColorConversionCodes.BGR2GRAY); // Aplicar blur para reduzir ruído using var blurredMat = new Mat(); Cv2.MedianBlur(grayMat, blurredMat, 5); // Aplicar equalização de histograma para melhorar o contraste using var equalizedMat = new Mat(); Cv2.EqualizeHist(blurredMat, equalizedMat); // Lista para armazenar resultados de múltiplos métodos var allCircles = new List<(Point Center, int Radius)>(); // Método 1: HoughCircles com parâmetros otimizados para rodas de veículos CircleSegment[] circles = Cv2.HoughCircles( equalizedMat, HoughModes.Gradient, 1, 100, // Distância mínima entre centros (aumentada para evitar detecções próximas) param1: 150, // Limite superior para detector de bordas Canny (reduzido) param2: 30, // Limite para detecção de centros (reduzido para detectar mais círculos) minRadius: 20, // Raio mínimo aumentado para focar em rodas maiores maxRadius: 150 // Raio máximo aumentado para rodas de caminhão ); if (circles != null && circles.Length > 0) { foreach (var circle in circles) { allCircles.Add((new Point((int)circle.Center.X, (int)circle.Center.Y), (int)circle.Radius)); } } // Método 2: Detecção baseada em bordas e contornos using var edges = new Mat(); Cv2.Canny(equalizedMat, edges, 50, 150); // Dilatação para conectar bordas próximas using var dilated = new Mat(); var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5)); Cv2.Dilate(edges, dilated, kernel); // Encontrar contornos Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(dilated, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); // Analisar contornos para encontrar possíveis rodas foreach (var contour in contours) { // Calcular área e perímetro double area = Cv2.ContourArea(contour); double perimeter = Cv2.ArcLength(contour, true); // Ignorar contornos muito pequenos if (area < 500) continue; // Calcular circularidade: 4π*área/perimetro² // Para um círculo perfeito, este valor é 1.0 double circularity = (4 * Math.PI * area) / (perimeter * perimeter); // Se for razoavelmente circular if (circularity > 0.5) { // Encontrar a bounding box var boundingRect = Cv2.BoundingRect(contour); // Calcular o centro e o raio aproximado var center = new Point( boundingRect.X + boundingRect.Width / 2, boundingRect.Y + boundingRect.Height / 2 ); int radius = Math.Max(boundingRect.Width, boundingRect.Height) / 2; // Verificar se já existe um círculo próximo (evitar duplicatas) bool isDuplicate = false; foreach (var existingCircle in allCircles) { // Calcular distância entre centros double distance = Math.Sqrt( Math.Pow(existingCircle.Center.X - center.X, 2) + Math.Pow(existingCircle.Center.Y - center.Y, 2) ); // Se a distância for menor que o raio, considerar como duplicata if (distance < radius) { isDuplicate = true; break; } } // Se não for duplicata, adicionar à lista if (!isDuplicate) { allCircles.Add((center, radius)); } } } // Método especial para detecção de eixos em caminhões: // Procurar rodas alinhadas horizontalmente com alturas similares var possibleAxles = new List<(Point Center, int Radius)>(); // Se temos mais de uma roda, analisar possíveis eixos if (allCircles.Count > 1) { // Agrupar rodas por altura similar (tolerância de 10% da altura da imagem) var heightGroups = allCircles .GroupBy(c => c.Center.Y / (image.Height * 0.1)) .Where(g => g.Count() >= 2) .ToList(); foreach (var group in heightGroups) { // Ordenar por posição X var orderedCircles = group.OrderBy(c => c.Center.X).ToList(); // Considerar pares de rodas como possíveis eixos for (int i = 0; i < orderedCircles.Count - 1; i++) { var left = orderedCircles[i]; var right = orderedCircles[i + 1]; // Calcular distância entre as rodas double distance = right.Center.X - left.Center.X; // Se a distância for razoável para um eixo (não muito pequena nem muito grande) if (distance > 50 && distance < image.Width * 0.8) { // Adicionar as duas rodas como um eixo possibleAxles.Add(left); possibleAxles.Add(right); } } } } // Se encontramos possíveis eixos, usar esses resultados if (possibleAxles.Count > 0) { return possibleAxles; } return allCircles; } } }