Consignes

La partie 3 du TP (section 4) est à rendre pour le lundi 14 décembre à 8h00 (matin).

Pour ce TP comme pour les suivants, la pièce la plus importante sera un fichier tp2-nom-prenom.py contenant votre programme. Le seul fait que votre programme fonctionne ne suffit pas pour avoir une bonne note. Les points suivants seront pris en compte :

  1. l'architecture de votre programme (code découpé en fonctions etc.),
  2. la lisibilité de votre programme (choix pertinent pour les noms de variables etc.),
  3. la présence de commentaires aux endroits appropriés,
  4. la présence de documentation pour vos fonctions.

Certaines questions appellent à une réponse que vous pouvez mettre en commentaire dans votre fichier tp2-nom-prenom.py. Si vous le souhaitez vraiment, vous pouvez aussi m'envoyer un rapport de TP. Le format de ce rapport sera au choix, mais par ordre de préférence :

Attention : les points suivants ne rapportent rien, mais ne pas les respecter pourra retrancher jusqu'à 10 points sur la note finale :

Liens utiles


Carte d'altitude du Grand Canyon

Carte d'altitude du Pic Emory (en niveau de gris)

1. Préliminaires : le format « USGS-DEM »

Le format de fichier « USGS-DEM » est un format pour les données altimétriques (DEM : « Digital Elevation Model ») utilisé par l'institut « US Geological Survey ». En gros, chaque fichier contient les données d'altitude pour un morceau de terrain.

Voici par exemple un tels fichier : DEM pour une partie du Grand Canyon. Comme vous pouvez le constater, ce fichier comporte une unique ligne. (Ceci vient du fait que ce format de fichier était surtout utilisé sur des bandes magnétiques, qu'on accédait donc au données de manière purement séquentielle. Découper le fichier en plusieurs lignes n'était pas forcement utile...)

1.1. L'entête « A » du fichier

Le tout début de la ligne contient des informations sur le lieu en question : c'est simplement du texte, et il se trouve entre le début de la ligne et la colonne 134. (Voir la page wikipedia pour un résumé.) Viennent ensuite quelques informations supplémentaires (voir les pages 28 et suivantes du document Standards for Digital Elevation Models pour les détails), puis les coordonnées x (« easting ») et y (« northing ») des coins du rectangle de terrain.

Les informations suivantes sont :

Toutes ces informations sont à des emplacements fixes. Par exemple, l'altitude minimal d'un morceau de terrain se trouve toujours entre les octets 738 et 761.

Cette entête se trouve obligatoirement sur un bloc de 1024 octets. (Ça veut dire par exemple que la fin des 1024 octets est remplie par des espaces...)

Les nombres entiers sont écrits avec la notation usuelle et les nombres flottants utilisent la notation FORTRAN. (Voir plus bas.)

1.2. « Blocs B » du fichier

L'entête « A » se termine à l'octet 915. Viennent ensuite tout un ensemble de « colonnes » de données. Chacune de ces colonnes s'appelle un « bloc B » et commence par quelques informations dont voici les plus importantes :

La suite de chaque « bloc B » (à partir de l'octet 144) contient des entiers pour l'altitude relative à l'altitude du premier point des points de la colonne. Quand toutes les altitudes ont été données, un nouveau « champ B » commence. Ces altitudes sont données en unité delta_z dont la valeur est donnée dans l'entête A du fichier. L'altitude réelle d'un point est donc obtenu par altitude_premier_point + delta_z*alt.

Les détails sur ce « bloc B » sont disponibles aux pages 41 et 42 du document Standards for Digital Elevation Models.

La totalité d'un « bloc B » est stocké sur un nombre d'octets multiple de 1024. Il peut donc y avoir des espaces supplémentaires en fin de « bloc B » avant de passer au « bloc B » suivant (ou au « bloc C »).

1.3. « Bloc C » du fichier

Le fichier se termine éventuellement par un « champs C » que l'on peut ignorer. (Voir p 44 du document Standards for Digital Elevation Models.) Ce bloc est stocké sur 1024 octets.

2. Quelques compléments sur Python, les fichiers etc.

2.1. Fichiers compressés

Vous pouvez choisir les fichiers DEM qui vous intéressent à partir d'ici, ou bien utiliser directement un des fichiers pour le Grand-Canyon :

Pour gagner de la place, les fichiers sont uniquement téléchargeables au format compressé. Pour les utiliser, vous pouvez soit les décompresser à la main ($ gunzip ./grand_canyon-w.gz dans un terminal), ou bien laisser Python le faire pour vous. Cette seconde option est meilleure car si vous voulez traiter de nombreux fichiers, vous n'aurez pas besoin de tous les décompresser à la fois. (Par exemple, pour traiter toute la Californie, les fichiers décompressés utilisent 659Mo alors que les mêmes fichiers compressés n'utilisent que 97Mo.)

