Volver a la lista

Búsqueda de incrustaciones de imágenes de patología: encontrar tejidos similares en lenguaje natural a partir de 100 láminas de cáncer de mama TCGA-BRCA.

Modelos de base especializados en patología, como Virchow2, UNI y CONCH, indexan a gran escala las incrustaciones de mosaicos de imágenes de portaobjetos completos (WSI) y utilizan FAISS IVF-PQ para la búsqueda de tejidos similares. Mediante el contraste Path × Language (CONCH), se pueden buscar casos de consulta en lenguaje natural, como "densidad de linfocitos en el borde de invasión tumoral". El escenario práctico TCGA-BRCA incluye un flujo de trabajo completo que abarca desde la segmentación de mosaicos WSI, la normalización del color Macenko, la generación de incrustaciones por lotes hasta la indexación mediante mmap.

Intermedio
|
40min
|
Verificado (2026-07)
Progreso0/15 (0%)

Búsqueda de incrustaciones de modelos fundacionales para imágenes de patología: encontrar tejidos similares mediante lenguaje natural en 100 diapositivas de cáncer de mama TCGA-BRCA

Las diapositivas de patología son imágenes de muestra completa (WSI) que pueden ocupar varios gigabytes cada una, y los patólogos las examinan alternando entre aumentos bajos y altos durante decenas de minutos por caso. Sin embargo, preguntas como "¿Hubo algún caso anterior con un patrón tisular similar?" o "Extrae solo los casos con linfocitos densamente agrupados en el borde de invasión tumoral" siguen siendo difíciles de responder mediante búsquedas. En este episodio, utilizando 100 diapositivas de patología de cáncer de mama TCGA-BRCA (The Cancer Genome Atlas, carcinoma invasivo de mama) como ejemplo, creamos un flujo de trabajo práctico que incrusta los mosaicos de WSI mediante modelos fundacionales de patología (Virchow2, UNI, CONCH), construye un índice de búsqueda de similitud a gran escala y permite la búsqueda mediante consultas en lenguaje natural.

📚 Lecturas previas recomendadas (muy recomendadas)

Este episodio es un contenido avanzado y técnico sobre IA y biología. Antes de comenzar, recomendamos encarecidamente que revise primero los siguientes episodios de DryBench:

Si aborda este contenido sin haber visto las lecturas previas, el código práctico comenzará directamente sin volver a explicar los principios del Vision Transformer (ViT), la alineación de texto e imagen al estilo CLIP con aprendizaje por contraste y los patrones prácticos para cargar modelos de visión de HuggingFace transformers, lo que dificultará su comprensión.


Lo que ya aprendimos en DryBench

En DryBench ai-native #3, aprendimos que las incrustaciones del transformer constituyen un "espacio latente aprendido" independiente del dominio y la modalidad; en #5, aprendimos que la atención aprende las relaciones entre cualquier par de parches de la imagen, lo que constituye la base del vision transformer; y en #13, aprendimos que HuggingFace proporciona una API estándar para cargar e inferir modelos de visión (como ViT, CLIP, etc.).

La patología es un campo en el que los Vision Transformers destacan especialmente. A diferencia de las imágenes naturales, presenta características visuales muy diferentes (predominio de los colores rojo de la hematoxilina y púrpura de la eosina en la tinción H&E, estructura jerárquica celular, tisular y epitelial, y capas de información según la escala). Los modelos fundacionales preentrenados con grandes corpus de diapositivas patológicas (millones de WSI y miles de millones de mosaicos) se utilizan directamente para múltiples tareas secundarias (clasificación del cáncer, predicción del grado, predicción de la supervivencia y búsqueda de similitudes). Este documento es una guía práctica para convertir estos embeddings fundacionales en "herramientas de búsqueda", aprovechando específicamente el aprendizaje contrastivo de CONCH y PLIP para emparejar imágenes y lenguaje natural en el mismo espacio.

Definición del problema

Escenario práctico: Búsqueda de similitudes en patología de cáncer de mama TCGA-BRCA

En una colección de 100 diapositivas de un archivo de patología de cáncer de mama de un hospital (o el subconjunto público de TCGA-BRCA), se debe poder realizar lo siguiente:

  • Búsqueda por imagen: Dada una imagen de un nuevo caso (por ejemplo, el límite de invasión tumoral), se deben encontrar los 10 casos anteriores con patrones tisulares más similares.
  • Búsqueda por texto: Dada una consulta en lenguaje natural, como "carcinoma ductal invasivo con infiltrado linfocítico denso en el margen tumoral", se deben encontrar los casos que presenten dicho hallazgo.
  • Similitud entre instituciones: Emparejamiento de datos entre el subconjunto TCGA y los datos de otro hospital (investigación en múltiples centros).
  • Velocidad de procesamiento: Indexación de los embeddings de un WSI (aproximadamente 10 a 100 000 mosaicos) en menos de 30 minutos y tiempo de respuesta a la consulta inferior a 100 ms.

