Imaging Lingo

Faire du flou avec imaging Lingo (1/2)

par Sébastien Portebois

Nous allons voir ici le principe de la création d'une image floue et une adaptation de ce principe en imaging Lingo, en mettant en avant les pièges de l'implémentation classique et une solution qui prend en compte les spécificités du Lingo.

 

1 - Qu'est ce qu'un flou?

Je suppose que tout le monde sera d'accord pour dire que faire un flou revient à faire la moyenne d'une image. Pour être plus précis nous dirons qu'une image floue est une copie d'une image où chaque pixel a pris la valeur moyenne de son proche voisinage.

Plus sérieusement, regardons la théorie de traitement d'image sous-jacente. Représentons nous une image comme un signal bi-dimensionnel. En réalité c'est bien de ça qu'il s'agit, une image est composée de deux signaux : le 'premier' étant horizontal et le 'second' vertical. (remarque : souvenez vous de cette définition d'une image pour comprendre pourquoi les compressions basées sur des algorithmes de type jpeg sont corrosifs avec les contours des images : un contour représente une variation importante de niveau de signal (dans le domaine temporel) , donc les harmoniques les plus hautes dans le domaine fréquentiel, qui correspondent aux fréquences les plus filtrées lors de la compression). Flouter une image consiste donc à calculer chaque pixel d'une nouvelle image, en regardant les valeurs des pixels correspondants dans l'image originale et en attribuant la valeur du nouveau pixel à la valeur moyenne de ce groupe de pixels originaux.

Cette opération est basée sur une matrice de convolution. Ne partez pas, nous ne nous intéresserons pas ici aux mathématiques entrant en jeu, retenons simplement qu'il s'agit d'une matrice nxn contenant des coefficients. Le centre de cette matrice nxn est positionné sur le pixel à floutter, puis nous lisons la valeur de chaque pixel adjacent (chaque case i,j de la matrice nxn) que nous pondérons par le coefficient contenu dans cette case i,j de la matrice. La nouvelle valeur du pixel central est simplement déterminée en additionnant toutes ces valeurs et en divisant ce total par le total des coefficients. Cela peut vous paraître encore confus, mais cette petite démo va vous montrer ce calcul étape par étape pour un pixel, normalement à la fin vous devriez avoir saisi... sinon rendez vous au début de ce paragraphe pour une relecture ;¬)


figure 1.) calcul de la valeur du pixel à partir de son voisinage

Le flou le plus simple est basé sur une matrice 3x3, avec tous ses coefficients égaux à 1 (le total des coefficients est donc 9). Ainsi la valeur du nouveau pixel est simplement leTotalDesPixels/9. Les même techniques sont utilisées pour faire de la détection de contour, simplement en modifiant les coefficients.
Et comment faire un flou plus flou? La première solution qui vient à l'esprit est d'agrandir la taille de la matrice de convolution à 5x5, puis 9x9 et ainsi de suite. La solution généralement utilisée est la conservation d'une matrice de petite taille (3x3 ou 5x5), et de l'appliquer plusieurs fois à l'image. Pour avoir une image très floutée vous commencez donc par un flou simple ; sur l'image légèremment floue obtenue vous appliquez à nouveau un flou, puis encore un, et ainsi de suite jusqu'à obtenir le flou souhaité.


figure 2.) Définir un flou simple dans Photoshop,
puis l'appliquer trois fois pour obtenir une image très floue


2 - On traduit ça en imaging Lingo et en avant...

A partir de là, la première implémentation en Lingo qui vient à l'esprit s'appuiera sur des multitudes d'appels à getPixel - jusqu'à 9 fois pour chaque pixel -, et sur des appels à setPixel à raison d'un par pixel recalculé. Vous obtiendrez une belle image de flou, mais étant passé du côté obscur de l'imaging Lingo vous avez le temps d'aller boire un verre et de revenir pour voir s'achever le calcul de l'image. set/getPixel sont des commandes trop lentes pour pouvoir être utilisées sur l'intégralité d'une image sans obtenir des temps de calculs (très) longs.

