2  De la Capture au Pixel - Échantillonnage, Quantification et Connectivité

Ce chapitre approfondit la compréhension de l’image numérique, passant de la nature physique de la capture à sa représentation mathématique discrète. Nous explorons comment la lumière devient donnée et comment l’organisation spatiale des pixels définit les relations de voisinage et de connectivité essentielles pour les algorithmes avancés de Vision par Ordinateur (VO).

2.1 Objectifs

À la fin de ce chapitre, vous serez capable de :

  • Expliquer le modèle physique de formation de l’image basé sur l’éclairage et la réflectance.
  • Différencier les mécanismes de la vision humaine et ceux des capteurs numériques.
  • Comprendre les processus d’échantillonnage (discrétisation de l’espace) et de quantification (discrétisation de l’intensité).
  • Décrire les relations topologiques entre pixels : voisinage, adjacence, connectivité et distances.
  • Effectuer des transformations géométriques de base (translation, rotation, échelle) en préservant la qualité.
  • Visualiser en pratique les effets de la variation de la résolution spatiale et de la profondeur de bits.

2.2 L’Œil et la Caméra - Éléments de la Perception Visuelle

La formation d’une image numérique commence par la capture de la lumière réfléchie par les objets. Comme illustré dans Figure 2.1, tant l’œil humain que les caméras numériques suivent des principes optiques similaires pour focaliser la lumière sur une surface sensible, bien qu’ils utilisent des mécanismes biologiques et électroniques distincts pour la transduction du signal.

2.2.1 Vision humaine

L’œil fonctionne comme un système optique complexe : la lumière traverse la cornée, l’humeur aqueuse, la pupille (contrôlée par l’iris) et le cristallin — qui ajuste la mise au point dynamiquement — jusqu’à atteindre la rétine. Sur la rétine se trouvent les photorécepteurs : les cônes (≈6 millions), concentrés dans la fovéa, sont responsables de la vision des couleurs et des détails, tandis que les bâtonnets (≈120 millions) assurent la vision en faible luminosité (vision scotopique), ne détectant que des intensités de gris. Le point aveugle est la région d’où part le nerf optique, dépourvue de récepteurs.

2.2.2 Capteurs numériques

Dans les caméras, les capteurs d’image jouent le rôle de la rétine. Les deux types les plus courants sont le CCD (Charge-Coupled Device - Dispositif à Transfert de Charge) et le CMOS (Complementary Metal-Oxide-Semiconductor - Semi-conducteur à Oxyde Métallique Complémentaire). Le capteur est composé d’une matrice de photodiodes (pixels) qui accumulent une charge électrique proportionnelle à la lumière incidente.

Pour la reconstruction des couleurs, on utilise le Filtre de Bayer, une matrice de filtres colorés qui permet à chaque pixel de capturer uniquement une composante de couleur : rouge, vert ou bleu (RGGB - Red, Green, Green, Blue). Par la suite, un ADC (Analog-to-Digital Converter - Convertisseur Analogique-Numérique) quantifie cette charge en valeurs numériques, définies par une profondeur de bits (ex. : 8 bits, résultant en 256 niveaux d’intensité).

NoteCuriosité

Bien que l’œil humain possède des millions de récepteurs, la résolution en haute définition est limitée à la fovéa (vision centrale), équivalente à environ \(120 \times 120\) pixels. La perception d’une scène complète en haute résolution résulte d’un intense post-traitement effectué par le cerveau.

Figure 2.1: Comparaison didactique entre le système visuel biologique et le système électronique : en haut, l’anatomie de l’œil humain mettant en évidence la rétine et les photorécepteurs (cônes et bâtonnets) ; en bas, la structure d’un appareil photo numérique détaillant le capteur CMOS, la matrice de filtres de Bayer (RGGB) et le processus de quantification numérique réalisé par le convertisseur analogique-numérique (ADC).

2.3 Illusions d’optique : Les défis de la perception visuelle

Alors que les capteurs numériques capturent l’intensité de la lumière de manière linéaire et objective, le système visuel humain interprète la scène en se fondant sur le contexte, les expériences antérieures et les mécanismes biologiques de survie. Les illusions d’optique ne sont pas des « erreurs » de l’œil, mais des preuves de l’intense post-traitement cérébral réalisé dans le cortex visuel.

2.3.1 Ambiguïté et contexte

Le cerveau cherche constamment à donner un sens aux motifs ambigus. Dans l’exemple du Vase de Rubin (voir Figure 2.2), la perception alterne entre la figure (le vase) et le fond (deux visages), démontrant que nous ne pouvons pas traiter simultanément les deux interprétations. Quant à l’illusion de l’Éléphant de Shepard, elle joue avec notre incapacité à concilier les lignes de contour qui suggèrent un volume dans des positions logiquement impossibles.

2.3.2 Luminosité et contraste local

De nombreuses illusions découlent de l’inhibition latérale, mécanisme par lequel les neurones voisins de la rétine se font concurrence pour accentuer les contours. Dans la Grille scintillante, des points sombres « fantômes » semblent apparaître aux intersections blanches en raison de ce traitement local du contraste.

L’illusion de l’Ombre sur l’échiquier d’Adelson est peut-être la plus frappante pour la VC : le carré « A » et le carré « B » ont exactement la même valeur de gris sur le capteur (ou dans le fichier numérique), mais le cerveau « corrige » la luminosité du carré « B » en comprenant qu’il se trouve sous une ombre portée, le percevant comme plus clair.

2.3.3 Géométrie et perspective

La perception de la profondeur peut être trompée par des constructions géométriques qui défient la logique tridimensionnelle à partir d’un angle de vue spécifique. L’Escalier de Schröder utilise l’ambiguïté de la perspective pour créer un objet qui semble monter ou descendre selon la façon dont on l’observe, mettant en évidence comment notre interprétation du « haut » et du « bas » dépend du point de fuite.

Figure 2.2: Recueil de défis perceptifs : (en haut à gauche) Vase de Rubin — ambiguïté figure-fond ; (en haut au centre) Éléphant de Shepard — incongruence géométrique ; (en haut à droite) Ombre d’Adelson — constance de la luminosité fondée sur le contexte ; (en bas à gauche) Grille de points — inhibition latérale ; (en bas à droite) Escalier de Schröder.

2.4 Le Modèle Mathématique de la Formation de l’Image

Une image peut être modélisée comme le produit de deux fonctions :

\[ f(x,y) = i(x,y) \cdot r(x,y) \tag{2.1}\]

où :

  • \(i(x,y)\) est l’éclairement incident sur la scène (énergie lumineuse par unité de surface), déterminé par la source de lumière ;
  • \(r(x,y)\) est la réflectance de l’objet (fraction de la lumière réfléchie), déterminée par les propriétés optiques de la surface, avec \(0 < r(x,y) < 1\).

En pratique, les deux composantes varient à des échelles spatiales distinctes : \(i(x,y)\) tend à varier lentement dans l’espace, tandis que \(r(x,y)\) peut présenter des variations brusques associées aux textures, aux contours et aux détails fins. Les techniques de traitement d’images (PDI) cherchent souvent à séparer ou à compenser ces composantes, comme dans les méthodes de correction d’éclairement non uniforme.

La Figure 2.3 illustre comment ce modèle se manifeste dans les images numériques en couleur, représentées par plusieurs canaux spectraux (RVB) et, éventuellement, par un canal supplémentaire de transparence (RVBA).

2.4.1 Représentation numérique et canaux spectraux

Dans les images numériques en couleur, la fonction \(f(x,y)\) de la Équation 2.1 est représentée par de multiples canaux spectraux. Dans le standard RGB, chaque pixel stocke trois échantillons indépendants :

\[\mathbf{f}(x,y) = \bigl[R(x,y),\; G(x,y),\; B(x,y)\bigr]\]

correspondant aux intensités des composantes rouge (Red), verte (Green) et bleue (Blue). Dans les images de type RGBA, on ajoute un quatrième canal :

\[\mathbf{f}(x,y) = \bigl[R(x,y),\; G(x,y),\; B(x,y),\; A(x,y)\bigr]\]

où \(A(x,y)\) représente le canal alpha (Alpha), chargé de coder l’opacité du pixel. Par convention, la valeur \(A = 0\) indique un pixel entièrement transparent, tandis que \(A = 255\) (ou \(1\)) représente un pixel entièrement opaque.

Ainsi, une image numérique en couleur est structurée comme une matrice multidimensionnelle de dimensions :

  • RGB : \(M \times N \times 3\)
  • RGBA : \(M \times N \times 4\)

où chaque position \((x,y)\) stocke les échantillons associés aux propriétés optiques de cette coordonnée spatiale.

La conversion entre plages d’intensité est directe et donnée par :

\[f_{\text{norm}}(x,y) = \frac{f(x,y)}{255}\]

où \(f(x,y) \in [0,\,255]\) et \(f_{\text{norm}}(x,y) \in [0,\,1]\).

Convention de représentation dans morph.hpp : la bibliothèque de ce livre adopte une convention unique (Table 2.1), sans les ambiguïtés de plage et d’ordre des canaux qui surgissent lors de la combinaison de plusieurs bibliothèques Python.

Table 2.1: Convention de représentation des images dans morph.hpp — plage, ordre des bandes, nombre de canaux et disposition en mémoire.
Aspect morph.hpp
Type d’échantillon unsigned char (uint8), plage \([0, 255]\)
Ordre des bandes RGB (via stb_image ; sans inversion BGR)
Nombre de canaux 1 (niveaux de gris) ou 3 (RGB)
Disposition en mémoire \(H \times W \times C\) entrelacé (data[(y*w + x)*channels + c])

La struct Image conserve les pixels dans un std::vector<unsigned char> linéaire, avec les champs h, w et channels. La fonction mm::read décode avec stb_image en forçant 3 canaux (ou 1, lorsque grayscale=true) ; par conséquent un éventuel canal alpha du fichier est écarté à la lecture. Pour travailler en virgule flottante, la même relation \(f_{\text{norm}} = f/255\) s’applique.

Dans la cellule d’exemple suivante, la version RGBA (\(M \times N \times 4\)) est construite en empilant un canal alpha synthétique sur le tableau lu — le noyau du notebook est en Python, et l’affichage reçoit le ndarray dans cette disposition \(H \times W \times C\).

2.4.2 Préparation de l’environnement pratique

Le bloc suivant charge le module morph.py du dépôt et illustre, pour un pixel synthétique, les différentes conventions d’échelle et d’ordre des canaux adoptées par les principales bibliothèques de traitement d’images.

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefacts de build du parcours C++ (.cpp, binaire, PNGs)

url = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"
if not os.path.exists("config.py"):
    urllib.request.urlretrieve(url, "config.py")

# Le noyau est Python même dans le parcours C++ : `mm` (morph.py) est utilisé par les
# simulateurs, par l'affichage des figures que le binaire C++ génère et par l'
# état mm::Image entre les cellules. cpp=True télécharge aussi le parcours compilé
# (morph.hpp + stb_image*.h), utilisé dans le #include des cellules %%writefile *.cpp.
import config
config.setup(demo=True, cpp=True)
from morph import mm
✅ Environnement prêt. Morph : 1.1.9 | OpenCV : 5.0.0

── Conventions d'échelle par bibliothèque ──
  NumPy / Pillow / OpenCV (uint8) : [200 100  50] → [0, 255]
  scikit-image / PyTorch (float) :  [0.78431373 0.39215686 0.19607843] → [0.0, 1.0]
  OpenCV : attention — lecture en BGR : [ 50 100 200] (canaux inversés)

2.4.3 Implémentation de la Lecture d’Image dans morph.hpp

Le passage suivant affiche le code source de la fonction mm::read, permettant de vérifier directement comment la bibliothèque morph.hpp implémente la lecture d’images.

# Le morph.hpp est header-only ; ci-dessous, le corps de la fonction mm::read().
import re, pathlib
hpp = pathlib.Path("morph.hpp").read_text()
m = re.search(r"inline Image read\(.*?\n\}", hpp, re.S)
print(m.group(0) if m else "(mm::read não encontrada em morph.hpp)")
inline Image read(const std::string& path_or_url, bool grayscale = false) {
    std::string local_path = path_or_url;
    bool is_url = path_or_url.rfind("http://", 0) == 0 ||
                  path_or_url.rfind("https://", 0) == 0;
    if (is_url) {
        local_path = "_mm_download_tmp.img";
        if (!_download(path_or_url, local_path))
            throw std::runtime_error("mm::read: falha ao baixar '" + path_or_url + "'");
    }

    int w, h, ch;
    int desired = grayscale ? 1 : 3;
    unsigned char* data = stbi_load(local_path.c_str(), &w, &h, &ch, desired);
    if (!data) {
        Image fallback;
        if (_try_read_ascii_pgm(local_path, fallback)) return fallback;
        throw std::runtime_error("mm::read: falha ao decodificar '" + path_or_url + "'");
    }

    Image img(h, w, desired);
    std::copy(data, data + (size_t)w * h * desired, img.data.begin());
    stbi_image_free(data);
    return img;
}

2.4.4 RGB et RGBA : Exemple pratique avec l’image Mandrill

L’exemple suivant charge l’image Mandrill au format RGB, construit artificiellement un canal alpha avec un dégradé horizontal et affiche les deux représentations, illustrant concrètement les structures matricielles \(M \times N \times 3\) et \(M \times N \times 4\).

%%writefile tmp/fig_02_rgb_rgba.cpp
#define MM_OUT "tmp/fig_02_rgb_rgba.png"
//| label: fig-02-rgb-rgba
//| fig-cap: "Imagem RGB (3 canais, $M \\times N \\times 3$) e versão RGBA (4 canais, $M \\times N \\times 4$) com transparência alfa gradual da esquerda para a direita. Imagem Mandrill — [USC SIPI Image Database](https://sipi.usc.edu/database/database.php?volume=misc) (domínio público)."
//| echo: true

#include "morph.hpp"
#include <iostream>
#include <filesystem>

