Vous êtes sur la page 1sur 16

Info S1 (suite) – page 1/ 16

Tp informatique semestre 1 (suite)

Table des matières


X Dictionnaire 1
1 Le type dict . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2 Recherche dans un dictionnaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
3 Comptage des éléments d’un tableau à l’aide d’un dictionnaire . . . . . . . . . . . . . . . . . . . . . . . . 3

XI Algorithmes avec boucles imbriquées 4


1 Algorithme inconnu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Sommes doubles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3 Recherche des plus proches voisins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

XIILecture/écriture de fichiers 5
1 Commandes de base pour la lecture/écriture dans un fichier . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Répertoire courant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 Premiers exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4 Loi de Benford (et tracé d’histogramme) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

XIIIAlgorithmes gloutons 8
1 Rendu de monnaie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2 Allocation de ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3 Problème du sac à dos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
a Version fractionnaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
b Version non-fractionnaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

XIVManipulation d’images 12
1 Quelques éléments sur le codage numérique d’une image . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2 Premières manipulations d’image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3 Symétrie et rotation « simples » . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4 Conversion en niveaux de gris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5 Détection de contour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
6 Application d’un filtre quelconque à une image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

X Dictionnaire
1 Le type dict
• En python, un objet de type dict (dictionnary, i.e. dictionnaire) est un ensemble de paires « clé : valeur ». L’appellation
dictionnaire est bien sûr tirée de la ressemblance avec le dictionnaire que tout un chacun connaît, qui est un ensemble
de paires « mot : définition ». La comparaison s’arrête cependant là ; le dictionnaire de la vie courante est trié (dans
l’ordre alphabétique), alors que le dict de python ne l’est pas. De plus, en python, une clé peut être presque n’importe
quel objet (sauf une liste) 1 . La valeur associée à une clé peut être n’importe quel objet.
• {1 : 'a', 'frite' : [1, 2, 3], 'tortue' : (7.5, 'carapace')} est un exemple de dictionnaire en python.
On voit que :
* Un dictionnaire est encadré avec des accolades.
1. En fait, une clé doit être un objet non muable, de manière à pouvoir être hachable (voir en deuxième année). En particulier, une clé ne
peut pas être une liste ou un dictionnaire
Info S1 (suite) – page 2/ 16
* Entre les accolades, on trouve des paires clé:valeur, séparées par des virgules.
* Une clé et la valeur associée sont séparées par « : ».
* Cet objet ressemble à une liste de listes de deux éléments (par exemple, l’exemple ci-dessus ressemble à
[[1, 'a'], ['frite',[1, 2, 3]], ['tortue', (7.5, 'carapace')]). Mais attention, il n’est pas une
liste de listes de deux éléments. En particulier, il n’est pas indiçable.

Q 83. Créer et visualiser le dictionnaire d avec le code :


1 d = {1 : 'a', 'frite' : [1, 2, 3], 'tortue' : (7.5, 'carapace')}
2 print(d)
Q 84. Ajouter ensuite le code suivant pour voir comment accéder à un élément du dictionnaire, modifier un élément, ajouter
un élément, supprimer un élément, itérer sur les clés, itérer sur les valeurs, itérer sur les paires clé:valeur, constater
que l’ordre des éléments n’a pas d’importance :
3 print(d['frite']) # d['frite'] renvoie la valeur associée à la clé 'frite'
4 # dans le dictionnaire d.
5 d['frite'] = 'pomme de terre' # Modifie la valeur associée à la clé 'frite'
6 print(d) # dans le dictionnaire d.
7 d['physique'] = 999 # Crée dans le dictionnaire d une nouvelle entrée avec
8 print(d) # la clé 'physique' et la valeur 999
9 for i in d.keys(): # Itération sur les clés
10 print(i)
11 for i in d: # Également itération sur les clés (uniquement)
12 print(i)
13 for i in d.values(): # Itération sur les valeurs
14 print(i)
15 for i in d.items(): # Itération sur les paires clé : valeur
16 print(i)
17 for i, j in d.items(): # Même chose avec une affectation conjointe
18 print('À la clé', i, 'correspond la valeur', j)
19 print(len(d)) # len(d) renvoie le nombre d'entrées de d
20 del(d['frite']) # Supprime la paire clé:valeur dont la clé est 'frite'
21 print(d)
22 d1 = {1:'un', 2:'deux'}
23 d2 = {2:'deux', 1:'un'}
24 print(d1 == d2) # L'ordre n'a pas d'importance

2 Recherche dans un dictionnaire


• Un intérêt de l’utilisation d’un dictionnaire plutôt que d’une liste de listes de deux éléments est que le test d’apparte-
nance d’un élément du dictionnaire se fait plus rapidement qu’en utilisant une liste. En effet, en utilisant une liste, il
faut, dans le pire des cas, parcourir l’ensemble des n éléments de la liste pour trouver (ou pas) l’élément recherché :
on a une complexité linéaire (c’est-à-dire en O(n)). En revanche, les dictionnaires utilisent une technique basée sur
des fonctions de hachage qui permet de trouver (ou pas) un élément du dictionnaire avec une complexité constante
(c’est-à-dire en O(1)).
• Les fonctions de hachage seront vues (un peu) en deuxième année. En simplifié (si vous ne saisissez pas tout, ce n’est
pas grave, ne perdez pas trop de temps là-dessus) :
* Une fonction de hachage associe un entier à n’importe quelle clé. Cet entier est unique (en fait pas tout à fait,
mais on arrive à gérer ce problème). En revanche, pour deux clés identiques, la fonction de hachage renvoie
toujours le même entier.
* Quand on ajoute un élément (= une paire clé:valeur) dans un dictionnaire, python calcule la fonction de hachage
de la clé. Un tableau de correspondance stocke les résultats de ce hachage, et la position en mémoire de la paire
clé:valeur qui lui correspond.
* Quand on souhaite accéder à un élément (= une paire clé:valeur) du dictionnaire qui correspond à une clé
donnée, python calcule la valeur de hachage (on dit aussi le hash, en anglais) de cette clé, et peut ainsi, via le
tableau de correspondance, accéder directement à l’élément du dictionnaire voulu.
Info S1 (suite) – page 3/ 16
* En fait, il peut arriver que deux clés différentes renvoie la même valeur de hachage. On parle de collision. On
gère ces collisions en stockant les paires clé:valeur associées à la même valeur de hachage au même endroit, et
en parcourant la liste de ces paires clé:valeur lorsqu’on souhaite accéder à l’une d’entre elles (ce qui ne génère
pas trop de complexité temporelle car on se débrouille pour que le nombre de clés possédant la même valeur de
hachage reste faible).
• On cherche à vérifier « expérimentalement » que l’accès à un élément d’un dictionnaire est plus rapide que l’accès à un
élément d’une liste de liste de deux éléments. Pour cela, on mesure le temps mis pour exécuter le code correspondant,
en utilisant la fonction time() du module time. Cette fonction renvoie le temps écoulé, en secondes, depuis une
origine arbitraire. La syntaxe est la suivante :
import time
tdebut = time.time()
le code
à
exécuter
duree = time.time() - tdebut

Q 85. Le fichier ville_population_dict.py disponible sur l’espace classe contient la définition d’un dictionnaire appelé
ville_pop_dict. Ce dictionnaire comprend plus de 4000 paires clé:valeur, où clé est le nom d’une ville (stocké dans
une chaîne de caractère) et valeur est le nombre d’habitants de cette ville (stocké dans un entier). Écrire le code qui
permet de récupérer le nombre d’habitants de la ville de Zenica 2 et de le stocker dans une variable nommée pop :
cela prend une ligne. Compléter le code pour mesurer le temps que prend l’exécution de cette ligne de code. Ajouter
ensuite un print pour afficher ce temps d’exécution (et aussi le nombre d’habitants de Zenica, bien sûr 3 .)
Remarque : ne pas inclure le print dans la portion de code dont on mesure le temps d’exécution. En effet, afficher
quelque chose prend énormément de temps, cela fausse complètement la mesure.
Q 86. Le fichier ville_population_list.py contient la définition de la liste ville_pop_list. Cette dernière contient
exactement les mêmes données que le dictionnaire ville_pop_dict, mais sous la forme d’une liste (de plus de 4000
éléments) de listes de deux éléments (le premier est le nom de la ville, le deuxième est le nombre d’habitants). Refaire
exactement le même travail que précédemment. Remarque : cette fois, la recherche du nombre d’habitants de Zenica
et l’affectation de ce nombre dans la variable pop prend plus qu’un ligne. En effet, il faut faire une boucle for, tester
l’égalité avec le nom de la ville, etc.
Q 87. Comparer les temps d’exécution. Remarque : Refaire le test plusieurs fois, car le temps n’est jamais tout à fait le
même. En effet, il dépend des autres processus qui sont en train de tourner en même temps.
On constate que la version « dictionnaire » est bien plus efficace que la version « liste ».
Refaire aussi le test en cherchant une ville vers le début de la liste (disons Accra pour fixer les idées) ou vers le milieu
de la liste (disons Lafayette), avec la version « dictionnaire » et avec la version « liste ». Suivant la manière dont
vous avez codé la recherche dans une liste, la performance de cette version s’améliore (ou pas) quand on cherche une
ville vers le début de la liste. Expliquer.
Q 88. Question bonus (si vous avancez vite : demandez au professeur). Pour vraiment constater expérimentalement que
l’accès à un élément d’un dictionnaire se fait en temps constant, il faut mesurer le temps d’exécution de l’accès à un
dictionnaire de n éléments, pour différentes valeur de n. On peut ensuite tracer la durée d’exécution en fonction de
n, et constater qu’on a (à peu près) une constante. Le faire. Indication : dict(L) renvoie une dictionnaire construit
à partir d’une liste de listes de deux éléments L.

3 Comptage des éléments d’un tableau à l’aide d’un dictionnaire


• Remarque préliminaire : Ici, lorsque vous avez besoin de balayer les caractères d’une chaîne, il faut faire une boucle.
Évidemment, il existe de nombreuses fonctions (par exemple la fonction counter du module collections) qui font
a priori mieux le travail que ce qui va être codé ici. Mais ce n’est pas la question. . .

Q 89. Question bonus (si vous avancez vite : demandez au professeur). Écrire une fonction estADN(c) qui prend en paramètre
une chaîne de caractère c et qui renvoie True si cette chaîne ne contient que les caractères A, C, G et T, ou bien si
la chaîne est vide. Sinon, elle renvoie False. Remarque : L’ADN est un enchaînement de nombreuses bases azotées
de quatre types : adénine, cytosine, guanine et thymine. On peut les représenter par les lettre A, C, G et T.
2. C’est en Bosnie.
3. Spoiler : 96 027 habitants
Info S1 (suite) – page 4/ 16
Tester sur la chaîne de caractère présente dans le fichier adn.py, ainsi que sur des cas particuliers pertinents. Quelle
est la complexité de cette fonction?
Q 90. Écrire une fonction compte_caracteres(s) qui prend une chaîne s en argument et qui renvoie un dictionnaire où les
clés sont les caractères présents dans la chaîne (et seulement ceux-là) et les valeurs associées le nombre d’occurrences
du caractère correspondant.
Tester sur la chaîne de caractères contenue dans le fichier adn.py, qui contient un petit extrait du code génétique du
chromosome humain numéro 1.
Tester aussi sur la liste de mots contenue dans le fichiers mots.py. Attention, il faut d’abord convertir cette liste de
chaînes en une seule grande chaîne pour pouvoir la passer en argument à compte_caracteres().
Q 91. Écrire une version modifiée de la fonction précédente, qu’on appelle compte_caracteres_v2(s), qui compte pour
un e les caractères e, é, è, ê et ë, pour un u les caractères ù et û, etc.
Tracer ensuite le nombre de d’occurrences en fonction du caractère (plt.plot fonctionne avec des listes de nombres,
mais aussi avec des listes d’autre chose : commencez par essayer plt.plot(['a', 'b', 'c'], [1, 5, 2]) pour
vous convaincre).

XI Algorithmes avec boucles imbriquées


1 Algorithme inconnu
Q 92. On donne le code de la fonction fonction(L), qui prend en argument une liste :
1 def fonction(L):
2 for i in range(len(L)):
3 for Lj in L[:i] + L[i+1:]:
4 if L[i] == Lj:
5 return True, Lj
6 return False

• Quelles valeurs prend i?


• Quelles valeurs prend Lj pour un i donné?
• Quand la fonction renvoie-t-elle True? Que fait cet algorithme, finalement?
• Quel est sa complexité?

2 Sommes doubles
Q 93. Écrire la fonction somme_double_1(n) qui prend un entier n > 1 en argument et qui renvoie la valeur de cette
somme :
Xn X n
ij
i=1 j=1

Tester (indication : somme_double_1(5) → 5699). Quelle est la complexité?


Q 94. Écrire la fonction somme_double_2(n) qui prend un entier n > 1 en argument et qui renvoie la valeur de cette
somme :
Xn Xn
i+j
i=1 j=1

Tester (indication : somme_double_2(12) → 1872). Quelle est la complexité?


Q 95. Vérifier « expérimentalement » que :
Xn Xn Xn
i+j =2·n· i
i=1 j=1 i=1

Indication : utiliser somme_double_2(), définir une autre fonction pour le membre de droite. Comparer les résultats
pour toutes les valeurs de n entre 1 et N (choisir N « pas trop grand » au départ pour ne pas que le temps de calcul
soit trop long).
Info S1 (suite) – page 5/ 16
3 Recherche des plus proches voisins
Q 96. Écrire la fonction proches_voisins(L) qui prend une liste d’au moins deux nombres en argument et renvoie les deux
valeurs les plus proches. Indication : comparer chaque valeur de la liste avec toutes les autres (combien cela fait-il de
boucles?). Rappel : la fonction abs(), appliquée à un flottant ou un entier, renvoie la valeur absolue.
Tester. Quelle est la complexité?

XII Lecture/écriture de fichiers


1 Commandes de base pour la lecture/écriture dans un fichier
Q 97. Copier le fichier data1.txt dans le même dossier que le fichier python que vous allez créer. Dans ce fichier python,
écrire et tester le code :
1 f = open('data1.txt', 'r')
2 a = f.read()
3 print(a)
4 f.close()
Noter la syntaxe pour ouvrir un fichier : open(nom_du_fichier, mode) ouvre le fichier dont le chemin d’accès est
donné par la chaîne de caractères nom_du_fichier. La chaîne de caractères mode indique si le fichier est ouvert en
lecture ('r' comme read) ou en écriture ('w' comme write). Remarque : par défaut, open ouvre un fichier en mode
texte, c’est-à-dire qu’on va lire ou écrire des chaînes de caractères dans le fichier 4 .
open renvoie ici un objet dont le type est un type fichier 5 . Cet objet est la structure de données présente en mémoire
qui fait le lien entre le code python et le fichier lui-même (sur le disque).
On accède au fichier en utilisant des méthodes (= des fonctions) associées à cet objet.
Noter la syntaxe pour fermer le fichier : on utilise la méthode .close(), ce qui donne ici f.close() (puisque dans
notre exemple cet objet s’appelle f).
Noter la syntaxe pour lire l’intégralité du fichier : f.read() renvoie une chaîne de caractères contenant l’intégralité
du fichier.
Q 98. Plutôt que d’utiliser open et close, on peut utiliser une syntaxe un peu différente :
1 with open('data1.txt', 'r') as f:
2 a = f.read()
3 print(a)
Il est plutôt recommandé d’utiliser cette syntaxe (qui ferme automatiquement le fichier lorsqu’on sort du bloc de code
qui suit le with). Noter la syntaxe :
with open('nom_du_fichier', mode) as variable_fichier:
bloc de code contenant
tout le code à exécuter
avec le fichier ouvert
Q 99. La méthode f.readline() renvoie une chaîne de caractères contenant une ligne du fichier. Essayer. Si on refait un
appel à f.readline(), cela renvoie la ligne suivante, etc. À la fin du fichier, f.readline() renvoie une chaîne de
caractères vide (= la chaîne ''). Cela permet d’envisager de lire un fichier ligne par ligne de la manière suivante
(essayer) :
1 with open('data1.txt', 'r') as f:
2 while True:
3 a = f.readline()
4 if a == '':
5 break # on force la sortie de la boucle while si la chaine renvoyée est vide
6 print(a)
Remarque : si le fait qu’il y ait dans l’affichage des « sauts de ligne » en plus par rapport à la question 97 vous gêne,
souvenez-vous que print() ajoute par défaut un retour à la ligne en plus de ce qu’on lui dit d’afficher.
4. Il est possible d’avoir un accès en mode binaire en ajoutant b dans la chaîne mode.
5. En fait, ici, le nom exact est _io.TextIOWrapper, on peut faire un type(f) dans la console pour le voir.
Info S1 (suite) – page 6/ 16
Q 100. En fait, l’objet fichier permet de faire ceci de manière bien plus simple. En effet, il est itérable. Essayer :
1 with open('data1.txt', 'r') as f:
2 for a in f:
3 print(a)
Remarque : dans cet exemple, tout se passe comme si f était une liste de chaîne de caractères, comprenant un élément
pour chaque ligne du fichier, mais ce n’est en fait pas le cas.
Q 101. On récapitule ici quelques méthodes permettant de lire dans un fichier :
• .read() : retourne tout le fichier en une chaîne de caractères ;
• .read(n) : retourne une chaîne de caractères comprenant les n caractères suivants ;
• .readline() : retourne une chaîne de caractères comprenant la ligne suivante ;
• .readlines() : retourne une liste de chaîne de caractères contenant chacune une ligne du fichier.
Essayer celles qui n’ont pas encore été essayées.
Q 102. On récapitule ici quelques méthodes permettant d’écrire dans un fichier :
• .write(c) : écrit la chaîne de caractères c dans le fichier ;
• .writeline(L) : L étant une liste de chaînes de caractères, écrit toutes les chaines de caractères dans le fichier
(remarque : n’ajoute pas de retour à la ligne '\n ' entre deux chaînes).
Essayer ces méthodes. Attention : il faut ouvrir un nouveau fichier en mode écriture (with open('data2.txt', 'w') as f:).
Attention aussi : si on ouvre un fichier existant, tout son contenu sera effacé ; si on souhaite ajouter des données à la
fin d’un fichier existant, il faut utiliser le mode 'a' (pour append, c’est-à-dire ajout).
Q 103. Tester le code :
1 t = []
2 temperature = []
3 with open('data1.txt', 'r') as f:
4 f.readline() # La 1ère ligne ne contient pas de données : on ne garde pas.
5 for a in f:
6 x, y = a.split()
7 t.append(float(x))
8 temperature.append(float(y))
9 print(t)
10 print(temperature)

• Remarquer la méthode .split(), qui permet de découper une chaîne de caractères en plusieurs chaînes de
caractères (le séparateur par défaut est le caractère espace).
• Remarquer aussi qu’on force un changement de type. En effet, la fonction float() renvoie son argument après
l’avoir converti en un flottant. Il existe aussi int() pour convertir en entier, str pour convertir en chaîne de
caractères, bool() pour convertir en booléen. . .

2 Répertoire courant
• Par défaut, open() cherche le fichier à ouvrir dans le répertoire (= le dossier) courant, c’est-à-dire celui où est
enregistré le fichier python en train d’être exécuté. Si besoin, on peut spécifier le chemin complet du fichier. Exemple :
open('U:/mondossierdetppython/monsousdossier/fichier.txt'). Ou bien on peut utiliser un chemin relatif
(= un chemin donné « à partir » du répertoire courant) : open('monsousdossier/fichier.txt').
Remarque : il faut utiliser des slashs / plutôt que des antislashs \ 6 Attention, si malgré tout on veut utiliser les \, il
faut écrire U:\\Dossier\\SousDossier et pas U:\Dossier\SousDossier.
• La fonction os.getcwd() (du module os = operating system = système d’exploitation) permet d’obtenir le répertoire
courant (getcwd = get current working directory = obtenir le répertoire de travail courant).
• La fonction os.chdir() (= change directory = changer de répertoire) prend comme argument une chaîne de caractères
correspondant à un chemin de répertoire et définit ce répertoire comme le répertoire de travail courant.
6. Car cela produit du code (un peu) plus portable d’un système à l’autre.
Info S1 (suite) – page 7/ 16
3 Premiers exercices
Q 104. Le fichier premiers_1000.txt est fourni. Il contient les 1000 premiers nombres premiers. Écrire un programme qui
récupére le 684ème nombre premier et le stocke dans une variable de type int. Indication : c’est 5113.
Q 105. Écrire un programme qui détermine la somme des 1000 premiers nombres premiers. Indication : c’est 3 682 913.
Q 106. Le fichier tennis_femmes_2022.csv contient le classement mondial des joueuses de tennis courant 2022, avec les
informations suivantes : rang, nom, prénom, nationalité, nombre de points. Écrire un programme déterminant :
• le nombre de joueuses françaises de ce classement (indication : 75) ;
• la moyenne des scores de toutes les joueuses (indication : envirion 178) ;
• la moyenne des scores des 100 premières joueuses (indication : envirion 1420) ;
• la moyenne des scores des joueuses françaises (indication : envirion 169) ;
• la moyenne des rangs des joueuses dont le prénom commence par un N (indication : envirion 761).
Indication : utiliser la méthode .split(), avec en argument le bon caractère.
Q 107. Le fichier admissibles.txt contient la liste des élèves de PT qui se sont présentés au concours. Il contient des lignes
de la forme :
1234 TOURNESOL Tryphon Admissible 2
56789 HADDOCK Archibald Eliminé -
La première valeur est le numéro du candidat et la dernière valeur est le numéro de la série d’oral du candidat (il y
a 4 séries d’oral) quand il est admissible, et - sinon. Le caractère de séparation des quatre champs d’une ligne est le
caractère tabulation \t.
Indication : avant d’utiliser la méthode .split(), utiliser la méthode .strip() sur chaque ligne : elle « nettoie »
(= élimine les espaces, \n. . . ) le début et la fin de la chaîne de caractères.

4 Loi de Benford (et tracé d’histogramme)


• On cherche à vérifier la loi de Benford (voir fr.wikipedia.org/wiki/Loi_de_Benford). Si on suit une certaine intuition,
on peut penser que la probabilité qu’un nombre pris au hasard commence par un certain chiffre est la même quel que
soit ce chiffre. C’est vrai, par exemple, pour des nombres tirés au hasard (avec une distribution uniforme) entre 1 et
10000. Mais, pour un certain nombre de série de nombres ce n’est pas le cas : la loi de Benford indique des proportions
qui ne sont pas les mêmes.
• Le fichier balance_commune_2018_court.csv contient les soldes des comptes de différentes organisations. Sur
chaque ligne du fichier on trouve, séparés par des virgules : le nom de la commune, le code INSEE (du département
ou de la commune), le numéro du compte, le solde du compte (qui peut être positif ou négatif).

Q 108. Écrire un programme qui, à partir du fichier, crée une liste d’entiers liste_chiffre, contenant le premier chiffre non
nul de chaque solde.

• On souhaite construire un histogramme de ces premiers chiffres. Dans un histogramme, on porte en abcisse la grandeur
étudiée (ici le premier chiffre p). Cet axe est découpé en intervalles appelés « classes » (bins en anglais). Ici cet
intervalle vaut 1. En ordonnée, on trouve le nombre d’occurences des valeurs de p dans l’intervalle correspondant.
La fonction plt.hist(A, bins = 'rice') du module matplotlib.pyplot (importé sous l’alias plt) réalise un
histogramme des valeurs contenues dans la liste A, en déterminant une largeur des classes (= bins) optimale 7 ). Ne
pas oublier le plt.show() après.

