INFO702 - TPs
Loading...
Searching...
No Matches
TP2 Traitement d'images en niveaux de gris et couleurs avec généricité (classes patrons, spécialisation, itérateurs).

( Archive images PPM: https:./images.zip )

1 - Objectifs, pointeurs utiles

L'objectif de ce TP est de vous familiariser avec la programmation générique en C++. A l'issue de cette séance, vous aurez pratiquer:

  • l'écriture de classes génériques via les patrons de classe
  • la définition de concepts et l'écriture de modèle satisfaisant les concepts
  • l'écriture de nouveaux itérateurs.
  • l'écriture de fonctions génériques avec des itérateurs.

Le fil conducteur est l'écriture d'une classe générique pour représenter des images 2D quelconques, c'est-à-dire ici des images couleurs (24bits) et des images en niveaux de gris (8bits). Vous développerez un certain nombre de services autour de ces images:

  • création d'image vide
  • lecture/écriture d'images PGM et PPM (le format le plus simple possible de stockage)
  • inversion des couleurs.
  • extraction des composantes rouges, vertes ou bleues.
  • vision "vieille télé" des images
  • histogramme(s) de l'image
  • correction de contraste d'images couleurs et niveaux de gris par égalisation d'histogramme(s)

Les sites suivants pourront être utile pendant le TP:

Note: Tout bon logiciel de manipulation d'image peut importer ou exporter du portable pixmap (PBM, PGM, PPM). Le logiciel ImageMagick (commandes convert et display notamment) est bien pratique pour convertir en ligne de commande une image:

   prompt> convert toto.jpg toto.ppm
   prompt> display toto.ppm 

2 - Définition d'une classe pour représenter des images arbitraires

Nous cherchons à représenter des images dont le type des valeurs peut être défini à l'instanciation. Par exemple, on voudra écrire:

Image2D<unsigned char> img_niv_de_gris( 256, 256, 0 ); // image en niveaux de gris, noire au début
Image2D<Color> img_couleur( 512, 512, Color(0,255,0) ); // image en couleur, verte au début
Classe générique pour représenter des images 2D.
Definition Image2D.hpp:8
Definition Color.hpp:9

Nous allons utiliser les patrons de classe avec le type de la valeur de chaque pixel comme paramètre. Comme dans le TP1 (TP1 Traitement d'images en niveaux de gris (POO simple, surcharge d'opérateurs, entrées/sorties).), on utilisera le conteneur std::vector pour stocker le tableau de taille W*H, avec W et H la largeur et la hauteur de l'image. Un extrait du fichier entête donne:

// file Image2D.hpp
#ifndef _IMAGE2D_HPP_
#define _IMAGE2D_HPP_
#include <vector>
/// Classe générique pour représenter des images 2D.
template <typename TValue>
class Image2D {
public:
typedef Image2D<TValue> Self; // le type de *this
typedef TValue Value; // le type pour la valeur des pixels
typedef std::vector<Value> Container; // le type pour stocker les valeurs des pixels de l'image.
// Constructeur par défaut
// Constructeur avec taille w x h. Remplit tout avec la valeur g
// (par défaut celle donnée par le constructeur par défaut).
Image2D( int w, int h, Value g = Value() );
// Remplit l'image avec la valeur \a g.
void fill( Value g );
/// @return la largeur de l'image.
int w() const;
/// @return la hauteur de l'image.
int h() const;
/// Accesseur read-only à la valeur d'un pixel.
/// @return la valeur du pixel(i,j)
Value at( int i, int j ) const;
/// Accesseur read-write à la valeur d'un pixel.
/// @return une référence à la valeur du pixel(i,j)
Value& at( int i, int j );
private:
Container m_data; // mes données; évitera de faire les allocations dynamiques
int m_width; // ma largeur
int m_height; // ma hauteur
/// @return l'index du pixel (x,y) dans le tableau \red m_data.
int index( int i, int j ) const;
};
#endif // _IMAGE2D_HPP_
int h() const
Value at(int i, int j) const
int w() const

On utilisera la classe std::vector de la STL. Cela nous évitera toute allocation dynamique ! Il suffit soit de l'initialiser avec la bonne taille (i.e. donc largeur * hauteur), soit d'appeler vector::resize pour changer la taille. Ecrivez donc cette première version de la classe Image2D, avec les deux constructeurs précisés. Faut-il réécrire le constructeur par copie et l'affectation ?