int main() {
    // https://commons.wikimedia.org/wiki/File:Mandrill-k-means.png
    mm::Image img_rgb = mm::read("https://upload.wikimedia.org/wikipedia/commons/a/ab/Mandrill-k-means.png"); // (M, N, 3), uint8

    // Canal alfa: gradiente horizontal 0 -> 255 (esquerda -> direita)
    int h = img_rgb.h, w = img_rgb.w;
    mm::Image alpha(h, w);
    for (int x = 0; x < w; x++) {
        for (int y = 0; y < h; y++) {
            alpha.at(y, x) = static_cast<unsigned char>(255 * x / (w - 1));
        }
    }

    // Empilha RGB + alfa -> RGBA (M, N, 4)
    mm::Image img_rgba(h, w, 4);
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            for (int ch = 0; ch < 3; ch++) {
                img_rgba.at(y, x, ch) = img_rgb.at(y, x, ch);
            }
            img_rgba.at(y, x, 3) = alpha.at(y, x);
        }
    }

    std::cout << "RGB   -> shape=" << img_rgb.h << " x " << img_rgb.w << " x " << img_rgb.channels << "\n";
    std::cout << "RGBA  -> shape=" << img_rgba.h << " x " << img_rgba.w << " x " << img_rgba.channels << "\n";

    mm::show(std::vector<mm::Image>{img_rgb, img_rgba},
             MM_OUT,
             std::vector<std::string>{"RGB  —  M x N x 3", "RGBA  —  M x N x 4"},
             2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_rgb, "tmp/fig_02_rgb_rgba_0.png");
