Encodage RLE et masques binaires avec python

Un focus sur l'encodage RLE et l'application d'un masque binaire sur vos images

Qu'est ce qu'un masque binaire ?

Le masquage est une opération de logique combinatoire utilisée en informatique et en électronique pour ne conserver qu'un sous-ensemble de bits. En ce qui concerne les images, on pourrait comparer cela de manière très grossière à un pochoir.
Voici ci-dessous à quoi ressemble un masque, ainsi à gauche nous avons l'image originale, et à droite le masque avec lequel nous allons travailler.

RLE masque binaire mask binary python

Le masque n'est en réalité rien d'autre qu'une matrice a 2 dimensions de la taille de l'image et constituée exclusivement de 0 et de 1. Les différentes librairies graphiques vont interpréter les pixels originaux situés au niveau des 1 du masque comme étant les pixels à conserver, les autres seront ignorés.
Outre un intérêt graphique, les masques binaires sont surtout utilisés pour identifier une partie d'une image et l'associer à une classe d'objet, c'est ce que l'on appelle la segmentation.
Les masques binaires, de par leur structure très basique, sont relativement simple à stocker. Ils seront de préférence stockés, non pas sous forme d'image, mais sous la forme d'un code RLE (pour Run-Length Encoding). Voyons comment se présente un tel code.

L'encodage RLE

Prenons un exemple de masque binaire très simple :

RLE masque binaire mask binary python

A gauche, un masque sur lequel nous avons matérialisé chaque pixel par un carré. Ce masque a une taille de 8 X 8 pixels.
A droite le même masque sur lequel nous avons numéroté chaque pixel. Le code RLE qui résulte de ce masque va commencer ainsi : 12 2 18 4 ...
Concrètement il faut lire : A partir du 12e pixel, on conserve 2 pixels, à partir du 18e pixel, on en conserve 4, etc ...

Convertir un code RLE en masque binaire

Nous avons donc notre code RLE et savons l'interpréter, il nous reste à écrire une fonction qui va nous permettre le convertir en masque binaire, comprenez en tableau a 2 dimensions constitué de 0 et de 1.
Nous considérons pour la suite que notre code RLE est stocké dans une variable "rle". Il s'agit du code RLE associé au masque de la chouette présenté plus haut. Voici a quoi il ressemble :

114218 5 114804 9 115390 13 115976 18 ...

Et voici maintenant la fonction qui va nous permettre de le convertir. L'objet qui en résulte est un numpy array.


import numpy as np 

def rleToMask(rle: str, shape: tuple  =(1400, 2100)) -> np.ndarray:
    """
     Conversion d'un codage RLE en masque  
  
     Paramètre  
     ----------  
     rle   : encodage RLE a convertir  
     shape : format du masque  
  
     Retour  
     ----------  
     np.array : masque  
    """

    width, height = shape[:2]

    mask= np.zeros( width*height ).astype(np.uint8)

    array = np.asarray([int(x) for x in rle.split()])
    starts = array[0::2]
    lengths = array[1::2]

    current_position = 0
    for index, start in enumerate(starts):
        mask[int(start):int(start+lengths[index])] = 1
        current_position += lengths[index]

    return mask.reshape(height, width).T

Appelons cette fonction sur notre code RLE. Nous précisons que nous voulons un tableau en sortie de taille 588 X 640 car d'une part c'est la taille de l'image d'origine, et d'autre part, vous avez surement constaté qu'un code RLE ne contient pas ces informations.


mask = rleToMask(rle, (588, 640))

Voici le résultat :


print('min:%s, max: %s' % (mask.min(), mask.max()))
print('taille du tableau : ', mask.shape)
mask
min:0, max: 1
taille du tableau :  (588, 640)
array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]],...

La matrice est constituée uniquement de 0 et de 1 et elle a une taille de 588 par 640.

Application simple d'un masque binaire

Commencons par appliquer le masque sur notre image originale.


#Import des librairies
import os
import matplotlib.pyplot as plt
import cv2

#Chargement de l'image originale
image_start = plt.imread("chouette.jpg")

#Application du masque binaire
result1 = image_start.copy()
result1[mask == 0] = 0
result1[mask != 0] = image_start[mask != 0]

#Affichage des images
f, ax = plt.subplots(1, 2, figsize = (10, 5))
ax[0].imshow(image_start) 
ax[1].imshow(result1) 
plt.show()

Voici ci-dessous le résultat. Pour information il existe une multitude de façon d'effectuer les opérations logiques ou encore de filtrer les pixels afin d'appliquer un masque. En complément de ce qui a été fait on peut citer notamment la méthode bitwise_and de la librairie open CV.

RLE masque binaire mask binary python

Superposition d'un masque binaire sur l'image originale

Plaçons-nous désormais dans la situation ou nous venons d'effectuer une segmentation. Notre modèle a identifié la classe d'intérêt dans l'image (une chouette en ce qui nous concerne) et nous voulons restituer le résultat sur l'image d'origine.
Pour cela plusieurs solutions, la première est de faire une simple superposition (overlay).