Espectro de enfoques existentes

  • Características diseñadas manualmente (histograma de color, Haralick, LBP): Posible para búsquedas a pequeña escala, pero no refleja la información jerárquica típica de la patología.
  • ResNet-50 preentrenado con ImageNet: Entrenado con imágenes naturales; gran cambio de dominio en patología, rendimiento limitado.
  • CTransPath (2022): ViT auto-supervisado temprano específico para patología. Línea de base.
  • UNI (2024, Mass General Brigham + Harvard): Entrenado con 100 000 WSI y más de 100 millones de mosaicos; potente para la búsqueda de similitudes [1].
  • Virchow (Paige.AI 2024): Entrenado con 1,5 millones de WSI; múltiples resultados de última generación en tareas clínicas [2]. Virchow2 (versión posterior de 2024) mejora el rendimiento y la eficiencia.
  • PLIP (2023, Nature Medicine): Aprendizaje contrastivo Path × Language, entrenado con pares de Twitter médico + PubMed, admite consultas en lenguaje natural [3].
  • CONCH (2024): Modelo de lenguaje de patología a gran escala, sucesor de PLIP, entrenado con subtítulos clínicos, potente en clasificación zero-shot [4].
  • PANTHER (2024, sucesor de PLIP): Modelo multilingüe y multi-regional de patología y lenguaje.
  • GigaPath (2024, Microsoft + Providence): Genera incrustaciones a nivel completo de imágenes de portaobjetos digital (WSI) [5].

Esta sección combina Virchow2 (incrustación base de imagen) + CONCH (puente de lenguaje natural).

Métricas objetivo de esta sección

  • Construcción del índice de incrustaciones de mosaicos en un subconjunto de 100 imágenes WSI de TCGA-BRCA (aproximadamente 1 millón de mosaicos).
  • Tasa de recuperación top-K ≥ 0.80 (en comparación con las etiquetas de referencia proporcionadas por patólogos).
  • Recuperación de imágenes mediante consultas en lenguaje natural, con una precisión R@10 ≥ 0.55 (basado en el conjunto de pruebas de CONCH).
  • Latencia de respuesta de consulta ≤ 100 ms (FAISS HNSW).
  • Medición del cambio en el rendimiento antes y después de la normalización del color (Macenko o Vahadane).

Conjunto de herramientas e infraestructura requerida

HerramientaFunciónLicencia
Virchow2 (paige-ai HuggingFace)Incrustación de imagen base para patologíaGratuita para uso académico (según la política de Paige)
UNI (MahmoodLab HuggingFace)Modelo alternativo/de conjuntoGratuito para uso académico
CONCH (MahmoodLab HuggingFace)Modelo contrastivo de patología y lenguajeGratuito para uso académico
PLIPAlternativa contrastivaGratuito para uso académico
OpenSlideLectura de archivos WSI (SVS, NDPI, MRXS, etc.)LGPL
FAISSÍndice KNN a gran escala (IVF-PQ, HNSW)MIT
HuggingFace transformers, timmCarga de modelos, red neuronal base para visiónApache 2.0
Macenko/Vahadane (staintools)Normalización del colorMIT
pyvips (opcional)Procesamiento eficiente en memoria para imágenes WSI de gran tamañoGPL v3
Chroma, Qdrant (opcional)Alternativa para el filtrado de metadatosApache 2.0

Requisitos de infraestructura:

  • Extracción de incrustaciones: Servidor GPU con al menos 16-32 GB de VRAM (Virchow2 y UNI pueden funcionar con 8 GB en fp16, pero se requieren 16 GB+ para lotes de 32+).
  • Índice FAISS: Se puede utilizar solo CPU. RAM ≥ 32 GB (1 millón de mosaicos × dimensión 1024 float16 = aprox. 2 GB; para escalas de 100 millones de mosaicos, la compresión IVF-PQ es esencial).
  • Almacenamiento: El almacenamiento de TCGA puede ser en el almacenamiento del hospital o en un NAS; el índice de incrustaciones ocupa aproximadamente 2~10 GB (según la escala).

Coste estimado para la reproducción por parte del alumno: El coste de la API es de 0 cuando se utiliza una GPU local. Consulte la tabla de precios por hora para el uso en la nube. La indexación de incrustaciones de 100 imágenes de porta de tejido (WSI) del subconjunto TCGA tarda aproximadamente entre 2 y 4 horas (estimado basado en una RTX 4090 de 24 GB).

Implementación práctica del flujo de trabajo

Flujo completo:

mermaid

Paso 1. División de imágenes WSI en mosaicos

