Traitements et analyses d'images

en langage python

L'essentiel de cette page

Traiter une image, c'est d'abord jouer sur les couleurs, la luminosité, le contraste. Dans un deuxième temps, c'est faire de la segmentation et utiliser le machine learning pour détecter des formes, des objets.

1- Ouvrir une image sous python

Installer PIL avec la commande pip :

pip install Pillow

from PIL import Image # Pour la gestion des images

import easygui # Pour ouvrir automatiquement en cliquant dans une fenêtre de l'explorateur (pas besoin d'indiquer un nom ni un chemin)

myimg = Image.open(easygui.fileopenbox())

  • Voir l'image

myimg.show()

  • La voir dans une fenêtre graphique

import matplotlib.pyplot as plt

plt.imshow(myimg) ; plt.show()

2- Décrire/retoucher une image en la convertissant en objet numpy

  • 1- Convertir l'image au format numpy et la décrire

import numpy as np

img = np.array(myimg)

plt.imshow(img) ; plt.show() # On peut continuer à utiliser la fonction imshow pour visualiser l'image même au format numpy

print("Dimensions de l'image : ",img.shape) # Dimensions x, y et nombre de couleurs

print("Pixel 500 sur 200 : ",img[200,500])

print("Composante rouge de ce dernier pixel :",img[200,500][0])

# Tous le rouge de tous les pixels

print("Rouge : ",img[:,:,0])

  • 2- Convertir les composantes rouge, vert et bleu en dataframe

red = np.reshape(img[:,:,0],img.shape[0]*img.shape[1])

green = np.reshape(img[:,:,1],img.shape[0]*img.shape[1])

blue = np.reshape(img[:,:,2],img.shape[0]*img.shape[1])

from pandas import *

couleurs = DataFrame({"r":red,"g":green,"b":blue})

print(couleurs.head(10))

  • 3- Réaliser des transformations sur ces composantes (exemples)

couleurs = couleurs + 30 # j'éclaircis tout de 30 niveau sur 255

couleurs.r = round(couleurs.r/3,0) # Je divise par 3 les valeurs de rouge

  • 4- Fabriquer une nouvelle image à partir des composantes rouge, vert bleu retravaillée

img1 = img.copy()

red = np.reshape(np.array(couleurs.r),(img.shape[0],img.shape[1]))

green = np.reshape(np.array(couleurs.g),(img.shape[0],img.shape[1]))

blue = np.reshape(np.array(couleurs.b),(img.shape[0],img.shape[1]))

img1[:,:,0] = red

img1[:,:,1] = green

img1[:,:,2] = blue

  • 5- Afficher comparer l'image avant après

plt.subplot(121)

plt.imshow(img)

plt.subplot(122)

plt.imshow(img1) ; plt.show()

3- Conversion en niveaux de gris, redimensionnement et facilitation de l'affichage

  • Une fonction pour faciliter l'affichage en basculant automatiquement en fonction du fait que l'image est en couleur ou en niveau de gris

def display(img,cmap='gray') :

if (len(img.shape) == 3) :

fig, ax = plt.subplots(figsize=(15, 12))

plt.imshow(img) ; plt.show()

else :

fig, ax = plt.subplots(figsize=(15, 12))

plt.imshow(img,cmap=cmap, vmin=0, vmax=255) ; plt.show()

  • Une fonction pour convertir une image en niveau de gris

###############################

# NVEAU DE GRIS

###############################

def gray_scale_img(img) :

temp_img = img[:,:,1]

temp_img = temp_img.reshape(temp_img.shape[0],temp_img.shape[1])

temp_img[:,:] <- img[:,:,0]*0.2126+img[:,:,1]*0.7152+img[:,:,2]*0.0722

return(temp_img)


img_grey = gray_scale_img(img)


display(img_grey)

  • Une fonction simple qui va réduire la taille d'une image (attention, pas de lissage = perte de signal plus accrue)

  • coeff : est le coeff de réduction

def redim_img(img,coeff=3) :

colonnes = list(range(0,img.shape[1]))

colonnes = colonnes[0::coeff]

lignes = list(range(0,img.shape[0]))

lignes = lignes[0::coeff]

if len(img.shape)==3 :

img_temp = img[lignes,:,:]

img_temp = img_temp[:,colonnes,:]

else :

img_temp = img[lignes,:]

img_temp = img_temp[:,colonnes]

return(img_temp)

Liens externes pour redimensionner une image avec un lissage de l'information (éviter les pertes de signal) :

# Réduction de la taille d'un facteur 6

img_red = redim_img(img,6)

display(img_red)


On voit que l'image est plus pixellisée, mais avantage, plus facile à manipuler pour tester des traitements.

4- Convolution, traitement de flou et de contraste

