Utilisation de la bibliothèque OpenCV pour reconnaître des arcs elliptiques dans des sections 2D de nuages ​​de points 3D

En lien avec la plus large diffusion de scanners laser abordables (lidars) capables de recevoir des nuages ​​de points 3D ( 3dOT ) et l'application plus large de cette technologie dans divers domaines (de l'ingénierie mécanique à la sécurité, de l'industrie pétrolière à l'architecture), l'intérêt pour les algorithmes de traitement a repris. nuages ​​de points.

L'une des applications populaires de 3d dans l'industrie est la création de documentation de conception pour les équipements construits, anciens ou convertis, qui se composent généralement de pipelines et d'autres structures de géométrie cylindrique.

Pour détecter les primitives géométriques dans 3dOT , des bibliothèques 3D spécialisées, par exemple, Microsoft PCL , sont généralement utilisées. L'approche avec l'utilisation de bibliothèques prêtes à l'emploi, avec des avantages, présente des inconvénients. Par exemple, il est difficile de les intégrer dans les schémas de traitement Kadov existants, qui ont généralement une dimension 2D.

Voyons comment il serait possible de traiter 3dOT , par exemple une station de pompage, en commençant par des sections 2D et en utilisant tout l'arsenal de traitement 2D, qui est disponible dans des bibliothèques de traitement d'image fiables et optimisées, par exemple OpenCV .


Figure 1. Modèle OT 3D d'une station de pompage

L'élément principal des sections obtenues en scannant diverses structures de tuyaux sont des arcs elliptiques .


Figure 2. Coupe horizontale d'un modèle 3D d'une station de pompage à un niveau moyen.

Pour cet article, nous limitons notre examen à un algorithme clé qui nous permet de détecter des arcs elliptiques arbitraires - il s'agit d'un algorithme itératif pour la croissance de segments d'arc et la liaison de régions ( croissance de régions et liaison de bords ).

Les algorithmes de croissance sont les plus évidents et les plus facilement vérifiables, quoique longs en comparaison avec les algorithmes statistiques, qui conviennent mieux au cas où la scène contient des objets distants à couplage lâche qui appartiennent à une ellipse. Ces algorithmes seront discutés dans de futurs articles.


Pour l'instant, pour des raisons de simplicité, nous omettons la procédure d'obtention d'une section à partir du fichier 3dOT source , le prétraitement d'une section, son regroupement pour isoler les primitives géométriques, ainsi que la liaison, la rectification et les autres opérations photogrammétriques nécessaires pour obtenir les paramètres du modèle. Nous ne discuterons pas de la même manière le paramétrage des algorithmes de recherche heuristique. Décrivons toutes les opérations de base à partir desquelles l'algorithme est construit.

Nous supposons que nous devons détecter (reconnaître, classer) un arc elliptique (c'est-à-dire calculer les paramètres de l'ellipse, ainsi que l'angle initial et final de l'arc elliptique) dans cette image, découpée dans la section horizontale du nuage de points.


Figure 3. L'un des arcs elliptiques de la section transversale du modèle 3D (après lissage)

Afin de minimiser le travail avec le raster à l'aveugle, nous effectuerons toutes les opérations avec le raster en décrivant .