Las imágenes WSI tienen una estructura piramidal (varios niveles de resolución). Para el procesamiento, el estándar son mosaicos de 224×224 a un aumento de 20x (0,5 µm/píxel). La mayoría de los datos de TCGA se escanean a 40x (0,25 µm/píxel).

python
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import Iterator
import openslide
import numpy as np
from PIL import Image
@dataclass
class TileMetadata:
wsi_id: str
slide_index: int
level: int
x_wsi: int # Coordenadas del nivel original de la imagen completa de tejido (WSI)
y_wsi: int
x_selected: int # Coordenada del nivel seleccionado (20x)
y_selected: int
size: int
tissue_ratio: float
def open_wsi(wsi_path: Path) -> openslide.OpenSlide:
try:
return openslide.OpenSlide(str(wsi_path))
except openslide.OpenSlideError as e:
raise RuntimeError(f"Error al abrir WSI {wsi_path}: {e}")
def compute_target_level(slide: openslide.OpenSlide, target_mpp: float = 0.5) -> tuple[int, float]:
"""Selecciona el nivel más cercano a la magnificación 20x (0.5 mpp)."""
mpp_x = float(slide.properties.get(openslide.PROPERTY_NAME_MPP_X, 0.25))
downsample = target_mpp / mpp_x
level = slide.get_best_level_for_downsample(downsample)
return level, slide.level_downsamples[level]
def compute_tissue_ratio(tile: np.ndarray) -> float:
"""Proporción de tejido basada en la saturación HSV. Excluye el fondo (blanco/baja saturación).
En entornos reales, se recomienda agregar umbralización Otsu sobre la saturación.
"""
from skimage.color import rgb2hsv
hsv = rgb2hsv(tile)
saturation = hsv[..., 1]
tissue_mask = saturation > 0.05
return float(tissue_mask.mean())
def tile_wsi(
wsi_path: Path,
wsi_id: str,
slide_index: int,
tile_size: int = 224,
target_mpp: float = 0.5,
tissue_threshold: float = 0.2,
output_dir: Path | None = None,
save_tiles: bool = False,
) -> Iterator[tuple[TileMetadata, np.ndarray]]:
"""Divide la WSI en una cuadrícula de mosaicos de 224×224. Excluye automáticamente el fondo.
Generador en streaming → minimiza la carga de memoria.
"""
slide = open_wsi(wsi_path)
level, level_downsample = compute_target_level(slide, target_mpp)
width, height = slide.level_dimensions[level]
print(f"[{wsi_id}] level={level}, dims={width}x{height}, mpp≈{target_mpp}")
for y in range(0, height - tile_size, tile_size):
for x in range(0, width - tile_size, tile_size):
# Coordenadas del nivel original
x_wsi = int(x * level_downsample)
y_wsi = int(y * level_downsample)
try:
tile_pil = slide.read_region((x_wsi, y_wsi), level, (tile_size, tile_size)).convert("RGB")
except openslide.OpenSlideError:
continue
tile_np = np.asarray(tile_pil)
tissue_ratio = compute_tissue_ratio(tile_np)
if tissue_ratio < tissue_threshold:
continue
meta = TileMetadata(
wsi_id=wsi_id, slide_index=slide_index, level=level,
x_wsi=x_wsi, y_wsi=y_wsi,
x_selected=x, y_selected=y,
size=tile_size, tissue_ratio=tissue_ratio,
)
if save_tiles and output_dir:
out = output_dir / f"{wsi_id}_x{x}_y{y}.png"
out.parent.mkdir(parents=True, exist_ok=True)
tile_pil.save(out, format="PNG")
yield meta, tile_np
slide.close()

Paso 2. Normalización del color (Macenko)

Dado que los protocolos de tinción H&E y los fabricantes de escáneres varían entre hospitales, los colores pueden diferir visualmente. El algoritmo de Macenko es el estándar.

python
def macenko_normalize(
tiles: list[np.ndarray],
target_stain_matrix: np.ndarray | None = None,
target_max_conc: np.ndarray | None = None,
alpha: float = 1.0,
beta: float = 0.15,
) -> tuple[list[np.ndarray], np.ndarray, np.ndarray]:
"""Normalización de color Macenko.
Si target_stain_matrix / target_max_conc es None, se entrena en el primer mosaico.
En la práctica, utilice las bibliotecas staintools y torchstain.
"""
try:
from torchstain import MacenkoNormalizer
except ImportError:
raise RuntimeError("se requiere torchstain (pip install torchstain)")
import torch
normalizer = MacenkoNormalizer(backend="numpy")
if target_stain_matrix is None:
# Primera ficha para entrenar el objetivo
normalizer.fit(np.transpose(tiles[0], (2, 0, 1)))
else:
normalizer.HERef = target_stain_matrix
normalizer.maxCRef = target_max_conc
normalized = []
for tile in tiles:
try:
tile_t = np.transpose(tile, (2, 0, 1))
norm_t, _, _ = normalizer.normalize(tile_t, stains=False)
normalized.append(np.transpose(np.asarray(norm_t), (1, 2, 0)))
except Exception:
normalized.append(tile) # Mantener original si falla la normalización
return normalized, normalizer.HERef, normalizer.maxCRef

