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≈75preserva 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=Truepara mayor precisión. matches_ratiocercano 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.