Q 109. Compléter le programme pour :


• tracer l’histogramme de la série statistique contenue dans liste_chiffre ;
• obtenir une liste L de la forme [n1, ... , n9], où ni est le nombre de nombres commençant par le chiffre i
• en déduire les proportions correspondantes.
Comparer ces proportions à celles prédites par la loi de Benford.
7. Avec la méthode de Rice.
Info S1 (suite) – page 8/ 16
XIII Algorithmes gloutons
• Les algorithmes gloutons (greedy algorithms en anglais) sont utilisés pour résoudre certains problèmes d’optimisation.
Un problème d’optimisation consiste à déterminer les valeurs de paramètres permettant :
* de minimiser ou maximiser une fonction objectif ;
* éventuellement en satisfaisant une ou des fonctions contraintes 8 .
Exemples classiques de problèmes d’optimisation : le rendu de monnaie, le problème du sac à dos, le problème du
voyageur de commerce. . .
• À chaque étape d’un algorithme glouton, on effectue le choix qui paraît optimal (= qui réduit le plus l’écart entre
le résultat souhaité et le point de départ) à cette étape, sans se soucier de la suite du problème. On passe ensuite à
l’étape suivante, etc.
• Les algorithmes gloutons fournissent une solution, mais ce n’est pas forcément la meilleure possible. Ils sont cependant
relativement faciles à mettre en œuvre.