Paso 3. Extracción de incrustaciones de Virchow2 por lotes.

python
import torch
from transformers import AutoImageProcessor, AutoModel
class Virchow2Embedder:
"""Incrustación de fundación patológica Virchow2 (o UNI) de Paige.AI."""
MODEL_ID = "paige-ai/Virchow2"
def __init__(self, device: str = "cuda", torch_dtype: torch.dtype = torch.float16):
self.device = device
self.processor = AutoImageProcessor.from_pretrained(self.MODEL_ID)
self.model = AutoModel.from_pretrained(
self.MODEL_ID,
torch_dtype=torch_dtype,
).to(device).eval()
# Verificar hidden_dim de Virchow2 en la configuración del modelo (en ejecución)
self.hidden_dim = getattr(self.model.config, "hidden_size", 1280)
@torch.no_grad()
def embed_batch(self, tiles: list[np.ndarray], batch_size: int = 32) -> np.ndarray:
"""(N, 224, 224, 3) uint8 → (N, hidden_dim) fp16."""
if not tiles:
return np.zeros((0, self.hidden_dim), dtype=np.float16)
embeddings = []
for i in range(0, len(tiles), batch_size):
chunk = tiles[i:i + batch_size]
inputs = self.processor(images=chunk, return_tensors="pt").to(self.device)
outputs = self.model(**inputs)
# Utilizar incrustación estándar de token CLS. Algunos modelos recomiendan pooling medio.
cls_emb = outputs.last_hidden_state[:, 0, :]
embeddings.append(cls_emb.cpu().numpy())
return np.concatenate(embeddings, axis=0).astype(np.float16)
def stream_and_embed_wsi(
wsi_path: Path,
wsi_id: str,
slide_index: int,
embedder: Virchow2Embedder,
normalize_color: bool = True,
batch_size: int = 32,
) -> tuple[np.ndarray, list[TileMetadata]]:
"""Transmitir un WSI e incrustar por lotes → (N, dim), metadatos."""
tile_buffer: list[np.ndarray] = []
meta_buffer: list[TileMetadata] = []
all_embeddings: list[np.ndarray] = []
all_metadata: list[TileMetadata] = []
for meta, tile in tile_wsi(wsi_path, wsi_id, slide_index):
tile_buffer.append(tile)
meta_buffer.append(meta)
if len(tile_buffer) >= batch_size:
if normalize_color:
tile_buffer, _, _ = macenko_normalize(tile_buffer)
emb = embedder.embed_batch(tile_buffer, batch_size=batch_size)
all_embeddings.append(emb)
all_metadata.extend(meta_buffer)
tile_buffer, meta_buffer = [], []
# vaciar buffer restante
if tile_buffer:
if normalize_color:
tile_buffer, _, _ = macenko_normalize(tile_buffer)
emb = embedder.embed_batch(tile_buffer, batch_size=batch_size)
all_embeddings.append(emb)
all_metadata.extend(meta_buffer)
if not all_embeddings:
return np.zeros((0, embedder.hidden_dim), dtype=np.float16), []
return np.concatenate(all_embeddings, axis=0), all_metadata

Paso 4. Índice FAISS IVF-PQ para conjuntos de datos grandes

Un millón de mosaicos (Virchow2 hidden_dim ~1280, float16) equivalen a aproximadamente 2.5 GB en memoria. Para un conjunto de datos de 100 millones de mosaicos, es imprescindible utilizar la compresión IVF-PQ (cuantización de productos) y el mapeo de memoria en disco (on-disk mmap).

