Saltar a contenido

Clase 14: Preprocesamiento de Imágenes (Assignment)

Ingeniería de Datos — Universidad Católica del Uruguay

Objetivo: Implementar un pipeline de preprocesamiento de imágenes: representación, espacios de color, contraste (global vs adaptativo), suavizado y detección/descripción de features con visualización y métricas.

Tiempo estimado: 120–150 minutos


📚 Lecturas mínimas (recuerdo)

  • Fundamentos de espacios de color: RGB, HSV/HSL y CIE Lab*.
  • Contraste: ecualización de histograma vs CLAHE (adaptativa).
  • Esquinas y puntos de interés: Harris, Shi–Tomasi; detectores/desciptores tipo ORB/SIFT.
  • Métricas y diagnóstico: histogramas, gradientes y repetibilidad de features.

Setup y Carga de Datos

Instalación rápida

!pip install -q opencv-python opencv-contrib-python numpy matplotlib scikit-image pandas

import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from skimage import exposure, filters, feature, color, img_as_float
import skimage
import pathlib, warnings, platform, os
warnings.filterwarnings('ignore')

print("✅ Librerías listas")
print(f"OpenCV: {cv2.__version__}")
print(f"scikit-image: {skimage.__version__}")
print(f"Python: {platform.python_version()}")

0) Dataset de ejemplo (descarga automática)

Opción A (rápida, 10–15 imágenes clásicas): usa imágenes de scikit-image.

from skimage import data as skdata, io as skio

RAW_DIR = pathlib.Path("data/raw")
RAW_DIR.mkdir(parents=True, exist_ok=True)

samples_sk = {
    "camera.png": skdata.camera(),
    "astronaut.png": skdata.astronaut(),
    "coffee.png": skdata.coffee(),
    "coins.png": skdata.coins(),
    "checkerboard.png": skdata.checkerboard(),
    "rocket.png": skdata.rocket(),
    "page.png": skdata.page()
}

for name, img in samples_sk.items():
    out = RAW_DIR / name
    skio.imsave(out.as_posix(), img)

print("✅ Pack mínimo descargado en", RAW_DIR)

1) Directorios y listado de imágenes

DATA_DIR = pathlib.Path("data/raw")
SAMPLES_DIR = pathlib.Path("data/samples")
OUTPUTS = {
    "preproc": pathlib.Path("outputs/preproc"),
    "features": pathlib.Path("outputs/features"),
    "metrics": pathlib.Path("outputs/metrics"),
}
for p in OUTPUTS.values():
    p.mkdir(parents=True, exist_ok=True)

images = sorted([p for p in DATA_DIR.glob("**/*")
                 if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"}])
print("Imágenes encontradas:", len(images))
images[:5]

Parte A — Representación e inspección inicial

A.1 Matriz y formato

from pathlib import Path

def read_image_bgr(path: Path):
    img_bgr = cv2.imread(str(path), cv2.IMREAD_COLOR)
    assert img_bgr is not None, f"No se pudo leer: {path}"
    return img_bgr

img_path = images[0]
img_bgr = read_image_bgr(img_path)
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

height, width = img_gray.shape[:2]
channels = img_rgb.shape[2] if img_rgb.ndim == 3 else 1
dtype = img_bgr.dtype
min_val, max_val = int(img_gray.min()), int(img_gray.max())
mean_gray = float(img_gray.mean())

print("H, W, C:", height, width, channels)
print("dtype:", dtype, "rango:", (min_val, max_val), "mean_gray:", round(mean_gray, 2))

A.2 Histogramas (diagnóstico)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(img_rgb); axes[0].set_title("Original (RGB)"); axes[0].axis("off")

axes[1].hist(img_gray.ravel(), bins=256, range=(0, 255), color="gray", alpha=0.9)
axes[1].set_title("Histograma de intensidades (grises)")
plt.tight_layout(); plt.show()

# Si es color: histogramas por canal
colors = ("R", "G", "B")
plt.figure(figsize=(6,4))
for i, c in enumerate(colors):
    plt.hist(img_rgb[..., i].ravel(), bins=256, range=(0, 255), alpha=0.5, label=c)
plt.legend(); plt.title("Histogramas por canal (RGB)"); plt.tight_layout(); plt.show()

📝 Preguntas de reflexión — Parte A (completa los espacios)

1) El rango dinámico observado fue de _ a . Esto sugiere __.

2) El histograma indica (bajo/alto) contraste porque _____.

3) En color, el canal con mayor dominancia fue _; implicancia: ___.

Pistas: - Rango típico en 8‑bit: 0–255; bajo contraste = histograma estrecho alrededor de la media. - Dominancia de canal: tintes de color (p. ej., rojo dominante en interiores cálidos).


Parte B — Espacios de color y contraste

B.1 Cambio de espacio

img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
img_lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)

salient_hsv_channel = "____"  # "H" | "S" | "V"
salient_lab_channel = "____"  # "L" | "A" | "B"
print("Canales relevantes:", salient_hsv_channel, salient_lab_channel)

