Uso de la biblioteca OpenCV para reconocer arcos elípticos en secciones 2D de nubes de puntos 3D

En relación con la difusión más amplia de escáneres láser asequibles (lidares) capaces de recibir nubes de puntos 3D ( 3dOT ) y la aplicación más amplia de esta tecnología en varios campos (desde ingeniería hasta seguridad, desde la industria petrolera hasta la arquitectura), el interés en los algoritmos de procesamiento ha revivido Nubes de puntos.

Una de las aplicaciones populares de 3d en la industria es la creación de documentación de diseño solo para equipos construidos, viejos o convertidos, que generalmente consisten en tuberías y otras estructuras de geometría cilíndrica.

Para detectar primitivas geométricas en 3dOT , generalmente se utilizan bibliotecas 3D especializadas, por ejemplo, Microsoft PCL .. El enfoque con el uso de bibliotecas listas para usar, junto con las ventajas, tiene desventajas. Por ejemplo, es difícil incorporarlos en los esquemas de procesamiento de Kadov existentes, que generalmente tienen una dimensión 2D.

Consideremos cómo sería posible procesar 3dOT , por ejemplo, una estación de bombeo, comenzando con secciones 2D y utilizando todo el arsenal de procesamiento 2D, que está disponible en bibliotecas de procesamiento de imágenes confiables y optimizadas, por ejemplo OpenCV .


Figura 1. Modelo 3D OT de una estación de bombeo

El elemento principal de las secciones obtenidas al escanear varias estructuras de tuberías son los arcos elípticos .


Figura 2. Sección transversal horizontal de un modelo 3D de una estación de bombeo a un nivel promedio.

Para este artículo, restringimos nuestra consideración a un algoritmo clave que nos permite detectar arcos elípticos arbitrarios: este es un algoritmo iterativo para el crecimiento de segmentos de arco y unión de región ( crecimiento de región y enlace de borde ).

Los algoritmos de crecimiento son los más obvios y fácilmente verificables, aunque consumen mucho tiempo en comparación con los algoritmos estadísticos, que son más adecuados para el caso cuando la escena contiene objetos distantes y poco acoplados que pertenecen a una elipse. Estos algoritmos se discutirán en futuros artículos.


Por ahora, por simplicidad, omitimos el procedimiento para obtener una sección del archivo fuente 3DOT , preprocesando una sección, agrupándola para aislar primitivas geométricas, así como la posterior unión, rectificación y otras operaciones fotogramétricas necesarias para obtener los parámetros del modelo. No discutiremos la parametrización de algoritmos de búsqueda heurística de la misma manera. Describamos todas las operaciones básicas a partir de las cuales se construye el algoritmo.

Suponemos que necesitamos detectar (reconocer, clasificar) un arco elíptico (es decir, calcular los parámetros de la elipse, así como el ángulo inicial y final del arco elíptico) en esta imagen, recortada de la sección horizontal de la nube de puntos.


Figura 3. Uno de los arcos elípticos de la sección transversal del modelo 3D (después del suavizado)

Para minimizar el trabajo con el ráster a ciegas, realizaremos todas las operaciones con el ráster a través del contorno .

