Recherche de site Web

Augmentation des données pour les boîtes englobantes : rotation et cisaillement


Dans cet article, nous expliquerons comment implémenter la rotation et le cisaillement des images ainsi que les cadres de délimitation à l'aide des fonctionnalités de transformation affine d'OpenCV.

Avant de commencer, il est fortement recommandé d'avoir parcouru les deux dernières parties de la série car elles constituent la base de ce que nous allons faire ici.

Dépôt GitHub

Tout ce qui est contenu dans cet article et dans l'intégralité de la bibliothèque d'augmentation peut être trouvé dans le dépôt Github suivant.

https://github.com/Paperspace/DataAugmentationForObjectDetection

Allons-y.

Rotation

Les résultats d'une transformation Rotation ressemblent généralement à ceci

La rotation est l’une des augmentations de données les plus désagréables à gérer. Bientôt, vous saurez pourquoi.

Avant de nous salir les mains avec le code, j'aimerais définir quelques termes ici.

Transformation affine. Transformation d'une image telle que les lignes parallèles d'une image restent parallèles après la transformation. La mise à l'échelle, la translation, la rotation sont tous des exemples de transformations affines

En infographie, nous utilisons également ce qu'on appelle une matrice de transformation, qui est un outil très pratique pour effectuer des transformations affines.

Une discussion détaillée de la matrice de transformation n’est pas possible car elle nous éloignerait de notre tâche. J’ai donc fourni un lien à la fin de l’article où vous pouvez en savoir plus à ce sujet. En attendant, considérez la matrice de transformation comme une matrice avec laquelle vous multipliez les coordonnées d’un point pour produire le point transformé.

$$T_p=M * [ x\:y\:1]^T $$

La matrice de transformation est une matrice 2 x 3, qui est multipliée par [x y 1] où (x,y) sont les coordonnées du point. L'idée d'avoir un 1 est de faciliter le cisaillement, et vous pouvez en savoir plus à ce sujet dans le lien ci-dessous. En multipliant une matrice 2 x 3 par une matrice 3 x 1, nous obtenons une matrice 2 x 1 contenant les nouvelles coordonnées du point.

La matrice de transformation peut également être utilisée pour obtenir les coordonnées d'un point après rotation autour du centre de l'image. Voici à quoi ressemble la matrice de transformation pour faire pivoter un point de $\theta$.

Heureusement, nous n’aurons pas à le coder. OpenCV fournit déjà des fonctionnalités intégrées pour le faire en utilisant sa fonction cv2.warpAffine. Alors, avec les connaissances théoriques requises derrière nous, commençons.

Nous commençons par définir notre fonction __init__.

def __init__(self, angle = 10):
    self.angle = angle

    if type(self.angle) == tuple:
        assert len(self.angle) == 2, "Invalid range"   
    else:
        self.angle = (-self.angle, self.angle)

Rotation de l'image

Maintenant, la première chose que nous devons faire est de faire pivoter notre image d'un angle $\theta$par rapport au centre. Pour cela, nous avons besoin de notre matrice de transformation. Nous utilisons la fonction OpenCV getRotationMatrix2D pour cela.

(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)

Maintenant, nous pouvons obtenir la rotation de l'image simplement en utilisant la fonction warpAffine.

image = cv2.warpAffine(image, M, (w, h))

Le troisième argument de la fonction est (w,h), ce qui est dû au fait que nous voulons conserver la résolution d'origine. Mais si vous imaginez un peu, une image pivotée aura des dimensions différentes, et si elles dépassent les dimensions d'origine, OpenCV les coupera simplement. Voici un exemple.

Effet secondaire de la rotation OpenCV.

Nous perdons ici des informations. Alors, comment pouvons-nous surmonter cela ? Heureusement, OpenCV nous fournit un argument à la fonction qui nous aide à déterminer les dimensions de l'image finale. Si nous pouvons le changer de (w,h) à une dimension qui juste s'adaptera à notre image pivotée, nous avons terminé.

L'inspiration vient d'un article d'Adrian Rosebrock sur son blog PyImageSearch.

La question est maintenant de savoir comment trouver ces nouvelles dimensions. Un peu de trigonométrie peut faire le travail à notre place. Justement, si vous regardez le schéma suivant.

Source de l'image :

$$N_w=h * sin(\theta) + w * cos(\theta) \\ N_h=h * cos(\theta) + w * sin(\theta) $$

Alors maintenant, nous calculons la nouvelle largeur et la nouvelle hauteur. Notez que nous pouvons obtenir les valeurs de $\sin(\theta)$et $\cos(\theta)$à partir de la matrice de transformation.

cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))