Définissons tout d'abord la fonction.


def maskInColor(image : np.ndarray, 
                mask : np.ndarray, 
                color : tuple = (0,0,255), 
                alpha : float=0.2) -> np.ndarray:
    """
    Overlay mask on image

     Parameter
     ----------
     image : image on which we want to overlay the mask 
     mask  : mask to process
     color : color we want to apply on mask
     alpha : opacity coefficient

     Return
     ----------
     np.array : result of layering
    """
    
    image = np.array(image)
    H,W,C = image.shape
    mask    = mask.reshape(H,W,1)
    overlay = image.astype(np.float32)
    overlay =  255-(255-overlay)*(1-mask*alpha*color/255 )
    overlay = np.clip(overlay,0,255)
    overlay = overlay.astype(np.uint8)
    return overlay

Voici le résultat ci-dessous. Le ciel est resté, comme tout autre objet, s'il y en avait eu. La chouette a été marquée d'une trame jaune légèrement transparente.


fig, ax = plt.subplots()

image = np.copy(image_start)         
image = maskInColor(image, mask, color=(255,255,0), alpha=0.4)

ax.imshow(image) 
plt.show()
RLE masque binaire mask binary python

Délimitation dans un cadre

Une seconde solution pourrait être de tracer un cadre autour de l'objet ciblé par le masque.

Voici la fonction associée à cette opération.


from skimage.measure import label, regionprops

def trace_boundingBox(image : np.ndarray,
                      mask : np.ndarray,
                      color : tuple = (0,0,255),
                      width : int = 10):
    """
    Draw a bounding box on image

     Parameter
     ----------
     image : image on which we want to draw the box 
     mask  : mask to process
     color : color we want to use to draw the box edges
     width : box edges's width

    """
    
    lbl = label(mask)
    props = regionprops(lbl)
    for prop in props:
        coin1 = (prop.bbox[3], prop.bbox[2])
        coin2 = (prop.bbox[1], prop.bbox[0])
        cv2.rectangle(image, coin2, coin1, color, width)

Et le résultat.


fig, ax = plt.subplots()

image = np.copy(image_start)
trace_boundingBox(image, mask, color=(255,255,0))

ax.imshow(image) 
plt.show()
RLE masque binaire mask binary python

Coloration

Enfin une troisième solution serait, pour un objet clair par rapport au reste de l'image, de le colorer en se basant sur un seuil de luminosité.

Voici la fonction associée à cette opération.


def coloration(image : np.ndarray, 
                 mask : np.ndarray, 
                 color : tuple = (0,0,255),
                 alpha : float = 0.7, 
                 threshold : int = 90) -> np.ndarray:
    """
    Color image through mask

     Parameter
     ----------
     image : image on which we want to colorize parts 
     mask  : mask to process
     color : color we want to use to colorize image
     alpha : opacity coefficient
     threshold : pixel value threshold to apply
     
     Return
     ----------
     np.array : result of layering
    """    
    imZone = cv2.bitwise_and(image, image, mask=mask)
    image_gray = cv2.cvtColor(imZone, cv2.COLOR_RGB2GRAY)
    (thresh, blackAndWhiteImage) = cv2.threshold(image_gray, 
                                                 threshold, 
                                                 255, 
                                                 cv2.THRESH_BINARY)
    inlay = maskInColor(image, blackAndWhiteImage, color=color, alpha=alpha)
    return inlay

Et le résultat.


fig, ax = plt.subplots()

image = np.copy(image_start)        
image = coloration(image, mask, color=(255,255,0), alpha=1, threshold=160)

ax.imshow(image) 
plt.show()
RLE masque binaire mask binary python

On constate une légère teinte jaune sur la chouette.

BONUS : encodage RLE d'un masque binaire

Pour ceux que cela pourrait intéresser, voici ci-dessous une fonction qui va permettre de passer d'un masque binaire à un encodage RLE.


def maskToRle(mask: np.ndarray) -> str:
    """
     Conversion d'un masque en encodage RLE  
     La presence d'un pixel dans le masque se materialise par la valeur 1,  
     autrement la valeur reste a 0  
  
     Paramètre  
     ----------  
     mask : masque a encoder  
  
     Retour  
     ----------  
     str : encodage RLE  
    """

    valeur_pixels = mask.T.flatten()
    valeur_pixels = np.concatenate([[0], valeur_pixels, [0]])
    segment_rle = np.where(valeur_pixels[1:] != valeur_pixels[:-1])[0] + 1
    segment_rle[1::2] -= segment_rle[::2]
    return ' '.join(str(x) for x in segment_rle)

Crédits

La magnifique chouette des neiges qui a servie de modèle est a mettre au crédit de James Bekkers sur Unsplash.


Retrouvez dans la rubrique "Nos datasets" toutes les données dont vous aurez besoin pour tester et pratiquer !