Pista: H (tono) es útil para segmentación por color; L* (luminancia) para contraste.

B.2 Contraste: Global vs Adaptativo (CLAHE)

# Global en grises
eq_gray = cv2.equalizeHist(img_gray)

# CLAHE en L* (LAB)
L, A, B = cv2.split(img_lab)
clahe = cv2.createCLAHE(clipLimit=____, tileGridSize=(____, ____))
L_clahe = clahe.apply(L)
lab_clahe = cv2.merge([L_clahe, A, B])
rgb_clahe = cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2RGB)

def contrast_std(x: np.ndarray) -> float:
    return float(np.std(x.astype(np.float32)))

std_before = contrast_std(img_gray)
std_eq = contrast_std(eq_gray)
std_clahe = contrast_std(cv2.cvtColor(rgb_clahe, cv2.COLOR_RGB2GRAY))
print("STD contraste — before/eq/clahe:", round(std_before,2), round(std_eq,2), round(std_clahe,2))

fig, axes = plt.subplots(1, 3, figsize=(12,4))
axes[0].imshow(img_rgb); axes[0].set_title("Original"); axes[0].axis("off")
axes[1].imshow(eq_gray, cmap="gray"); axes[1].set_title("Equalize (global)"); axes[1].axis("off")
axes[2].imshow(rgb_clahe); axes[2].set_title("CLAHE en L*"); axes[2].axis("off")
plt.tight_layout(); plt.show()

Pistas:

  • clipLimit: 1.5–4.0 (comienza con 2.0 o 3.0). Más alto = más contraste pero riesgo de ruido.
  • tileGridSize: (8, 8) por defecto; prueba 4–16 según tamaño/estructura de la imagen.

📝 Preguntas de reflexión — Parte B (completa los espacios)

1) El canal más informativo (HSV/LAB) fue _ porque ___.

2) CLAHE mejoró (más/menos) que la ecualización global en zonas homogéneas porque _____.

3) El cambio en la desviación estándar sugiere _____ sobre el contraste global.

Pistas:

  • H en HSV → segmentación por tono; L* en LAB → ajustes de luminancia y contraste.
  • CLAHE limita saturación local y evita artefactos en áreas planas mejor que la global.

Parte C — Suavizado y bordes

C.1 Suavizado

gaussian = cv2.GaussianBlur(img_gray, ksize=(____, ____), sigmaX=____)
bilateral = cv2.bilateralFilter(img_gray, d=____, sigmaColor=____, sigmaSpace=____)

def grad_variance(x: np.ndarray) -> float:
    gx = cv2.Sobel(x, cv2.CV_32F, 1, 0, ksize=3)
    gy = cv2.Sobel(x, cv2.CV_32F, 0, 1, ksize=3)
    g = np.hypot(gx, gy)
    return float(np.var(g))

gv_before = grad_variance(img_gray)
gv_gauss = grad_variance(gaussian)
gv_bilat = grad_variance(bilateral)
print("Var(grad) — before/gauss/bilateral:", round(gv_before,2), round(gv_gauss,2), round(gv_bilat,2))

Pistas:

  • Gaussian: ksize=(3,3)|(5,5)|(7,7), sigmaX≈1.0–2.0.
  • Bilateral: d≈9, sigmaColor≈75, sigmaSpace≈75 preserva bordes mejor que gaussian.

C.2 Bordes

edges_before = cv2.Canny(img_gray, threshold1=____, threshold2=____)
edges_gauss = cv2.Canny(gaussian, threshold1=____, threshold2=____)
edges_bilat = cv2.Canny(bilateral, threshold1=____, threshold2=____)

def edge_ratio(x: np.ndarray) -> float:
    return float((x > 0).mean())

er_before = edge_ratio(edges_before)
er_gauss = edge_ratio(edges_gauss)
er_bilat = edge_ratio(edges_bilat)
print("Edges ratio — before/gauss/bilateral:", round(er_before,3), round(er_gauss,3), round(er_bilat,3))

fig, axes = plt.subplots(1, 4, figsize=(14,4))
axes[0].imshow(img_gray, cmap="gray"); axes[0].set_title("Original (gray)"); axes[0].axis("off")
axes[1].imshow(gaussian, cmap="gray"); axes[1].set_title("Gaussian"); axes[1].axis("off")
axes[2].imshow(bilateral, cmap="gray"); axes[2].set_title("Bilateral"); axes[2].axis("off")
axes[3].imshow(edges_bilat, cmap="gray"); axes[3].set_title("Bordes (Canny)"); axes[3].axis("off")
plt.tight_layout(); plt.show()

Pistas:

  • Canny inicial: (50,150) o (100,200); baja iluminación → baja los umbrales.
  • Usa suavizado previo para reducir bordes falsos por ruido.

📝 Preguntas de reflexión — Parte C (completa los espacios)

1) El suavizado que mejor conservó bordes fue _ porque ___.

2) El ratio de bordes sugiere (ruido/detalle) en la variante _____.