1 Rendu de monnaie
• Exemple : vous devez payez 86 €. Vous donnez un billet de 200 €. Il faut rendre 200 - 86 = 114 €. On cherche la
façon optimale de faire cela, au sens où on veut rendre 114 € en utilisant le moins possible de pièces/billets.
* Le commerçant commence par rendre un billet de 100 €. C’est le choix qui paraît le meilleur, au sens où c’est
celui qui minimise le plus possible (parmi les pièces/billets disponibles) la somme à rendre à l’étape suivante. Dit
autrement, c’est la pièce/billet la plus grande possible qu’on puisse rendre.
* Il reste 114 - 100 = 14 € à rendre. Le commerçant rend alors un billet de 10 €.
* Il reste 14 - 10 = 4 €. Il rend 2 €.
* 4 - 2 = 2 €. Il rend (encore) 2 €.
* 2 - 2 = 0. C’est fini. . .
Remarque : en fait, dans la vraie vie, le commerçant ne fait pas comme ça, mais exactement dans l’ordre inverse 9 .
Autre remarque : en fait, le fait que ce soit un rendu de monnaie n’a aucune importance ; il s’agit juste, étant donnée
une somme d’argent, de trouver une combinaison de pièces/billets permettant de totaliser cette somme (en utilisant
le moins possible de pièces/billets si on veut une solution optimale).
• On se dote de la liste des pièces/billets, appelée système :
S = [500, 200, 100, 50, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01]
Remarque : les valeurs dans le système sont classées par ordre décroissant. C’est indispensable pour le bon fonction-
nement de l’algorithme.
On note v la valeur à rendre (qui va diminuer au cours de l’avancement de l’algorithme). On suppose que cette valeur
a déjà été calculée. On note R la liste des pièces/billets à rendre.
• Fonctionnement de l’algorithme :
1. On compare v au premier élément de S (c’est-à-dire S[0]). Si v est plus petit que S[0], alors on passe à l’élément
suivant, jusqu’à arriver au premier élément (numéro i) tel que v soit plus grand que S[i]. Si même le dernier
élément de S est trop grand, alors l’algorithme se termine.
2. On ajoute ensuite la valeur de ce dernier élément à la liste L des pièces/billets à rendre, et on retranche cette
valeur à v.
3. On recommence jusqu’à ce que v soit inférieur au plus petit élément de S (comme déjà dit à l’étape 1).
• En supposant que la valeur à rendre est 114 €, vérifier que le tableau ci-dessous correspond bien à cet algorithme :
8. Le problème peut être avec ou sans contrainte.
9. Il commence par donner 2 € en comptant « 86 + 2 = 88 € », puis encore 2 € en comptant « 88 + 2 = 90 € », puis « 90 + 10 =
100 € » etc. jusqu’à arriver à 200 €. Mais l’algorithme glouton, lui, commence bien par la plus grande des pièces/billets.
Info S1 (suite) – page 9/ 16
i v (en début d’itération) S[i] v (en fin d’itération) L
0 114 500 sans objet []
1 114 200 sans objet []
2 114 100 14 [100]
0 14 500 sans objet [100]
1 14 200 sans objet [100]
2 14 100 sans objet [100]
3 14 50 sans objet [100]
4 14 20 sans objet [100]
5 14 10 4 [100, 10]
0 4 500 sans objet [100, 10]
.. .. .. .. ..
. . . . .
4 4 20 sans objet [100, 10]
5 4 10 sans objet [100, 10]
6 4 5 sans objet [100, 10]
7 4 2 2 [100, 10, 2]
0 2 500 sans objet [100, 10, 2]
.. .. .. .. ..
. . . . .
6 2 5 sans objet [100, 10, 2]
7 2 2 0 [100, 10, 2, 2]
Remarque : les étapes en italiques ne servent à rien, on pourrait se débrouiller pour les sauter (mais attention, après
avoir déterminé qu’il faut rendre un billet de 10 €, il faut à nouveau se demander s’il faut rendre un autre billet de
10 €, et pas passer à 5 € directement).

