Capture de Textures par Photométrie Stéréo

Capture de Textures par Photométrie Stéréo

Christophe Bolduc

Introduction

En infographie, l'utilisation de textures est largement utilisée pour augmenter le photoréalisme, permettre un rendu artistique et augmenter la quantité de détails sans avoir à augmenter la géométrie d'une scène. Cependant, les librairies de textures disponibles en ligne sont soit dispendieuses, soit peu variées ou incomplètes. De plus, les logiciels permettant de synthétiser des textures sont complexes, dispendieux et parfois non nécéssaire (surtout si le matériau existe physiquement!). Le présent document présente les résultats de l'exploration de photométrie stéréo pour la capture de normal et d'albedo de textures photographiées. La première partie illustre le montage physique pour la capture d'images. La seconde partie consiste en l'implentation de la méthode présentée par Woodham pour extraire l'orientation et la couleur de base d'une surface. Puis, l'utilisation de la normal map pour obtenir une image de profondeur est présentée. Ensuite, un algorithme simple pour permettre la répétition d'une texture est implémenté. Le travail est ensuite regroupé dans une interface pour simplifier l'utilisation et visualiser les résultats. Finalement, des résultats sont présentés et de possibles améliorations sont visitées.

Note: Cliquer sur une image l'ouvre en pleine taille, permettant de l'agrandir.

Montage

Puisque l'algorithme requiert plusieurs photos d'une même surface avec un éclairage varié, l'utilisation d'un trépied est impératif. Pour que la caméra utilisée (Canon 70D) soit orientée en direction du bas, celle-ci est attachée au bas du trépied à l'aide d'un équerre. De plus, pour éviter tout mouvement dans la prise de photographies, l'appareil est déclenché à distance à l'aide d'un téléphone cellulaire. Les photographies sont prises en mode RAW pour assurer la linéarité de la valeur des pixels selon l'intensité de l'éclairage dans l'image résultante. La surface à capturer est étendue en-dessous de la caméra et une lumière au DEL de faible diamètre est déplacée par l'utilisateur entre chaque photographie. Pour être en mesure d'identifier la direction de la lumière par la suite, une sphère métallique est placée dans un coin de l'image. Pour éviter l'effet indésirable de l'éclairage ambient, les photographies ont étés prises dans un endroit sombre ou durant la nuit pour les captures extérieures. L'image suivante illustre le montage utilisé.

Montage utilisé

Normal et Albedo

Direction de la lumière

La direction de la lumière doit d'abord être identifiée pour chacune des images. Pour ce faire, l'algorithme suivant a été implanté:

  • Une première image est chargée et l'utilisateur identifie la région dans laquelle se trouve la sphère et son diamètre approximatif
  • Pour chaque image dans le répertoire chargée en noir et blanc avec un filtre gaussien pour retirer les hautes fréquences:
    • La transformée de Hough pour un cercle est implanté
    • La moyenne de position des pixels au-dessus d'un seuil d'intensité est calculée; Le pixel avec la plus grande valeur est choisi si aucun pixel n'est plus grand que le seuil
    • Le vecteur normal de la sphère au highlight est calculée:
    • $${N_x=\frac{HighLight_x - Centre_x}{rayon}}$$

      $${N_y=\frac{HighLight_y - Centre_y}{rayon}}$$

      $${N_z=\sqrt{1 - N_x^2 - N_y^2}}$$

    • La direction de la lumière pour cette image est calculée (avec R = [0 0 1].T puisque la caméra pointe vers le bas):
    • $${L=2(N*R)N-R}$$

    • Cette dernière est enregistrée dans un fichier txt du même nom que l'image

L'algorithme est implémenté dans le fichier direction_lumiere_sphere_raw.py.

L'algorithme appliqué à une série de photos identifie les sphères avec les highlights suivants:

L'algorithme n'identifie pas parfaitement la sphère, mais les résultats obtenus sont suffisamment près de la sphère réelle pour estimer avec assez de précision la direction de lumière. Les réflexions circulaires dans la sphère ainsi que l'ombrage rond produit contribuent à ces légers décalages. De plus, il serait fastidieux d'identifier la sphère sur chaque image et il est plus facile de valider les images résultantes dans la console et relancer le programme si la sphère est mal identifiée.

Résolution Normal et Albedo

