5.17. Un catalogue de noyaux standard

Le traitement d’image classique a accumulé un catalogue conséquent de motifs de pondération de noyaux qui reviennent sans cesse – détecteurs de contours, accentuateurs de netteté, gaufrages, lisseurs, flous de mouvement – et chacun d’eux s’exécute via morph(). Chacun est concis, chacun fait une seule chose, et la plupart sont simples à lire une fois que la logique de base des pondérations est comprise.

Les noyaux ci-dessous sont tous des matrices 3 par 3 sauf mention contraire, ils utilisent donc tous size=1 dans l’appel. La structure de pondération de chaque noyau est décrite à côté de celui-ci, car c’est la lecture des pondérations qui développe l’intuition expliquant pourquoi un noyau gaufre et un autre accentue la netteté.

5.17.1. Le noyau identité

Le noyau le plus simple possible est l”identité – un au centre, zéro partout ailleurs :

identity = [0, 0, 0,
            0, 1, 0,
            0, 0, 0]

img.morph(1, identity)

Chaque pixel de sortie prend sa valeur au centre du voisinage, c’est-à-dire le pixel d’entrée situé à la même position. L’image passe inchangée. L’identité n’a aucune utilité pratique en tant que filtre, mais elle constitue la référence utile pour comprendre tous les autres noyaux : tout noyau autre que l’identité est l’identité augmentée d’une certaine modification.

Un noyau dont la pondération centrale est élevée avec de petites pondérations négatives autour soustrait l’entourage du centre. Un noyau avec une pondération centrale nulle ignore le pixel lui-même et ne répond qu’aux différences entre ses voisins. Lire un noyau de cette manière – ce que la pondération centrale fait au pixel, ce que les pondérations environnantes ajoutent ou retranchent – est le moyen le plus rapide de prédire son effet.

5.17.2. Détection de contours

Les noyaux de détection de contours répondent fortement aux positions où la luminosité change rapidement dans une direction donnée, et produisent une sortie proche de zéro là où la luminosité est uniforme. Ils forment la famille dont les pondérations s’annulent : une zone plate (tous les pixels de même valeur) produit une sortie nulle, car chaque pondération positive est exactement compensée par une pondération négative de magnitude égale.

Sobel-x en est l’exemple canonique. Il détecte les contours verticaux (transitions de luminosité gauche/droite) :

sobel_x = [-1,  0,  1,
           -2,  0,  2,
           -1,  0,  1]

img.morph(1, sobel_x, mul=0.25, add=128)

Le Sobel-y correspondant est le même motif pivoté de 90 degrés ; il détecte les contours horizontaux (transitions de luminosité haut/bas) :

sobel_y = [-1, -2, -1,
            0,  0,  0,
            1,  2,  1]

La ligne centrale de Sobel-x comporte les pondérations -2 et 2 plutôt que -1 et 1. La pondération supplémentaire sur la ligne centrale confère au noyau un léger lissage intégré dans la direction le long du contour, ce qui le rend plus robuste au bruit que l’opérateur Prewitt, plus simple, qui supprime ces magnitudes supplémentaires :

prewitt_x = [-1, 0, 1,
             -1, 0, 1,
             -1, 0, 1]

prewitt_y = [-1, -1, -1,
              0,  0,  0,
              1,  1,  1]

Prewitt pondère chaque ligne de manière égale, sa réponse est donc un peu plus nette que celle de Sobel, au prix d’une plus grande sensibilité au bruit ponctuel à l’échelle d’un pixel (le coût d’exécution du noyau est identique – la convolution effectue le même travail quelles que soient les pondérations). Sur une image propre aux contours marqués, c’est un substitut parfaitement utilisable de Sobel.

Scharr va dans l’autre direction. Ses pondérations sont plus grandes et calibrées pour une détection précise de la direction des contours à des angles plus fins :

scharr_x = [-3,   0,  3,
            -10,  0, 10,
            -3,   0,  3]

img.morph(1, scharr_x, mul=0.0625, add=128)

Le diviseur mul=0.0625 (1/16) ramène la sortie dans la plage 0255 après la somme de produits plus importante. Scharr est la bonne réponse lorsque l’application requiert la réponse de gradient géométriquement la plus fidèle et accepte de payer un peu plus d’arithmétique pour cela.

5.17.3. Le laplacien

Un noyau laplacien répond aux contours dans toutes les directions à la fois. Là où chaque Sobel détecte les changements de luminosité le long d’un axe, le motif de pondération symétrique du laplacien répond de la même façon quelle que soit la direction du contour :