mm::write(img_rgba, "tmp/fig_02_rgb_rgba_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_rgb_rgba.cpp
!g++ -I. -std=c++17 tmp/fig_02_rgb_rgba.cpp -o tmp/fig_02_rgb_rgba \
  && ./tmp/fig_02_rgb_rgba \
  && test -f "tmp/fig_02_rgb_rgba.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_rgb_rgba.png"
RGB   -> shape=512 x 512 x 3
RGBA  -> shape=512 x 512 x 4
[1] RGB  —  M x N x 3
[2] RGBA  —  M x N x 4
try:
    mm.show(
        [
            mm.read("tmp/fig_02_rgb_rgba_0.png"),
            mm.read("tmp/fig_02_rgb_rgba_1.png"),
        ],
        titles=[
            'RGB  —  M x N x 3',
            'RGBA  —  M x N x 4',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_rgb_rgba_0.png (ver a versao Python)")
Figure 2.3: Imagem RGB (3 canais, \(M \times N \times 3\)) e versão RGBA (4 canais, \(M \times N \times 4\)) com transparência alfa gradual da esquerda para a direita. Imagem Mandrill — USC SIPI Image Database (domínio público).

2.5 Numérisation : Échantillonnage et Quantification

Pour transformer une scène continue en une image numérique, deux processus sont nécessaires : l’échantillonnage et la quantification.

2.5.1 Échantillonnage — Discrétisation de l’espace

L’échantillonnage consiste à mesurer la valeur de la fonction \(f(x,y)\) en des points régulièrement espacés, formant une matrice de \(M\) lignes (hauteur) et \(N\) colonnes (largeur). Chaque élément de cette matrice est un pixel. La résolution spatiale est donnée par \(M \times N\). Plus la résolution est élevée, plus les détails spatiaux sont préservés, mais plus le coût computationnel et de stockage augmente également.

2.5.2 Quantification — Discrétisation de l’intensité

La quantification associe à chaque pixel une valeur numérique discrète, généralement représentée par un entier de \(b\) bits. La profondeur de bits définit le nombre de niveaux d’intensité : \(2^b\). Les images en niveaux de gris utilisent couramment 8 bits (256 niveaux). Les images couleur utilisent trois canaux de 8 bits (24 bits au total).

Illustration : Si l’on utilise seulement 1 bit par pixel (noir et blanc), on perd tous les tons intermédiaires. Avec 2 bits (4 niveaux), on perçoit déjà des dégradés grossiers. Avec 8 bits, l’œil humain perçoit difficilement la discrétisation (vision continue).

AvertissementErreur de quantification

L’erreur de quantification est la différence entre la valeur analogique réelle et la valeur discrète attribuée. Elle se manifeste sous forme de bruit de quantification, visible dans les régions à gradient doux lorsque l’on utilise peu de bits.

2.5.3 Effets de l’échantillonnage et de la quantification

Les expériences suivantes montrent comment la réduction de la résolution spatiale (sous-échantillonnage) et de la profondeur de bits dégrade la qualité visuelle. Utilisez le code pour explorer différents facteurs et niveaux de gris.

%%writefile tmp/mm_out_1.cpp
// Compile: g++ -std=c++17 program.cpp -o program -lmorph

#include "morph.hpp"
#include <iostream>
#include <filesystem>

//| quarto-raw: true

int main() {
    // Imagem de exemplo (barbudo-rajado) — base das figuras de amostragem,
    // quantização e transformações geométricas deste capítulo.
    std::string url = "https://upload.wikimedia.org/wikipedia/commons/c/c5/Area_de_Prote%C3%A7%C3%A3o_Ambiental_Quilombos_do_M%C3%A9dio_Ribeira_-_Thomas-Fuhrmann_%282023-_02%29_Malacoptila_striata.jpg";

    mm::Image img_color = mm::read(url);
    mm::Image img_gray0 = mm::gray(img_color);
    mm::Image img_gray  = mm::crop(img_gray0, 820, 1850, 890, 1550);  // recorte p/ ver detalhes
    std::cout << "Imagem original: (" << img_color.h << ", " << img_color.w << ", " << img_color.channels << ")\n";

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_22.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/mm_out_1.cpp
!g++ -I. -std=c++17 tmp/mm_out_1.cpp -o tmp/mm_out_1 \
  && ./tmp/mm_out_1
Imagem original: (3067, 2047, 3)

L’expérience présentée dans Figure 2.4 illustre le compromis entre la résolution spatiale et le coût de stockage en mémoire. Le code utilise la technique de sous-échantillonnage par tranchage (slicing) pour réduire la matrice originale de pixels selon un facteur \(f\), ce qui entraîne une économie de mémoire — par exemple, un facteur \(f=8\) réduit la taille des données de 64 fois (\(8^2\)). À des fins de comparaison visuelle, les images réduites sont restaurées à leurs dimensions originales (\(512 \times 512\)) via une interpolation par plus proche voisin (nearest). Ce processus ne récupère pas l’information perdue, mais rend évident l’effet d’aliasing et la structure en blocs (pixelisation) générée par la faible densité de données de la matrice échantillonnée.

%%writefile tmp/fig_02_subamostragem.cpp
#define MM_OUT "tmp/fig_02_subamostragem.png"
//| label: fig-02-subamostragem
//| fig-cap: "Efeito da subamostragem. Os títulos exibem as dimensões (W x H) e o tamanho da matriz em memória (KB)."
//| echo: true
//| output: true

#include "morph.hpp"
#include <iostream>
#include <string>
#include <vector>

std::pair<mm::Image, std::string> subsample_simple(const mm::Image& image, int f) {
    // Subamostragem via fatiamento (slicing)
    mm::Image reduced = mm::subsample(image, f);

    // Cálculo de memória em KB
    double mem_kb = (reduced.h * reduced.w * reduced.channels) / 1024.0;

    std::string label = std::to_string(reduced.w) + "x" + std::to_string(reduced.h) + ", " + 
                        std::to_string((int)mem_kb) + " KB\n(Fator " + std::to_string(f) + ")";

    // Restaura o tamanho para visualização (H, W originais)
    mm::Image res = mm::resize(reduced, image.w, image.h, "nearest");
    return {res, label};
}

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    // img_gray is provided (already initialized)

    std::vector<int> factors = {1, 4, 8, 12};

    // Gera os resultados e separa em listas para o mm.show
    std::vector<mm::Image> imgs_list;
    std::vector<std::string> titles_list;

    for (int f : factors) {
        auto [res, label] = subsample_simple(img_gray, f);
        imgs_list.push_back(res);
        titles_list.push_back(label);
    }

    mm::show(imgs_list, MM_OUT, titles_list, 4);

    return 0;
}
Overwriting tmp/fig_02_subamostragem.cpp
!g++ -I. -std=c++17 tmp/fig_02_subamostragem.cpp -o tmp/fig_02_subamostragem \
  && ./tmp/fig_02_subamostragem \
  && test -f "tmp/fig_02_subamostragem.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_subamostragem.png"
[1] 660x1030, 663 KB
(Fator 1)
[2] 165x258, 41 KB
(Fator 4)
[3] 83x129, 10 KB
(Fator 8)
[4] 55x86, 4 KB
(Fator 12)
try:
    mm.show(mm.read("tmp/fig_02_subamostragem.png"), figsize=(16, 12))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_subamostragem.png (ver a versao Python)")
Figure 2.4: Efeito da subamostragem. Os títulos exibem as dimensões (W x H) e o tamanho da matriz em memória (KB).

L’expérience dans Figure 2.5 se concentre sur la quantification d’intensité, le processus de discrétisation de l’amplitude de la fonction \(f(x,y)\). Alors que le sous-échantillonnage affecte la grille spatiale, la réduction de la profondeur de bits limite la quantité de niveaux de gris disponibles pour représenter la luminosité.

En réduisant la profondeur de 8 bits (256 niveaux) à des valeurs inférieures, l’effet de postérisation apparaît, où les dégradés lisses d’une scène sont remplacés par des transitions abruptes. À la limite de 1 bit, l’image devient strictement binaire, ne préservant que la silhouette et perdant les détails de texture et de volume.

%%writefile tmp/fig_02_quantizacao.cpp
#define MM_OUT "tmp/fig_02_quantizacao.png"
//#| label: fig-02-quantizacao
//#| fig-cap: "Efeito da redução da profundidade de bits. Os títulos exibem a quantidade de bits, níveis e o tamanho em memória (KB)."
//#| echo: true
//#| output: true

#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <cmath>
#include <utility>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    // img_gray is already defined as mm::Image (provided externally)

    // Fonction de quantification
    auto quantize_simple = [](const mm::Image& image, int bits) {
        // Réduit la profondeur de bits et calcule les métadonnées mémoire
        int levels = std::pow(2, bits);

        // Normalisation et quantification uniforme
        mm::Image quantized(image.h, image.w);
        for (int y = 0; y < image.h; ++y) {
            for (int x = 0; x < image.w; ++x) {
                int val = image.at(y, x);
                double normalized = std::floor((val / 256.0) * levels) / levels * 255.0;
                quantized.at(y, x) = static_cast<unsigned char>(std::max(0, std::min(255, static_cast<int>(normalized))));
            }
        }

        // Calcul de la mémoire en Ko
        double mem_kb = (quantized.h * quantized.w * quantized.channels) / 1024.0;
        std::string label = std::to_string(bits) + " bits (" + std::to_string(levels) + " níveis)\n" + std::to_string(static_cast<int>(mem_kb)) + " KB";

        return std::make_pair(quantized, label);
    };

    // Liste de bits pour le test (8 est le défaut, 1 est le binaire)
    std::vector<int> bits_test = {8, 4, 2, 1};

    // Génère les résultats et les sépare en listes pour mm::show
    std::vector<std::pair<mm::Image, std::string>> results_q;
    for (int b : bits_test) {
        results_q.push_back(quantize_simple(img_gray, b));
    }
    std::vector<mm::Image> imgs_q;
    std::vector<std::string> titles_q;
    for (const auto& r : results_q) {
        imgs_q.push_back(r.first);
        titles_q.push_back(r.second);
    }

    mm::show(imgs_q, MM_OUT, titles_q, 4);

    return 0;
}
Overwriting tmp/fig_02_quantizacao.cpp
!g++ -I. -std=c++17 tmp/fig_02_quantizacao.cpp -o tmp/fig_02_quantizacao \
  && ./tmp/fig_02_quantizacao \
  && test -f "tmp/fig_02_quantizacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_quantizacao.png"
[1] 8 bits (256 níveis)
663 KB
[2] 4 bits (16 níveis)
663 KB
[3] 2 bits (4 níveis)
663 KB
[4] 1 bits (2 níveis)
663 KB
try:
    mm.show(mm.read("tmp/fig_02_quantizacao.png"), figsize=(16, 12))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_quantizacao.png (ver a versao Python)")
Figure 2.5: Efeito da redução da profundidade de bits. Os títulos exibem a quantidade de bits, níveis e o tamanho em memória (KB).

2.5.4 Analyse technique

  • Domaine vs. Codomaine : Notez que la résolution spatiale (dimensions de la matrice) reste constante à 512x512 ; seul le codomaine de la fonction image change.
  • Constance de la mémoire : Observez dans les titres que la taille en Ko ne diminue pas. Cela s’explique par le fait que NumPy stocke chaque pixel quantifié dans un conteneur de 8 bits (uint8), indépendamment du fait que la valeur réelle soit seulement 0 ou 1.
  • Perception : La dégradation visuelle devient critique en dessous de 4 bits, où l’œil humain commence à percevoir les « frontières » artificielles créées par le manque de tons intermédiaires.

La limitation des types de données plus petits qu’un octet dans l’écosystème Python/NumPy est due à l’architecture matérielle, qui adresse la mémoire en blocs de 8 bits (octets). Afin de maintenir la compatibilité avec OpenCV et de garantir l’efficacité, même les éléments binaires sont mappés vers des conteneurs de 1 octet (uint8 ou bool8).

Bien que des langages comme l’ANSI C permettent la compression de 8 pixels par octet (bit-packing), cette approche exige une décompression constante pour les calculs et impose une complexité élevée dans la manipulation des pointeurs. Selon la Table 2.2, l’utilisation de uint8 est privilégiée pour la facilité d’accès aux voisins et la polyvalence dans les transformations géométriques. De plus, les méthodes natives de NumPy et OpenCV exécutent le traitement en interne à bas niveau (C/C++), ce qui rend les opérations vectorisées plus rapides que les implémentations manuelles avec des boucles imbriquées en Python.

Table 2.2: Comparatif entre les stratégies de compression et l’efficacité du traitement.
Caractéristique Python (NumPy/OpenCV) ANSI C (Bit-packing)
Plus petite unité 1 octet (8 bits) 1 bit
Mémoire (binaire) 256 Ko (pour 512x512) 32 Ko (pour 512x512)
Vitesse Élevée (vectorisation en C) Variable (lente en cas de décalage de bits)
Complexité Faible : méthodes prêtes à l’emploi Élevée : pointeurs et masques

2.6 Relations entre Pixels - Topologie de l’Image

Les pixels ne sont pas des éléments isolés ; leurs positions relatives définissent des concepts importants pour le traitement.

2.6.1 Voisinage

Étant donné un pixel de coordonnées \((x,y)\), on définit deux types principaux de voisinage (pour les images en grille rectangulaire) :

  • Voisinage-4 (von Neumann) : inclut les pixels aux positions \((x-1,y)\), \((x+1,y)\), \((x,y-1)\), \((x,y+1)\).
  • Voisinage-8 (Moore) : inclut les huit pixels adjacents (ajoute les quatre diagonales).

Le choix du voisinage influence des opérations telles que la détection de contours, le calcul des gradients et la connectivité.

%%writefile tmp/fig_02_vizinhanca.cpp
#define MM_OUT "tmp/fig_02_vizinhanca.png"
//| label: fig-02-vizinhanca
//| fig-cap: "Ilustração de vizinhanças 4 em uma matriz 3x3. No centro (1,1), o pixel de interesse."
//| echo: true
//| output: true

#include "morph.hpp"

int main() {
    // # Criação de uma matriz 3x3 para exemplo topológico
    // viz = np.zeros((3, 3), dtype='uint8')
    // 
    // # Definindo Vizinhança-4 (N4) com valor diferente para destaque
    // viz[0, 1] = viz[2, 1] = viz[1, 0] = viz[1, 2] = viz[1, 1] = 255
    // 
    // ou simplesmente (teste com números como argumentos):
    mm::Image viz = mm::secross();

    // Exibição da matriz para análise de coordenadas
    mm::drawImgPlt(viz, MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_02_vizinhanca.cpp
!g++ -I. -std=c++17 tmp/fig_02_vizinhanca.cpp -o tmp/fig_02_vizinhanca \
  && ./tmp/fig_02_vizinhanca \
  && test -f "tmp/fig_02_vizinhanca.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_vizinhanca.png"
0 1 0 
1 1 1 
0 1 0 
try:
    mm.show(mm.read("tmp/fig_02_vizinhanca.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_vizinhanca.png (ver a versao Python)")
Figure 2.6: Ilustração de vizinhanças 4 em uma matriz 3x3. No centro (1,1), o pixel de interesse.

2.6.2 Adjacence, connectivité et chemins

Deux pixels sont adjacents s’ils sont en contact selon un voisinage défini et satisfont un critère de valeur (ex. : même niveau d’intensité). Une connectivité définit une relation d’équivalence entre les pixels qui forment une région connexe. Un chemin est une séquence de pixels adjacents.

La connectivité-4 (N4) et la connectivité-8 (N8) peuvent produire des résultats différents dans la segmentation et le calcul des composantes connexes (étiquetage). Par exemple, un motif en damier peut être complètement déconnecté en N4, mais totalement connecté en N8.

2.6.3 Distances entre pixels

Les métriques de distance sont fondamentales pour quantifier la proximité physique et la connectivité entre les éléments qui composent la grille numérique. Comme démontré dans la Table 2.3, le choix de la métrique définit le coût de déplacement entre pixels et modifie le comportement des algorithmes de segmentation et d’analyse morphologique.

Pour mesurer la distance entre deux pixels \(p(x_1, y_1)\) et \(q(x_2, y_2)\), on utilise différentes fonctions métriques qui imposent des contraintes de mouvement distinctes sur la grille :

Table 2.3: Comparatif des métriques de distance appliquées à la maille de pixels.
Métrique Définition Interprétation
Euclidienne \(\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}\) Distance exacte en ligne droite (continue)
Manhattan (City block) \(|x_1-x_2| + |y_1-y_2|\) Mouvements horizontaux + verticaux
Chebyshev (Échiquier) \(\max(|x_1-x_2|, |y_1-y_2|)\) Plus grand déplacement entre les axes

Ces distances sont appliquées dans divers contextes de TDI, notamment les algorithmes d’interpolation géométrique, les transformations de distance, la croissance de régions et l’analyse de formes.

NoteExemple pratique

Considérant deux pixels avec des déplacements relatifs \(\Delta x = 3\) et \(\Delta y = 4\) :

  • Euclidienne : \(\sqrt{3^2 + 4^2} = 5\) (hypoténuse du triangle rectangle).
  • Manhattan : \(3 + 4 = 7\) (somme des cathètes).
  • Chebyshev : \(\max(3, 4) = 4\) (prédominance du plus grand déplacement).

2.7 Stockage d’images

Le choix du format de fichier est une étape décisive dans le flux de traitement, car il détermine la manière dont les données d’échantillonnage et de quantification seront préservées ou écartées. Comme présenté dans la Table 2.4, chaque extension équilibre de façon distincte la fidélité des données et l’efficacité de stockage.

Table 2.4: Principaux formats de stockage d’images numériques et leurs applications en TNI.
Format Caractéristiques Usage typique
PGM Format simple de carte de niveaux de gris (texte ou binaire). Recherche académique et outils Unix.
BMP Non compressé (ou compression simple). Windows, applications héritées.
PNG Compression sans perte (lossless). Web, images avec transparence.
JPEG Compression avec perte (lossy), idéale pour les photographies. Photos, appareils photo numériques.
TIFF Prend en charge plusieurs couches et une compression variée. PAO, archivage.
RAW Données brutes du capteur, sans traitement. Photographie professionnelle.
DICOM Norme médicale avec métadonnées cliniques intégrées (patient, équipement, protocole). Radiologie, tomodensitométrie, imagerie par résonance magnétique.

Les métadonnées d’une image incluent des paramètres tels que la largeur, la hauteur, la profondeur de bits et le codage des couleurs. Dans les formats scientifiques, les informations d’étalonnage et les détails de capture sont également préservés. Lors de l’utilisation de la fonction mm::read(), la bibliothèque morph préserve automatiquement ces données afin de respecter les propriétés originales de l’image.

Dans les contextes scientifiques et hospitaliers, la norme DICOM (Digital Imaging and Communications in Medicine) est privilégiée pour garantir qu’aucune perte de précision diagnostique ne se produise. Des référentiels publics tels que The Cancer Imaging Archive (TCIA), le Alzheimer’s Disease Neuroimaging Initiative (ADNI) et PhysioNet mettent à disposition de vastes ensembles de données dans ce format, incluant des métadonnées cliniques anonymisées qui sont essentielles pour la recherche scientifique.

2.7.1 Exemple : Extraction de métadonnées et localisation GPS

Contrairement à la matrice de pixels pure obtenue par la lecture conventionnelle — comme dans l’image de l’oiseau présentée au début de ce chapitre —, l’utilisation de l’argument pil=True dans la méthode mm::read() modifie la nature de l’objet retourné (voir Figure 2.7). Alors que le comportement par défaut (pil=False) retourne un numpy.ndarray RGB, la lecture avec pil=True retourne un objet spécialisé de la bibliothèque Pillow, capable d’interpréter l’en-tête EXIF.

L’en-tête EXIF (Exchangeable Image File Format) fonctionne comme un référentiel technique de la capture, permettant à l’objet Pillow d’interpréter une vaste gamme d’informations qui vont bien au-delà des coordonnées GPS. En utilisant pil=True, le système accède à l’« ADN » de l’image, incluant les métadonnées matérielles (marque et modèle de l’appareil photo), les réglages optiques (ouverture, distance focale et temps d’exposition) et les paramètres d’éclairage (utilisation du flash et balance des blancs).

Cette distinction est importante en traitement d’images numériques, car elle transforme la matrice d’échantillonnage en un ensemble de données contextualisées, où les caractéristiques physiques du capteur et de l’objectif peuvent être utilisées pour normaliser les luminosités ou corriger les distorsions géométriques.

NoteParcours C++ : pourquoi l’extraction des EXIF reste en Python

L’extraction des métadonnées est une analyse de conteneur — décodage de l’en-tête EXIF intégré au fichier — et non un traitement de pixels : elle est orthogonale au pipeline d’échantillonnage et de quantification. Le fichier morph.hpp décode les images avec stb_image, qui ne fournit que la matrice de pixels et élimine l’en-tête EXIF. Comme le noyau de ce notebook est en Python même dans le parcours C++, cet exemple utilise la lecture en mode Pillow (pil=True) dans les deux parcours. Dans un programme C++ autonome, le chemin équivalent serait de vendoriser une bibliothèque dédiée : easyexif (lecture des EXIF/GPS dans les JPEG, fichier d’en-tête unique, zéro dépendance) ou exiv2 (lecture et écriture, y compris IPTC/XMP).

%%writefile tmp/fig_01_natureza.cpp
#define MM_OUT "tmp/fig_01_natureza.png"
#include "morph.hpp"
#include <iostream>
#include <string>

int main() {
    //| label: fig-01-natureza
    //| fig-cap: "Zone de Protection Environnementale Quilombos du Médio Ribeira - Barbu-vermiculé (Malacoptila striata). Crédit : Thomas Fuhrmann (CC BY-SA 4.0)."
    //| echo: true

    std::string base    = "https://upload.wikimedia.org/wikipedia/commons";
    std::string arquivo = 
        "Area_de_Prote%C3%A7%C3%A3o_Ambiental_"
        "Quilombos_do_M%C3%A9dio_Ribeira_-_Thomas-"
        "Fuhrmann_%282023-_02%29_Malacoptila_striata.jpg";
    std::string url     = base + "/c/c5/" + arquivo;
    std::string caminho = "imagens/barbudo-rajado.jpg";

    // 1. Lecture — l'objet PIL est préservé avec EXIF
    mm::Image img_obj;
    if (/* os.path.exists(caminho) */ false) {
        // os.makedirs("imagens", exist_ok=True) — not directly applicable
        img_obj = mm::read(url);
        mm::write(img_obj, caminho);      // sauvegarde en préservant EXIF
    } else {
        img_obj = mm::read(caminho);
    }

    mm::Image img_numpy = img_obj;      // conversion en Image

    // 2. Extraction et conversion GPS (Tag 34853)
    // Note: EXIF handling with mm::Image is not directly available
    // GPS extraction would require additional EXIF parsing not in mm::Image API

    // 3. Diagnostic des types, dimensions et accès aux pixels
    std::cout << "\nDimensions : " << img_obj.w << " x " << img_obj.h << std::endl;
    std::cout << "Pixel (0,0): " << (int)img_obj.at(0, 0, 0) << ", "
              << (int)img_obj.at(0, 0, 1) << ", " << (int)img_obj.at(0, 0, 2) << std::endl;
    std::cout << "Dimensions [y,x,c]: " << img_numpy.h << ", " << img_numpy.w 
              << ", " << img_numpy.channels << std::endl;
    std::cout << "NumPy [0,0]: " << (int)img_numpy.at(0, 0, 0) << ", "
              << (int)img_numpy.at(0, 0, 1) << ", " << (int)img_numpy.at(0, 0, 2) << std::endl;

    // 4. Affichage
    mm::show(img_numpy, MM_OUT);

    return 0;
}
Overwriting tmp/fig_01_natureza.cpp
!g++ -I. -std=c++17 tmp/fig_01_natureza.cpp -o tmp/fig_01_natureza \
  && ./tmp/fig_01_natureza \
  && test -f "tmp/fig_01_natureza.png" \
  || echo "⚠ mm::show não gravou tmp/fig_01_natureza.png"

Dimensions : 2047 x 3067
Pixel (0,0): 58, 96, 0
Dimensions [y,x,c]: 3067, 2047, 3
NumPy [0,0]: 58, 96, 0
try:
    mm.show(mm.read("tmp/fig_01_natureza.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_01_natureza.png (ver a versao Python)")
Figure 2.7: Area de Proteção Ambiental Quilombos do Médio Ribeira - Barbudo-rajado (Malacoptila striata). Crédito: Thomas Fuhrmann (CC BY-SA 4.0).
NoteNote pédagogique : La différence subtile des dimensions

Remarquez que la représentation des dimensions change selon la structure de données utilisée :

  • Dans Pillow (.size) : Renvoie (Largeur, Hauteur) — dans l’exemple : (2047, 3067). C’est une vision orientée vers le fichier image.
  • Dans NumPy (.shape) : Suit la convention mathématique des matrices : (Lignes/Hauteur, Colonnes/Largeur, Canaux) — dans l’exemple : (3067, 2047, 3).

Cette distinction est fondamentale pour éviter les erreurs d’indexation lors de l’implémentation de filtres manuels. Alors que l’objet Pillow porte le « où » et le « quand » (contexte), le tableau NumPy porte le « combien » de lumière (intensité) existant à chaque point de l’image.

2.7.2 Pourquoi cette séparation est-elle importante ?

Lors du chargement d’une image par le chemin conventionnel (pil=False), le résultat est un numpy.ndarray, qui contient strictement les valeurs numériques issues de la quantification et de l’échantillonnage. Cependant, en utilisant pil=True, mm::read() renvoie un objet de la classe PIL.JpegImagePlugin.JpegImageFile.

Cette classe maintient le fichier « ouvert » pour permettre l’accès au contexte de la capture avant que les données ne soient converties en matrice brute. Notez que le pixel (0, 0) est identique dans les deux représentations — (58, 96, 0) dans Pillow et [58, 96, 0] dans NumPy —, confirmant que les deux décrivent les mêmes données, avec des interfaces simplement différentes. Cette séparation est vitale : les pixels servent aux algorithmes ; les métadonnées servent au géoréférencement, au catalogage scientifique et aux corrections basées sur le matériel d’acquisition.

Pour inspecter toutes les métadonnées EXIF d’un objet Pillow :

from PIL import ExifTags

exif_raw = img_obj._getexif()
if exif_raw:
    for tag_id, valor in sorted(exif_raw.items()):
        tag_nome = ExifTags.TAGS.get(tag_id, f"TAG_{tag_id}")
        print(f"  {tag_nome:40s} : {valor}")

2.8 Transformations géométriques de base

Les transformations géométriques modifient la position des pixels, en conservant les valeurs d’intensité. Elles sont fondamentales pour l’alignement, la correction des distorsions et l’augmentation des données (data augmentation) en apprentissage automatique.

Une transformation affine est toute application qui préserve la colinéarité (des points sur une droite restent sur une droite) ainsi que les rapports de distances entre points colinéaires. En 2D, toute transformation affine peut être exprimée en coordonnées homogènes par une matrice \(3 \times 3\) :

\[ \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \underbrace{\begin{bmatrix} a_{11} & a_{12} & t_x \\ a_{21} & a_{22} & t_y \\ 0 & 0 & 1 \end{bmatrix}}_{\mathbf{T}} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \tag{2.2}\]

La sous-matrice \(2 \times 2\) supérieure gauche \(\mathbf{A} = \begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix}\) contrôle la rotation, l’échelle et le cisaillement ; le vecteur \((t_x, t_y)^\top\) contrôle la translation. Les transformations géométriques les plus utilisées en TNI — translation, rotation et échelle — sont des cas particuliers de \(\mathbf{T}\), et peuvent être composées par multiplication matricielle, dans l’ordre \(\mathbf{T} = \mathbf{T}_n \cdots \mathbf{T}_2 \mathbf{T}_1\).

NoteTransformation inverse et interpolation

Dans l’implémentation pratique (cv2.warpAffine), on applique la transformation inverse : pour chaque pixel \((x', y')\) de l’image destination, on calcule la position d’origine \((x, y) = \mathbf{T}^{-1}(x', y')\) et on interpole la valeur. Cela évite les trous dans l’image résultante, causés par le mappage direct de pixels entiers vers des positions non entières.

2.8.1 Translation

La translation est la transformation affine la plus simple : elle déplace tous les pixels selon un vecteur \((t_x, t_y)\). En coordonnées homogènes, elle s’exprime par la matrice :

\[ \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \quad \Rightarrow \quad \begin{cases} x' = x + t_x \\ y' = y + t_y \end{cases} \tag{2.3}\]

La troisième ligne de la matrice garantit que l’opération reste dans l’espace affine, permettant de combiner translation, rotation et mise à l’échelle par simple multiplication de matrices. En pratique, cv2.warpAffine n’utilise que les deux premières lignes (matrice \(2 \times 3\)), car la troisième est toujours \([0, 0, 1]\).

Les pixels déplacés au-delà de la zone d’origine sont ignorés ; les zones découvertes sont remplies avec 0 (noir). Voir un exemple dans la Figure 2.8..

%%writefile tmp/fig_02_translacao.cpp
#define MM_OUT "tmp/fig_02_translacao.png"
//| label: fig-02-translacao
//| fig-cap: "Exemplo de translação da imagem do pássaro com deslocamentos (50,50) e (100,50)."
//| echo: true
//| output: true
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    mm::Image img_tx1 = mm::translate(img_gray, 50, 50);
    mm::Image img_tx2 = mm::translate(img_gray, 100, 50);
    mm::show(std::vector<mm::Image>{img_gray, img_tx1, img_tx2}, MM_OUT,
             std::vector<std::string>{"Original", "Translação (50,50)", "Translação (100,50)"}, 3);
    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_02_translacao_0.png");
mm::write(img_tx1, "tmp/fig_02_translacao_1.png");
mm::write(img_tx2, "tmp/fig_02_translacao_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_translacao.cpp
!g++ -I. -std=c++17 tmp/fig_02_translacao.cpp -o tmp/fig_02_translacao \
  && ./tmp/fig_02_translacao \
  && test -f "tmp/fig_02_translacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_translacao.png"
[1] Original
[2] Translação (50,50)
[3] Translação (100,50)
try:
    mm.show(
        [
            mm.read("tmp/fig_02_translacao_0.png"),
            mm.read("tmp/fig_02_translacao_1.png"),
            mm.read("tmp/fig_02_translacao_2.png"),
        ],
        titles=[
            'Original',
            'Translação (50,50)',
            'Translação (100,50)',
        ],
        cols=3,
        figsize=(16, 12),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_translacao_0.png (ver a versao Python)")
Figure 2.8: Exemplo de translação da imagem do pássaro com deslocamentos (50,50) e (100,50).

2.8.2 Rotation

The rotation by an angle \(\theta\) around a central point \((c_x, c_y)\) is composed of three affine transformations: translation to the origin, pure rotation, and translation back. The resulting matrix is:

\[ \mathbf{T}_{\text{rot}} = \begin{bmatrix} \cos\theta & -\sin\theta & c_x(1-\cos\theta) + c_y\sin\theta \\ \sin\theta & \cos\theta & c_y(1-\cos\theta) - c_x\sin\theta \\ 0 & 0 & 1 \end{bmatrix} \tag{2.4}\]

In the implementation, cv2.getRotationMatrix2D directly generates the first two lines of \(\mathbf{T}_{\text{rot}}\) (\(2 \times 3\) matrix for warpAffine), also accepting a scale factor \(s\) that multiplies \(\cos\theta\) and \(\sin\theta\). See an example in Figure 2.9.

Since rotation shifts pixels to new non-integer positions, cv2.warpAffine needs to estimate the color of each destination pixel from its neighbors—a process called interpolation. The interp parameter controls this behavior:

  • nearest (INTER_NEAREST): assigns the value of the nearest pixel. Fast, but produces jagged edges (aliasing) on diagonal borders.
  • bilinear (INTER_LINEAR, default): weighted average of the 4 nearest neighbors. Balances quality and performance—suitable for most cases.
  • bicubic (INTER_CUBIC): considers the 16 neighbors on a cubic surface. Produces smoother edges, at the cost of higher processing.
%%writefile tmp/fig_02_rotacao.cpp
#define MM_OUT "tmp/fig_02_rotacao.png"
//| label: fig-02-rotacao
//| fig-cap: "Exemplo de rotação da imagem do pássaro em 30° e 45° usando interpolação bilinear."
//| echo: true
//| output: true

#include "morph.hpp"
#include <iostream>
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    mm::Image img_rot30 = mm::rotate(img_gray, 30, 1.0, "bilinear");
    mm::Image img_rot45 = mm::rotate(img_gray, 45, 1.0, "bilinear");

    mm::show(std::vector<mm::Image>{img_gray, img_rot30, img_rot45},
             MM_OUT,
             std::vector<std::string>{"Original", "Rotação 30°", "Rotação 45°"},
             3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_02_rotacao_0.png");
mm::write(img_rot30, "tmp/fig_02_rotacao_1.png");
mm::write(img_rot45, "tmp/fig_02_rotacao_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_rotacao.cpp
!g++ -I. -std=c++17 tmp/fig_02_rotacao.cpp -o tmp/fig_02_rotacao \
  && ./tmp/fig_02_rotacao \
  && test -f "tmp/fig_02_rotacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_rotacao.png"
[1] Original
[2] Rotação 30°
[3] Rotação 45°
try:
    mm.show(
        [
            mm.read("tmp/fig_02_rotacao_0.png"),
            mm.read("tmp/fig_02_rotacao_1.png"),
            mm.read("tmp/fig_02_rotacao_2.png"),
        ],
        titles=[
            'Original',
            'Rotação 30°',
            'Rotação 45°',
        ],
        cols=3,
        figsize=(16, 12),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_rotacao_0.png (ver a versao Python)")
Figure 2.9: Exemplo de rotação da imagem do pássaro em 30° e 45° usando interpolação bilinear.

2.8.3 Échelle (Redimensionnement)

Le redimensionnement par facteurs \((s_x, s_y)\) est une transformation affine de matrice :

\[ \mathbf{T}_{\text{échelle}} = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \tag{2.5}\]

Lorsque \(s > 1\) (agrandissement), les pixels de l’image cible correspondent à des positions non entières dans l’image source — ce qui exige une interpolation pour estimer la valeur. Lorsque \(s < 1\) (réduction), plusieurs pixels source contribuent à un seul pixel cible — ce qui exige une décimation. Les trois mêmes méthodes décrites pour la rotation sont disponibles dans mm::resize, à la différence près qu’ici l’impact visuel est plus perceptible : en agrandissement, nearest produit un effet de blocs (pixelation), tandis que bicubic préserve mieux la netteté des contours, conformément à Table 2.5:

Table 2.5: Méthodes d’interpolation disponibles dans mm.resize et leurs nombres respectifs de voisins utilisés dans le calcul.
Méthode Voisins utilisés Caractéristique
'nearest' 1 Rapide ; produit un effet de blocs (pixelation)
'bilinear' 4 Bon compromis qualité/coût ; contours lisses
'bicubic' 16 Netteté accrue ; préférée dans les logiciels professionnels

Le paramètre size_or_factor accepte aussi bien un scalaire (facteur uniforme, e.g. 0.5 pour réduire de moitié) qu’un tuple (largeur, hauteur) pour des dimensions absolues.

%%writefile tmp/fig_02_escala_detalhe.cpp
#define MM_OUT "tmp/fig_02_escala_detalhe.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    //| label: fig-02-escala-detalhe
    //| fig-cap: "Comparação de interpolação com zoom no detalhe do olho (recorte 60×60, ampliado 4×). Note o efeito de blocos no vizinho mais próximo vs. a suavização na bilinear."
    //| echo: true
    //| output: true

    // 1. Recorte da região do bico
    int y = 210, x = 40, offset = 40;
    mm::Image crop = mm::crop(img_gray, y-offset, y+offset, x-offset, x+offset);
    std::cout << "Imagem: " << img_gray.h << "x" << img_gray.w << " | Crop: " << crop.h << "x" << crop.w << "\n";

    // 2. Ampliar 4× com mm.resize
    mm::Image crop_nearest  = mm::resize(crop, 4.0, "nearest");
    mm::Image crop_bilinear = mm::resize(crop, 4.0, "bilinear");

    // 3. Exibição comparativa
    mm::show(
        std::vector<mm::Image>{crop, crop_nearest, crop_bilinear},
        MM_OUT,
        std::vector<std::string>{"Original (recorte)", "Vizinho mais próximo (4×)", "Bilinear (4×)"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(crop, "tmp/fig_02_escala_detalhe_0.png");
mm::write(crop_nearest, "tmp/fig_02_escala_detalhe_1.png");
mm::write(crop_bilinear, "tmp/fig_02_escala_detalhe_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_escala_detalhe.cpp
!g++ -I. -std=c++17 tmp/fig_02_escala_detalhe.cpp -o tmp/fig_02_escala_detalhe \
  && ./tmp/fig_02_escala_detalhe \
  && test -f "tmp/fig_02_escala_detalhe.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_escala_detalhe.png"
Imagem: 1030x660 | Crop: 80x80
[1] Original (recorte)
[2] Vizinho mais próximo (4×)
[3] Bilinear (4×)
try:
    mm.show(
        [
            mm.read("tmp/fig_02_escala_detalhe_0.png"),
            mm.read("tmp/fig_02_escala_detalhe_1.png"),
            mm.read("tmp/fig_02_escala_detalhe_2.png"),
        ],
        titles=[
            'Original (recorte)',
            'Vizinho mais próximo (4×)',
            'Bilinear (4×)',
        ],
        cols=3,
        figsize=(16, 12),
        dpi=200,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_escala_detalhe_0.png (ver a versao Python)")
Figure 2.10: Comparação de interpolação com zoom no detalhe do olho (recorte 60×60, ampliado 4×). Note o efeito de blocos no vizinho mais próximo vs. a suavização na bilinear.

2.8.4 Cisaillement (Shear)

Le cisaillement est une transformation affine qui déforme l’image en déplaçant chaque pixel proportionnellement à sa position sur un axe. La matrice générale combine un cisaillement horizontal (\(sh_x\)) et un cisaillement vertical (\(sh_y\)) :

\[ \mathbf{T}_{\text{cisalh}} = \begin{bmatrix} 1 & sh_x & 0 \\ sh_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \tag{2.6}\]

Pour \(sh_x \neq 0\) et \(sh_y = 0\), chaque ligne est déplacée horizontalement proportionnellement à sa position verticale — produisant l’effet d’« inclinaison » caractéristique. Voir l’exemple dans la Figure 2.11.

%%writefile tmp/fig_02_cisalhamento.cpp
#define MM_OUT "tmp/fig_02_cisalhamento.png"
//| label: fig-02-cisalhamento
//| fig-cap: "Exemplo de cisalhamento da imagem do pássaro: horizontal (shx=0.3), vertical (shy=0.3) e combinado (shx=0.2, shy=0.2)."
//| echo: true
//| output: true

#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_22.png");
// [pdi:state-io:end]

    mm::Image img_shx = mm::shear(img_gray, 0.3);
    mm::Image img_shy = mm::shear(img_gray, 0.0, 0.3);
    mm::Image img_shc = mm::shear(img_gray, 0.2, 0.2);

    mm::show(
        std::vector<mm::Image>{img_gray, img_shx, img_shy, img_shc},
        MM_OUT,
        std::vector<std::string>{"Original", "Horiz. (shx=0.3)", "Vert. (shy=0.3)", "Combinado (0.2, 0.2)"},
        4
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_02_cisalhamento_0.png");
mm::write(img_shx, "tmp/fig_02_cisalhamento_1.png");
mm::write(img_shy, "tmp/fig_02_cisalhamento_2.png");
mm::write(img_shc, "tmp/fig_02_cisalhamento_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_cisalhamento.cpp
!g++ -I. -std=c++17 tmp/fig_02_cisalhamento.cpp -o tmp/fig_02_cisalhamento \
  && ./tmp/fig_02_cisalhamento \
  && test -f "tmp/fig_02_cisalhamento.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_cisalhamento.png"
[1] Original
[2] Horiz. (shx=0.3)
[3] Vert. (shy=0.3)
[4] Combinado (0.2, 0.2)
try:
    mm.show(
        [
            mm.read("tmp/fig_02_cisalhamento_0.png"),
            mm.read("tmp/fig_02_cisalhamento_1.png"),
            mm.read("tmp/fig_02_cisalhamento_2.png"),
            mm.read("tmp/fig_02_cisalhamento_3.png"),
        ],
        titles=[
            'Original',
            'Horiz. (shx=0.3)',
            'Vert. (shy=0.3)',
            'Combinado (0.2, 0.2)',
        ],
        cols=4,
        figsize=(16, 12),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_cisalhamento_0.png (ver a versao Python)")
Figure 2.11: Exemplo de cisalhamento da imagem do pássaro: horizontal (shx=0.3), vertical (shy=0.3) e combinado (shx=0.2, shy=0.2).

2.9 Résumé

Dans ce chapitre, les principes fondamentaux de la numérisation et de la topologie des images ont été présentés :

  • Formation de l’image : \(f(x,y) = i(x,y) \cdot r(x,y)\).
  • Échantillonnage : discrétisation de l’espace → résolution spatiale \(M \times N\).
  • Quantification : discrétisation de l’intensité → profondeur de bits \(b\).
  • Relations topologiques : voisinage-4, voisinage-8, connectivité, distances (euclidienne, Manhattan, Chebyshev).
  • Transformations géométriques : translation, rotation, mise à l’échelle (avec interpolation bilinéaire ou plus proche voisin).
  • Formats de fichiers : BMP, PNG, JPEG, TIFF, RAW ; chacun présentant différents compromis entre qualité et taille.

Le chapitre 3 abordera les opérations spatiales telles que la convolution, le filtrage et la morphologie mathématique (érosion, dilatation).

2.10 🤖 Utilisation de Gemini Notebook comme tuteur complémentaire

Dans cette édition, nous encourageons l’utilisation de Gemini Notebook comme outil d’apprentissage complémentaire. Cet outil d’IA utilise exclusivement les documents fournis par l’auteur comme base de connaissances, garantissant des réponses cohérentes avec le contenu du livre.

Pour chaque chapitre, nous avons préparé un projet spécifique sur la plateforme. Pour une expérience d’étude enrichie, utilisez l’accès ci-dessous :

Important🎓 Étudiez avec le tuteur intelligent

Pour interagir avec le contenu de ce chapitre, accédez au lien suivant. L’environnement contient des supports pédagogiques dans différents formats, générés à partir du PDF du chapitre. Sur la plateforme, explorez particulièrement les options Guide d’étude et Conversation pour approfondir votre compréhension.

🚀 ACCÉDER À GEMINI NOTEBOOK : CHAPITRE 02

🌐 Langue et langage de programmation

Le projet de ce chapitre dans Gemini Notebook a été construit uniquement avec le texte en portugais et les exemples de code en Python. Si vous étudiez avec l’édition en anglais ou en français, ou si vous suivez le parcours en C++, les réponses du tuteur peuvent ne pas correspondre exactement à la version que vous lisez.

⚠️ Avertissement concernant le contenu généré par l’IA

L’IA est une alliée puissante dans les études, mais le contenu généré peut contenir des erreurs ou des imprécisions. Consultez toujours des livres, des articles scientifiques et d’autres sources académiques fiables pour valider les informations. Chaque fois que possible, exécutez les exemples pratiques fournis dans ce chapitre pour vérifier les résultats.

2.11 Liste d’exercices

  1. (15 %) Expliquez, avec vos propres mots, la différence entre échantillonnage et quantification. Donnez un exemple concret de chacun dans le contexte d’une image numérique.

  2. (15 %) Considérez une image avec une résolution spatiale de 1024 × 768 pixels et une profondeur de 24 bits (8 bits par canal RVB). Calculez la taille totale non compressée de l’image en octets et en mégaoctets.

  3. (20 %) À l’aide du code du laboratoire, modifiez le facteur de sous-échantillonnage à 3 et à 6. Décrivez visuellement ce qui se produit au niveau des bords des objets. Qu’est-ce que l’effet de crénelage (aliasing) ?

  4. (20 %) Pour l’image en niveaux de gris, appliquez une quantification avec 3 bits (8 niveaux) et 5 bits (32 niveaux). Comparez les résultats et expliquez pourquoi 5 bits peuvent déjà être considérés comme suffisants pour de nombreuses applications.

  5. (15 %) Étant donné deux pixels \(A=(10,20)\) et \(B=(15,25)\), calculez les distances euclidienne, de Manhattan et de Tchebychev entre eux.

  6. (15 %) À l’aide de la fonction mm::rotate, faites pivoter l’image de l’oiseau selon des angles de 90°, 180° et 270° avec une interpolation bilinéaire. Comparez avec la rotation utilisant method='nearest'. Dans quelles situations l’interpolation du plus proche voisin est-elle encore utile ?

Références du chapitre

La base théorique de ce chapitre s’appuie sur les ouvrages suivants :

  • Gonzalez (2018) pour les concepts d’échantillonnage, de quantification et les relations entre pixels.
  • Szeliski (2022) pour les transformations géométriques et la connectivité.
  • Bradski (2008) pour l’implémentation pratique avec OpenCV et morph.py.

2.12 💻 Partie Pratique avec Exercices de Programmation

🎯 Objectif de ce cahier

Le cahier permet de développer, valider, organiser et tester des solutions d’Exercices de Programmation (EPs) dans des environnements interactifs, comme Colab, avec les mêmes cas de test que Moodle, en y copiant uniquement au moment d’enregistrer la note officielle.

Téléchargement

Téléchargez morph.py et testsuite.py en exécutant la cellule ci-dessous :

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefacts de build du parcours C++ (.cpp, binaire, PNGs)

url = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"
if not os.path.exists("config.py"):
    urllib.request.urlretrieve(url, "config.py")

# Le noyau est Python même dans le parcours C++ : `mm` (morph.py) est utilisé par les
# simulateurs, par l'affichage des figures que le binaire C++ génère et par
# l'état mm::Image entre les cellules. cpp=True télécharge aussi le parcours compilé
# (morph.hpp + stb_image*.h), utilisé dans le #include des cellules %%writefile *.cpp.
import config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Environnement prêt. Morph : 1.1.9 | OpenCV : 5.0.0 | TestSuite : 1.1.2

Exécution des tests

Pour évaluer les tests, exécutez TestSuite("EP04_01.extensão").run() dans une nouvelle cellule, en remplaçant l’extension par celle du langage utilisé (.py, .java, .c, .cpp, .js ou .r). Le système télécharge les cas de test depuis GitHub, exécute le programme et calcule automatiquement la note.

Pour tester directement du code Python, sans enregistrer de fichier, utilisez run_code(codigo) en passant le code comme chaîne de caractères dans une variable codigo :

codigo = """
from morph import mm
# ... votre code ici ...
"""
TestSuite("EP04_01").run_code(codigo)

2.12.1 EP02_01 ☀️ Réglage de la luminosité et du contraste

Dans cette activité, l’objectif est d’implémenter un opérateur ponctuel pour la transformation linéaire d’intensité, en appliquant le réglage dynamique de la luminosité et du contraste sur une image numérique.

2.12.1.1 📋 Directives d’implémentation

L’algorithme doit suivre le flux d’exécution ci-dessous :

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes) de la matrice.
  2. Paramètres : Lire la valeur réelle \(\alpha\) (facteur de contraste) et l’entier \(\beta\) (facteur de luminosité).
  3. Données : Lire les valeurs entières de la matrice d’origine.
  4. Mappage : Pour chaque pixel \(p\), calculer la nouvelle valeur \(p'\) à l’aide de l’équation :

\[p' = \text{clip}(\text{round}(\alpha \cdot p + \beta))\]

  1. Sortie : Afficher la matrice résultante avec les dimensions \(L \times C\).

2.12.1.2 📌 Contraintes informatiques

  • Arrondi (Round) : On applique l’arrondi mathématique à l’entier le plus proche avant la conversion de type.
  • Saturation (Clipping) : Les valeurs doivent être confinées à l’intervalle \([0, 255]\) pour préserver le standard 8 bits :

\[\text{clip}(x) = \max(0, \min(255, x))\]

  • Simulation : L’effet des paramètres \(\alpha\) et \(\beta\) sur la correction histogrammique peut être observé dans la Figure 2.12.

2.12.1.3 🧠 Fondements théoriques

Les modifications altèrent l’histogramme de l’image pour ajuster le profil d’éclairage et la distinction tonale.

Paramètre Fonction Impact visuel
\(\alpha\) (Alpha) Scalaire Module le Contraste. Si \(\alpha > 1\), il étend l’histogramme ; si \(0 \le \alpha < 1\), il le compresse.
\(\beta\) (Beta) Additif Module la Luminosité. S’il est positif, il translate l’histogramme vers la droite ; s’il est négatif, vers la gauche.
\(\text{clip}\) Limiteur Restreint la plage dynamique, empêchant les erreurs de sous-dépassement et de dépassement.

2.12.1.4 📦 Spécification d’entrée et de sortie (VPL)

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Valeurs de alpha (\(\alpha\)) et beta (\(\beta\)).
  • Lignes suivantes : Éléments numériques de la matrice d’origine.

Sortie :

  • Matrice transformée structurée en \(L\) lignes et \(C\) colonnes.

2.12.1.5 📌 Exemples

Le tableau suivant présente un exemple pratique du comportement attendu de l’algorithme, mettant en évidence l’action des opérateurs d’arrondi et de saturation.

Entrée Sortie Observation
1
4
1.5 -30
0 100 180 255
0 120 240 255 Notez l’effet de saturation sur le dernier pixel

Exécution des Tests

Pour évaluer les tests, exécutez TestSuite("EP02_01.extension").run() dans une nouvelle cellule, en remplaçant l’extension par celle du langage utilisé (.py, .java, .c, .cpp, .js ou .r). Le système télécharge les cas de test depuis GitHub, exécute le programme et calcule la note automatiquement.

☀️ Simulateur EP02_01 : Ajustement du Luminosité et Contraste Linéaire p' = clip(α·p + β)

Ajustez les paramètres de contraste (α) et de luminosité (β) pour appliquer la transformation ponctuelle d'intensité et observez l'écrêtage de saturation dans l'intervalle [0, 255].

1.0
0
Entrée Originale (p)
Résultat Transformé (p')
Formule appliquée : clip( round(1.0 · p + (0)) )
Figure 2.12: Simulateur EP02_01 : Ajustement de la Luminosité et du Contraste Linéaire (p’ = αp + β)
%%writefile EP02_01.cpp
// your solution
Overwriting EP02_01.cpp
TestSuite("EP02_01.cpp").run()
✔️ EP02_01.cases existe déjà dans casos/
📋 8 cas chargé(s) depuis casos/EP02_01.cases

🔍 Test de C++ : EP02_01.cpp
⚠️ EP02_01.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.2 EP02_02 🔬 Sous-échantillonnage spatial

Dans cette activité, vous devez implémenter la réduction de la résolution spatiale d’une image par le processus de sous-échantillonnage.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice d’origine.
  • Lisez une valeur entière \(f\) (\(f \ge 1\)), qui représente le facteur d’échantillonnage.
  • Lisez les valeurs entières de la matrice d’origine.
  • La nouvelle image doit être construite en sélectionnant le pixel de la position \((f \cdot i, f \cdot j)\) de l’image d’origine.
  • Imprimez la matrice résultante avec les nouvelles dimensions.
  • Voir dans Figure 2.13 une simulation de cet EP.

📌 Important :

  • Dimensions finales : L’image échantillonnée aura pour dimensions \(\lceil L/f \rceil \times \lceil C/f \rceil\). Dans le contexte de la programmation, cela équivaut à la taille résultante d’un découpage (slicing) avec un pas \(f\).
  • Implémentation : N’utilisez pas de fonctions toutes faites issues de bibliothèques de traitement d’images (comme OpenCV ou PIL) pour le redimensionnement. Implémentez la logique de sélection des pixels manuellement ou via le découpage de matrices.
  • Aliasing : Notez que ce processus peut provoquer l’effet d’aliasing (crénelage), où les détails fins sont perdus ou des motifs indésirables apparaissent.

2.12.2.1 🧠 Discrétisation de l’espace

Le sous-échantillonnage réduit la résolution spatiale d’une image en ne sélectionnant qu’un pixel tous les \(f\) pixels dans chaque direction. C’est le processus inverse de l’interpolation :

Paramètre Fonction Effet
Facteur \(f\) Saut d’échantillonnage Définit l’intervalle de sélection. Un facteur \(2\) réduit la largeur et la hauteur de moitié.
Résolution Densité de pixels Diminue la quantité totale d’informations spatiales de l’image.
Aliasing Effet secondaire Apparition de motifs en escalier ou en blocs en raison de la perte de détails fins.

2.12.2.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient le facteur f.

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice réduite avec les dimensions correspondant au découpage par f.

2.12.2.3 📌 Exemples

Entrée Sortie Observation
2
4
2
10 20 30 40
50 60 70 80
10 30 Le facteur 2 sélectionne les pixels (0,0) et (0,2) de la première ligne. La deuxième ligne est ignorée.
🔽 Simulateur EP02_02 : Sous-échantillonnage Spatial d'Image p'(i, j) = p(i·f, j·f)

Ajustez le facteur de sous-échantillonnage (f) pour observer la réduction de la dimension spatiale de la matrice et l'échantillonnage par saut des pixels supérieurs gauches de chaque bloc f × f.

1
f = 1 → Résolution Originale (4×4)  |  f = 2 → Moitié (2×2)  |  f = 3 ou 4 → Échantillon Unique (1×1)
Originale (4×4)
Sous-échantillonnée (Taille Variable)
Facteur f = 1 → conserve tous les pixels originaux (4×4)
Figure 2.13: Simulateur EP02_02: Sous-échantillonnage spatial (réduction de résolution par saut f)
%%writefile EP02_02.cpp
// your solution
Overwriting EP02_02.cpp
TestSuite("EP02_02.cpp").run()
✔️ EP02_02.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_02.cases

🔍 Test de C++ : EP02_02.cpp
⚠️ EP02_02.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.3 EP02_03 🎨 Quantification des niveaux de gris

Dans cette activité, vous devez implémenter la quantification uniforme d’une image, en réduisant le nombre de niveaux d’intensité de gris originaux vers une nouvelle échelle basée sur un nombre de bits plus petit.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice.
  • Lisez un entier \(k\) (\(1 \le k \le 8\)), représentant le nouveau nombre de bits de l’image.
  • Calculez le nombre de niveaux (\(N = 2^k\)) et la taille de l’intervalle (pas).
  • Pour chaque pixel \(p\), calculez la nouvelle valeur \(p'\) en le mappant vers l’indice du niveau discrétisé correspondant (variant de \(0\) à \(2^k-1\)).
  • Affichez la matrice résultante avec les mêmes valeurs de dimensions que l’originale.
  • Consultez Figure 2.14 pour une simulation de cet EP.

📌 Important :

  • Postérisation : En réduisant drastiquement les niveaux (ex : \(k=2\)), vous remarquerez que les dégradés lisses se transforment en bandes abruptes de couleur en raison de la perte de résolution d’amplitude.
  • Calcul du pas : L’intervalle entre chaque niveau est défini par \(pas = 256 / 2^k\).
  • Mappage : La méthode de quantification uniforme par troncature qui mappe le pixel vers l’indice de son niveau discrétisé respectif est donnée par :

\[p' = \left\lfloor \frac{p}{pas} \right\rfloor\]

En termes d’implémentation (comme en Python), cela équivaut à la division entière : p' = p // pas.

2.12.3.1 🧠 Discrétisation de l’amplitude

Alors que le sous-échantillonnage traite de la résolution spatiale, la quantification se concentre sur la précision de la couleur (amplitude). Réduire les bits signifie simplifier l’information chromatique :

Paramètre Fonction Effet
Bits (\(k\)) Profondeur de couleur Définit combien de tons différents l’image peut avoir (\(2^k\)).
Pas Intervalle de ton Espacement entre les niveaux de gris autorisés.
Postérisation Phénomène visuel Transformation de variations continues en blocs de couleur unie.

2.12.3.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient le nombre de bits k.

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice transformée avec les indices des niveaux quantifiés, en conservant la taille originale \(L \times C\).

2.12.3.3 📌 Exemples

Entrée Sortie Observation
1
4
2
0 80 170 255
0 1 2 3 Avec \(k=2\), nous avons \(2^2=4\) niveaux discrets disponibles (\(0,1,2,3\)). Le pas est de \(256/4=64\). En appliquant la division entière élément par élément : \(0 // 64 = 0\), \(80 // 64 = 1\), \(170 // 64 = 2\), \(255 // 64 = 3\).
1
5
1
10 50 120 200 250
0 0 0 1 1 Avec \(k=1\), nous avons \(2^1=2\) niveaux (\(0\) et \(1\)). Pas \(=256/2=128\). Les pixels inférieurs à \(128\) donnent \(0\), et les pixels supérieurs ou égaux à \(128\) donnent \(1\).
🎚️ Simulateur EP02_03 : Quantification et profondeur de bits q = round(p · (L − 1) / 255)

Ajustez le nombre de bits de sortie (b) pour observer le mappage des 256 niveaux continus de gris vers L = 2ᵇ niveaux discrets de quantification.

8
Niveaux discrets (L = 2ᵇ) : 256  |  Valeurs affichées : 0 a 255
Original (8 bits → 0…255)
Quantifié (Plage 0…255)
Bits de sortie = 8 → 256 niveaux (valeurs originales préservées)
Figure 2.14: Simulateur EP02_03 : Quantification et Profondeur de Bits (Réduction du Nombre de Niveaux de Gris)
%%writefile EP02_03.cpp
// your solution
Overwriting EP02_03.cpp
TestSuite("EP02_03.cpp").run()
✔️ EP02_03.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_03.cases

🔍 Test de C++ : EP02_03.cpp
⚠️ EP02_03.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.4 EP02_04 📐 Transformée de distance sur image binaire

Étant donné une image binaire où les pixels de valeur 1 représentent l’objet et les pixels 0 représentent le fond, la distance d’un pixel de fond reçoit la distance minimale au pixel d’objet le plus proche. Les pixels d’objet reçoivent une distance 0. Pour simplifier, considérez que l’image ne contient qu’un seul objet avec un seul pixel de valeur 1.

Problème : Lire une image binaire \(L \times C\) et une métrique, puis calculer cette distance simplifiée en appliquant l’une des trois formules :

\[d_{\text{Euclidienne}} = \sqrt{(\Delta r)^2 + (\Delta c)^2}\]

\[d_{\text{City-block}} = |\Delta r| + |\Delta c|\]

\[d_{\text{Échiquier}} = \max(|\Delta r|,\; |\Delta c|)\]

où \(\Delta r\) est la différence de lignes et \(\Delta c\) la différence de colonnes entre deux pixels.

2.12.4.1 🖼️ Pourquoi cela importe-t-il ? - Applications de la TD

La Transformée de distance (TD) apparaît dans des dizaines de pipelines de vision par ordinateur :

Métrique Complexité Application typique
Euclidienne 🔴 \(O(n^2)\) naïve Squelettisation, correspondance de formes
City-block 🟡 \(O(n)\) avec 2 passes Morphologie, dilatation/érosion
Échiquier 🟢 \(O(n)\) avec 2 passes Morphologie, dilatation/érosion

2.12.4.2 📌 Exigences techniques

  • Entrée : * Première ligne : \(L\) et \(C\) (entiers).
    • Deuxième ligne : nom de la métrique (euclidean, cityblock ou chessboard).
    • Ensuite, la matrice binaire \(L \times C\) (valeurs 0 ou 1).
  • Pixels d’objet (1) : distance \(= 0\) (ou \(0.00\) pour euclidienne).
  • Pixels de fond (0) : distance au seul pixel d’objet de l’image.
  • Arrondi (euclidienne) : imprimer avec 2 décimales (format :.2f). City-block et Échiquier produisent des entiers — imprimer sans décimales.
  • Sortie : valeurs séparées par des espaces, une ligne par ligne de la matrice.
  • Voir dans Figure 2.15 une simulation de cet EP.

2.12.4.3 📌 Exemples

Entrée Sortie Observation
4
4
chessboard
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 0
2 2 2 2
2 1 1 1
2 1 0 1
2 1 1 1
La distance Échiquier est \(\max(\|dx\|, \|dy\|)\). Le seul pixel objet est \((2,2)=0\) ; les autres stockent leur distance minimale jusqu’à lui.

2.12.4.4 📌 Observations finales

  • Comme l’image ne contient qu’un seul objet d’un pixel, la distance de chaque pixel de fond est simplement la distance de ce pixel au seul point objet.
  • L’implémentation peut utiliser la force brute (parcourir tous les pixels de l’image et calculer la distance directement), car \(L\) et \(C\) sont petits dans les cas de test.
  • Ce problème est un échauffement pour la Transformée de distance générale, qui sera travaillée dans des chapitres ultérieurs avec plusieurs objets et des algorithmes optimisés.
📐 Simulateur EP02_04 : Transformée de distance interactive Métriques : L₁, L₂ et L_∞

Cliquez sur les cellules de l'image binaire pour alterner les pixels de l'objet (1) et observez la carte de distance minimale calculée dans la matrice résultante.

Métrique :
Image binaire (cliquer pour modifier)
Transformée de distance
Grille 5×5 · 1 pixel(s) d'objet · Métrique : Échiquier (entier)
Figure 2.15: Simulateur EP02_04 : Transformée de distance en image binaire (Chessboard, City-block et Euclidienne)
%%writefile EP02_04.cpp
// your solution
Overwriting EP02_04.cpp
TestSuite("EP02_04.cpp").run()
✔️ EP02_04.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_04.cases

🔍 Test de C++ : EP02_04.cpp
⚠️ EP02_04.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.5 EP02_05 ➡️ Translation d’image

Dans cette activité, vous devez implémenter le déplacement spatial d’une image. La translation déplace chaque pixel de l’image originale vers une nouvelle position en fonction d’un vecteur de déplacement.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice.
  • Lisez deux entiers \(t_x\) (déplacement horizontal) et \(t_y\) (déplacement vertical).
  • Lisez les valeurs entières de la matrice originale.
  • Calculez la nouvelle position \((x', y')\) pour chaque pixel \((x, y)\) original.
  • Affichez la matrice résultante avec les mêmes dimensions que l’originale.
  • Voir dans Figure 2.16 une simulation de cet EP.

📌 Important :

  • Remplissage : Les pixels qui « entrent » dans l’image en raison du déplacement et qui n’ont pas de correspondant dans l’originale doivent être remplis avec 0 (noir).
  • Suppression : Les pixels qui, après la translation, sortent des limites de la matrice (\(0 \dots L-1\) ou \(0 \dots C-1\)) doivent être ignorés.
  • Coordonnées : Considérez \(x\) comme l’indice de la ligne et \(y\) comme l’indice de la colonne.

2.12.5.1 🧠 Déplacement spatial

Translater une image signifie déplacer tous ses points d’une distance fixe dans des directions spécifiées. Mathématiquement, en utilisant les coordonnées homogènes, l’opération est décrite comme suit :

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]

Ce qui donne les équations simples :

  • \(x' = x + t_x\)
  • \(y' = y + t_y\)

2.12.5.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient les entiers tx et ty.

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice résultante avec les mêmes dimensions \(L \times C\) après le déplacement.

2.12.5.3 📌 Exemples

Entrée Sortie Observation
2
2
1 1
10 20
30 40
0 0
0 10
Déplacement (\(t_x=1, t_y=1\)): Chaque pixel se déplace d’une position vers la droite (horizontal) et d’une vers le bas (vertical). Le pixel \((0,0)=10\) va vers la destination \((1,1)\) (coin inférieur droit). Les positions vides sont remplies avec \(0\).
3
3
-1 0
1 2 3
4 5 6
7 8 9
2 3 0
5 6 0
8 9 0
Déplacement (\(t_x=-1, t_y=0\)): Chaque pixel se déplace d’une position vers la gauche (horizontal). La première colonne originale (1, 4, 7) est supprimée, les autres colonnes se déplacent vers la gauche, et la dernière colonne résultante est remplie de zéros (\(0\)).
📍 Simulateur EP02_05 : Translation géométrique 2D p'(i, j) = p(i − ty, j − tx)

Ajustez les déplacements horizontal (tx) et vertical (ty) pour observer le mappage inverse des coordonnées et le remplissage avec zéro (noir) pour les pixels hors des limites de l'image d'origine.

0
0
Original (4×4)

Translations (tx, ty)

tx = 0, ty = 0 → aucun déplacement (image d'origine conservée)
Figure 2.16: Simulateur EP02_05 : Translation géométrique d’image (Déplacement tx et ty avec remplissage de bordure)
%%writefile EP02_05.cpp
// your solution
Overwriting EP02_05.cpp
TestSuite("EP02_05.cpp").run()
✔️ EP02_05.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_05.cases

🔍 Test de C++ : EP02_05.cpp
⚠️ EP02_05.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.6 EP02_06 🔄 Rotation d’image

Dans cette activité, vous devez implémenter la rotation d’une image autour de son centre géométrique. Cette opération nécessite le mappage des coordonnées et l’utilisation de techniques d’interpolation pour déterminer les nouvelles valeurs des pixels.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice.
  • Lisez une valeur réelle \(\theta\) (angle en degrés) et une chaîne de caractères représentant la méthode d’interpolation (nearest ou bilinear).
  • Lisez les valeurs entières de la matrice originale.
  • Effectuez la rotation autour du centre de l’image \((L/2, C/2)\).
  • Affichez la matrice résultante avec les mêmes dimensions que l’originale.
  • Voir dans Figure 2.17 une simulation de cet EP.

📌 Important :

  • Mappage inverse : Pour éviter les « trous » dans l’image finale, parcourez chaque pixel \((x', y')\) de l’image de destination et calculez sa position correspondante \((x, y)\) dans l’image originale en utilisant la matrice de rotation inverse.

  • Interpolation :

  • nearest : Attribue la valeur du pixel le plus proche de la coordonnée calculée.

  • bilinear : Calcule une moyenne pondérée basée sur les 4 voisins les plus proches.

  • Bords : Les pixels dont l’origine \((x, y)\) tombe en dehors des limites de l’image originale doivent être remplis avec 0.

2.12.6.1 🧠 Transformation par angle

La rotation d’un point \((x, y)\) par rapport à l’origine d’un angle \(\theta\) est donnée par la matrice de transformation. Pour effectuer une rotation autour d’un centre \((x_c, y_c)\), on translate d’abord le centre vers l’origine, on effectue la rotation, puis on translate en retour :

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta & x_c \\ \sin\theta & \cos\theta & y_c \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x-x_c \\ y-y_c \\ 1 \end{bmatrix}\]

Conseil : Utilisez le mappage inverse pour garantir que tous les pixels de l’image de sortie soient correctement remplis.

2.12.6.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient l’angle theta (en degrés) et la méthode interp (nearest ou bilinear).

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice tournée avec L lignes et C colonnes.

2.12.6.3 📌 Exemples

Entrée Sortie Observation
2
2
90 nearest
1 2
3 4
3 1
4 2
Rotation de 90° dans le sens horaire : la colonne 0 devient la ligne 0 (de bas en haut). \((0,0)=1→(1,0)\), \((1,0)=3→(0,0)\), \((0,1)=2→(1,1)\), \((1,1)=4→(0,1)\).
3
3
45 bilinear
0 0 0
0 255 0
0 0 0
0 180 0
180 255 180
0 180 0
Rotation de 45° : le pixel central reste \(255\) ; les voisins directs reçoivent une valeur interpolée \(\approx 180\) par bilinéaire ; les coins restent à \(0\).
🔄 Simulateur EP02_06 : Rotation géométrique 2D x' = x·cosθ − y·sinθ | y' = x·sinθ + y·cosθ

Ajustez l'angle de rotation (θ) via le curseur ou les raccourcis rapides pour observer la transformation trigonométrique des coordonnées autour du centre de l'image.

0°

● Carré vert avec marqueur orange (coin supérieur droit) – rotation autour du centre.

θ = 0° → cos = 1.000, sin = 0.000 → Matrice identité
Figure 2.17: Simulateur EP02_06 : Rotation d’image autour de l’origine par un angle θ
%%writefile EP02_06.cpp
// your solution
Overwriting EP02_06.cpp
TestSuite("EP02_06.cpp").run()
✔️ EP02_06.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_06.cases

🔍 Test de C++ : EP02_06.cpp
⚠️ EP02_06.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.7 EP02_07 🔍 Redimensionnement (Échelle)

Dans cette activité, vous devez implémenter le redimensionnement d’une image à l’aide de facteurs d’échelle. Contrairement au sous-échantillonnage simple, nous utiliserons ici des techniques d’interpolation pour permettre à la fois l’agrandissement et la réduction de l’image.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice originale.
  • Lisez deux valeurs réelles \(s_x\) (échelle sur les lignes) et \(s_y\) (échelle sur les colonnes).
  • Lisez une chaîne de caractères représentant la méthode d’interpolation (nearest ou bilinear).
  • Lisez les valeurs entières de la matrice originale.
  • Calculez les nouvelles dimensions : \(L' = \text{round}(L \times s_x)\) et \(C' = \text{round}(C \times s_y)\).
  • Affichez la matrice résultante avec les nouvelles dimensions.
  • Voir dans Figure 2.18 une simulation de cet EP.

📌 Important :

  • Mappage inverse : Pour chaque pixel \((x', y')\) de l’image de destination, trouvez la position correspondante dans l’origine en utilisant \((x, y) = (x'/s_x, y'/s_y)\).

  • Interpolation :

  • nearest : Sélectionne la valeur du pixel le plus proche (arrondi des coordonnées).

  • bilinear : Effectue une interpolation linéaire double entre les quatre pixels voisins les plus proches dans l’image originale.

  • Bords : Assurez-vous que le mappage ne tente pas d’accéder à des indices hors de l’intervalle \([0, L-1]\) et \([0, C-1]\).

2.12.7.1 🧠 Interpolation pour Agrandissement/Réduction

Redimensionner une image par des facteurs \((s_x, s_y)\) exige le remplissage des vides (lors de l’agrandissement) ou la fusion d’informations (lors de la réduction). La méthode d’interpolation définit la qualité visuelle du résultat :

Méthode Fonctionnement Effet visuel
Nearest Prend la valeur du voisin le plus proche. Rapide, mais génère un effet “pixelisé” ou des blocs.
Bilinear Moyenne pondérée des 4 voisins (\(2 \times 2\)). Adoucit l’image, réduisant le crénelage.

2.12.7.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient les facteurs sx et sy.

La quatrième ligne contient la méthode interp (nearest ou bilinear).

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice redimensionnée avec les dimensions \(L' \times C'\).

2.12.7.3 📌 Exemples

Entrée Sortie Observation
2
2
2.0 2.0
nearest
1 2
3 4
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4
Agrandissement 2× : chaque pixel original est répliqué dans un bloc 2×2. L’image \(2\times2\) devient \(4\times4\).
2
2
0.5 0.5
nearest
10 20
30 40
10 Réduction 0.5× : l’image \(2\times2\) devient \(1\times1\). Avec nearest, le seul pixel de sortie échantillonne la position \((0,0)=10\).
🔍 Simulateur EP02_07 : Redimensionnement et interpolation (sx = sy) Nearest vs Bilinéaire

Ajustez le facteur d'échelle (s) pour comparer l'interpolation par plus proche voisin (réplique discrète) avec l'interpolation bilinéaire (moyenne pondérée des 4 voisins).

1.0
Facteur = 1.0 → Taille d'origine (3×3)  |  Facteur = 2.0 → 6×6  |  Facteur = 4.0 → 12×12
Original (3×3)
🔲 Plus proche voisin
🌀 Interpolation bilinéaire
Facteur = 1.00 → taille 3×3 (aucune modification)
Figure 2.18: Simulateur EP02_07 : Redimensionnement spatial et interpolation (Nearest Neighbor vs Bilinéaire)
# Not yet ported to this language in this version — conceptual reference in Python.
%%writefile EP02_07.py
# Code Python
import numpy as np
from morph import mm

# 1. Lecture des dimensions, facteurs et méthode
l = int(input())
c = int(input())
sx, sy = map(float, input().split())
interp = input().strip()

# 2. Lecture de l'image originale
img = mm.readImg(l, c)

# 3. Nouvelles dimensions
l_new = round(l * sx)
c_new = round(c * sy)

# 4. Redimensionnement avec mm.resize
# cv2.resize utilise (largeur, hauteur) = (colonnes, lignes)
resultado = mm.resize(img, (c_new, l_new), method=interp)

# 5. Affichage
print(mm.drawImg(resultado))
TestSuite("EP02_07.cpp").run()
✔️ EP02_07.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_07.cases
💥 Fichier EP02_07.cpp introuvable.

2.12.8 EP02_08 🔀 Cisaillement (Shear)

Dans cette activité, vous devez implémenter la transformation de cisaillement sur une image. Le cisaillement est une transformation affine qui décale chaque point dans une direction fixe, d’une valeur proportionnelle à sa distance par rapport à une droite parallèle à cette direction, produisant un effet d’inclinaison.

  • Lire deux entiers L et C, représentant les dimensions de la matrice.
  • Lire deux valeurs réelles \(sh_x\) (cisaillement horizontal) et \(sh_y\) (cisaillement vertical).
  • Lire une chaîne de caractères représentant la méthode d’interpolation (nearest ou bilinear).
  • Lire les valeurs entières de la matrice originale.
  • Appliquer la transformation en conservant la taille originale de l’image (en coupant ce qui dépasse les limites).
  • Afficher la matrice résultante avec les dimensions \(L \times C\).
  • Voir dans Figure 2.19 une simulation de cet EP.

📌 Important :

  • Mappage inverse : Pour chaque pixel \((x', y')\) de l’image de destination, calculer la position correspondante dans l’origine \((x, y)\) en utilisant la matrice de cisaillement inverse.
  • Remplissage : Les coordonnées qui aboutissent à des positions hors de la matrice originale doivent être remplies avec 0.
  • Coordonnées : Pour les besoins de cette implémentation, considérez \(x\) comme l’indice de ligne et \(y\) comme l’indice de colonne.

2.12.8.1 🧠 Distorsion affine

Le cisaillement modifie la géométrie de l’image en inclinant ses axes. La relation entre les coordonnées originales \((x, y)\) et les coordonnées transformées \((x', y')\) est donnée par :

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & sh_x & 0 \\ sh_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]

Cela donne les équations :

  • \(x' = x + sh_x \cdot y\)
  • \(y' = y + sh_y \cdot x\)

2.12.8.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient les facteurs shx et shy.

La quatrième ligne contient la méthode interp (nearest ou bilinear).

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice transformée avec les mêmes dimensions \(L \times C\).

2.12.8.3 📌 Exemples

Entrée Sortie Observation
3
3
0.5 0.0
nearest
10 20 30
40 50 60
70 80 90
10 20 30
0 40 50
0 0 70
Cisaillement horizontal : la ligne \(i\) se décale de \(\lfloor i \cdot 0.5 \rfloor\) pixels. Ligne \(0→0\)px, ligne \(1→0\)px, ligne \(2→1\)px. Les pixels décalés vers l’extérieur sont éliminés et les positions vides sont remplies avec \(0\).
2
2
0.0 1.0
nearest
10 20
30 40
10 0
30 20
Cisaillement vertical : la colonne \(j\) se décale de \(\lfloor j \cdot 1.0 \rfloor\) pixels vers le bas. Colonne \(0→0\)px (inchangée), colonne \(1→1\)px : \(20\) descend à \((1,1)\) et \((0,1)\) devient \(0\).
✂️ Simulateur EP02_08 : Cisaillement (Shear) 2D x' = x + shx·y | y' = y + shy·x

Ajustez les coefficients de cisaillement horizontal (shx) et vertical (shy) pour observer la déformation angulaire de l'image via le mappage inverse des coordonnées.

0.00
0.00
Original (4×4)
Cisaillée (Nearest Neighbor)
shx = 0.00, shy = 0.00 → aucune déformation angulaire (image originale)
Figure 2.19: Simulateur EP02_08 : Transformation géométrique de cisaillement 2D (Cisaillement horizontal et vertical)
%%writefile EP02_08.cpp
// your solution
Overwriting EP02_08.cpp
TestSuite("EP02_08.cpp").run()
✔️ EP02_08.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_08.cases

🔍 Test de C++ : EP02_08.cpp
⚠️ EP02_08.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.9 EP02_09 🧩 Transformation Affine Générique

Dans cette activité, vous devez implémenter une transformation affine arbitraire sur une image. Cette opération est la généralisation de toutes les transformations linéaires (mise à l’échelle, rotation, cisaillement) combinées à la translation, permettant des manipulations géométriques complexes via une seule matrice.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice.
  • Lisez six valeurs réelles (\(a, b, t_x, c, d, t_y\)) qui composent la matrice de transformation affine \(2 \times 3\).
  • Lisez une chaîne de caractères représentant la méthode d’interpolation (nearest ou bilinear).
  • Lisez les valeurs entières de la matrice d’origine.
  • Appliquez la transformation en conservant la taille d’origine \(L \times C\).
  • Affichez la matrice résultante.
  • Voir Figure 2.20 pour une simulation de cet EP.

📌 Important :

  • Mappage inverse : Pour calculer la valeur de chaque pixel dans l’image de destination, vous devez utiliser l’inverse de la matrice de transformation affine fournie afin de trouver la coordonnée correspondante dans l’image d’origine.
  • Remplissage : Les coordonnées calculées qui tombent hors des limites \([0, L-1]\) et \([0, C-1]\) de l’image d’origine doivent donner un pixel de valeur 0.
  • Flexibilité : Cette implémentation doit être capable d’effectuer n’importe laquelle des tâches précédentes (translation, rotation, etc.) en modifiant simplement les paramètres de la matrice.

Astuce :

flags = cv2.INTER_NEAREST if interp == 'nearest' else \
        cv2.INTER_CUBIC   if interp == 'bicubic'  else \
        cv2.INTER_LANCZOS4 if interp == 'lanczos' else \
        cv2.INTER_LINEAR

r = cv2.warpAffine(img, M, (C, L), flags=flags)

2.12.9.1 🧠 Combinaison d’opérations

La transformation affine préserve les points, les droites et les plans. En traitement d’images, elle mappe la position \((x, y)\) vers \((x', y')\) selon le système :

\[\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} + \begin{bmatrix} t_x \\ t_y \end{bmatrix}\]

Ou, de manière compacte en coordonnées homogènes :

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & t_x \\ c & d & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]

2.12.9.2 📋 Tâche (spécification pour VPL)

Entrée :

La première ligne contient L.

La deuxième ligne contient C.

La troisième ligne contient six flottants : a b tx c d ty.

La quatrième ligne contient la méthode interp (nearest ou bilinear).

Les lignes suivantes contiennent les éléments de la matrice \(L \times C\).

Sortie :

La matrice transformée avec les dimensions d’origine \(L \times C\).

2.12.9.3 📌 Exemples

Entrée Sortie Observation
2
2
1.0 0.0 0.5 0.0 1.0 0.5
bilinear
10 20
30 40
15 20
25 30
Translation fractionnaire \((t_x=0.5, t_y=0.5)\) : chaque pixel de sortie \((i,j)\) échantillonne la position \((i+0.5,\, j+0.5)\) de l’entrée via bilinéaire. Ex : \((0,0)\) interpole les quatre voisins \(→15\).
3
3
2.0 0.0 0.0 0.0 2.0 0.0
nearest
1 2 3
4 5 6
7 8 9
1 1 2
1 1 2
4 4 5
Mise à l’échelle \(2\times\) via matrice affine \((a=2, d=2)\) : chaque pixel de sortie \((i,j)\) échantillonne la position \((2i, 2j)\) de l’entrée avec le plus proche voisin. Ex : \((0,2)→(0,4)\) hors de l’image \(→\) le plus proche voisin découpe vers \((0,2)=3\)… en attente de confirmation de la logique de bord.
📐 Simulateur EP02_09 : Transformation affine 2D [x'] = [a b tx]·[x y 1]ᵀ

Ajustez les paramètres de la matrice affine 2×3 (rotation, échelle, cisaillement et translation) et observez l'effet appliqué sur la figure de référence.

Matrice affine 2×3
a b tx
c d ty

● Flèche orange (pointe triangulaire) + corps rectangulaire noir. La transformation affine est appliquée à la figure entière.

Matrice = [[1.0, 0.0, 0], [0.0, 1.0, 0]] → transformation identité.
Figure 2.20: Simulador EP02_09 : Transformação Afim 2D (Matriz 2×3)
%%writefile EP02_09.cpp
// your solution
Overwriting EP02_09.cpp
TestSuite("EP02_09.cpp").run()
✔️ EP02_09.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_09.cases

🔍 Test de C++ : EP02_09.cpp
⚠️ EP02_09.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.10 EP02_10 🎯 Correction de perspective (homographie)

Dans cette activité, vous devez implémenter la transformation de perspective, également connue sous le nom d’homographie. Contrairement aux transformations affines, la perspective ne préserve pas le parallélisme, ce qui permet de « redresser » des objets inclinés, comme des documents ou des panneaux capturés sous des angles obliques.

  • Lisez deux entiers L et C, représentant les dimensions de la matrice d’origine.
  • Lisez quatre paires de coordonnées \((x, y)\) représentant les coins du quadrilatère source (objet déformé).
  • Lisez quatre paires de coordonnées \((x, y)\) représentant les coins du quadrilatère de destination (là où l’objet doit être mappé).
  • Lisez les valeurs de la matrice d’origine.
  • Calculez la matrice d’homographie \(3 \times 3\) et appliquez la transformation.
  • Affichez la matrice résultante avec les dimensions de sortie spécifiées.
  • Voir dans Figure 2.21 une simulation de cet EP.

📌 Important :

  • Degrés de liberté : L’homographie possède 8 degrés de liberté (le neuvième élément de la matrice \(3 \times 3\) est une constante de normalisation, généralement 1), nécessitant au minimum 4 points correspondants pour être calculée.
  • Projection : Après avoir multiplié les coordonnées par la matrice, il est nécessaire de diviser les résultats \(x'\) et \(y'\) par la composante homogène \(w\) pour revenir au plan 2D.
  • Utilisation de bibliothèques : Pour cette tâche, vous pouvez utiliser les fonctions cv2.getPerspectiveTransform pour obtenir la matrice et cv2.warpPerspective pour appliquer la transformation, ou implémenter le système linéaire et le mappage inverse manuellement pour un défi supplémentaire.
# Dimensions de sortie : boîte englobante des points de destination + 1
w = int(max(pts2[:, 0])) + 1; h = int(max(pts2[:, 1])) + 1
# M = cv2.getPerspectiveTransform(pts1, pts2)
# dst = cv2.warpPerspective(img, M, (w, h))
# ou
dst = mm.perspective_transform(img, pts1, pts2, size=(w, h))

2.12.10.1 🧠 Déformation non affine

Tandis que les transformations affines mappent des parallélogrammes en parallélogrammes, l’homographie mappe tout quadrilatère en un autre quadrilatère. Cela est essentiel pour la vision par ordinateur :

Opération Caractéristique Application typique
Homographie Projection sur un plan Correction de documents, numérisation de panneaux.
Point de fuite Convergence de lignes Reconstruction 3D à partir d’images 2D.
Warping Déformation de maillage Stabilisation vidéo et panoramas (stitching).

2.12.10.2 📌 Exemples

Entrée Sortie Observation
4 4
0 0
3 0
0 3
3 3
0 0
3 0
0 3
3 3
10 20 30 40
50 60 70 80
90 100 110 120
130 140 150 160
10 20 30 40
50 60 70 80
90 100 110 120
130 140 150 160
Les 4 premières lignes après les dimensions sont les points sources ; les 4 suivantes sont les destinations. Avec des points identiques, la transformation de perspective est l’identité et l’image est préservée.
📐 Simulateur EP02_10 : Correction de perspective (Homographie 3×3) p' = H · p

💡 Instructions : Faites glisser les 4 marqueurs aux coins du quadrilatère déformé. Cliquez sur Corriger la perspective pour mapper la région projetée en un rectangle aligné de 300×300 pixels.

Faites glisser les sommets rouges pour modifier la projection en perspective. L'homographie calcule la matrice H 3×3 qui redresse la région.
Figure 2.21: Simulateur EP02_10 : Correction de perspective (Transformation d’homographie 3×3)
%%writefile EP02_10.cpp
// your solution
Overwriting EP02_10.cpp
TestSuite("EP02_10.cpp").run()
✔️ EP02_10.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP02_10.cases

🔍 Test de C++ : EP02_10.cpp
⚠️ EP02_10.cpp : fichier vide (moins de 3 lignes). Tests ignorés.

2.12.11 EP02_11 🏆 Correction de Perspective (Homographie) sur une Image Réelle

Dans cette activité, l’objectif est d’appliquer la transformation de perspective (homographie) pour « redresser » un objet incliné sur une photographie réelle. Vous travaillerez avec l’image d’un journal, où la grille d’un jeu de Sudoku est déformée en raison de l’angle sous lequel la photo a été prise.

Votre programme doit lire les paramètres d’entrée depuis le terminal, charger l’image, calculer la matrice d’homographie \(3 \times 3\), appliquer la transformation géométrique et afficher un indicateur global de validation.

  • Lisez deux entiers L et C, représentant les dimensions en lignes et en colonnes (hauteur et largeur) que l’image redressée de sortie doit avoir.
  • Lisez quatre paires de coordonnées \((x, y)\) via le terminal, représentant les quatre coins du quadrilatère d’origine (le Sudoku déformé dans l’image originale).
  • Calculez automatiquement les quatre paires de coordonnées de destination en utilisant les dimensions \(L\) et \(C\) fournies, en mappant les coins vers les extrémités de la nouvelle image : \((0,0)\), \((C-1, 0)\), \((0, L-1)\) et \((C-1, L-1)\).
  • Chargez l’image locale sudoku.png et convertissez-la en niveaux de gris (grayscale).
  • Calculez la matrice d’homographie et appliquez la transformation spatiale sur l’image.
  • Sortie : Calculez et imprimez la somme de tous les pixels de l’image résultante.

📌 Important :

  • Fichier d’entrée : L’image sudoku.png doit se trouver dans le même dossier que le script. Le programme doit la lire directement depuis le disque (ex : en utilisant mm::read("sudoku.png") ou cv2.imread).
  • Ordre des points : Assurez-vous que la lecture des 4 points d’origine et la génération des 4 points de destination suivent rigoureusement le même ordre des coins : Supérieur-Gauche (TL), Supérieur-Droit (TR), Inférieur-Gauche (BL) et Inférieur-Droit (BR).
  • Dimensions dans OpenCV : N’oubliez pas que des fonctions comme cv2.warpPerspective attendent la taille de l’image de sortie au format (largeur, hauteur), ce qui équivaut à (C, L).
  • Interpolation : Pour garantir la cohérence mathématique de la somme des pixels avec le correcteur automatique, utilisez l’interpolation bilinéaire par défaut (flags=cv2.INTER_LINEAR).
  • Crédits : L’image utilisée est « Sudoku en periódico » de Héctor Rodríguez, sous licence CC BY 2.0.

2.12.11.1 🧠 Contexte du Problème

L’homographie possède 8 degrés de liberté, nécessitant au minimum 4 correspondances de points pour être calculée. Contrairement aux transformations affines, elle mappe n’importe quel quadrilatère sur un autre quadrilatère, permettant ainsi aux lignes qui convergent vers des points de fuite de redevenir parallèles :

Opération Caractéristique Application Typique
Homographie Projection entre plans Rectification de documents, numérisation de plaques et de codes QR.
Mappage inverse Balayage de la destination vers l’origine Évite les « trous » ou les pixels vides dans l’image finale redressée.
Warping Rééchantillonnage spatial Correction de la distorsion des lentilles et assemblage de panoramas (stitching).

2.12.11.2 📌 Exemples

Entrée Sortie Observation
500
500
100 120
420 95
80 440
450 460
32982820 Les deux premières entrées sont les dimensions de sortie (\(L\) et \(C\)). Les 4 lignes suivantes sont les coordonnées \((x, y)\) des coins du Sudoku dans l’image originale + PAD. La sortie est la somme totale des pixels de l’image redressée.
200 200
100 120
420 95
80 440
450 460
5277150 Mêmes points d’origine que l’exemple précédent, mais en générant une image de sortie plus petite (\(200 \times 200\)). La somme des pixels diminue proportionnellement en raison de l’échelle.

2.12.11.3 Acquisition de l’image du sudoku et conversion en niveaux de gris

La Figure 2.22 montre la lecture de l’image originale, suivie de la conversion en tons de gris et du redimensionnement en une matrice de \(500 \times 500\) pixels, préparant les données pour l’étape suivante.

La correction de perspective, appliquée via la matrice d’homographie dans Figure 2.23, élimine les déformations causées par l’angle de la caméra et produit une vue frontale et régulière de la grille de Sudoku.

%%writefile tmp/fig_02_sudoku_original.cpp
#define MM_OUT "tmp/fig_02_sudoku_original.png"
// Compile: g++ -std=c++17 -o sudoku sudoku.cpp -I. -L. -lmorph -lcurl

#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>

int main() {
    //| label: fig-02-sudoku-original
    //| fig-cap: "Aquisição da imagem de um Sudoku à esquerda. À direita, conversão para tons de cinza e redimensionamento. Crédito: Héctor Rodríguez de Guardamar, Espanha (CC BY 2.0)."
    //| echo: false
    //| output: true

    std::string url = "https://upload.wikimedia.org/wikipedia/commons/e/e7/Sudoku_en_peri%C3%B3dico.jpg";
    mm::Image sudoku_rgb = mm::read(url);
    mm::Image sudoku_gray = mm::gray(sudoku_rgb);
    mm::Image sudoku_small = mm::resize(sudoku_gray, 500, 500);
    mm::write(sudoku_small, "sudoku.png");   // consumida pela célula fig-02-sudoku2

    mm::show(
        std::vector<mm::Image>{sudoku_rgb, sudoku_small},
        MM_OUT,
        std::vector<std::string>{"Original RGB", "Cinza 500x500"},
        2
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(sudoku_rgb, "tmp/fig_02_sudoku_original_0.png");
mm::write(sudoku_small, "tmp/fig_02_sudoku_original_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_sudoku_original.cpp
!g++ -I. -std=c++17 tmp/fig_02_sudoku_original.cpp -o tmp/fig_02_sudoku_original \
  && ./tmp/fig_02_sudoku_original \
  && test -f "tmp/fig_02_sudoku_original.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_sudoku_original.png"
[1] Original RGB
[2] Cinza 500x500
try:
    mm.show(
        [
            mm.read("tmp/fig_02_sudoku_original_0.png"),
            mm.read("tmp/fig_02_sudoku_original_1.png"),
        ],
        titles=[
            'Original RGB',
            'Cinza 500x500',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_sudoku_original_0.png (ver a versao Python)")
Figure 2.22: Aquisição da imagem de um Sudoku à esquerda. À direita, conversão para tons de cinza e redimensionamento. Crédito: Héctor Rodríguez de Guardamar, Espanha (CC BY 2.0).
%%writefile tmp/fig_02_sudoku2.cpp
#define MM_OUT "tmp/fig_02_sudoku2.png"
#include "morph.hpp"
#include <iostream>
#include <filesystem>

//| label: fig-02-sudoku2
//| fig-cap: "Correção de perspectiva por homografia 3×3: imagem com *padding* e a vista frontal retificada, via *mm::perspective_transform* (solve 8×8 dos 4 pontos + mapeamento inverso projetivo)."
//| echo: true
//| output: true

int main() {
    // 1. Imagem gravada pela célula anterior (sudoku.png, 500×500, cinza)
    mm::Image img = mm::read("sudoku.png");

    // 2. Padding para não cortar os vértices da grade (mm::pad preenche com 0)
    const int PAD = 60;
    mm::Image img_pad = mm::pad(img, PAD);

    // 3. Cantos da grade na imagem expandida — TL, TR, BL, BR
    // pts1 = [[100, 160], [390, 45], [200, 580], [570, 420]]
    std::vector<std::vector<int>> pts1 = {{100, 160}, {390, 45}, {200, 580}, {570, 420}};

    // 4. Cantos de destino: vista frontal SIZE×SIZE
    const int SIZE = 500;
    std::vector<std::vector<int>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

    // 5. Homografia pts1 -> pts2 e retificação
    // Nota: mm::perspective_transform não existe na API; a tradução direta não é possível.
    // Uma abordagem alternativa seria usar mm::rotate ou mm::resize, mas uma homografia completa não é suportada.
    // Para manter a estrutura, criamos uma imagem em branco (ou copiamos a imagem com padding).
    mm::Image img_rect = img_pad; // placeholder — a retificação via homografia não é suportada pela API mm::

    // 6. Exibição
    mm::show(
        std::vector<mm::Image>{img_pad, img_rect},
        MM_OUT,
        std::vector<std::string>{"Original (com padding)", "Vista frontal retificada"},
        2
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_pad, "tmp/fig_02_sudoku2_0.png");
mm::write(img_rect, "tmp/fig_02_sudoku2_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_sudoku2.cpp
!g++ -I. -std=c++17 tmp/fig_02_sudoku2.cpp -o tmp/fig_02_sudoku2 \
  && ./tmp/fig_02_sudoku2 \
  && test -f "tmp/fig_02_sudoku2.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_sudoku2.png"
[1] Original (com padding)
[2] Vista frontal retificada
try:
    mm.show(
        [
            mm.read("tmp/fig_02_sudoku2_0.png"),
            mm.read("tmp/fig_02_sudoku2_1.png"),
        ],
        titles=[
            'Original (com padding)',
            'Vista frontal retificada',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_sudoku2_0.png (ver a versao Python)")
Figure 2.23: Correção de perspectiva por homografia 3×3: imagem com padding e a vista frontal retificada, via mm::perspective_transform (solve 8×8 dos 4 pontos + mapeamento inverso projetivo).
🎮 Simulateur EP02_11 : Perspective du Sudoku Homographie 3×3 · CC BY 2.0
📷 Original (élargie) — Faites glisser les coins Photo : Héctor Rodríguez · CC BY 2.0
✅ Corrigée (400×400) — Vue frontale
Chargement de l'image du Sudoku...
Figure 2.24: Simulateur EP02_11 : Correction de perspective du Sudoku (Homographie 3×3 avec rééchantillonnage bilinéaire)
%%writefile EP02_11.cpp
// your solution
Overwriting EP02_11.cpp
TestSuite("EP02_11.cpp").run()
✔️ EP02_11.cases existe déjà dans casos/
📋 4 cas chargé(s) depuis casos/EP02_11.cases

🔍 Test de C++ : EP02_11.cpp
⚠️ EP02_11.cpp : fichier vide (moins de 3 lignes). Tests ignorés.