Introduction à l'image stitching avec OpenCV

Introduction à l’image stitching avec OpenCV

L’image stitching, est une technique de traitement d’images qui permet de combiner plusieurs images en une seule image panoramique.

Cette technique est utilisée pour créer de grandes images à partir de plusieurs images plus petites, en utilisant des modèles de features matching pour trouver les points communs entre les images et les assembler de manière cohérente.

L’image stitching est souvent utilisée pour créer des panoramas, mais elle peut également être utilisée pour combiner des images de différents angles de vue pour créer une image 3D ou pour créer des images de haute résolution à partir de plusieurs images de basse résolution.

Cette technique est largement utilisée dans de nombreux domaines, notamment la photographie, la cartographie, la robotique et la réalité augmentée.

Dans cet article, je vous explique comment fonctionne l’image stitching, je vous parle de ses applications et on verra comment faire de l’image stitching avec Python et OpenCV.

Quelles sont les applications de l’image stitching ?

L’image stitching est largement utilisé dans plusieurs applications.

  • Image stitching et photomosaïques : cette technique est celle qui est utilisée dans les smartphones pour créer des panoramas à partir de plusieurs images prises de différents angles.
  • Mapping et surveillance : elle permet la création de cartes et de modèles en 3D à partir de photos aériennes ou satellites. Elle est très utiles pour les images extra large, qui ne peuvent pas être prises en une seule fois
  • Réalité virtuelle et augmentée : l’image stitching est utilisé pour la création de mondes virtuels en combinant des images pour créer des scènes immersives plus rapidement
  • Inspection industrielle : elle permet l’évaluation de la qualité et de l’état des équipements industriels en combinant des images prises de différents angles
  • Imagerie médicale : pour la reconstruction de modèles en 3D et la visualisation de données médicales telles que des scanners ou des IRM
  • Analyse de la biologie cellulaire : pour la reconstruction de modèles en 3D des structures cellulaires à partir de plusieurs images prises à différents angles et profondeurs
  • Imagerie scientifique : l’image stitching va aussi beaucoup aider pour l’analyse de grandes quantités d’images pour l’étude de la morphologie, de la dynamique et de la distribution des objets dans différents domaines scientifiques tels que l’astronomie, la microscopie électronique et la microscopie confocale

Comment faire de l’image stitching avec Python et OpenCV ?

Dans cette section, nous allons voir ensemble comment faire de l’image stitching avec Python et OpenCV.

La technique repose sur des modèles d’extraction de features qui sont beaucoup utilisés en computer vision, comme le modèle SIFT.

C’est d’ailleurs ce modèle que nous allons utiliser dans ce tutoriel.

Choix de l’image à recoller

Pour ce tutoriel, j’ai choisi de travailler avec cette image générées par stable diffusion :

Introduction à l'image stitching avec OpenCV

J’ai commencé par couper l’image en 2 verticalement, en laissant une petite bande commune.

Je l’ai fait en utilisant ce code là sur OpenCV :

import cv2

# import de image
img = cv2.imread("./paysage_image_stitching.jpeg")
height, width = img.shape[:2]

# calcul du point central de l'image
midpoint = int(width / 2)

# division de l'image en 2 parties en gardant une bande de 60 px commune
left_part = img[:, :midpoint + 60]
right_part = img[:, midpoint:]

# Enregistrement des images
cv2.imwrite("./left_part.jpg", left_part)
cv2.imwrite("./right_part.jpg", right_part)

Après l’execution de ce code on obtient les 2 images suivantes :

Maintenant qu’on a 2 images de base sur lesquelles faire l’image stitching, entrons dans le vif du sujet.

Image stitching avec OpenCV

On commence par importer les librairies necéssaires :

import cv2
import numpy as np

On va maintenant définir de la fonction « FindMatches ».

Cette fonction prend en entrée deux images et utilise la méthode SIFT (Scale-Invariant Feature Transform) pour trouver les points clés et les descripteurs dans les images.

Il utilise également un « Brute Force Matcher » pour trouver des correspondances entre les points clés des images. Il s’agit simplement de tester toutes les paires possibles de matchs pour conserver les meilleures, en utilisant un threshold.

def FindMatches(BaseImage, SecImage):

    # Using SIFT to find the keypoints and decriptors in the images
    Sift = cv2.SIFT_create()
    BaseImage_kp, BaseImage_des = Sift.detectAndCompute(cv2.cvtColor(BaseImage, cv2.COLOR_BGR2GRAY), None)
    SecImage_kp, SecImage_des = Sift.detectAndCompute(cv2.cvtColor(SecImage, cv2.COLOR_BGR2GRAY), None)

    # Using Brute Force matcher to find matches.
    BF_Matcher = cv2.BFMatcher()
    InitialMatches = BF_Matcher.knnMatch(BaseImage_des, SecImage_des, k=2)

    # Applying ratio test and filtering out the good matches.
    GoodMatches = []
    for m, n in InitialMatches:
        if m.distance < 0.75 * n.distance:
            GoodMatches.append([m])

    return GoodMatches, BaseImage_kp, SecImage_kp

On va maintenant définir une fonction qui va permettre de trouver les éventuelles projections à faire une fois qu’un match est trouvé. Ceci se produit quand les images à recoller ne sont pas exactement sur le même plan.

Cette fonction prend en entrée les matches trouvés et les points clés des images de droite et de gauche. Et elle utilise les correspondances pour trouver la matrice d’homographie entre les images, qui décrit la transformation entre les images.

def FindHomography(Matches, BaseImage_kp, SecImage_kp):
    # If less than 4 matches found, exit the code.
    if len(Matches) < 4:
        print("\nNot enough matches found between the images.\n")
        exit(0)

    # Storing coordinates of points corresponding to the matches found in both the images
    BaseImage_pts = []
    SecImage_pts = []
    for Match in Matches:
        BaseImage_pts.append(BaseImage_kp[Match[0].queryIdx].pt)
        SecImage_pts.append(SecImage_kp[Match[0].trainIdx].pt)

    # Changing the datatype to "float32" for finding homography
    BaseImage_pts = np.float32(BaseImage_pts)
    SecImage_pts = np.float32(SecImage_pts)

    # Finding the homography matrix(transformation matrix).
    (HomographyMatrix, Status) = cv2.findHomography(SecImage_pts, BaseImage_pts, cv2.RANSAC, 4.0)

    return HomographyMatrix, Status

On défini maintenant la fonction GetNewFrameSizeAndMatrix ci-dessous.

Elle calcule la nouvelle taille et la matrice de transformation pour l’image qui sera recadrée. La taille de l’image est déterminée en multipliant la largeur et la hauteur d’origine de l’image par le ratio de la nouvelle taille sur la taille originale.

La matrice de transformation est calculée à partir de la position et de l’orientation de l’objet. Tout d’abord, la fonction calcule le centre de l’objet en utilisant les coordonnées x et y du coin supérieur gauche de l’objet et sa largeur et sa hauteur. Ensuite, elle calcule l’angle de rotation de l’objet en utilisant la valeur de l’orientation. La matrice de transformation est construite en utilisant ces informations.

La matrice d’homographie est une matrice 3×3 qui est utilisée pour transformer l’image. Elle comprend des valeurs de rotation, de translation et de mise à l’échelle.

Les valeurs de rotation sont déterminées en utilisant l’angle de rotation de l’objet. Les valeurs de translation sont calculées en utilisant la position du centre de l’objet. Les valeurs de mise à l’échelle sont déterminées en utilisant le rapport de la nouvelle taille sur la taille originale de l’image.

En fin de compte, la fonction renvoie la nouvelle taille de l’image ainsi que la matrice de transformation qui sera utilisée pour recadrer l’image.