Q 110. Écrire une fonction le_plus_grand_a_rendre(v, systeme), qui prend en argument la valeur à rendre v et le
système utilisé systeme, et renvoie le plus grand élément de systeme qu’on puisse rendre. Si aucun élément ne
convient, la fonction renvoie le booléen False.
Q 111. Écrire une fonction rendu_monnaie(v, systeme), qui prend en argument la valeur totale à rendre v et le système
utilisé systeme, et renvoie la liste des pièces/billets à rendre. Si on ne peut rien rendre, la fonction renvoie une liste
vide []. Indication : cette fonction fait appel à la fonction le_plus_grand_a_rendre().
Essayer cette fonction avec le système « euro » donné plus haut, pour rendre 114 €, puis pour 77,21 €.
Q 112. Essayer aussi avec 77,22 €. Quel est le problème (ne pas hésiter à mettre un print ou deux pour voir ce qu’il se passe)?
Modifier la fonction rendu_monnaie() pour régler ce problème. Indication : utiliser la fonction round(number, ndigits=None).
Q 113. Avant le 15 février 1971, le pound (= la livre sterling) britannique était divisée en 20 shillings, chaque shilling étant
divisé en 12 pence (pluriel de penny). Si on exprime les valeurs des différentes pièces qui existaient en pence, cela
donne le systeme S2 = [240, 120, 60, 30, 24, 12, 6, 3, 1] 10 .
Essayer la fonction rendu_monnaie() avec ce système, pour rendre 48 pence. Dans ce cas, est-ce que l’algorithme
glouton renvoie la solution optimale? Quelle serait la solution optimale?

