Phase 04 - Lesson 08
Segmentacion de Instancias — Mask R-CNN
Agrega una pequena rama de mascara a un detector Faster R-CNN y tienes segmentacion de instancias. La parte dificil es RoIAlign, y es mas dificil de lo que parece.
Type: Build + Learn Languages: Python Prerequisites: Fase 4 Leccion 06 (YOLO), Fase 4 Leccion 07 (U-Net) Time: ~75 minutos
Objetivos de Aprendizaje
- Trazar la arquitectura de Mask R-CNN de extremo a extremo: backbone, FPN, RPN, RoIAlign, cabeza de caja, cabeza de mascara
- Implementar RoIAlign desde cero y explicar por que ya no se usa RoIPool
- Usar el modelo preentrenado
maskrcnn_resnet50_fpn_v2de torchvision para mascaras de instancia de calidad de produccion y leer correctamente el formato de su salida - Hacer fine-tuning de Mask R-CNN en un pequeno conjunto de datos personalizado reemplazando las cabezas de caja y de mascara y manteniendo el backbone congelado
El Problema
La segmentacion semantica te da una mascara por clase. La segmentacion de instancias te da una mascara por objeto, incluso cuando dos objetos comparten una clase. Contar individuos, rastrear a traves de cuadros y medir cosas (la caja delimitadora de cada ladrillo en una pared, cada celula en una imagen de microscopio) todo exige segmentacion de instancias.
Mask R-CNN (He et al., 2017) resolvio esto reformulando la segmentacion de instancias como deteccion mas una mascara. El diseno era tan limpio que, durante los siguientes cinco anos, casi todo articulo de segmentacion de instancias fue una variante de Mask R-CNN, y la implementacion de torchvision sigue siendo el estandar de produccion para conjuntos de datos pequenos a medianos.
El problema de ingenieria dificil es el muestreo: como recortas una region de features de tamano fijo de una caja de propuesta cuyas esquinas no se alinean con los limites de los pixeles? Equivocarse en eso cuesta decimas de punto de mAP en todas partes. RoIAlign es la respuesta.
El Concepto
La arquitectura
flowchart LR
IMG["Entrada"] --> BB["ResNet<br/>backbone"]
BB --> FPN["Red de<br/>Piramide de Features"]
FPN --> RPN["Red de<br/>Propuesta de<br/>Regiones"]
FPN --> RA["RoIAlign"]
RPN -->|"top-K propuestas"| RA
RA --> BH["Cabeza de caja<br/>(clase + refinar)"]
RA --> MH["Cabeza de mascara<br/>(conv 14x14)"]
BH --> NMS["NMS"]
MH --> NMS
NMS --> OUT["cajas +<br/>clases + mascaras"]
style BB fill:#dbeafe,stroke:#2563eb
style FPN fill:#fef3c7,stroke:#d97706
style RPN fill:#fecaca,stroke:#dc2626
style OUT fill:#dcfce7,stroke:#16a34a
Cinco piezas que entender:
- Backbone — ResNet-50 o ResNet-101 entrenado en ImageNet. Produce una jerarquia de mapas de features en strides 4, 8, 16, 32.
- FPN (Red de Piramide de Features) — conexiones top-down + laterales que dan a cada nivel C canales de features semanticamente ricas. La deteccion consulta el nivel de la FPN que coincide con el tamano del objeto.
- RPN (Red de Propuesta de Regiones) — una pequena cabeza conv que, en cada posicion de ancla, predice "hay un objeto aqui?" y "como refino la caja?". Produce ~1000 propuestas por imagen.
- RoIAlign — muestrea una region de features de tamano fijo (por ejemplo, 7x7) de cualquier caja en cualquier nivel de la FPN. Muestreo bilineal, sin cuantizacion.
- Cabezas — cabeza de caja de dos capas que refina la caja y elige una clase, mas una pequena cabeza conv que produce una mascara binaria
28x28para cada propuesta.
Por que RoIAlign, y no RoIPool
El Fast R-CNN original usaba RoIPool, que divide una caja de propuesta en una cuadricula, toma la feature maxima en cada celda y redondea todas las coordenadas a enteros. Ese redondeo desalinea el mapa de features de las coordenadas de pixel de entrada hasta en un pixel completo del mapa de features — pequeno en una imagen de 224x224, catastrofico cuando el mapa de features es de stride 32.
RoIPool:
box (34.7, 51.3, 98.2, 142.9)
round -> (34, 51, 98, 142)
split grid -> round each cell boundary
misalignment accumulates at every step
RoIAlign:
box (34.7, 51.3, 98.2, 142.9)
sample at exact float coordinates using bilinear interpolation
no rounding anywhere
RoIAlign aumenta el AP de mascara en 3-4 puntos en COCO gratis. Todo detector que se preocupa por la localizacion ahora lo usa — YOLOv7 seg, RT-DETR, Mask2Former, todos por igual.
La RPN en un parrafo
En cada posicion de un mapa de features, coloca K cajas de ancla de diferentes tamanos y formas. Predice una puntuacion de objetividad para cada ancla y un desplazamiento de regresion para convertir el ancla en una caja mejor ajustada. Conserva las ~1.000 mejores cajas por puntuacion, aplica NMS en IoU 0.7 y entrega los sobrevivientes a las cabezas. La RPN se entrena con su propia mini-perdida — la misma estructura que la perdida de YOLO de la Leccion 6, solo que con dos clases (objeto / no objeto).
La cabeza de mascara
Para cada propuesta (despues de RoIAlign) la cabeza de mascara es una pequena FCN: cuatro convs 3x3, un deconv 2x, una conv final 1x1 que produce num_classes canales de salida en resolucion 28x28. Solo se conserva el canal correspondiente a la clase predicha; los demas se ignoran. Esto desacopla la prediccion de mascara de la clasificacion.
Haz upsample de la mascara 28x28 al tamano original en pixeles de la propuesta para producir la mascara binaria final.
Perdidas
Mask R-CNN tiene cuatro perdidas sumadas:
L = L_rpn_cls + L_rpn_box + L_box_cls + L_box_reg + L_mask
L_rpn_cls,L_rpn_box— objetividad + regresion de caja para las propuestas de la RPN.L_box_cls— entropia cruzada sobre (C+1) clases (incluyendo el fondo) en el clasificador de la cabeza.L_box_reg— smooth L1 en el refinamiento de caja de la cabeza.L_mask— entropia cruzada binaria por pixel en la salida de la mascara 28x28.
Cada perdida tiene su propio peso por defecto; la implementacion de torchvision los expone como argumentos del constructor.
Formato de salida
torchvision.models.detection.maskrcnn_resnet50_fpn_v2 devuelve una lista de dicts, uno por imagen:
{
"boxes": (N, 4) in (x1, y1, x2, y2) pixel coordinates,
"labels": (N,) class IDs, 0 = background so indices are 1-based,
"scores": (N,) confidence scores,
"masks": (N, 1, H, W) float masks in [0, 1] — threshold at 0.5 for binary,
}
La mascara ya esta en la resolucion completa de la imagen. La salida 28x28 de la cabeza fue upsampled internamente.
Construyelo
Paso 1: RoIAlign desde cero
Este es el unico componente de Mask R-CNN que es mas simple de entender como codigo que como prosa.
import torch
import torch.nn.functional as F
def roi_align_single(feature, box, output_size=7, spatial_scale=1 / 16.0):
"""
feature: (C, H, W) single-image feature map
box: (x1, y1, x2, y2) in original image pixel coordinates
output_size: side of the output grid (7 for box head, 14 for mask head)
spatial_scale: reciprocal of the feature map stride
"""
C, H, W = feature.shape
x1, y1, x2, y2 = [c * spatial_scale - 0.5 for c in box]
bin_w = (x2 - x1) / output_size
bin_h = (y2 - y1) / output_size
grid_y = torch.linspace(y1 + bin_h / 2, y2 - bin_h / 2, output_size)
grid_x = torch.linspace(x1 + bin_w / 2, x2 - bin_w / 2, output_size)
yy, xx = torch.meshgrid(grid_y, grid_x, indexing="ij")
gx = 2 * (xx + 0.5) / W - 1
gy = 2 * (yy + 0.5) / H - 1
grid = torch.stack([gx, gy], dim=-1).unsqueeze(0)
sampled = F.grid_sample(feature.unsqueeze(0), grid, mode="bilinear",
align_corners=False)
return sampled.squeeze(0)
Cada numero esta en una posicion muestreada bilinealmente. Sin redondeo, sin cuantizacion, sin gradientes descartados.
Paso 2: Compara con el RoIAlign de torchvision
from torchvision.ops import roi_align
feature = torch.randn(1, 16, 50, 50)
boxes = torch.tensor([[0, 10, 20, 100, 90]], dtype=torch.float32) # (batch_idx, x1, y1, x2, y2)
ours = roi_align_single(feature[0], boxes[0, 1:].tolist(), output_size=7, spatial_scale=1/4)
theirs = roi_align(feature, boxes, output_size=(7, 7), spatial_scale=1/4, sampling_ratio=1, aligned=True)[0]
print(f"shape ours: {tuple(ours.shape)}")
print(f"shape theirs: {tuple(theirs.shape)}")
print(f"max|diff|: {(ours - theirs).abs().max().item():.3e}")
Con sampling_ratio=1 y aligned=True, los dos coinciden dentro de 1e-5.
Paso 3: Carga un Mask R-CNN preentrenado
import torch
from torchvision.models.detection import maskrcnn_resnet50_fpn_v2, MaskRCNN_ResNet50_FPN_V2_Weights
model = maskrcnn_resnet50_fpn_v2(weights=MaskRCNN_ResNet50_FPN_V2_Weights.DEFAULT)
model.eval()
print(f"params: {sum(p.numel() for p in model.parameters()):,}")
print(f"classes (including background): {len(model.roi_heads.box_predictor.cls_score.out_features * [0])}")
46M de parametros, 91 clases (COCO). La primera clase (id 0) es el fondo; todo lo que el modelo realmente detecta empieza en el id 1.
Paso 4: Ejecuta la inferencia
with torch.no_grad():
x = torch.randn(3, 400, 600)
predictions = model([x])
p = predictions[0]
print(f"boxes: {tuple(p['boxes'].shape)}")
print(f"labels: {tuple(p['labels'].shape)}")
print(f"scores: {tuple(p['scores'].shape)}")
print(f"masks: {tuple(p['masks'].shape)}")
El tensor de mascara tiene forma (N, 1, H, W). Aplica un umbral de 0.5 para obtener una mascara binaria por objeto:
binary_masks = (p['masks'] > 0.5).squeeze(1) # (N, H, W) boolean
Paso 5: Cambia las cabezas por un conteo de clases personalizado
La receta comun de fine-tuning: reutiliza el backbone, la FPN y la RPN; reemplaza las dos cabezas clasificadoras.
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor
def build_custom_maskrcnn(num_classes):
model = maskrcnn_resnet50_fpn_v2(weights=MaskRCNN_ResNet50_FPN_V2_Weights.DEFAULT)
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels
hidden_layer = 256
model.roi_heads.mask_predictor = MaskRCNNPredictor(in_features_mask, hidden_layer, num_classes)
return model
custom = build_custom_maskrcnn(num_classes=5)
print(f"custom cls_score.out_features: {custom.roi_heads.box_predictor.cls_score.out_features}")
num_classes debe incluir la clase de fondo, asi que un conjunto de datos con 4 clases de objeto usa num_classes=5.
Paso 6: Congela lo que no necesita entrenamiento
En conjuntos de datos pequenos, congela el backbone y la FPN. Solo la objetividad + regresion de la RPN y las dos cabezas aprenden.
def freeze_backbone_and_fpn(model):
# torchvision Mask R-CNN packs the FPN inside `model.backbone` (as
# `model.backbone.fpn`), so iterating `model.backbone.parameters()` covers
# both the ResNet feature layers and the FPN lateral/output convs.
for p in model.backbone.parameters():
p.requires_grad = False
return model
custom = freeze_backbone_and_fpn(custom)
trainable = sum(p.numel() for p in custom.parameters() if p.requires_grad)
print(f"trainable after freeze: {trainable:,}")
En conjuntos de datos de 500 imagenes, esta es la diferencia entre convergencia y overfitting.
Usalo
El loop de entrenamiento completo para Mask R-CNN en torchvision tiene 40 lineas y no cambia de forma significativa entre tareas — cambia los conjuntos de datos y adelante.
def train_step(model, images, targets, optimizer):
model.train()
loss_dict = model(images, targets)
losses = sum(loss for loss in loss_dict.values())
optimizer.zero_grad()
losses.backward()
optimizer.step()
return {k: v.item() for k, v in loss_dict.items()}
La lista targets debe tener dicts por imagen con boxes, labels y masks (como tensores binarios (num_instances, H, W)). El modelo devuelve un dict de cuatro perdidas durante el entrenamiento y una lista de predicciones durante la evaluacion, segun model.training.
El evaluador pycocotools produce mAP@IoU=0.5:0.95 tanto para cajas como para mascaras; necesitas ambos numeros para saber si la cabeza de caja o la cabeza de mascara es el cuello de botella.
Entregalo
Esta leccion produce:
outputs/prompt-instance-vs-semantic-router.md— un prompt que hace tres preguntas y elige entre instancia, semantica o panoptica, mas el modelo exacto con el cual empezar.outputs/skill-mask-rcnn-head-swapper.md— una skill que genera las 10 lineas de codigo para cambiar las cabezas en cualquier modelo de deteccion de torchvision, dado el nuevonum_classes.
Ejercicios
- (Facil) Verifica tu RoIAlign contra
torchvision.ops.roi_alignen 100 cajas aleatorias. Reporta la diferencia absoluta maxima. Tambien ejecuta RoIPool (comportamiento pre-2017) y muestra que diverge en ~1-2 pixeles del mapa de features en cajas cercanas al borde. - (Medio) Haz fine-tuning de
maskrcnn_resnet50_fpn_v2en un conjunto de datos personalizado de 50 imagenes (cualquier par de clases: globos, peces, baches, logos). Congela el backbone, entrena por 20 epocas, reporta el AP de mascara@0.5. - (Dificil) Reemplaza la cabeza de mascara de Mask R-CNN por una que prediga en 56x56 en lugar de 28x28. Mide el mAP@IoU=0.75 antes y despues. Explica por que la ganancia (o la falta de ella) coincide con el trade-off esperado entre precision de borde y memoria.
Terminos Clave
| Termino | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| Mask R-CNN | "Deteccion mas mascaras" | Faster R-CNN + una pequena cabeza FCN que predice una mascara 28x28 por propuesta por clase |
| FPN | "Piramide de features" | Conexiones top-down + laterales que dan a cada nivel de stride C canales de features semanticamente ricas |
| RPN | "Proponente de regiones" | Una pequena cabeza conv que produce ~1000 propuestas objeto/no-objeto por imagen |
| RoIAlign | "Recorte sin redondeo" | Muestrea bilinealmente una cuadricula de features de tamano fijo de cualquier caja con coordenadas en float |
| RoIPool | "Recorte pre-2017" | Mismo proposito que RoIAlign pero redondea coordenadas de caja; obsoleto |
| Mask AP | "mAP de instancia" | Precision promedio calculada con IoU de mascara en lugar de IoU de caja; la metrica de segmentacion de instancias de COCO |
| Cabeza de mascara binaria | "Mascara por clase" | Predice una mascara binaria por clase para cada propuesta; solo se conserva el canal de la clase predicha |
| Clase de fondo | "Clase 0" | La clase comodin "ningun objeto"; los indices para las clases reales empiezan en 1 |
Lectura Adicional
- Mask R-CNN (He et al., 2017) — el articulo; la seccion 3 sobre RoIAlign es la lectura critica
- FPN: Feature Pyramid Networks (Lin et al., 2017) — el articulo de la FPN; todo detector moderno la usa
- torchvision Mask R-CNN tutorial — la referencia para el loop de fine-tuning
- Detectron2 model zoo — implementaciones de produccion con pesos entrenados para casi todas las variantes de deteccion y segmentacion