Introduction au traitement d'images Python dans la photographie computationnelle

Publié: 2022-03-11

La photographie computationnelle consiste à améliorer le processus photographique avec le calcul. Alors que nous avons normalement tendance à penser que cela ne s'applique qu'au post-traitement du résultat final (similaire à la retouche photo), les possibilités sont beaucoup plus riches puisque le calcul peut être activé à chaque étape du processus photographique - en commençant par l'éclairage de la scène, en continuant avec l'objectif, et éventuellement même à l'affichage de l'image capturée.

Ceci est important car cela permet de faire beaucoup plus et de différentes manières que ce qui peut être réalisé avec un appareil photo normal. C'est également important parce que le type d'appareil photo le plus répandu de nos jours, l'appareil photo mobile, n'est pas particulièrement puissant par rapport à son grand frère (le reflex numérique), mais il parvient à faire du bon travail en exploitant la puissance de calcul dont il dispose sur l'appareil. .

Nous examinerons deux exemples où le calcul peut améliorer la photographie. Plus précisément, nous verrons comment le simple fait de prendre plus de photos et d'utiliser un peu de Python pour les combiner peut créer de beaux résultats dans deux situations où le matériel de caméra mobile ne fonctionne pas. brille vraiment - faible luminosité et plage dynamique élevée.

Photographie en basse lumière

Disons que nous voulons prendre une photo d'une scène en basse lumière, mais que l'appareil photo a une petite ouverture (objectif) et un temps d'exposition limité. Il s'agit d'une situation typique pour les caméras de téléphones portables qui, étant donné une scène à faible luminosité, pourraient produire une image comme celle-ci (prise avec une caméra iPhone 6) :

Image de quelques jouets dans un environnement peu éclairé

Si on essaie d'améliorer le contraste le résultat est le suivant, qui est aussi assez mauvais :

La même image que ci-dessus, beaucoup plus lumineuse mais avec un bruit visuel gênant

Ce qui se produit? D'où vient tout ce bruit ?

