5.13. Filtres linéaires et de voisinage

Les opérations de calcul sur les pixels présentées plus tôt dans ce chapitre combinaient deux images point par point. Les filtres effectuent un travail apparenté d’une manière différente : ils calculent la valeur de chaque pixel de sortie à partir d’un petit voisinage de pixels d’entrée entourant la position correspondante. La sortie en (x, y) est une statistique donnée – la moyenne, la médiane, la valeur la plus fréquente – des pixels d’entrée dans une petite boîte centrée sur (x, y).

Ce léger changement de point de vue – passer d’un pixel à la fois à une fenêtre de pixels à la fois – est ce qui fait fonctionner toute une famille d’opérations utiles. Une simple moyenne sur une petite fenêtre lisse le bruit du capteur. La médiane sur la même fenêtre supprime les mouchetures à pixel isolé sans adoucir autant les contours. Une moyenne bilatérale refuse de lisser à travers les frontières de fort contraste, préservant les contours des objets tout en nettoyant les textures à l’intérieur. Le voisinage est l’unité de travail ; le choix de la statistique décide de ce que fait le filtre.

5.13.1. La taille du noyau

Chaque filtre de voisinage prend un paramètre size qui définit le rayon de la fenêtre en pixels. La fenêtre elle-même est carrée et couvre (2 * size + 1) pixels de côté – ainsi size=1 désigne un voisinage de 3 sur 3, size=2 un voisinage de 5 sur 5, size=3 un voisinage de 7 sur 7, et ainsi de suite.

Une petite grille d'image avec un sous-quadrillage de 3 sur 3 mis en évidence représentant le voisinage du filtre. Une flèche montre le voisinage se déplaçant d'un pixel vers la droite. Une seconde flèche le montre descendant à la ligne suivante en fin de ligne. Le pixel de sortie pour chaque position est dessiné sous le voisinage, avec une petite note indiquant que la sortie est une statistique du voisinage d'entrée.

Le voisinage glisse à travers l’image un pixel à la fois, du coin supérieur gauche au coin inférieur droit. Chaque pixel de sortie est le résultat de l’application de la statistique du filtre au voisinage d’entrée centré sur lui.

Des tailles plus grandes signifient des voisinages plus grands, donc un filtrage plus lisse (ou plus agressif). Le coût croît avec l’aire de la fenêtre, de sorte qu’un filtre size=3 effectue environ neuf fois plus de travail par pixel qu’un filtre size=1. La valeur par défaut pratique pour la plupart des travaux de nettoyage est size=1 ou size=2 ; n’optez pour des tailles plus grandes que lorsque les petits voisinages ne suffisent pas à supprimer la caractéristique que l’application cherche à supprimer.

5.13.2. Le filtre de moyenne

mean() remplace chaque pixel par la moyenne arithmétique de son voisinage. Le résultat lisse la variation pixel à pixel sur la taille de la fenêtre, ce qui en fait le moyen le moins coûteux de supprimer les mouchetures de bruit du capteur : la variation haute fréquence se moyenne, le contenu basse fréquence subsiste.

Le compromis est que les contours et autres caractéristiques nettes sont eux aussi moyennés. Un contour clair large d’un pixel avant le filtre devient large de deux ou trois pixels après un filtre de moyenne size=1, avec une luminosité atténuée en pente sur les épaulements. Pour une pure réduction de bruit sur une image pauvre en texture (un mur propre, l’intérieur d’un marqueur coloré), le compromis est acceptable. Pour une scène chargée où les contours comptent, l’un des filtres suivants convient généralement mieux.

img.mean(1)        # 3x3 box average -- fast, gentle smoothing
img.mean(2)        # 5x5 box average -- stronger, slower

5.13.3. Médiane, mode, point médian

Les trois autres filtres statistiques de voisinage troquent la simple moyenne arithmétique contre quelque chose de plus robuste face aux valeurs aberrantes.

median() renvoie la médiane du voisinage – la valeur qui se retrouve au milieu de la liste triée des pixels de la fenêtre. Un unique pixel très clair ou très sombre dans la fenêtre ne tire pas la médiane ; il devient simplement l’une des valeurs extrêmes écartées. L’effet pratique est que le filtrage médian supprime les mouchetures à pixel isolé et le bruit poivre et sel sans adoucir les contours comme le fait mean. Le coût est davantage de calcul par pixel – trier une fenêtre est plus lent que la moyenner – et le résultat n’est pas strictement une moyenne, ce qui importe parfois pour les calculs en aval.

Un paramètre percentile (par défaut 0.5) déplace la valeur choisie hors de la médiane stricte. percentile=0.0 renvoie le minimum du voisinage, percentile=1.0 le maximum ; les valeurs intermédiaires sélectionnent proportionnellement entre eux dans la fenêtre triée. Cela donne à median la capacité de mettre l’accent sur les parties sombres ou claires du voisinage sans perdre la robustesse aux valeurs aberrantes de la statistique d’ordre.