python
import faiss
def build_ivf_pq_index(
embeddings: np.ndarray,
nlist: int = 4096, # número de clústeres (por escala: 1M → 4k, 100M → 65k)
m: int = 32, # número de subcuantizadores (PQ)
nbits: int = 8, # bits por cada subcuantizador
training_sample: int = 100_000,
) -> faiss.Index:
"""IVF-PQ: índice de compresión para grandes volúmenes.
hidden_dim=1280 × 1M × float32 = 5GB → tras PQ, aproximadamente 40MB comprimidos.
"""
dim = embeddings.shape[1]
quantizer = faiss.IndexFlatIP(dim)
index = faiss.IndexIVFPQ(quantizer, dim, nlist, m, nbits, faiss.METRIC_INNER_PRODUCT)
# Muestra de entrenamiento (si el total es demasiado grande, usar solo 100k aleatorios)
sample_size = min(training_sample, len(embeddings))
sample_idx = np.random.choice(len(embeddings), sample_size, replace=False)
train_sample = embeddings[sample_idx].astype(np.float32)
faiss.normalize_L2(train_sample)
print(f"Inicio del entrenamiento IVF-PQ (nlist={nlist}, m={m}, muestra={sample_size})")
index.train(train_sample)
print("Entrenamiento completado")
return index
def build_hnsw_index(
embeddings: np.ndarray,
m: int = 32,
ef_construction: int = 200,
) -> faiss.Index:
\"\"\"HNSW: excelente precisión y velocidad, memoria relativamente grande. Recomendado para escalas hasta 1 millón.\"\"\"
dim = embeddings.shape[1]
index = faiss.IndexHNSWFlat(dim, m, faiss.METRIC_INNER_PRODUCT)
index.hnsw.efConstruction = ef_construction
embs = embeddings.astype(np.float32)
faiss.normalize_L2(embs)
index.add(embs)
return index
def add_embeddings_in_chunks(
index: faiss.Index,
embeddings_iter: Iterator[np.ndarray],
chunk_size: int = 100_000,
) -> None:
\"\"\"Agregar embeddings de gran volumen al índice en fragmentos (gestión de memoria).\"\"\"
for chunk in embeddings_iter:
chunk_f32 = chunk.astype(np.float32)
faiss.normalize_L2(chunk_f32)
index.add(chunk_f32)
def save_index_mmap(index: faiss.Index, path: Path) -> None:
\"\"\"Guardar el índice con soporte para MMAP.\"\"\"
faiss.write_index(index, str(path))
def load_index_mmap(path: Path) -> faiss.Index:
"""Carga MMAP (ahorro de memoria)."""
return faiss.read_index(str(path), faiss.IO_FLAG_MMAP | faiss.IO_FLAG_READ_ONLY)

Paso 5. Consulta — Búsqueda de imágenes

python
def search_by_image(
query_tile: np.ndarray,
embedder: Virchow2Embedder,
index: faiss.Index,
metadata: list[TileMetadata],
k: int = 20,
nprobe: int = 128, # Número de sondas IVF
ef_search: int = 128, # HNSW efSearch
) -> list[dict]:
"""Un tile de consulta → metadatos del top-K tiles similares."""
query_emb = embedder.embed_batch([query_tile]).astype(np.float32)
faiss.normalize_L2(query_emb)
if hasattr(index, "nprobe"):
index.nprobe = nprobe
if hasattr(index, "hnsw"):
index.hnsw.efSearch = ef_search
distances, indices = index.search(query_emb, k)
results = []
for dist, idx in zip(distances[0], indices[0]):
if idx < 0 or idx >= len(metadata):
continue
m = metadata[idx]
results.append({
**asdict(m),
"similarity": float(dist),
})
return results

Paso 6. Consulta — Búsqueda por lenguaje natural (CONCH)

CONCH alinea un codificador de imágenes y un codificador de texto en un espacio de incrustación compartido. Consulta en lenguaje natural → búsqueda de imágenes.

python
from transformers import CLIPModel, CLIPProcessor
class CONCHTextEncoder:
"""CONCH text encoder wrapping."""
MODEL_ID = "MahmoodLab/CONCH"
def __init__(self, device: str = "cuda"):
self.device = device
self.processor = CLIPProcessor.from_pretrained(self.MODEL_ID)
self.model = CLIPModel.from_pretrained(self.MODEL_ID).to(device).eval()
@torch.no_grad()
def encode_text(self, text: str) -> np.ndarray:
inputs = self.processor(text=[text], return_tensors="pt", padding=True).to(self.device)
text_emb = self.model.get_text_features(**inputs)
return text_emb.cpu().numpy()
def search_by_text(
query_text: str,
text_encoder: CONCHTextEncoder,
index: faiss.Index, # Debe ser un índice creado con el codificador de imágenes CONCH para que coincida
metadata: list[TileMetadata],
k: int = 20,
) -> list[dict]:
"""Consulta en lenguaje natural → tiles de imagen similares.
※ Atención: este índice debe crearse con incrustaciones del codificador de imágenes CONCH.
Diferente del espacio de incrustación del índice de imágenes Virchow2.
"""
query_emb = text_encoder.encode_text(query_text).astype(np.float32)
faiss.normalize_L2(query_emb)
distances, indices = index.search(query_emb, k)
return [
{**asdict(metadata[i]), "similarity": float(d)}
for d, i in zip(distances[0], indices[0]) if 0 <= i < len(metadata)
]

Importante: El índice para la búsqueda de imágenes y el índice para la búsqueda cruzada de texto e imágenes utilizan diferentes incrustaciones del codificador. En un entorno real, podría ser necesario mantener tanto el índice de imágenes Virchow2 como el índice de imágenes CONCH (lo que duplicaría la carga de la memoria RAM y del disco).

