# Recherche du plus grand carré blanc dans une "image"

## Objectif et déroulement

On considère le problème suivant : étant donné une image monochrome $n \times n$, déterminer le plus grand carré qui ne contient aucun point noir.

Le but du TP est d'établir plusieurs algorithmes permettant de trouver le plus grand carré blanc dans une image.

Déroulement du TP :

- Conception d'un algorithme récursif naïf : ```RechercheCarreBlanc_naif()```
- Programmation dynamique - Utilisation de la mémoïsation : ```RechercheCarreBlanc_memoisation()```
- Programmation dynamique - Optimisation par "Bottom_up" : ```RechercheCarreBlanc_BU()```

# Conception d'un algorithme récursif naïf : ```RechercheCarreBlanc_naif()```

## Une image c'est quoi ?

Une image, c'est un tableau de tuples qui représentent les couleurs des pixels. Dans le cadre de ce TP, nous nous limiterons à des images noir et blanc :

![Image N/B](1.png)

```python
t=[
    [1,0,0,1,0,0,1,0,0,1],
    [0,0,0,0,0,0,1,0,0,0],
    ...
    [0,0,0,0,1,0,0,0,1,0]
]
```

```t[5][1]==1``` car le pixel de la ligne ```5``` et de la colonne ```1``` est noir. (Attention les numéros des lignes/colonnes commencent à 0).

## Etude du problème

L'objectif est de trouver le carré blanc le plus grand possible à partir d'un pixel donné (coin inférieur droit).

- ```PlusGrandCarreBlanc_Rec(lig,col)``` : Retourne la taille du carré blanc à partir du coin inférieur droit (ligne,colonne).

Par ex :

- ```PlusGrandCarreBlanc_Rec(1,2) == 2```
- ```PlusGrandCarreBlanc_Rec(6,4) == 2```

![Image N/B](2.png)

