Consignes
La note ne valide pas seulement le résultat de votre programme, mais également son style :
-
choix approprié des noms de variables,
-
présence de documentation pour les fonctions importantes,
-
commentaires pertinents: la paraphrase de code, par exemple:
y = x * 2 # y est le double de x
n'apporte rien,
-
découpage des fonctions / procédures complexes en sous-fonctions / sous-procédures lorsque nécessaire...
Vérifiez ces points avant de demander à votre intervenant de valider votre code.
Liens utiles
-
le fichier image.py pour manipuler les images
-
le fichier vector.py pour manipuler des vecteurs
-
le fichier tp4-titi_grosminet.py contenant le squelette du TP
-
le fichier lac.ppm contenant un exemple d'image au format PPM. (images.zip contient d'autres exemples...)
-
utilitaire IrfanView pour visualiser les images
-
email des enseignants : pierre.hyvernat@univ-smb.fr, jacques-olivier.lachaud@univ-smb.fr, tom.hirschowitz@univ-smb.fr, daniel.martins-antunes@univ-smb.fr, florent.lorne@univ-savoie.fr
1. Dégrader une image pour mieux la compresser
Au TP2, nous avons vu que plus les données présentent des répétitions, plus les algorithmes de compression peuvent être efficaces.
Il est parfois intéressant de transformer les données d'une image avant d'utiliser un algorithme de compression. Un exemple connu est par exemple la transformée de Burrows Wheeler. Ces transformations ne modifie pas la taille des données, mais facilite les algorithmes de compressions utilisés ensuite...
Certaines transformations entrainent une perte de qualité : on parle alors de compression de données avec perte. L'objectif est de sacrifier un peu d'information pour gagner beaucoup en compression.
1.1. Compression avec perte
Nous allons nous intéresser à la compression d'images. Pour cela, nous allons réutiliser la bibliothèque image.py du TP1.
1.1.1. Une transformation simple
La manière la plus simple pour augmenter les répétitions dans une image est de considérer les pixels par blocs et de tous leur affecter la même couleur.
Telechargez les fichiers image.py, vector.py, tp4-titi_grosminet.py et lac.ppm. Vous allez completer le fichier tp4-titi_grosminet.py dans la suite du TP...
Complétez la fonction moyenneParBlocs(T, lng)
qui prend en
entrée un tableau T
et un entier lng
. Cette fonction
retourne un tableau de la même longueur que T
dont les
lng
premières valeurs sont toutes égales à la moyenne des
lng
premières valeurs de T
. Ensuite, un deuxième
bloc prend la valeur des lng
valeurs suivantes, et ainsi de
suite.
Exemples:
>>> T = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> moyenneParBlocs(T, 5)
[2, 2, 2, 2, 2, 7, 7, 7, 7, 7]
>>> moyenneParBlocs(T, 4)
[1, 1, 1, 1, 5, 5, 5, 5, 8, 8]
>>> moyenneParBlocs(T, 3)
[1, 1, 1, 4, 4, 4, 7, 7, 7, 9]
Le code de cette fonction ressemble à ceci. Modifiez le pour prendre en
compte le dernier bloc, dont la taille peut être différente de
lng
.
def moyenneParBlocs(T, lng):
"""
Retourne un tableau dont les ``lng`` premières valeurs sont égales à la
moyenne des ``lng`` première valeurs de ``T``, les ``lng`` suivantes sont
égales à la moyenne des ``lng`` valeurs suivantes de ``T`` et ainsi de
suite.
"""
res = []
for i in range(0, len(T), lng):
taille_bloc = lng # modifier pour gérer le dernier bloc
moyenne = sum(T[i:i+lng]) // taille_bloc
res.extend([moyenne]*taille_bloc)
return res
1.1.2. Impact sur la compression d'une image
On peut charger une image à partir d'un fichier de la manière suivante :
>>> im = Image('lac.ppm')
>>> im
Image de dimensions 500 x 353
Une fois l'image chargée, on peut la convertir en une chaine de caractères
avec la méthode im.to_string
.
>>> s = im.to_string()
>>> len(s) # une très très très longue chaine...
1770291
>>> s[:100] # l'image est décrite comme une suite de nombres
'500 353 151 128 126 120 121 127 134 140 140 140 133 126 124 124 121 119 119 119 119 120 119 119 119 '
La procédure (fournie) simplifieImageMoyenne(im, lng)
utilise
votre fonction moyenneParBlocs
pour simplifier les couleurs d'une
image, créant ainsi des blocs de répétitions de taille lng
.
-
Écrivez la procédure suivante :
def testCompresseMoyenne(fichier, lng): """ Charge l'image contenu dans le fichier et - affiche la taille de l'image (sous forme de chaine) - affiche la taille de l'image compressée (sous forme de tableau d'octets) - simplifie l'image en remplaçant les blocs par leurs moyennes - affiche la taille de l'image simplifiée (sous forme de chaine) - affiche la taille de l'image simplifiée compressée (sous forme de tableau d'octets) - sauve l'image simplifiée dans un fichier "...-lng.ppm" """
Les fonctions fournies
compresse
etdecompresse
permettent de compresser/décompresser une chaine de caractères (str
) et produisent un tableau d'octets (bytearray
). Ces fonctions utilisent une variante de l'algorithme LZ78 étudié au TP2.Par exemple sur l'un des fichiers du TP1:
>>> testCompresseMoyenne("vague.ppm", 8) image : 1901226 image compressée : 511636 image après simplification : 1903335 image après simplification compressée : 113996
L'image obtenue ressemble alors à
-
Exécutez le code suivant :
>>> testCompresseMoyenne("lac.ppm", 5) ??? >>> testCompresseMoyenne("lac.ppm", 8) ??? >>> testCompresseMoyenne("lac.ppm", 20) ???
Observez les images obtenues dans les fichiers lac-5.ppm, lac-8.ppm et lac-20.ppm.
-
Que pouvez-vous dire de la taille de l'image une fois compressée en fonction de la taille des blocs ?
-
Que pouvez-vous dire de la qualité visuelle de l'image obtenue en fonction de la taille des blocs ?
Inscrivez vos observations dans votre fichier.
-
2. Changement de représentation
Il est parfois utile de changer la manière de représenter les données, afin de simplifier des calcul ou améliorer l'efficacité de la compression.
Nous allons utiliser un changement de représentation qui correspond à un changement de base dans un espace vectoriel !
2.1. Préliminaires : vecteurs
Parmi les fichiers fournis, le fichier vector.py implémente le type Vector
.
Un Vector
est initialisé avec une liste de nombres.
>>> v = Vector([1,2,3,4])
>>> v
Vector([1, 2, 3, 4])
Les variables de types Vector
peuvent être additionnées,
soustraites ou multipliées par un nombre.
>>> u = Vector([1,1,1])
>>> v = Vector([2,0,-1])
>>> u+v
Vector([3, 1, 0])
>>> u-v
Vector(-[1, 1, 2])
>>> 2*u
Vector([2, 2, 2])
>>> 3*u - v
Vector([1, 3, 4])
Toutes les autres opérations sur les listes (autres que +
et
*
) sont valides sur les vecteurs.
>>> u = Vector([])
>>> u.append(5)
>>> u.append(2)
>>> u.append(21)
>>> u
Vector([5, 2, 21])
>>> for i in range(len(u)):
... print('coordonnée {} : {}'.format(i, u[i]))
...
coordonnée 0 : 5
coordonnée 1 : 2
coordonnée 2 : 21
2.2. Changement de bases
Il est évident que le vecteur (a,b,c) peut s'écrire sous la forme a*(1,0,0) + b*(0,1,0) + c*(0,0,1). Si on appelle e1 le vecteur (1,0,0), e2 le vecteur (0,1,0) et e3 le vecteur (0,0,1), on peut dire que (a,b,c) est égal à a*e1 + b*e2 + c*e3.
Les vecteurs e1, e2 et e3 forment une base. La base e1, e2, e3 est si naturelle qu'on l'appelle la base canonique.
On peut faire exactement la même chose en utilisant d'autres vecteurs que e1, e2, e3. Par exemple, si on choisi comme base les vecteurs (1,1,0), (1,0,1), (0,1,1), alors (a,b,c) désignera le vecteur a*(1,1,0) + b*(1,0,1) + c*(0,1,1).
Écrivez la fonction changeBase
qui prend en argument
-
un vecteur (x1, ..., xk)
-
une base (c'est à dire une liste de vecteurs) v1, ..., vk
et qui calcule le nouveau vecteur x1*v1 + ... + xk * vk.
def changeBase(v, B):
"""
calcule les coordonnées du vecteur ``v`` dans la base ``B``
Note : la taille de ``v`` doit être égale à la taille de ``B`` !
"""
En terme d'algèbre matricielle, la fonction changeBase
n'est rien d'autre que le produit du vecteur v
par la matrice
B
où les lignes de B
seraient les vecteurs de la
base en question.
Dans l'exemple précédent :
[ 1, 1, 0 |
(2, 0, -1) x | 1, 0, 1 | = (2, 1, -1)
| 0, 1, 1 ]
Nous allons maintenant considérer une base différente pour des vecteurs de longueurs arbitraires.
La base escalier
La fonction fournie baseEscalier(n)
retourne n
vecteurs formant la base escalier de dimension n
.
>>> B = baseEscalier(6)
>>> B
[(1, 1, 1, 1, 1, 1),
(0, 1, 1, 1, 1, 1),
(0, 0, 1, 1, 1, 1),
(0, 0, 0, 1, 1, 1),
(0, 0, 0, 0, 1, 1),
(0, 0, 0, 0, 0, 1)]
>>> v = Vector([3, 2, 1, -4, -5, -6])
>>> u = changeBase(v, B)
>>> u
(3, 5, 6, 2, -3, -9)
On peut vérifier que (3,5,6,2,-3,-9)
est bien égal à
3*(1,1,1,1,1,1) + 2*(0,1,1,1,1,1) + 1*(0,0,1,1,1,1) + ...
.
Vérifiez que votre fonction changeBase
produit bien le même
résultat que les exemples précédents.
Dans le langage de l'algèbre linéaire, la matrice "escalier"
[ 1, 1, 1, 1, 1, 1, 1, 1 | | 0, 1, 1, 1, 1, 1, 1, 1 | | 0, 0, 1, 1, 1, 1, 1, 1 | | 0, 0, 0, 1, 1, 1, 1, 1 | | 0, 0, 0, 0, 1, 1, 1, 1 | | 0, 0, 0, 0, 0, 1, 1, 1 | | 0, 0, 0, 0, 0, 0, 1, 1 | | 0, 0, 0, 0, 0, 0, 0, 1 ]
est inversible et a pour inverse la matrice
[ 1, -1, 0, 0, 0, 0, 0, 0 | | 0, 1, -1, 0, 0, 0, 0, 0 | | 0, 0, 1, -1, 0, 0, 0, 0 | | 0, 0, 0, 1, -1, 0, 0, 0 | | 0, 0, 0, 0, 1, -1, 0, 0 | | 0, 0, 0, 0, 0, 1, -1, 0 | | 0, 0, 0, 0, 0, 0, 1, -1 | | 0, 0, 0, 0, 0, 0, 0, 1 ]
-
Écrivez la fonction
baseEscalierInverse
qui calcule la base correspondante. -
Vérifiez que cette base permet effectivement de retrouver un vecteur
v
à partir du résultat dechangeBase(v, baseEscalier(len(v)))
.
Conclusion : un changement de base n'est qu'une manière alternative de représenter la même information.
3. Une base utile
Nous allons maintenant étudier un changement de base appelé Transformée de Fourier Discrète (TFD), très utilisée en pratique.
La fonction fournie baseTFD(n)
produit une base de vecteurs
entiers pour la TFD en dimension n
.
-
Exécutez le code suivante et observez les images obtenues :
>>> B = baseTFD(100) >>> barGraph(B[1]).save('TFD-1.ppm') >>> barGraph(B[51]).save('TFD-51.ppm') >>> barGraph(B[2]).save('TFD-2.ppm') >>> barGraph(B[52]).save('TFD-52.ppm') >>> barGraph(B[3]).save('TFD-3.ppm') >>> barGraph(B[53]).save('TFD-53.ppm')
-
Quelle est la forme générale des vecteurs de la base donnée par
baseTFD
?
La fonction fournie TFD
transforme un vecteur exprimé dans la
base canonique dans le même vecteur exprimé dans la base produite par
baseTFD
.
Autrement dit, elle fait théoriquement la transformation inverse à
changeBase(_, baseTFD)
.
Exécutez le code :
>>> v = Vector((1,2,3,4,5))
>>> u = TFD(v)
>>> B = baseTFD(5)
>>> v2 = changeBase(u, B)
>>> v == v2
False
>>> print(v2)
???
Observez le vecteur v2
, comment le corriger pour avoir v
== v2
?
Écrivez la fonction inverseTFD(u)
qui prend en entré un vecteur
exprimé dans la base TFD
et retourne le vecteur correspondant
dans la base canonique dont toutes les coordonnées sont des entiers.
Testez votre code :
>>> v = randomVector(1000)
>>> u = TFD(v)
>>> v2 = inverseTFD(u)
>>> v == v2
True
Aide : la fonction round
arrondi un flottant à l'entier
le plus proche...
3.1. Des TFD et des images
En exécutant le code suivant, on constate que la représentation après
application de la TFD contient beaucou de petites valeurs, mais jamais de
0
.
>>> im = Image("lac.ppm")
>>> v = im.bvalues() # récupère les intensités de bleu
>>> v = v[2000:2200] # on ne garde que 200 pixels consécutifs
>>> barGraph(v).save('out.ppm') # on visualise ces intensités
>>> u = TFD(v) # on change la représentation
>>> barGraph(u).save('out.ppm') # on observe quelques grandes valeurs et beaucoup de petites
>>> u.count(0) # beaucoup de petites valeurs mais jamais de 0
0
Programmez une fonction filtre
qui remplace les petites valeurs
d'un vecteur par 0
.
Pour ceci, vous devez :
-
créer un tableau auxiliaire contenant les valeurs absolues des valeurs de
v
(la fonctionabs
permet de calculer la valeur absolue d'un nombre) -
trier ce tableau (la méthode
T.sort()
permet de trier un tableauT
) -
repérer la valeur seuil dans ce tableau qui sépare les valeur "hautes" (a conserver) des valeur "basses" (a remplacer par
0
) -
créer le tableau résultat en recopiant les valeurs "hautes" du tableau initial et en remplaçant les valeurs "basses" par
0
.def filtre(v, qualite): """ renvoie une copie du vecteur ``v`` où les plus petites valeurs (en valeur absolue) sont remplacées par 0 Par exemple, - si ``qualite`` est 0.5, les 50% des valeurs les plus petites sont remplacées par 0 - si ``qualite`` est 0.9, les 10% des valeurs les plus petites sont remplacées par 0 - ... """
Par exemple :
>>> v = Vector([27, -27, -6, -5, -16, -2, -56, 17, 5, 74, -9, -43, -19, 73, 8, 6])
>>> filtre(v, 0.5)
Vector([27, -27, 0, 0, 0, 0, -56, 17, 0, 74, 0, -43, -19, 73, 0, 0])
>>> filtre(Vector([0,1,2,3,4,5,6,7,8,9]), 0.7)
Vector([0, 0, 0, 3, 4, 5, 6, 7, 8, 9])
Inscrivez les réponses aux questions suivantes dans votre fichier :
-
Que peut-on dire de la compression du vecteur filtré par rapport à celle du vecteur original ?
-
De manière qualitative, que pouvez-vous dire du graphique à barre du vecteur filtré par rapport à celui du vecteur original ?
3.2. Compression avec perte
Nous allons maintenant utiliser le principe exposé à la question précédent pour faire une compression d'images avec perte.
Problème
Le temps de calcul de la fonction TFD
devient très long
lorsqu'on l'applique à de grands vecteurs. Si on essaie de l'appliquer à
toutes les lignes d'une image (trois fois par ligne car il y a trois canaux de
couleurs : R, G, B), le temps de calcul devient prohibitif.
Écrivez la fonction la fonction encodeTFDParBlocs(v, lng,
qualite)
. Cette fonction reprend les mêmes principes que la fonction
moyenneParBlocs
mais cette fois-ci, chaque bloc de données est
obtenu en appliquant la fonction TFD
sur un bloc, puis en
remplaçant les petites valeurs par des 0 avec la fonction filtre
.
def encodeTFDParBlocs(v, lng, qualite):
"""
Applique TFD sur le vecteur ``v`` par blocs de taille ``lng``. Chaque
bloc résultant est filtré avec la fonction ``filtre``.
"""
Exemples :
>>> v = Vector((1,2,3,4,5,6,7,8,9,10))
>>> v
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> encodeTFDParBlocs(v, 4, 0.25)
(10, 0, 0, 0, 26, 0, 0, 0, 19, 0)
>>> encodeTFDParBlocs(v, 6, 0.6 )
(21, -6, -6, 0, -10, 0, 34, -4, 0, -4)
>>> encodeTFDParBlocs(v, 3, 0.3 )
(6, 0, 0, 15, 0, 0, 24, 0, 0, 10)
Écrivez la fonction decodeTFDParBlocs(u, lng)
qui effectue la
transformation inverse de la fonction encodeTFDParBlocs
en
appelant la fonction inverseTFD
sur les blocs de taille
lng
.
>>> v = randomVector(10000)
>>> u = encodeTFDParBlocs(v, 100, 1)
>>> v2 = decodeTFDParBlocs(u, 100)
>>> v == v2
True
Test
La fonction fournie simplifieImageTFD(im, lng, qualite)
applique
votre fonction encodeTFDParBlocs
aux pixels d'une image. Plus
précisément, la liste des intensité de rouge des pixels de l'image est vue
comme un très long vecteur et votre fonction encodeTFDParBlocs
est appliquée à ce vecteur. Ensuite, on fait de même avec le vert et le bleu.
>>> im = Image("vague.ppm")
>>> simplifieImageTFD(im, 32, 0.5)
À cet instant, im
n'est même plus une image : les valeurs des
pixels ne sont pas toujours entre 0 et 255 ! Il faut restaurer l'image.
La fonction fournie restaurerImageTFD
fait appel à votre fonction
decodeTFDParBlocs
afin de reconstruire une image.
>>> restaurerImageTFD(im, 32)
>>> im.save('tfd-32-50.ppm')
Vous devriez observer une image semblable à la première mais légèrement dégradée. Cette dégradation est le résultat de la suppression de la moitié des valeurs dans chacun des blocs de taille 32.
Comparaison des deux méthodes de compression avec perte
Écrivez la fonction testCompressionTFD
, similaire à la fonction
testCompresseMoyenne
du début du TP.
def testCompresseTFD(fichier, lng, qualite):
"""
Charge l'image contenu dans le fichier et
- affiche la taille de l'image (sous forme de chaine)
- affiche la taille de l'image compressée (sous forme de tableau
d'octets)
- simplifie l'image avec la fonction encodeTDFParBloc
- affiche la taille de l'image simplifiée (sous forme de chaine)
- affiche la taille de l'image simplifiée compressée (sous forme
de tableau d'octets)
- décode l'image simplifiée et la sauve dans un fichier
"...TFD-lng-qualite.ppm"
"""
Le codage / décodage de l'image peut prendre du temps. Voila une petite image que vous pouvez utiliser pour vos tests..
Exécutez la commande :
>>> testCompresseTFD( 'lac.ppm', 'tfd-100-05.ppm', 100, 0.1 )
-
Dans votre fichier, notez la taille de l'image compressée.
-
Déterminez la valeur de
lng
qui vous permet d'obtenir une taille comparable avec la fonctiontesteCompressionMoyenne
. -
Visuellement, quelle image vous parait le plus dégradée ? Notez vos observations dans votre fichier.
Bonus
Après avoir transformé une image avec simplifieImageTFD
,
lorsqu'on veut reconstruire l'image il faut absolument connaitre le taille des
blocs.
Écrivez une fonction qui, à partir d'un nom de fichier :
-
charge l'image,
-
appelle
simplifieImageTFD
pour la dégrader, -
converti le résultat en une chaine de caractères,
-
écrire dans un fichier toutes les informations nécessaire pour reconstruire l'image. Ces informations doivent être compressées et écrite sous la forme d'une suite d'octets (
bytearray
).
Ensuite, écrivez une deuxième fonction qui, à partir d'un tel fichier reconstruit une image.
On peut retrouver une image a partir de sa chaine de caractere avec à la
fonction Image.init_from_string
.
>>> im = Image.init_from_string(s) # Construction d'un image à partir de la chaine de caractères.
>>> im.save( 'out.ppm' )