Pour utiliser les versions compressés des fichiers, il suffit de remplacer la fonction open() par la fonction gzip.open(). (Il faut donc avoir un import gzip au début de votre fichier.)

2.2. Lire un nombre d'octets donné dans un fichier

Si fd est un descripteur de fichier obtenu grâce à un open ou gzip.open, on peut lire un nombre donné d'octet dans une chaîne de caractères avec la fonction fd.read. Par exemple :

>>> fd = open("./donnees")
>>> huit_octets = fd.read(8)

Pour un fichier texte « normal » octet correspond exactement à un caractère. (Ce n'est plus vrai pour les fichiers avec accents au format UTF8.)

2.3. Le format FORTRAN pour les flottants

Python utilise la « notation scientifique » pour les flottants :

Si une chaîne de caractère contient un tels nombre, on peut la convertir au format flottant de python avec la fonction float(). Par exemple :

>>> s = '314159265e-6'
>>> pi = float(s)
>>> print pi
3.14159265

Le format USGS-DEM utilise la notation FORTRAN, ou l'exposant n'est pas indiqué par la lettre e, mais par la lettre D. Pour convertir une telle chaîne s au format float, il faut donc commencer par remplacer dans s chaque D par un e. Vous pouvez par exemple utiliser la fonction s.replace().

Écrivez le corps de la fonction suivante :

def fortran_float(s):
    """Transforme la chaîne de caractère "s" représentant un flottant au
format FORTRAN en un flottant.
Cette fonction ignore les caractères blancs au début et à la fin de la
chaîne "s"."""
    ...
    ...

2.4. Les tableaux associatifs

En plus des tableaux « normaux », Python offre la possibilité d'avoir des « tableaux associatifs » (aussi appelés « tables de hachages », « tables de hash » ou « map »). Ce sont un peu comme des tableaux, mais où on peut indexer les cases par (presque) n'importe quoi.

On initialise un tableau associatif avec le tableau associatif vide : assoc = {}, et on ajoute / modifie des éléments avec assoc[index] = valeur. Pour obtenir la valeur associée à un index, on peut utiliser assoc[index], mais cela provoque une erreur s'il n'y a pas de valeur stockée à cet index. Le plus simple est d'utiliser assoc.get(index, valeur_par_defaut).

Par exemple :

>>> information = {}
>>> information["alt max"] = 4807
>>> print information
{ 'alt max': 4807 }
>>> h = information["alt max"]
>>> print h
4807
>>> b = information["alt min"]   # Erreur
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'alt min'
>>> b = information.get("alt min",0)
>>> print b
0

2.5. Un fichier Python comme programme principal

2.5.1. Programme principal

Un fichier python peut être utilisé de deux façons : soit comme une bibliothèque (c'est ce qui se passe quand vous faites un import math), soit comme un programme principal (c'est ce qu'il se passe quand vous tapez F5 dans Idle).

Pour pouvoir utiliser un fichier comme bibliothèque, il faut éviter que lors de l'import, votre fichier fasse des choses comme utiliser des variables globales (c'est Mal) ou faire des entrées / sorties (print et consorts). C'est seulement lorsqu'on l'utilise comme programme que votre programme devra faire tout ça.

Une manière de procéder est d'organiser son programme de la manière suivante :

# les bibliothèques dont j'ai besoin
import ...
import ...

# mes fonctions intermédiaires
def rectangle(...):
    ...
    ...

def disque(...):
    ...
    ...

# la fonction principale
if __name__ == "__main__":
    ...
    ...
    ...

La ligne if __name__ == "__main__": permet de tester si le fichier est utilisé comme programme principal (main). Si on l'utilise comme une bibliothèque, alors ce qui se trouve après cette ligne ne sera pas exécuté.

Écrivez votre fichier (ou vos fichiers) de cette manière.

3. Visualisation d'un fichier DEM sous forme d'image

Le but des prochains TP sera de transformer un (ou plusieurs) fichiers au format USGS-DEM en un fichier image comme ceux qui se trouvent au début du sujet.

3.1. Extraction des informations d'un fichier

3.1.1. Entête A

Pour commencer, nous allons juste regarder les entêtes de type A du fichier :

Écrivez la fonction suivante :

def recupere_entete_A(fd):
    """Récupère les informations intéressantes contenues dans l'entête A
d'un fichier DEM.
L'argument "fd" est le descripteur de fichier correspondant au fichier DEM.
La fonction renvoie un tableau associatif contenant au moins les champs
suivants :
 - un champs 'description' (chaîne de caractères)
 - des champs 'coin_NO', 'coin_NE', 'coin_SW' et 'coin_SE' qui contiennent
   les coordonnées (paires de flottants) des 4 coins de la région
   correspondante au fichier
 - des champs 'altitude_max' et 'altitude_min' (flottants)
 - un champs 'nb_colonnes' qui contient le nombre de colonnes de données
   contenu dans la suite du fichier (entier).

À la fin de la fonction, le descripteur de fichier devra être en position
pour commencer à lire l'entête du premier champ B du fichier."""
    ...
    ...
    ...

Le descripteur de fichier fd aura été obtenu grâce à un open() ou un gzip.open(). Remarquez que cette fonction est à la fois une fonction (elle renvoie une valeur) et une procédure (elle modifie le descripteur de fichier). Pour la tester, utiliser votre fichier comme programme principal.

Écrivez la fonction suivante :

def affiche_info_entete_A(info):
    """Affiche de manière lisible l'information de l'entête A d'un fichier DEM.
Les informations sont données en argument à travers le tableau associatif
"info". (Voir la fonction recupere_entete_A() pour le contenu exact de ce tableau.)"""
    ...
    ...
    ...

3.1.2. Blocs de type B

Écrivez la fonction suivante :

def recupere_entete_B(fd):
    """Récupère les informations dans l'entête d'un bloc de type B dans un
fichier DEM. L'argument "fd" est un descripteur de fichier se trouvant au
début d'un bloc de type B.

La fonction renvoie un tableau associatif qui contient au moins les champs
 - 'colonne' pour indiquer le numéro de la colonne contenu dans ce bloc,
 - 'nb_lignes' pour indiquer le nombre de lignes dans ce bloc,
 - 'altitude_initiale' pour l'altitude du premier point de ce bloc,
 - 'coord' pour les coordonnées du premier point de ce bloc,
 - 'altitude_min' et 'altitude_max' pour les min / max locaux à ce bloc.

Après cette fonction, le descripteur de fichier devra se trouver en position
pour commencer à lire les données du bloc."""
    ...
    ...
    ...

Bien sûr, comme précédemment,

Écrivez la fonction suivante :

def affiche_info_entete_B(info):
    """Affiche de manière lisible l'information d'un bloc B d'un fichier DEM.
Les informations sont données en argument à travers le tableau associatif
"info". (Voir la fonction recupere_entete_B() pour le contenu exact de ce tableau.)"""
    ...
    ...
    ...

3.2. Extraction des altitudes

Après chaque « entete B » viennent les altitudes relatives de tous les points de la colonne correspondante. Le nombre de points est donnée dans l'entête B aux octets 12--17. Chaque altitude est un nombre entier, et pour obtenir l'altitude réelle du point, il faut :

  1. multiplier ce nombre par delta_z qui se trouve dans l'entête A, (en général 1 mètre, mais ce n'est pas garanti),
  2. additionner le résultats à l'altitude locale qui se trouve dans l'entête B.