El procedimiento findContours de OpenCV encuentra en el tapete ráster todos los contornos externos (sin formas internas) en forma de un vector de vectores de puntos enteros (en coordenadas ráster):
 Mat mat(size);
 vector<vector<Point>> contours;
 findContours(mat, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

Esta es nuestra operación clave, que en algunos casos simples resuelve completamente la tarea . Pero como no siempre se encuentran casos degenerados, consideremos con más detalle la tecnología de procesamiento mediante contorneado.

La operación inversa, que genera un ráster de acuerdo con el circuito externo existente utilizando la función OpenCV , también parece simple:
 drawContours(mat, contours, -1, Scalar(255), -1);

También se usa con mucha frecuencia para enmascarar contornos, dibujar o calcular áreas.

Por lo tanto, en la etapa inicial, tenemos un conjunto de parches (piezas de cierta curva) que deben conectarse a un arco elíptico, eliminando partes de otros componentes de la estructura (por ejemplo, sujetadores) o ruido óptico causado por el sombreado durante el escaneo y otros razones.

Creemos una función discriminante que devolverá el tipo de contorno (elipse, segmento lineal, sombreado u otra cosa), así como los puntos finales del contorno y su rectángulo de contorno girado:
 contourTypeSearch(
   const vector<Point> &contour, Vec4i &def, RotatedRect &rc);

La relación entre la longitud y el ancho del rectángulo ayuda a discriminar rápidamente los contornos cercanos a los segmentos lineales , así como a los pequeños contornos de ruido .

El rectángulo girado en OpenCV tiene un complejo sistema de coordenadas. Si no se requiere el ángulo en sí, sino sus funciones trigonométricas , es mucho menos obvio desde el contexto. Si se usa el valor absoluto del ángulo , debe tenerse en cuenta que el ángulo se cuenta desde el horizontal hasta el primer borde del rectángulo en sentido antihorario y tiene un valor negativo .

Los puntos finales de los contornos elípticos se encuentran utilizando nuestro procedimiento, que recibe el ráster matecon un contorno discriminado extraído de la imagen original enmascarando y devuelve el defecto máximo :
 contourConvFeature(mat, &def, … );

El código principal para esta función es llamar a dos procedimientos OpenCV :

 vector<int> *hull = new vector<int>();
 convexHull(contour, *hull);
 vector<Vec4i> *defs = new vector<Vec4i>();
 convexityDefects(contour, *hull, *defs);

El primer procedimiento encuentra un polígono convexo para el contorno en estudio, el segundo calcula todos los defectos de convexidad .

Tomamos solo el defecto más grande en términos de convexidad, considerando que determina los puntos finales del contorno. Este puede no ser el caso si los límites externos o internos del contorno tienen características . Para suavizarlos , aplicamos suavizado adicional al contorno en estudio (y no a toda la imagen para no "desenfocar" los istmos entre los contornos y no violar la topología original).


Figura 4. Cálculo del defecto de abultamiento La

opción (a) define erróneamente el punto final rojo. Opción (b)define correctamente los puntos finales. La opción (c) redefine los puntos finales en la forma original.

Como en la tecnología que adoptamos, el circuito se regenera cada vez , tenemos que volver a buscar los puntos de correspondencia (o más bien, sus índices) mediante el exhaustivo procedimiento de búsqueda :
 nearestContourPtIdx(const vector<Point> &contour, const Point& pt);

Para los casos en que no es posible deshacerse por completo de las características, también se implementó un modo adicional de separación de arco (trabajar por separado con el arco interno / externo). Esto es importante, por ejemplo, en casos donde el arco externo del contorno está en contacto con otros objetos o es ruidoso . En este caso, puede ir a trabajar con el arco interno. En este caso, no es necesario procesar los arcos externos e internos por separado.

Además, de acuerdo con la fórmula bien conocida para la relación de convexidad del arco , el radio del círculo se estima aproximadamente y se rechazan las elipses demasiado grandes:
R = bulge / 2 + SQR(hypot) / (8 * bulge);

Por lo tanto, para todos los contornos, se encontró su métrica de defecto de convexidad (o se clasifican como lineales o pequeños y se eliminan del procedimiento). En la última etapa, se agregan parámetros adicionales a la métrica original, como el parámetro de dimensión girada, etc., y el conjunto completo de métricas en estudio se ordena por tamaño .
 typedef tuple<int , // 
   RotatedRect, //  
   Vec4i, //  
   int> // 
   RectDefMetric;


Algoritmo para vincular segmentos de arco en puntos finales


El algoritmo de crecimiento es claro y obvio: tomamos el contorno más grande como semilla y tratamos de cultivarlo, es decir, encontrar y adjuntar los parches más cercanos a sus puntos finales que satisfagan las condiciones de crecimiento. En la figura adulta, ingresamos el arco elíptico deseado . Enmascarar y restar la figura del conjunto original. Repetimos el procedimiento de crecimiento hasta que se agote el conjunto inicial .

El procedimiento básico del algoritmo de crecimiento se ve así:
 vector<Point> *patch =
    growingContours(contour, def, tmp, hull);

donde contorno es el contorno en estudio, def es su defecto de convexidad, el casco es el polígono convexo de toda la región, tmp es la matriz auxiliar de amortiguación. En la salida, obtenemos un contorno de crecimiento vectorial.

El procedimiento consiste en un ciclo de intentos de crecimiento de la semilla, que termina agotando los parches disponibles para el crecimiento o limitado por el parámetro del número máximo de iteraciones .


Figura 5. Muchos parches para el crecimiento sin semilla La

principal dificultad es seleccionar los parches más cercanos a los puntos finales del contorno, de modo que la figura crezca solo hacia adelante . Para dirección tangencialtomamos la línea promediada que pertenece al arco en la vecindad del punto final. En la Figura 6, se muestran los candidatos para la conexión a la semilla en una determinada iteración.


Figura 6. Semilla rodeada por una pluralidad de parches candidatos de crecimiento.

Para cada parche candidato, se calcula la siguiente métrica:
typedef tuple<
   double, //    2      2   
   bool, bool, //,  4   
   int, // 
   Vec4i> //  
   DistMetric;

Solo se tienen en cuenta los parches que caen en el cono tangencial . Luego, se selecciona el parche con la distancia más pequeña y, al imprimir la sección de conexión en la trama, se conecta al extremo correspondiente de la semilla. Para el otro extremo de la semilla, se busca un parche que coincida con los parámetros y, si se encuentra, también se conecta a la semilla. Luego, la semilla se enmascara y se resta de los muchos parches. El procedimiento se repite desde el principio.

Al final del procedimiento de crecimiento, obtuvimos un arco elíptico , que aún no se ha verificado.

Para comenzar, usando el estándar OpenCVEl procedimiento que recibe nuestro parche (en forma de una ruta, recordamos que la ruta y el ráster son intercambiables con nosotros) y devuelve la dimensión rotada , es decir, una elipse completa.
 RotatedRect box = fitEllipse(patch);

Luego, rechazamos las elipses demasiado grandes y demasiado pequeñas, y luego aplicamos nuestro procedimiento original para comparar las áreas del arco elíptico resultante y el parche de crecimiento inicial en forma de trama . Este procedimiento incluye algunos trucos disfrazados, por lo que omitiremos su descripción por ahora.

Y finalmente, encontramos los parámetros restantes de la elipse detectada: los ángulos de inicio y finalización (ya conocemos los semiejes de fitEllipse ).

Para determinar los ángulos inicial y final, procedemos de la siguiente manera: transformamos nuestra elipse completa, de vuelta al polígono, y por enumeración directa encontramos sus puntos más cercanos a nuestros extremos. Sus coordenadas angulares(de hecho, índices) y serán los ángulos inicial y final de un arco elíptico. En el código, se ve así (un poco 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);
}

Nuestro procedimiento cutSides tiene en cuenta la topología transversal del arco elíptico. En total, deben considerarse ocho posibles casos de omisión de los índices i0, i1, i2 . ¿Seguimos el contorno exterior o el interior, y cuál de los índices es mayor, inicial o final?

Código más fácil de ver:
 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) {...}
        }}}

Algunos resultados de la detección de elipses en casos complejos se muestran en la Figura 7 .



En los siguientes artículos, se considerarán los métodos de detección estadística.

All Articles