Metodología

El proyecto contado como CRISP-DM

Arquitectura explica cómo funciona el sistema hoy, paso a paso, cuando llega una foto. Esta página cuenta lo mismo desde otro ángulo: el proceso que siguió el proyecto para llegar hasta ahí, usando las 6 fases del marco CRISP-DM (Cross-Industry Standard Process for Data Mining) — el esquema estándar para proyectos de ciencia de datos desde los años 90, todavía el más usado en la industria. No es lineal en la práctica: hay vueltas atrás constantes entre fases (de "Evaluación" a "Preparación de datos", sobre todo), pero sirve como mapa.

Fase 1

Comprensión del negocio

El objetivo no es "identificar el hueso exacto" ni sustituir a un osteólogo — es dar una primera clasificación rápida por grupo morfológico (cráneo, hueso largo, vértebra...) a partir de una foto, útil para laboratorio, docencia o una criba inicial antes de la revisión experta. De ahí sale la decisión clave de todo el proyecto: la taxonomía son 9 grupos de forma, no 21 huesos exactos ni especies — con 1-9 especímenes físicos por hueso, pedir más granularidad hace que el modelo memorice objetos concretos en vez de aprender formas (comprobado con especímenes completos fuera de validación, no solo fotos sueltas).

Restricción que viene de aquí y condiciona todo lo demás: no hay presupuesto para fotografiar miles de huesos reales. Esa única restricción de negocio es la que obliga a generar datos sintéticos a partir de modelos 3D (fase 2) y a resolver después el problema sim2real (fases 3-5).

Fase 2

Comprensión de los datos

No existe un data/raw/<clase>/ con miles de fotos. Lo que hay es un catálogo de mallas 3D verificadas a mano, varias recetas de render con distinto nivel de realismo, y un puñado de fotos reales curadas. Ver la carpeta de cada fuente, imagen por imagen, es el primer paso para entender qué se le está dando de comer al modelo — exactamente lo que se ve abajo.

Las carpetas de datos, de verdad

Mismo espécimen (fémur de gorila) a través de las tres recetas sintéticas, más una foto real de otro grupo para comparar contra el objetivo final. Clic para ampliar.

Fémur de gorila, receta renders_lab: escena de mesa con física y material multicapa

renders_lab/ — escena de laboratorio: el hueso cae físicamente sobre una mesa, HDRI real, material multicapa de 6 capas (bone_material.py). La receta más realista de las tres, y la base del dataset de producción.

Fémur de gorila, receta noisy_composited: silueta pegada sobre fondo real

renders_noisy_composited/ — silueta sin material realista, pegada sobre una foto de fondo real. Pose de órbita fija, no física — distinta a la de renders_lab para el mismo objeto.

Fémur de gorila, receta noisy_bproc: BlenderProc con HDRI y suciedad procedural

renders_noisy_bproc/ — BlenderProc: HDRI real, suciedad procedural, máscara ground-truth exacta. Otra órbita fija distinta, material más simple que renders_lab.

Foto real curada de una vértebra

Fotos entrenamiento/_curated/ — hueso real fotografiado y revisado a mano. Pocas, pero son el dominio que de verdad importa: el examen final siempre se hace aquí, nunca solo contra sintético.

Un ejemplar de cada grupo morfológico (renders_lab)

Los 9 grupos que aprende el modelo, cada uno con su imagen de ejemplo real del dataset de producción.

Ejemplo de cráneo

cráneo

Ejemplo de mandíbula/maxilar

mandíbula / maxilar

Ejemplo de hueso largo

hueso largo

Ejemplo de hueso plano

hueso plano

Ejemplo de pelvis

pelvis

Ejemplo de sacro

sacro

Ejemplo de vértebra

vértebra

Ejemplo de costilla

costilla

Ejemplo de hueso pequeño

hueso pequeño

Vista completa navegable con miniaturas de todas las carpetas (mallas, cada generación de render, fotos reales, recortes segmentados): 📁 Ver "Datos" completo →

Fase 3

Preparación de datos

Cada imagen se mide con shared/cv_extractor/extract.py (geometría clásica: elongación, circularidad, momentos de Hu, perfil de anchura...) y se vuelca en una tabla. El dataset de producción, bone_geometric_features_combined_lab.csv, mezcla renders_lab + noisy_composited + noisy_bproc + fotos reales de entrenamiento: 2938 filas.

2938 filas en el dataset de producción (combined_lab)
548 especímenes físicos únicos detrás de esas filas
9 grupos morfológicos objetivo, no 21 huesos exactos
specimen_split especímenes completos fuera del split train/val — nunca filas sueltas

Regla fija para evitar fuga: un conjunto de especímenes completos se reserva entero para banco de evaluación y nunca entra en entrenamiento (24 vistas del mismo objeto no se reparten entre train y test). Detalle completo, paso a paso y ejecutable, en notebooks/pycon26_01_preparacion_datos.ipynb.

Fase 4

Modelado

Dos modelos sobre los mismos datos: un Random Forest sobre las features geométricas (clásico, interpretable), y una CNN híbrida (recorte de imagen + vector geométrico, transferencia de aprendizaje sobre resnet18). El modelo en producción es hybrid_lab.