Paso 7. Ejecución del flujo de trabajo integrado · 100 imágenes de porta de TCGA-BRCA.

python
def full_pipeline(
wsi_paths: list[Path],
work_dir: Path,
device: str = "cuda",
) -> tuple[faiss.Index, list[TileMetadata]]:
"""Lista WSI → Índice FAISS + metadatos."""
work_dir.mkdir(parents=True, exist_ok=True)
embedder = Virchow2Embedder(device=device)
all_metadata: list[TileMetadata] = []
all_embeddings: list[np.ndarray] = []
for i, wsi_path in enumerate(wsi_paths):
wsi_id = wsi_path.stem
print(f"[{i+1}/{len(wsi_paths)}] {wsi_id}")
try:
emb, meta = stream_and_embed_wsi(
wsi_path, wsi_id=wsi_id, slide_index=i,
embedder=embedder, normalize_color=True,
)
all_embeddings.append(emb)
all_metadata.extend(meta)
print(f" {len(meta)} mosaicos")
except Exception as e:
print(f" Error: {e}")
continue
if not all_embeddings:
raise RuntimeError("Sin resultados de incrustación")
concat_emb = np.concatenate(all_embeddings, axis=0)
print(f"Total de mosaicos {len(concat_emb)}, dim={concat_emb.shape[1]}")
# Determinar el tamaño y seleccionar el tipo de índice
if len(concat_emb) < 500_000:
print("Índice HNSW (tamaño ≤ 500k)")
index = build_hnsw_index(concat_emb)
else:
print("Índice IVF-PQ (tamaño > 500k)")
index = build_ivf_pq_index(concat_emb)
add_embeddings_in_chunks(index, [concat_emb])
save_index_mmap(index, work_dir / "wsi_index.faiss")
# Guardar también los metadatos (msgpack, jsonl, etc.)
import json
with open(work_dir / "metadata.jsonl", "w") as f:
for m in all_metadata:
f.write(json.dumps(asdict(m)) + "\n")
print(f"Guardado completo: {work_dir}")
return index, all_metadata
# Ejemplo de ejecución (escenario con 100 WSI de TCGA-BRCA)
# tcga_wsi_dir = Path("/data/tcga_brca_svs")
# wsi_files = sorted(tcga_wsi_dir.glob("*.svs"))[:100]
# index, meta = full_pipeline(wsi_files, work_dir=Path("./tcga_brca_output"))

Paso 8. Conjunto de consultas en lenguaje natural de ejemplo

Ejemplos de consultas en lenguaje natural que los patólogos podrían usar en la práctica para realizar búsquedas, formuladas de manera que CONCH pueda interpretarlas.

python
EXAMPLE_QUERIES = [
"invasive ductal carcinoma with dense lymphocytic infiltrate at tumor margin",
"high-grade tumor with necrotic center and prominent nucleoli",
"tubular carcinoma with well-formed glandular structures",
"adjacent normal breast tissue with lobular architecture preserved",
"medullary carcinoma with syncytial growth pattern and TIL",
"ductal carcinoma in situ (DCIS) with cribriform architecture",
"stromal desmoplasia with fibroblast proliferation surrounding tumor nests",
]
def demo_text_queries(
text_encoder: CONCHTextEncoder,
conch_index: faiss.Index,
metadata: list[TileMetadata],
output_dir: Path,
) -> None:
"""Almacena los resultados top-5 para cada conjunto de consultas en lenguaje natural."""
output_dir.mkdir(parents=True, exist_ok=True)
import json
for query in EXAMPLE_QUERIES:
results = search_by_text(query, text_encoder, conch_index, metadata, k=5)
out_path = output_dir / f"query_{hash(query) % 10**6}.json"
with open(out_path, "w") as f:
json.dump({"query": query, "top_5": results}, f, ensure_ascii=False, indent=2)

Rendimiento, costo y casos de fallo conocidos

Referencias de rendimiento (citación de benchmarks públicos)

ModeloBenchmarkAUC con Linear ProbingR@10 en RetrievalFuente
ImageNet ResNet-50Camelyon160.830.32Línea base heredada
CTransPathCamelyon160.910.51Wang et al., MedIA 2022
UNICamelyon16 + BRACS0.95+0.68Chen et al., Nat Med 2024 [1]
VirchowMulti-cohort0.94~0.970.70Vorontsov et al., Nat Med 2024 [2]
Virchow2Multi-cohort0.96~0.98 (estimado)0.72 (estimado)Paige.AI 2024 [2]
PLIPClasificación zero-shot0.870.45 (consulta de texto)Huang et al., Nat Med 2023 [3]
CONCHZero-shot0.900.60 (consulta de texto)Lu et al., Nat Med 2024 [4]
GigaPath (nivel WSI)Multi-tarea0.88~0.94Xu et al., Nature 2024 [5]

