Em conexão com a ampla gama de scanners a laser acessíveis (lidares) capazes de receber nuvens de pontos 3D ( 3dOT ) e a aplicação mais ampla dessa tecnologia em vários campos (da engenharia mecânica à segurança, da indústria do petróleo à arquitetura), o interesse pelos algoritmos de processamento foi revivido nuvens de pontos.
Uma das aplicações populares do 3d na indústria é a criação de documentação de projeto apenas para equipamentos construídos, antigos ou convertidos, que geralmente consistem em tubulações e outras estruturas de geometria cilíndrica.
Para detectar primitivas geométricas no 3dOT , geralmente são usadas bibliotecas 3D especializadas, por exemplo, Microsoft PCL .. A abordagem com o uso de bibliotecas prontas, além de vantagens, apresenta desvantagens. Por exemplo, é difícil incorporá-los aos esquemas de processamento Kadov existentes, que geralmente têm uma dimensão 2D.
Vamos considerar como seria possível processar o 3dOT , por exemplo, uma estação de bombeamento, começando com seções 2D e usando todo o arsenal de processamento 2D, disponível em bibliotecas confiáveis e otimizadas de processamento de imagem, por exemplo, OpenCV .

Figura 1. Modelo 3D OT de uma estação de bombeamento
O principal elemento das seções obtidas pela varredura de várias estruturas de tubos são arcos elípticos .

