3  Opérations Spatiales : Intensité, Histogramme et Filtrage

Ce chapitre approfondit le traitement d’images dans le domaine spatial, en partant de la manipulation directe des pixels et des histogrammes pour le rehaussement de contraste, jusqu’à l’application de filtres locaux par convolution pour le lissage, la réduction du bruit et la détection de contours. L’objectif est de développer l’intuition mathématique et computationnelle qui sous-tend une grande partie des algorithmes modernes de Vision par Ordinateur.

3.1 Objetivos

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

  • Manipuler l’intensité et les pixels : Exécuter des opérations arithmétiques saturées (mm::addm, mm::subm) et logiques bit à bit (mm::band, mm::bor, mm::bnot) pour la combinaison et la sélection de régions d’intérêt (ROI), et appliquer l’alpha blending (mm::blend) pour la fusion pondérée d’images ;
  • Traiter les histogrammes : Interpréter l’histogramme comme diagnostic des tons et appliquer l’égalisation globale via la CDF (mm::equalize) ; dans le parcours Python, également l’égalisation adaptative (CLAHE) et la spécification d’histogramme pour le transfert de profil tonal entre images ;
  • Comprendre les fondamentaux spatiaux : Saisir la notion de voisinage, de padding des bords (mm::pad) et la différence entre corrélation croisée (mm::conv) et convolution — y compris pourquoi des kernels asymétriques comme celui de Sobel produisent des résultats distincts dans les deux opérations ;
  • Appliquer le filtrage de lissage : Utiliser le filtre moyenneur (mm::blur, ou mm::conv avec un kernel uniforme) et le filtre gaussien (mm::gaussian) pour la réduction du bruit, en comprenant l’avantage de la pondération radiale et de la séparabilité gaussienne ;
  • Appliquer le filtrage de rehaussement : Utiliser le laplacien \(w_4\) et \(w_8\) (mm::laplacian) pour le rehaussement isotrope des contours, l’opérateur de Sobel (mm::sobel) pour la magnitude du gradient — et, dans le parcours Python, la décomposition directionnelle \(G_x\), \(G_y\) et l’angle —, ainsi que l’Unsharp Masking (mm::usm) pour l’amplification des hautes fréquences contrôlée par le paramètre \(k\) ;
  • Utiliser les filtres d’ordre : Appliquer le filtre médian (mm::median) pour supprimer le bruit sel et poivre, en comprenant pourquoi sa nature non linéaire et sa robustesse aux outliers le rendent supérieur aux filtres linéaires dans ce scénario ;
  • Résoudre des problèmes pratiques : Enchaîner les techniques dans des pipelines de prétraitement (égalisation → gaussien → Canny ; avec CLAHE à la place de l’égalisation dans le parcours Python) et utiliser les fonctions de morph (mm::conv, mm::histImg, mm::equalize, mm::drawImgKernel) pour l’analyse et la visualisation didactique de chaque étape.

3.2 Opérations au Niveau d’Intensité

Le niveau le plus élémentaire de traitement d’images agit directement sur les valeurs des pixels, sans considérer le voisinage. Ces opérations — appelées transformations ponctuelles (point operations) — sont les plus rapides sur le plan computationnel et constituent la base de techniques plus complexes.

Formellement, une transformation ponctuelle peut être décrite comme :

\[ g(x,y) = T[f(x,y)] \tag{3.1}\]

où \(f(x,y)\) est l’image d’entrée, \(g(x,y)\) est la sortie et \(T\) est une fonction appliquée à chaque pixel individuellement.

3.2.1 Préparation de l’environnement pratique