laplacian_4 = [ 0, -1,  0,
               -1,  4, -1,
                0, -1,  0]

img.morph(1, laplacian_4, add=128)

La structure : pondération centrale 4, quatre voisins horizontaux/verticaux pondérés à -1, les quatre diagonales pondérées à zéro. Le noyau s’annule, les zones plates produisent donc une sortie nulle. Là où la luminosité change, la valeur centrale diffère de la moyenne de ses quatre voisins cardinaux, et la sortie correspond à l’ampleur de cette différence.

La variante en 8-connexité inclut les voisins diagonaux :

laplacian_8 = [-1, -1, -1,
               -1,  8, -1,
               -1, -1, -1]

Chaque noyau détecte des choses légèrement différentes. La version en 4-connexité produit une sortie plus nette sur les contours horizontaux et verticaux ; celle en 8-connexité est plus isotrope – elle répond aussi bien dans toutes les directions – mais produit une sortie un peu plus bruitée. Le noyau en 8-connexité circule aussi sous le nom d”outline, du fait de son usage pour visualiser les contours.

5.17.4. Accentuation de la netteté

Un noyau d”accentuation de la netteté est l’identité augmentée d’un noyau de réponse aux contours. La sortie est l’image originale plus une copie des contours, de sorte que les caractéristiques haute fréquence sont amplifiées par rapport aux intérieurs lisses.

Le noyau d’accentuation standard en 4-connexité ajoute le laplacien en 4-connexité à l’identité :

sharpen = [ 0, -1,  0,
           -1,  5, -1,
            0, -1,  0]

img.morph(1, sharpen)

Lecture du noyau : la pondération centrale vaut identity (1) + Laplacian centre (4) = 5, et l’entourage correspond à celui du laplacien. Les zones plates produisent 5 * 1 - 4 * 1 = 1 fois la valeur centrale – l’identité. Les contours produisent l’original plus la réponse du laplacien. La somme des pondérations vaut 1, donc mul et add restent à leurs valeurs par défaut.

Pour une accentuation plus forte, la variante en 8-connexité va plus loin :

sharpen_strong = [-1, -1, -1,
                  -1,  9, -1,
                  -1, -1, -1]

img.morph(1, sharpen_strong)

La pondération centrale 9 vaut identity (1) + Laplacian-8 centre (8). Même logique, plus d’amplification, plus de risque d’amplifier aussi le bruit du capteur.

Les noyaux d’accentuation forte sont essentiellement gaussian() avec unsharp=True, simplement exprimés directement sous forme de noyau plutôt qu’au travers de l’option de masque flou. Le comportement au niveau du pixel est le même ; le choix se fait entre la commodité de la méthode nommée et le contrôle fin d’un noyau réglé à la main.

5.17.5. Gaufrage

Un noyau de gaufrage produit l’effet d’éclairage latéral que l’on trouve dans les éditeurs d’images classiques. La sortie donne l’impression que l’image a été extrudée en relief puis éclairée depuis un coin :

emboss = [-2, -1,  0,
          -1,  1,  1,
           0,  1,  2]

img.morph(1, emboss, add=128)

L’astuce réside dans l”asymétrie le long de la diagonale. Le coin supérieur gauche a la pondération la plus négative, le coin inférieur droit la plus positive, et la diagonale d’un coin à l’autre passe du négatif au positif en traversant le un. À chaque pixel, le noyau calcule essentiellement « la luminosité en bas à droite moins la luminosité en haut à gauche », ce qui est positif là où l’image s’éclaircit dans cette direction et négatif là où elle s’assombrit. Ajouter 128 recentre la sortie signée sur le gris moyen afin que l’effet soit visible.

Faire pivoter l’asymétrie le long de l’autre diagonale gaufre depuis la direction opposée :

emboss_alt = [ 0,  1,  2,
              -1,  1,  1,
              -2, -1,  0]

img.morph(1, emboss_alt, add=128)

Les deux directions de gaufrage sont utiles en combinaison – en soustrayant l’une de l’autre, ou en appliquant chacune sur la même image et en comparant les réponses – lorsqu’une application doit détecter l’orientation.

5.17.6. Lissage

Les noyaux de lissage forment la famille dont les pondérations somment à un (et sont toutes non négatives). Une zone plate passée par un tel noyau produit la même luminosité uniforme, car le noyau moyenne les valeurs des pixels entre elles plutôt que d’amplifier leurs différences.

