TesteCaminhao/TesteImagemCaminhao/WheelDetector.cs
2025-03-17 11:13:59 -03:00

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;
}
}
}