Ceci donne l'altitude réelle du point qui doit donc se trouver entre l'altitude minimale et maximale globales (dans l'entête A) et locales (dans l'entête B).

Écrivez la fonction suivante :

def altitudes_bloc_B(fd, entete_A, entete_B):
    """Récupère les données d'altitude d'un bloc B d'un fichier DEM.
L'argument "fd" est un descripteur de fichier positionné au début des données
du bloc B en question.
L'argument "entete_A" est un tableau associatif contenant les informations de
l'entête du fichier DEM, et l'argument "entete_B" est un tableau associatif
contenant les informations relative à ce bloc.

Cette fonction devra renvoyer un tableau d'altitude réelles : une case par ligne.
Si jamais le bloc contient une incohérence (altitude plus grande que la maximum
local par exemple), la fonction provoquera une erreur.

Après la fonction, le descripteur de fichier devra se trouver au début du bloc B
suivant (s'il existe)."""
    ...
    ...
    ...

Détaillez bien votre fonction en expliquant ce que vous faites et pourquoi vous le faites...

Pour cette fonction, je vous conseille de provoquer une erreur explicative lorsque vous rencontrer une incohérence. Par exemple :

    if alt > alt_min:
        # incohérence !
        raise Exception('Altitude (%f) plus grande que l'altitude minimale locale (%f)' % (alt, alt_min))

Le raise Exception(...) permet de provoquer volontairement une erreur (une « exception »), le 'Altitude (%f) ... permet de donner un message explicatif pour cette exception...

3.3. Image de résolution maximale

Une fois que vous disposer des altitudes réelles d'une colonne, il est assez facile de dessiner les pixels correspondants. Pour commencer, nous allons créer une image de dimension maximale, càd nb_colonne (entete A) par nb_lignes pixels (entete B). Ainsi, le point j dans la colonne i correspondra exactement au pixel (i,j).

Écrivez la fonction

def image_altimetrie_1 (fichier_DEM, nom_image):
    """Cette fonction crée une image (dont le nom est donné par l'argument
"nom_image") des données altimétriques contenues dans le fichier DEM
"fichier_DEM")"""
    ...
    ...
    ...

Bien entendu, cette fonction fera appel aux fonctions définies précédemment...

  1. Pour commencer, créez l'image en n'utilisant que des niveaux de gris : noir (0,0,0) pour le point le plus bas et blanc `(255,255,255)`` pour le point le plus haut. Le points intermédiaires auront un gris (k,k,k) proportionnel à leur dénivelé par rapport au point le plus bas.

  2. Comme il y a de nombreux points dans un fichier, vous pouvez ne parcourir qu'un morceau du fichier : par exemple les 300 premiers points des 300 premières colonnes.

4. Finalisation du programme

Il s'agit du dernier TP : c'est en grande partie sur ce que vous me rendrez que sera basée votre note. N'hésitez pas à améliorer le début de votre programme...

En plus de votre fichier tp2-nom-prenom.py (qui fonctionne), je demande un fichier supplémentaire manuel-nom-prenom. Ce fichier contiendra un petit manuel d'utilisation de votre programme : il décrira

Ce fichier pourra être :

4.1. Améliorations

Écrivez maintenant une fonction image_altimetrie_2 qui permet de créer une image altimétrique à partir d'un fichier DEM mais qui comporte des arguments supplémentaires permettant :

  1. de décider de la taille (en pixels) de l'image finale
  2. de décider d'une altitude minimale / maximale externe (par exemple, même si l'on dessine une carte altimétrique pour un morceau de la France, en pourrait mettre l'altitude maximale à 4810 mètres)
  3. de décider les couleurs qu'on donne aux différentes altitudes. Pour faire ça, on peut donner un tableau de 256 couleurs comme arguments. La couleur 0 correspondra alors au point le plus bas et la couleur 255 au point le plus haut. Pour obtenir des niveaux de gris, il suffit de donner le tableau [(0,0,0), (1,1,1), ..., (255,2 55,255)]. (Les images de type "PNG" ont la possibilité d'avoir une palette, càd un tels tableau stocké dans l'image. On peut alors donner le numéro de la couleur plutôt que la couleur elle même...)

Donnez des valeurs par defaut aux arguments supplémentaires pour que l'on retrouve la fonction image_altimetrie_1 si on ne donne pas les arguments supplémentaires.

Notez bien les points suivants :

  1. donner une taille à votre fonction ne devra pas tronquer l'image. Par exemple avec taille à 256, on doit obtenir une petite image de tout le Grand Canyon (et pas seulement un coin).
  2. les arguments taille, alt_min, alt_max, ... sont tous optionnels : si on ne les fournit pas à la fonction, il faut leur donner des valeur par defaut pertinentes.

4.2. Lancer le programme sans Idle

4.2.1. Ligne de commandes

Si Python est disponible sur l'ordinateur (assez courant), il est possible de lancer votre programme sans passer par Idle (qui n'est pas installé partout). La manière la plus simple de faire ceci est

  1. de rajouter les 2 lignes
    #!/usr/bin/env python
    #encoding: utf8
    
    tout en haut de votre fichier.
  2. de se mettre dans le répertoire qui contient votre fichier tp2-nom-prenom.py, de rendre votre fichier exécutable :
    $ cd info-719/tp2
    $ chmod 755 tp2-hyvernat-pierre.py
    
  3. de l'exécuter (toujours dans le répertoire contenant votre fichier) :
    $ ./tp2-hyvernat-pierre.py
      ...
      ...
    

Le point 2 n'a besoin d'être fait qu'une seule fois.

Si vous copier votre fichier dans un répertoire système, ou si vous modifier votre variable d'environnement $PATH, vous n'avez même pas besoin de vous mettre dans le répertoire qui contient votre fichier...

Essayez de lancer votre programme de cette manière.

Le résultat de l'opération devrait être exactement le même que si vous aviez exécuté votre programme à partir de Idle (touche F5).

4.2.2. Arguments de la ligne de commande

Exemples

Il y a plusieurs avantages à lancer votre programme de cette façon :

  1. l'utilisateur n'a pas besoin d'avoir Idle sur son ordinateur (mais il a quand même besoin de l'interprète Python),
  2. le lancement du programme se fait en une étape au lieu de deux ou trois (lancer Idle, ouvrir votre fichier, l'exécuter),
  3. vous pouvez utiliser des arguments optionnels pour votre programme.

Voici par exemple une exécution de mon programme :

$ ./hyvernat.py -h
Utilisation :
          ./hyvernat.py fichier
pour créer une image à partir du fichiers DEM 'fichier'

Arguments optionnels :
    -h --help        affiche ce message d'aide
    -i --image=nom   spécifie le nom de l'image produite (défaut : fichier.png)
    -x --ecrase      ne renomme pas le fichier image si celui ci risque d'écraser un ficher existant
    --largeur=l      fixe la largeur de l'image
    --amin=a         fixe une altitude minimale globale de 'a'
    --amax=a         fixe une altitude maximale globale de 'a'
    --palette=p      fixe une palette de couleurs pour l'image finale
                     les palettes possibles sont :
                        . 0 : niveaux de gris (defaut)
                        . 1 : niveaux de gris, inverse
                        . 8 : fausses couleurs
                        . le nom d'une image palettisée pour récupérer une palette existante
    -m --marge=m     fixe la taille, en pixels, de la marge
    -v --verbose     affiche un peu plus d'information

Le programme Python hyvernat.py a été lancé avec un unique argument optionnel : -h. Cet argument optionnel provoque juste l'affichage d'un message d'aide qui explique comment on peut utiliser le programme.

Voici un autre exemple d'exécution plus intéressant :

./hyvernat.py -i carte-grand-canyon.png --largeur 256 --marge 10 -v  grand_canyon-w.gz
========================================

Entête A du fichier :
  description : "GRAND CANYON - W                 AZ" -- "NJ12-10W"
  unité de mesure pour les coordonnées : arc-seconde
  unité de mesure pour les altitudes : metre
  coins de la région : (-410400.00,129600.00)--(-410400.00,133200.00)--(-406800.00,133200.00)--(-406800.00,129600.00)
  surface : 3600 x 3600
  angle = 0.000000
  altitude minimale = 278.00
  altitude maximale = 2513.00
  (lignes, colonnes) = (1,1201)
   delta_x = 3, delta_y = 3 et delta_z = 1



Image "carte-grand-canyon.png" créée avec succès en 2.11 secondes.

========================================

Les arguments optionnel ont permis :

Le point important est qu'à aucun moment il n'a été besoin de modifier le fichier hyvernat.py.

Principe de fonctionnement

On peut récupérer les arguments optionnels en utilisant le tableau argv[] qui est initialisé par Python. Dans l'exemple précédent, argv[] prenait la valeur "[ '-i', 'carte-grand-canyon.png', '--largeur', '256', '--marge', '10', '-v', 'grand_canyon-w.gz']".

Pour avoir accès à ce tableau, il faut utiliser la bibliothèque sys :

...
import sys

...
if __name__ == '__main__':
    print sys.argv

Pour traiter « simplement » ces arguments optionnels, le mieux est d'utiliser la fonction getopt de la bibliothèque getopt. Voici par exemple un morceau de mon propre programme :

import getopt
...
opts, args = getopt.getopt(sys.argv[1:], "hi:vm:", ["largeur=", "amin=", "amax=", "marge="])
...

Cette ligne permet de mettre dans le tableau opts les couples (argument, valeur) pour les arguments optionnels -h, -i, -v, -m en --largeur, --amin, --amax, --marge. Les « : » ou « = » permettent de préciser que certains arguments optionnels attentent une valeur juste derrière (cf. l'exemple ci dessus).

Le reste du tableau argv[] se retrouve ensuite dans la variable args. Dans l'exemple ci dessus, args contient donc seulement grand_canyon-w.gz.

Dans l'exemple ci dessus, opts prenait la valeur "[('-i', 'carte-grand-canyon.png'), ('--largeur', '256'), ('--marge', '10'), ('-v', '')]" et args la valeur "['grand_canyon-w.gz']".

Ajouter la possibilité de donner des arguments optionnels à votre programme :

4.3. Coller plusieurs images

Ajoutez la possibilité de coller plusieurs images entre elles, comme pour les exemples au début du sujet de ce TP...

Si vous n'y arrivez pas, essayer de décrire comment vous vous y prendriez.