5.16. Noyaux de convolution personnalisés¶
Les filtres de voisinage abordés jusqu’ici disposaient chacun d’une statistique intégrée que le filtre appliquait à la fenêtre en chaque position – la moyenne, la moyenne pondérée gaussienne, la médiane. morph() est le seul filtre qui permet à l’application de fournir elle-même la statistique, sous la forme d’un noyau : une petite matrice de poids qui décrit comment le filtre doit combiner les pixels du voisinage en une unique valeur de sortie.
Le mécanisme repose sur l’opération classique de convolution. En chaque position de sortie, chaque pixel du voisinage est multiplié par le poids correspondant dans le noyau, les produits sont additionnés, le résultat est éventuellement mis à l’échelle et décalé, puis la valeur est écrite dans le pixel de sortie. Des noyaux différents produisent des résultats différents à partir d’une même entrée. Un noyau dont tous les poids sont positifs et égaux reproduit le filtre mean() ; un noyau en forme de cloche reproduit gaussian(). Les motifs au-delà de ceux-ci produisent des réponses de contours, des reliefs, des gradients, de l’accentuation, du flou de mouvement, et tout un long catalogue d’autres effets – tout ce que le traitement d’image classique a toujours voulu réaliser en une seule passe linéaire.
5.16.1. La méthode morph¶
La signature ressemble à celle des autres filtres de voisinage, avec un argument supplémentaire :
img.morph(size, kernel, mul=1.0, add=0.0)
size désigne le rayon comme partout ailleurs, de sorte que le noyau doit comporter exactement (2 * size + 1) lignes par (2 * size + 1) colonnes. Le noyau lui-même est une liste Python plate de ce nombre de valeurs, dans l’ordre par lignes (row-major) – les (2 * size + 1) premières entrées constituent la ligne du haut, les (2 * size + 1) suivantes la deuxième ligne, et ainsi de suite jusqu’à la ligne du bas. mul met à l’échelle la somme des produits avant son écriture dans le pixel de sortie, et add ajoute une constante. Les valeurs par défaut mul=1.0 et add=0.0 laissent la sortie de la convolution inchangée.
Un détail qui mérite d’être explicité : la méthode divise automatiquement la somme des produits par la somme des entrées du noyau avant d’écrire la sortie. Cette division automatique signifie qu’un noyau de moyennage dont les entrées totalisent neuf – un flou de boîte 3 par 3, par exemple – ressort à l’échelle d’un neuvième sans effort supplémentaire, et qu’un noyau d’approximation gaussienne totalisant seize ressort à l’échelle d’un seizième, le tout sans que l’application ait à calculer elle-même la division. L’application ne définit mul que lorsqu’elle souhaite une mise à l’échelle supplémentaire par-dessus la normalisation automatique – ou, plus couramment, lorsque le noyau totalise zéro (un noyau de réponse de contours) et que la division automatique serait une division par rien. Le framework considère alors la somme comme valant un, et mul devient le seul réglage pour maintenir la somme des produits non mise à l’échelle dans la plage.
Le couple threshold=True / offset=N de la section sur le seuillage adaptatif fonctionne également avec morph(), de sorte que le même framework de noyaux personnalisés peut produire un seuil binaire dont le seuil de coupure est calculé par une statistique personnalisée.
5.16.2. La disposition du noyau¶
Un noyau 3 par 3 (size=1) est une liste plate de neuf valeurs disposées de gauche à droite et de haut en bas. La convention se lit naturellement si la liste est répartie sur trois lignes Python :
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Il s’agit de l’opérateur de gradient Sobel-x – le premier noyau standard que toute application va vouloir et qu’il est utile de parcourir de bout en bout. Le motif est simple : poids négatifs sur la colonne de gauche, poids positifs sur la colonne de droite, la colonne centrale étant nulle. Les poids de ligne -1, -2, -1 (ou 1, 2, 1 à droite) sont plus élevés au milieu qu’aux coins, ce qui donne à la ligne centrale plus d’influence sur le résultat qu’aux lignes des coins.
Lorsque le noyau balaie un contour vertical – une colonne de pixels passant du sombre à gauche au clair à droite – les poids négatifs captent le côté sombre et les poids positifs captent le côté clair. La somme des produits est un grand nombre positif, que le filtre écrit sous la forme d’un pixel de sortie clair. Une zone horizontale de luminosité uniforme produit zéro, car chaque poids positif est compensé par un poids négatif de même amplitude sur un pixel de même valeur.
Exécution du noyau :
img.morph(1, sobel_x, mul=0.25)
Le noyau de Sobel totalise zéro – chaque poids négatif du côté gauche est compensé par un poids positif égal à droite – de sorte que la division automatique ne divise par rien, et mul est la seule mise à l’échelle de la somme des produits. mul=0.25 maintient la réponse dans la plage : la plus grande somme absolue que Sobel-x peut produire à partir d’une zone 3 par 3 est d’environ 4 * 255 = 1020 (huit pixels clairs pondérés jusqu’à 2), et la diviser par quatre ramène les cas extrêmes à 255, où le format les écrête proprement.
Le noyau Sobel-y correspondant détecte les contours horizontaux en faisant pivoter le même motif de poids de 90 degrés :
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Les applications qui veulent détecter n’importe quel contour, quelle que soit sa direction, exécutent généralement les deux Sobel et combinent les réponses.
5.16.3. Décalage de la sortie¶
add est l’autre moitié de l’histoire de la mise à l’échelle. La réponse d’un noyau à somme nulle est signée – positive d’un côté d’un contour, négative de l’autre – et la moitié négative est écrêtée à zéro lors de l’écriture dans un pixel non signé. add=128 décale la réponse pour la centrer sur le gris moyen, de sorte que les réponses négatives survivent sous forme de valeurs inférieures à 128 et les positives au-dessus : une réponse de contour ou un relief devient visible dans les deux directions, au prix de la moitié de la plage dans chacune.
La combinaison de mul et add qu’un noyau attend fait partie de la conception du noyau ; le catalogue de noyaux standard indique les bons réglages pour chaque noyau courant.
5.16.4. Noyaux plus grands¶
Tout ce qui figure sur cette page a été décrit avec des noyaux 3 par 3 (size=1), car c’est la taille utilisée par le catalogue standard et parce que la disposition par lignes est facile à écrire à la main à cette taille. Rien dans le mécanisme ne restreint cependant le noyau au 3 par 3. size=2 exécute un noyau 5 par 5, avec vingt-cinq entrées dans la liste plate ; size=3 exécute un 7 par 7 avec quarante-neuf entrées ; et ainsi de suite, jusqu’à n’importe quel rayon que l’application est prête à payer. Le framework gère aussi bien les dispositions en liste plate qu’en lignes imbriquées, pour n’importe quelle taille impaire.
La raison de recourir à un noyau plus grand est la même que celle de recourir à un voisinage plus grand pour n’importe lequel des filtres intégrés : plus de moyennage, une détection de caractéristiques plus large, une moindre sensibilité au bruit isolé d’un pixel. Le coût croît comme le carré du rayon – un 5 par 5 effectue environ 2,8 fois le travail par pixel d’un 3 par 3, un 7 par 7 environ 5,4 fois – et ce multiplicateur se répercute directement sur la fréquence d’images.
Le schéma pratique consiste à rester à size=1 pour le catalogue standard et à recourir à des tailles plus grandes uniquement lorsque l’algorithme a besoin du voisinage plus large. Les détecteurs de contours profitent rarement d’un dépassement du 3 par 3 ; les filtres de lissage le font parfois ; la bonne taille dépend de l’échelle des caractéristiques que l’application cherche à mettre en valeur ou à supprimer.
5.16.5. Quand recourir à morph¶
Pour le lissage courant, mean(), gaussian() et bilateral() sont plus rapides et plus propres. Pour la détection de contours, laplacian() et find_edges() sont conçus à cet effet. L’intérêt de recourir directement à morph() se présente lorsque l’application a besoin d’une convolution spécifique que les filtres intégrés n’exposent pas – un Sobel directionnel, un modèle de contour personnalisé, un noyau réglé pour une texture particulière que le reste du pipeline va rechercher, ou l’un quelconque des noyaux utiles du catalogue standard que le traitement d’image classique a accumulés au fil des décennies. Toute la souplesse des noyaux arbitraires est disponible ; le prix à payer est que l’application est responsable du choix des valeurs du noyau qui produisent le résultat souhaité.