Note
Faites des copier/coller avec votre classe GrayLevelImage2D du TP précédent.
les méthodes seront aussi écrites dans le fichier entête Image2D.hpp, soit directement, soit après la définition de la classe. Par exemple, on pourra écrire:
int w() const { return m_width; }

On testera les méthodes précédents en créant un fichier testGrayLevelImage2D.cpp avec (au moins) les lignes suivantes.

#include <iostream>
#include <fstream>
#include "Image2D.hpp"
using namespace std;
int main( int argc, char** argv )
{
typedef unsigned char GrayLevel;
GrayLevelImage2D img( 8, 8, 5 ); // imagette 8x8 remplie de 5
for ( int y = 0; y < img.h(); ++y )
{
for ( int x = 0; x < img.w(); ++x )
std::cout << " " << (int) img.at( x, y ); // la conversion permet de voir les caractères sous forme d'entiers.
std::cout << std::endl;
}
return 0;
}

Vous pourrez utiliser le Makefile suivant:

LD=g++
CXX=g++ -g -c

EXEC_SRC=\
	testGrayLevelImage2D.cpp

MODULE_SRC=\

MODULE_OBJ=${MODULE_SRC:.cpp=.o}
EXEC_OBJ=${EXEC_SRC:.cpp=.o}

EXEC_PRG=${EXEC_SRC:.cpp=}

all: ${EXEC_PRG} ${MODULE_OBJ} ${EXEC_OBJ}

testGrayLevelImage2D: testGrayLevelImage2D.o ${MODULE_OBJ}
	${LD} ${MODULE_OBJ} $< -o $@

.cpp.o: 
	${CXX} $<

clean:
	rm -f ${EXEC_PRG} ${MODULE_OBJ} ${EXEC_OBJ}

Il vous suffira alors à vous de taper la commande make dans votre terminal pour que tout soit compilé. L'exécution affiche

   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5 
   5 5 5 5 5 5 5 5

En un sens, vous avez transformé votre classe non générique GrayLevelImage2D en une classe générique Image2D, puis vous avez paramétré Image2D de manière à représenter des images en niveaux de gris. Pour le moment, vous devez avoir l'impression d'avoir fait du boulot pour pas grand chose. C'est normal, mais ça va s'arranger.

3 - Introduction des images couleurs.

On va instancier une image couleur. Pour ce faire, il faut avoir une classe qui représente une couleur. C'est la classe Color (fichier Color.hpp) donnée ci-dessous.

#ifndef _COLOR_HPP_
#define _COLOR_HPP_
/**
Représente une couleur avec un codage RGB. Ce codage utilise 3
octets, le premier octet code l'intensité du rouge, le deuxième
l'intensité du vert, le troisième l'intensité du bleu.
*/
struct Color {
typedef unsigned char Byte;
/// Code les 3 canaux RGB sur 3 octets.
Byte red, green, blue;
Color() {}
/// Crée la couleur spécifiée par (_red,_green,_blue).
Color( Byte _red, Byte _green, Byte _blue )
: red( _red ), green( _green ), blue( _blue ) {}
/// @return l'intensité de rouge (entre 0.0 et 1.0)
float r() const { return ( (float) red ) / 255.0; }
/// @return l'intensité de vert (entre 0.0 et 1.0)
float g() const { return ( (float) green ) / 255.0; }
/// @return l'intensité de bleu (entre 0.0 et 1.0)
float b() const { return ( (float) blue ) / 255.0; }
/// Sert à désigner un canal.
enum Channel { Red, Green, Blue };
/// @return le canal le plus intense.
Channel argmax() const
{
if ( red >= green ) return red >= blue ? Red : Blue;
else return green >= blue ? Green : Blue;
}
/// @return l'intensité maximale des canaux
float max() const { return std::max( std::max( r(), g() ), b() ); }
/// @return l'intensité minimale des canaux
float min() const { return std::min( std::min( r(), g() ), b() ); }
/**
Convertit la couleur RGB en le modèle HSV (TSV en français).
@param h la teinte de la couleur (entre 0 et 359), hue en anglais.
@param s la saturation de la couleur (entre 0.0 et 1.0)
@param v la valeur ou brillance de la couleur (entre 0.0 et 1.0).
*/
void getHSV( int & h, float & s, float & v ) const
{
// Taking care of hue
if ( max() == min() ) h = 0;
else {
switch ( argmax() ) {
case Red: h = ( (int) ( 60.0 * ( g() - b() ) / ( max() - min() ) + 360.0 ) ) % 360;
break;
case Green: h = ( (int) ( 60.0 * ( b() - r() ) / ( max() - min() ) + 120.0 ) );
break;
case Blue: h = ( (int) ( 60.0 * ( r() - g() ) / ( max() - min() ) + 240.0 ) );
break;
}
}
// Taking care of saturation
s = max() == 0.0 ? 0.0 : 1.0 - min() / max();
// Taking care of value
v = max();
}
/**
TODO: Convertit la couleur donnée avec le modèle HSV (TSV en
français) en une couleur RGB.
*/
void setHSV( int h, float s, float v )
{}
};
#endif //_COLOR_HPP_
float g() const
Definition Color.hpp:23
float b() const
Definition Color.hpp:25
Channel
Sert à désigner un canal.
Definition Color.hpp:28
float max() const
Definition Color.hpp:36
Channel argmax() const
Definition Color.hpp:30
float r() const
Definition Color.hpp:21
void setHSV(int h, float s, float v)
Definition Color.hpp:68
void getHSV(int &h, float &s, float &v) const
Definition Color.hpp:45
float min() const
Definition Color.hpp:38
Byte red
Code les 3 canaux RGB sur 3 octets.
Definition Color.hpp:13

