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.

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 :

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.

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()

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()

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()

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 !