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
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!