3) Cambiarías los thresholds de Canny a (_, ) para escenas nocturnas porque __.

Pistas:

  • Edge‑preserving (bilateral) mantiene contornos finos; gaussian tiende a “lavar” bordes.
  • Umbrales menores en Canny detectan más bordes pero más ruido.

Parte D — Puntos de interés y descriptores

Objetivo: evidenciar cómo el preprocesamiento afecta cantidad y calidad de features locales.

D.1 Detección en variantes

variants = {
    "orig": img_gray,
    "gauss": gaussian,
    "claheL": cv2.cvtColor(rgb_clahe, cv2.COLOR_RGB2GRAY)
}

# ORB (rápido, binario)
orb = cv2.ORB_create(nfeatures=____, scaleFactor=____, nlevels=____)

kp_stats = []
overlay_examples = []
for name, img in variants.items():
    kp, des = orb.detectAndCompute(img, None)
    kp_stats.append({"variant": name, "num_keypoints": len(kp), "descriptor_size": 0 if des is None else des.shape[1]})
    out = cv2.drawKeypoints(img, kp, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS, color=(0,255,0))
    overlay_examples.append((name, out))

pd.DataFrame(kp_stats)
fig, axes = plt.subplots(1, len(overlay_examples), figsize=(14,4))
for ax, (name, out) in zip(axes, overlay_examples):
    ax.imshow(out, cmap="gray"); ax.set_title(name); ax.axis("off")
plt.tight_layout(); plt.show()

Pista: alternativamente cv2.goodFeaturesToTrack para esquinas (Shi–Tomasi) o cv2.SIFT_create si disponible.

Pistas:

  • ORB: nfeatures=500–1500, scaleFactor≈1.2, nlevels≈8. Más features ⇒ más costo.

D.2 Matching y repetibilidad (A vs A’)

# Emparejar orig vs claheL con ORB
kp1, des1 = orb.detectAndCompute(variants["orig"], None)
kp2, des2 = orb.detectAndCompute(variants["claheL"], None)

bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2) if (des1 is not None and des2 is not None) else []
matches = sorted(matches, key=lambda m: m.distance)

matches_ratio = (len(matches) / max(1, min(len(kp1), len(kp2)))) if (len(kp1) and len(kp2)) else 0.0
print("#kp_orig:", len(kp1), "#kp_claheL:", len(kp2), "matches:", len(matches), "ratio:", round(matches_ratio,2))

img_match = cv2.drawMatches(variants["orig"], kp1, variants["claheL"], kp2, matches[:____], None, flags=2)
plt.figure(figsize=(12,5)); plt.imshow(img_match, cmap="gray"); plt.axis("off"); plt.show()

Pistas:

  • Muestra 30–50 matches para inspección visual; usa crossCheck=True para mayor precisión.
  • matches_ratio cercano a 1 indica alta repetibilidad entre variantes.

📝 Preguntas de reflexión — Parte D (completa los espacios)

1) La variante con mayor densidad de keypoints fue _; motivo probable: ___.

2) La repetibilidad (matches_ratio) aumentó/disminuyó con _ porque ___.

3) Cambiarías parámetros de ORB (nfeatures, scaleFactor) a (_, ___) para equilibrar calidad/tiempo.

Pistas:

  • Más contraste local (CLAHE) suele aumentar keypoints; blur reduce detecciones finas.
  • nfeatures ↑ ⇒ más puntos, scaleFactor ↓ ⇒ más niveles efectivos pero más costo.

Preguntas de reflexión finales (completa los espacios)

1) La transformación más útil para tu dataset fue _ porque mejoró sin introducir __.

2) El canal más informativo (HSV/LAB) fue _; lo usarías para ___.

3) El trade‑off más claro entre suavizado y features fue _; criterio de selección: ___.

4) Checks automáticos que propondrías: umbrales para num_keypoints < _, edges_ratio ∉ [, __], alerta por caída de contraste STD < _____.

Pistas:

  • Ejemplos de checks: num_keypoints < 100, edges_ratio ∉ [0.02, 0.15], STD < 20 (ajusta a tu dataset).
  • Documenta parámetros por lote y compara contra baseline de referencia.

Tareas extra (opcional)

1) Curva sensibilidad‑ruido: barrer parámetros de CLAHE/suavizado y graficar num_keypoints vs proxy de ruido (falsos bordes).

2) Benchmark mini‑matching: comparar descriptor flotante vs binario (p. ej., SIFT vs ORB) en tus datos (tiempos relativos + matches válidos).

3) Dashboard QA: KPIs por lote (conteo de features, contraste medio, % bordes, repetibilidad) y alertas.


Pistas y referencias (opcionales)

  • OpenCV: Feature2D/ORB, Harris/Shi–Tomasi, SIFT, Canny, CLAHE, conversiones de color (docs oficiales).
  • scikit‑image: exposure.equalize_adapthist, filtros, transformaciones.
  • Buenas prácticas: registrar parámetros, fijar semillas, guardar variantes y métricas por imagen.