Une fonction pour appliquer un flou médian, moyen ou un calcul d'écart-type (cliquer sur la flèche pour afficher le code)

import numpy as np

def smooth(img,type="mean",kernel=9) : # type : convol, mean, median, sd...

img1 = img.copy()

border = (kernel-1)/2

border = int(border)

if (len(img.shape)==3) :

colored = img.shape[2]

else :

img = img.reshape(img.shape[0],img.shape[1],1)

img1 =img1.reshape(img1.shape[0],img1.shape[1],1)

colored = 1

for color in range(0,colored) :

lignemax = (img.shape[0]-border-1)

lignemax = int(lignemax)

for ligne in range(0,img.shape[0]) :

for colonne in range(0,img.shape[1]) :

lignemin = (ligne-border)

if (lignemin<0) :lignemin=0

lignemax = (ligne+border+1)

if (lignemax>(img.shape[0]-1)) : lignemax = img.shape[0]-1

colmin = (colonne-border)

if colmin < 0 : colmin=0

colmax = (colonne+border+1)

if colmax>(img.shape[1]-1) : colmax = img.shape[1]-1

extrait = img[lignemin:lignemax,colmin:colmax,(color):(color+1)]

if type == "mean" :

new_value = np.mean(extrait)

elif type == "median" :

new_value = np.median(extrait)

elif type == "sd" :

new_value = np.std(extrait, ddof =1)

img1[ligne,colonne,color] = new_value

if (colored==1):

img1 = img1.reshape(img.shape[0],img.shape[1])

return(img1)

img_median = smooth(img_red,"median",9)

display(img_median)

Autres types disponibles : mean (flou moyen) et sd (détection des zones de signal par l'écart-type).

Le 9 ici indique la taille de la fenêtre.

Une fonction pour appliquer un noyau de convolution (code à copier-coller en cliquant sur la flèche)

# Dimensions de l'image

img.shape

def convol(img,kernel) : # type : convol, mean, median, sd...

img1 = img.copy()

border = (kernel.shape[0]-1)/2

border = int(border)

if (len(img.shape)==3) :

colored = img.shape[2]

else :

img = img.reshape(img.shape[0],img.shape[1],1)

img1 =img1.reshape(img1.shape[0],img1.shape[1],1)

colored = 1

for color in range(0,colored) :

lignemax = (img.shape[0]-border-1)

lignemax = int(lignemax)

for ligne in range(border,lignemax) :

for colonne in range(border,(img.shape[1]-border-1)) :

#print(ligne) ; print(colonne) ; print(color)

extrait = img[(ligne-border):(ligne+border+1),(colonne-border):(colonne+border+1),(color):(color+1)]

extrait = extrait.reshape(kernel.shape[0],kernel.shape[0])

new_value = np.sum(extrait*kernel)

img1[ligne,colonne,color] = new_value

if (colored==1):

img1 = img1.reshape(img.shape[0],img.shape[1])

return(img1)

Exemples d'applications :

# Noyau de contours

contour = np.array([-1,-1,-1,-1,8,-1,-1,-1,-1])

contour = contour.reshape(3,3)

print(contour)

# Netteté

net = np.array([0,-1,0,-1,5,-1,0,-1,0])

net = net.reshape(3,3)

print(net)

# Exemple d'application du noyau de netteté

img_net = convol(smooth(redim_img(img_grey)),net)

display(img_net)

Cette fonction est l'équivalent de la fonction de scipy, ndimage.convolve(), mais elle a l'avantage de s'adapter au format couleur ou niveau de gris automatiquement, tout en permettant de contrôler le code pas à pas. Cet équivalent :

from scipy import ndimage

# Travail sur une seule composante couleur de img à la fois

imgA = img[:,:,1]

imgA = imgA.reshape(imgA.shape[0],imgA.shape[1])

img2 = ndimage.convolve(imgA, contour, mode='constant', cval=0.0)

5- Segmenter une image et détecter les régions de même nature

Il existe plusieurs méthode de k-means pour catégoriser automatiquement les points et les segmenter mais problème ! C'est en général tellement lourd que ça plante.

On pourrait très bien faire un k-means classique sur le tableau de composantes rgb de l'exemple ci-dessus (partie 2) mais la fonction de k-means a toutes les chances de planter à cause de la taille de la dataframe.

L'idéal est d'utiliser opencv, ce module permet d'implémenter de nombreuses fonctions très rapide de traitement d'images.

1- Réaliser une segmentation par k-means

1) Défaire l'image pour la transformer dans une array

Z = img_red.reshape((-1,3))

Z = np.float32(Z)

2) Réaliser le K-means sur l'image déroulée

from sklearn.cluster import KMeans

nombre_de_categories = 5