Le plus simple est le flou de boîte (box blur), qui correspond exactement à ce que calcule mean() :

box_blur = [1, 1, 1,
            1, 1, 1,
            1, 1, 1]

img.morph(1, box_blur)

Le noyau somme à 9, de sorte que la division automatique par la somme du noyau transforme la somme de produits en une véritable moyenne sur les neuf pixels du voisinage. En pratique, mean() est le meilleur moyen d’exécuter ce noyau – il produit la même sortie plus rapidement, par un chemin optimisé pour calculer la moyenne et rien d’autre, là où morph met en œuvre la machinerie de convolution générale. Le flou de boîte figure au catalogue parce qu’il constitue la bonne référence pour comprendre tous les autres noyaux de lissage.

Une approximation 3 par 3 de la gaussienne pondère le centre et les voisins cardinaux davantage que les coins :

gaussian = [1, 2, 1,
            2, 4, 2,
            1, 2, 1]

img.morph(1, gaussian)

Les pondérations sont la ligne 1, 2, 1 du triangle de Pascal multipliée en produit extérieur par elle-même. La pondération centrale 4 est la plus grande car le pixel central contribue le plus à sa propre sortie ; les coins valent 1 car ils sont les plus éloignés du centre. Le noyau somme à 16, et la division automatique par la somme du noyau gère la normalisation – aucun argument mul nécessaire. La forme 3 par 3 est une approximation grossière d’une véritable gaussienne et indiscernable de gaussian() à size=1 ; la forme morph est surtout utile lorsqu’une application veut composer le lissage avec une autre opération dans la même passe.

5.17.7. Flou de mouvement

Un noyau de flou de mouvement moyenne les pixels le long d”une seule direction, laissant la direction perpendiculaire non floutée. Le cas le plus simple est horizontal :

motion_h = [0, 0, 0,
            1, 1, 1,
            0, 0, 0]

img.morph(1, motion_h)

La ligne centrale moyenne trois pixels le long de l’axe horizontal ; les lignes supérieure et inférieure sont nulles. Le noyau somme à 3, de sorte que la division automatique par la somme du noyau produit une véritable moyenne de trois pixels sans aucun mul nécessaire. La sortie est une copie de l’entrée étalée horizontalement – l’effet qu’une caméra capture lorsque le sujet se déplace latéralement pendant l’exposition. Le flou de mouvement vertical est le même motif pivoté :

motion_v = [0, 1, 0,
            0, 1, 0,
            0, 1, 0]

Un flou de mouvement diagonal utilise la diagonale principale :

motion_diag = [1, 0, 0,
               0, 1, 0,
               0, 0, 1]

img.morph(1, motion_diag)

Les noyaux de flou de mouvement sont utiles à la fois comme effet (flouter délibérément une trame à des fins visuelles) et comme motif de test pour les algorithmes qui doivent être robustes aux artefacts de mouvement (exécuter l’algorithme sur une entrée floutée par le mouvement et vérifier qu’il produit toujours la bonne réponse).

5.17.8. Lire les noyaux d’un coup d’œil

Quelques règles empiriques rendent les nouveaux noyaux plus faciles à lire à vue :

  • Somme égale à un avec des pondérations non négatives ⇒ lissage (préserve la luminosité moyenne).

  • Somme égale à zéro avec des pondérations à la fois positives et négatives ⇒ réponse aux contours (nulle sur les zones plates).

  • Somme égale à un avec un centre fortement positif et un faible entourage négatif ⇒ accentuation de la netteté (identité plus réponse aux contours).

  • Asymétrie le long d’une diagonale avec somme égale à un ⇒ gaufrage (met en évidence un côté de chaque transition de luminosité).

  • Concentration le long d’un axe avec somme égale à un ⇒ flou directionnel.

La première de ces règles à laquelle le noyau correspond est généralement la bonne supposition quant à son effet. La plupart des noyaux utiles sont reconnaissables à la seule disposition de leur motif de pondération.

Lorsque aucun des noyaux standard ne fait ce que l’application veut, l’étape suivante consiste à en régler un à la main. La combinaison des règles ci-dessus et des contrôles mul / add couvre presque toutes les passes linéaires qu’un pipeline de vision industrielle classique ait jamais souhaitées ; à partir de là, il s’agit d’essayer des pondérations, d’observer la sortie et d’itérer.