Il manque encore quelque chose. Une chose est sûre, le centre de l'image ne bouge pas puisqu'il s'agit de l'axe de rotation lui-même. Cependant, puisque la largeur et la hauteur de l'image sont désormais nW, nH, le centre doit se situer à nW/2, nH/2. Pour être sûr que cela se produise, nous devons traduire l'image par nW/2 - cX, nH/2 - cHcX, cH sont les centres précédents.

# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY

Pour résumer, on met le code responsable de la rotation d'une image dans une fonction rotate_im et on le place dans le bbox_util.py

def rotate_im(image, angle):
    """Rotate the image.

    Rotate the image such that the rotated image is enclosed inside the tightest
    rectangle. The area not occupied by the pixels of the original image is colored
    black. 

    Parameters
    ----------

    image : numpy.ndarray
        numpy image

    angle : float
        angle by which the image is to be rotated

    Returns
    -------

    numpy.ndarray
        Rotated Image

    """
    # grab the dimensions of the image and then determine the
    # centre
    (h, w) = image.shape[:2]
    (cX, cY) = (w // 2, h // 2)

    # grab the rotation matrix (applying the negative of the
    # angle to rotate clockwise), then grab the sine and cosine
    # (i.e., the rotation components of the matrix)
    M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])

    # compute the new bounding dimensions of the image
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))

    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY

    # perform the actual rotation and return the image
    image = cv2.warpAffine(image, M, (nW, nH))
   image = cv2.resize(image, (w,h))
    return image

Rotation du cadre de sélection

C’est la partie la plus difficile de cette augmentation. Ici, nous devons d’abord faire pivoter la boîte englobante, ce qui nous donne une boîte rectangulaire inclinée. Ensuite, il faut trouver le rectangle le plus serré parallèle aux côtés de l'image contenant la boîte rectangulaire inclinée.

Voici ce que je veux dire.

Boîte de sélection finale, affichée uniquement pour une image.

Maintenant, pour obtenir la boîte englobante pivotée, comme le montre l'image du milieu, nous devons avoir toutes les coordonnées des 4 coins d'une boîte.

Nous pourrions en fait obtenir le cadre de délimitation final en utilisant 2 coins seulement, mais cela prendrait plus de trigonométrie pour déterminer les dimensions du cadre de délimitation final (à droite dans l'image ci-dessus, en noir) en utilisant seulement 2 coins. Avec 4 coins de la boîte intermédiaire au milieu, il est beaucoup plus facile de faire ce calcul. Il s’agit simplement de rendre le code plus compliqué.

Donc, d'abord, on écrit la fonction get_corners dans le fichier bbox_utils.py pour obtenir les 4 coins.

def get_corners(bboxes):
    
    """Get corners of bounding boxes

    Parameters
    ----------

    bboxes: numpy.ndarray
        Numpy array containing bounding boxes of shape `N X 4` where N is the 
        number of bounding boxes and the bounding boxes are represented in the
        format `x1 y1 x2 y2`

    returns
    -------

    numpy.ndarray
        Numpy array of shape `N x 8` containing N bounding boxes each described by their 
        corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`      

    """
    width = (bboxes[:,2] - bboxes[:,0]).reshape(-1,1)
    height = (bboxes[:,3] - bboxes[:,1]).reshape(-1,1)

    x1 = bboxes[:,0].reshape(-1,1)
    y1 = bboxes[:,1].reshape(-1,1)

    x2 = x1 + width
    y2 = y1 

    x3 = x1
    y3 = y1 + height

    x4 = bboxes[:,2].reshape(-1,1)
    y4 = bboxes[:,3].reshape(-1,1)

    corners = np.hstack((x1,y1,x2,y2,x3,y3,x4,y4))

    return corners

Une fois cela fait, nous avons maintenant chaque cadre de délimitation décrit par 8 coordonnées x1,y1,x2,y2,x3,y3,x4,y4. Nous définissons maintenant la fonction rotate_box dans le fichier bbox_util.py qui fait pivoter les boîtes englobantes pour nous en nous donnant les points transformés. Nous utilisons pour cela la matrice de transformation.

def rotate_box(corners,angle,  cx, cy, h, w):
    
    """Rotate the bounding box.


    Parameters
    ----------

    corners : numpy.ndarray
        Numpy array of shape `N x 8` containing N bounding boxes each described by their 
        corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`

    angle : float
        angle by which the image is to be rotated

    cx : int
        x coordinate of the center of image (about which the box will be rotated)

    cy : int
        y coordinate of the center of image (about which the box will be rotated)

    h : int 
        height of the image

    w : int 
        width of the image

    Returns
    -------

    numpy.ndarray
        Numpy array of shape `N x 8` containing N rotated bounding boxes each described by their 
        corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`
    """

    corners = corners.reshape(-1,2)
    corners = np.hstack((corners, np.ones((corners.shape[0],1), dtype = type(corners[0][0]))))

    M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0)


    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])

    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))
    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cx
    M[1, 2] += (nH / 2) - cy
    # Prepare the vector to be transformed
    calculated = np.dot(M,corners.T).T

    calculated = calculated.reshape(-1,8)

    return calculated