def GetNewFrameSizeAndMatrix(HomographyMatrix, Sec_ImageShape, Base_ImageShape):
    # Reading the size of the image
    (Height, Width) = Sec_ImageShape

    # Taking the matrix of initial coordinates of the corners of the secondary image
    # Stored in the following format: [[x1, x2, x3, x4], [y1, y2, y3, y4], [1, 1, 1, 1]]
    # Where (xi, yi) is the coordinate of the i th corner of the image.
    InitialMatrix = np.array([[0, Width - 1, Width - 1, 0],
                              [0, 0, Height - 1, Height - 1],
                              [1, 1, 1, 1]])

    # Finding the final coordinates of the corners of the image after transformation.
    # NOTE: Here, the coordinates of the corners of the frame may go out of the
    # frame(negative values). We will correct this afterwards by updating the
    # homography matrix accordingly.
    FinalMatrix = np.dot(HomographyMatrix, InitialMatrix)

    [x, y, c] = FinalMatrix
    x = np.divide(x, c)
    y = np.divide(y, c)

    # Finding the dimentions of the stitched image frame and the "Correction" factor
    min_x, max_x = int(round(min(x))), int(round(max(x)))
    min_y, max_y = int(round(min(y))), int(round(max(y)))

    New_Width = max_x
    New_Height = max_y
    Correction = [0, 0]
    if min_x < 0:
        New_Width -= min_x
        Correction[0] = abs(min_x)
    if min_y < 0:
        New_Height -= min_y
        Correction[1] = abs(min_y)

    # Again correcting New_Width and New_Height
    # Helpful when secondary image is overlaped on the left hand side of the Base image.
    if New_Width < Base_ImageShape[1] + Correction[0]:
        New_Width = Base_ImageShape[1] + Correction[0]
    if New_Height < Base_ImageShape[0] + Correction[1]:
        New_Height = Base_ImageShape[0] + Correction[1]

    # Finding the coordinates of the corners of the image if they all were within the frame.
    x = np.add(x, Correction[0])
    y = np.add(y, Correction[1])
    OldInitialPoints = np.float32([[0, 0],
                                   [Width - 1, 0],
                                   [Width - 1, Height - 1],
                                   [0, Height - 1]])
    NewFinalPonts = np.float32(np.array([x, y]).transpose())

    # Updating the homography matrix. Done so that now the secondary image completely
    # lies inside the frame
    HomographyMatrix = cv2.getPerspectiveTransform(OldInitialPoints, NewFinalPonts)
    
    return [New_Height, New_Width], Correction, HomographyMatrix

Maintenant, nous devons assembler les images. Pour cela, nous utiliserons la matrice d’homographie pour déformer la 2ème image, pour qu’elle puisse se coller sur l’image de base.

On utilise pour ça la fonction « warpPerspective » dans OpenCV. Cette fonction prend les arguments suivants :

  • L’image source, qui est l’image de droite dans notre cas
  • La matrice d’homographie que nous avons calculée précédemment
  • Les dimensions de l’image de sortie
  • Des indicateurs pour l’interpolation

Voici le code pour assembler les images :

def StitchImages(BaseImage, SecImage):
    # Finding matches between the 2 images and their keypoints
    Matches, BaseImage_kp, SecImage_kp = FindMatches(BaseImage, SecImage)

    # Finding homography matrix.
    HomographyMatrix, Status = FindHomography(Matches, BaseImage_kp, SecImage_kp)

    # Finding size of new frame and correction matrix
    [New_Height, New_Width], Correction, NewHomographyMatrix = GetNewFrameSizeAndMatrix(HomographyMatrix, SecImage.shape, BaseImage.shape)

    # Warping the secondary image onto the base image using homography matrix
    WarpedImage = cv2.warpPerspective(SecImage, NewHomographyMatrix, (New_Width, New_Height))

    # Combining the base and secondary image
    BaseImage[Correction[1]:Correction[1] + WarpedImage.shape[0], Correction[0]:Correction[0] + WarpedImage.shape[1]] = WarpedImage

    return BaseImage

Pour utiliser ces fonctions en situation concrète pour assembler 2 images, il ne reste plus qu’à faire ceci :

# Reading in two images
BaseImage = cv2.imread('image1.jpg')
SecImage = cv2.imread('image2.jpg')

# Stitching the images together
StitchedImage = StitchImages(BaseImage, SecImage)

# Displaying the final image
cv2.imshow('Stitched Image', StitchedImage)
cv2.waitKey(0)
cv2.destroyAllWindows()

Conclusion

Dans ce tutoriel, nous avons appris à réaliser l’image stitching à partir de deux images en utilisant la bibliothèque OpenCV en Python.

Nous avons commencé par faire de l’extraction de features sur les deux images, puis nous avons utilisé l’algorithme RANSAC pour estimer la matrice d’homographie qui permet de projeter une image sur l’autre. Enfin, nous avons fusionné les deux images en utilisant la fonction warpPerspective d’OpenCV.

Si vous voulez réaliser l’image stitching sur plus que deux images, il sera facile d’adapter ce code de cette façon :

  • Choisissez une image de base à laquelle vous souhaitez ajouter d’autres images
  • Pour chaque image supplémentaire, calculez la matrice d’homographie qui correspond à la transformation de l’image pour l’ajuster à la base
  • Combinez les matrices d’homographie des différentes images pour obtenir une matrice globale
  • Utilisez cette matrice globale pour projeter toutes les images sur la base
  • Enfin, fusionnez toutes les images en une seule image en utilisant des méthodes comme la moyenne pondérée ou la fusion pyramidale