>
> 01. Pour l'image suivante, donner le résultat de :
>
> - ```PlusGrandCarreBlanc_Rec(2,1) == ...```
> - ```PlusGrandCarreBlanc_Rec(1,2) == ...```
> - ```PlusGrandCarreBlanc_Rec(1,1) == ...```
>
>![Image N/B](3.png)
>
> 02. Quelle relation existe-il entre ```PlusGrandCarreBlanc_Rec(2,2)``` et les 3 valeurs précédentes ?
>
> 03. Généraliser ce résultat pour un pixel quelconque :
>
> ```PlusGrandCarreBlanc_Rec(lig,col) = min( .........  ,  .........  ,  ......... ) + .........```
>
> 4. Proposer un algorithme récursif pour ```PlusGrandCarreBlanc_Rec(i,j)``` :
>
> ```python
> def PlusGrandCarreBlanc_Rec(t,lig,col):     # t est le tableau représentant l'image noir et blanc
> 
>     if t(lig, col) == 1:                    # Si le pixel est noir
>         return ...                          # On retourne ... 
> 
>                                             # Sinon :
>     elif ...... :                           # Si le pixel est sur la 1ère ligne ou 1ère colonne
>         return ...                          # On retourne ...
> 
>     else :                                  # Sinon :
>         return ...                          # On retourne 1 + min(PlusGrandCarre des pixels adjacents)
>                                             # Adjacents => à gauche, en haut, en diagonale haut-gauche
> ```
>
> 05. Tester votre algorithme sur le tableau suivant :
>
> ```python
> t=[ [0,0,0,1,0],
>     [0,0,0,0,1],
>     [1,0,0,0,0],
>     [0,0,1,1,0],
>     [0,1,0,0,1] ]
> ```
>
> ```python
> >>> print(PlusGrandCarreBlanc_Rec(t,2,2))
> 2
> ```
>
> **Remarque :** D'autres tableaux sont présent dans le fichier ```TP.py```
>
> **Remarque :** Lors de l'évaluation du minimum par ```min()```, l'interpréteur DOIT calculer les 3 valeurs à comparer. Or, nous pouvons lui éviter dans certains cas ce travail (et économiser un nombre d'appels récursifs). En effet, si la première valeur calculée est nulle alors le ```min(... , ... , ...)``` retournera FORCEMENT 0 et la fonction retournera 1. De même pour la seconde et la troisième.
>
> 6. Modifier ```PlusGrandCarreBlanc_Rec(lig,col)``` en tenant compte de la remarque précédente.
>
> ```python
> def PlusGrandCarreBlanc_Rec(t,lig,col):
>     ...
>     else :                                  
>         g = ...                   # Valeur du pixel de gauche
>         if g==0:
>               return 1
>         h = ...                   # Valeur du pixel du haut
>         if h==0:
>               return 1
>         ...                       # ...
>         return ...                # On retourne 1 + min(g,h,...)
> ```
>
> 7. Ecrire un algorithme simple pour afficher ```t``` à l'aide de ```matplotlib``` et vérifier le bon fonctionnement de votre algorithme.
>
> > **Aide**
> >
> > ```python
> > import matplotlib.pyplot as plt
> > ...
> > plt.imshow(t,cmap='binary')
> > plt.show()
> > ```
>
> 8. Ecrire un algorithme qui parcourt toute l'image et qui retourne la ligne et la colonne du carré le plus grand ainsi que sa taille.
>
>       - ```RechercheCarreBlanc_naif(t)``` : Retourne la ligne et la colonne du coin inférieur droit du carré blanc le plus grand ainsi que sa taille.
>
> ```python
> def RechercheCarreBlanc_naif(t):
>     ...
>     for lig in range(...):
>         for col in range(...):
>             ...
>     return l,c,tmax
> ```
>
> 9. Bonus : Faire en sorte d'afficher ce carré lors de l'affichage de l'image.
>
> 10. Ajouter un compteur (variable global) ```compteur_naif``` permetttant de compter le nombre d'appels récursifs
>
> 11. Ecrire une fonction permettant de générer un tableau de $n\times n$ content un ratio de points noir de ```ratio_noir```%
>
> ```python
> def genere_tab_alea(n, ratio_noir):
>     ...
>     return t
> ```

# Programmation dynamique - Utilisation de la mémoïsation : ```RechercheCarreBlanc_memoisation()```

Remplacons nous dans le cadre d'un cas simple, on cherche ```PlusGrandCarreBlanc_Rec(2,2)```.

![Image N/B](3.png)

> 1. Pendant l'appel de ```PlusGrandCarreBlanc_Rec(2,2)```, quelles sont les 3 appels récursifs qui vont être exécutés ?
>
> - Appel n°1 : ```PlusGrandCarreBlanc_Rec( ... , ... )```
> - Appel n°2 : ```PlusGrandCarreBlanc_Rec( ... , ... )```
> - Appel n°3 : ```PlusGrandCarreBlanc_Rec( ... , ... )```
>
> 2. Compléter le tableau suivant qui liste, pour les appels n°1, n°2 et n°3, les appels récursifs exécutés.
>
> Appel n°1 : ```PGCB_Rec( ... , ... )``` | Appel n°2 : ```PGCB_Rec( ... , ... )``` | Appel n°3 : ```PGCB_Rec( ... , ... )```
> ---------|----------|---------
> ```PGCB_Rec( ... , ... )``` | ```PGrCB_Rec( ... , ... )``` | ```PGCB_Rec( ... , ... )```
> ```PGCB_Rec( ... , ... )``` | ```PGrCB_Rec( ... , ... )``` | ```PGCB_Rec( ... , ... )```
> ```PGCB_Rec( ... , ... )``` | ```PGrCB_Rec( ... , ... )``` | ```PGCB_Rec( ... , ... )```
>
> 3. Quels appels sont redondants ? Proposez une solution pour éviter cela.
>

## Mise en place du tableau de mémoïsation

Pour économiser des appels récursifs, vous allez stocker **au fur et à mesure** les résultats de ces appels dans un tableau de même taille que l'image.

Ce tableau sera initialisé entièrement à ```-1```, ce qui signifie qu'aucun résultat n'a déjà été calculé pour ce pixel. Dans le cas contraire, il faudra stocker le résultat dans ce ```t_memo```

- ```t_memo``` : Tableau de même dimension que ```t``` qui stocke les valeurs de ```PlusGrandCarreBlanc_memo()``` au fur et à mesure des appels récursifs. Ce tableau est une variable globale.

> 4. Initialiser la variable globale ```t_memo```.
>
> **Aide :** En une ligne ... ```t_memo = [[-1 for i in ... ] for j ... ]```
>
> **Remarque :** On peut être malin et faire une copie de ```t``` en remplaceant les ```1``` (pixel noir) par des ```0``` (résultat connu) et les ```0``` (pixels blancs) par des ```-1``` (résultat inconnu)

## Évolution de ```PlusGrandCarreBlanc_Rec()``` en ```PlusGrandCarreBlanc_memo()```

> 5. En vous aidant de  ```PlusGrandCarreBlanc_Rec()```, écrire la fonction ```PlusGrandCarreBlanc_memo()``` en stockant et utilisant les résultats précédamment calculés dans le tableau ```t_memo```

```python
def PlusGrandCarreBlanc_memo(t, lig, col):
    if t_memo[lig][col] != -1:          # Utilisation d'une valeur présente dans t_memo
        return ...
    if t[lig][col] == 1:
    ...
    return ...
```

> 6. Effectuer les tests sur différents pixels d'une image et sur différentes images. Vous pouvez aussi utiliser la fonction ```genere_tab_alea(n, ratio_noir)``` pour valider votre algorithme.

## Pour aller plus vite ... ```RechercheCarreBlanc_memo()``` ?

```PlusGrandCarreBlanc_memo()``` est une fonction équivalente à ```PlusGrandCarreBlanc_Rec()``` mais normalement plus rapide.

Il reste donc à parcourir une image pour trouver les coordonnées du coin inférieur droit du plus grand carré blanc dans l'image.

> 7. Ecrire la fonction ```RechercheCarreBlanc_memo()``` qui retourne les coordonnées du coin inférieur droit du plus grand carré blanc ainsi que sa taille.
>
> ```python
> def RechercheCarreBlanc_memo(t):
>     l, c, tmax = 0, 0, 0
>     ...
>     return l, c, tmax
> ```

## Complexité

> 8. A l'aide du module ```time```, mesurer la différence de performance de l'algorithme naïf et de celui qui utilisa le mémoïsation sur des images de tailles et de "ratio de noir" différents.
>
> $~$ | ```n=10``` / ```ratio=50``` | ```n=30``` / ```ratio=50``` | ```n=50``` / ```ratio=40``` | ```n=70``` / ```ratio=35```
> ---------|----------|---------|----------|----------
> ```RechercheCarreBlanc_Rec``` | $~$ | $~$| $~$| $~$
> ```RechercheCarreBlanc_memo``` | $~$ | $~$| $~$| $~$
>  
> 9. De la même manière que pour l'algorithme naïf, implanter un ```compteur_memo``` qui permettra de compter le nombre d'appels récursifs. Effectuer les tests avec les mêmes paramètres que dans la question précédente.
>
> $~$ | ```n=10``` / ```ratio=50``` | ```n=30``` / ```ratio=50``` | ```n=50``` / ```ratio=40``` | ```n=70``` / ```ratio=35```
> ---------|----------|---------|----------|----------
> ```RechercheCarreBlanc_Rec``` | $~$ | $~$| $~$| $~$
> ```RechercheCarreBlanc_memo``` | $~$ | $~$| $~$| $~$
>
> Uniquement avec ```RechercheCarreBlanc_memo```, mesurer le temps d'exécution de l'algorithme avec ```n=500``` / ```ratio=50``` puis avec ```n=1000``` / ```ratio=50```.
>
> 10. Par combien a été multiplié le temps d'exécution lorsque ```n``` a doublé ? Conjecturer sur la complexité de cet algorithme.
>
> 11. Le gain de performance est-il subtanciel ?
>

![Roadrunner](https://media1.tenor.com/images/272319d1c44ce6047b25e7f266da00a0/tenor.gif)

# Programmation dynamique - Optimisation "Bottom Up" : ```RechercheCarreBlanc_BU()```

L'optimisation "BOTTOM UP", consiste à traiter les problèmes du plus évident (les "plus petits") au moins évident (les "plus gros") en stockant les résultats au fur et à mesure dans un tableau.

Il faut toutefois identifier un ordre dans lequel résoudre les sous-problèmes. Dans notre cas l'ordre est :

- Traiter les problèmes de taille "0" : Les cases noires
- Traiter les problèmes de taille "1" : Les cases sur la première ligne et première colonne
- Traiter les problèmes de taille "2" : ...

Le tableau de "résultats", qui stocke au fur et à mesure les résultats de ````PlusGrandCarreBlanc(lig,col)````, se nommera ```res```.

> 1. Compléter, à la main, le tableau ```res``` pour les problèmes de taille "0".
>
> - Si le ```pixel(lig,col)``` est noir alors ```res[lig][col] = 0```
>

![Image N/B](4.png)

> 2. Compléter, à la main, le précédent tableau ```res``` pour les problèmes de taille "1".
>
> - Si le ```pixel(lig,col)``` est sur la première ligne et/ou colonne alors ```res[lig][col] = 1```
>

![Image N/B](4.png)

> 3. En l'état actuel, peut-on, à l'aide de ```res```, calculer ```PlusGrandCarreBlanc(1,1)``` directement (sans calcul intermédiaire) ? et ```PlusGrandCarreBlanc(2,2)``` ?
>
> 4. Comment faut-il parcourir le tableau pour être sûr qu'à chaque appel, l'algorithme dispose des résultats des problèmes de taille inférieur.
>
> 5. Ecrire un algorithme ```PlusGrandCarreBlanc_BU()``` qui :
>
> - Initialise le tableau ```res``` à -1
> - Complète ```res``` avec les résultats de taille "0" (les cases noires)
> - Complète ```res``` avec les résultats de taille "1" (1ère ligne/colonne)
> - Parcours l'image et calcule les résultats de taille supérieur
> - Et retourne ```res```

**Aide :**

- Remplir un tableau ```res```, qui a la même taille que l'image, et qui est initialisé -1 dans toutes les cases.
- A l'aide de 2 boucles imbriquées sur les lignes et colonnes :
  - Si le ```t[lig][col]``` est noir, affectez ```0``` à ```res[lig][col]```
  - Sinon si ```lig==0``` ou ```col==0```, affectez 1 à ```res[lig][col]```
    - Sinon affectez ```min(res[lig][col-1] , res[lig-1][col-1] , res[lig-1][col] ) + 1``` à ```res[lig][col]```
- Retourner ```res```

```python
def PlusGrandCarreBlanc_BU(t):
    nb_lig, nb_col = ...
    res = [[-1 for i in ...] for j in ...]
    for lig in range(nb_lig):
        for col in range(nb_col):
            ...
    return res
```

## .... et trouver le carré blanc le plus grand (presque à la vitesse de la lumière) : ```RechercheCarreBlanc_BU()```

> 6. Ecrire ```RechercheCarreBlanc_BU()```  qui retourne les coordonnées du coin inférieur droit du plus grand carré blanc ainsi que sa taille à l'aide de l'algorithme précédent.
>
> ```
> def RechercheCarreBlanc_BU(t):
>     l, c, tmax = 0, 0, 0
>     ...
>     return l, c, tmax
> ```

## Comparaison des algorithmes

> 7. Comparer le temps d'éxecution des ces algorithmes.
>
> $~$ | ```n=100``` / ```ratio=30``` | ```n=300``` / ```ratio=20``` | ```n=1000``` / ```ratio=20``` | ```n=3000``` / ```ratio=40```
> ---------|----------|---------|----------|----------
> ```RechercheCarreBlanc_memo``` | $~$ | $~$| $~$| $~$
> ```RechercheCarreBlanc_BU``` | $~$ | $~$| $~$| $~$

## Conclusion

Programmation dynamique = Gain en performance

L'étude théorique d'un problème permet une optimisation précise de l'algorithme qui le résoudra.

![Programmation dynamique](https://www.artelys.com/wp-content/uploads/2018/11/formation4.jpg)