• On voit que l’algorithme glouton renvoie ici une solution, mais qui n’est pas la solution optimale. Un tel algorithme
est parfois appelé une heuristique.
• Dans le cas du système « euro », la solution renvoyée par l’algorithme glouton est toujours optimale. Cela est du au
fait que les valeurs des pièces/billets ont été « bien choisies ». Un tel système est appelé un système canonique.
L’ancien système britannique n’est pas un système canonique.

2 Allocation de ressources
• On souhaite trouver une solution, grâce à un algorithme glouton, à un problème d’allocation d’une ressource en réponse
à plusieurs demandes. On prend l’exemple de l’attribution de créneaux horaires pour l’utilisation d’une salle sur une
journée. Plusieurs demandes de réservation sont faites, chacune avec une heure de début et une heure de fin. Exemple
de liste de demandes de réservation : [[12.15, 14, 'a'], [13, 14, 'b'], [7, 13, 'z'], [15, 17, 'r']]
pour 4 demandes de réservations (de 12h15 à 14h, de 13h à 14h, etc.). La chaîne de caractère ('a', 'z'. . . ) sert à
identifier une réservation. Elle n’est pas indispensable ici. L’objectif est de trouver une solution optimale, c’est-à-dire
de satisfaire le maximum de demandes.
10. Dans l’ordre : pound ou sovereign (240), half sovereign (120), crown (60), half crown (30), florin (24), shilling (12), sixpence (6), threepence
(3), penny. Et encore, il manque : guinea (252), double florin (48), halfpenny (0,5), farthing (0,25), 1/2 farthing (0,125), 1/3 farthing (≃ 0,083).
Voir fr.wikipedia.org/wiki/Livre_sterling.
Info S1 (suite) – page 10/ 16
• Quelques précisions :
* la salle ne peut être utilisée qu’une fois à un instant donné ;
* il n’y a pas besoin de prévoir de période de battement entre deux réservations ;
* on cherche à privilégier le nombre de réservations plutôt que la durée totale de réservation : par exemple, on
préfère réserver la salle trois fois une heure plutôt que une fois cinq heures, ou même que une fois trois heures.
• Pour chercher à répondre au problème avec un algorithme glouton, il faut choisir quel critère « glouton » utiliser. En
effet, on peut en envisager plusieurs, par exemple :
* durée croissante : on considère les réservations par ordre de durée croissante ;
* durée décroissante : on considère les réservations par ordre de durée décroissante ;
* début croissant : on considère les réservations par ordre chronologique de leur début ;
* fin croissante : on considère les réservations par ordre chronologique de leur fin ;
* le moins de conflits : on considère les réservations par ordre croissant de leur nombre de conflits, un conflit étant
ici le fait que deux réservations portent sur le même créneau horaire.

Q 114. On choisit le critère de fin croissante. La fonction tri_fin_croiss(L) est fournie (dans le fichier allocation_de_salle.py).
Elle prend en argument une liste L de demandes de réservations, et renvoie une nouvelle liste, où ces demandes sont
triées par date de fin croissante. Remarque : on verra plus tard dans l’année différents algorithmes de tri. Écrire la
fonction glouton_fin_croiss(L), qui prend en argument une liste de demandes de réservation L (non triée), et
renvoie une nouvelle liste, contenant les réservations acceptées, dans l’ordre chronologique.

Optimalité de l’algorithme glouton avec le critère de fin croissante


• On peut montrer que l’algorithme glouton avec le critère de fin croissante (earliest finish time) fournit une solution
optimale (c’est-à-dire qui permet d’accepter le maximum de demandes de réservation). Pour information, voici cette
preuve.
• Tout d’abord, il est certain qu’il existe au moins une solution optimale au problème (démonstration triviale, par
l’absurde). On la note S. Son cardinal est k. Ses éléments sont les {s1 ,s2 · · · sk }. On note d(si ) et f (si ) les dates de
début et de fin de l’élément numéro i.
• L’algorithme glouton avec heure de fin croissante fournit une solution notée G. Son cardinal est ℓ. Ses éléments sont
notés {g1 ,g2 . . . gℓ }.
• Tout d’abord, on montre qu’il existe une solution optimale T , de (de cardinal ℓ, donc) telle que les k premiers éléments
de T sont les mêmes que les k éléments de G (i.e. ∀i ∈ J1,jK,gi = ti ). On fait une démonstration par récurrence. La
propriété Pj est « il existe une solution optimale T telle que les j premiers éléments de T et de G sont identiques »
(j ≤ ℓ).
* Initialisation (P1 ) : S étant une solution optimale, on a :

d(s1 ) ≤ f (s1 ) ≤ d(s2 ) ≤ f (s2 ) ≤ · · · ≤ d(sk ) ≤ f (sk )

Puisque G est une solution gloutonne avec heure de fin croissante, alors on a nécessairement f (g1 ) ≤ f (s1 ).
Donc g1 est compatible avec s2 (i.e. f (g1 ) ≤ d(s2 )) puisque f (s1 ) ≤ d(s2 ).
Donc l’ensemble T = {g1 ,s2 ,s3 · · · sk } est une solution optimale dont le premier élément est commun avec G.
P1 est vérifiée
* Propagation (Pj =⇒ Pj+1 ) : l’ensemble {g1 , · · · ,gj ,sj+1 , · · · ,sk } vérifie Pj .
On a f (gj ) ≤ d(gj+1 ) (car G est une solution).
On a aussi f (gj+1 ) ≤ d(sj+2 ) car f (gj+1 ) ≤ f (sj+1 ) (car sj+1 est une demande dont la date de fin est minimale
dans l’ensemble des demandes auquel on a enlevé les demandes g1 à gj ) et f (sj+1 ) ≤ d(sj+2 ) (car S est une
solution).
Donc gj+1 est compatible avec gj et sj+2 .
Donc l’ensemble {g1 , · · · ,gj+1 ,sj+2 , · · · ,sk } vérifie bien Pj+1 .
* Conclusion de la récurrence : il existe une solution optimale T = {g1 , · · · ,gk ,sk+1 , · · · ,sℓ } dont les k premiers
éléments sont les mêmes que G.
• Il reste à montrer que k = ℓ (donc que G est optimale). Par l’absurde : on suppose k < ℓ. On remarque alors que
tk+1 est une demande compatible avec les demandes précédentes de G. Donc l’algorithme qui a fournit G n’aurait
pas du s’arrêter. C’est une contradiction.
Donc k = ℓ. Donc G est optimale.
Info S1 (suite) – page 11/ 16
Questions bonus : autres critères
Q 115. On fournit la fonction tri_duree_croiss{L}, qui prend en argument une liste de demandes de réservation et renvoie
une nouvelle liste, triée par durée croissante. Écrire une fonction glouton_duree_croiss(L) qui prend en argument
une liste de demandes de réservation L (non triée), et renvoie une nouvelle liste, contenant les réservations acceptées,
dans l’ordre chronologique. Tester et trouver un contre-exemple qui montre que ce critère permet pas de trouver la
solution optimale.
Q 116. Refaire le même travail avec le critère début croissant (il faut commencer par écrire la fonction de tri, en s’inspirant
des précédentes). Tester avec [[0, 6], [1, 3], [3, 4], [4, 6]].
Q 117. De même avec le critère le moins de conflits. Tester avec [[0, 2], [2, 4], [4, 6], [6, 8], [1, 3], [1, 3],
[1, 3],[3, 5], [5, 7], [5, 7], [5, 7]].