Ecrivez un petit programme de test testColorImage2D.cpp sur le modèle de testGrayLevelImage2D.cpp afin de tester l'instanciation d'une image couleur sous la forme

#include "Color.hpp"
//...
typedef Image2D<Color> ColorImage2D;
ColorImage2D img( 8, 8, Color( 255, 0, 255 ) ); // imagette 8x8 remplie de magenta
//...

4 - Premier itérateur sur images quelconques.

On repart du modèle des itérateurs que l'on avait fait au TP1, c'est-à-dire qu'on va définir un itérateur dans une image comme une classe dérivée d'un itérateur sur un std::vector. Cela donne ce genre de code:

/// Un itérateur (non-constant) simple sur l'image.
struct Iterator : public Container::iterator {
Iterator( Self & image, int x, int y )
: Container::iterator( image.m_data.begin() + image.index( x, y ) )
{}
};

On rajoute aussi les méthodes usuelles begin, end, start.

/// @return un itérateur pointant sur le début de l'image
Iterator begin() { return start( 0, 0 ); }
/// @return un itérateur pointant après la fin de l'image
Iterator end() { return start( 0, h() ); }
/// @return un itérateur pointant sur le pixel (x,y).
Iterator start( int x, int y ) { return Iterator( *this, x, y ); }

Maintenant le code suivant vous génère une image avec pleins de couleurs et l'exporte en PPM.

// file testColorImage2D.cpp
#include <iostream>
#include <fstream>
#include "Image2D.hpp"
#include "Color.hpp"
int main()
{
typedef Image2D<Color> ColorImage2D;
typedef ColorImage2D::Iterator Iterator;
ColorImage2D img( 256, 256, Color( 0, 0, 0 ) );
Iterator it = img.begin();
for ( int y = 0; y < 256; ++y )
for ( int x = 0; x < 256; ++x )
{
*it++ = Color( y, x, (2*x+2*y) % 256 );
}
// ios::binary est nécessaire pour les systèmes Windows, optionnel
// sur les autres systèmes.
std::ofstream output( "colors.ppm", ios::binary );
output << "P6" << std::endl; // PPM raw
output << "# Generated by You !" << std::endl;
output << img.w() << " " << img.h() << std::endl;
output << "255" << std::endl;
for ( Iterator it = img.begin(), itE = img.end(); it != itE; ++it ) // (*)
{
Color c = *it;
// c.red, c.green et c.blue sont des `unsigned char`, donc la ligne ci-dessous
// écrit 3 octets sur le flux de sortie `output`.
output << c.red << c.green << c.blue;
}
output.close();
return 0;
}

qui doit vous afficher:

Votre première image couleur.

Ecrivez ensuite une version const de la classe Image2D::Iterator. Vous appelerez cette classe Image2D::ConstIterator et vous écrirez alors les versions const des méthodes begin, end, start.

Note
La classe Image2D::ConstIterator dérive de Container::const_iterator (donc des itérateurs non mutables de la classe vector<T>).

Notez maintenant que la boucle (*) du programme précédent peut être réécrite avec ConstIterator sous la forme suivante:

