Vous êtes sur la page 1sur 7

D.S.

2 : PHOTOMOSAÏQUE
CORRIGÉ

1. Nommer un type de conteneur indexé mais pas ordonné en Python. Donner une instruction Python
permettant de créer un tel conteneur, avec deux éléments quelconques de votre choix.

Un dictionnaire est un conteneur indexé mais pas ordonné en Python. On le crée ainsi :
dictionnaire = {'prenom': 'Benjamin', 'age': 18}

2. Combien de couleurs différentes peut-on représenter avec un tel pixel ?

Chaque composante peut prendre 256 valeurs différentes (en n’oubliant pas de compter la valeur 0). Un pixel
peut donc prendre 2563 = 16 777 216 de couleurs différentes.

3. Donner une instruction permettant de créer un vecteur correspondant à un pixel rouge, puis un pixel
blanc.

pixel_rouge = np.array([255, 0, 0])


pixel_blanc = np.array([255, 255, 255])

4. Écrire une fonction d’en-tête


def gris(p:pixel) -> int:
qui calcule le niveau de gris correspondant au pixel p.

def gris(p:pixel) -> int:


# Ces trois lignes sont équivalentes
return round(p.mean())
return round(p.sum() / 3)
return round((p[0] + p[1] + p[2]) / 3)

5. Interpréter ces valeurs.

source.shape renvoie la taille du tableau numpy associé à l’image surfer.jpg suivant ses différentes
dimensions. Il s’agit donc d’un de 3 000 lignes, 4 000 colonnes et comportant 3 informations pour chaque
élément, c’est-à-dire une image de 3 000 pixels de haut et 4 000 pixels de large, chaque pixel contenant les
informations RGB.

source[0,0] renvoie les valeurs de l’élément situé aux coordonnées (0,0), c’est-à-dire en haut à gauche du
tableau. Le pixel à ces coordonnées possède une intensité de rouge de 144, de vert de 191 et de bleu de 221.

6. Écrire une fonction d’en-tête


def conversion(a:np.ndarray) -> image:
qui génère une image en niveaux de gris correspondant à la conversion de l’image en couleurs a.

def conversion(a:np.ndarray) -> image:


H = a.shape[0]
W = a.shape[1]
image = np.empty((H, W)) # On crée une image en niveau de gris
# de la même taille que "a"

for i in range(0, H):


for j in range(0, W):
image[i,j] = gris(a[i,j])

return image

TSI 1 p.1/7
7. Quelle taille de vignette (𝒘 × 𝒉, en pixels) faut-il choisir ? Quelle sera alors la taille en pixels de la
photomosaïque ?

La largeur d’une vignette doit être :