Ablación: ¿aporta algo el vector geométrico?

Entrenados cnn_only y hybrid con receta idéntica (mismo dataset, misma semilla) y evaluados en el banco grande de fotos reales (117 fotos, dominio nunca visto en entrenamiento):

ModeloBanco sintético fijoFotos reales (n=117)Balanced accuracy
cnn_only (sin geometría)0.48150.70940.6755
hybrid (con geometría)0.59720.81200.8098

+10 a +13 puntos con geometría, en ambos dominios. El vector geométrico se queda en producción — no es un accesorio, es la diferencia entre 0.71 y 0.81 de accuracy en el dominio que de verdad importa.

Fase 5

Evaluación

Tres bancos, de más fácil a más difícil — solo el último cuenta como resultado real: validación interna → banco sintético con especímenes excluidos → banco de fotos reales. Si un cambio mejora el banco 2 y empeora el 3, es mala señal: el modelo se está ajustando a particularidades del render, no a la forma real del hueso.

Caso de estudio en vivo: una intuición razonable que falló al medirla

Durante la preparación de esta charla se notó que noisy_composited y noisy_bproc repiten casi los mismos especímenes que ya están en renders_lab (112/113 y 90/91 coinciden). Parecía una limpieza obvia: quedarse solo con renders_lab + fotos reales. Se probó de verdad:

DatasetFilasBanco sintético fijoFotos reales (n=117)Balanced accuracy
combined_lab (3 variantes + reales, producción)29380.59720.81200.8098
combined_lab deduplicado (solo renders_lab + reales)14470.54630.69230.7280

Empeoró 12 puntos. No había fuga (el split separa especímenes completos, verificado en código) — el motivo real es que cada receta usa una pose física distinta (caída real en renders_lab vs. órbita fija en las otras dos) y un material distinto: no es duplicación cosmética, es variación visual legítima que ayuda a generalizar a fotos reales. Se revirtió el cambio y quedó documentado en base_datos_osea/HISTORIAL.md (2026-10-30).

Segundo intento: más poses dentro de renders_lab, sin pedir prestado

Si renders_lab es la receta de mejor acabado, ¿generar poses nuevas ahí mismo (física real, otra semilla) en vez de mezclar recetas de menor calidad para ganar volumen? Renderizados 189 especímenes × 6 vistas nuevas (1134 imágenes), dataset de 2527 filas (renders_lab + poses nuevas + reales, sin las otras dos recetas):

DatasetFilasBanco sintético fijoFotos reales (n=117)Balanced accuracy
combined_lab (3 variantes + reales, producción)29380.59720.81200.8098
combined_lab + poses nuevas (renders_lab + extra + reales)25270.51850.76070.7404
combined_lab lab + bproc (sin composited)18610.53240.72650.7471
combined_lab deduplicado (solo renders_lab + reales)14470.54630.69230.7280

Mejora sobre el dedup (+6.8 puntos) pero sigue por debajo del original (-5.1 puntos). Parte del problema del dedup era volumen/pose, pero no todo.

Un fondo que la CNN nunca llega a ver

Hipótesis inicial: noisy_composited ayudaba por el fondo fotográfico real pegado detrás del hueso. Al mirar el recorte segmentado real que recibe la CNN (no la imagen completa, el crop_image ya aislado) se descubrió que es falso — el fondo se recorta siempre a gris neutro, la red nunca lo ve:

Render completo de renders_lab, antes de recortar

renders_lab, imagen completa — material con textura real.

Render completo de noisy_composited, antes de recortar

noisy_composited, imagen completa — silueta plana sobre fondo fotográfico.

Tras segmentar y recortar (lo que de verdad entrena la CNN), el fondo desaparece en los dos casos y lo único que queda de noisy_composited es una silueta casi sin textura. Lo que sí aporta esa receta: no tiene máscara ground-truth, así que se segmenta con grounded_sam — el mismo método (con su mismo ruido de contorno) que se usa en producción sobre fotos reales — y una pose de cámara distinta a las otras dos recetas. Patrón que se repite en los cuatro experimentos: ninguna pieza individual (pose, ruido de segmentación, acabado de material) explica la mejora por sí sola, es la diversidad combinada de las tres. Producción no cambia: sigue hybrid_lab, 3 variantes + reales. Los cuatro modelos quedan en MLflow/DVC como referencia, ninguno promovido.

Fase 6

Despliegue

El modelo entrenado no es el final — se sirve en una cascada de dos etapas (ver Arquitectura para el diagrama completo) detrás de una API FastAPI, con reproducibilidad real: cada entrenamiento y evaluación queda registrado en MLflow, cada artefacto grande (modelos, datasets de imagen) en DVC, cada decisión y su motivo en HISTORIAL.md, y el código en git. Nada queda solo en el disco de una máquina.

MLflow cada run de entrenamiento/evaluación, parámetros + métricas
DVC modelos y carpetas de imágenes grandes, versionados con dvc push
Git código, CSVs procesados, notebooks, historial