3 Problème du sac à dos


a Version fractionnaire
• Un promeneur souhaite transporter dans son sac à dos le fruit de sa cueillette. On a les contraintes suivantes :
* La capacité maximale du sac à dos est 5 kg, mais la masse totale cueillie est strictement supérieure à 5 kg.
* Les fruits cueillis ont des valeurs au kg différentes.
* Le promeneur souhaite maximiser la valeur de son chargement.
* Récapitulatif des quantités cueillies et des prix unitaires :

Fruit quantité prix unitaire


framboise 1,2 kg 24 €/kg
myrtille 3 kg 16 €/kg
fraise 5 kg 6 €/kg
mûre 5 kg 3 €/kg

• Remarque : on peut prélever une quantité arbitraire de fruit dans la quantité disponible. Dit autrement, la masse d’une
seule framboise est suffisamment petite pour qu’on puisse prélever une masse choisie arbitrairement entre 0 et 1,2 kg.
Vocabulaire : on parle de la version fractionnaire de l’algorithme du sac à dos.
• Principe de l’algorithme : on place d’abord dans le sac la quantité maximale de fruit les plus chers par unité de masse.
S’il y a encore de la place, on continue avec les fruits les plus chers par unité de masse restant, etc.

Q 118. Écrire une fonction sac_a_dos(cueillette, capacite). Elle prend en argument le flottant capacite, qui est la ca-
pacité du sac à dos en kg. Elle prend aussi en argument la liste cueillette, qui représente la cueillette. Exemple de dé-
finition de cette liste : cueillette = [['framboise', 1.2, 24], ['myrtille', 3, 16], ['fraise', 5, 6],
['mûre', 5, 3]]. On suppose que cette liste est déjà triée par ordre de prix au kg décroissant. La fonction renvoie
une liste décrivant le contenu optimal du sac à dos (exemple : [['fraise', 0.3], ['myrtille', 2] si le sac à
dos doit contenir 0,3 kg de fraise et 2 kg de myrtille). Elle renvoie aussi la valeur, en euros, du contenu du sac.

• On voit (on peut le montrer) que ce problème du sac à dos trouve ici une solution optimale par une méthode gloutonne.
On va voir que ce n’est pas le cas pour une autre variante de ce problème, dite non fractionnaire.

b Version non-fractionnaire
• On a les fruits suivants :
Fruit quantité masse d’un fruit prix au kg
melon charentais 1 fruit 1,1 kg 4 €/kg
melon jaune 1 fruit 1,9 kg 2,5 €/kg
pastèque 2 fruits 3 kg 2 €/kg

• Cette fois, les éléments à transporter ne sont pas fractionnables. Par exemple, si on prend de la pastèque, c’est soit 0
kg, soit 3 kg (= 1 pastèque), soit 6 kg (= 2 pastèques), etc.

Q 119. Écrire une fonction sac_a_dos_nonfrac(cueillette, capacite) similaire à la précédente, sauf que :
• cueillette est par exemple de la forme [['melon charentais', 1, 1.1, 4], ['melon jaune', 1, 1.9, 2.5],
['pastèque', 2, 3, 2]] ;
Info S1 (suite) – page 12/ 16
• elle ne renvoie pas des masses mais des nombres de fruits.
Indication : on peut copier-coller le code de la fonction précédente et le modifier (beaucoup).
Q 120. Considérer la solution [['pastèque', 1], ['melon jaune', 1]]. La solution obtenue par l’algorithme à la ques-
tion précédente est-elle optimale?

• En fait, cet algorithme glouton n’est pas adapté pour trouver une solution optimale du problème du sac à dos non
fractionnaire. Il présente bien la propriété de sous-structure optimale : la solution d’un sous-problème (= le choix fait à
une étape du problème) est bien optimal (c’est normal, c’est un algorithme glouton). Mais les choix gloutons successifs
n’engendrent pas une solution optimale globalement.
• Pour répondre de manière optimale à la question, il faudrait étudier toutes les combinaisons possibles, de préférence
de manière «efficace » : voir la programmation dynamique en deuxième année.

XIV Manipulation d’images


1 Quelques éléments sur le codage numérique d’une image
• Il existe deux grandes catégories de d’image numérique :
* les images matricielles (ou bitmap) : on a une « grille » de pixels, et on associe à chaque pixel une ou des
propriétés (couleur, luminance. . . ).
Exemples de format : bmp, gif, png, jpeg
* les images vectorielles : on décrit l’image sous une forme « mathématique », par exemple « un segment noir
d’épaisseur 1 mm de tel point à tel autre point. » 11
Exemples de format : svg, ps, pdf (même si un pdf peut « inclure » une image bitmap).
• On s’intéresse ici à des images bitmap.
• On décrit de manière simplifiée l’affichage d’une image sur un écran. Chaque pixel de l’écran comprend plusieurs
sources lumineuses, de couleurs différentes : une rouge, une verte, une bleue (système RVB, en anglais RGB). Les
différentes couleurs sont réalisées par synthèse additive de ces trois couleurs primaires, en allumant de manière plus
ou moins intense les trois sources 12 .
• Dans ce système, on peut coder la couleur d’un pixel sur trois octets : un pour le rouge, un pour le vert, un pour le
bleu. un nombre entier codé sur un octet peut prendre des valeur comprises entre 0 et 255.
Précisions : Un octet = 8 bits. Chaque bit ne peut prendre que 2 valeurs : soit 0, soit 1. Donc sur un octet, on peut
coder 28 = 256 valeurs différentes. Par exemple, on peut coder un entier compris entre 0 et 255.
• Exemples :
* le triplet (0,0,0) code le noir.
* (255,255,255) code le blanc.
* (255,0,0) code le rouge, (0,255,0) code le vert, (0,0,255) code le bleu.

2 Premières manipulations d’image


Q 121. On utilise les sous-modules image et pyplot du module matplotlib. On fournit le fichier image logo.bmp. Essayer
le code :
1 import matplotlib.pyplot as plt
2 import matplotlib.image as img
3

4 image = img.imread('logo.bmp')
5 plt.imshow(image)
6 plt.show()
Noter l’utilisation de la fonction img.imread() : elle prend en argument une chaîne de caractères contenant le nom
du fichier image à charger. Elle renvoie un tableau (un np.ndarray du module numpy), stockée ici dans la variable
image.
11. Remarque : dans ce cas, on peut zoomer autant que souhaité, l’image ne sera jamais pixelisée, car l’afficheur recalcule les pixels à afficher
à chaque changement de niveau de zoom.
12. Remarque : en fait l’espace de couleur RGB est plus restreint que ce qu’un œil humain peut voir. Il n’arrive notamment pas à reproduire les
bleus ou les verts « purs » par exemple.
Info S1 (suite) – page 13/ 16
Noter aussi la fonction plt.imshow() qui prend en argument un np.ndarray(dont on va voir la forme ci-dessous),
et affiche l’image (ne pas oublier le plt.show()).
Passer la souris au-dessus de l’image. On voit que les coordonnées du pointeur de la souris s’affichent. On voit de plus
le contenu du pixel correspondant : un tableau à une dimension de 3 éléments, contenant les valeurs R, V et B pour
ce pixel.
Remarquer aussi que le pixel de coordonnées (0,0) est en haut à gauche.
Q 122. Ajouter au début du code la ligne import numpy as np, de manière à disposer des outils du module numpy pour
manipuler les images en mémoire (puisque Python les stocke sous la forme de np.ndarray). Ajouter ce code à la
suite du précédent (il faut aussi commenter le plt.show(), ou le déplacer en fin de code) :

7 print(image) # Affiche tout le tableau : ça peut être long,


8 # mais l'affichage est automatiquement raccourci.
9 print(type(image))
10 print(np.shape(image)) # Ou bien utiliser la méthode : print(image.shape).
11 print(type(image[0, 0, 0]))

On constate que image est bien un tableau numpy. C’est un tableau à trois dimensions puisque image.shape renvoie
un tuple de 3 éléments. Dans cet exemple, c’est un tableau de forme (= shape) (46, 46, 3). On peut voir cela comme
un tableau à deux dimensions (46 lignes × 46 colonnes), correspondant à une image de 46 × 46 pixels, dont chaque
élément contient un tableau à une dimension de 3 éléments (les trois composantes R, V, B du pixel).
On constate aussi que chaque élément du tableau est un entier codé sur 8 bits : en effet, type(image[0, 0, 0]
renvoie numpy.uint8.
Q 123. À l’aide de la fonction np.zeros, créer un tableau à trois dimensions, de forme 20 × 30 × 3, contenant uniquement
des 0. Ce tableau représente donc une image de 20 lignes par 30 colonnes, complètement noire.
Rappel : np.zeros prend en argument un tuple comprenant le même nombre d’éléments que celui de dimensions
souhaitées pour le tableau.
Indication : pour que chaque élément soit un entier codé sur 8 bits, ajouter aux arguments de np.zeros l’argument
optionnel dtype = np.uint8.
Q 124. Afficher l’image pour vérifier. Modifier la valeur des 3 premiers pixels de la première ligne pour qu’ils soient rouge
([255, 0, 0]), vert ([0, 255, 0]) et bleu ([0, 0, 255]). Afficher.
Q 125. Sauvegarder l’image en utilisant la fonction img.imsave('nom_du_fichier.bmp', tableau_numpy), où les argu-
ments sont une chaîne de caractères contenant le nom du fichier souhaité, et le tableau numpy contenant l’image à
enregistrer. Ouvrir l’image avec un logiciel quelconque pour vérifier. . .
Q 126. Créer une image blanche en utilisant imageblanche = 255 * np.ones((20, 40, 3), dtype = np.uint8). Af-
ficher.
Remarque : si on souhaite faire plusieurs figures, chacune avec une image, il faut utiliser plusieurs fois img.imshow(),
en ayant à chaque fois utilisé un plt.figure('Figure 1') avant (avec un nouvel argument à chaque fois).

3 Symétrie et rotation « simples »


Q 127. Écrire une fonction symetrie_verticale(im) qui prend en argument un tableau numpy im représentant une image,
et renvoie une nouvelle image, symétrique de la première par rapport à un axe vertical (passant au milieu de l’image).
Il ne faut pas modifier l’image d’origine.
Indication : si image est un tableau numpy représentant une image en mémoire, alors image.shape[0] renvoie
le nombre de lignes, image.shape[1] renvoie le nombre de colonnes, et image.shape[2] renvoie le nombre de
composantes par pixel (c’est-à-dire 3).
Tester sur logo.bmp, red_pill.bmp, ou autre chose !
Q 128. Écrire une fonction quart_de_tour(im) renvoyant une nouvelle image, la même que celle donnée en argument mais
pivotée de + π2 .
Remarque : attention, la forme de la nouvelle image n’est pas la même que celle de l’ancienne.
Tester.
Info S1 (suite) – page 14/ 16
4 Conversion en niveaux de gris
Q 129. On propose, pour convertir une image en couleur en niveaux de gris, d’attribuer à chaque composante d’un pixel la
valeur moyenne des trois composantes de ce pixel. Écrire un fonction niv_gris_moy(im) qui renvoie une nouvelle
image, résultat de la conversion en niveaux de gris de l’image prise en argument. Tester.
Indication : Si vous rencontrez l’avertissement (warning) overflow encountered in ubyte_scalars, c’est proba-
blement que vous additionnez (ou faites une autre opération) deux entiers de type np.uint8, et que le résultat de
l’opération est plus grand que 255 (c’est-à-dire la valeur maximale qu’on peut stocker dans un np.uint8). Il ne peut
donc pas être stocké dans un np.uint8. Il y a ce qu’on appelle un dépassement de capacité (overflow). Il faut
donc forcer la conversion du type d’au moins un des deux nombres impliqués dans l’opération en un type ayant une
« capacité » plus grande, par exemple int ou float. Par exemple, on peut utiliser float(a) + b au lieu de a + b
(où a et b sont deux np.uint8).
Q 130. Cette méthode de conversion en niveaux de gris d’une image en couleurs n’est pas toujours convaincante. En effet,
les différentes couleurs ne sont pas perçues de la même façons par l’œil. Une norme, éditée par la Commission
Internationale de l’Éclairage (CIE), recommande qu’une composante d’une image en niveaux de gris soit calculée par
une moyenne pondérée des trois composantes R, V et B : G = 0,2125 · R + 0,7154 · V + 0,0721 · B. Écrire un fonction
niv_gris_CIE(im) qui renvoie une nouvelle image, résultat de la conversion en niveaux de gris de l’image prise en
argument, utilisant cette recommandation.

5 Détection de contour
• Un intérêt de détecter les contours des objets présents sur une image est par exemple de procéder ensuite à la
reconnaissance de cet objet (en reconnaissant sa forme), ou bien de pouvoir suivre son mouvement. On étudie ici
sommairement la détection de contour.
• Un contour est la limite d’un objet dans une image. Cette limite est caractérisée par une variation brutale de couleur
ou de contraste. Dans cette optique, pour un pixel donné, on peut se demander : est-il semblable, de même couleur
que ses voisins ? sinon, quelle est la différence entre lui et ses voisins ? cette différence est-elle suffisamment grande
pour qu’il s’agisse de la limite d’un objet? dit autrement, c’est combien, « suffisamment grande »?
• Pour chaque pixel, on calcule une distance avec son voisinage, puis on décide si ce pixel est sur un contour en effectuant
un seuillage : si la distance est supérieure au seuil, alors le pixel est situé sur un contour. On crée une nouvelle image,
de mêmes dimensions que celle d’origine, où tous les pixels sont noirs sauf ceux des contours, qui sont blancs.
• Il reste deux questions à régler :
* comment calculer une distance : norme euclidienne, maximum des différences avec les voisins, une autre norme?
* jusqu’où étendre le voisinage : une rangée de voisins (i.e. les 8 pixels les plus proches), 2 rangées (i.e. les 24
pixels les plus proches)? plus? une partie seulement de ces 8 ou 24 pixels?
• On propose ici un algorithme simple. La distance est calculée en faisant la moyenne des valeurs absolues des différences
entre la valeur du pixel et celles des 8 plus proches voisins. Cela donne la relation suivante, où d(i,j) est la distance
entre la valeur p(i,j) d’un pixel de coordonnées (i,j) et les valeurs de ses 8 plus proches voisins :
i+1 j+1
1 X X ′ ′
p(i ,j ) − p(i,j)
d(i,j) =
8 ′
i =i−1 j ′ =j−1

Remarque : inutile d’exclure de la somme le terme tel que i = i′ et j = j ′ , car il est nul.
• La sous-matrice suivante, extraite de l’image globale, est constituée du pixel considéré plus ses 8 voisins :
 
pi−1,j−1
 pi−1,j pi−1,j+1  
 pi,j−1 pi,j pi,j+1 

 
 

 
pi+1,j−1 pi+1,j pi+1,j+1

Chaque pixel pi′ ,j ′ comprend trois composantes (R, V, B).


On appelle cette sous-matrice matrice locale.

Q 131. Écrire une fonction distance_p(mloc) qui prend en argument une matrice locale (extraite d’une image en niveaux
de gris), et renvoie la distance du pixel central de cette matrice locale à ses voisins, en suivant la relation donnée
ci-dessus. Cette matrice locale est sous la forme d’une np.ndarray à trois dimensions, de format 3 × 3 × 3.
Remarque : il suffit de travailler sur une des composantes R, V ou B car elles sont égales (on travaille avec des images
en niveaux de gris).
Info S1 (suite) – page 15/ 16
Q 132. Écrire une fonction contour(image:np.ndarray, seuil) -> np.ndarray, np.ndarray qui prend en argument
une image sous la forme d’un np.ndarray à trois dimensions, et renvoie deux np.ndarray (qui correspondent à
des images). Cette fonction convertit l’image en niveaux de gris (avec niv_gris_CIE()) et place le résultat dans
une nouvelle variable. Ensuite, elle crée deux nouvelles images de mêmes dimensions que la première (im_contour et
im_distance). im_distance contient les distances calculées pour chaque pixel (et 0 ailleurs). im_contour contient
le résultat du seuillage (les pixels où distance > seuil sont blancs, les autres noirs). Ces deux nouvelles images sont
renvoyées. Tester sur logo.bmp, sur red_pill.bmp : il faut ajuster la valeur de seuil.

6 Application d’un filtre quelconque à une image


• On considère la matrice F , de taille n × n, où n est un entier impair. Cette matrice est le filtre, ou masque, qu’on va
appliquer sur l’image, en réalisant ce qu’on appelle un produit de convolution. Par exemple, pour réaliser une opération
de moyennage-floutage, on peut utiliser ce filtre :
 
1 1 1
1   
F = · 1 1 1
 
9 

 


1 1 1

Un élément de F est noté fi,j . Sur cet exemple, n = 3.


• On considère également une image comportant Nli lignes et Ncol colonnes, chaque pixel comportant trois compo-
santes R, V et B. En python, on utilise un np.ndarray à trois dimensions, de format Nli×Ncol×3. Pour décrire le
fonctionnement de l’opération de filtrage, on considère une matrice de taille Nli×Ncol, représentant une des trois
composantes R, V ou B de l’image. On note cette matrice M . Un élément de M est noté mi,j .
• Le résultat de l’opération de filtrage (= masquage = produit de convolution) est la matrice N . Elle a la même taille
que M . Un élément de N est noté ni,j . N est définie ainsi :
* On définit k =
2 (partie entière de 2 , dit autrement le résultat de la division entière de n par 2). On définit
n n

les éléments du bord d’une matrice comme ceux situés sur les k premières et les k dernières lignes et colonnes.
Les éléments du bord de N sont identiques aux éléments des bords de M 13 .
* Pour tous les éléments qui ne sont pas sur les bords, on calcule ni,j de la manière suivante :

i+k
X j+k
X
ni,j = fi′ ,j ′ · mi′ ,j ′
i′ =i−k j ′ =j−k

Dit autrement : on effectue le produit terme à terme 14 des éléments du filtre F par les éléments du voisinage du
pixel concerné (F et ce voisinage ayant tous les deux une taille n × n).
Remarque : on voit bien qu’on ne peut pas calculer les ni,j sur les bords avec cette relation.

Q 133. Écrire une fonction filtrer(image:np.ndarray, filtre:np.ndarray) -> np.ndarray. Elle prend en argument
deux np.ndarray, qui sont une image et un filtre. Elle crée une nouvelle image, résultat de l’opération de filtrage
décrite ci-dessus, et la renvoie.
Rappel et indication : Si A et B sont deux np.ndarray de même format, alors A*B renvoie bien un tableau de même
format, où on a effectué un produit terme à terme 15 . De plus, np.sum(A) renvoie la somme des éléments de A.

• On peut tout à fait imaginer des filtres pour lesquels les valeurs obtenues seraient en dehors de l’intervalle possible
(i.e. moins de 0 ou plus de 255). Pour éviter cette situation, on peut imaginer trois stratégies :
* La première, qui est celle qu’on a utilisé ci-dessus, consiste à utiliser un filtre normalisé, c’est-à-dire tel que la
somme de ses coefficients soit égale à 1. On s’assure ainsi que le résultat ne dépassera jamais 255.
* La deuxième consiste à écrêter les valeurs qui dépassent. Il faut alors faire le calcul en utilisant un type qui
supporte des valeurs nettement plus grandes que 255 (et plus petites que 0), et ensuite mettre à 255 toutes les
valeurs supérieures à 255, et à 0 toutes les valeurs inférieures à 0.
* La troisième consiste à faire le calcul comme dans le cas précédent, mais en ramenant ensuite toutes les valeurs
dans l’intervalle [0,255] (avec une relation de proportionnalité par exemple).
13. En fait, il faudrait traiter ces éléments de façon spécifique, mais on fait ici l’opération de filtrage de manière simplifiée.
14. Ce n’est pas un produit matriciel.
15. Là aussi, ce n’est pas un produit matriciel.
Info S1 (suite) – page 16/ 16
Q 134. Essayer les filtres suivants (éventuellement en les appliquant à une image en niveaux de gris) :
 
−2 −1 0

* Repoussage : F2 = −1

1 1

 
 

 
0 1 2
 
0 1 0
* Contours : F3 = 1
 
 −4 1
 


 

0 1 0
 
 0 −1 0 

* Réhausseur : F4 = 

−1 5 −1
 

 
0 −1 0
 
 
1 2 1
1 

* Moyennage gaussien 3 × 3 : F5 = . Remarque : on a F5 = 1 · V ∗ t V , où V est la matrice
 
2 4 2
16 

 

 16
1 2 1
 
 1
, ∗ est le produit matriciel, et t V est la transposée de V . En python, si A et B sont des np.ndarray,
 
2

 


  
1
alors np.dot(A, B) renvoie le produit matriciel de A par B (on peut aussi utiliser A@B, ou encore A.dot(B)),
et A.T renvoie la transposée de A.
   
1 4 6 4 1
  1 
 
4 16 24 16 4 4

 
 
 
1 

 
 
 

* Moyennage gaussien 5 × 5 : F6 = . Remarque : , où .
1 t
·V ∗
   
 6 34 36 24 6 
 F 6 = 256 V V = 
 6 

256 

 
 
 
4 16 24 16 4 4 
 
 
 
   
  
1 4 6 4 1

1

Vous aimerez peut-être aussi