Le bloc suivant charge la bibliothèque morph du dépôt (le module morph.py et, dans le parcours C++, également le morph.hpp utilisé dans le #include des cellules compilées).

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefacts de construction de la piste 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 la piste 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 la piste compilée
# (morph.hpp + stb_image*.h), utilisée dans le #include des cellules %%writefile *.cpp.
import config
config.setup(cpp=True)
from morph import mm
import numpy as np
✅ Environnement prêt. Morph : 1.1.9 | OpenCV : 5.0.0

Comme objet d’étude tout au long de ce chapitre, nous utiliserons les images de vie sauvage présentées dans les Figure 3.1 et Figure 3.3.. À partir de celles-ci, nous explorerons les opérations spatiales sur l’intensité, les histogrammes et le filtrage, en analysant leurs effets sur le rehaussement, le lissage, la réduction du bruit et la détection de contours, afin de comprendre les fondements mathématiques et informatiques du TNI.

%%writefile tmp/fig_03_mandrill.cpp
#define MM_OUT "tmp/fig_03_mandrill.png"
//| label: fig-03-mandrill
//| fig-cap: "*Mandrill* (*Mandrillus sphinx*) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0)."
//| echo: true

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

int main() {
    std::string base    = "https://upload.wikimedia.org/wikipedia/commons";
    std::string arquivo = "Carlos_Guilherme_Rodrigues_%2876515283%29.jpeg";
    std::string url     = base + "/9/9b/" + arquivo;

    mm::Image img_color = mm::read(url);
    mm::Image img_gray  = mm::gray(img_color);

    mm::show(img_color, MM_OUT);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_8.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/fig_03_mandrill.cpp
!g++ -I. -std=c++17 tmp/fig_03_mandrill.cpp -o tmp/fig_03_mandrill \
  && ./tmp/fig_03_mandrill \
  && test -f "tmp/fig_03_mandrill.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_mandrill.png"
try:
    mm.show(mm.read("tmp/fig_03_mandrill.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_mandrill.png (ver a versao Python)")
Figure 3.1: Mandrill (Mandrillus sphinx) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0).

3.2.2 Opérations Arithmétiques

Les opérations arithmétiques entre images sont largement utilisées en TDI pour combiner, comparer ou rehausser des informations. La soustraction d’images est particulièrement puissante pour détecter des différences entre deux trames — par exemple, lors de la suppression d’un fond statique dans les caméras de surveillance :

\[ g(x,y) = f_1(x,y) - f_2(x,y) \tag{3.2}\]

L’addition saturée limite le résultat à l’intervalle \([0, 255]\) : les valeurs supérieures à 255 sont plafonnées à 255, évitant ainsi le dépassement silencieux du type uint8 (ex. : \(200 + 100 = 44\) au lieu de 300). La soustraction saturée applique le même principe par le bas : les valeurs négatives sont plafonnées à 0.

AvertissementSaturation et dépassement

Les opérations arithmétiques sur uint8 subissent un dépassement silencieux : \(200 + 100 = 44\) (et non 300). mm::addm et mm::subm effectuent une saturation automatique, en plafonnant le résultat dans \([0, 255]\). Le mélange utilise des poids fractionnaires : mm::blend opère en interne en virgule flottante, puis arrondit et sature seulement ensuite vers uint8.

La Figure 3.2 illustre l’addition d’une constante (éclaircissement) et la soustraction d’une constante (assombrissement avec saturation à 0).

%%writefile tmp/fig_03_aritmetica.cpp
#define MM_OUT "tmp/fig_03_aritmetica.png"
//| label: fig-03-aritmetica
//| fig-cap: "Opérations arithmétiques saturées : addition de constante (éclaircissement) et soustraction de constante (assombrissement avec saturation à 0)."
//| echo: true
//| output: true

#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_8.png");
// [pdi:state-io:end]

    int fundo = 60;

    mm::Image img_add = mm::addm(img_gray, fundo);
    mm::Image img_sub = mm::subm(img_gray, fundo);

    mm::show(
        std::vector<mm::Image>{img_gray, img_add, img_sub},
        MM_OUT,
        std::vector<std::string>{"Original", "addm (+60)", "subm (−60)"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_aritmetica_0.png");
mm::write(img_add, "tmp/fig_03_aritmetica_1.png");
mm::write(img_sub, "tmp/fig_03_aritmetica_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_aritmetica.cpp
!g++ -I. -std=c++17 tmp/fig_03_aritmetica.cpp -o tmp/fig_03_aritmetica \
  && ./tmp/fig_03_aritmetica \
  && test -f "tmp/fig_03_aritmetica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_aritmetica.png"
[1] Original
[2] addm (+60)
[3] subm (−60)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_aritmetica_0.png"),
            mm.read("tmp/fig_03_aritmetica_1.png"),
            mm.read("tmp/fig_03_aritmetica_2.png"),
        ],
        titles=[
            'Original',
            'addm (+60)',
            'subm (−60)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_aritmetica_0.png (ver a versao Python)")
Figure 3.2: Operações aritméticas saturadas: adição de constante (clareamento) e subtração de constante (escurecimento com saturação em 0).

3.2.3 Pondération Pondérée (Alpha Blending)

La pondération pondérée (alpha blending) combine deux images en utilisant des poids complémentaires \(\alpha\) et \((1-\alpha)\) :

\[ g(x,y) = \alpha\,f_1(x,y) + (1-\alpha)\,f_2(x,y), \quad \alpha \in [0,1] \tag{3.3}\]

Lorsque \(\alpha = 1\), seule l’image \(f_1\) est obtenue ; lorsque \(\alpha = 0\), seule \(f_2\) l’est. Les valeurs intermédiaires produisent une transition douce entre les deux, et sont largement utilisées dans la composition d’images, la superposition de calques, les filigranes et les effets de fusion visuelle.

Pour que la combinaison produise un résultat cohérent, il est nécessaire d’aligner au préalable les régions d’intérêt. Dans la Figure 3.4, on découpe le visage du léopard avec mm::crop(img_leop_gray, 250, H-300, 100, W-200) et la région faciale du mandrill avec mm::crop(img_gray, 100, 400, 380, 530), de sorte que les yeux et la structure faciale soient approximativement alignés. Le découpage du léopard est ensuite redimensionné (mm::resize) aux dimensions du mandrill avant la fusion.

mm::blend effectue l’opération en virgule flottante — évitant tout dépassement dans les calculs avec des poids fractionnaires — puis arrondit et sature le résultat en uint8.

%%writefile tmp/fig_03_leopardo.cpp
#define MM_OUT "tmp/fig_03_leopardo.png"
// Compile: g++ -std=c++17 -o program program.cpp -lmorph

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

//| label: fig-03-leopardo
//| fig-cap: "Retrato de um leopardo (*Panthera pardus*) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0)."
//| echo: true

int main() {
    mm::Image img_leop = mm::read("https://upload.wikimedia.org/wikipedia/commons/9/92/Leopard_%28Panthera_pardus%29_portrait.jpg");
    mm::Image img_leop_gray = mm::gray(img_leop);

    mm::show(img_leop, MM_OUT);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_leop, "tmp/state/img_leop_12.png");
mm::write(img_leop_gray, "tmp/state/img_leop_gray_12.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/fig_03_leopardo.cpp
!g++ -I. -std=c++17 tmp/fig_03_leopardo.cpp -o tmp/fig_03_leopardo \
  && ./tmp/fig_03_leopardo \
  && test -f "tmp/fig_03_leopardo.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_leopardo.png"
try:
    mm.show(mm.read("tmp/fig_03_leopardo.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_leopardo.png (ver a versao Python)")
Figure 3.3: Retrato de um leopardo (Panthera pardus) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0).
%%writefile tmp/fig_03_blend.cpp
#define MM_OUT "tmp/fig_03_blend.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

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

    // Variables img_gray and img_leop_gray are already initialized

    // recortes alinhados: rosto do leopardo e região facial do mandril
    mm::Image leo = mm::crop(img_leop_gray, 250, img_leop_gray.h - 300, 100, img_leop_gray.w - 200);
    mm::Image mandrill = mm::crop(img_gray, 100, 400, 380, 530);
    mm::Image leo_r = mm::resize(leo, mandrill.w, mandrill.h, "bilinear");

    mm::show(
        std::vector<mm::Image>{mm::blend(mandrill, leo_r, 1.0), mm::blend(mandrill, leo_r, 0.8),
         mm::blend(mandrill, leo_r, 0.6), mm::blend(mandrill, leo_r, 0.4),
         mm::blend(mandrill, leo_r, 0.2), mm::blend(mandrill, leo_r, 0.0)},
        MM_OUT,
        std::vector<std::string>{"α=1.0", "α=0.8", "α=0.6", "α=0.4", "α=0.2", "α=0.0"},
        6
    );

    return 0;
}
Overwriting tmp/fig_03_blend.cpp
!g++ -I. -std=c++17 tmp/fig_03_blend.cpp -o tmp/fig_03_blend \
  && ./tmp/fig_03_blend \
  && test -f "tmp/fig_03_blend.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_blend.png"
[1] α=1.0
[2] α=0.8
[3] α=0.6
[4] α=0.4
[5] α=0.2
[6] α=0.0
try:
    mm.show(mm.read("tmp/fig_03_blend.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_blend.png (ver a versao Python)")
Figure 3.4: Alpha blending entre recortes alinhados de mandrill e do leopardo (Figure 3.3) para diferentes valores de α. Em α=1 vê-se apenas mandrill; em α=0, apenas o leopardo; valores intermediários fundem os olhares das duas imagens proporcionalmente.

3.2.4 Opérations Logiques et Masques Bit à Bit

Les opérations logiques bit à bit (AND, OR et NOT) agissent directement sur les bits de chaque pixel et constituent la base pour la création et l’application de masques (masks) — des images binaires avec uniquement 0 (noir) et 255 (blanc) utilisées pour isoler des Régions d’Intérêt (ROI).

Le comportement de chaque opération découle de la représentation binaire de 255 (11111111) et de 0 (00000000) :

  • AND avec le masque : où \(m = 255\), les bits d’origine sont préservés ; où \(m = 0\), le pixel est mis à zéro. Résultat : découpage de la ROI. \[g(x,y) = f(x,y) \;\text{AND}\; m(x,y) \tag{3.4}\]
  • OR avec le masque : où \(m = 255\), le pixel est forcé au blanc ; où \(m = 0\), la valeur d’origine est conservée. Résultat : éclairage de la ROI.
  • NOT (sans masque) : inverse tous les bits (\(g = 255 - f\)), produisant le négatif photographique de l’image.

La Figure 3.5 illustre les trois opérations appliquées à l’image du mandrill avec un masque circulaire.

%%writefile tmp/fig_03_logica.cpp
#define MM_OUT "tmp/fig_03_logica.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_8.png");
// [pdi:state-io:end]

    //| label: fig-03-logica
    //| fig-cap: "Opérations logiques bit à bit avec masque circulaire : AND (isolement de la ROI), OR (éclairage de la ROI) et NOT (négatif)."
    //| echo: true
    //| output: true

    int h = img_gray.h;
    int w = img_gray.w;

    // Masque circulaire rempli, centré sur l'image (mm.circle dessine le
    // disque ; la version didactique, test de rayon pixel par pixel, est mm.circle0).
    mm::Image mask_circ(h, w);
    mask_circ = mm::circle(mask_circ, w / 2, h / 2, std::min(h, w) / 3 - 10, 255, -1);

    // Opérations via morph
    mm::Image img_not = mm::bnot(img_gray);            // NOT : négatif photographique
    mm::Image img_and = mm::band(img_gray, mask_circ); // préserve uniquement la ROI circulaire
    mm::Image img_or  = mm::bor(img_gray, mask_circ);  // éclaire la région du masque

    mm::show(
        std::vector<mm::Image>{img_gray, img_and, img_or, img_not},
        MM_OUT,
        std::vector<std::string>{"Original", "AND (ROI circulaire)", "OR (éclaire ROI)", "NOT (négatif)"},
        4
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_logica_0.png");
mm::write(img_and, "tmp/fig_03_logica_1.png");
mm::write(img_or, "tmp/fig_03_logica_2.png");
mm::write(img_not, "tmp/fig_03_logica_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_logica.cpp
!g++ -I. -std=c++17 tmp/fig_03_logica.cpp -o tmp/fig_03_logica \
  && ./tmp/fig_03_logica \
  && test -f "tmp/fig_03_logica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_logica.png"
[1] Original
[2] AND (ROI circulaire)
[3] OR (éclaire ROI)
[4] NOT (négatif)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_logica_0.png"),
            mm.read("tmp/fig_03_logica_1.png"),
            mm.read("tmp/fig_03_logica_2.png"),
            mm.read("tmp/fig_03_logica_3.png"),
        ],
        titles=[
            'Original',
            'AND (ROI circular)',
            'OR (ilumina ROI)',
            'NOT (negativo)',
        ],
        cols=4,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_logica_0.png (ver a versao Python)")
Figure 3.5: Operações lógicas bit a bit com máscara circular: AND (isolamento da ROI), OR (iluminação da ROI) e NOT (negativo).

3.3 Histogramme d’images

L’histogramme d’une image en niveaux de gris est une fonction discrète qui décrit la distribution des fréquences des intensités :

\[ h(r_k) = n_k, \quad k = 0, 1, \ldots, L-1 \tag{3.5}\]

où \(r_k\) est le \(k\)-ième niveau d’intensité, \(n_k\) est le nombre de pixels ayant cette intensité et \(L\) est le nombre total de niveaux (typiquement 256 pour 8 bits). L’histogramme normalisé estime la probabilité de chaque niveau :

\[ p(r_k) = \frac{n_k}{MN} \tag{3.6}\]

où \(MN\) est le nombre total de pixels. Étant une statistique globale, l’histogramme ne contient pas d’information positionnelle, mais révèle des caractéristiques essentielles telles que la luminosité moyenne, le contraste et la distribution tonale. En pratique, mm::hist(img) retourne le vecteur des comptages \(h(r_k)\), qui sert aussi bien à la visualisation (via mm::histImg) qu’à des calculs comme la fonction de distribution cumulative (CDF) et l’égalisation.

NoteInterprétation de l’histogramme
  • Étroit à gauche : image sous-exposée (sombre).
  • Étroit à droite : image surexposée (claire).
  • Concentré au centre : faible contraste.
  • Réparti sur toute la plage : contraste élevé, bonne utilisation des tons disponibles.

La Figure 3.6 présente l’histogramme de l’image du mandrill, ainsi que des versions assombrie (mm::subm) et éclaircie (mm::addm). On observe le déplacement de la distribution des intensités vers la gauche et vers la droite, respectivement. Notez que l’intervalle représenté sur l’axe \(x\) ne correspond pas nécessairement à toute la plage de 0 à 255.

%%writefile tmp/fig_03_histograma.cpp
#define MM_OUT "tmp/fig_03_histograma.png"
//| label: fig-03-histograma
//| fig-cap: "Histogramas da imagem original, de uma versão escurecida (−80) e de uma clareada (+80). A subtração/adição satura em 0 e 255."
//| echo: true
//| output: true

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

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

    mm::Image img_dark = mm::subm(img_gray, 80);
    mm::Image img_high = mm::addm(img_gray, 80);

    mm::show(
        {img_gray, img_dark, img_high,
         mm::histImg(img_gray), mm::histImg(img_dark), mm::histImg(img_high)},
        MM_OUT,
        {"Original", "Escurecida (-80)", "Clareada (+80)",
         "Histograma - original", "Histograma - escurecida", "Histograma - clareada"},
        3
    );

    return 0;
}
Overwriting tmp/fig_03_histograma.cpp
!g++ -I. -std=c++17 tmp/fig_03_histograma.cpp -o tmp/fig_03_histograma \
  && ./tmp/fig_03_histograma \
  && test -f "tmp/fig_03_histograma.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_histograma.png"
[1] Original
[2] Escurecida (-80)
[3] Clareada (+80)
[4] Histograma - original
[5] Histograma - escurecida
[6] Histograma - clareada
try:
    mm.show(mm.read("tmp/fig_03_histograma.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_histograma.png (ver a versao Python)")
Figure 3.6: Histogramas da imagem original, de uma versão escurecida (−80) e de uma clareada (+80). A subtração/adição satura em 0 e 255.

3.3.1 Égalisation d’histogramme

L’égalisation d’histogramme redistribue les intensités afin que l’histogramme résultant soit aussi uniforme que possible. Le mappage est donné par la fonction de distribution cumulative (CDF) :

\[ s_k = T(r_k) = (L-1)\sum_{j=0}^{k} p(r_j) = \frac{L-1}{MN}\sum_{j=0}^{k} n_j \tag{3.7}\]

La transformation est monotone : les niveaux fréquents reçoivent des intervalles plus larges dans le domaine de sortie (plus grande séparation → plus de contraste), tandis que les niveaux rares sont compressés.

L’algorithme complet, en cinq étapes, est présenté dans la Table 3.1.

Table 3.1: Algorithme d’égalisation d’histogramme.
Étape Opération Formule
1 Histogramme \(h[k] \leftarrow\) nombre de pixels avec l’intensité \(k\), \(k=0\ldots L-1\)
2 Probabilité \(p[k] \leftarrow h[k] / MN\)
3 CDF \(\text{cdf}[k] \leftarrow \sum_{j=0}^{k} p[j]\) (somme cumulée)
4 Table de correspondance (mapping) \(\text{lut}[k] \leftarrow \text{round}(\text{cdf}[k] \times (L-1))\)
5 Application \(g[i,j] \leftarrow \text{lut}[f[i,j]]\) (pour chaque pixel)

Notez dans la Figure 3.7 que l’égalisation redistribue les tons existants vers des positions plus espacées dans la plage \([0, L-1]\), mais ne crée pas de nouveaux tons — l’image égalisée conserve exactement 3 tons distincts, désormais en \(\{1, 5, 7\}\) au lieu de \(\{2, 3, 4\}\).

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

//| label: fig-03-equalizacao-didatica
//| fig-cap: "Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. *mm::equalize(img, 3)* faz o mapeamento."
//| echo: true
//| output: true

int main() {
    mm::Image img5(5, 5);
    int vals5[5][5] = {{3, 4, 2, 3, 4},
                       {4, 3, 3, 4, 3},
                       {2, 3, 4, 3, 2},
                       {3, 4, 3, 2, 3},
                       {4, 3, 2, 3, 4}};
    for (int y = 0; y < 5; y++)
        for (int x = 0; x < 5; x++)
            img5.at(y, x) = vals5[y][x];

    mm::Image img5_eq = mm::equalize(img5, 3);   // L = 2^3 = 8

    std::cout << "Imagem original 5x5 (3 bits):" << "\n";
    std::cout << mm::drawImg(img5);
    std::cout << "Imagem equalizada 5x5:" << "\n";
    std::cout << mm::drawImg(img5_eq);

    mm::show(std::vector<mm::Image>{img5, img5_eq, mm::histImg(img5), mm::histImg(img5_eq)},
             MM_OUT,
             std::vector<std::string>{"Original", "Equalizada", "Histograma - original", "Histograma - equalizada"},
             2);
    return 0;
}
Overwriting tmp/fig_03_equalizacao_didatica.cpp
!g++ -I. -std=c++17 tmp/fig_03_equalizacao_didatica.cpp -o tmp/fig_03_equalizacao_didatica \
  && ./tmp/fig_03_equalizacao_didatica \
  && test -f "tmp/fig_03_equalizacao_didatica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_equalizacao_didatica.png"
Imagem original 5x5 (3 bits):
3 4 2 3 4 
4 3 3 4 3 
2 3 4 3 2 
3 4 3 2 3 
4 3 2 3 4 
Imagem equalizada 5x5:
5 7 1 5 7 
7 5 5 7 5 
1 5 7 5 1 
5 7 5 1 5 
7 5 1 5 7 
[1] Original
[2] Equalizada
[3] Histograma - original
[4] Histograma - equalizada
try:
    mm.show(mm.read("tmp/fig_03_equalizacao_didatica.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_equalizacao_didatica.png (ver a versao Python)")
Figure 3.7: Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. mm::equalize(img, 3) faz o mapeamento.

Limitation : l’égalisation globale peut surexhausser les bruits et produire un contraste excessif dans les régions homogènes. Le CLAHE (Contrast Limited Adaptive Histogram Equalization) réduit ce problème en appliquant l’égalisation sur des blocs locaux (tiles) et en limitant la hauteur des pics de l’histogramme avant l’égalisation.

La Figure 3.8 compare l’image originale, l’égalisation globale via mm::equalize et le CLAHE d’OpenCV, en affichant également les histogrammes résultants. Contrairement à l’égalisation globale, qui utilise une transformation unique basée sur la CDF de toute l’image, le CLAHE adapte le contraste à chaque région, étant particulièrement utile pour les images avec un éclairage non uniforme.

Dans l’exemple, clipLimit=2.0 et tileGridSize=(32,32) ont été utilisés. Le paramètre clipLimit définit dans quelle mesure les pics de l’histogramme local peuvent croître avant d’être coupés (clipped). Dans OpenCV, cette valeur est un facteur relatif : la limite réelle est approximativement calculée comme clipLimit × (nombre de pixels du bloc / nombre de niveaux de gris). Par exemple, dans un bloc de 4096 pixels et une image 8 bits (256 niveaux de gris), la fréquence moyenne par niveau est \(4096/256=16\). Ainsi, clipLimit=2.0 permet des pics d’environ \(2\times16=32\) occurrences avant la coupure. Les occurrences excédentaires ne sont pas rejetées : elles sont redistribuées entre les autres niveaux de gris de l’histogramme, réduisant la concentration excessive sur quelques niveaux et évitant une amplification exagérée du contraste local. Des valeurs plus faibles limitent davantage le contraste et réduisent l’amplification du bruit, tandis que des valeurs plus élevées permettent un rehaussement plus intense, mais peuvent introduire des artefacts.

  • clipLimit=1.0 : rehaussement doux et conservateur ;
  • clipLimit=2.0 : bon équilibre entre contraste et naturel ;
  • clipLimit=4.0 : mise en valeur accrue des détails locaux ;
  • clipLimit=8.0 : contraste agressif, avec amplification possible du bruit.

Ainsi, le CLAHE produit généralement des résultats plus naturels que l’égalisation globale, en particulier pour les images avec des ombres, des reflets ou un éclairage inégal.

%%writefile tmp/fig_03_equalizacao.cpp
#define MM_OUT "tmp/fig_03_equalizacao.png"
//#| label: fig-03-equalizacao
//#| fig-cap: "Égalisation globale de l'histogramme (mm::equalize, via CDF) et les histogrammes avant/après. CLAHE (adaptative) reste uniquement dans le parcours Python — pas d'équivalent dans morph.hpp."
//#| echo: true
//#| output: true

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

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

    mm::Image img_eq = mm::equalize(img_gray);

    mm::show(
        std::vector<mm::Image>{img_gray, img_eq, mm::histImg(img_gray), mm::histImg(img_eq)},
        MM_OUT,
        std::vector<std::string>{"Original", "mm.equalize (CDF)", "Histogramme - original", "Histogramme - égalisé"},
        2
    );

    return 0;
}
Overwriting tmp/fig_03_equalizacao.cpp
!g++ -I. -std=c++17 tmp/fig_03_equalizacao.cpp -o tmp/fig_03_equalizacao \
  && ./tmp/fig_03_equalizacao \
  && test -f "tmp/fig_03_equalizacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_equalizacao.png"
[1] Original
[2] mm.equalize (CDF)
[3] Histogramme - original
[4] Histogramme - égalisé
try:
    mm.show(mm.read("tmp/fig_03_equalizacao.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_equalizacao.png (ver a versao Python)")
Figure 3.8: Equalização de histograma global (mm::equalize, via CDF) e os histogramas antes/depois. CLAHE (adaptativa) fica só na trilha Python — não tem equivalente em morph.hpp.

3.3.2 Spécification d’histogramme

Alors que l’égalisation impose une distribution uniforme, la spécification d’histogramme (histogram matching) permet à l’histogramme de l’image de sortie de suivre une distribution arbitraire — par exemple, l’histogramme d’une autre image de référence.

La procédure comporte trois étapes :

  1. Calculer la CDF de l’image d’entrée : \(P_r(r_k)\).
  2. Calculer la CDF de l’image de référence : \(P_z(z_k)\).
  3. Pour chaque niveau \(r_k\), trouver le niveau \(z\) qui minimise \(|P_z(z) - P_r(r_k)|\).

\[ T(r_k) = \arg\min_{z}\,|P_z(z) - P_r(r_k)| \tag{3.8}\]

Dans Figure 3.9, nous transférons le profil tonal du léopard (Figure 3.3) vers l’image du mandrill — une application directe du concept vu dans le blending : au lieu de fusionner des pixels, nous fusionnons ici des distributions tonales.

%%writefile tmp/fig_03_especificacao.cpp
#define MM_OUT "tmp/fig_03_especificacao.png"
#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_8.png");
mm::Image img_leop_gray = mm::_read_state("tmp/state/img_leop_gray_12.png");
// [pdi:state-io:end]

    //| label: fig-03-especificacao
    //| fig-cap: "Especificação de histograma (mapear o mandril para o perfil tonal do leopardo) precisa da CDF inversa da referência — fica só na trilha Python. Aqui, a equalização global do mandril, como comparação."
    //| echo: true
    //| output: true

    mm::Image img_eq = mm::equalize(img_gray);

    mm::show(std::vector<mm::Image>{img_gray, img_leop_gray, img_eq},
            MM_OUT,
            std::vector<std::string>{"Mandril (original)", "Leopardo (referencia)", "Mandril equalizado"},
            3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_especificacao_0.png");
mm::write(img_leop_gray, "tmp/fig_03_especificacao_1.png");
mm::write(img_eq, "tmp/fig_03_especificacao_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_especificacao.cpp
!g++ -I. -std=c++17 tmp/fig_03_especificacao.cpp -o tmp/fig_03_especificacao \
  && ./tmp/fig_03_especificacao \
  && test -f "tmp/fig_03_especificacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_especificacao.png"
[1] Mandril (original)
[2] Leopardo (referencia)
[3] Mandril equalizado
try:
    mm.show(
        [
            mm.read("tmp/fig_03_especificacao_0.png"),
            mm.read("tmp/fig_03_especificacao_1.png"),
            mm.read("tmp/fig_03_especificacao_2.png"),
        ],
        titles=[
            'Mandril (original)',
            'Leopardo (referencia)',
            'Mandril equalizado',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_especificacao_0.png (ver a versao Python)")
Figure 3.9: Especificação de histograma (mapear o mandril para o perfil tonal do leopardo) precisa da CDF inversa da referência — fica só na trilha Python. Aqui, a equalização global do mandril, como comparação.

3.4 Fondements spatiaux : Voisinage, convolution et noyaux

Les opérations de filtrage spatial n’agissent pas sur un pixel isolé, mais sur un voisinage qui l’entoure. Pour cela, on utilise une petite matrice de coefficients appelée noyau (ou masque), qui parcourt toute l’image au moyen d’une fenêtre glissante (sliding window).

Les fenêtres les plus courantes sont de taille 3×3, 5×5 et 7×7. Dans une fenêtre 3×3, par exemple, le pixel central est traité avec ses huit voisins immédiats. À chaque position de la fenêtre, les valeurs des pixels sont combinées avec les coefficients du noyau, produisant une nouvelle valeur pour le pixel central.

3.4.1 Voisinage

Considérons une fenêtre 3×3 centrée sur le pixel \((x,y)\) :

\[ \begin{bmatrix} (x-1,y-1) & (x,y-1) & (x+1,y-1) \\ (x-1,y) & (x,y) & (x+1,y) \\ (x-1,y+1) & (x,y+1) & (x+1,y+1) \end{bmatrix} \tag{3.9}\]

De manière générale, une fenêtre de taille \((2a+1)\times(2b+1)\) englobe tous les pixels situés jusqu’à \(a\) positions à l’horizontale et jusqu’à \(b\) positions à la verticale par rapport au pixel central. Ainsi, une fenêtre 3×3 correspond à \(a=b=1\), une fenêtre 5×5 à \(a=b=2\), et ainsi de suite.

Mathématiquement, le voisinage est défini par

\[ \mathcal{V}(x,y)= \{(x+s,\,y+t) : -a\le s\le a,\,-b\le t\le b\} \tag{3.10}\]

3.4.2 Traitement des bords

Les pixels proches des bords ont une partie de leur voisinage en dehors de l’image. Pour appliquer des filtres dans ces régions, il est nécessaire de définir comment les valeurs externes seront obtenues. Les trois stratégies les plus courantes (avec la constante équivalente d’OpenCV entre parenthèses) sont :

  • Remplissage par zéros (BORDER_CONSTANT) : complète la région externe avec des zéros.
  • Réplication (BORDER_REPLICATE) : répète la valeur du pixel du bord.
  • Réflexion (BORDER_REFLECT_101) : reflète les pixels voisins, sans répéter celui du bord.

mm::conv utilise la réflexion par défaut, car elle préserve mieux la continuité des niveaux de gris et réduit les artefacts dans le traitement des bords.

L’exemple suivant compare les trois stratégies avec mm::pad sur une matrice 3×3. Observez comment chacune remplit les pixels externes nécessaires pour appliquer un filtre 3×3 également aux coins.

%%writefile tmp/mm_out_1.cpp
//| Convert this Python code to C++:
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

int main() {
    mm::Image img(3, 3, 1);  // 3 lignes, 3 colonnes, 1 canal
    int vals[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    for (int y = 0; y < 3; ++y) {
        for (int x = 0; x < 3; ++x) {
            img.at(y, x) = static_cast<unsigned char>(vals[y][x]);
        }
    }

    std::vector<std::string> bordas = {"constant", "replicate", "reflect101"};

    std::cout << "Imagem original:" << std::endl;
    std::cout << mm::drawImg(img) << std::endl;

    for (int k : {3, 5}) {
        int b = k / 2;
        std::cout << "=== Kernel " << k << "x" << k << " (padding b=" << b << ") ===" << std::endl;
        for (const auto& nome : bordas) {
            std::cout << nome << std::endl;
            mm::Border border_type;
            if (nome == "constant") border_type = mm::Border::CONSTANT;
            else if (nome == "replicate") border_type = mm::Border::REPLICATE;
            else border_type = mm::Border::REFLECT101;
            std::cout << mm::drawImg(mm::pad(img, b, border_type)) << std::endl;
        }
    }

    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:
1 2 3 
4 5 6 
7 8 9 

=== Kernel 3x3 (padding b=1) ===
constant
0 0 0 0 0 
0 1 2 3 0 
0 4 5 6 0 
0 7 8 9 0 
0 0 0 0 0 

replicate
1 1 2 3 3 
1 1 2 3 3 
4 4 5 6 6 
7 7 8 9 9 
7 7 8 9 9 

reflect101
5 4 5 6 5 
2 1 2 3 2 
5 4 5 6 5 
8 7 8 9 8 
5 4 5 6 5 

=== Kernel 5x5 (padding b=2) ===
constant
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 
0 0 1 2 3 0 0 
0 0 4 5 6 0 0 
0 0 7 8 9 0 0 
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 

replicate
1 1 1 2 3 3 3 
1 1 1 2 3 3 3 
1 1 1 2 3 3 3 
4 4 4 5 6 6 6 
7 7 7 8 9 9 9 
7 7 7 8 9 9 9 
7 7 7 8 9 9 9 

reflect101
9 8 7 8 9 8 7 
6 5 4 5 6 5 4 
3 2 1 2 3 2 1 
6 5 4 5 6 5 4 
9 8 7 8 9 8 7 
6 5 4 5 6 5 4 
3 2 1 2 3 2 1 

Notez que le résultat d’un filtre peut varier considérablement selon le traitement adopté pour les bords de l’image.

Dans morph.hpp, les fonctions de filtrage (mm::conv, mm::blur, mm::gaussian, mm::laplacian, mm::usm) appliquent un padding par réflexion (mm::Border::REFLECT101) par défaut — le même comportement que cv2.filter2D. La variante didactique mm::conv0 utilise mm::Border::KEEP : les pixels du bord conservent leur valeur d’origine, sans filtrage. Quant à mm::sobel et mm::prewitt, elles laissent le bord à zéro (elles ne calculent que l’intérieur).

3.4.3 Corrélation vs. Convolution

Il existe deux mécanismes mathématiquement liés.

Corrélation croisée (cross-correlation) — le noyau est appliqué directement :

\[ g(x,y) = \sum_{s=-a}^{a}\sum_{t=-b}^{b} w(s,t)\,f(x+s,\,y+t) \tag{3.11}\]

Convolution bidimensionnelle — le noyau est pivoté de 180° avant son application :

\[ g(x,y) = \sum_{s=-a}^{a}\sum_{t=-b}^{b} w(s,t)\,f(x-s,\,y-t) \tag{3.12}\]

Pour les noyaux symétriques (gaussien, laplacien, moyenne), les deux opérations produisent des résultats identiques. Pour les noyaux asymétriques (Sobel, Prewitt), la différence est significative, comme le montrent les exemples suivants.

3.4.3.1 Corrélation (mm::conv)

%%writefile tmp/mm_out_2.cpp
#include "morph.hpp"
#include <iostream>

int main() {
    // imagem 4x4 (uint8) e kernel assimétrico 3x3
    mm::Image img(4, 4);
    unsigned char data_img[4][4] = {{ 1,  2,  3,  4},
                                    { 5,  6,  7,  8},
                                    { 9, 10, 11, 12},
                                    {13, 14, 15, 16}};
    for (int y = 0; y < 4; y++)
        for (int x = 0; x < 4; x++)
            img.at(y, x) = data_img[y][x];

    mm::Kernel w{{0,1,2},{0,0,0},{0,0,0}};

    mm::Image corr = mm::conv(img, w, mm::Border::CONSTANT);  // zero fora da imagem

    std::cout << "Imagem original:" << "\n";         std::cout << mm::drawImg(img) << "\n";
    std::cout << "Kernel:" << "\n";                  std::cout << mm::drawImg(w) << "\n";
    std::cout << "Resultado da correlação:" << "\n"; std::cout << mm::drawImg(corr) << "\n";

    return 0;
}
Overwriting tmp/mm_out_2.cpp
!g++ -I. -std=c++17 tmp/mm_out_2.cpp -o tmp/mm_out_2 \
  && ./tmp/mm_out_2
Imagem original:
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 
13 14 15 16 

Kernel:
   0    1    2 
   0    0    0 
   0    0    0 

Resultado da correlação:
 0  0  0  0 
 5  8 11  4 
17 20 23  8 
29 32 35 12 

mm::conv effectue une corrélation, c’est-à-dire qu’il applique le noyau exactement dans l’orientation fournie.

3.4.3.2 Convolution

%%writefile tmp/mm_out_3.cpp
#include "morph.hpp"
#include <iostream>

int main() {
    // repetidos aqui para a célula ser independente
    mm::Image img(4, 4);
    // Preenchendo a imagem com os valores
    for (int y = 0; y < 4; y++) {
        for (int x = 0; x < 4; x++) {
            img.at(y, x) = (unsigned char)(y * 4 + x + 1);
        }
    }

    mm::Kernel w{{0,1,2},{0,0,0},{0,0,0}};

    // kernel rotacionado 180° (equivale a np.rot90(w, 2))
    mm::Kernel w_conv{{0,0,0},{0,0,0},{2,1,0}};

    mm::Image conv = mm::conv(img, w_conv, mm::Border::CONSTANT);

    std::cout << "Imagem original:" << std::endl;
    std::cout << mm::drawImg(img) << std::endl;
    std::cout << "Kernel original:" << std::endl;
    std::cout << mm::drawImg(w) << std::endl;
    std::cout << "Kernel rotacionado 180°:" << std::endl;
    std::cout << mm::drawImg(w_conv) << std::endl;
    std::cout << "Resultado da convolução:" << std::endl;
    std::cout << mm::drawImg(conv) << std::endl;

    return 0;
}
Overwriting tmp/mm_out_3.cpp
!g++ -I. -std=c++17 tmp/mm_out_3.cpp -o tmp/mm_out_3 \
  && ./tmp/mm_out_3
Imagem original:
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 
13 14 15 16 

Kernel original:
   0    1    2 
   0    0    0 
   0    0    0 

Kernel rotacionado 180°:
   0    0    0 
   0    0    0 
   2    1    0 

Resultado da convolução:
 5 16 19 22 
 9 28 31 34 
13 40 43 46 
 0  0  0  0 

La convolution utilise le noyau tourné de 180°. Pour reproduire la définition mathématique de la convolution, on tourne le noyau (ici, [[0,1,2],[0,0,0],[0,0,0]] → [[0,0,0],[0,0,0],[2,1,0]]) avant d’appliquer mm::conv.

3.4.4 Le Rôle du Noyau

Les coefficients du noyau déterminent complètement l’effet produit par le filtre, comme résumé dans la Table 3.2.

Table 3.2: Interprétation typique des coefficients du noyau.
Caractéristique Effet typique
Coefficients positifs dont la somme vaut 1 Lissage (passe-bas)
Somme égale à 0, avec des valeurs positives et négatives Détection de contours (passe-haut)
Coefficient central positif dominant et voisins négatifs Renforcement de la netteté
Coefficients asymétriques Gradient directionnel

Exemples :

Lissage : \[ \frac{1}{9} \begin{bmatrix} 1&1&1\\ 1&1&1\\ 1&1&1 \end{bmatrix} \]

Détection de contours : \[ \begin{bmatrix} -1&-1&-1\\ -1&8&-1\\ -1&-1&-1 \end{bmatrix} \]

Renforcement de la netteté : \[ \begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix} \]

Gradient directionnel (Sobel) : \[ \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} \]

La Figure 3.10 démontre le mécanisme étape par étape : pour chaque position de la fenêtre, on multiplie chaque coefficient du kernel par le pixel correspondant de la voisinage et on additionne les produits obtenus. Le résultat est exactement la valeur définie par la Équation 3.11 pour cette position de l’image. Bien que les images produites par mm::conv0 et cv2.filter2D (ou mm::conv) soient visuellement très similaires, l’implémentation basée sur OpenCV est des milliers de fois plus rapide, comme montré ci-dessous.

AvertissementPerformance : boucles Python vs. opérations vectorisées

La fonction mm::conv0 implémente la corrélation directement en Python au moyen de boucles imbriquées. Bien que cette approche soit adaptée à des fins pédagogiques, elle exécute un grand nombre d’opérations et devient lente pour des images de grande taille.

Quant à mm::conv, elle utilise cv2.filter2D, implémenté en C++ et optimisé pour les opérations matricielles. Dans l’exemple présenté, la version vectorisée a été plus de 3000 fois plus rapide que l’implémentation pédagogique, produisant un résultat visuellement équivalent.

Les différences numériques observées se concentrent principalement sur les bords de l’image. Dans mm::conv0, les pixels des bords restent inchangés, tandis que mm::conv utilise une stratégie de réflexion des bords (cv2.BORDER_REFLECT_101, valeur par défaut de cv2.filter2D).

C’est pourquoi mm::conv0 doit être utilisée pour comprendre l’algorithme, tandis que mm::conv est l’option recommandée pour les applications pratiques.

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

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

    // Variables img_leop_gray will be provided as an already-valid mm::Image

    mm::Kernel w_mean = mm::Kernel::mean(3);
    mm::Image img_gray = img_leop_gray;

    mm::Image img_conv0 = mm::conv0(img_gray, w_mean);   // laços, bordas preservadas
    mm::Image img_conv  = mm::conv(img_gray, w_mean);    // borda refletida

    std::cout << "Correlacao no pixel central [251,251]:" << "\n";
    std::cout << "  original = " << (int)img_gray.at(251, 251) << "\n";
    std::cout << "  conv0    = " << (int)img_conv0.at(251, 251) << "\n";
    std::cout << "  conv     = " << (int)img_conv.at(251, 251) << "\n";

    mm::show(std::vector<mm::Image>{img_gray, img_conv0, img_conv},
             MM_OUT,
             std::vector<std::string>{"Original", "conv0 (laços)", "conv (vetorizado)"}, 3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_convolucao_passo_0.png");
mm::write(img_conv0, "tmp/fig_03_convolucao_passo_1.png");
mm::write(img_conv, "tmp/fig_03_convolucao_passo_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_convolucao_passo.cpp
!g++ -I. -std=c++17 tmp/fig_03_convolucao_passo.cpp -o tmp/fig_03_convolucao_passo \
  && ./tmp/fig_03_convolucao_passo \
  && test -f "tmp/fig_03_convolucao_passo.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_convolucao_passo.png"
Correlacao no pixel central [251,251]:
  original = 104
  conv0    = 102
  conv     = 102
[1] Original
[2] conv0 (laços)
[3] conv (vetorizado)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_convolucao_passo_0.png"),
            mm.read("tmp/fig_03_convolucao_passo_1.png"),
            mm.read("tmp/fig_03_convolucao_passo_2.png"),
        ],
        titles=[
            'Original',
            'conv0 (laços)',
            'conv (vetorizado)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_convolucao_passo_0.png (ver a versao Python)")
Figure 3.10: Correlação com kernel de média 3×3: versão didática mm::conv0 (bordas preservadas) vs. mm::conv (borda refletida). A diferença se concentra nas bordas.
%%writefile tmp/mm_out_4.cpp
//| echo: false
// A partir daqui o "sujeito" dos exemplos de filtragem passa a ser o
// leopardo (mais textura e bordas que o mandril).
#include "morph.hpp"
#include <filesystem>

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

    mm::Image img_gray = mm::gray(img_leop);
    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_45.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/mm_out_4.cpp
!g++ -I. -std=c++17 tmp/mm_out_4.cpp -o tmp/mm_out_4 \
  && ./tmp/mm_out_4

3.4.5 Exemple Numérique : Corrélation Pas à Pas

Pour rendre concret le mécanisme de la Équation 3.11, considérez le noyau de moyenne 3×3 (\(a=b=1\), tous les coefficients \(= 1/9 \approx 0{,}111\)) appliqué au patch 5×5 extrait de l’image du léopard. La Figure 3.11 affiche le patch avec une grille et met en évidence en jaune la fenêtre 3×3 centrée sur le pixel \([1,1]\) :

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

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

    //| label: fig-03-patch
    //| fig-cap: "*Patch* 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada."
    //| echo: true
    //| output: true

    mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255);
    mm::Kernel B = mm::Kernel::ones(3);

    std::cout << "Patch 5×5 (intensidades):\n";
    std::cout << mm::drawImg(patch);

    mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_patch.cpp
!g++ -I. -std=c++17 tmp/fig_03_patch.cpp -o tmp/fig_03_patch \
  && ./tmp/fig_03_patch \
  && test -f "tmp/fig_03_patch.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_patch.png"
Patch 5×5 (intensidades):
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 
Processando pixel (x,y)=(1,1)  |  janela do kernel 3x3
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 
try:
    mm.show(mm.read("tmp/fig_03_patch.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_patch.png (ver a versao Python)")
Figure 3.11: Patch 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada.

Pour illustrer le calcul de la corrélation, considérons le pixel à la position \([1,1]\) du patch 5×5 montré dans la Figure 3.11.. Cette position a été choisie uniquement pour des raisons pédagogiques, car elle possède un voisinage 3×3 complet autour d’elle.

Les valeurs de ce voisinage correspondent à la sous-matrice supérieure gauche du patch :

\[ \text{vizinhança} = \begin{bmatrix} 91 & 95 & 108 \\ 106 & 107 & 108 \\ 102 & 103 & 107 \end{bmatrix} \]

En appliquant la Équation 3.11 avec le noyau de moyenne :

\[ g[1,1] = \frac{ 91+95+108+106+107+108+102+103+107}{9} = \frac{927}{9} = 103 \]

Le résultat (103) est légèrement inférieur à la valeur originale du pixel central (107), car la moyenne intègre des voisins de moindre intensité, produisant l’effet de lissage. En pratique, l’algorithme démarre le traitement à \([0,0]\) et répète ce même calcul pour chaque position de l’image, déplaçant la fenêtre jusqu’à couvrir tout le domaine.

3.5 Filtrage Spatial de Lissage

Les filtres de lissage (smoothing filters) atténuent les variations brusques d’intensité, réduisant le bruit et les détails à haute fréquence. Ce sont des filtres passe-bas — ils préservent les composantes de basse fréquence (structures de grande taille) et atténuent celles de haute fréquence (bruit, contours).

3.5.1 Filtre Moyenneur (Box Filter)

Le filtre moyenneur utilise un noyau uniforme de taille \(n \times n\), dont tous les coefficients valent \(1/n^2\) :

\[ w_{\text{moyenne}} = \frac{1}{n^2} \begin{bmatrix} 1 & \cdots & 1 \\ \vdots & \ddots & \vdots \\ 1 & \cdots & 1 \end{bmatrix}_{n \times n} \tag{3.13}\]

Chaque pixel de sortie est la moyenne arithmétique des \(n^2\) pixels de son voisinage. Notez que la somme des coefficients est toujours égale à 1 — la luminosité moyenne de l’image est préservée. Des noyaux plus grands produisent un lissage plus agressif, mais estompent progressivement les contours.

La Figure 3.12 montre l’effet du filtre de moyenne avec des noyaux \(3\times3\), \(7\times7\) et \(15\times15\) sur un détail de l’image du léopard. Les résultats ont été obtenus avec mm::blur, qui implémente le filtre de moyenne via la fonction cv2.blur, équivalente à la convolution de l’image avec un noyau uniforme dont les coefficients sont \(h(x,y)=1/N^2\) ; de manière équivalente, le même résultat peut être obtenu avec mm::conv, en calculant (\(g=f*h\)). À mesure que le noyau augmente, davantage de pixels contribuent à chaque valeur de sortie, intensifiant le lissage, réduisant le bruit et rendant les détails fins et les bords progressivement plus flous.

%%writefile tmp/fig_03_media.cpp
#define MM_OUT "tmp/fig_03_media.png"
//| label: fig-03-media
//| fig-cap: "Filtro de média com *kernels* de tamanho crescente (3×3, 7×7, 15×15). O borramento das bordas aumenta com o tamanho do *kernel*."
//| echo: true
//| output: true

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

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

    // img_gray is provided

    // Détail de la région de l'œil
    int y0 = 580, y1 = 740, x0 = 680, x1 = 900;
    mm::Image img_gray_crop = mm::crop(img_gray, y0, y1, x0, x1);

    std::vector<int> sizes = {3, 7, 15};
    std::vector<mm::Image> imgs;
    imgs.push_back(img_gray_crop);
    for (int k : sizes) {
        imgs.push_back(mm::blur(img_gray_crop, k)); // ou
        //mm::conv(img_gray_crop, mm::Kernel::mean(k))
    }

    std::vector<std::string> titles;
    titles.push_back("Original");
    for (int k : sizes) {
        titles.push_back("Média " + std::to_string(k) + "×" + std::to_string(k));
    }

    mm::show(imgs, MM_OUT, titles, 4);

    return 0;
}
Overwriting tmp/fig_03_media.cpp
!g++ -I. -std=c++17 tmp/fig_03_media.cpp -o tmp/fig_03_media \
  && ./tmp/fig_03_media \
  && test -f "tmp/fig_03_media.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_media.png"
[1] Original
[2] Média 3×3
[3] Média 7×7
[4] Média 15×15
try:
    mm.show(mm.read("tmp/fig_03_media.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_media.png (ver a versao Python)")
Figure 3.12: Filtro de média com kernels de tamanho crescente (3×3, 7×7, 15×15). O borramento das bordas aumenta com o tamanho do kernel.

3.5.2 Filtre gaussien

Le filtre gaussien pondère les pixels du voisinage selon une fonction gaussienne bidimensionnelle :

\[ G(s,t) = \frac{1}{2\pi\sigma^2}\,e^{-\frac{s^2+t^2}{2\sigma^2}} \tag{3.14}\]

où \(\sigma\) est l’écart type et contrôle le rayon d’influence. Les pixels plus proches du centre ont un poids plus important ; les pixels éloignés sont progressivement ignorés.

La Figure 3.13 présente le noyau gaussien \(5\times5\) généré pour \(\sigma=1\). Le noyau a été construit à partir du produit externe de deux vecteurs gaussiens unidimensionnels, puis normalisé pour que la somme de ses coefficients soit égale à \(1\). On observe que les poids les plus importants se concentrent au centre de la matrice, en décroissant radialement vers les bords. Cette distribution fait que les pixels centraux ont une plus grande influence sur le résultat du filtrage, contribuant à un lissage plus naturel et à une meilleure préservation des contours que le filtre de moyenne.

%%writefile tmp/fig_03_gauss_kernel.cpp
#define MM_OUT "tmp/fig_03_gauss_kernel.png"
// g++ -std=c++17 -o program program.cpp -I. -lm
#include "morph.hpp"
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>

int main() {
    //| label: fig-03-gauss-kernel
    //| fig-cap: "*Kernel* Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente."
    //| echo: true
    //| output: true

    mm::Kernel w = mm::Kernel::gaussian(5, 1.0);   // mm::Kernel::gaussian(5, 1.0) na morph.hpp

    std::cout << "Kernel Gaussiano 5x5 (s=1), normalizado:" << "\n";
    for (int y = 0; y < 5; y++) {
        std::cout << "  ";
        for (int x = 0; x < 5; x++) {
            std::cout << std::fixed << std::setprecision(4) << w.at(y, x);
            if (x < 4) std::cout << "  ";
        }
        std::cout << "\n";
    }
    std::cout << "Peso central [2,2] = " << std::fixed << std::setprecision(4) << w.at(2, 2) << "   |   canto [0,0] = " << std::fixed << std::setprecision(4) << w.at(0, 0) << "\n";

    // Visualização: resposta do filtro Gaussiano a um impulso central
    mm::Image impulso(5, 5);
    impulso.at(2, 2) = 255;
    mm::drawImgPlt(mm::gaussian(impulso, 5, 1.0), MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_gauss_kernel.cpp
!g++ -I. -std=c++17 tmp/fig_03_gauss_kernel.cpp -o tmp/fig_03_gauss_kernel \
  && ./tmp/fig_03_gauss_kernel \
  && test -f "tmp/fig_03_gauss_kernel.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_gauss_kernel.png"
Kernel Gaussiano 5x5 (s=1), normalizado:
  0.0030  0.0133  0.0219  0.0133  0.0030
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0219  0.0983  0.1621  0.0983  0.0219
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0030  0.0133  0.0219  0.0133  0.0030
Peso central [2,2] = 0.1621   |   canto [0,0] = 0.0030
 3  7 11  7  3 
 7 15 25 15  7 
11 25 41 25 11 
 7 15 25 15  7 
 3  7 11  7  3 
try:
    mm.show(mm.read("tmp/fig_03_gauss_kernel.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_gauss_kernel.png (ver a versao Python)")
Figure 3.13: Kernel Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente.
NoteAvantage computationnelle de la séparabilité

Considérons un noyau carré de taille \(n \times n\). Si ce filtre est séparable (comme le filtre gaussien), la convolution 2D peut être décomposée en deux convolutions 1D : une horizontale et une verticale.

Dans ce cas, le coût par pixel passe d’environ \(O(n^2)\) opérations (convolution 2D directe) à \(O(2n)\) opérations (deux convolutions 1D). Ainsi, la complexité est réduite de manière significative, rendant le traitement plus efficace.

Comparé au filtre moyenneur, le filtre gaussien :

  • Préserve mieux les contours — la pondération radiale lisse sans créer de transitions abruptes ;
  • N’introduit pas d’effet d’anneau (ringing) dans le domaine fréquentiel, car la gaussienne est sa propre transformée de Fourier (Chapitre 5) ;
  • Est contrôlé par \(\sigma\) — augmenter \(\sigma\) équivaut à augmenter le rayon de lissage de manière continue et prévisible.

La Figure 3.14 compare les filtres de moyenne et gaussien appliqués à l’image du léopard à l’aide d’une fenêtre \(9\times9\). Le filtre de moyenne a été implémenté par convolution avec un noyau uniforme, où tous les \(81\) pixels du voisinage possèdent le même poids (\(1/81\)), tandis que le filtre gaussien a été obtenu avec cv2.GaussianBlur, utilisant des poids définis par une distribution gaussienne. Tous deux réduisent le bruit et lissent l’image, mais le filtre gaussien préserve mieux les contours et les détails locaux, comme on peut l’observer dans la région agrandie de l’œil.

La Figure 3.14 compare les filtres de moyenne et gaussien appliqués à un détail de l’image du léopard avec des noyaux \(9\times 9\). Le filtre de moyenne a été obtenu avec mm::blur, équivalent à la convolution avec un noyau uniforme dont les coefficients valent \(1/81\), tandis que le filtre gaussien a été obtenu avec mm::gaussian, équivalent à la convolution avec un noyau généré à partir d’une distribution gaussienne. Tous deux favorisent le lissage et la réduction du bruit, mais le filtre gaussien attribue un poids plus important aux pixels centraux du voisinage, préservant mieux les contours et les détails locaux, comme on peut l’observer dans la région agrandie de l’œil.

%%writefile tmp/fig_03_gauss.cpp
#define MM_OUT "tmp/fig_03_gauss.png"
//| label: fig-03-gauss
//| fig-cap: "Comparaison entre filtro de média e Gaussiano (*kernel* 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto."
//| 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_45.png");
// [pdi:state-io:end]

    // img_gray is provided as an already-valid mm::Image

    mm::Image img_media9 = mm::blur(img_gray, 9);
    mm::Image img_gauss9 = mm::gaussian(img_gray, 9, 0);

    // Détail de la région de l'œil
    mm::Image img_gray_crop = mm::crop(img_gray, 580, 740, 680, 900);
    mm::Image img_media9_crop = mm::crop(img_media9, 580, 740, 680, 900);
    mm::Image img_gauss9_crop = mm::crop(img_gauss9, 580, 740, 680, 900);

    mm::show(
        std::vector<mm::Image>{img_gray_crop, img_media9_crop, img_gauss9_crop},
        MM_OUT,
        std::vector<std::string>{"Détail: Original", "Moyenne 9×9", "Gaussien 9×9"},
        3
    );

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray_crop, "tmp/state/img_gray_crop_58.png");
// [pdi:state-io:end]

// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_gauss_0.png");
mm::write(img_media9_crop, "tmp/fig_03_gauss_1.png");
mm::write(img_gauss9_crop, "tmp/fig_03_gauss_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_gauss.cpp
!g++ -I. -std=c++17 tmp/fig_03_gauss.cpp -o tmp/fig_03_gauss \
  && ./tmp/fig_03_gauss \
  && test -f "tmp/fig_03_gauss.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_gauss.png"
[1] Détail: Original
[2] Moyenne 9×9
[3] Gaussien 9×9
try:
    mm.show(
        [
            mm.read("tmp/fig_03_gauss_0.png"),
            mm.read("tmp/fig_03_gauss_1.png"),
            mm.read("tmp/fig_03_gauss_2.png"),
        ],
        titles=[
            'Detalhe: Original',
            'Média 9×9',
            'Gaussiano 9×9',
        ],
        cols=3,
        figsize=(12, 8),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_gauss_0.png (ver a versao Python)")
Figure 3.14: Comparação entre filtro de média e Gaussiano (kernel 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto.

3.6 Filtrage Spatial de Rehaussement

Les filtres de rehaussement (sharpening filters) mettent en évidence les transitions abruptes d’intensité, augmentant la netteté et la visibilité des contours. Ce sont des filtres passe-haut — ils amplifient les composantes haute fréquence (contours, texture) et suppriment celles de basse fréquence (régions uniformes).

L’intuition est simple : si l’on soustrait d’une image sa version lissée (qui ne contient que les basses fréquences), ce qui reste correspond aux hautes fréquences — contours et détails. En ajoutant ce résidu à l’image originale, le contraste local augmente :

\[ g = f + k\,(f - f_{\text{lisse}}), \quad k > 0 \tag{3.15}\]

La Figure 3.15 illustre ce processus sur un signal 1D synthétique avec trois structures distinctes : un échelon large, un pic fin et une rampe douce. Dans le panneau ①, le signal original \(f(x)\) ; dans le ②, la version lissée \(f_{\text{lisse}}(x)\) obtenue par moyenne mobile — on note comment le pic fin est atténué. Le panneau ③ affiche le résidu \(f - f_{\text{lisse}}\), qui ne conserve que les transitions abruptes. Enfin, le panneau ④ montre \(g(x)\) : le pic, auparavant atténué, est restauré et amplifié par rapport à l’original. Ajustez \(k\) et la taille de la fenêtre pour observer le trade-off entre netteté et amplification du bruit.

Les filtres de rehaussement formalisent cette idée directement dans le kernel, sans avoir besoin de deux étapes séparées.

🎮 Simulateur : Filtrage Spatial d’Amélioration 1D g = f + k·(f − f_lisse)
k = 1.5
fenêtre = 9
σ = 0.04
① f(x) — Signal Original
↓ filtre passe-bas (moyenne mobile)
② f_lisse(x) — Pic Atténué par le Filtre
↓ soustraction : f − f_lisse
③ Résidu (f − f_lisse) — Hautes Fréquences / Bords
↓ somme : f + k · résidu
④ g(x) — Signal avec Pic Amélioré
Figure 3.15: Simulateur : Filtrage spatial d’accentuation 1D (Unsharp Masking et High-Boost)

3.6.1 Laplacien

Le Laplacien est un opérateur isotrope de dérivée seconde, c’est-à-dire qu’il répond de manière identique aux variations dans toutes les directions, contrairement aux opérateurs de dérivée première, comme Sobel et Prewitt, qui sont directionnels :

\[ \nabla^2 f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2} \tag{3.16}\]

Une propriété importante de la dérivée seconde est que sa valeur est proche de zéro dans les régions uniformes et élevée aux transitions d’intensité. Ainsi, en soustrayant le Laplacien de l’image originale, on renforce les contours et les détails, augmentant le contraste local :

\[ g(x,y) = f(x,y) - \nabla^2 f(x,y) \tag{3.17}\]

Sous forme discrète, la dérivée seconde en \(x\) est approchée par \(f(x+1,y) - 2f(x,y) + f(x-1,y)\), et de manière analogue en \(y\). En sommant les deux directions, on obtient le kernel \(w_4\) (4-voisins) ou \(w_8\) (8-voisins, incluant les diagonales) :

\[ w_4 = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}, \qquad w_8 = \begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix} \tag{3.18}\]

NoteSomme nulle et centre négatif

Les deux kernels ont une somme des coefficients égale à zéro : dans les régions uniformes, la sortie est 0 — le Laplacien ne modifie pas la luminosité moyenne, il détecte uniquement les variations. Le centre négatif indique que le pixel est comparé à ses voisins : plus il se démarque (vers le haut ou vers le bas), plus la valeur absolue du Laplacien en ce point est grande.

Dans l’exemple suivant, le pixel central \([1,1]=107\) possède des voisins \(\{95, 106, 108, 103\}\). Comme ces valeurs sont proches les unes des autres, la région est presque uniforme et le Laplacien retourne une valeur faible, produisant peu de rehaussement. Dans les régions de bord, où il y a des différences plus grandes entre le pixel central et ses voisins, le Laplacien prend des valeurs plus élevées (positives ou négatives), et l’opération de Équation 3.17 intensifie ces transitions.

La Figure 3.16 illustre le calcul du Laplacien avec le noyau \(w_4\) dans un voisinage \(3\times3\) mis en évidence à l’intérieur d’un patch \(5\times5\). L’exemple montre la valeur obtenue par l’opérateur et le pixel correspondant rehaussé dans l’image de sortie, qui passe de 107 à 123.

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

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

    // img_gray is provided as an already-valid mm::Image variable

    //| label: fig-03-laplaciano-patch
    //| fig-cap: "*Kernel* Laplaciano w4 sobre o *patch* 5×5: a janela amarela destaca a vizinhança 3×3 onde o operador de segunda derivada é calculado."
    //| echo: true
    //| output: true

    mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255);
    mm::Kernel B{{1,1,1},{1,1,1},{1,1,1}};  // 3x3 kernel with all ones (laplacian visualization)

    std::cout << "Patch 5x5 (intensidades):" << std::endl;
    std::cout << mm::drawImg(patch) << std::endl;

    mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_laplaciano_patch.cpp
!g++ -I. -std=c++17 tmp/fig_03_laplaciano_patch.cpp -o tmp/fig_03_laplaciano_patch \
  && ./tmp/fig_03_laplaciano_patch \
  && test -f "tmp/fig_03_laplaciano_patch.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_laplaciano_patch.png"
Patch 5x5 (intensidades):
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 

Processando pixel (x,y)=(1,1)  |  janela do kernel 3x3
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 
try:
    mm.show(mm.read("tmp/fig_03_laplaciano_patch.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_laplaciano_patch.png (ver a versao Python)")
Figure 3.16: Kernel Laplaciano w4 sobre o patch 5×5: a janela amarela destaca a vizinhança 3×3 onde o operador de segunda derivada é calculado.

La Figure 3.17 compare l’application des kernels Laplaciens \(w_4\) et \(w_8\) sur un recadrage plus grand de l’image du léopard. Pour chaque cas, sont présentées la réponse brute de l’opérateur, qui met en évidence les contours et les transitions d’intensité, ainsi que l’image obtenue après la rehaussement par soustraction du Laplacien. On observe que le kernel \(w_8\), en considérant également les voisins diagonaux, produit une réponse plus intense et détecte des variations dans davantage de directions, résultant en un rehaussement légèrement plus accentué.

%%writefile tmp/fig_03_laplaciano.cpp
#define MM_OUT "tmp/fig_03_laplaciano.png"
//| label: fig-03-laplaciano
//| fig-cap: "Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais."
//| echo: true
//| output: true

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

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

    mm::Kernel w4{{0,1,0},{1,-4,1},{0,1,0}};
    mm::Kernel w8{{1,1,1},{1,-8,1},{1,1,1}};

    mm::show(
        std::vector<mm::Image>{img_gray_crop, mm::laplacian_viz(img_gray_crop, w4), mm::laplacian(img_gray_crop, w4),
                             img_gray_crop, mm::laplacian_viz(img_gray_crop, w8), mm::laplacian(img_gray_crop, w8)},
        MM_OUT,
        std::vector<std::string>{"Original", "Laplaciano w4", "Realce w4",
                                "Original", "Laplaciano w8", "Realce w8"},
        3
    );

    return 0;
}
Overwriting tmp/fig_03_laplaciano.cpp
!g++ -I. -std=c++17 tmp/fig_03_laplaciano.cpp -o tmp/fig_03_laplaciano \
  && ./tmp/fig_03_laplaciano \
  && test -f "tmp/fig_03_laplaciano.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_laplaciano.png"
[1] Original
[2] Laplaciano w4
[3] Realce w4
[4] Original
[5] Laplaciano w8
[6] Realce w8
try:
    mm.show(mm.read("tmp/fig_03_laplaciano.png"), figsize=(14, 8))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_laplaciano.png (ver a versao Python)")
Figure 3.17: Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais.

3.6.2 Opérateur de Sobel

L’opérateur de Sobel estime les dérivées partielles du premier ordre dans les directions horizontale et verticale. Contrairement au Laplacien (dérivée seconde), Sobel est directionnel et plus robuste au bruit, car chaque kernel combine une dérivée avec un lissage gaussien perpendiculaire :

\[ G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} * f, \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} * f \tag{3.19}\]

\(G_x\) détecte les bords verticaux (variation dans la direction \(x\)) ; \(G_y\) détecte les bords horizontaux (variation dans la direction \(y\)). Les poids \(\{1,2,1\}\) dans la direction perpendiculaire correspondent au lissage gaussien 1D, qui réduit la sensibilité au bruit.

NoteSobel est une corrélation, pas une convolution

Les kernels de Sobel sont asymétriques — la rotation de 180° modifie le résultat. cv2.Sobel implémente une corrélation croisée (comme cv2.filter2D). Pour obtenir la dérivée directionnelle correcte, les signes sont déjà définis pour la corrélation : \(G_x\) retourne des valeurs positives lorsque l’intensité croît de la gauche vers la droite.

La magnitude du gradient combine les deux composantes, représentant la force du bord indépendamment de la direction :

\[ |\nabla f| = \sqrt{G_x^2 + G_y^2} \tag{3.20}\]

Et la direction du gradient (perpendiculaire au bord) est :

\[ \theta = \arctan\left(\frac{G_y}{G_x}\right) \tag{3.21}\]

Pour illustrer numériquement, on calcule \(G_x\) et \(G_y\) manuellement au pixel central \([1,1]\) du patch 5×5 :

La valeur réduite de \(|{\nabla f}|\) dans ce patch confirme que la région est presque uniforme, car le gradient prend des valeurs élevées uniquement là où il y a des changements significatifs d’intensité. La Figure 3.18 applique l’opérateur de Sobel à un recadrage plus large de l’image du léopard. Sont présentées les réponses horizontale (\(G_x\)) et verticale (\(G_y\)), obtenues par convolution avec les kernels de Sobel respectifs, ainsi que la magnitude \(|{\nabla f}|\), calculée à partir de la combinaison des deux. Alors que \(G_x\) met en évidence les bords verticaux et \(G_y\) les bords horizontaux, la magnitude souligne les bords dans toutes les directions.

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

//| label: fig-03-sobel
//| fig-cap: "Opérateur de Sobel sur l'image du léopard : la magnitude |∇f| combine les gradients horizontal et vertical, révélant toutes les arêtes. La décomposition Gx/Gy avec signe reste dans la piste Python — mm::sobel retourne la magnitude déjà avec clip."
//| echo: true
//| output: true

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

    mm::Image mag = mm::sobel(img_gray_crop);

    mm::show(std::vector<mm::Image>{img_gray_crop, mag},
             MM_OUT,
             std::vector<std::string>{"Original", "Magnitude |grad f| (mm.sobel)"}, 2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_sobel_0.png");
mm::write(mag, "tmp/fig_03_sobel_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_sobel.cpp
!g++ -I. -std=c++17 tmp/fig_03_sobel.cpp -o tmp/fig_03_sobel \
  && ./tmp/fig_03_sobel \
  && test -f "tmp/fig_03_sobel.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_sobel.png"
[1] Original
[2] Magnitude |grad f| (mm.sobel)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_sobel_0.png"),
            mm.read("tmp/fig_03_sobel_1.png"),
        ],
        titles=[
            'Original',
            'Magnitude |grad f| (mm.sobel)',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_sobel_0.png (ver a versao Python)")
Figure 3.18: Operador de Sobel na imagem do leopardo: a magnitude |∇f| combina os gradientes horizontal e vertical, revelando todas as bordas. A decomposição Gx/Gy com sinal fica na trilha Python — mm::sobel devolve a magnitude já com clip.

3.6.3 Opérateur de Prewitt

L’opérateur de Prewitt est structurellement identique à Sobel, mais remplace la pondération gaussienne \(\{1,2,1\}\) par des poids uniformes \(\{1,1,1\}\) :

\[ G_x = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{bmatrix} * f, \qquad G_y = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix} * f \tag{3.22}\]

L’amplitude et la direction du gradient suivent les mêmes équations que Sobel (Équation 3.20 et Équation 3.21). La différence pratique est que Prewitt est légèrement plus sensible au bruit — le lissage perpendiculaire uniforme accorde moins de poids au pixel central de la ligne — mais il est plus simple sur le plan computationnel. Sur des images à faible bruit, les résultats sont équivalents.

%%writefile tmp/fig_03_prewitt.cpp
#define MM_OUT "tmp/fig_03_prewitt.png"
//| label: fig-03-prewitt
//| fig-cap: "Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip."
//| echo: true
//| output: true
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

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

    mm::show(std::vector<mm::Image>{img_gray_crop, mm::prewitt(img_gray_crop)},
             MM_OUT,
             std::vector<std::string>{"Original", "Magnitude |grad f| (mm.prewitt)"},
             2);
    return 0;
}
Overwriting tmp/fig_03_prewitt.cpp
!g++ -I. -std=c++17 tmp/fig_03_prewitt.cpp -o tmp/fig_03_prewitt \
  && ./tmp/fig_03_prewitt \
  && test -f "tmp/fig_03_prewitt.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_prewitt.png"
[1] Original
[2] Magnitude |grad f| (mm.prewitt)
try:
    mm.show(mm.read("tmp/fig_03_prewitt.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_prewitt.png (ver a versao Python)")
Figure 3.19: Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip.

3.6.4 Unsharp Masking (USM)

L’Unsharp Masking est une technique classique de renforcement de netteté issue de la photographie analogique, aujourd’hui largement utilisée dans les logiciels de retouche d’images. L’idée centrale est d’extraire les composantes haute fréquence de l’image (contours et détails) et de les additionner à l’originale avec un poids \(k\) :

Table 3.3: Étapes de l’Unsharp Masking.
Étape Opération Description
1 \(\bar{f} = f * G_\sigma\) Lissage par gaussienne — conserve les basses fréquences
2 \(m = f - \bar{f}\) Masque : différence = hautes fréquences (contours)
3 \(g = f + k \cdot m\) Somme pondérée du masque à l’originale

En remplaçant l’étape 2 dans l’étape 3, on obtient l’expression compacte :

\[ g = f + k\,(f - f*G_\sigma) = (1+k)\,f - k\,(f*G_\sigma) \tag{3.23}\]

Le paramètre \(k\) contrôle l’intensité du renforcement :

  • \(k = 0\) : aucun renforcement (\(g = f\)) ;
  • \(k = 1\) : USM classique — double la contribution des hautes fréquences ;
  • \(k > 1\) : High Boost Filtering — amplification au-delà du double, utile pour les images très floues.
AvertissementAmplification du bruit

L’USM ne distingue pas les contours du bruit — les deux sont des composantes haute fréquence. Pour un \(k\) élevé, le bruit présent dans l’image est amplifié en même temps que les contours. Il est donc recommandé d’appliquer un léger lissage avant l’USM sur les images bruitées, ou d’utiliser un \(\sigma\) petit dans la gaussienne.

Pour illustrer les étapes de l’USM, la Figure 3.20 applique la méthode à un patch \(30\times30\) de l’image du léopard, en utilisant \(\sigma=1\) et \(k=1\). Initialement, l’image est lissée par un filtre gaussien. Ensuite, le masque haute fréquence est obtenu par la différence entre l’image originale et l’image lissée. Enfin, ce masque est ajouté à l’image originale, renforçant les contours et les détails. La figure présente les trois étapes du processus et le résultat final du rehaussement.

%%writefile tmp/fig_03_usm2.cpp
#define MM_OUT "tmp/fig_03_usm2.png"
//| label: fig-03-usm2
//| fig-cap: "Realce por *Unsharp Masking* num *patch* do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada."
//| 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_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");
// [pdi:state-io:end]

    mm::Image patch = mm::crop(img_gray_crop, 35, 65, 45, 75);

    mm::Image p_suave = mm::gaussian(patch, 7, 1.0);   // suavização Gaussiana
    mm::Image p_usm   = mm::usm(patch, 1.0);           // realce completo (k = 1.0)

    mm::show(std::vector<mm::Image>{patch, p_suave, p_usm},
             MM_OUT,
             std::vector<std::string>{"Patch original", "Suavizado (σ=1)", "Realçado USM (k=1)"}, 3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(patch, "tmp/fig_03_usm2_0.png");
mm::write(p_suave, "tmp/fig_03_usm2_1.png");
mm::write(p_usm, "tmp/fig_03_usm2_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_usm2.cpp
!g++ -I. -std=c++17 tmp/fig_03_usm2.cpp -o tmp/fig_03_usm2 \
  && ./tmp/fig_03_usm2 \
  && test -f "tmp/fig_03_usm2.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_usm2.png"
[1] Patch original
[2] Suavizado (σ=1)
[3] Realçado USM (k=1)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_usm2_0.png"),
            mm.read("tmp/fig_03_usm2_1.png"),
            mm.read("tmp/fig_03_usm2_2.png"),
        ],
        titles=[
            'Patch original',
            'Suavizado (σ=1)',
            'Realçado USM (k=1)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_usm2_0.png (ver a versao Python)")
Figure 3.20: Realce por Unsharp Masking num patch do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada.

La Figure 3.21 applique la méthode USM à un recadrage plus large de l’image du léopard en utilisant \(\sigma=1\) et différentes valeurs du facteur de gain \(k\). Dans tous les cas, le masque haute fréquence est obtenu par la différence entre l’image originale et sa version lissée par un filtre gaussien. Le paramètre \(k\) contrôle l’intensité du rehaussement : des valeurs plus faibles produisent une augmentation subtile de la netteté, tandis que des valeurs plus élevées renforcent progressivement les contours et les détails. On observe que, pour des valeurs élevées de \(k\), des halos apparaissent autour des contours et le bruit présent dans l’image se trouve amplifié.

%%writefile tmp/fig_03_usm.cpp
#define MM_OUT "tmp/fig_03_usm.png"
//| label: fig-03-usm
//| fig-cap: "*Unsharp Masking* na imagem do leopardo com σ=1 e k de 0.5 a 8.0. Para k>2 surgem halos nas bordas e o ruído de fundo aparece."
//| echo: true
//| output: true

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

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

    mm::show(
        std::vector<mm::Image>{img_gray_crop,
            mm::usm(img_gray_crop, 0.5), mm::usm(img_gray_crop, 1.0),
            mm::usm(img_gray_crop, 3.0), mm::usm(img_gray_crop, 5.0),
            mm::usm(img_gray_crop, 8.0)},
        MM_OUT,
        std::vector<std::string>{"Original", "USM k=0.5", "USM k=1.0", "USM k=3.0", "USM k=5.0", "USM k=8.0"},
        3
    );
    return 0;
}
Overwriting tmp/fig_03_usm.cpp
!g++ -I. -std=c++17 tmp/fig_03_usm.cpp -o tmp/fig_03_usm \
  && ./tmp/fig_03_usm \
  && test -f "tmp/fig_03_usm.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_usm.png"
[1] Original
[2] USM k=0.5
[3] USM k=1.0
[4] USM k=3.0
[5] USM k=5.0
[6] USM k=8.0
try:
    mm.show(mm.read("tmp/fig_03_usm.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_usm.png (ver a versao Python)")
Figure 3.21: Unsharp Masking na imagem do leopardo com σ=1 e k de 0.5 a 8.0. Para k>2 surgem halos nas bordas e o ruído de fundo aparece.

3.6.5 Détecteur de Canny

Canny combine quatre étapes en séquence — lissage gaussien, gradient de Sobel, suppression des non-maxima et hystérésis par double seuil — pour produire des contours fins, binaires et connectés. Contrairement à Sobel et Prewitt, le résultat n’est pas une carte de gradient continue, mais un masque où chaque pixel est un contour ou ne l’est pas.

Le paramètre central est la paire de seuils \((T_{low}, T_{high})\). Les pixels dont le gradient est supérieur à \(T_{high}\) sont des contours certains ; ceux en dessous de \(T_{low}\) sont écartés. Les pixels ambigus — entre les deux seuils — sont décidés par hystérésis : ils deviennent des contours s’ils sont connectés à un contour certain, et sont écartés dans le cas contraire. Cela évite à la fois la perte de segments faibles de contours réels et l’inclusion de bruit isolé. Une heuristique courante est \(T_{high} = 3 \times T_{low}\).

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

using namespace std;

//| label: fig-03-canny
//| fig-cap: "Detecteur de Canny avec différentes paires de seuil : les seuils bas capturent plus de bords (y compris le bruit) ; les seuils élevés ne retiennent que les bords les plus forts."
//| echo: true
//| output: true

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

    mm::show(
        vector<mm::Image>{img_gray_crop,
                          mm::canny(img_gray_crop, 30, 90),
                          mm::canny(img_gray_crop, 60, 180),
                          mm::canny(img_gray_crop, 120, 240)},
        MM_OUT,
        vector<string>{"Original", "Canny (30/90)", "Canny (60/180)", "Canny (120/240)"},
        4
    );

    return 0;
}
Overwriting tmp/fig_03_canny.cpp
!g++ -I. -std=c++17 tmp/fig_03_canny.cpp -o tmp/fig_03_canny \
  && ./tmp/fig_03_canny \
  && test -f "tmp/fig_03_canny.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_canny.png"
[1] Original
[2] Canny (30/90)
[3] Canny (60/180)
[4] Canny (120/240)
try:
    mm.show(mm.read("tmp/fig_03_canny.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_canny.png (ver a versao Python)")
Figure 3.22: Detector de Canny com diferentes pares de limiar: limiares baixos capturam mais bordas (inclusive ruído); limiares altos retêm apenas as bordas mais fortes.
NoteChoix des seuils

Une heuristique courante est \(T_{high} = 3 \times T_{low}\). Les valeurs typiques dépendent de la plage de gradient de l’image — cv2.Canny accepte des valeurs absolues dans \([0, 255]\). Pour des images à contraste variable, calculer les seuils à partir des percentiles de la magnitude de Sobel est plus robuste que des valeurs fixes.

3.7 Filtres d’Ordre : Filtre Médian

Les filtres d’ordre (order-statistic filters) remplacent le pixel central par la valeur d’un centile de la distribution des intensités du voisinage — contrairement aux filtres linéaires, qui calculent des combinaisons pondérées. Le plus important est le filtre médian.

3.7.1 Bruit Impulsif : Sel et Poivre

Le bruit sel et poivre (salt-and-pepper noise) remplace des pixels aléatoires par des valeurs extrêmes : 0 (poivre, noir) ou 255 (sel, blanc). Il est courant dans la transmission d’images avec des erreurs de bits et dans les caméras équipées de capteurs défectueux.

Pour comprendre pourquoi les filtres linéaires échouent, considérons un voisinage 3×3 où un seul pixel a été corrompu à 255 :

\[ \text{voisinage} = \begin{bmatrix} 102 & 98 & 105 \\ 100 & \mathbf{255} & 97 \\ 103 & 99 & 101 \end{bmatrix} \]

Table 3.4: Moyenne vs. médiane avec un pixel corrompu. La médiane ignore la valeur aberrante ; la moyenne est décalée d’environ 40 niveaux.
Méthode Calcul Résultat
Moyenne (102+98+…+255+…+101)/9 ≈ 140
Médiane {97,98,99,100,101,102,103,105,255} 101
AvertissementPourquoi les filtres de moyenne échouent-ils avec le bruit impulsif ?

La moyenne est sensible aux valeurs aberrantes — un seul pixel avec une valeur de 255 dans un voisinage de valeur ≈ 100 élève la sortie à ≈ 140, propageant le bruit dans l’image. La médiane, étant un estimateur robuste, sélectionne la valeur centrale de la distribution ordonnée, écartant naturellement les extrêmes sans aucun ajustement spécial.

L’exemple suivant illustre le comportement de la moyenne et de la médiane en présence d’un pixel corrompu par un bruit impulsif. On observe que la moyenne est fortement influencée par la valeur extrême (255), produisant une estimation éloignée des valeurs prédominantes du voisinage. Quant à la médiane, elle reste proche de la valeur originale de la région, mettant en évidence sa plus grande robustesse aux valeurs aberrantes et justifiant son utilisation dans la suppression du bruit sel et poivre.

La Figure 3.23 présente l’effet du bruit sel et poivre à différentes densités. Le bruit a été généré en remplaçant aléatoirement une fraction des pixels par des valeurs minimales (0, poivre) et maximales (255, sel). À mesure que la densité augmente de 2 % à 10 %, la quantité de pixels corrompus croît, rendant la dégradation visuelle plus évidente et rendant plus difficile la perception des détails de l’image.

%%writefile tmp/fig_03_ruido.cpp
#define MM_OUT "tmp/fig_03_ruido.png"
//| label: fig-03-ruido
//| fig-cap: "Ruído sal e pimenta com densidades crescentes (2%, 5%, 10%): metade dos pixels corrompidos vira sal (255), metade pimenta (0)."
//| echo: true
//| output: true

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

mm::Image salt_pepper(const mm::Image& img, double prob) {
    mm::Image out = img;
    int h = out.h;
    int w = out.w;
    std::mt19937 rng(42);
    std::uniform_int_distribution<int> dist_y(0, h - 1);
    std::uniform_int_distribution<int> dist_x(0, w - 1);
    std::uniform_real_distribution<double> dist_r(0.0, 1.0);
    int n = static_cast<int>(prob * h * w);
    for (int i = 0; i < n; ++i) {
        int y = dist_y(rng);
        int x = dist_x(rng);
        out.at(y, x) = (dist_r(rng) < 0.5) ? 0 : 255;
    }
    return out;
}

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

    mm::Image n2 = salt_pepper(img_gray_crop, 0.02);
    mm::Image n5 = salt_pepper(img_gray_crop, 0.05);
    mm::Image n10 = salt_pepper(img_gray_crop, 0.10);

    mm::show(std::vector<mm::Image>{img_gray_crop, n2, n5, n10},
             MM_OUT,
             std::vector<std::string>{"Original", "Ruido 2%", "Ruido 5%", "Ruido 10%"}, 4);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_ruido_0.png");
mm::write(n2, "tmp/fig_03_ruido_1.png");
mm::write(n5, "tmp/fig_03_ruido_2.png");
mm::write(n10, "tmp/fig_03_ruido_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_ruido.cpp
!g++ -I. -std=c++17 tmp/fig_03_ruido.cpp -o tmp/fig_03_ruido \
  && ./tmp/fig_03_ruido \
  && test -f "tmp/fig_03_ruido.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_ruido.png"
[1] Original
[2] Ruido 2%
[3] Ruido 5%
[4] Ruido 10%
try:
    mm.show(
        [
            mm.read("tmp/fig_03_ruido_0.png"),
            mm.read("tmp/fig_03_ruido_1.png"),
            mm.read("tmp/fig_03_ruido_2.png"),
            mm.read("tmp/fig_03_ruido_3.png"),
        ],
        titles=[
            'Original',
            'Ruido 2%',
            'Ruido 5%',
            'Ruido 10%',
        ],
        cols=4,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_ruido_0.png (ver a versao Python)")
Figure 3.23: Ruído sal e pimenta com densidades crescentes (2%, 5%, 10%): metade dos pixels corrompidos vira sal (255), metade pimenta (0).

3.7.2 Filtre Médian

Le filtre médian remplace chaque pixel par la valeur médiane des pixels de son voisinage \(n \times n\) :

\[ g(x,y) = \text{med}_{(s,t) \in \mathcal{V}_{n}} \{f(x+s, y+t)\} \tag{3.24}\]

La valeur médiane est celle qui occupe la position centrale lorsque les \(n^2\) valeurs du voisinage sont triées. Pour une fenêtre \(3\times3\) (\(n^2=9\) pixels), la médiane est la 5ᵉ valeur de la séquence ordonnée.

Pour illustrer, considérons le même patch 5×5 avec un pixel corrompu artificiellement en \([1,1]\) :

L’exemple confirme : même avec le pixel corrompu à 255, la médiane retourne la valeur centrale correcte — le outlier occupe la dernière position dans le tri et est écarté naturellement.

Parce qu’elle est basée sur le tri et non sur la somme, la médiane possède trois propriétés fondamentales qui la distinguent des filtres linéaires :

  • Robuste au bruit impulsionnel — les outliers vont aux extrémités de la séquence triée et n’affectent pas la valeur centrale ;
  • Préservatrice des contours — les transitions abruptes d’intensité sont conservées, car la médiane sélectionne une valeur qui existe déjà dans le voisinage, sans créer de nouveaux niveaux intermédiaires ;
  • Non linéaire — elle ne peut pas être exprimée comme une convolution, donc mm::conv ne s’applique pas ; on utilise cv2.medianBlur.

La Figure 3.24 compare différentes techniques de suppression du bruit sel et poivre appliquées à une image contenant 10 % de pixels corrompus. Les filtres gaussien, moyen, médian, bilatéral et morphologique (chapitre suivant) ont été évalués, permettant ainsi d’observer le compromis entre la suppression du bruit et la préservation des détails. En général, les filtres de moyenne et gaussien réduisent le bruit, mais tendent à estomper les contours, tandis que le filtre médian offre de meilleures performances pour le bruit impulsionnel. Le filtre bilatéral préserve mieux les contours, et le filtre morphologique élimine une grande partie des pixels corrompus sans dégrader excessivement la structure de l’image.

%%writefile tmp/fig_03_ruido_filtros.cpp
#define MM_OUT "tmp/fig_03_ruido_filtros.png"
//#| label: fig-03-ruido-filtros
//#| fig-cap: "Filtros para ruído sal e pimenta (10%): Gaussiano, Média e Mediana. Bilateral e morfológico (open+close) ficam só na trilha Python — bilateral não tem equivalente em morph.hpp e morfologia é do próximo capítulo."
//#| echo: true
//#| output: true
#include "morph.hpp"
#include <random>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
#include <filesystem>

// Função para adicionar ruído sal e pimenta
mm::Image salt_pepper(const mm::Image& img, double prob) {
    mm::Image out = img;  // cópia
    int h = img.h, w = img.w;
    std::mt19937 rng(42);  // semente fixa, mesma do original
    std::uniform_int_distribution<int> dist_y(0, h - 1);
    std::uniform_int_distribution<int> dist_x(0, w - 1);
    std::uniform_real_distribution<double> dist_rand(0.0, 1.0);

    int n_pixels = static_cast<int>(prob * h * w);
    for (int i = 0; i < n_pixels; ++i) {
        int y = dist_y(rng);
        int x = dist_x(rng);
        out.at(y, x) = (dist_rand(rng) < 0.5) ? 0 : 255;
    }
    return out;
}

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

    // img_gray_crop é fornecido automaticamente

    // Aplicar ruído sal e pimenta
    mm::Image noisy = salt_pepper(img_gray_crop, 0.10);

    // Filtros para comparação
    mm::Image f_gauss   = mm::gaussian(noisy, 5, 1.0);
    mm::Image f_media   = mm::blur(noisy, 5);
    mm::Image f_median3 = mm::median(noisy, 3);
    mm::Image f_median5 = mm::median(noisy, 5);

    // Exibir os resultados
    mm::show(
        std::vector<mm::Image>{img_gray_crop, noisy, f_gauss, f_media, f_median3, f_median5},
        MM_OUT,
        std::vector<std::string>{"Original", "Ruido 10%", "Gaussiano 5x5", "Media 5x5",
                                 "Mediana 3x3", "Mediana 5x5"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_ruido_filtros_0.png");
mm::write(noisy, "tmp/fig_03_ruido_filtros_1.png");
mm::write(f_gauss, "tmp/fig_03_ruido_filtros_2.png");
mm::write(f_media, "tmp/fig_03_ruido_filtros_3.png");
mm::write(f_median3, "tmp/fig_03_ruido_filtros_4.png");
mm::write(f_median5, "tmp/fig_03_ruido_filtros_5.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_ruido_filtros.cpp
!g++ -I. -std=c++17 tmp/fig_03_ruido_filtros.cpp -o tmp/fig_03_ruido_filtros \
  && ./tmp/fig_03_ruido_filtros \
  && test -f "tmp/fig_03_ruido_filtros.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_ruido_filtros.png"
[1] Original
[2] Ruido 10%
[3] Gaussiano 5x5
[4] Media 5x5
[5] Mediana 3x3
[6] Mediana 5x5
try:
    mm.show(
        [
            mm.read("tmp/fig_03_ruido_filtros_0.png"),
            mm.read("tmp/fig_03_ruido_filtros_1.png"),
            mm.read("tmp/fig_03_ruido_filtros_2.png"),
            mm.read("tmp/fig_03_ruido_filtros_3.png"),
            mm.read("tmp/fig_03_ruido_filtros_4.png"),
            mm.read("tmp/fig_03_ruido_filtros_5.png"),
        ],
        titles=[
            'Original',
            'Ruido 10%',
            'Gaussiano 5x5',
            'Media 5x5',
            'Mediana 3x3',
            'Mediana 5x5',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_ruido_filtros_0.png (ver a versao Python)")
Figure 3.24: Filtros para ruído sal e pimenta (10%): Gaussiano, Média e Mediana. Bilateral e morfológico (open+close) ficam só na trilha Python — bilateral não tem equivalente em morph.hpp e morfologia é do próximo capítulo.

3.8 Application pratique : Prétraitement pour la segmentation

En pratique, les techniques de ce chapitre sont rarement utilisées de manière isolée. Un pipeline de prétraitement typique combine plusieurs étapes en séquence, en s’adaptant au type d’image et à l’application. La Figure 3.25 illustre un pipeline complet :

  1. Égalisation d’histogramme (CLAHE) : normalise le contraste indépendamment des conditions d’éclairage ;
  2. Filtre gaussien : lisse le bruit d’acquisition sans détruire les contours ;
  3. Détection de contours (Sobel/Canny) : extrait les structures pertinentes pour la segmentation.
NoteL’ordre compte

L’ordre des opérations affecte le résultat final. En général : (1) normalisation de l’intensité → (2) réduction du bruit → (3) rehaussement/segmentation. Inverser l’ordre peut amplifier le bruit ou perdre les contours avant de les détecter.

%%writefile tmp/fig_03_pipeline.cpp
#define MM_OUT "tmp/fig_03_pipeline.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_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");
// [pdi:state-io:end]

    //| label: fig-03-pipeline
    //| fig-cap: "*Pipeline* de pré-processamento: equalização → Gaussiano → Canny. A trilha Python usa CLAHE no lugar da equalização global; CLAHE não tem equivalente em morph.hpp."
    //| echo: true
    //| output: true

    mm::Image img_eq = mm::equalize(img_gray_crop);     // Etapa 1: equalização global
    mm::Image img_gauss = mm::gaussian(img_eq, 5, 0);      // Etapa 2: Gaussiano
    mm::Image edges = mm::canny(img_gauss, 50, 150);   // Etapa 3: Canny

    mm::Image edges_direct = mm::canny(img_gray_crop, 50, 150);   // Canny direto, sem pré-processo

    mm::show(
        std::vector<mm::Image>{img_gray_crop, img_eq, img_gauss, edges, edges_direct},
        MM_OUT,
        std::vector<std::string>{"Original", "1. Equalizado", "2. Gaussiano", "3. Canny (pipeline)", "Canny (direto)"},
        5
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_pipeline_0.png");
mm::write(img_eq, "tmp/fig_03_pipeline_1.png");
mm::write(img_gauss, "tmp/fig_03_pipeline_2.png");
mm::write(edges, "tmp/fig_03_pipeline_3.png");
mm::write(edges_direct, "tmp/fig_03_pipeline_4.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_pipeline.cpp
!g++ -I. -std=c++17 tmp/fig_03_pipeline.cpp -o tmp/fig_03_pipeline \
  && ./tmp/fig_03_pipeline \
  && test -f "tmp/fig_03_pipeline.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_pipeline.png"
[1] Original
[2] 1. Equalizado
[3] 2. Gaussiano
[4] 3. Canny (pipeline)
[5] Canny (direto)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_pipeline_0.png"),
            mm.read("tmp/fig_03_pipeline_1.png"),
            mm.read("tmp/fig_03_pipeline_2.png"),
            mm.read("tmp/fig_03_pipeline_3.png"),
            mm.read("tmp/fig_03_pipeline_4.png"),
        ],
        titles=[
            'Original',
            '1. Equalizado',
            '2. Gaussiano',
            '3. Canny (pipeline)',
            'Canny (direto)',
        ],
        cols=5,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_pipeline_0.png (ver a versao Python)")
Figure 3.25: Pipeline de pré-processamento: equalização → Gaussiano → Canny. A trilha Python usa CLAHE no lugar da equalização global; CLAHE não tem equivalente em morph.hpp.

3.9 Résumé

Dans ce chapitre, les principales techniques de traitement dans le domaine spatial ont été présentées, de la manipulation directe des pixels au filtrage par voisinage :

  • Opérations ponctuelles : arithmétiques saturées (mm::addm, mm::subm) et logiques bit à bit (mm::band, mm::bor, mm::bnot) pour le recadrage de ROI et la combinaison d’images ; alpha blending (mm::blend) pour la fusion pondérée avec poids \(\alpha \in [0,1]\).
  • Histogramme : fonction discrète de distribution des intensités ; visualisé avec mm::histImg et calculé avec mm::hist ; base pour le diagnostic tonal et pour les techniques d’égalisation et de spécification.
  • Égalisation : redistribution automatique des intensités par la CDF (mm::equalize), avec variante adaptative CLAHE pour le contrôle local du contraste.
  • Spécification d’histogramme : transfert du profil tonal d’une image de référence via le mappage inverse de la CDF — généralisation de l’égalisation pour des distributions arbitraires.
  • Corrélation et convolution : mécanisme de fenêtre glissante implémenté dans mm::conv (cv2.filter2D) ; différenciés par la rotation de 180° du kernel — pertinent uniquement pour les kernels asymétriques.
  • Filtres de lissage : moyenne (kernel uniforme, estompe les bords proportionnellement à la taille) et gaussien (pondération radiale, séparable, sans ringing, préserve mieux les bords).
  • Filtres de rehaussement : laplacien (\(w_4\)/\(w_8\), seconde dérivée isotrope), Sobel (gradient directionnel du premier ordre, avec magnitude \(|\nabla f|\) et direction \(\theta\)) et Unsharp Masking (amplification des hautes fréquences avec paramètre \(k\)).
  • Filtre médian : non linéaire, robuste aux outliers, préserve les bords — supérieur aux filtres linéaires pour le bruit sel et poivre.
  • Pipeline pratique : enchaînement CLAHE → Gaussien → Canny comme stratégie de prétraitement ; mm::drawImgKernel pour la visualisation didactique de la fenêtre glissante.

Le Chapitre 4 abordera la morphologie mathématique (érosion, dilatation, ouverture et fermeture), en explorant en profondeur les fonctions mm::ero et mm::dil de la bibliothèque morph.py. Ensuite, le Chapitre 5 présentera le traitement dans le domaine fréquentiel, avec un accent sur la Transformée de Fourier et les techniques de filtrage spectral.

3.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 — y compris les fonctions de la bibliothèque morph.py et les expériences réalisées dans ce chapitre.

Pour chaque chapitre, nous avons préparé un projet spécifique sur la plateforme avec le PDF du chapitre, les notebooks et les supports auxiliaires. Nous suggérons d’explorer en particulier :

  • Guide d’étude : résumé structuré des concepts, idéal pour la révision avant les examens ;
  • Conversation : posez vos questions sur l’égalisation, la convolution, les filtres et les pipelines directement au tuteur ;
  • Questions fréquentes : questions typiques sur la différence entre moyenne et médiane, USM, Laplacien vs. Sobel.
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 sous 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 03

🌐 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 à partir de l’édition en anglais ou en français, ou que 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, articles scientifiques et autres sources académiques fiables pour valider les informations. Dans la mesure du possible, exécutez les exemples pratiques fournis dans ce chapitre pour vérifier les résultats.

3.11 Liste d’exercices

  1. (10%) Expliquez la différence entre convolution et corrélation croisée. Pour quels types de noyaux les résultats sont-ils identiques ? Donnez un exemple de noyau asymétrique (comme Sobel \(G_x\)) et montrez numériquement que les résultats diffèrent en l’appliquant au patch 5×5 du chapitre de deux manières.

  2. (15%) Considérez une image 5×5 avec des intensités concentrées entre les niveaux 3 et 5 (faible contraste, 3 bits). Appliquez manuellement l’algorithme d’égalisation de la Table 3.1, en remplissant toutes les colonnes du tableau (\(k\), \(h[k]\), \(p[k]\), \(\text{cdf}[k]\), \(\text{lut}[k]\)). Vérifiez le résultat avec mm::equalize.

  3. (15%) En utilisant mm::conv, appliquez le filtre moyenneur avec des noyaux de tailles 3×3, 9×9 et 21×21 à l’image du mandrill. Pour chaque version, calculez le PSNR (Peak Signal-to-Noise Ratio) par rapport à l’originale : \[\text{PSNR} = 10\log_{10}\!\left(\frac{255^2}{\text{MSE}}\right), \quad \text{MSE} = \frac{1}{MN}\sum_{i,j}(f-g)^2\] Tracez le PSNR en fonction de la taille du noyau et expliquez ce que la chute progressive indique sur la relation entre le lissage et la perte d’information.

  4. (15%) En utilisant add_salt_pepper avec une densité de 5%, appliquez et comparez : (a) mm::conv avec moyenne 3×3, (b) cv2.GaussianBlur avec \(\sigma=1\), (c) cv2.medianBlur avec fenêtre 3×3 et (d) cv2.medianBlur avec fenêtre 5×5. Affichez les images avec mm::show dans une grille 2×4 (ligne 1 : images, ligne 2 : histogrammes via mm::histImg). Expliquez pourquoi la médiane surpasse les filtres linéaires en utilisant l’argument de la Table 3.4..

  5. (15%) Implémentez mm::conv0 en utilisant uniquement des opérations NumPy vectorisées — sans boucles Python et sans cv2.filter2D — avec l’opérateur de stride tricks (np.lib.stride_tricks.sliding_window_view). Comparez le résultat et le temps d’exécution avec mm::conv0 (boucles) et mm::conv (cv2) pour des noyaux 3×3 et 15×15 sur l’image du mandrill.

  6. (15%) Appliquez le Unsharp Masking avec \(\sigma=1\) et \(k \in \{0.5, 1.0, 2.0, 4.0\}\) en utilisant la fonction usm du chapitre. Pour chaque valeur de \(k\) : (a) calculez la différence absolue \(|g - f|\), (b) affichez les images et les différences avec mm::show, et (c) tracez l’histogramme des différences avec mm::histImg. Identifiez à partir de quel \(k\) les artefacts (halos et amplification du bruit) deviennent visuellement inacceptables.

  7. (15%) Choisissez une image de radiographie ou de tomographie disponible publiquement (ex. : via mm::read à partir d’une URL) et concevez un pipeline de prétraitement avec au moins 4 étapes séquentielles, en justifiant chaque choix à partir des concepts du chapitre. Affichez avec mm::show dans une grille : l’image originale, chaque étape intermédiaire et le résultat final avec leurs histogrammes (mm::histImg).

Références du chapitre

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

  • Gonzalez (2018) pour les concepts d’opérations d’intensité, d’histogramme, de convolution et de filtrage spatial.
  • Szeliski (2022) pour la vision par ordinateur et les applications pratiques du filtrage.
  • Bradski (2008) pour l’implémentation pratique avec OpenCV et morph.py.

3.12 💻 Partie Pratique avec Exercices de Programmation

🎯 Objectif de ce cahier

Ce cahier permet de développer, valider, organiser et tester des solutions d’Exercices de Programmation (EPs) dans des environnements interactifs, tels que 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 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("EP03_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 la note automatiquement.

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

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

3.12.1 EP03_01 ➕ Addition Saturée de Constante

Dans les systèmes de surveillance vidéo, les caméras situées dans des environnements à éclairage variable produisent des images sous-exposées. Le réglage de la luminosité par addition saturée d’une constante est l’opération la plus simple pour une correction immédiate, étant appliquée en temps réel sur les puces des caméras embarquées et dans les pipelines de pré-traitement des robots mobiles.

Voir dans Figure 3.26 une simulation de ce EP.

3.12.1.1 📋 Directives d’Implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Constante : Lire l’entier \(k\) (valeur à ajouter).
  3. Données : Lire les valeurs entières de la matrice originale ligne par ligne.
  4. Mappage : Pour chaque pixel \(p\), calculer la nouvelle valeur selon l’équation :

\[p' = \text{clip}(p + k)\]

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

3.12.1.2 📌 Contraintes Computationnelles

  • Saturation (Clipping) : Les valeurs doivent être confinées à l’intervalle \([0, 255]\) : \[\text{clip}(x) = \max(0, \min(255, x))\]
  • Type : Le résultat final doit être entier (sans décimales).
  • \(k\) peut être négatif : les valeurs négatives assombrissent l’image ; les valeurs positives l’éclaircissent.

3.12.1.3 🧠 Fondement Théorique

Paramètre Type Impact Visuel
\(k > 0\) Entier Éclaircit l’image ; les pixels proches de 255 saturent en blanc
\(k < 0\) Entier Assombrit l’image ; les pixels proches de 0 saturent en noir
\(k = 0\) Entier Image inchangée

3.12.1.4 📦 Spécification d’Entrée et de Sortie (VPL)

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Entier \(k\).
  • Lignes suivantes : Éléments entiers de la matrice originale.

Sortie :

  • Matrice transformée en \(L\) lignes et \(C\) colonnes, valeurs entières séparées par des espaces.

3.12.1.5 📌 Exemples

Entrée Sortie Observation
2
3
50
0 100 200
210 240 255
50 150 250
255 255 255
Saturation à 255 sur les pixels élevés
1
4
-30
0 20 200 255
0 0 170 225 Saturation à 0 sur les pixels bas
➕ Simulateur EP03_01 : Addition saturée d'une constante p' = clip(p + k)

Ajustez la valeur de la constante k pour observer le décalage de luminosité de l'image et le troncature par saturation dans l'intervalle [0, 255].

0
Entrée originale (p)
Résultat transformé (p')
Formule appliquée : clip(p + (0))
Figure 3.26: Simulateur EP03_01 : Addition saturée de constante (p’ = clip(p + k))
%%writefile EP03_01.cpp
// your solution
Overwriting EP03_01.cpp
TestSuite("EP03_01.cpp").run()
✔️ EP03_01.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_01.cases

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

3.12.2 EP03_02 🔀 Fusion Alpha de Deux Images

En médecine nucléaire, des images de différentes modalités (tomodensitométrie et imagerie par résonance magnétique) sont fusionnées pour faciliter le diagnostic. Le mélange pondéré (alpha blending) est l’opération fondamentale de ce processus, permettant au radiologue de contrôler interactivement le poids de chaque modalité dans l’image affichée.

Voir dans Figure 3.27 une simulation de ce EP.

3.12.2.1 📋 Directives d’Implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Paramètre : Lire la valeur réelle \(\alpha \in [0, 1]\).
  3. Données : Lire les valeurs entières de la matrice \(f_1\) (image 1), puis celles de la matrice \(f_2\) (image 2).
  4. Mappage : Pour chaque position \((i, j)\), calculer :

\[g(i,j) = \text{clip}\left(\text{round}\left(\alpha \cdot f_1(i,j) + (1-\alpha) \cdot f_2(i,j)\right)\right)\]

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

3.12.2.2 📌 Contraintes de Calcul

  • Arrondi : Appliquer round avant la conversion en entier.
  • Saturation : Confiner à l’intervalle \([0, 255]\) avec \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Opération en virgule flottante : Effectuer l’opération en virgule flottante avant d’arrondir.

3.12.2.3 🧠 Fondement Théorique

Valeur de \(\alpha\) Résultat
\(\alpha = 1.0\) Uniquement \(f_1\)
\(\alpha = 0.5\) Moyenne arithmétique de \(f_1\) et \(f_2\)
\(\alpha = 0.0\) Uniquement \(f_2\)

3.12.2.4 📦 Spécification des Entrées et Sorties (VPL)

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Réel \(\alpha\).
  • Lignes suivantes : Éléments de \(f_1\) (\(L\) lignes avec \(C\) valeurs chacune).
  • Lignes suivantes : Éléments de \(f_2\) (\(L\) lignes avec \(C\) valeurs chacune).

Sortie :

  • Matrice résultante \(L \times C\).

3.12.2.5 📌 Exemples

Entrée Sortie Observation
1
3
0.5
0 100 200
100 200 50
50 150 125 Moyenne entre les deux images
1
3
1.0
10 20 30
90 80 70
10 20 30 Uniquement \(f_1\) (alpha=1)
🔀 Simulateur EP03_02 : Alpha Blending de Deux Images g = α·f1 + (1−α)·f2

Ajustez le paramètre de transparence α pour observer la combinaison linéaire pondérée pixel par pixel entre les images f1 et f2.

0.50
α = 0.00 → Seulement f2  |  α = 0.50 → Moyenne Pondérée Égale  |  α = 1.00 → Seulement f1
Image f1
Image f2
Résultat g
Formule : clip(round(0,50 · f1 + 0,50 · f2))
Figure 3.27: Simulateur EP03_02 : Mélange Alpha de Deux Images (g = α·f1 + (1−α)·f2)
%%writefile EP03_02.cpp
// your solution
Overwriting EP03_02.cpp
TestSuite("EP03_02.cpp").run()
✔️ EP03_02.cases existe déjà dans casos/
📋 6 cas chargé(s) depuis casos/EP03_02.cases

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

3.12.3 EP03_03 🎭 Inversion d’image (négatif photographique)

En radiologie, les images de rayons X sont traditionnellement visualisées en négatif : les os apparaissent en noir sur fond blanc. L’opération de négatif photographique est appliquée de manière courante dans les PACS (Picture Archiving and Communication Systems) pour faciliter la détection des fractures et des densités osseuses.

Voir dans Figure 3.28 une simulation de cet EP.

3.12.3.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Données : Lire les valeurs entières de la matrice originale.
  3. Mappage : Pour chaque pixel \(p\), calculer le négatif :

\[p' = 255 - p\]

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

3.12.3.2 📌 Contraintes informatiques

  • Aucun clipping nécessaire : Le résultat de \(255 - p\) avec \(p \in [0, 255]\) est toujours \(\in [0, 255]\).
  • Type entier : La sortie doit être des valeurs entières.
  • Équivalence logique : L’opération est identique au NOT bit à bit (mm::bnot) sur les images en 8 bits.

3.12.3.3 🧠 Fondements théoriques

Pixel original \(p\) Pixel négatif \(p'\) Observation
0 (noir) 255 (blanc) Inversion totale
128 (gris moyen) 127 (gris moyen) Valeur centrale
255 (blanc) 0 (noir) Inversion totale

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Lignes suivantes : Éléments entiers de la matrice originale.

Sortie :

  • Matrice négative en \(L\) lignes et \(C\) colonnes.

3.12.3.5 📌 Exemples

Entrée Sortie Observation
1
4
0 128 200 255
255 127 55 0 Inversion de chaque pixel
2
2
10 20
30 40
245 235
225 215
Matrice 2x2 inversée
🎭 Simulateur EP03_03 : Négatif photographique (inversion) p' = 255 − p

Observez l'inversion complémentaire d'intensité : les tons sombres deviennent clairs et les tons clairs deviennent sombres en soustrayant chaque pixel de la valeur maximale de 255.

Entrée originale (p)
Négatif (p' = 255 − p)
 
Formule appliquée : p' = 255 − p
Figure 3.28: Simulador EP03_03 : Inversion d’image — Négatif photographique (p’ = 255 − p)
%%writefile EP03_03.cpp
// your solution
Overwriting EP03_03.cpp
TestSuite("EP03_03.cpp").run()
✔️ EP03_03.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_03.cases

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

3.12.4 EP03_04 📊 Égalisation d’histogramme (L bits)

Dans les images satellites de télédétection, la variation de l’éclairage au cours de la journée produit des images à faible contraste. L’égalisation d’histogramme est appliquée automatiquement sur des satellites comme Landsat pour redistribuer les tons, révélant des détails de végétation, de relief et de zones urbaines invisibles dans l’image originale.

Voir dans Figure 3.29 une simulation de cet EP.

3.12.4.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes), \(C\) (colonnes) et \(B\) (nombre de bits, avec \(L_{\max} = 2^B\)).
  2. Données : Lire la matrice de pixels \(f\) avec des valeurs dans \([0, 2^B - 1]\).
  3. Histogramme : Calculer \(h[k]\) = nombre de pixels avec l’intensité \(k\), pour \(k = 0 \ldots 2^B-1\).
  4. Probabilité : \(p[k] = h[k] / (L \cdot C)\).
  5. CDF : \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\) ; fonction de distribution cumulée.
  6. LUT : \(\text{lut}[k] = \text{round}\left(\text{cdf}[k] \cdot (2^B - 1)\right)\) ; Look-Up Table (table de consultation).
  7. Application : \(g[i,j] = \text{lut}[f[i,j]]\).
  8. Sortie : Afficher la matrice égalisée \(L \times C\).

3.12.4.2 📌 Contraintes computationnelles

  • Arrondi : Utiliser l’arrondi mathématique (round) dans la LUT.
  • Bits : Le nombre de niveaux est \(2^B\) (ex. : \(B=3 \Rightarrow 8\) niveaux, \(B=8 \Rightarrow 256\) niveaux).
  • CDF cumulée : \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\), avec \(\text{cdf}[2^B-1] = 1.0\).

3.12.4.3 🧠 Fondement théorique

Étape Opération Formule
1 Histogramme \(h[k] \leftarrow\) nombre de pixels avec l’intensité \(k\)
2 Probabilité \(p[k] = h[k] / (L \cdot C)\)
3 CDF \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\)
4 LUT \(\text{lut}[k] = \text{round}(\text{cdf}[k] \cdot (2^B-1))\)
5 Application \(g[i,j] = \text{lut}[f[i,j]]\)

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Entier \(B\) (nombre de bits).
  • Lignes suivantes : Éléments entiers de la matrice.

Sortie :

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

3.12.4.5 📌 Exemples

Entrée Sortie Observation
5
5
3
3 4 2 3 4
4 3 3 4 3
2 3 4 3 2
3 4 3 2 3
4 3 2 3 4
5 7 1 5 7
7 5 5 7 5
1 5 7 5 1
5 7 5 1 5
7 5 1 5 7
Exemple 3 bits du chapitre
1
4
3
0 0 7 7
0 0 7 7 Histogramme bimodal extrême
📊 Simulateur EP03_04 : Égalisation d'histogramme lut[k] = round(cdf[k] · (L − 1))

Choisissez la profondeur de bits (B) et générez des images pour analyser l'étalement dynamique de l'histogramme et la table de remappage (LUT) en temps réel.

3 bits → 8 niveaux
1 bit (2 niveaux) 4 bits (16 niveaux) 8 bits (256 niveaux)
Entrée originale
Résultat égalisé
Histogramme original
Histogramme égalisé
LUT (Table de remappage k → v)
lut[k] = round(cdf[k] · 7) | B=3, niveaux=8
Figure 3.29: Simulateur EP03_04 : Égalisation d’histogramme (Niveaux L = 2^B)
%%writefile EP03_04.cpp
// your solution
Overwriting EP03_04.cpp
TestSuite("EP03_04.cpp").run()
✔️ EP03_04.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_04.cases

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

3.12.5 EP03_05 🔲 Application du masque binaire ET

Dans les systèmes d’inspection industrielle par vision par ordinateur, il est nécessaire d’isoler les régions d’intérêt (ROI) dans les images de pièces afin de vérifier les défauts de fabrication. L’opération ET bit à bit avec un masque binaire est le mécanisme fondamental pour découper exactement la zone d’inspection, en mettant à zéro tous les pixels en dehors de celle-ci.

Voir dans Figure 3.30 une simulation de cet EP.

3.12.5.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Données : Lire la matrice de pixels \(f\) (valeurs \(\in [0, 255]\)).
  3. Masque : Lire la matrice binaire \(m\) (valeurs : uniquement 0 ou 255).
  4. Mappage : Pour chaque pixel \((i,j)\), appliquer le ET bit à bit :

\[ g(i,j) = f(i,j) \;\text{ET}\; m(i,j) \]

où \(255 =\) 11111111 et \(0 =\) 00000000 en binaire.

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

3.12.5.2 📌 Contraintes de calcul

  • ET avec 255 : \(p \; \text{ET} \; 255 = p\) (tous les bits sont préservés).
  • ET avec 0 : \(p \; \text{ET} \; 0 = 0\) (tous les bits sont mis à zéro).
  • Masque : Les seules valeurs possibles dans le masque sont 0 et 255.
  • Implémentation : En Python, le ET bit à bit entre entiers utilise l’opérateur &.

3.12.5.3 🧠 Fondement théorique

Pixel \(f\) Masque \(m\) Résultat \(f\) ET \(m\)
n’importe quel \(v\) 255 (11111111) \(v\) (préservé)
n’importe quel \(v\) 0 (00000000) 0 (mis à zéro)

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Lignes suivantes : Éléments de \(f\) (\(L\) lignes).
  • Lignes suivantes : Éléments de \(m\) (\(L\) lignes avec valeurs 0 ou 255).

Sortie :

  • Matrice résultante \(L \times C\).

3.12.5.5 📌 Exemples

Entrée Sortie Observation
2
3
100 150 200
50 80 120
255 255 0
0 255 255
100 150 0
0 80 120
Le masque sélectionne la région
1
4
10 20 30 40
255 0 255 0
10 0 30 0 Alternance préservé/mis à zéro
⬛ Simulateur EP03_05 : Masque AND binaire g = f ET m

Cliquez sur les cellules du Masque m pour alterner entre passant (255) et bloquant (0), en appliquant l’opération logique pixel par pixel.

Image f (0–255)
Masque m (Cliquer pour Alterner)
Résultat g = f ET m
—
—conservés
—mis à zéro
—visible
Légende :
255
Passant (conservé)
0
Bloquant (mis à zéro)
g(i,j) = f(i,j) & m(i,j)
Figure 3.30: Simulateur EP03_05: Application de masque AND binaire
%%writefile EP03_05.cpp
// your solution
Overwriting EP03_05.cpp
TestSuite("EP03_05.cpp").run()
✔️ EP03_05.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_05.cases

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

3.12.6 EP03_06 🌫️ Filtre de moyenne avec kernel N×N

Dans les caméras de véhicules autonomes, les images capturées sous la pluie ou le brouillard présentent un bruit gaussien. Le filtre de moyenne est largement utilisé pour sa réduction en temps réel, étant implémenté directement dans le ISP (Image Signal Processor) des capteurs CMOS (Complementary Metal-Oxide-Semiconductor).

Les capteurs CMOS sont les capteurs d’image utilisés dans la plupart des caméras modernes (smartphones, webcams, caméras automobiles, etc.). Ils convertissent la lumière en signaux électriques, et l’ISP traite ces signaux en temps réel — en appliquant des opérations telles que la réduction du bruit, la balance des blancs et d’autres ajustements d’image.

Voir dans Figure 3.31 une simulation de cet EP.

3.12.6.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes), \(C\) (colonnes) et \(N\) (taille du kernel, toujours impair).
  2. Données : Lire la matrice de pixels \(f\).
  3. Filtre de moyenne : Pour chaque pixel \((i,j)\) interne (sans bordures), calculer :

\[g(i,j) = \text{round}\left(\frac{1}{N^2} \sum_{s=-(r)}^{r} \sum_{t=-(r)}^{r} f(i+s,\, j+t)\right), \quad r = \lfloor N/2 \rfloor\]

  1. Traitement des bords : Les pixels sur le bord (où la fenêtre \(N \times N\) dépasse les limites) doivent être copiés directement depuis l’original sans modification.
  2. Sortie : Afficher la matrice résultante \(L \times C\).

3.12.6.2 📌 Contraintes de calcul

  • Rayon : \(r = \lfloor N/2 \rfloor\) (moitié du kernel, entier).
  • Pixels internes : \((i,j)\) avec \(r \le i < L-r\) et \(r \le j < C-r\).
  • Arrondi : Utiliser l’arrondi mathématique avant de convertir en entier.
  • Sans clipping : La moyenne des valeurs \(\in [0,255]\) reste dans \([0,255]\).

3.12.6.3 🧠 Fondements théoriques

Taille \(N\) Coefficient Pixels dans la fenêtre Effet
3 \(1/9 \approx 0.111\) 9 Lisse
5 \(1/25 = 0.04\) 25 Moyen
7 \(1/49 \approx 0.020\) 49 Fort

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Entier \(N\) (impair, \(N \ge 3\)).
  • Lignes suivantes : Éléments de la matrice originale.

Sortie :

  • Matrice filtrée \(L \times C\).

3.12.6.5 📌 Exemples

Entrée Sortie Observation
3
3
3
10 20 30
40 50 60
70 80 90
10 20 30
40 50 60
70 80 90
Bordure uniquement (3×3 = bordure totale)
5
5
3
0 0 0 0 0
0 0 0 0 0
0 0 100 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 11 11 11 0
0 11 11 11 0
0 11 11 11 0
0 0 0 0 0
Pixel isolé : tous les 9 pixels internes dont la fenêtre 3×3 inclut la valeur 100 reçoivent round(100/9)=11
🔲 Simulateur EP03_06 : Filtre Moyenne avec Noyau N×N g = Moyenne(Voisins)

Sélectionnez la taille du noyau et survolez les pixels du résultat pour inspecter le voisinage et le calcul de la moyenne arithmétique.

Taille du noyau :
Image originale f (7×7) Avec bruit sel et poivre
Résultat g (Filtre Lissé) Survolez pour inspecter
Légende :
Fenêtre du Noyau
Bord (Copié)
Pixel Inspecté
Survolez un pixel interne du résultat pour voir le calcul de la moyenne.
Figure 3.31: Simulateur EP03_06: Filtre de Moyenne avec Noyau N×N
%%writefile EP03_06.cpp
// your solution
Overwriting EP03_06.cpp
TestSuite("EP03_06.cpp").run()
✔️ EP03_06.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_06.cases

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

3.12.7 EP03_07 🔍 Opérateur Laplacien (w4) pour le Rehaussement des Contours

Dans les tomographies à haute résolution, la netteté des contours entre les tissus est critique pour le diagnostic. L’opérateur Laplacien est largement utilisé dans les pipelines de prétraitement d’images médicales pour rehausser automatiquement les contours anatomiques avant la segmentation, évitant ainsi une intervention manuelle du radiologiste.

Voir dans Figure 3.32 une simulation de cet EP.

3.12.7.1 📋 Directives d’Implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Données : Lire la matrice de pixels \(f\).
  3. Laplacien (w4) : Pour chaque pixel interne \((i,j)\) avec \(1 \le i < L-1\), \(1 \le j < C-1\), calculer :

\[\nabla^2 f(i,j) = f(i-1,j) + f(i+1,j) + f(i,j-1) + f(i,j+1) - 4 \cdot f(i,j)\]

  1. Rehaussement : Calculer l’image rehaussée :

\[g(i,j) = \text{clip}(f(i,j) - \nabla^2 f(i,j))\]

  1. Bordure : Les pixels en bordure sont copiés directement : \(g(i,j) = f(i,j)\).
  2. Sortie : Afficher la matrice rehaussée \(L \times C\).

3.12.7.2 📌 Contraintes Computationnelles

  • Noyau w4 : \(\begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}\) — uniquement 4-voisins.
  • Saturation : \(\text{clip}(x) = \max(0, \min(255, x))\) appliqué au résultat du rehaussement.
  • Sans arrondi : Le Laplacien n’utilise que des additions/soustractions d’entiers.

3.12.7.3 🧠 Fondement Théorique

Région \(\nabla^2 f\) Effet du Rehaussement
Uniforme \(\approx 0\) Aucune modification
Contour croissant \(< 0\) Pixel éclairci
Contour décroissant \(> 0\) Pixel assombri

3.12.7.4 📦 Spécification d’Entrée et de Sortie (VPL)

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Lignes suivantes : Éléments de la matrice originale.

Sortie :

  • Matrice rehaussée \(L \times C\).

3.12.7.5 📌 Exemples

Entrée Sortie Observation
3
3
0 0 0
0 100 0
0 0 0
0 0 0
0 255 0
0 0 0
Pic isolé : lap=−400, g=100−(−400)=500 → clip=255
3
3
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
Région uniforme : Laplacien=0, aucune modification
📐 Simulateur EP03_07 : Opérateur Laplacien (w4) g = f ∓ ∇²f

Sélectionnez la variante de rehaussement et survolez les pixels internes du résultat pour inspecter le voisinage de 4 points et l'équation du Laplacien.

Variante :
① Image originale f Palier avec léger bruit
② Laplacien ∇²f Bords détectés (décalage ±128)
③ Résultat g = f − ∇²f Survolez pour inspecter
Noyau w4 (4-voisins)
0
+1
0
+1
−4
+1
0
+1
0
∇²f = T + B + L + R − 4·f
Légende :
4-voisins du noyau
Pixel central
Bord (copié)
Survolez un pixel interne du résultat pour détailler l'équation.
Figure 3.32: Simulateur EP03_07 : Opérateur Laplacien (w4) pour le rehaussement des contours
%%writefile EP03_07.cpp
// your solution
Overwriting EP03_07.cpp
TestSuite("EP03_07.cpp").run()
✔️ EP03_07.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_07.cases

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

3.12.8 EP03_08 🧭 Gradient de Sobel : Gx et Gy

Dans les robots explorateurs de Mars (comme Perseverance), la détection d’obstacles est réalisée en temps réel par des caméras stéréoscopiques. L’opérateur de Sobel calcule le gradient directionnel de la scène et est utilisé dans l’algorithme de détection de contours pour identifier les roches, les fissures et les dénivelés du terrain qui pourraient compromettre la navigation.

Voir dans Figure 3.33 une simulation de cet EP.

3.12.8.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Données : Lire la matrice \(f\).
  3. Gx et Gy : Pour chaque pixel interne \((i,j)\) avec \(1 \le i < L-1\), \(1 \le j < C-1\) :

\[G_x(i,j) = [f(i-1,j+1) + 2f(i,j+1) + f(i+1,j+1)] - [f(i-1,j-1) + 2f(i,j-1) + f(i+1,j-1)]\]

\[G_y(i,j) = [f(i+1,j-1) + 2f(i+1,j) + f(i+1,j+1)] - [f(i-1,j-1) + 2f(i-1,j) + f(i-1,j+1)]\]

  1. Magnitude : \(|\nabla f(i,j)| = \text{clip}(\text{round}(\sqrt{G_x^2 + G_y^2}))\).
  2. Bordure : Les pixels de bordure reçoivent une magnitude de 0.
  3. Sortie : Afficher la magnitude \(L \times C\).

3.12.8.2 📌 Contraintes computationnelles

  • Arrondi : Appliquer round avant de convertir en entier.
  • Saturation : \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Racine carrée : Utiliser \(\sqrt{G_x^2 + G_y^2}\) (pas l’approximation \(|G_x| + |G_y|\)).

3.12.8.3 🧠 Fondement théorique

Opérateur Détecte Coefficients diagonaux
\(G_x\) Contours verticaux \(\pm 1\)
\(G_y\) Contours horizontaux \(\pm 1\)
\(|\nabla f|\) Tous les contours Combiné

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Lignes suivantes : Éléments de la matrice.

Sortie :

  • Magnitude du gradient, matrice \(L \times C\).

3.12.8.5 📌 Exemples

Entrée Sortie Observation
3
3
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
Image nulle : gradient zéro
3
3
0 0 255
0 0 255
0 0 255
0 0 0
0 255 0
0 0 0
Contour vertical central : Gx élevé
🧭 Simulateur EP03_08 : Gradient de Sobel (Gx et Gy) |∇f| = √(Gx² + Gy²)

Analysez la décomposition horizontale (Gx) et verticale (Gy) de l'opérateur de Sobel et survolez les pixels de la magnitude pour inspecter le voisinage 3×3.

Noyaux de Sobel :
−1
0
+1
−2
0
+2
−1
0
+1
Gx
−1
−2
−1
0
0
0
+1
+2
+1
Gy
Image originale f Matrice 5×5 pixels
Magnitude |∇f| √(Gx² + Gy²)
Gx — Gradient horizontal Bleu = Négatif · Blanc = Zéro · Bleu vif = Positif
Gy — Gradient vertical Ambre = Négatif · Blanc = Zéro · Ambre vif = Positif
Légende :
Voisinage 3×3 inspecté
Pixel central
Bord (forcé à 0)
Survolez un pixel interne de la magnitude pour voir la décomposition Gx et Gy.
Figure 3.33: Simulateur EP03_08 : Gradient de Sobel (Gx et Gy)
%%writefile EP03_08.cpp
// your solution
Overwriting EP03_08.cpp
TestSuite("EP03_08.cpp").run()
✔️ EP03_08.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_08.cases

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

3.12.9 EP03_09 📡 Filtre Médian 3×3

Les images radar à synthèse d’ouverture (SAR) utilisées en surveillance environnementale et militaire souffrent d’un type spécifique de bruit appelé speckle, qui présente des caractéristiques similaires au bruit sel et poivre. Le filtre médian est la méthode standard pour supprimer ce bruit, car il préserve les contours des structures tout en éliminant les points parasites.

Voir dans Figure 3.34 une simulation de cet EP.

3.12.9.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Données : Lire la matrice de pixels \(f\).
  3. Filtre Médian 3×3 : Pour chaque pixel interne \((i,j)\) avec \(1 \le i < L-1\), \(1 \le j < C-1\) :
    • Collecter les 9 pixels du voisinage \(3 \times 3\) : \(\{f(i+s, j+t) : s,t \in \{-1,0,1\}\}\).
    • Trier les 9 valeurs en ordre croissant.
    • Affecter \(g(i,j)\) à la valeur centrale (position d’indice 4, en considérant l’indice 0).

\[g(i,j) = \text{médiane}\{f(i+s, j+t) : s,t \in \{-1,0,1\}\}\]

  1. Bordure : Copier directement : \(g(i,j) = f(i,j)\).
  2. Sortie : Afficher la matrice filtrée \(L \times C\).

3.12.9.2 📌 Contraintes computationnelles

  • Fenêtre : Toujours \(3 \times 3 = 9\) éléments.
  • Médiane : L’élément central de la séquence triée (indice 4 de 0 à 8).
  • Pas de clipping : La médiane des valeurs dans \([0, 255]\) reste dans \([0, 255]\).
  • Non linéaire : Le filtre médian ne peut pas être exprimé comme une convolution linéaire.

3.12.9.3 🧠 Fondement théorique

Bruit Filtre de moyenne Filtre médian
Sel et poivre (0 ou 255) Étale le bruit Supprime sans déformer les contours
Gaussien Réduit efficacement Réduit partiellement

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Lignes suivantes : Éléments de la matrice.

Sortie :

  • Matrice filtrée \(L \times C\).

3.12.9.5 📌 Exemples

Entrée Sortie Observation
3
3
100 100 100
100 0 100
100 100 100
100 100 100
100 100 100
100 100 100
Point noir éliminé : médiane de 8×100+1×0 = 100
3
3
50 50 50
50 255 50
50 50 50
50 50 50
50 50 50
50 50 50
Point blanc (sel) éliminé
📉 Simulateur EP03_09 : Filtre Médian 3×3 g = Médiane(Voisins)

Injectez du bruit impulsionnel (sel et poivre) et survolez les pixels internes du résultat pour inspecter le tri du vecteur de voisinage et l'élimination du bruit.

Bruit sel (255) et poivre (0) — ~30 % des pixels internes affectés
Image f — Avec Bruit Sel (255) et poivre (0) visibles
Résultat g — Sans Bruit Survolez pour inspecter
Vecteur de Voisinage 3×3 — Trié Survolez un pixel interne du résultat pour visualiser
—
Légende :
Fenêtre 3×3
Pixel Central
Bord (Copié)
Médiane
Bruit (Éliminé)
Survolez un pixel interne du résultat pour voir le processus de tri.
Figure 3.34: Simulateur EP03_09 : Filtre de la Médiane 3×3
%%writefile EP03_09.cpp
// your solution
Overwriting EP03_09.cpp
TestSuite("EP03_09.cpp").run()
✔️ EP03_09.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_09.cases

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

3.12.10 EP03_10 ✨ Unsharp Masking (USM)

Dans les systèmes de numérisation de documents historiques et d’œuvres d’art, la netteté des images est essentielle pour la lecture de textes manuscrits et de détails ornementaux. L’Unsharp Masking (USM) est l’algorithme de rehaussement de netteté standard utilisé dans les scanners professionnels et les logiciels tels qu’Adobe Photoshop, contrôlé par le paramètre \(k\) qui détermine l’intensité du rehaussement.

Voir dans Figure 3.35 une simulation de cet EP.

3.12.10.1 📋 Directives d’implémentation

  1. Dimensions : Lire les entiers \(L\) (lignes) et \(C\) (colonnes).
  2. Paramètre : Lire la valeur réelle \(k\) (intensité du rehaussement, \(k \ge 0\)).
  3. Données : Lire la matrice de pixels \(f\).
  4. Lissage : Calculer \(\bar{f}\) avec un filtre de moyenne \(3\times3\) (uniquement les pixels internes ; les bords sont conservés) :

\[\bar{f}(i,j) = \frac{1}{9} \sum_{s=-1}^{1} \sum_{t=-1}^{1} f(i+s, j+t)\]

  1. Masque haute fréquence : \(m(i,j) = f(i,j) - \bar{f}(i,j)\).
  2. Rehaussement USM : Pour chaque pixel interne :

\[g(i,j) = \text{clip}\left(\text{round}\left(f(i,j) + k \cdot m(i,j)\right)\right)\]

  1. Bord : \(g(i,j) = f(i,j)\) (copie directe).
  2. Sortie : Afficher la matrice rehaussée \(L \times C\).

3.12.10.2 📌 Contraintes computationnelles

  • Arrondi : Appliquer round avant le clipping.
  • Saturation : \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Opérations en float : Calculer \(\bar{f}\) et \(m\) en virgule flottante avant d’arrondir le résultat final.
  • \(k = 0\) : Pas de rehaussement — la sortie est identique à l’entrée (sauf pour les bords).

3.12.10.3 🧠 Fondement théorique

Étape Opération Description
1 \(\bar{f} = f * \frac{1}{9}\mathbf{1}_{3\times3}\) Lissage (basses fréquences)
2 \(m = f - \bar{f}\) Masque (hautes fréquences)
3 \(g = \text{clip}(\text{round}(f + k \cdot m))\) Rehaussement pondéré

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

Entrée :

  • Ligne 1 : Entier \(L\).
  • Ligne 2 : Entier \(C\).
  • Ligne 3 : Réel \(k\).
  • Lignes suivantes : Éléments de la matrice originale.

Sortie :

  • Matrice rehaussée \(L \times C\).

3.12.10.5 📌 Exemples

Entrée Sortie Observation
3
3
0.0
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
k=0 : pas de rehaussement
3
3
1.0
50 50 50
50 200 50
50 50 50
50 50 50
50 255 50
50 50 50
k=1 : pixel central rehaussé et saturé
✨ Simulateur EP03_10 : Unsharp Masking (USM) g = f + k · m

Réglez le facteur de gain k, observez le pipeline complet de rehaussement (floutage, masque haute fréquence) et survolez le résultat.

Facteur de gain k : k = 1.0
① Image originale f Matrice 5×5 pixels
② Flouté f̄ Moyenne 3 × 3
③ Masque m m = f − f̄ (hautes fréquences)
④ Résultat g = f + 1.0·m Survolez pour inspecter
Légende :
Voisinage 3×3
Pixel central
Bord (copié)
Masque positif/négatif
Survolez un pixel interne du résultat pour tracer le pipeline complet.
Figure 3.35: Simulateur EP03_10 : Unsharp Masking (USM)
%%writefile EP03_10.cpp
// your solution
Overwriting EP03_10.cpp
TestSuite("EP03_10.cpp").run()
✔️ EP03_10.cases existe déjà dans casos/
📋 5 cas chargé(s) depuis casos/EP03_10.cases

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