Figura 2. Seção transversal horizontal de um modelo 3D de uma estação de bombeamento em um nível médio.
Para este artigo, restringimos nossa consideração a um algoritmo chave que nos permite detectar arcos elípticos arbitrários - este é um algoritmo iterativo para o crescimento de segmentos de arco e ligação de região ( crescimento de região e ligação de borda ).
Os algoritmos de crescimento são os mais óbvios e facilmente verificáveis, embora demorados em comparação com os algoritmos estatísticos, que são mais adequados para o caso em que a cena contém objetos distantes e fracamente acoplados que pertencem a uma elipse. Esses algoritmos serão discutidos em artigos futuros.
Por enquanto, por simplicidade, omitimos o procedimento para obter uma seção do arquivo 3dOT de origem , pré-processando uma seção, agrupando-a para isolar primitivas geométricas, bem como operações subsequentes de ligação, retificação e outras operações fotogramétricas necessárias para obter os parâmetros do modelo. Não discutiremos a parametrização de algoritmos de pesquisa heurística da mesma maneira. Vamos descrever todas as operações básicas a partir das quais o algoritmo é construído.Assumimos que precisamos detectar (reconhecer, classificar) um arco elíptico (isto é, calcular os parâmetros da elipse, bem como o ângulo inicial e final do arco elíptico) nesta imagem, cortados na seção horizontal da nuvem de pontos.
Figura 3. Um dos arcos elípticos da seção transversal do modelo 3D (após suavização)A fim de minimizar o trabalho com a varredura às cegas, realizaremos todas as operações com a varredura através do esboço .O procedimento findCours do OpenCV localiza no raster mat todos os contornos externos (sem formas internas) na forma de um vetor de vetores de pontos inteiros (em coordenadas raster): Mat mat(size);
vector<vector<Point>> contours;
findContours(mat, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Esta é a nossa operação principal, que em alguns casos simples resolve completamente a tarefa . Porém, como nem sempre são encontrados casos degenerados, vamos considerar com mais detalhes a tecnologia de processamento através do contorno.A operação reversa, gerando uma varredura de acordo com o circuito externo existente usando a função OpenCV , também parece simples: drawContours(mat, contours, -1, Scalar(255), -1);
Também é frequentemente usado para mascarar contornos, desenhar ou calcular áreas.Assim, no estágio inicial, temos um conjunto de remendos (peças de uma determinada curva) que precisam ser conectados a um arco elíptico, eliminando partes de outros componentes da estrutura (por exemplo, fixadores) ou ruído óptico causado por sombras durante a digitalização e outras razões.Vamos criar uma função discriminante que retornará o tipo de contorno (elipse, segmento linear, hachura ou outra coisa), bem como os pontos finais do contorno e seu retângulo de contorno girado: contourTypeSearch(
const vector<Point> &contour, Vec4i &def, RotatedRect &rc);
A proporção do comprimento e largura do retângulo ajuda a discriminar rapidamente contornos próximos a segmentos lineares , bem como pequenos contornos de ruído .O retângulo girado no OpenCV possui um sistema de coordenadas complexo . Se não é o próprio ângulo que é necessário, mas suas funções trigonométricas , é mais ou menos óbvio do contexto. Se o valor absoluto do ângulo for usado , deve-se levar em consideração que o ângulo é contado da horizontal até a primeira aresta do retângulo no sentido anti-horário e tem um valor negativo .Os pontos finais dos contornos elípticos são encontrados usando nosso procedimento, que recebe o raster matcom um contorno discriminado extraído da imagem original mascarando e retornando o defeito máximo : contourConvFeature(mat, &def, … );
O código principal para esta função é chamar dois procedimentos OpenCV : vector<int> *hull = new vector<int>();
convexHull(contour, *hull);
vector<Vec4i> *defs = new vector<Vec4i>();
convexityDefects(contour, *hull, *defs);
O primeiro procedimento encontra um polígono convexo para o contorno em estudo, o segundo - calcula todos os defeitos de convexidade .Tomamos apenas o maior defeito em termos de convexidade, considerando que ele determina os pontos finais do contorno. Pode não ser esse o caso se os limites externos ou internos do contorno tiverem recursos . Para suavizá- los , aplicamos suavização adicional ao contorno em estudo (e não à imagem inteira, para não "embaçar" os istmuses entre os contornos e não violar a topologia original).
Figura 4. Cálculo do defeito de protuberância Aopção (a) define erroneamente o ponto final vermelho. Opção (b)define corretamente os pontos de extremidade. A opção (c) redefine os pontos finais na forma original.Como na tecnologia adotada, o circuito é regenerado a cada vez , é preciso procurar novamente os pontos de correspondência (ou melhor, seus índices) pelo exaustivo procedimento de busca : nearestContourPtIdx(const vector<Point> &contour, const Point& pt);
Nos casos em que não é possível se livrar completamente dos recursos, também foi implementado um modo adicional de separação de arco (trabalhe separadamente com o arco interno / externo). Isso é importante, por exemplo, nos casos em que o arco externo do contorno está em contato com outros objetos ou é barulhento . Nesse caso, você pode trabalhar com o arco interno. Nesse caso, não é necessário processar os arcos externo e interno separadamente.Além disso, de acordo com a fórmula conhecida para a razão da convexidade do arco , o raio do círculo é estimado aproximadamente e elipses muito grandes são rejeitadas:R = bulge / 2 + SQR(hypot) / (8 * bulge);
Assim, para todos os contornos, foi encontrada sua métrica de defeito de convexidade (ou são classificados como lineares ou pequenos e removidos do procedimento). No último estágio, parâmetros adicionais são adicionados à métrica inicial, como o parâmetro de dimensão girada, etc., e o conjunto completo das métricas estudadas é ordenado por tamanho . typedef tuple<int ,
RotatedRect,
Vec4i,
int>
RectDefMetric;
Algoritmo para vincular segmentos de arco nos pontos finais
O algoritmo de crescimento é claro e óbvio: pegamos o maior contorno como semente e tentamos cultivá-lo, ou seja, encontramos e anexamos os patches mais próximos aos seus pontos finais que satisfazem as condições de crescimento. Na figura adulta, inserimos o arco elíptico desejado . Mascarar e subtrair a figura do conjunto original. Repetimos o procedimento de crescimento até que o conjunto inicial acabe .O procedimento básico do algoritmo de crescimento é assim: vector<Point> *patch =
growingContours(contour, def, tmp, hull);
onde contorno é o contorno em estudo, def é seu defeito de convexidade, casco é o polígono convexo de toda a região, tmp é a matriz de buffer auxiliar. Na saída, obtemos um contorno de vetor aumentado.O procedimento consiste em um ciclo de tentativas de semear o crescimento, terminando esgotando os patches disponíveis para o crescimento ou limitados pelo parâmetro do número máximo de iterações .
Figura 5. Muitas amostras para crescimento sem semente Aprincipal dificuldade é selecionar as amostras mais próximas aos pontos finais do contorno, para que a figura cresça apenas para a frente . Para direção tangencialtomamos a linha média pertencente ao arco nas proximidades do ponto final. Na Figura 6, são exibidos os candidatos à conexão com a semente em uma determinada iteração.
Figura 6. Sementes cercadas por uma pluralidade de amostras candidatas a crescimentoPara cada amostra candidata, é calculada a seguinte métrica:typedef tuple<
double,
bool, bool,
int,
Vec4i>
DistMetric;
Somente manchas que caem no cone tangencial são levadas em consideração . Em seguida, o patch com a menor distância é selecionado e, imprimindo a seção de conexão na varredura, se conecta à extremidade correspondente da semente. Para a outra extremidade da semente, é pesquisado um patch correspondente aos parâmetros e, se encontrado, também é conectado à semente. Em seguida, a semente é mascarada e subtraída dos muitos fragmentos. O procedimento é repetido desde o início.Ao final do processo de crescimento, obtivemos um arco elíptico , que ainda precisa ser verificado.Para começar, use o OpenCV padrãoo procedimento que nosso patch recebe (na forma de um caminho, lembramos que o caminho e a varredura são intercambiáveis conosco) e retorna a dimensão girada , ou seja, uma elipse completa. RotatedRect box = fitEllipse(patch);
Em seguida, rejeitamos elipses muito grandes e muito pequenas e aplicamos nosso procedimento original para comparar as áreas do arco elíptico resultante e o fragmento inicial de crescimento na forma de varredura . Este procedimento inclui alguns truques disfarçados, portanto omitiremos sua descrição por enquanto.E, finalmente, encontramos os parâmetros restantes da elipse detectada - os ângulos de início e fim (já conhecemos os semi-eixos do fitEllipse ).Para determinar os ângulos inicial e final, procedemos da seguinte forma: transformamos nossa elipse completa, de volta ao polígono e , por enumeração direta, encontramos seus pontos mais próximos às nossas extremidades. Suas coordenadas angulares(de fato, índices) e serão os ângulos inicial e final de um arco elíptico. No código, fica assim (um pouco simplificado): pair<int, int>
ellipseAngles(const RotatedRect &box,
vector<Point> &ell, const Point &ps,
const Point &pe, const Point &pm)
{
vector<Point> ell0;
ellipse2Poly(Point(box.center.x, box.center.y),
Size(box.size.width / 2, box.size.height / 2),
box.angle, 0, 355, 5, ell0);
int i0 = nearestContourPtIdx(ell0, ps);
int i1 = nearestContourPtIdx(ell0, pe);
cutSides(ell0, i0, i1, i2, ell, nullptr);
return pair<int, int>(i0, i1);
}
Nosso procedimento cutSides leva em consideração a topologia transversal do arco elíptico. No total, oito possíveis casos de desvio dos índices i0, i1, i2 devem ser considerados . Vamos ao longo do contorno externo ou interno e qual dos índices é maior, inicial ou final?Mais fácil de ver código: void cutSides(
const vector<Point> &ell0, int i0, int i1, int i2,
vector<Point> *ell_in, vector<Point> *ell_out)
{
if (i0 < i1) {
if (i2 > i0 && i2 < i1) {
if (ell_in) {...}
if (ell_out) {...}
} else {
if (ell_in) {...}
if (ell_out) {...}
}}
else {
if (i2 > i1 && i2 < i0) {
if (ell_in) {...}
if (ell_out) {...}
} else {
if (ell_in) {...}
if (ell_out) {...}
}}}
Alguns resultados da detecção de elipses em casos complexos são mostrados na Figura 7 .
Nos artigos a seguir, serão considerados métodos de detecção estatística.