Usando a biblioteca OpenCV para reconhecer arcos elípticos em seções 2D de nuvens de pontos 3D

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 A

opçã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 A

principal 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 crescimento

Para cada amostra candidata, é calculada a seguinte métrica:
typedef tuple<
   double, //    2      2   
   bool, bool, //,  4   
   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.

All Articles