Maintenant, la dernière chose est de définir une fonction get_enclosing_box qui nous permet d'obtenir la boîte la plus étroite dont on parle.

def get_enclosing_box(corners):
    """Get an enclosing box for ratated corners of a bounding box
    
    Parameters
    ----------
    
    corners : numpy.ndarray
        Numpy array of shape `N x 8` containing N bounding boxes each described by their 
        corner co-ordinates `x1 y1 x2 y2 x3 y3 x4 y4`  
    
    Returns 
    -------
    
    numpy.ndarray
        Numpy array containing enclosing bounding boxes of shape `N X 4` where N is the 
        number of bounding boxes and the bounding boxes are represented in the
        format `x1 y1 x2 y2`
        
    """
    x_ = corners[:,[0,2,4,6]]
    y_ = corners[:,[1,3,5,7]]
    
    xmin = np.min(x_,1).reshape(-1,1)
    ymin = np.min(y_,1).reshape(-1,1)
    xmax = np.max(x_,1).reshape(-1,1)
    ymax = np.max(y_,1).reshape(-1,1)
    
    final = np.hstack((xmin, ymin, xmax, ymax,corners[:,8:]))
    
    return final

Cela nous donne encore une fois une notation où chaque boîte englobante est déterminée par 4 coordonnées ou deux coins. En utilisant toutes ces fonctions d'assistance, nous avons finalement mis en place notre fonction __call__.

def __call__(self, img, bboxes):

    angle = random.uniform(*self.angle)

    w,h = img.shape[1], img.shape[0]
    cx, cy = w//2, h//2

    img = rotate_im(img, angle)

    corners = get_corners(bboxes)

    corners = np.hstack((corners, bboxes[:,4:]))


    corners[:,:8] = rotate_box(corners[:,:8], angle, cx, cy, h, w)

    new_bbox = get_enclosing_box(corners)


    scale_factor_x = img.shape[1] / w

    scale_factor_y = img.shape[0] / h

    img = cv2.resize(img, (w,h))

    new_bbox[:,:4] /= [scale_factor_x, scale_factor_y, scale_factor_x, scale_factor_y] 

    bboxes  = new_bbox

    bboxes = clip_box(bboxes, [0,0,w, h], 0.25)

    return img, bboxes

Remarquez à la fin de la fonction, nous redimensionnons notre image et les cadres englobants afin que nos dimensions finales soient w,h et non nW, nH. C'est juste pour préserver les dimensions de l'image. Nous découpons également les boîtes au cas où une boîte serait hors de l'image après transformation.

Tonte

Le cisaillement est une autre transformation de boîte englobante, qui peut être effectuée à l'aide de la matrice de transformation. L'effet produit par le cisaillement ressemble à.

Lors du cisaillement, nous transformons l'image rectangulaire en… euh… une sorte d'image parallélogramme ? La matrice de transformation utilisée en cisaillement est la suivante.

Ce qui précède est un exemple de cisaillement horizontal. En cela, le pixel avec les coordonnées x, y est déplacé vers x + alpha*y, y. alpha est le facteur de cisaillement. Nous définissons donc notre fonction __init__ comme.

class RandomShear(object):
    """Randomly shears an image in horizontal direction   
    
    
    Bounding boxes which have an area of less than 25% in the remaining in the 
    transformed image is dropped. The resolution is maintained, and the remaining
    area if any is filled by black color.
    
    Parameters
    ----------
    shear_factor: float or tuple(float)
        if **float**, the image is sheared horizontally by a factor drawn 
        randomly from a range (-`shear_factor`, `shear_factor`). If **tuple**,
        the `shear_factor` is drawn randomly from values specified by the 
        tuple
        
    Returns
    -------
    
    numpy.ndaaray
        Sheared image in the numpy format of shape `HxWxC`
    
    numpy.ndarray
        Tranformed bounding box co-ordinates of the format `n x 4` where n is 
        number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
        
    """

    def __init__(self, shear_factor = 0.2):
        self.shear_factor = shear_factor
        
        if type(self.shear_factor) == tuple:
            assert len(self.shear_factor) == 2, "Invalid range for scaling factor"   
        else:
            self.shear_factor = (-self.shear_factor, self.shear_factor)
        
        shear_factor = random.uniform(*self.shear_factor)

