Monocular Depth Map

Construire une depth map avec un flux d'image monoculaire

Utiliser un flux vidéo issu d’une seule caméra pour estimer la profondeur des objets dans l’image. Un petit plongeon dans le monde de la géométrie épipolaire et de l’analyse des points d’intérêts.

Depth maps ?

Si vous voulez donner à votre système de vision une notion de distance, il n’existe pas (encore !) 36 solutions :

  • Vision stéréo : Utiliser deux caméras et un peu d’ingéniosité
  • Vision monoculaire : utiliser une caméra et beaucoup plus d’ingéniosité.
  • Utiliser une caméra RGB-D, qui donne directement une information de profondeur.

Chacune de ces options a ses propres qualités et défauts. La vision stéréo donne des estimations correctes a une distance convenable, mais demande une puissance de calcul non négligeable. Un système monoculaire peut fonctionner avec du machine learning, ou en exploitant la suite d’image comme un système stéréo (Plus à ce sujet un peu plus tard), avec les mêmes limitations que celui-ci. La caméra RGB-D est très fiable et donne des mesures précises, mais ne fonctionne qu’à courte distance.

Eh bien, je n’ai pas de quoi faire un système stéréo, mais je possède la meilleure des caméras : une RGB-D (Oui, j’adore les RGB-D). Cette caméra est fantastique, mais son estimation de profondeur dépend exclusivement d’une projection infrarouge, ce qui amène son lot de problèmes. L’estimation de profondeur est dérangée par plusieurs facteurs :

  • Les surfaces sombres, ou très peu réfléchissantes
  • Les surfaces très réfléchissantes (miroirs)
  • les surfaces très texturées (comme les tapis)
  • La lumière du soleil, qui ajoute sa dose d’infrarouge à nos mesures
  • La distance, limitée généralement à 4 ou 5 mètres.

Un petit exemple d’une depth map incomplète (dataset: yoga_mat, de CAPE) : depth Holes Holes Holes

On observe des trous dans la mesure de profondeur, dues à des discontinuités de profondeur/une occlusion (à gauche du cylindre), et des trous qui découlent de la transformation géométrique de l’image pour correspondre à l’image RGB (en haut et à droite de l’image). Les trous alignés sur le sol sont dues (à mon avis) à la nature discrète de la mesure de profondeur, qui ressort ici à cause de la transformation géométrique.

Il y a un bon nombre de méthodes pour remplir ces discontinuités (Gaussian fusion, Machine learning, …) mais j’ai voulu tenter ma chance avec une idée assez simple : Pourquoi ne pas fusionner l’estimation de profondeur avec une autre estimation, moins précise, mais plus fiable ?

Eh bien, j’ai une caméra RGB-D sur mon bureau (et donc des mesures de profondeur), reste plus qu’à trouver une autre estimation de profondeur avec les données RGB.

La première étape, pas des plus simples, est d’obtenir l’image de profondeur monoculaire. C’est parti.

Map de profondeur monoculaire

Bon, comment on obtient une estimation de profondeur depuis une seule image ? Il n’y a pas une foule de réponses, la plupart d’entre elles sont “réseaux de neurones”.

Mais comme l’a évoqué notre mentor Maitre Yoda, There is another Merci Yoda !

Pourquoi ne pas utiliser deux images qui se suivent dans notre vidéo comme si elles étaient issues d’un système stéréo ? On pourrait alors déduire la transformée entre ces images et en extraire les informations 3D (cad de profondeur). Et vous savez quoi ? Cette transformée est relativement facile à obtenir.

La clef de cette histoire a un nom : la géométrie épipolaire. Je n’entrerais pas dans les détails ici, mais voici une explication approximative en une phrase : la géométrie épipolaire permet d’estimer la transformée d’une image vers une autre.

Ce n’est pas exact, ni précis, mais une meilleure explication se trouve facilement dans la documentation OpenCV.

La logique pour obtenir notre profondeur est assez simple :

  • Trouver les points clefs/caractéristiques/d’intérêt des deux images
  • Trouver quel point d’une image correspond à quel point de l’autre.
  • Estimer la transformation (épipolaire, vous suivez ?) à appliquer aux points d’une image pour retrouver les points de l’autre.
  • Obtenir une image de disparité (une sorte de représentation de profondeur dans notre cas)

Toutes ces opérations peuvent être réalisées à l’aide d’OpenCV.

Points d’intérêt

Pour trouver les points d’intérêt, j’ai utilisé le détecteur SURF. Ces points peuvent ensuite être associés à l’aide du DescriptorMatcher en utilisant FLANN.

Matched keypoints

On obtient les points d’intérêts associés entre les deux images. On peut ensuite en éliminer certains en fonction de leur distance au point d’origine, et obtenir une liste de points nettoyés.

Dans l’image ci-dessus, on peut voir au moins deux outliers, c’est-à-dire dans ce contexte, des points reliés a un autre point qui ne leur correspond pas. Ces outliers seront éliminés par notre test de distance évoqué précédemment, où seront supprimés par la prochaine étape.

Détails sur la représentation épipolaire Une note rapide: La projection épipolaire des deux images les déforme de façon à ce que deux points d’intérêts associés se retrouvent sur la même ligne. Ici, on dirait que c’est déjà le cas : C’est à cause de la proximité temporelle des deux images (issues d’une vidéo). Pour une meilleure visualisation (et explications en profondeur), consultez la page OpenCV.

Matrice fondamentale

OpenCV va également nous aider pour trouver la matrice de transformation entre les deux images. Appeler la fonction cv::findHomography sur nos pairs de points d’intérêts nous renverra une matrice fondamentale, que nous pouvons utiliser pour déformer notre deuxième image (avec la fonction cv::warpPerspective). J’ai utilisé findHomography avec le paramètre cv::FM_RANSAC, qui permet d’utiliser la méthode RANSAC. Nous pouvons en obtenir une liste d’in/outliers pour nettoyer encore nos données, mais ici, nous n’en aurons pas besoin.

Détails sur RANSAC

RANSAC (RANdom SAmples Consensus) est une méthode fantastique (mais non déterministe, personne n’est parfait) qui permet entre autre de détecter les outlier dans un set de données. Ici, on l’utilise pour associer les paires de points caractéristiques pour trouver la matrice fondamentale idéale malgré la présence d’outliers.

RANSAC a bien d’autres cas d’applications bien sûr, je l’ai également utilisé pour détecter les cylindres dans un graph de plans, comme CAPE l’a fait.

Stereo matching

La dernière astuce pour obtenir la depth map est aussi simple que la précédente. Nous pouvons utiliser une fois de plus le glorieux OpenCV, avec sa classe StereoSGBM. Cette classe est censée être utilisée sur des images issues d’un setup d’acquisition stéréo, mais maintenant que nos deux images sont projetées correctement, personne ne nous empêchera de faire semblant qu’elles viennent d’un setup stéréo !

Eh bien le résultat n’est pas si mal (après une bonne dose de réglages de paramètres) :

Simulated depth map

Il y a quelques trous, quelques discontinuités dans la profondeur et quelques aberrations au niveau du sommet du cylindre, mais c’est globalement pas mal.

Pour la comparaison, voici la version brute (avec les paramètres par défaut) :

Ugly mess map

On peut y voir assez clairement que des textures sensées êtres plates sont affichées avec du relief (une différence de couleur), notamment la feuille de QR codes. C’est également le cas en haut de l’image, mais la différence est moins visible.

Filtrage

La dernière étape (non obligatoire) est de filtrer un peu notre estimation, pour obtenir une depth map plus propre. Pour cela, j’utilise DisparityWLSDFilter de cv::ximproc, à utiliser normalement sur un setup stéréo.

Le résultat final est affiché sur l’image ci-dessous, avec la depth map monoculaire à gauche et la depth map issue directement de la caméra RGB-D à droite, afin de pouvoir les comparer :

Filtered result

Globalement, le résultat final est plutôt bon ! Mieux qu’attendu, dans mon cas.

Comparé à la depth map brut de la RGB-D, la depth map monoculaire a moins de trous de discontinuités (ces deux images devraient être exactement les mêmes dans un monde idéal). Quelques remarques tout de même :

  • La depth map monoculaire n’a pas d’unités (mètres, mm, …) et ne peux pas être utilisée tel quel.
  • Le processus de filtrage à créer des “fuites” de profondeur au niveau des côtés du cylindre (fond plus sombre)
  • La texture des QR codes est encore visible sur le sol, ce qui est une erreur d’estimation
  • Le haut du cylindre est montré plus proche de l’observateur que le sol en bas de l’image, et nous pouvons voir que c’est faux (voir image de profondeur brut pour comparaison)
  • Plus il y a de mouvement, meilleure est l’estimation. En cas de mouvement trop faible, on obtient une map aberrante.

Un autre point très important que je n’ai pas mentionné avant : L’estimation de profondeur monoculaire ne peut pas être aussi bonne que l’estimation stéréo. Les depth map stéréo sont bien plus précises et fiables. Elles fournissent également une vraie mesure de profondeur (avec des unités), car la distance/rotation entre les deux caméras est connue précisément, ou du moins, elle est fixe.

La qualité de la depth map dépend beaucoup de la distance : un exercice de trigonométrie basique indique que l’estimation de profondeur sera OK seulement pour des points dans la même échelle que cette distance. Ici, mes “deux” caméras se trouvent à un temps/distance proches l’une de l’autre, leur prise se suivant uniquement de quelques centièmes de secondes. La qualité d’estimation de profondeur sera néanmoins bonne pour des courtes distances, de l’ordre du mètre.

Mais malgré ces imperfections, je suis satisfait du résultat initial. Cette depth map monoculaire relève des informations utiles de l’environnement qui sont exactement ce qu’il me faut pour la prochaine étape : la fusion des depth map.

Un petit bonus

Voici le résultat de la map d’erreur, sortie tout droit du filtre. Comme prévu, les erreurs d’estimation de profondeur sont pour la plupart situées au niveau des discontinuités de profondeur.

Filtered result


Suggestions de lecture :