La réponse est que le bruit provient du capteur, l'appareil qui essaie de déterminer quand la lumière le frappe et à quel point cette lumière est intense. En basse lumière, cependant, il doit augmenter considérablement sa sensibilité pour enregistrer quoi que ce soit, et cette sensibilité élevée signifie qu'il commence également à détecter les faux positifs - des photons qui ne sont tout simplement pas là. (En passant, ce problème n'affecte pas seulement les appareils, mais aussi nous, les humains : la prochaine fois que vous serez dans une pièce sombre, prenez un moment pour remarquer le bruit présent dans votre champ visuel.)

Une certaine quantité de bruit sera toujours présente dans un dispositif d'imagerie ; cependant, si le signal (information utile) est de forte intensité, le bruit sera négligeable (rapport signal sur bruit élevé). Lorsque le signal est faible, par exemple en cas de faible luminosité, le bruit se fera remarquer (faible signal sur bruit).

Pourtant, nous pouvons surmonter le problème du bruit, même avec toutes les limitations de l'appareil photo, afin d'obtenir de meilleurs clichés que celui ci-dessus.

Pour ce faire, nous devons tenir compte de ce qui se passe dans le temps : le signal restera le même (même scène et nous supposons qu'il est statique) tandis que le bruit sera complètement aléatoire. Cela signifie que, si nous prenons plusieurs photos de la scène, elles auront différentes versions du bruit, mais les mêmes informations utiles.

Ainsi, si nous faisons la moyenne de nombreuses images prises au fil du temps, le bruit s'annulera tandis que le signal ne sera pas affecté.

L'illustration suivante montre un exemple simplifié : nous avons un signal (triangle) affecté par du bruit, et nous essayons de récupérer le signal en faisant la moyenne de plusieurs instances du même signal affectées par un bruit différent.

Une démonstration à quatre panneaux du triangle, une image dispersée représentant le triangle avec un bruit supplémentaire, une sorte de triangle dentelé représentant la moyenne de 50 instances et la moyenne de 1000 instances, qui semble presque identique au triangle d'origine.

Nous voyons que, bien que le bruit soit suffisamment fort pour déformer complètement le signal dans un seul cas, la moyenne réduit progressivement le bruit et nous récupérons le signal d'origine.

Voyons comment ce principe s'applique aux images : Tout d'abord, nous devons prendre plusieurs photos du sujet avec l'exposition maximale autorisée par l'appareil photo. Pour de meilleurs résultats, utilisez une application qui permet la prise de vue manuelle. Il est important que les photos soient prises depuis le même endroit, donc un trépied (improvisé) aidera.

Plus de prises de vue signifient généralement une meilleure qualité, mais le nombre exact dépend de la situation : la quantité de lumière, la sensibilité de l'appareil photo, etc. Une bonne plage peut se situer entre 10 et 100.

Une fois que nous avons ces images (au format brut si possible), nous pouvons les lire et les traiter en Python.

Pour ceux qui ne sont pas familiarisés avec le traitement d'image en Python, nous devons mentionner qu'une image est représentée sous la forme d'un tableau 2D de valeurs d'octets (0-255), c'est-à-dire pour une image monochrome ou en niveaux de gris. Une image couleur peut être considérée comme un ensemble de trois images de ce type, une pour chaque canal de couleur (R, V, B), ou en fait un tableau 3D indexé par position verticale, position horizontale et canal de couleur (0, 1, 2) .

Nous utiliserons deux bibliothèques : NumPy (http://www.numpy.org/) et OpenCV (https://opencv.org/). Le premier nous permet d'effectuer des calculs sur des tableaux très efficacement (avec un code étonnamment court), tandis qu'OpenCV gère la lecture/écriture des fichiers image dans ce cas, mais est beaucoup plus capable, fournissant de nombreuses procédures graphiques avancées - dont certaines nous allons utiliser plus loin dans l'article.

 import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)

Le résultat (avec le contraste automatique appliqué) montre que le bruit a disparu, une très grande amélioration par rapport à l'image d'origine.

La photographie originale de jouets, cette fois plus lumineuse et beaucoup plus claire, avec très peu de bruit perceptible

Cependant, nous remarquons encore quelques artefacts étranges, tels que le cadre verdâtre et le motif en forme de grille. Cette fois, ce n'est pas un bruit aléatoire, mais un bruit de motif fixe. Qu'est-il arrivé?

Un gros plan du coin supérieur gauche de l'image ci-dessus

Un gros plan du coin supérieur gauche, montrant le cadre vert et le quadrillage

Encore une fois, nous pouvons blâmer le capteur. Dans ce cas, nous voyons que différentes parties du capteur réagissent différemment à la lumière, ce qui donne un motif visible. Certains éléments de ces motifs sont réguliers et sont très probablement liés au substrat du capteur (métal/silicium) et à la façon dont il réfléchit/absorbe les photons entrants. D'autres éléments, comme le pixel blanc, sont simplement des pixels de capteur défectueux, qui peuvent être trop sensibles ou trop insensibles à la lumière.

Heureusement, il existe également un moyen de se débarrasser de ce type de bruit. C'est ce qu'on appelle la soustraction de trame noire.

Pour ce faire, nous avons besoin d'une image du bruit de motif lui-même, et cela peut être obtenu si nous photographions l'obscurité. Oui, c'est vrai - couvrez simplement le trou de l'appareil photo et prenez beaucoup de photos (disons 100) avec un temps d'exposition et une valeur ISO maximum, et traitez-les comme décrit ci-dessus.

Lors de la moyenne sur de nombreuses images noires (qui ne sont en fait pas noires, en raison du bruit aléatoire), nous nous retrouverons avec le bruit de motif fixe. Nous pouvons supposer que ce bruit fixe restera constant, donc cette étape n'est nécessaire qu'une seule fois : l'image résultante peut être réutilisée pour toutes les futures prises de vue en basse lumière.

Voici à quoi ressemble la partie supérieure droite du motif de bruit (contraste ajusté) pour un iPhone 6 :

Le bruit de motif pour la partie de l'image affichée dans l'image précédente

Encore une fois, nous remarquons la texture en forme de grille, et même ce qui semble être un pixel blanc collé.

Une fois que nous avons la valeur de ce bruit de dark frame (dans la variable average_noise ), nous pouvons simplement le soustraire de notre plan jusqu'à présent, avant de normaliser :

 average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)

Voici notre photo finale :

Une image de plus de la photo, cette fois sans aucune preuve d'avoir été prise en basse lumière

Plage dynamique élevée

Une autre limitation d'une petite caméra (mobile) est sa petite plage dynamique, ce qui signifie que la plage d'intensités lumineuses à laquelle elle peut capturer des détails est plutôt petite.

En d'autres termes, la caméra est capable de capturer uniquement une bande étroite des intensités lumineuses d'une scène ; les intensités en dessous de cette bande apparaissent en noir pur, tandis que les intensités au-dessus apparaissent en blanc pur, et tous les détails sont perdus de ces régions.

Cependant, il existe une astuce que l'appareil photo (ou le photographe) peut utiliser - et qui consiste à ajuster le temps d'exposition (le temps pendant lequel le capteur est exposé à la lumière) afin de contrôler efficacement la quantité totale de lumière qui parvient au capteur. décaler la plage vers le haut ou vers le bas afin de capturer la plage la plus appropriée pour une scène donnée.

Mais c'est un compromis. De nombreux détails ne parviennent pas à en faire la photo finale. Dans les deux images ci-dessous, on voit la même scène capturée avec des temps de pose différents : une pose très courte (1/1000 sec), une pose moyenne (1/50 sec) et une pose longue (1/4 sec).

Trois versions de la même image de fleurs, une si sombre que la majeure partie de la photo est noire, une d'apparence normale, bien qu'avec un éclairage un peu malheureux, et une troisième avec la lumière si élevée qu'il est difficile de voir les fleurs dans le premier plan

Comme vous pouvez le voir, aucune des trois images n'est capable de capturer tous les détails disponibles : le filament de la lampe n'est visible que sur le premier plan, et certains détails de la fleur sont visibles soit au milieu, soit sur le dernier plan mais pas tous les deux.

La bonne nouvelle est qu'il y a quelque chose que nous pouvons faire à ce sujet, et encore une fois, cela implique de construire sur plusieurs plans avec un peu de code Python.

L'approche que nous adopterons est basée sur les travaux de Paul Debevec et al., qui décrit la méthode dans son article ici. La méthode fonctionne comme ceci :

Tout d'abord, cela nécessite plusieurs prises de vue de la même scène (à l'arrêt) mais avec des temps d'exposition différents. Encore une fois, comme dans le cas précédent, nous avons besoin d'un trépied ou d'un support pour nous assurer que la caméra ne bouge pas du tout. Nous avons également besoin d'une application de prise de vue manuelle (si vous utilisez un téléphone) afin de pouvoir contrôler le temps d'exposition et d'empêcher les ajustements automatiques de l'appareil photo. Le nombre de prises de vue nécessaires dépend de la gamme d'intensités présentes dans l'image (à partir de trois) et les temps d'exposition doivent être espacés sur cette gamme afin que les détails que nous souhaitons préserver apparaissent clairement dans au moins une prise de vue.

Ensuite, un algorithme est utilisé pour reconstruire la courbe de réponse de la caméra en fonction de la couleur des mêmes pixels sur les différents temps d'exposition. Cela nous permet essentiellement d'établir une carte entre la luminosité réelle de la scène d'un point, le temps d'exposition et la valeur que le pixel correspondant aura dans l'image capturée. Nous utiliserons l'implémentation de la méthode Debevec de la librairie OpenCV.

 # Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)

La courbe de réponse ressemble à ceci :

Un graphique affichant la courbe de réponse sous forme d'exposition de pixel (log) sur la valeur de pixel

Sur l'axe vertical, nous avons l'effet cumulé de la luminosité de la scène d'un point et du temps d'exposition, tandis que sur l'axe horizontal nous avons la valeur (0 à 255 par canal) que le pixel correspondant aura.

Cette courbe nous permet ensuite d'effectuer l'opération inverse (qui est l'étape suivante du processus) - étant donné la valeur du pixel et le temps d'exposition, nous pouvons calculer la luminosité réelle de chaque point de la scène. Cette valeur de luminosité est appelée irradiance et mesure la quantité d'énergie lumineuse qui tombe sur une unité de surface de capteur. Contrairement aux données d'image, elles sont représentées à l'aide de nombres à virgule flottante car elles reflètent une plage de valeurs beaucoup plus large (d'où une plage dynamique élevée). Une fois que nous avons l'image d'éclairement (l'image HDR), nous pouvons simplement l'enregistrer :

 # Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)

Pour ceux d'entre nous qui ont la chance d'avoir un écran HDR (qui devient de plus en plus courant), il peut être possible de visualiser directement cette image dans toute sa splendeur. Malheureusement, les normes HDR en sont encore à leurs balbutiements, de sorte que le processus à suivre peut être quelque peu différent pour différents écrans.

Pour le reste d'entre nous, la bonne nouvelle est que nous pouvons toujours profiter de ces données, bien qu'un affichage normal nécessite que l'image ait des canaux de valeur d'octet (0-255). Bien que nous devions renoncer à une partie de la richesse de la carte d'irradiance, nous avons au moins le contrôle sur la façon de le faire.

Ce processus est appelé mappage de tonalité et implique la conversion de la carte d'irradiance en virgule flottante (avec une plage de valeurs élevée) en une image de valeur d'octet standard. Il existe des techniques pour le faire afin que de nombreux détails supplémentaires soient préservés. Juste pour vous donner un exemple de la façon dont cela peut fonctionner, imaginez qu'avant de réduire la plage de virgule flottante en valeurs d'octets, nous améliorons (renforçons) les bords présents dans l'image HDR. L'amélioration de ces bords aidera à les préserver (et implicitement les détails qu'ils fournissent) également dans l'image à faible plage dynamique.

OpenCV fournit un ensemble de ces opérateurs de mappage de tonalité, tels que Drago, Durand, Mantiuk ou Reinhardt. Voici un exemple d'utilisation d'un de ces opérateurs (Durand) et du résultat qu'il produit.

 durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255) 

Le résultat du calcul ci-dessus affiché sous forme d'image

En utilisant Python, vous pouvez également créer vos propres opérateurs si vous avez besoin de plus de contrôle sur le processus. Par exemple, voici le résultat obtenu avec un opérateur personnalisé qui supprime les intensités représentées en très peu de pixels avant de réduire la plage de valeurs à 8 bits (suivi d'une étape d'auto-contraste) :

L'image qui résulte de la suite du processus ci-dessus

Et voici le code de l'opérateur ci-dessus :

 def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)

Conclusion

Nous avons vu comment avec un peu de Python et quelques bibliothèques de support, nous pouvons repousser les limites de la caméra physique afin d'améliorer le résultat final. Les deux exemples dont nous avons discuté utilisent plusieurs prises de vue de faible qualité pour créer quelque chose de mieux, mais il existe de nombreuses autres approches pour différents problèmes et limitations.

Alors que de nombreux téléphones avec appareil photo ont des applications en magasin ou intégrées qui traitent de ces exemples particuliers, il n'est clairement pas difficile du tout de les programmer à la main et de profiter du niveau supérieur de contrôle et de compréhension qui peut être obtenu.

Si vous êtes intéressé par les calculs d'images sur un appareil mobile, consultez le didacticiel OpenCV : Détection d'objets en temps réel à l'aide de MSER dans iOS par Altaibayar Tseveenbayar, collègue Toptaler et développeur OpenCV d'élite.