const ColorImage2D& cimg = img; // Vue "constante" sur l'image img.
for ( ConstIterator it = cimg.begin(), itE = cimg.end(); it != itE; ++it ) // (*)
{
Color c = *it;
output << c.red << c.green << c.blue;
}
Note
A partir de C++11, on écrirait aussi les méthodes cbegin() et cend() (et cstart ici) pour récupérer un const-itérateur, que l'on utiliserait si l'on veut forcer leur usage.
// 2eme façon (plus C++11)
for ( ConstIterator it = img.cbegin(), itE = img.cend(); it != itE; ++it ) // (*)
{
Color c = *it;
output << c.red << c.green << c.blue;
}
// 3eme façon avec (:)
for ( const auto c : img )
{
output << c.red << c.green << c.blue;
}

5 - Un importeur / exporteur PBM générique

Il est clair qu'on ne peut pas écrire le même code pour lire/écrire du PGM (image niveaux de gris) ou du PPM (image couleur). Cela va a priori à l'encontre de la généricité de Image2D. Ce n'est pas un problème ici, on va juste utiliser la spécialisation des classes. Comme on ne veut pas spécialiser tout Image2D, on va plutôt créer des classes à part pour importer ou exporter les images. Par exemple, pour exporter/sauvegarder une image, on définit une classe et ses spécialisations:

template <typename TValue> struct Image2DWriter { ... };
// spécialisée ensuite
template <> struct Image2DWriter<unsigned char> { ... }; // PGM
template <> struct Image2DWriter<Color> { ... }; // PPM

Plus précisément, complétez le code suivant

#ifndef _IMAGE2DWRITER_HPP_
#define _IMAGE2DWRITER_HPP_
#include <iostream>
#include <string>
#include "Color.hpp"
#include "Image2D.hpp"
template <typename TValue>
class Image2DWriter {
public:
typedef TValue Value;
typedef Image2D<Value> Image;
static bool write( const Image & img, std::ostream & output, bool ascii )
{
std::cerr << "[Image2DWriter<TValue>::write] NOT IMPLEMENTED." << std::endl;
return false;
}
};
/// Specialization for gray-level images.
template <>
class Image2DWriter<unsigned char> {
public:
typedef unsigned char Value;
typedef Image2D<Value> Image;
static bool write( const Image & img, std::ostream & output, bool ascii )
{
// Reprenez votre code du TP1.
}
};
/// Specialization for color images.
template <>
class Image2DWriter<Color> {
public:
typedef Color Value;
typedef Image2D<Value> Image;
static bool write( const Image & img, std::ostream & output, bool ascii )
{
// Reprenez la partie sauvegarde de l'exemple précédent testColorImage2D.cpp
}
};
#endif // _IMAGE2DWRITER_HPP_

Maintenant, dans l'exemple testColorImage2D.cpp, vous pouvez sauvegarder l'image couleur "colors.ppm" ainsi:

ofstream output( "colors.ppm" );
bool ok2 = Image2DWriter<Color>::write( img, output, false );
if ( !ok2 ) {
std::cerr << "Error writing output file." << std::endl;
return 1;
}
output.close();

Il ne vous reste plus qu'à écrire la classe Image2DReader pour lire/importer une image en niveaux de gris ou une image couleur. On procède exactement de la même manière.

Warning
Attention, il faut passer l'image en référence dans static bool Image2DReader<TValue>::read(Image & img, std::istream & input ), i.e. non const, car vous allez modifier cette image dans cette méthode de classe.

6 Premier test: on inverse les canaux rouge et bleu

Comme on peut lire/écrire des images PPM, on peut maintenant faire du traitement d'image. Ecrivez un petit programme invert-red-blue.cpp en ligne de commande qui inverse les canaux rouge et bleu d'une image donnée en paramètre, puis sauve le résultat. On l'appelerait ainsi:

prompt$ g++ invert-red-blue.cpp -o invert-red-blue
prompt$ ./invert-red-blue kowloon.ppm kowloon-inv.ppm

L'idée est de parcourir l'image par pixel par pixel, et d'inverser les canaux rouge et bleu de chaque pixel. Cela donne:

kowloon.ppm kowloon-inv.ppm

On note ici que la généricité n'est pas essentielle, elle a juste permis de factoriser le code pour représenter les images en niveaux de gris et couleur.

7 On rajoute les accesseurs et un itérateur générique