kmeans = KMeans(n_clusters=nombre_de_categories).fit(Z)

groupes = kmeans.predict(Z)

img_groupes = groupes.reshape(img_red.shape[0],img_red.shape[1])

display(img_groupes*40)

Les 5 catégories me permettent d'estimer ici que j'ai isolé mon herbier (couleur sombre) : je vais donc pouvoir continuer en segmentant mon image.

3) Récupérer les valeurs moyennes de couleurs de chaque catégorie

centers = kmeans.cluster_centers_

centers = centers.astype(np.uint8)

  • 4) Attribuer à chaque pixel la valeur moyenne de couleur de sa catégorie (segmentation)

Zimg = Z.copy()

for i in range(0,centers.shape[0]) :

Zimg[np.where(groupes==i),:] = centers[i,:]

# recompiler une image segmentée

img_K = Zimg.reshape(img_red.shape[0],img_red.shape[1],img_red.shape[2])

img_K = img_K.astype(np.uint8)

display(img_K)

On voit sur l'image 5 couleurs. Clairement, les herbiers correspondent au pixels sombre. Il faut voir quel numéro de catégorie lui correspond.

  • 5) Afficher les couleurs de centres pour identifier la catégorie d'intérêt

centers_img = centers.reshape(1,nombre_de_categories,3)

display(centers_img)

On voit que le groupe 0 correspond à nos herbiers de posidonies.

  • 6) Binariser l'image avec 1) ce que je veux (herbiers) et 0) ce que je ne veux pas (le reste).

herbiers = groupes.copy()

mycat = 0

herbiers[np.where(herbiers!=mycat)]= 1000

herbiers[np.where(herbiers==mycat)]= 1

herbiers[np.where(herbiers==1000)]= 0

plt.hist(herbiers) ; plt.show()


herbiers2 = herbiers.copy()

herbiers2 = herbiers2.reshape(img_red.shape[0],img_red.shape[1])

display(herbiers2*250)

Tous les pixels herbiers valent maintenant 1 et les autres pixels 0. Je vais maintenant pouvoir tenir compte des dimensions spatiales pour localiser les herbiers. La méthode SVM permet cette approche.

2- Réaliser une détection lissée des zones par SVM

  • 1) Créons un tableau qui contient 3 colonnes, 2 qui détaillent les position des pixels (ligne et colonne) et la troisième le type (1- herbier ou 0- pas herbier)

# Créons une array contenant une colonne row, une colonne col, une colonne catégorie (herbier)

myrows = herbiers.copy()

myrows = myrows.reshape(herbiers2.shape[0],herbiers2.shape[1])

for i in range(0,myrows.shape[0]) :

myrows[i,:] = i

mycols = herbiers.copy()

mycols = mycols.reshape(herbiers2.shape[0],herbiers2.shape[1])

for i in range(0,mycols.shape[1]) :

mycols[:,i] = i

myrows = myrows.reshape((-1))

mycols = mycols.reshape((-1))

print(myrows.shape)

print(mycols.shape)

print(herbiers.shape)

DOC = np.empty([herbiers.shape[0],3])

DOC.shape

DOC[:,0] = myrows

DOC[:,1] = mycols

DOC[:,2] = herbiers

#DOC[0:200,:]

plt.scatter(DOC[:,0],DOC[:,1],c=DOC[:,2])

plt.show()

  • 2) Appliquons la méthode SVM pour entourer les zones à herbiers (paramétrage gamma et C à ajuster à son image)

# Regroupement SVM

from sklearn import svm

clf = svm.SVC(kernel='rbf', C=0.1, gamma=0.1)

# gamma : épaisseur du feutre, grossier ou petits traits

# C : inverse du coût => coefficient de lissage

clf.fit(DOC[:,0:2],DOC[:,2])

prediction = clf.predict(DOC[:,0:2])

prediction_img = prediction.reshape(img_red.shape[0],img_red.shape[1])

prediction_img = prediction_img.astype(np.uint8)

prediction_img.shape

display(prediction_img*250)

On voit que les herbiers sont pas si mal identifiés. Le bateau au centre a pu être éliminé.

Ci-dessous, le résultat de la détection est superposé en rouge à l'image. Détection d'herbiers en cours ! ;-)

# Pour afficher le résultat de détection coloré en rouge

compil = img_red.copy()

# Ajoute 60 à la composante rouge de chaque pixel de la catégorie 1

compil[:,:,0] = compil[:,:,0] + prediction_img*60

display(compil)

Remarque : un package puissant dédié aux images (cv2) permet de faire la même chose et même d'aller plus loin (comptage, interconnexions). Le lien interne suivant explique comment l'utiliser pour les k-means.

Attention : il n'est en revanche pas toujours facile à installer...

K-means avec open-cv (cv2)