Ayant à présent la direction de la lumière pour chaque image, la couleur de base de la surface (appelée albedo) ainsi que sa normal peuvent être identifiés. Pour ce faire, on suppose une surface avec une réflexion diffuse, c'est-à-dire que son intensité est égale peu importe la direction à partir de laquelle elle est observée (contrairement à une réflexion spéculaire, ex: un mirroir).

Les surfaces diffuses sont gouvernées par la loi de Lambert:

$${I=k_d N*L}$$

Ou I représente l'intensité du pixel, kd représente l'albedo de la surface et N sa normal. Puisque I et L sont connus et que N est une matrice de vecteurs avec norme de 1, kd et N sont regroupés pour devenir n. On obtient:

$${I=n*L}$$

Puisqu'il y a plusieurs images, I devient une matrice du nombres de pixels par nombre d'images et L devient une matrice de 3 par le nombre d'images. n est calculé par moindre carrée:

$${n=(IL^T)(LL^T)^{-1}}$$

Puis, kd et N sont retrouvés:

$${k_d=|n|}$$

$${N=\frac{n}{k_d}}$$

Pour une image en couleur, le calcul est d'abord effectué sur l'image en noir et blanc pour obtenir N. kd est ensuite minimisé pour chaque cannal de couleur par méthode de moindre carrée:

$${\sum_{i}(I_i - k_d L_i N^T)^2}$$

En dérivant par rapport à kd et égalant à 0 pour obtenir le minimum:

$${\sum_{i}2(I_i - k_d L_i N^T)(L_i N^T)=0}$$

$${k_d=\frac{\sum_{i}I_i L_i N^T}{\sum_{i}(L_i N^T)^2}}$$

L'utilisateur peut ensuite rogner l'image pour conserver la partie d'intéret et l'albedo couleur et la normal sont enregistrés. Il est à noter que la normal map est modifiée selon l'équation suivante:

$${N=\frac{N+1}{2}}$$

L'algorithme est implémenté dans le fichier Main_solve_raw.py.

L'algorithme appliqué à une série de photos (intensité linéaire) produit les résultats suivants:

Albedo

Normal

Le résultat semble très bien, l'albedo est bien la couleur du tissu sans ombrage et la normal du tissu est bien représentée par l'algorithme, même dans ses détails. L'application des textures à des rendus 3d sera discutée dans la partie résultats, mais un test rapide produit le rendu suivant, supportant la qualité du résultat:

Albedo et Normal appliqué a un plan

Profondeur

Le calcul de la profondeur à partir de la normal map qui semblait, à première vue, une simple addition au programme fût, en somme, une grande disgression. Le principe de base est relativement simple, il suffit de calculer le gradient (perpendiculaire à la normal) pour chaque point et d'intégrer. Cependant, ceci s'est avéré une tâche plus complexe que prévue. Les différentes stratégies envisagées sont présentées.

Solution matricielle

La solution optimale initialement considérée est matricielle. Choisissons un point arbitraire avec coordonnées (x,y,z) et un point voisin sur l'axe x. En supposant que la surface est continue, le vecteur entre les deux points sera:

$${V=(x+1,y,z_{x+1,y})-(x,y,z_{x,y})}$$

$${V=(1,0,z_{x+1,y}-z_{x,y})}$$

Sachant que le produit scalaire entre 2 vecteurs perpendiculaires est 0:

$${V*N=0}$$

$${(1,0,z_{x+1,y}-z_{x,y})*(N_x,N_y,N_z)=0}$$

$${n_x+n_z(z_{x+1,y}-z_{x,y})=0}$$

Le même procédé pour un voisin sur l'axe y donne l'équation suivante:

$${n_y+n_z(z_{x,y+1}-z_{x,y})=0}$$

L'équation matricielle représentant ce système d'équations est une matrice creuse de 2 fois le nombre de pixels par le nombre de pixels. Puisque la profondeur pour toute l'image est souhaitée et n'ayant pas d'expérience avec les classes de matrices creuses offertes par le module scipy, la recherche d'une technique alternative a débutée.

Solution linéaire

La seconde idée envisagée est l'utilisation des équations présentées précédemment à partir du point (0,0). L'origine à été mise à 0, puis, la profondeur du prochain point sur l'axe y est calculé utilisant le point précédent. L'opération est répétée jusqu'au dernier point sur l'axe. Ensuite, la même procédure est exécutée sur l'axe x utilisant la rangée précédente dans le calcul. L'image suivante montre le résultat de l'algorithme appliqué sur la normal map présentée dans la section Résolution Normal et Albedo:

Profondeur calculée linéaire

Le résultat est très peu convaincant, produisant une forme vaguement reconnaissable, mais comportant des lignes non souhaitées et des grandes variations non réalistes. Ce résultat peu appréciable était attendu puisque la technique étire toutes les erreurs, qu'elles soient causées par le bruit de la caméra ou l'imprécision numérique, sur le reste de l'image.

Pour tenter d'obtenir une meilleure image, la même opération a été effectuée à plusieurs reprises en commencant à différents points et en moyennant les images résultantes. L'algorithme complet est implanté dans le fichier depth.py.

En utilisant la même normal map mais avec 100 différents points initiaux, l'image suivante est obtenue:

Profondeur calculée linéaire moyenne de 100 points

Le résultat, quoique meilleur qu'avec un seul point, n'est pas suffisamment bon pour être utilisé dans un rendu 3D, manquant encore largement les détails présents dans la normal map. Une méthode alternative reste à être trouvée.

Solution itérative

Pour résoudre le problème d'accumulation d'erreur, une méthode itérative est utilisée. L'implentation, une version matricielle de la solution proposée par Rich Sedman, utilise les mêmes équations présentées précédemment mais ceux-ci sont appliquées à chaque point avec les résultats de l'itération précédente des quatre points voisins. L'algorithme est implanté dans le fichier depth_iteration.py.

En lancant le programme avec 200 itérations pour la normal map précédemment utilisée, le résultat suivant est obtenu:

Profondeur calculée avec 200 itérations

Le résultat est considérablement plus appréciable que la méthode linéaire, la forme du tissu est facilement identifiable dans ses détails. Considérons maintenant l'exemple d'une sphère. La première image représente la normal map, la seconde la résolution de profondeur avec 200 itérations et la troisième avec 2000 itérations.

Normal map (Source)

Profondeur avec 200 itération

Profondeur avec 2000 itérations

Les images ci-dessus illustrent que les basses fréquences ne se propagent pas rapidement dans l'image; il faut donc beaucoup d'itérations pour percevoir la forme de la sphere. Par contre, puisque les détails sont les éléments importants dans une texture, cette limitation ne cause pas problème pour ce contexte d'utilisation de la méthode. Une texture comportant des plus basses fréquences dans sa profondeur peut être résolue en augmentant le nombre d'itérations.

Texture Répétable

Pour rendre une texture utilisable confortablement dans un rendu, il est essentiel de la rendre répétable sans coupure notable. Pour ce faire, un algorithme rendant la texture répétable est implanté dans le fichier tileable.py. Son fonctionnement général est le suivant:

  • L'utilisateur identifie une ligne horizontal de la texture pour rectifier son orientation.
  • L'image albedo est chargée en mémoire.
  • Pour chaque axe:
    • Une fenêtre de largeur déterminée se terminant à la fin de l'image est comparée à chaque fenêtre de même taille sur l'axe par somme des différences au carrée.
    • La position de la rangée ayant la plus grande ressemblance à la fin est ajoutée à la fin de l'image avec un dégradé simple*.
    • Similairement, une fenêtre du début de l'image est comparée à chaque rangée.
    • L'image de la rangée ayant la plus grande ressemblance au début remplace le début de l'image avec un dégradé simple et la fin est coupée en conséquence.
  • Parallèlement, les mêmes transformations effectuées sur la texture albedo sont appliquées à la normal et à la profondeur.
  • Les textures répétables sont enregistrées.

*Un dégradé en fréquences a été testé mais puisque la texture de chaque coté du dégradé est très similaire, la technique augmentait largement le temps d'exécution sans permettre un meilleur résultat. De plus, l'image est d'abord aggrandie pour éviter d'obtenir une image trop coupée en sortie.

En appliquant l'algorithme aux résultats conservés précédemment, les images suivantes sont obtenues:

Albedo répétable

Normal répétable

Profondeur répétable

Aperçu de l'albedo répétée (4 copies)

Quoique imparfait, le résultat est statisfaisant pour une texture avec un motif qui se répète. L'exemple suivant illustre l'utilisation du même algorithme sur une texture sans motif répétable:

Albedo initial

Albedo répétable

Aperçu de l'albedo répétée (4 copies)

Surprenamment, le résultat est convenable malgré le manque de répétition dans la texture, possiblement en raison des nombreux éléments dans l'image, rendant une coupure supplémentaire difficile à identifier.

Interface

Pour faciliter l'utilisation des algorithmes et permettre la visualisation des résultats rapidement, une interface graphique a été fabriquée avec le module Tkinter.

Celle-ci permet d'abord d'ouvrir le répertoire dans lequel se trouve les images sources en format CR2 (Raw Canon). Puis, chacun des programmes python peut être appelé, affichant les résultats après l'exécution. L'interface est montrée dans la figure suivante:

Interface

Pour permettre une visualisation facile de la texture sur un modèle 3D, la fonctionnalité "Aperçu" lance le logiciel blender en arrière-plan et exécute un script (preview.py) pour apparier automatique les textures albedo, normal et profondeur au cube et produire un rendu qui est ensuite affiché dans l'interface. Le paramètre "Facteur profondeur" réfère à l'échelle de l'amplitude physique du déplacement de la géométrie. Il est à noter que l'emplacement du logiciel Blender doit être modifié dans le scipt Main_interface.py pour fonctionner sur une autre machine. Les images suivantes en sont des exemples:

À première vue, la profondeur semble manquer de résolution; cependant, ceci est dû à la subdivision limitée du modèle pour éviter d'avoir trop de géométrie, ralentissant largement le temps de rendu. En augmentant la subdivision par un facteur de trois, le rendu suivant du gravier est obtenu:

L'augmentation de géométrie réduit légèrement l'impression de manque de résolution. Finalement, l'étirement de la texture au coins est causée par le dépliage UV de la géométrie du cube produit automatiquement par Blender. En somme, l'interface s'est avéré un outil grandement utile pour accélérer l'utilisation des scripts générant les textures.

Résultats

Chacune des textures capturées est présentée ci-dessous.

Béton:

Céramique:

Chandail blanc rayé noir:

Chandail gris:

Chandail noir:

Chandail rouge:

Ciment:

Cuir foncé:

Coussin rouge:

Essuie tout:

Tissu vert:

Gazon:

Gravier:

Jeans:

Laine grise:

Laine rouge:

Mur de brique:

Revêtement de maison:

Pierres de sol:

Tissu gris:

Bois pâle:

Bois foncé:

Feuille de menthe:

La plupart des résultats sont très utilisables pour des rendus 3d. Cependant, il est arrivé à une seule reprises sur 24 que la solution de moindre carrée pour la résolution de la normal ne pouvait pas être trouvée puisque la décomposition en valeurs singulières ne convergait pas. De plus, puisque la caméra n'a pas été calibrée, quelques textures sont sur-exposées, notamment la laine rouge. Les photos de cette dernière ont également été prises à un angle trop faible, l'information dans les crevasses plus profondes de la laine n'a pu être identifiée, résultant en des points abérants à quelques endroits, qui ont eu pour effet de créer des extrêmes dans l'image de profondeur. Ces extrêmes, lorsque l'image est normalisée, compressent l'information pertinente dans une petite zone et résultent en une grande perte de précision; produisant une image pour la plupart grise. D'autre part, les limitations de l'algorithme de répétition deviennent flagrantes dans quelques exemples: lorsque le tissu capturé n'est pas étalé parfaitement, les courbes produites produisent des vagues non souhaitées dans la grille produite (laine rouge) et lorsque le motif n'est pas assez similaire, le résultat est médiocre (pierre de sol). En addition, la répétition de la texture est parfois flagrante dans la normal map résultante, par contre, cet effet n'est pas une erreur de l'algorithme de répétition, plutôt, l'algorithme de résolution de normal suppose que la caméra et la lumière sont à distance infini et que tous les rayons sont parallèles. En pratique, une légère variation est présente, produisant un dégradé notable dans les normal map produites. L'algorithme de profondeur a aussi ses lacunes, la plus majeure étant l'effet des basses fréquences qui ne sont pas uniformes puisqu'elles prennent énormément d'itérations pour se propager, comme discuté précédemment. De plus, l'effet d'une surface discontinue est illustrée avec les brains de gazon. Leur profondeur est mieux calculée que prévue, mais la surface est tout de même considérablement moins belle que les autres résultats. Finalement, même si les matériaux capturés ont été choisis pour leur aspect diffus, certains ont des aspects spéculaires plus prononcés qui se rendent à la texture calculée, notamment le bois pâle qui a une partie plus brillante dans son coin supérieur gauche.