Costo estimado de reproducción

  • Costo de API: 0 (completamente local).
  • Con una GPU de 16-32 GB de VRAM, la indexación de incrustaciones de 100 WSI de TCGA-BRCA tarda aproximadamente 2~4 horas.
  • Entrenamiento del índice FAISS IVF-PQ: 10~20 minutos para entrenar con una muestra de 1 millón de vectores (CPU).
  • Respuesta a consultas en lenguaje natural: menos de 100 ms (mapeo del índice en memoria).

5 casos de fallo conocidos (recopilación comunitaria y bibliográfica)

  1. Desajuste de dominio en datos de otros hospitales (diferencias de escáner y tinción) Síntoma: El modelo entrenado e indexado con datos del hospital A sufre una caída drástica en el rendimiento de búsqueda en WSI del hospital B (R@10 de 0.72 a 0.42). Causa: Diferencias en los protocolos de tinción H&E, proveedores de escáneres (Aperio, Hamamatsu, Leica) y corrección de color. La normalización de Reinhard por sí sola es insuficiente. Evitar: (a) normalización de color Macenko · Vahadane (paso 2 de este capítulo), (b) ajuste fino con datos de múltiples hospitales, (c) uso de aumento de tinción, (d) entrenamiento adversarial por dominio, (e) CONCH y UNI son relativamente robustos frente al cambio de dominio porque se entrenan con datos de diversos hospitales. Fuente: Ciompi et al. "The importance of stain normalization in colorectal tissue classification" ISBI 2017; discusión sobre UNI [1].

  2. Compatibilidad de formatos de archivo WSI (SVS · NDPI · MRXS · isyntax) Síntoma: OpenSlide no puede leer ciertos archivos de proveedores específicos (por ejemplo, Philips isyntax). Causa: OpenSlide tiene soporte limitado para algunos formatos comerciales. Evitar: (a) para isyntax, usar el SDK de Philips o herramientas de conversión independientes (bfconvert · isyntax-cli), (b) para MRXS, usar la versión más reciente de OpenSlide, (c) como alternativa, utilizar TIAToolbox (OpenSlide + extensiones), (d) convertir a WSI en formato DICOM (estándar DICOM-WG 26). Fuente: Problemas de GitHub de OpenSlide [6]; documentación de TIAToolbox [7].

  3. Sesgo de los datos de entrenamiento para FAISS IVF-PQ Síntoma: Si durante el aprendizaje del libro de códigos PQ un tipo de tejido está sobrerrepresentado, la precisión de búsqueda de otros tejidos disminuye drásticamente. Causa: Falta de aleatoriedad en las muestras de entrenamiento o sesgo hacia casos específicos. Evitar: (a) extraer muestras estratificadas de entrenamiento de múltiples hospitales y tipos de tejido, (b) establecer nprobe en un valor alto (256+) para garantizar la precisión, (c) volver a entrenar el índice al agregar nuevos tipos de tejido, (d) preferir HNSW cuando el tamaño sea inferior a 500 k. Fuente: Wiki de FAISS "IVF training pitfalls" [8].

  4. Fallo en la coincidencia entre consultas de texto CONCH y el índice de imágenes (incompatibilidad de espacios de codificación) Síntoma: Al consultar un índice de imágenes creado con Virchow2 usando incrustaciones de texto de CONCH, los resultados carecen de sentido. Causa: Virchow2 y CONCH tienen diferentes procesos de entrenamiento y ocupan distintos espacios de incrustación; no hay alineación contrastiva. Evitar: (a) para consultas en lenguaje natural, crear un índice separado utilizando el codificador de imágenes de CONCH, (b) mantener dos índices separados (imágenes Virchow2 e imágenes+texto CONCH), (c) evaluar el compromiso entre costo de almacenamiento/búsqueda y precisión. Fuente: Ejemplos de uso de CONCH en GitHub [9]; principios básicos del aprendizaje contrastivo.

  5. Descarga y almacenamiento de grandes volúmenes de imágenes de patología digital (WSI) de TCGA Síntoma: El conjunto completo de más de 1000 imágenes WSI de TCGA-BRCA ocupa varios terabytes. Los usuarios experimentan largos tiempos de descarga local y limitaciones de espacio en disco. Causa: Cada imagen WSI de TCGA tiene un tamaño promedio de 500 MB a 2 GB. Solución: (a) Utilizar un subconjunto de TCGA (por ejemplo, 100 diapositivas en lugar del conjunto completo), (b) procesar las imágenes mediante transmisión con la API de GDC (Genomic Data Commons), almacenando solo los índices y eliminando los archivos originales, (c) procesar las imágenes directamente en la nube utilizando S3 o GCS, (d) utilizar consorcios de computación en la nube para la investigación (por ejemplo, NCI Cloud Resource). Fuente: Documentación oficial de TCGA / GDC [10].