Si vous voulez essayer cette approche académique je vous conseille vivement de regarder les démos de Charles Forman sur setpixel.com (en anglais) : parmi de nombreuses démos très bien faites vous trouverez une démo sur le flou gaussien basée sur cette technique. Les résultats sont très bons, mais le temps de calcul n'est pas négligeable.

Alors pourquoi ai-je appelé cette approche le côté obscur de l'imaging Lingo? Tout simplement a.) pour attiser votre curiosité pour que vous teniez jusqu'au bout de l'article et b.) simplement parce que set/getPixel sont les commandes les plus simples à utiliser, mais se limiter à elles seules revient à considérer l'imaging Lingo comme un coffre aux trésors. En vous limitant à set/getPixel vous regardez le coffre et vous dites : "c'est un très beau coffre à trésors", mais vous n'ouvres jamais le coffre pour accéder au potentiel énorme de copyPixels (ici nous nous limiterons simplement à l'utilisation basique de son paramètre #blendLevel, mais vous si vous approfondissez pour découvrir les richesses des quads et des encres vous deviendrez beaucoup plus puissant qu'avec les seules méthodes set/getPixel).

Et comment faire le flou plus rapidement?

set/getPixel sont des méthodes lentes à cause (mais ce n'est que ma représentation mentale de leur fonctionnement, libre à vous d'avoir une autre interprétation) d'incessants aller-retour entre la machine virtuelle Lingo (qui exécute votre code Lingo traduit en C) et vos instructions Lingo. Au plus vous devez effectuer d'opérations Lingo pour chaque image, au plus vous effectuez d'allers-retours. Or comme nous l'avons vu se baser sur set/getPixel requiert de faire 9 getPixel + 1 setPixel par pixel, soit 144 000 manipulations d'image pour une seule passe sur une image carrée de 120 pixels de côté. Et nous avons vu que pour avoir un flou nous devions faire plusieurs passes....

Pour trouver une solution plus rapide nous allons essayer d'appliquer le flou non pas pixel par pixel mais sur l'ensemble de l'image d'un coup.


3 - Se baser sur les spécificités de l'imaging Lingo pour accélérer le flou

Le paramètre blendLevel de copyPixels nous permet de faire une copie semi-transparente d'une portion d'image vers une autres image (un buffer par exemple). L'idée sur laquelle nous allons nous baser est de généraliser le traitement fait pixel par pixel à toute l'image à la fois. Ainsi nous ne devons plus faire la moyenne des pixels environnants pour calculer un nouveau pixel en limitant notre champ de vue à une simple zone nxn, mais en l'étendant à toute l'image. Nous remplaçons donc la multiplication par un facteur - divisé par le total des facteurs - à la valeur du pixel, mais à l'opacité d'une copie décalée de l'image originale. En effet la matrice nxn utilisée reste la même partout dans l'image, donc le facteur (i,j) de la matrice nxn lue pour le pixel (M,N) sera exactement la même que pour le pixel (M', N'), et en général pour n'importe quel autre pixel de l'image. La valeur maximum du blendLevel est de 255, ici pour un flou uniforme nous avons un facteur de 1, et un total de 9. J'utiliserai une valeur de 1.7 * 255 / 9 en paramètre #blendLevel pour les 9 copyPixels qui composeront une passe de mon flou. La valeur 1.7 est un facteur de correction déterminé empiriquement, pouvant aller de 1 à 4 environ, suivant la quantité de flou que vous appliquez. Ce facteur sert à corriger la perte de luminosité et de contraste générée par cette approche.

Pour résumer les étapes pour faire un flou sont donc :

1 - Créer un buffer...
...d'une taille plus grande que l'image originale... mais j'y reviens dans un instant,
2 - Blend+Offset-copyPixels de l'image originale vers le buffer.
Nous venons de voir la détermination du blendLevel, occupons nous des décalages (offsets) : la matrice de convolution étant constante sur l'image, il nous suffit de faire des copies décalées de l'image originale pour appliquer l'influence de chaque pixel sur ses voisins. Pour une matrice 3x3, les décalages utilisés seront donc [-1,-1], [-1,0], [-1,1], [0,-1], [0,1], [1,-1], [1,0] et [1,1] pour les images floues, et [0,0] pour la composante originale.
3 - {optionel} Rogner le résultat
En 1.) J'ai dit que pour utiliser simplement cette méthode à base de copyPixels j'utilise un buffer plus grand que l'image originale. Bien que je trouve ce résultat très pratique, vous pouvez désirer de rogner l'image floue pour obtenir un résultat de la même taille que l'image originale.