Applications

Le but du travail étant de produire des textures pouvant être utilisées dans des rendus de scènes 3D, cet objectif a été mis au test. La scène 3D suivante a été modélisée dans le logiciel Blender pour le projet:

Scene 3D

Les texures suivantes ont été sélectionnées et appliquées aux objets. Certaines ont été modifiées avec de simples courbes RGB pour modifier l'albedo et une constante spéculaire a été ajoutée pour certaines.

  • Bois pâle pour le plancher.
  • Bois foncé pour les cadres, la fenêtre, le garde-robe, les pattes de la chaise, le bureau et le tronc de l'arbre.
  • Le chandail blanc rayé noir pour le tapis.
  • Le ciment pour le pot de fleurs.
  • Le gravier pour la base de l'arbre.
  • La feuille de menthe pour les feuilles de l'arbre.
  • Le tissu gris pour la chaise.
  • La laine grise pour la couverture sur la chaise.
  • Le béton et le tissu vert pour les blocs sur le bureau.
  • Le jeans dans le cadre de gauche.
  • La brique dans le cadre de droit.

L'image du batiment au travers de la fenêtre a été prise sur textures.com .

Scene 3D texturée

Le résultat est très appréciable, confirmant l'utilité des textures capturées dans une application réelle.

Améliorations

Deux catégories d'améliorations sont considérées: les améliorations matérielles et les améliorations logicielles.

Améliorations matérielles

La plus grande limitation du montage utilisé pour le projet est l'effet de l'éclairage ambiant sur les mesures. En effet, n'étant pas isolé de l'éclairage extérieur au montage, les photographies ont dû être prises dans un endroit sombre ou durant la nuit à l'extérieur. De plus, la position de la lumière étant opérée par un utilisateur, sa direction devait être calculée pour chaque image. Pour remédier à ces deux problèmes, des montages plus robustes et permanents sont imaginés. Ceux-ci comportent des lumières attachées au système et isolées de l'éclairage extérieur. Chaque lumière est allumée une à une par un microprocesseur et la caméra est déclenchée pour chaque état. Sachant la position des lumières, le calcul de direction pour chaque image n'est plus requis, accélérant la production de résultats. Malheureusement, la situation d'isolement courante durant le travail sur ce projet a empêché l'acquisition des composantes pour la construction d'un tel montage. Toutefois, des plans ont été développés: un pour la capture de textures de taille semblable à celle de ce projet et un second pour la capture de textures de plus petite taille. Les deux montages sont présentés dans les figures suivantes:

Montage pour grandes textures

Montage pour petites textures

Améliorations logicielles

Pour améliorer la fidélité des résultats face aux matériaux physiques, les couleurs peuvent être calibrées à l'aide d'un ColorChecker. Les distortions produites par la lentille de la caméra peuvent également être corrigées par logiciel à l'aide d'un quadrillé. Cependant, puisque le projet a un objectif plus artistique et que le matériel n'était pas disponible, ces deux corrections ont été négligées.

Comme présenté dans les résultats, une majeure limitation de l'algorithme est l'impossibilité de capturer des matériaux spéculaires. Pour ce faire, il faudrait modifier le modèle Lambertien utilisé en ajoutant un terme pour prendre en compte l'aspect spéculaire. Par exemple, le modèle Blinn-Phong décrit par l'équation suivante peut être ajouté au modèle Lambertien:

$${I=i k_d (N*H)^n}$$

Ou i représente l'intensité de la source spéculaire, H représente une moyenne entre la direction de la lumière et le point d'observation et n représente le niveau de brillance du matériau. Cependant, l'implantation d'un tel modèle en photométrie stéréo augmente considérablement la complexité de l'algorithme. Pour cette raison, la technique ne sera pas explorée dans ce document.

Conclusion

En somme, la photomértie stéréo appliquée à un modèle suivant la loi de Lambert présente des résultats impressionants pour la capture de normal et d'albedo de textures diffuses. Ces textures peuvent ensuite produire une map de profondeur du matériau et peuvent être relativement facilement être mise en texture répétables pour être utilisées dans un rendu 3D.