Ideas para ampliar

  • Incrustación a nivel de diapositiva: Agregar las incrustaciones de los mosaicos mediante agrupación con atención (por ejemplo, ABMIL o GigaPath) para obtener incrustaciones a nivel de diapositiva, lo que permite la búsqueda y clasificación a nivel de diapositiva. Similar al enfoque de Geneformer en la parte 12.
  • Fusión multimodal: Combinar las imágenes WSI y los datos clínicos (edad, estadio del tumor, mutación) mediante fusión tardía para reordenar los resultados de la búsqueda.
  • Predicción de la supervivencia: Agregar los resultados de los casos similares encontrados para estimar el pronóstico de los nuevos casos.
  • Búsqueda federada: Compartir solo las incrustaciones, sin extraer los datos originales de varios hospitales, para crear un índice de búsqueda federado de casos similares.
  • Complemento para QuPath: Permitir que el patólogo seleccione un área específica en QuPath, realice una búsqueda automática y muestre una ventana emergente con casos similares.
  • Reordenamiento con lenguaje natural: Reordenar los 100 mejores resultados de la búsqueda de imágenes según su relevancia con la consulta en lenguaje natural.

Próximo capítulo

  • Capítulo 03 cellpose-sam-segmentation: Segmentación de células individuales a partir de los mosaicos WSI de este capítulo, combinada con incrustaciones a nivel celular.
  • Capítulo 10 med-llm-reproduction: Marco de trabajo para reproducir las pruebas de referencia de CONCH y PLIP.
  • Capítulo 12 single-cell-perturbation: Integración de imágenes de patología y datos de células individuales (con transcriptómica espacial).
  • Capítulo 14 bio-mcp-agent: Exponer la búsqueda de patología como una herramienta MCP para permitir la ejecución autónoma de la tarea: "Encuentra casos pasados similares a este".

Referencias

  1. Chen RJ, Ding T, Lu MY, et al. "Towards a general-purpose foundation model for computational pathology (UNI)." Nature Medicine 2024. https://www.nature.com/articles/s41591-024-02857-3
  2. Vorontsov E, Bozkurt A, Casson A, et al. "Un modelo base para la patología computacional de grado clínico (Virchow)." Nature Medicine 2024. https://www.nature.com/articles/s41591-024-03141-0 / Información sobre la siguiente versión de Virchow2: https://huggingface.co/paige-ai/Virchow2
  3. Huang Z, Bianchi F, Yuksekgonul M, et al. "Un modelo base visual-lingüístico para el análisis de imágenes de patología utilizando datos de Twitter médico (PLIP)." Nature Medicine 2023. https://www.nature.com/articles/s41591-023-02504-3
  4. Lu MY, Chen B, Williamson DFK, et al. "Un modelo base visual-lingüístico para la patología computacional (CONCH)." Nature Medicine 2024. https://www.nature.com/articles/s41591-024-02856-4
  5. Xu H, Usuyama N, Bagga J, et al. "Un modelo base para el análisis de imágenes de patología digital de diapositivas completas, basado en datos del mundo real (GigaPath)." Nature 2024. https://www.nature.com/articles/s41586-024-07441-w
  6. Problemas de OpenSlide en GitHub: https://github.com/openslide/openslide/issues
  7. Documentación de TIAToolbox: https://tia-toolbox.readthedocs.io/
  8. Wiki de FAISS (optimización de IVF-PQ): https://github.com/facebookresearch/faiss/wiki
  9. CONCH de MahmoodLab en Hugging Face: https://huggingface.co/MahmoodLab/CONCH
  10. Portal de datos TCGA / GDC: https://portal.gdc.cancer.gov/
  11. Conjunto de datos del desafío Camelyon16: https://camelyon16.grand-challenge.org/
  12. QuPath (visor de patología de código abierto): https://qupath.github.io/
  13. Macenko M et al. "A method for normalizing histology slides for quantitative analysis." ISBI 2009.
  14. Vahadane A et al. "Structure-Preserving Color Normalization and Sparse Stain Separation for Histological Images." IEEE TMI 2016.
  15. torchstain (implementación de Macenko): https://github.com/EIDOSLAB/torchstain
  16. FAISS GitHub: https://github.com/facebookresearch/faiss
  17. HuggingFace paige-ai/Virchow2: https://huggingface.co/paige-ai/Virchow2
  18. HuggingFace MahmoodLab UNI: https://huggingface.co/MahmoodLab/UNI
  19. DICOM WSI (estándar WG 26): https://dicom.nema.org/
  20. staintools (alternativa para la normalización del color): https://github.com/Peter554/StainTools

💬 Preguntas y comentarios

0 comentarios

Puedes publicar sin iniciar sesión. Los comentarios de invitados no pueden editarse ni eliminarse después.

0/2000

Cargando...