398 lines
16 KiB
C#
398 lines
16 KiB
C#
using OpenCvSharp;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace CircleDetectionApi.Helpers
|
|
{
|
|
public static class WheelDetector
|
|
{
|
|
/// <summary>
|
|
/// Detector especializado para rodas de veículos em imagens laterais
|
|
/// </summary>
|
|
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<CircleSegment> allCircles = new List<CircleSegment>();
|
|
|
|
// 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<CircleSegment> 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<CircleSegment> FilterCircles(List<CircleSegment> circles, Rect lowerRegion, Size imageSize)
|
|
{
|
|
var filtered = new List<CircleSegment>();
|
|
|
|
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<double>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Estima a quantidade de eixos baseado nas rodas detectadas
|
|
/// </summary>
|
|
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<double>();
|
|
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;
|
|
}
|
|
}
|
|
} |