Parlons de la taille de l'image. Comme j'applique une matrice 3x3, je rajoute une ligne de 1 pixel en haut, une en bas, une colonne à droite et une dernière à gauche. Ceci nous permet de ne pas nous soucier des effets de bord (avec une matrice de convolution 3x3 appliquée au niveau des pixels, lors du traitement d'un pixel en bordure de l'image, quelle valeur prenez vous pour les pixels manquants?), et libre à nous de rogner le résultat pour obtenir un résultat plus conventionnel.

Il reste un dernier point à détailler. Bien que j'ai écrit les décalages d'une manière aisée à comprendre, il faut se souvenir que les coordonnées d'une image commencent à (0,0), et que donc l'image originale (après ajout des lignes et colonnes) a subit un décalage de (1,1), pour que le centre de l'image de résultat soit aussi le centre de l'image originale. Les décalages utilisables deviennent donc :

  NO     N      NE     E     SE      S      SO     O    centre
[0,0], [1,0], [2,0], [2,1], [2,2], [1,2], [0,2], [0,1], [1, 1]

Et c'est tout. nous avons fini de voir le principe, nous pouvons enfin écrire du code!

-- flou sur une image
monImg = pOriginalMember.image.duplicate()
imgW =
monImg.width
imgH =
monImg.height
buffer1 =
monImg.duplicate()

--            NO      SE       NE      SO      O        E        N        S   centre
offsetL = [[0,0], [2, 2], [0, 2], [2, 0], [0, 1], [2, 1], [1, 0], [1, 2], [1,1]]
myBlend = 1.7*255/offsetL.count

-- optionnel : boucle pour effectuer plusieurs passes
-- repeat with i = 1 to nBlurLevel
buffer2 = image( imgW+2*i, imgH+2*i, 32 )
myRect = rect( 0, 0, buffer1.width, buffer1.height )
repeat with j = 1 to 9 -- = offsetL.count
  destRect = myRect.offset(offsetL[j][1], offsetL[j][2])
  buffer2.copyPixels(buffer1, destRect, myRect, [#blendLevel : myBlend])
end repeat


-- mise à jour du buffer de travail
if bCrop then
  -- on ne rogne pas
  buffer1 = buffer2.duplicate()
else
  -- on rogne le résultat
  buffer1 = buffer2.duplicate().crop(myRect.offset(1, 1))
end if
-- fin de la boucle optionnelle
-- end repeat


-- on applique le résultat
pMember.image = buffer1


La partie la plus délicate à saisir est peut-être la déclaration et l'utilisation des décalages via la liste offsetL. Il s'agit simplement d'une liste linéaire des décalages à appliquer sur le rectangle de destination du copyPixels. Cela nous épargne la répétition (9 fois) des deux lignes qui définissent le rectangle de destination de la copie destRect, et la copie en elle même avec buffer2.copyPixels(...). Si vous avez encore du mal à saisir ces lignes j'ai laissé la version longue (redondante mais plus conviviale) en commentaire dans les sources, dans le gestionnaire Blur du script d'animation "3x3 blur". Tout y est, il vous suffit de supprimer les deux tirets de la section '3-bis' et vous y êtes!



figure 3.) Deux implémentations du gestionnaire de flou 3x3


Les sources (director 8) sont disponibles ici.

Vous pouvez m'envoyer vos commentaires à mailto:jesus@oeilpouroeil.fr, ou en faire profiter tout le monde en réagissant sur la piste : <Pistes-L>.


Dans la suite nous approfondirons et généraliserons ce principe. Le cas vu ici était plutôt simple - une fois l'idée de base assimilée - parce que tous les coefficients étaient égaux à 1. Nous modifierons légèrement le gestionnaire Blur pour créer - entre autres - du flou gaussien.

 


Director Hors Piste - http://www.director-fr.com - Tous droits réservés. - (c) 2001 Director Hors Piste - Macromedia Director est une marque déposée de Macromedia - Contactez-nous!