Il est délicat de manipuler les images couleur de la même façon que les images en niveaux de gris, car l'information couleur a plus de degrés de liberté que l'information niveaux de gris. Pourtant, on peut aussi "linéariser" la couleur (i.e. la voir comme un axe) de différentes façons:

  • on ne regarde qu'une composante (rouge, vert, bleu). Dans ce cas, la composante concernée est une sorte d'image en niveaux de gris
  • on ne regarde que la valeur de la couleur dans le modèle TSV de couleur. Dans ce cas là, on ne regarde que la brillance de la couleur, pas sa teinte.
  • on pourrait regarder d'autres modèles de couleur et ne garder qu'un seul axe.

Or, nos itérateurs (ConstIterator et Iterator) sont un peu simplet. Leur valeur pointée est toujours une couleur. On va définir des itérateurs sélectifs qui ne voient qu'une composante de la couleur (en lecture et en écriture). Plutôt que de les réécrire à chaque fois, on définit la notion d'accesseur, puis nos nouveaux itérateurs seront paramétrés par un accesseur.

Dans ce TP, nos accesseurs n'auront même pas besoin d'instance pour fonctionner (e.g., sélectionner le canal rouge ne nécessite pas d'information propre). Leur méthodes access seront donc des méthodes de classe (mot-clé static) et non d'instance. Voilà ci-dessous deux exemples d'accesseur:

#ifndef _ACCESSOR_HPP_
#define _ACCESSOR_HPP_
#include "Color.hpp"
/// Accesseur trivial générique
template <typename TValue>
struct TrivialAccessor {
typedef TValue Value;
typedef Value Argument;
typedef Value& Reference;
// Acces en lecture.
static Value access( const Argument & arg )
{ return arg; }
// Acces en écriture.
static Reference access( Argument & arg )
{ return arg; }
};
// Accesseur trivial pour une image en niveaux de gris
typedef TrivialAccessor<unsigned char> GrayLevelTrivialAccessor;
// Accesseur trivial pour une image en couleur.
typedef TrivialAccessor<Color> ColorTrivialAccessor;
/// Accesseur à la composante verte.
struct ColorGreenAccessor {
typedef unsigned char Value;
typedef Color Argument;
/// Même astuce que pour les références à un bit dans un tableau de bool.
struct ColorGreenReference {
Argument & arg;
ColorGreenReference( Argument & someArg ) : arg( someArg ) {}
// Accesseur lvalue (écriture)
// permet d'écrire *it = 120 pour changer l'intensité du vert
ColorGreenReference& operator=( Value val )
{
arg.green = val;
return *this;
}
// Accesseur rvalue (lecture)
// permet d'écrire *it pour récupérer l'intensité du vert
operator Value() const
{
return arg.green; // arg.green est de type Value.
}
};
typedef ColorGreenReference Reference;
// Acces en lecture.
static Value access( const Argument & arg )
{ return arg.green; }
// Acces en écriture.
static Reference access( Argument & arg )
{ return ColorGreenReference( arg ); }
};
#endif // _ACCESSOR_HPP_
Note
Observez-bien l'accesseur à la composante verte (ColorGreenAccessor). Cet accesseur nous permettra de tranformer un itérateur it sur image de couleur (donc *it est la couleur de pixel, de type Color) en un itérateur sur image couleur mais où it est la composante verte du pixel, de type unsigned char.

Comment se servir des accesseurs dans les itérateurs ? On va "simplement" définir un itérateur générique (paramétré par un type Accessor) qui utilise l'accesseur au moment du déréférencement (operator*). Un extrait du code donne donc:

template <typename TValue>
struct Image2D {
...
template <typename TAccessor>
struct GenericConstIterator : public Container::const_iterator {
typedef TAccessor Accessor;
typedef typename Accessor::Argument ImageValue; // Color ou unsigned char
typedef typename Accessor::Value Value; // unsigned char (pour ColorGreenAccessor)
typedef typename Accessor::Reference Reference; // ColorGreenReference (pour ColorGreenAccessor)
GenericConstIterator( const Image2D<ImageValue>& image, int x, int y );
// Accès en lecture (rvalue)
Value operator*() const
{ return Accessor::access( Container::const_iterator::operator*() ); } //< Appel de op* de l'térateur de vector
};
...

Complétez le constructeur de GenericConstIterator (similaire à Iterator). Ensuite surchargez les méthodes begin, end, start de Image2D avec des patrons de méthodes. Par exemple, un start générique peut s'écrire ainsi:

template <typename Accessor>
GenericConstIterator< Accessor > start( int x = 0, int y = 0 ) const
{ return GenericConstIterator< Accessor >( *this, x, y ); }

Le code suivant doit maintenant fonctionner. Il charge une image couleur et sauve sa composante verte sous forme d'image en niveaux de gris.

// save-green-channel.cpp
#include <cmath>
#include <iostream>
#include <fstream>
#include "Image2D.hpp"
#include "Image2DReader.hpp"
#include "Image2DWriter.hpp"
#include "Accessor.hpp"
int main( int argc, char** argv )
{
typedef Image2D<Color> ColorImage2D;
typedef Image2DReader<Color> ColorImage2DReader;
typedef ColorImage2D::Iterator ColorIterator;
if ( argc < 3 )
{
std::cerr << "Usage: save-green-channel <input.ppm> <output.pgm>" << std::endl;
return 0;
}
ColorImage2D img;
std::ifstream input( argv[1] ); // récupère le 1er argument.
bool ok = ColorImage2DReader::read( img, input );
if ( !ok )
{
std::cerr << "Error reading input file." << std::endl;
return 1;
}
input.close();
typedef Image2DWriter<unsigned char> GrayLevelImage2DWriter;
typedef GrayLevelImage2D::Iterator GrayLevelIterator;
GrayLevelImage2D img2( img.w(), img.h() );
//-----------------------------------------------------------------------------
// vvvvvvvvv Toute la transformation couleur -> canal vert est ici vvvvvvvvvvvv
//
// Servira à parcourir la composante verte de l'image couleur.
typedef ColorImage2D::GenericConstIterator< ColorGreenAccessor > ColorGreenConstIterator;
// Notez comment on appelle la méthode \b générique `begin` de `Image2D`.
ColorGreenConstIterator itGreen = img.begin< ColorGreenAccessor >();
// On écrit la composante verte dans l'image en niveaux de gris.
for ( GrayLevelIterator it = img2.begin(), itE = img2.end();
it != itE; ++it )
{
*it = *itGreen;
++itGreen;
// NB: si on veut faire *itGreen++, il faut redéfinir GenericConstIterator<T>::operator++(int).
}
//-----------------------------------------------------------------------------
std::ofstream output( argv[2] ); // récupère le 2eme argument.
bool ok2 = GrayLevelImage2DWriter::write( img2, output, false );
if ( !ok2 )
{
std::cerr << "Error writing output file." << std::endl;
return 1;
}
output.close();
return 0;
}
prompt$ ./save-green-channel papillon.ppm papillon-green.pgm
Image papillon.ppm Sa composante verte vue comme une image en niveaux de gris
Note
La pré-incrémentation seule ++itGreen marche car on n'exploite pas la valeur de l'itérateur. On peut donc directement appeler cet opérateur écrit dans la super-classe Container::const_iterator.
Si on veut écrire une expression comme *itGreen++ alors l'opérateur de post-incrémentation (operator++(int)) par défaut de Container::const_iterator n'est pas correct (il retourne un Container::const_iterator au lieu d'un GenericConstIterator). On écrirait donc les lignes suivantes:
// Post-incrémentation de l'itérateur.
GenericConstIterator operator++( int /* dummy_parameter */ )
{
GenericConstIterator tmp = *this; // sauve la position courante.
Container::const_iterator::operator++(); // avance avec Container::const_iterator::op++()
return tmp; // retourne la position précédente
}

8 Créer les nouveaux accesseurs à la composante rouge et bleue

Ecrivez maintenant deux nouvelles classes ColorRedAccessor et ColorBlueAccessor. Ecrire alors un nouveau programme save-channels.cpp qui prend une image en entrée et sauvegarde 3 images en sortie, une par composante couleur. Ainsi,

prompt$ ./save-channels papillon.ppm
Image papillon.ppm Sa composante rouge (papillon_red.pgm)
Sa composante verte (papillon_green.pgm)Sa composante bleue (papillon_blue.pgm)

Les insectes butineurs (abeilles, guêpes, papillons) voient beaucoup mieux le bleu que nous (et d'ailleurs ils voient aussi un peu dans l'ultra-violet). L'image en composante bleue montre combien les fleurs (violettes) ressortent par rapport au fond vert, ceci afin d'attirer les insectes butineurs.

9 Itérateurs génériques non constants

Nous avons fait l'itérateur générique en lecture. Il faut faire maintenant l'itérateur générique en lecture/écriture (i.e. non const). Ce n'est pas difficile, il suffit de créer une classe générique GenericIterator sur le même modèle que GenericConstIterator, mais en dérivant cette fois de Container::iterator. L'opérateur de déréférencement pour les lvalue (lecture/écriture) s'écrit ainsi:

// Accès en écriture (lvalue)
Reference operator*()
{ return Accessor::access( Container::iterator::operator*() ); }

La seule différence avec l'opérateur lecture seule est le type retourné. Il ne vous reste plus qu'à écrire les méthodes génériques begin, end, et start sur le même modèle que précédemment.

A titre d'illustration, l'extrait de code suivant transforme une image couleur de façon à donner l'illusion de la voir sur les vieux écrans cathodiques couleur. Sur ces écrans, on voyait nettement que les composantes rouge, vert et bleu étaient affichées côte à côte. Le code visite l'image couleur, et ne garde qu'une composante à chaque pixel, en alternant rouge, vert et bleu, les autres étant mises à zéro.

typedef ColorImage2D::GenericIterator< ColorRedAccessor > ColorRedConstIterator;
typedef ColorImage2D::GenericIterator< ColorGreenAccessor > ColorGreenConstIterator;
typedef ColorImage2D::GenericIterator< ColorBlueAccessor > ColorBlueConstIterator;
ColorRedConstIterator itRed = img.begin< ColorRedAccessor >();
ColorGreenConstIterator itGreen = img.begin< ColorGreenAccessor >();
ColorBlueConstIterator itBlue = img.begin< ColorBlueAccessor >();
// On écrit la composante verte dans l'image en niveaux de gris.
int x = 0;
for ( ColorIterator it = img.begin(), itE = img.end();
it != itE; ++it )
{
switch ( x % 3 ) {
case 0: *itGreen = *itBlue = 0; break;
case 1: *itRed = *itBlue = 0; break;
case 2: *itRed = *itGreen = 0; break;
}
++itRed; ++itGreen; ++itBlue;
x = ( x+1 ) % img.w();
}

Cela donne pour l'image kowloon.ppm:

Image kowloon comme sur les vieux écrans Zoom dessus

Vérifiez donc que vos itérateurs fonctionnent correctement en créant ce petit programme.

10 Espace TSV (HSV) et histogramme d'une image couleur

Au TP précédent, vous avez vu comment calculer l'histogramme d'une image en niveaux de gris (voir 7 - Histogramme et Histogramme cumulé d'une image.). Qu'en est-il d'une image couleur ? Tel quel, cela n'a pas vraiment de sens de calculer l'histogramme dans l'espace RGB. En revanche, cela a plus de sens dans un autre espace de couleurs appelé Teinte Saturation Valeur (TSV) ou Hue Saturation Value (HSV) en anglais. La page [http://fr.wikipedia.org/wiki/Teinte_Saturation_Valeur Wikipedia TSV] décrit cet espace de couleur ainsi que les fonctions pour transformer RGB vers TSV et réciproquement.

Nous allons définir l'histogramme d'une image couleur comme l'histogramme des valeurs (au sens de TSV) des pixels. Pour ce faire, il faut d'abord enrichir la classe Color, car elle ne comporte la méthode Color::getHSV pour convertir RGB vers TSV, mais pas l'inverse. Ecrivez donc la méthode de Color qui réalise la transformation TSV vers RGB, dont le prototype sera:

void Color::setHSV( int h, float s, float v ) { ... };

Ensuite, nous pouvons maintenant écrire un nouvel accesseur vers la valeur d'une couleur. Sa structure est ainsi, qu'il s'agit de compléter:

struct ColorValueAccessor {
typedef unsigned char Value;
typedef Color Argument;
struct ColorValueReference {
// Référence vers la variable Color donnée à l'objet à sa construction
Argument & arg;
/// Constructeur permettant à cet objet de référencer la variable
/// Color donnée en paramètre.
ColorValueReference( Argument & someArg ) : arg( someArg ) {}
// Cette fonction sera appelée lors d'un `*it = ...`.
// S'occupe de changer la valeur de la couleur arg
// en fonction de la valeur donnée val.
// Il faut utiliser arg.getHSV et arg.setHSV.
ColorValueReference& operator=( Value val )
{ ... }
// S'occupe de retourner la valeur de la couleur arg (sans la changer).
// Un simple appel à arg.getHSV suffira.
operator Value() const
{ ... }
};
typedef ColorValueReference Reference;
// Il s'agit d'un simple accès en lecture à la valeur de la couleur arg.
// Un simple appel à arg.getHSV suffira.
static Value access( const Argument & arg )
{ ... }
// Il suffit de créer et retourner un objet de type ColorValueReference référençant arg.
static Reference access( Argument & arg )
{ ... }
};

Il reste maintenant à écrire l'algorithme qui calcule l'histogramme et l'histogramme cumulé des valeurs de l'image couleur. Ecrivez donc une classe Histogramme qui contiendra deux tableaux de 256 entrées double, l'un pour l'histogramme \(h_I\), l'autre pour l'histograme cumulé \(H_I\). Pour calculer l'histogramme sur des images arbitraires, cette classe aura une méthode générique

template <typename InputIterator> void init( InputIterator it, InputIterator itE ) { ... }

qui parcourera l'intervalle [it,itE) pour calculer son histogramme et son histogramme cumulé. Ainsi on pourra se servir des histogrammes indifféremment pour les images couleurs et niveaux de gris, comme ci-dessous:

Histogramme H;
GrayLevelImage2D img( 512, 512 );
H.init( img.begin(), img.end() ); // fonctionne, car l'itérateur a une valeur de type unsigned char.
ColorImage2D img2( 512, 512 );
H.init( img2.begin< ColorValueAccessor >(),
img2.end< ColorValueAccessor >() ); // fonctionne, car l'itérateur a une valeur de type unsigned char.

Ecrivez maintenant le programme histogramme qui prend en entrée une image couleur et qui sauvegarde une image en niveaux de gris qui représente l'histogramme des valeurs de l'image couleur.

prompt$ ./histogramme kowloon.ppm kowloon-h.pgm
Image kowloon.ppm Histogramme (à gauche) et histogramme cumulé (à droite)

On note que l'image kowloon.ppm n'est pas parfaitement bien balancée.

11 Egalisation d'image couleur

D'après le TP précédent (7 - Histogramme et Histogramme cumulé d'une image.), dès que l'on a un histogramme cumulé d'une image, il est maintenant facile d'égaliser l'image pour la rendre bien balancée. On reprendra donc la formule du TP précédent pour écrire la méthode int egalisation( int i ) const de la classe Histogramme.

On utilisera cette fonction d'égalisation mais sur les valeurs des couleurs, via nos itérateurs génériques avec accesseur ColorValueAccessor. Vous écrirez donc le programme egaliseur-couleur.cpp qui réalise cette égalisation d'une image couleur. Par exemple

prompt$ ./egaliseur-couleur kowloon.ppm kowloon-eg.ppm
prompt$ ./egaliseur-couleur papillon.ppm papillon-eg.ppm
Image kowloon.ppm Image kowloon.ppm dont les valeurs sont égalisées
Histogramme (à gauche) et histogramme cumulé (à droite) Histogramme (à gauche) et histogramme cumulé (à droite)
Image papillon.ppm Image papillon.ppm dont les valeurs sont égalisées
Histogramme (à gauche) et histogramme cumulé (à droite) Histogramme (à gauche) et histogramme cumulé (à droite)

12 Un peu d'imagination

Proposez un traitement quelconque (effet artistique, transformation sepia, flou, filtrage de bruit, fusion de photos, tourner les couleurs de 60°, saturer (rendre criard) ou désaturer (rendre pastel) une image, ...) et mettez le en oeuvre avec vos classes génériques.

13 TP à rendre

Vous écrirez un compte-rendu de ce TP (format Open office, Word, texte, PDF, LaTeX au choix) dans lequel vous préciserez les points suivants :

  • nom(s), prénom(s), groupe (monôme ou binôme)
  • Vous listerez d'abord les points du TP que vous avez abordés, en précisant si selon vous, vous avez traité complètement ou traité partiellement la question. Précisez aussi pour chaque question si votre code fonctionne ou dans quelle mesure il fonctionne.
  • vous préciserez pour chaque partie quels sont les fichiers entête et sources que je dois regarder, et éventuellement le programme que je dois compiler/relancer.
  • n'hésitez pas à étoffer de commentaires utiles vos travaux, ou d'images de tests.
  • vous placerez dans une archive (zip ou tar.gz) votre compte-rendu ainsi que vos sources/entêtes/makefile, de façon à ce que je puisse tout recompiler en tapant "make".
  • le nom de votre archive sera de la forme TP-XXX-YYY.*XXX et YYY désignent les noms respectifs des étudiants du binôme.
  • vous m'enverrez votre archive via TPLab avant la fin de la séance. Vous pouvez m'envoyer votre version finale avant le vendredi 3 novembre 2023 minuit.