La procédure OpenCV findContours recherche sur le tapis raster tous les contours externes (sans formes internes) sous la forme d'un vecteur de vecteurs de points entiers (en coordonnées raster):
 Mat mat(size);
 vector<vector<Point>> contours;
 findContours(mat, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

Il s'agit de notre opération clé, qui dans certains cas simples résout complètement la tâche . Mais comme les cas dégénérés ne sont pas toujours retrouvés, considérons plus en détail la technologie de traitement par contournage.

L'opération inverse, générant un raster en fonction du circuit externe existant à l'aide de la fonction OpenCV , semble également simple:
 drawContours(mat, contours, -1, Scalar(255), -1);

Il est également très souvent utilisé pour masquer les contours, dessiner ou calculer la surface.

Ainsi, au stade initial, nous avons un ensemble de patchs (morceaux d'une certaine courbe) qui doivent être connectés en un arc elliptique, éliminant les parties d'autres composants de la structure (par exemple, les attaches) ou le bruit optique provoqué par l'ombrage pendant la numérisation et autres les raisons.

Créons une fonction discriminante qui retournera le type de contour (ellipse, segment linéaire, hachures ou autre), ainsi que les points finaux du contour et son rectangle de contour pivoté:
 contourTypeSearch(
   const vector<Point> &contour, Vec4i &def, RotatedRect &rc);

Le rapport entre la longueur et la largeur du rectangle permet de distinguer rapidement les contours proches des segments linéaires , ainsi que les petits contours de bruit .

Le rectangle pivoté dans OpenCV a un système de coordonnées complexe . Si ce n'est pas l'angle lui-même qui est requis, mais ses fonctions trigonométriques , c'est d'autant moins évident du contexte. Si la valeur absolue de l'angle est utilisée , il faut tenir compte du fait que l' angle est compté de l'horizontale au premier bord du rectangle dans le sens antihoraire et a une valeur négative .

Les extrémités des contours elliptiques sont trouvées en utilisant notre procédure, qui reçoit le mat matricielavec un contour discret extrait de l'image originale par masquage et renvoie le défaut maximum :
 contourConvFeature(mat, &def, … );

Le code principal de cette fonction est d'appeler deux procédures OpenCV :

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

La première procédure trouve un polygone convexe pour le contour étudié, la seconde - calcule tous les défauts de convexité .

Nous ne prenons que le plus grand défaut en termes de convexité, considérant qu'il détermine les points terminaux du contour. Cela peut ne pas être le cas si les limites externes ou internes du contour ont des caractéristiques . Afin de les lisser , nous appliquons un lissage supplémentaire au contour étudié (et non à l'image entière afin de ne pas «brouiller» les isthmes entre les contours et ne pas violer la topologie d'origine).


Figure 4. Calcul du défaut de gonflement L'

option (a) définit par erreur le point final rouge. Option (b)définit correctement les points de terminaison. L'option (c) redéfinit les points d'extrémité sur la forme d'origine.

Puisque dans la technologie que nous avons adoptée, le circuit est régénéré à chaque fois , nous devons rechercher à nouveau les points de correspondance (ou plutôt leurs indices) par la procédure de recherche exhaustive :
 nearestContourPtIdx(const vector<Point> &contour, const Point& pt);

Pour les cas où il n'est pas possible de se débarrasser complètement des fonctionnalités, un mode supplémentaire de séparation d'arc a également été implémenté (travailler séparément avec l'arc interne / externe). Ceci est important, par exemple, dans les cas où l'arc externe du contour est en contact avec d'autres objets ou est bruyant . Dans ce cas, vous pouvez aller travailler avec l'arc interne. Dans ce cas, il n'est pas nécessaire de traiter séparément les arcs externes et internes.

De plus, selon la formule bien connue du rapport de la convexité de l'arc , le rayon du cercle est approximativement estimé et les ellipses trop grandes sont rejetées:
R = bulge / 2 + SQR(hypot) / (8 * bulge);

Ainsi, pour tous les contours, leur métrique de défaut de convexité a été trouvée (ou ils sont classés comme linéaires ou petits et retirés de la procédure). À la dernière étape, des paramètres supplémentaires sont ajoutés à la métrique d'origine, tels que le paramètre de dimension pivotée, etc., et l'ensemble complet des métriques à l'étude est trié par taille .
 typedef tuple<int , // 
   RotatedRect, //  
   Vec4i, //  
   int> // 
   RectDefMetric;


Algorithme pour relier des segments d'arc aux points d'extrémité


L'algorithme de croissance est clair et évident: nous prenons le plus grand contour comme graine et essayons de le cultiver, c'est-à-dire de trouver et d'attacher les patchs les plus proches à ses points finaux qui satisfont aux conditions de croissance. Dans la figure adulte, nous entrons dans l' arc elliptique souhaité . Masquez et soustrayez la figure de l'ensemble d'origine. Nous répétons la procédure de croissance jusqu'à épuisement de la série initiale .

La procédure de base de l'algorithme de croissance ressemble à ceci:
 vector<Point> *patch =
    growingContours(contour, def, tmp, hull);

contour est le contour à l'étude, def est son défaut de convexité, coque est le polygone convexe de toute la région, tmp est la matrice tampon auxiliaire. À la sortie, nous obtenons un contour développé par vecteur.

La procédure consiste en un cycle de tentatives de germination de la croissance, se terminant soit par l'épuisement des patchs disponibles pour la croissance, soit limité par le paramètre du nombre maximal d'itérations .


Figure 5. Nombreux patchs pour la croissance sans graines La

principale difficulté est de sélectionner les patchs les plus proches des points d'extrémité du contour, de sorte que la figure ne pousse que vers l'avant . Pour la direction tangentiellenous prenons la ligne moyenne appartenant à l'arc au voisinage du point final. La figure 6 affiche les candidats pour la connexion à la graine à une certaine itération.


Figure 6. Graine entourée d'une pluralité de patchs candidats à la croissance

Pour chaque patch candidat, la métrique suivante est calculée:
typedef tuple<
   double, //    2      2   
   bool, bool, //,  4   
   int, // 
   Vec4i> //  
   DistMetric;

Seuls les patchs qui tombent dans le cône tangentiel sont pris en compte . Ensuite, le patch avec la plus petite distance est sélectionné et, en imprimant la section de connexion dans le raster, se connecte à l'extrémité correspondante de la graine. Pour l'autre extrémité de la graine, un patch correspondant aux paramètres est recherché et, s'il est trouvé, est également connecté à la graine. Ensuite, la graine est masquée et soustraite des nombreux patchs. La procédure est répétée depuis le tout début.

À la fin de la procédure de croissance, nous avons obtenu un arc elliptique , qui reste à vérifier.

Pour commencer, en utilisant le standard OpenCVla procédure que notre patch reçoit (sous la forme d'un chemin, nous nous souvenons que le chemin et le raster sont interchangeables avec nous) et renvoie la dimension pivotée , c'est-à-dire une ellipse complète.
 RotatedRect box = fitEllipse(patch);

Ensuite, nous rejetons les ellipses trop grandes et trop petites, puis nous appliquons notre procédure originale pour comparer les zones de l' arc elliptique résultant et le patch de croissance initial sous forme raster . Cette procédure comprend quelques astuces déguisées, nous allons donc omettre sa description pour l'instant.

Et enfin, nous trouvons les paramètres restants de l'ellipse détectée - les angles de début et de fin (nous connaissons déjà les demi-axes de fitEllipse ).

Pour déterminer les angles de début et de fin, nous procédons comme suit: nous transformons notre ellipse complète, de retour au polygone, et par dénombrement direct nous trouvons ses points les plus proches de nos extrémités. Leurs coordonnées angulaires(en fait, les indices) et seront les angles de début et de fin d'un arc elliptique. En code, cela ressemble à ceci (un peu simplifié):
 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);
}

Notre procédure cutSides prend en compte la topologie de la traversée de l'arc elliptique. Au total, huit cas possibles de contournement des indices i0, i1, i2 doivent être considérés . Allons-nous le long du contour extérieur ou intérieur, et lequel des indices est le plus grand, initial ou final?

Code plus facile à voir:
 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) {...}
        }}}

Certains résultats de la détection d'ellipses dans des cas complexes sont présentés à la figure 7 .



Dans les articles suivants, les méthodes de détection statistique seront considérées.

All Articles