𝑙𝑎𝑟𝑔𝑒𝑢𝑟_𝑡𝑜𝑡𝑎𝑙𝑒 = 2 000 𝑚𝑚
𝑙𝑎𝑟𝑔𝑒𝑢𝑟_𝑡𝑜𝑡𝑎𝑙𝑒 ⋅ 𝑟é𝑠𝑜𝑙𝑢𝑡𝑖𝑜𝑛
𝑤= = 500 𝑝𝑥 𝑎𝑣𝑒𝑐 { 𝑟é𝑠𝑜𝑙𝑢𝑡𝑖𝑜𝑛 = 10 𝑝𝑥 ⁄𝑚𝑚
𝑛𝑜𝑚𝑏𝑟𝑒_𝑣𝑖𝑔𝑛𝑒𝑡𝑡𝑒𝑠
𝑛𝑜𝑚𝑏𝑟𝑒_𝑣𝑖𝑔𝑛𝑒𝑡𝑡𝑒𝑠 = 40

𝑤 4 3
Pour avoir un ratio 4:3, on doit donc avoir = et donc ℎ = 𝑤 = 375 𝑝𝑥 . La photomosaïque dans son
ℎ 3 4
ensemble aura alors une taille 𝑊 = 40 ⋅ 𝑤 = 20 000 𝑝𝑥 et 𝐻 = 40 ⋅ ℎ = 15 000 𝑝𝑥.

8. Écrire une fonction d’en-tête


def proche_voisin(A:image, w:int, h:int) -> image:
qui renvoie une nouvelle image correspondant au redimensionnement de l’image A à la taille 𝒘 × 𝒉
en utilisant l’interpolation au plus proche voisin.

def proche_voisin(A:image, w:int, h:int) -> image:


H = A.shape[0]
W = A.shape[1]
image = np.empty((h, w))

for i in range(0, h):


for j in range(0, w):
image[i,j] = A[int(i*H/h), int(j*W/w)]

return image

9. Sur le code de la question précédente, entourer les opérations élémentaires effectuées par
l’algorithme. En déduire le nombre total d’opérations élémentaires en fonction de 𝒘 et de 𝒉, puis la
complexité temporelle en fonction de 𝒏.

On suppose que np.empty() est une opération élémentaire, et on néglige l’accès à un élément d’un tableau :

def proche_voisin(A:image, w:int, h:int) -> image:


H = A.shape[0]
W = A.shape[1]
image = np.empty((h, w))

for i in range(0, h):


for j in range(0, w):
image[i,j] = A[int(i*H/h), int(j*W/w)]

return image
L’instruction à l’intérieur des doubles s’exécutant ℎ ⋅ 𝑤 fois, on a un total de 4 + 7 ⋅ ℎ ⋅ 𝑤 = 4 + 7 ⋅ 𝑛 opérations
élémentaires, et donc un algorithme de complexité 𝑂(𝑛).

10. Expliquer en quelques lignes son principe de fonctionnement, et en particulier en quoi cette fonction
diffère de la fonction proche_voisin.

Comme la fonction proche_voisin, cette fonction prend comme argument une image initiale et les
dimensions de l’image réduite que l’on souhaite obtenir. Contrairement à proche_voisin qui se contente de
prendre en compte la valeur d’un unique pixel de l’image initiale pour obtenir un pixel de l’image finale, celle-ci
fait la moyenne de tout un bloc de pixels.

TSI 1 p.2/7
Figure 1 : principe de la fonction proche_voisin (à gauche) et moyenne_locale (à droite).

Figure 2 : comparaison entre l'image d'origine (à gauche), l'image réduite par proche_voisin (au centre) et l’image réduite par
moyenne_locale (à droite).

11. Sur le code de la question précédente, entourer les opérations élémentaires effectuées par
l’algorithme. En déduire le nombre total d’opérations élémentaires en fonction de 𝒘 et de 𝒉, puis la
complexité temporelle en fonction de 𝒏.

On suppose que np.empty() est une opération élémentaire, et on néglige l’accès à un élément d’un tableau.
En revanche, la fonction np.mean(A) nécessite en revanche approximativement A.size opérations. Dans ce
cas, cela correspond à 𝑝ℎ ⋅ 𝑝𝑤 opérations :

def moyenne_locale(A:image, w:int, h:int) -> image:


a = np.empty((h, w))
H = A.shape[0]
W = A.shape[1]
ph = H // h
pw = W // w
for I in range(0, H, ph):
for J in range(0, W, pw):
a[I // ph, J // pw] = round(np.mean(A[I:I+ph, J:J+pw]))
return a

𝐻 𝑊
L’instruction à l’intérieur des doubles s’exécutant ⋅ = ℎ ⋅ 𝑤 fois, on a un total de :
𝑝ℎ 𝑝𝑤

8 + ℎ ⋅ 𝑤 ⋅ (3 + 𝑝ℎ ⋅ 𝑝𝑤) = 8 + 𝑛 ⋅ (3 + 𝑝ℎ ⋅ 𝑝𝑤) opérations élémentaires


En supposant les rapports 𝑝ℎ et 𝑝𝑤 constant, on a donc un algorithme de complexité 𝑂(𝑛).

TSI 1 p.3/7
A. OPTIMISATION DE LA RÉDUCTION PAR MOYENNE LOCALE

12. Calculer la table de sommation associée à l’image de la figure 4.

0 0 0 0
0 7 3 7+3
⟶ 0 0 7
= 10
9 2 5
7+9+2 7+3+9+2+5
0 9
= 18 = 26
13. Quel est le plus grand nombre possible qui pourrait apparaître dans la table de sommation d’une
image de 𝟓𝟎 millions de pixels ?

Le plus grand nombre de la table de sommation apparait en bas à droite : il correspond à la somme de tous les
éléments de l’image. Chaque élément pouvant prendre une valeur jusqu’à 255, le plus grand nombre de la table
de sommation est alors 255 ⋅ 50 ⋅ 106 = 12.75 ⋅ 109 .

14. Écrire une fonction de complexité temporelle 𝑶(𝑵) et d’en-tête


def table_sommation(A:image) -> np.ndarray:
qui calcule la table de sommation de l’image A.

Attention, on ne souhaite ni utiliser np.sum(), ni utiliser deux boucles supplémentaires pour calculer chaque
valeur de 𝑆, car l’algorithme serait alors de complexité 𝑂(𝑁 2 ).

def table_sommation(A:image) -> np.ndarray:


H = A.shape[0]
W = A.shape[1]
S = np.zeros((H + 1, W + 1))

for l in range(1, H + 1):


for c in range(1, W + 1):
S[l,c] = S[l-1, c] + S[l, c-1] - S[l-1, c-1] + A[l-1, c-1]

return S

15. Expliquer en quelques lignes son principe de fonctionnement.

La fonction réduction_sommation prend comme arguments l’image initiale A, la table de sommation S et


les dimensions de la nouvelle image w et h, et renvoie l’image finale réduite a. Elle segmente l’image initiale en
fonction de la taille de l’image finale à l’aide de divisions entières et chaque pixel de l’image finale est la moyenne
locale du segment de l’image initiale associé.

Cette moyenne est réalisée à l’aide de la table de sommation qui est manipulée de sorte à obtenir X : la somme
des valeurs comprises dans la zone (de 𝑝ℎ × 𝑝𝑤 pixels de l’image initiale) à réduire en un pixel de l’image finale.
Cette somme est finalement moyennée (en la divisant par nbp : le nombre de pixel compris dans cette zone)
puis affectée à la nouvelle image réduite.

16. Déterminer sa complexité temporelle.

On suppose que np.empty() est une opération élémentaire, et on néglige l’accès à un élément d’un tableau :

def réduction_sommation(A:image, S:np.ndarray, w:int, h:int) -> image:


a = np.empty((h, w))
H = A.shape[0]
W = A.shape[1]
ph = H // h
pw = W // w

TSI 1 p.4/7
nbp = ph * pw
for I in range(0, H, ph):
for J in range(0, W, pw):
X = (S[I+ph, J+pw] - S[I+ph, J]) - (S[I, J+pw] - S[I, J])
a[I // ph, J // pw] = round(X / nbp)
return a

𝐻 𝑊
L’instruction à l’intérieur des doubles s’exécutant ⋅ = ℎ𝑤 fois, on a un total de 9 + 13 ⋅ ℎ𝑤 = 9 + 13 ⋅ 𝑛
𝑝ℎ 𝑝𝑤
opérations élémentaires, et donc un algorithme de complexité 𝑂(𝑛).

17. Discuter des avantages et des cas d’usage respectifs de proche_voisin, moyenne_locale et
réduction_sommation pour redimensionner une image.

La fonction proche_voisin perd de l’information (on ne prend que quelques pixels parmi l’image initiale A).
La fonction moyenne_locale ne fonctionne que si les dimensions de l’image a divisent celles de l’image A.
Finalement, la fonction réduction_sommation permet de réduire la taille de l’image initiale en prenant en
compte l’intégralité de ses éléments, tout fonctionnant même si les dimensions de l’image a ne divisent pas
celles de l’image A.

18. Écrire une fonction d’entête


def init_mosaïque(source:image, w:int, h:int, p:int) -> image:
qui prend en paramètre l’image source, les dimensions w et h d’une vignette et le nombre p de
vignettes par coté. Cette fonction renvoie une version redimensionnée de source, de même taille
que la photomosaïque finale. On rappelle qu’il est possible d’utiliser les fonctions définies
précédemment.

def init_mosaïque(source:image, w:int, h:int, p:int) -> image:


H = source.shape[0]
W = source.shape[1]
S = table_sommation(source)
mosaique = réduction_sommation(source, S, w, h)
return mosaique

19. Écrire une fonction d’en-tête


def L(a:image, b:image) -> int:
qui calcule la distance 𝑳 entre deux images de même taille.

def L(a:image, b:image) -> int:


# Possibilité 1
return np.sum(abs(a - b))

# Possibilité 2
h = a.shape[0]
w = a.shape[1]
resultat = 0

for i in range(0, h):


for j in range(0, w):
resultat += abs(a[i,j] - b[i,j])

return resultat

TSI 1 p.5/7
20. Sans utiliser les fonctions min et max, écrire une fonction d’en-tête
def index_minimum(liste:list[int]) -> int:
qui prend en paramètre une liste de nombres et qui renvoie l’index du plus petit nombre de cette liste
(ou de l’un d’entre eux si plusieurs conviennent).

def index_minimum(liste:list[int]) -> int:


index_min = 0
for i in range(1, len(liste)):
if liste[i] < liste[index_min]:
index_min = i

return index_min

21. Écrire une fonction d’en-tête


def choix_vignette(pavé:image, vignettes:list[image]) -> int:
qui prend en paramètre une image correspondant à un pavé et une liste de vignettes et qui renvoie
l’index 𝒊 tel que L(pavé, vignettes[i]) est minimal (ou l’un d’entre eux si plusieurs vignettes
conviennent). Cette fonction ne doit pas modifier la liste des vignettes.

def choix_vignette(pavé:image, vignettes:list[image]) -> int:


liste_valeurs = []
for i in range(0,len(vignettes)):
liste_valeurs.append(L(pavé, vignettes[i]))

return index_minimum(liste_valeurs)

22. Écrire, à l’aide de ce qui précède, une fonction d’en-tête


def construire_mosaïque(source:image, vignettes:list[image], p:int) -> image:
qui construit une photomosaïque de même rapport 𝒍𝒂𝒓𝒈𝒆𝒖𝒓⁄𝒉𝒂𝒖𝒕𝒆𝒖𝒓 que l’image source
comportant p vignettes par côté.

def construire_mosaïque(source:image, vignettes:list[image], p:int)


h = vignettes[0].shape[0]
w = vignettes[0].shape[1]
mosaique_finale = np.empty((h*p, w*p))
mosaique_initiale = init_mosaïque(source, w, h, p)

for i in range(0,h*p,h):
for j in range(0,w*p,w):
mosaique_finale[i:i+h, j:j+w] =
vignettes[choix_vignette(mosaique_initiale[i:i+h,j:j+w], vignettes)]

return mosaique_finale

23. Déterminer sa complexité temporelle en fonction de la taille 𝒏 = 𝒘 ⋅ 𝒉 des vignettes, du nombre 𝒓


de vignettes dans la mosaïque et de la longueur 𝒒 de la liste vignettes.

La complexité temporelle 𝐶𝑇 de la fonction construire_mosaïque est de :

𝐶𝑇𝑐𝑜𝑛𝑠𝑡𝑟𝑢𝑖𝑟𝑒_𝑚𝑜𝑠𝑎ï𝑞𝑢𝑒 = 𝐶𝑇𝑖𝑛𝑖𝑡_𝑚𝑜𝑠𝑎ï𝑞𝑢𝑒 + 𝑂(𝑟) ⋅ 𝐶𝑇𝑐ℎ𝑜𝑖𝑥_𝑣𝑖𝑔𝑛𝑒𝑡𝑡𝑒


𝑜𝑟 𝐶𝑇𝑖𝑛𝑖𝑡_𝑚𝑜𝑠𝑎ï𝑞𝑢𝑒 = 𝐶𝑇𝑡𝑎𝑏𝑙𝑒_𝑠𝑜𝑚𝑚𝑎𝑡𝑖𝑜𝑛 + 𝐶𝑇𝑟é𝑑𝑢𝑐𝑡𝑖𝑜𝑛_𝑠𝑜𝑚𝑚𝑎𝑡𝑖𝑜𝑛
𝑒𝑡 𝐶𝑇𝑐ℎ𝑜𝑖𝑥_𝑣𝑖𝑔𝑛𝑒𝑡𝑡𝑒 = 𝑂(𝑞)
𝑒𝑡 𝐶𝑇𝑡𝑎𝑏𝑙𝑒_𝑠𝑜𝑚𝑚𝑎𝑡𝑖𝑜𝑛 = 𝑂(𝑁) = 𝑂(𝑛 ⋅ 𝑟)
𝑒𝑡 𝐶𝑇𝑟é𝑑𝑢𝑐𝑡𝑖𝑜𝑛_𝑠𝑜𝑚𝑚𝑎𝑡𝑖𝑜𝑛 = 𝑂(1)

Finalement, on a :

𝐶𝑇𝑐𝑜𝑛𝑠𝑡𝑟𝑢𝑖𝑟𝑒_𝑚𝑜𝑠𝑎ï𝑞𝑢𝑒 = 𝑂(𝑛 ⋅ 𝑟) + 𝑂(1) + 𝑂(𝑟 ⋅ 𝑞) = 𝑂(𝑟 ⋅ (𝑛 + 𝑞))

TSI 1 p.6/7
24. Proposer une stratégie de construction de photomosaïque permettant de sélectionner un maximum
de vignettes différentes et, au cas où une vignette serait réutilisée, d’éviter que les différentes
apparitions de la même vignette se retrouvent trop proches.

Pour sélectionner un maximum de vignettes différentes, on peut définir des groupes de vignettes qui minimisent
la fonction L qui définit la distance entre deux images. Lorsque l’on doit affecter une vignette sur la
photomosaïque, on prend alors la vignette minimisant L sauf si un des proches voisins possède la même
vignette ; dans ce cas, on pioche une autre vignette dans le groupe qui minimisant L.

On pourrait également supprimer de la liste la vignette après l’avoir affectée dans la matrice de la mosaïque.

On pourrait enfin modifier une autre vignette que celle minimisant la fonction L (en augmentant la transparence,
en accentuant la luminosité, …) dans le cas où un des proches voisins possède la même vignette.

25. Implanter cette stratégie sous la forme d’une fonction belle_mosaïque, version améliorée de la
fonction construire_mosaïque, dont on définira les éventuels paramètres supplémentaires.

L’implémentation la plus simple est de supprimer de la liste la vignette utilisée dans la matrice finale.

def belle_mosaïque(source:image, vignettes:list[image], p:int):


h = vignettes[0].shape[0]
w = vignettes[0].shape[1]
mosaique_finale = np.empty((h*p,w*p),np.uint8)
mosaique_initiale = init_mosaïque(source, w, h, p)

for i in range(0,h*p,h):
for j in range(0,w*p,w):
i_vignette = choix_vignette(mosaique_initiale[i:i+h,j:j+w],
vignettes)
mosaique_finale[i:i+h,j:j+w] = vignettes[i_vignette]
vignettes = vignettes.pop(i_vignette)

return mosaique_finale

TSI 1 p.7/7

Vous aimerez peut-être aussi