mode() renvoie la valeur la plus fréquente du voisinage. Utile lorsque le modèle de bruit est « la plupart des pixels sont corrects, quelques-uns ont été corrompus à des degrés divers », où la bonne réponse est la valeur qui apparaît le plus souvent – ce que la médiane peut manquer lorsque les valeurs corrompues s’accumulent d’un côté de la fenêtre triée.

midpoint() renvoie une combinaison pondérée du minimum et du maximum du voisinage – bias=0.5 donne le point médian entre eux, bias=0.0 donne le minimum, bias=1.0 donne le maximum. Moins couramment utilisé que les autres, mais bon à connaître lorsque l’objectif est spécifiquement d’extraire des caractéristiques sombres ou claires.

5.13.4. Bilatéral, la version préservant les contours

bilateral() est le filtre de voisinage qu’il vaut le plus la peine de bien comprendre. Il produit l’effet de lissage de mean(), mais avec une contrainte supplémentaire : plus un pixel du voisinage diffère du pixel central, moins il compte dans la moyenne. Le résultat lisse l’intérieur de chaque région uniforme sans déborder à travers les contours qui les séparent, ce qui est exactement ce que la plupart des applications veulent en réalité.

Deux paramètres contrôlent l’agressivité avec laquelle le filtre déprécie les pixels :

  • color_sigma décide de la façon dont la différence de couleur affecte la pondération. Des valeurs plus petites rendent le filtre plus strict pour déprécier les pixels qui diffèrent du centre.

  • space_sigma décide de la façon dont la distance spatiale affecte la pondération. Des valeurs plus petites accordent plus de poids aux pixels proches du centre.

Les valeurs par défaut (color_sigma=0.1, space_sigma=1.0) constituent des points de départ raisonnables ; les ajuster revient généralement à exécuter le filtre sur une trame d’exemple et à régler jusqu’à ce que les contours soient nets et les intérieurs propres.

Le filtre bilatéral est plus coûteux que median() et nettement plus coûteux que mean(), il vaut donc la peine d’y recourir uniquement lorsque le comportement préservant les contours est ce dont l’application a besoin.

5.13.5. Seuillage adaptatif

Les filtres de moyenne, médiane, mode et point médian portent tous la même paire d’arguments nommés qui transforment leur sortie en un seuil binaire :

  • threshold=True fait basculer le filtre en mode seuillage.

  • offset=N décale le seuil local de N unités avant la comparaison.

Le mécanisme s’appuie directement sur le comportement ordinaire du filtre. Sans threshold=True, le filtre calcule sa statistique sur le voisinage et écrit cette statistique dans le pixel de sortie. Avec threshold=True, le filtre calcule la même statistique, puis compare le pixel source à la même position à la statistique augmentée de l’offset, et écrit la valeur maximale du format si la source est supérieure, zéro sinon.

Le résultat est une image binaire dont le seuil évolue avec la luminosité locale à travers la trame. Les régions claires obtiennent un seuil élevé, les régions sombres un seuil bas, et un pixel de premier plan localement plus clair que ses voisins correspond qu’il se trouve dans une région claire ou sombre – ce qui est exactement le comportement qu’un seuil global unique ne pourrait pas produire sur une image éclairée de façon inégale.

img.mean(3, threshold=True, offset=5)

Le paramètre offset est l’endroit où l’application contrôle la sévérité du test. Un petit offset positif exige que le pixel source soit sensiblement plus clair que ses voisins avant de compter comme une correspondance, ce qui supprime les faux positifs dus au bruit du capteur au prix de l’abandon du premier plan faible. Un petit offset négatif capte le premier plan faible au prix de laisser passer un peu de bruit. Le choix dépend de ce que le reste du pipeline va faire de la sortie binaire.

Trois panneaux d'image alignés. Le premier est une trame d'entrée en niveaux de gris avec un dégradé de luminosité et des marques de premier plan disséminées à une obscurité uniforme. Le deuxième panneau montre un seuil global qui lui est appliqué : le premier plan est correctement classé du côté clair, mais tout le côté sombre est lu comme premier plan car la page et le premier plan tombent tous deux sous le seuil. Le troisième panneau montre un seuil adaptatif appliqué à la même entrée : le premier plan est correctement classé sur toute la trame.

Sous un éclairage inégal, un seuil global unique ne peut pas décrire le premier plan à chaque position. Un filtre de voisinage exécuté avec threshold=True produit un seuil qui évolue avec la luminosité locale et classe correctement le premier plan sur toute la trame.

La famille de filtres exécute le seuil adaptatif, le choix du bon filtre importe donc : mean() pour le seuil adaptatif le moins coûteux, median() lorsque l’entrée comporte du bruit poivre et sel que le filtre doit rejeter avant de calculer le seuil local.