Logique d'augmentation

Puisque nous ne couvrons que le cisaillement horizontal, il nous suffit de modifier les coordonnées x des coins des boîtes selon l'équation x=x + alpha*y.  Notre fonction d'appel ressemble à.

def __call__(self, img, bboxes):

    shear_factor = random.uniform(*self.shear_factor)

    w,h = img.shape[1], img.shape[0]

    if shear_factor < 0:
        img, bboxes = HorizontalFlip()(img, bboxes)

    M = np.array([[1, abs(shear_factor), 0],[0,1,0]])

    nW =  img.shape[1] + abs(shear_factor*img.shape[0])

    bboxes[:,[0,2]] += ((bboxes[:,[1,3]]) * abs(shear_factor) ).astype(int) 


    img = cv2.warpAffine(img, M, (int(nW), img.shape[0]))

    if shear_factor < 0:
        img, bboxes = HorizontalFlip()(img, bboxes)

    img = cv2.resize(img, (w,h))

    scale_factor_x = nW / w

    bboxes[:,:4] /= [scale_factor_x, 1, scale_factor_x, 1] 


    return img, bboxes

Un cas intéressant est celui d’un cisaillement négatif. Un cisaillement négatif nécessite un peu plus de piratage pour fonctionner. Si nous cisaillons simplement en utilisant le cas que nous faisons avec un cisaillement positif, nos boîtes résultantes doivent être plus petites. En effet, pour que l'équation fonctionne, les coordonnées des cases doivent être au format x1, y1, x2, y2x2 est le coin qui se trouve le plus loin dans la direction dans laquelle nous cidons.

Cela fonctionne dans le cas d'un cisaillement positif, car dans notre paramètre par défaut, x2 est la coordonnée x du coin inférieur droit tandis que x1 est la coordonnée supérieure gauche. La direction du cisaillement est positive ou de gauche à droite.

Lorsque nous utilisons un cisaillement négatif, la direction du cisaillement est de droite à gauche, tandis que x2 n'est pas plus loin dans la direction négative que x1. Une façon de résoudre ce problème pourrait être d'obtenir l'autre ensemble de coins (cela satisferait la contrainte, pouvez-vous le prouver ?). Appliquez la transformation de cisaillement, puis passez à l'autre ensemble de coins en raison de la notation que nous suivons.

Eh bien, nous pourrions le faire, mais il existe une meilleure méthode. Voici comment effectuer un cisaillement négatif avec le facteur de cisaillement -alpha.

  1. Retournez l'image et les cases horizontalement.
  2. Appliquer la transformation de cisaillement positif avec le facteur de cisaillement alpha
  3. Retournez à nouveau l'image et les cases horizontalement.

Je préférerais que vous preniez une feuille de papier et un stylo pour valider pourquoi la méthodologie ci-dessus fonctionne ! Pour cette raison, vous verrez les deux occurrences de lignes de code dans la fonction ci-dessus qui traitent des cisaillements négatifs.

if shear_factor < 0:
    img, bboxes = HorizontalFlip()(img, bboxes)
 

Le tester

Maintenant que nous en avons terminé avec les augmentations de rotation et de cisaillement, il est temps de les tester.

from data_aug.bbox_utils import *
import matplotlib.pyplot as plt

rotate = RandomRotate(20)  
shear = RandomShear(0.7)

img, bboxes = rotate(img, bboxes)
img,bboxes = shear(img, bboxes)

plt.imshow(draw_rect(img, bboxes))

C'est tout pour cette partie, et nous avons presque terminé nos augmentations. Il ne reste plus qu'une petite augmentation Redimensionnement, qui est plus une étape de prétraitement d'entrée qu'une augmentation.

Dans la prochaine et dernière partie, nous vous montrerons comment intégrer rapidement ces augmentations dans vos pipelines d'entrée d'apprentissage en profondeur, comment les combiner de manière transparente et comment générer de la documentation.

Exercices

Voici quelques-unes des choses que vous pouvez essayer par vous-même.

  1. Implémentez les versions déterministes des augmentations ci-dessus.
  2. La mise à l'échelle et la traduction peuvent également être implémentées à l'aide de la matrice de transformation avec un code beaucoup plus petit. Essayez de les implémenter de cette manière.
  3. Mettre en œuvre une cisaille verticale

Lectures complémentaires

  1. Rotation dans OpenCV
  2. Matrice de transformation
  3. Faire pivoter les images (correctement) avec OpenCV et Python

Articles connexes: