Implementamos un tutorial avanzado de Kornia de un extremo a otro y demostramos cómo se puede construir una visión por computadora moderna y diferenciable completamente en PyTorch. Comenzamos construyendo canales de aumento sincronizados y acelerados por GPU para imágenes, máscaras y puntos clave, luego pasamos a una geometría diferenciable optimizando una homografía directamente a través del descenso de gradiente. También mostramos cómo la coincidencia de características aprendidas con LoFTR se integra con RANSAC de Kornia para estimar homografías robustas y producir una salida unida simple, incluso en condiciones restringidas o fuera de línea. Finalmente, ponemos estas ideas en práctica entrenando una CNN liviana en CIFAR-10 utilizando los aumentos de GPU de Kornia, destacando cómo los canales de visión de grado de investigación se traducen naturalmente en sistemas de aprendizaje. Consulta los CÓDIGOS COMPLETOS aquí.
importar sistema operativo, matemáticas, tiempo, aleatorio, urllib.request desde clases de datos importar clase de datos desde escribir importar tupla importar sistema, subproceso def pip_install(pkgs): subprocess.check_call([sys.executable, "-m", "pip", "install", "-q"] + pkgs) pip_install([ "kornia==0.8.2", "torch", "torchvision", "matplotlib", "numpy", "opencv-python-headless" ]) importar numpy como np importar antorcha importar antorcha.nn como nn importar antorcha.nn.funcional como F importar torchvision importar torchvision.transforms.funcional como TF importar matplotlib.pyplot como plt importar cv2 importar kornia importar kornia.augmentation como K importar kornia.geometry.transform como KG desde kornia.geometry.ransac importar RANSAC de kornia.feature import LoFTR torch.manual_seed(0) np.random.seed(0) random.seed(0) print("Antorcha:", torch.__version__) print("Kornia:", kornia.__version__) print("Dispositivo:", dispositivo)
Comenzamos configurando un entorno totalmente reproducible, instalando Kornia y sus dependencias principales para garantizar que la visión por computadora diferenciable y acelerada por GPU se ejecute sin problemas en Google Colab. Luego importamos y organizamos PyTorch, Kornia y las bibliotecas de soporte, estableciendo una base limpia para los flujos de trabajo de geometría, aumento y coincidencia de características. Configuramos la semilla aleatoria y seleccionamos el dispositivo informático disponible para que todos los experimentos posteriores sigan siendo deterministas, depurables y conscientes del rendimiento. Consulta los CÓDIGOS COMPLETOS aquí.
def to_tensor_img_uint8(img_bgr_uint8: np.ndarray) -> torch.Tensor: img_rgb = cv2.cvtColor(img_bgr_uint8, cv2.COLOR_BGR2RGB) t = torch.from_numpy(img_rgb).permute(2, 0, 1).float() / 255.0 return t.unsqueeze(0) def show(img_t: torch.Tensor, title: str = "", max_size: int = 900): x = img_t.detach().float().cpu().clamp(0, 1) if x.shape[1]== 1: x = x.repetir(1, 3, 1, 1) x = x[0].permute(1, 2, 0).numpy() h, w = x.shape[:2] escala = min(1.0, max_size / max(h, w)) si escala < 1.0: x = cv2.resize(x, (int(w * escala), int(h * escala)), interpolación=cv2.INTER_AREA) plt.figure(figsize=(7, 5)) plt.imshow(x) plt.axis("off") plt.title(title) plt.show() def show_mask(mask_t: torch.Tensor, title: str = ""): x = mask_t.detach().float().cpu().clamp(0, 1)[0, 0].numpy() plt.figure(figsize=(6, 4)) plt.imshow(x) plt.axis("off") plt.title(title) plt.show() def descargar(url: str, ruta: str): os.makedirs(os.path.dirname(ruta), exist_ok=True) si no os.path.exists(ruta): urllib.request.urlretrieve(url, ruta) def safe_download(url: str, ruta: str) -> bool: intente: os.makedirs(os.path.dirname(ruta), exist_ok=True) si no os.path.exists(ruta): urllib.request.urlretrieve(url, ruta) devuelve Verdadero excepto Excepción como e: print("Error en la descarga:", e) devuelve Falso def make_grid_mask(h: int, w: int, cell: int = 32) -> torch.Tensor: yy, xx = torch.meshgrid(torch.arange(h), torch.arange(w), indexing="ij") m = (((yy // celda) % 2) ^ ((xx // celda) % 2)).float() return m.unsqueeze(0).unsqueeze(0) def draw_matches(img0_rgb: np.ndarray, img1_rgb: np.ndarray, pts0: np.ndarray, pts1: np.ndarray, max_draw: int = 200) -> np.ndarray: h0, w0 = img0_rgb.shape[:2] h1, w1 = img1_rgb.shape[:2] out = np.zeros((max(h0, h1), w0 + w1, 3), dtype=np.uint8) out[:h0, :w0] = img0_rgb out[:h1, w0:w0+w1] = img1_rgb n = min(len(pts0), len(pts1), max_draw) si n == 0: devolver idx = np.random.choice(len(pts0), size=n, replace=False) if len(pts0) > n else np.arange(n) para i en idx: x0, y0 = pts0[i] x1, y1 = pts1[i] x1_shift = x1 + w0 p0 = (int(round(x0)), int(round(y0))) p1 = (int(round(x1_shift)), int(round(y1))) cv2.circle(out, p0, 2, (255, 255, 255), -1, lineType=cv2.LINE_AA) cv2.circle(out, p1, 2, (255, 255, 255), -1, lineType=cv2.LINE_AA) cv2.line(out, p0, p1, (255, 255, 255), 1, lineType=cv2.LINE_AA) devuelve salida def normalize_img_for_loftr(img_rgb01: torch.Tensor) -> torch.Tensor: si img_rgb01.shape[1]== 3: devolver kornia.color.rgb_to_grayscale(img_rgb01) devolver img_rgb01
Definimos un conjunto de utilidades auxiliares reutilizables para la conversión de imágenes, visualización, descarga segura de datos y generación de máscaras sintéticas, manteniendo el canal de visión limpio y modular. También implementamos una visualización sólida y ayudas de coincidencia que nos permiten inspeccionar imágenes aumentadas, máscaras y correspondencias LoFTR directamente durante la experimentación. Normalizamos las entradas de imágenes a los formatos tensoriales exactos esperados por Kornia y LoFTR, asegurando que toda la geometría posterior y los componentes de coincidencia de características funcionen de manera consistente y correcta. Consulta los CÓDIGOS COMPLETOS aquí.
imprimir("n[1]Aumentos diferenciables: imagen + máscara + puntos clave") B, C, H, W = 1, 3, 256, 384 img = torch.rand(B, C, H, W, dispositivo=dispositivo) máscara = make_grid_mask(H, W, cell=24).to(dispositivo) kps = torch.tensor([[ [40.0, 40.0], [W – 50.0, 50,0], [W * 0,6, H * 0,8], [W * 0,25, H * 0,65], ]], dispositivo=dispositivo) aug = K.AugmentationSequential( K.RandomResizedCrop((224, 224), escala=(0,6, 1,0), ratio=(0,8, 1,25), p=1,0), K.RandomHorizontalFlip(p=0.5), K.RandomRotation(grados=18.0, p=0.7), K.ColorJiggle(0.2, 0.2, 0.2, 0.1, p=0.8), data_keys=["input", "mask", "keypoints"], Same_on_batch=True ).to(dispositivo) img_aug, mask_aug, kps_aug = aug(img, máscara, kps) print("imagen:", tuple(img.shape), "->", tuple(img_aug.shape)) print("máscara:", tuple(máscara.shape), "->", tuple(mask_aug.shape)) print("kps:", tuple(kps.shape), "->", tuple(kps_aug.shape)) print("Ejemplo de puntos clave (antes -> después):") print(torch.cat([kps[0], kps_aug[0]], dim=1)) show(img, "Original (sintético)") show_mask(mask, "Máscara original (sintético)") show(img_aug, "Aumentado (sincronizado)") show_mask(mask_aug, "Máscara aumentada (sincronizado)")
Construimos un canal de aumento sincronizado y totalmente diferenciable que aplica las mismas transformaciones geométricas a imágenes, máscaras y puntos clave en la GPU. Generamos datos sintéticos para demostrar claramente cómo se preserva la coherencia espacial en todas las modalidades y al mismo tiempo introducimos una variabilidad realista mediante el recorte, la rotación, la inversión y la fluctuación del color. Visualizamos los resultados del antes y el después para verificar que las imágenes aumentadas, las máscaras de segmentación y los puntos clave permanezcan perfectamente alineados después de la transformación. Consulta los CÓDIGOS COMPLETOS aquí.
imprimir("n[2]Alineación de homografía diferenciable por optimización") base = torch.rand(1, 1, 240, 320, dispositivo=dispositivo) show(base, "Imagen base (escala de grises)") true_H_px = torch.eye(3, dispositivo=dispositivo).unsqueeze(0) true_H_px[:, 0, 2] = 18.0 true_H_px[:, 1, 2] = -12.0 true_H_px[:, 0, 1] = 0.03 true_H_px[:, 1, 0] = -0.02 true_H_px[:, 2, 0] = 1e-4 true_H_px[:, 2, 1] = -8e-5 objetivo = KG.warp_perspective(base, true_H_px, dsize=(base.shape[-2], base.shape[-1]), align_corners=True) show(target, "Target (base deformada por homografía verdadera)") p = torch.zeros(1, 8, dispositivo=dispositivo, require_grad=True) def params_to_H(p8: torch.Tensor) -> torch.Tensor: Bp = p8.shape[0]Hm = antorcha.ojo(3, dispositivo=p8.dispositivo).unsqueeze(0).repeat(Bp, 1, 1) Hm[:, 0, 0] = 1.0 + p8[:, 0] Hm[:, 0, 1] = p8[:, 1] Hm[:, 0, 2] = p8[:, 2] Hm[:, 1, 0] = p8[:, 3] Hm[:, 1, 1] = 1.0 + p8[:, 4] Hm[:, 1, 2] = p8[:, 5] Hm[:, 2, 0] = p8[:, 6] Hm[:, 2, 1] = p8[:, 7] return Hm opt = torch.optim.Adam([p], lr=0.08) pérdidas =[]para el paso en el rango (120): opt.zero_grad(set_to_none=True) H_est = params_to_H(p) pred = KG.warp_perspective(base, H_est, dsize=(base.shape[-2], base.shape[-1]), align_corners=True) loss_photo = (pred – target).abs().mean() loss_reg = 1e-3 * (p ** 2).mean() pérdida = loss_photo + loss_reg pérdida.backward() opt.step() pérdidas.append(loss.item()) print("Pérdida final:", pérdidas[-1]) plt.figure(figsize=(6,4)) plt.plot(losses) plt.title("Pérdida de optimización de homografía") plt.xlabel("paso") plt.ylabel("loss") plt.show() H_est_final = params_to_H(p.detach()) pred_final = KG.warp_perspective(base, H_est_final, dsize=(base.shape[-2], base.shape[-1]), align_corners=True) show(pred_final, "Warp recuperado (optimizado)") show((pred_final – target).abs(), "Error Abs (recuperado vs objetivo)") print("True H (píxel):n", true_H_px.squeeze(0).detach().cpu().numpy()) print("Est H:n", H_est_final.squeeze(0).detach().cpu().numpy())
Demostramos que la alineación geométrica puede tratarse como un problema de optimización diferenciable recuperando directamente una homografía mediante el descenso de gradiente. Primero generamos una imagen objetivo deformando una imagen base con una homografía conocida y luego aprendemos los parámetros de transformación minimizando la pérdida de reconstrucción fotométrica con regularización. Además, visualizamos el mapa de error y deformación optimizado para confirmar que la homografía estimada coincide estrechamente con la transformación de la verdad del terreno. Consulta los CÓDIGOS COMPLETOS aquí.
imprimir("n[3]Coincidencia LoFTR + homografía RANSAC + costura (403-safe)") data_dir = "/content/kornia_demo" os.makedirs(data_dir, exist_ok=True) img0_path = os.path.join(data_dir, "img0.png") img1_path = os.path.join(data_dir, "img1.png") ok0 = safe_download( "https://raw.githubusercontent.com/opencv/opencv/master/samples/data/graf1.png", img0_path ) ok1 = safe_download( "https://raw.githubusercontent.com/opencv/opencv/master/samples/data/graf3.png", img1_path ) si no (ok0 y ok1): print("⚠️ Usando imágenes alternativas sintéticas (sin red/bloqueadas) descargas)") base_rgb = torch.rand(1, 3, 480, 640, dispositivo=dispositivo) H_syn = torch.tensor([[ [1.0, 0.05, 40.0], [-0.03, 1.0, 25.0], [1e-4, -8e-5, 1.0] ]], dispositivo=dispositivo) t0 = base_rgb t1 = KG.warp_perspective(base_rgb, H_syn, dsize=(480, 640), align_corners=True) img0_rgb = (t0[0].permute(1,2,0).detach().cpu().numpy() * 255).astype(np.uint8) img1_rgb = (t1[0].permute(1,2,0).detach().cpu().numpy() * 255).astype(np.uint8) más: img0_bgr = cv2.imread(img0_path, cv2.IMREAD_COLOR) img1_bgr = cv2.imread(img1_path, cv2.IMREAD_COLOR) si img0_bgr es Ninguno o img1_bgr es Ninguno: elevar RuntimeError("Error al cargar las imágenes descargadas.") img0_rgb = cv2.cvtColor(img0_bgr, cv2.COLOR_BGR2RGB) img1_rgb = cv2.cvtColor(img1_bgr, cv2.COLOR_BGR2RGB) t0 = to_tensor_img_uint8(img0_bgr).to(dispositivo) t1 = to_tensor_img_uint8(img1_bgr).to(dispositivo) show(t0, "Imagen 0") show(t1, "Imagen 1") g0 = normalize_img_for_loftr(t0) g1 = normalize_img_for_loftr(t1) loftr = LoFTR(pretrained="outdoor").to(device).eval() con torch.inference_mode(): correspondencias = loftr({"image0": g0, "image1": g1}) mkpts0 = correspondencias["keypoints0"] mkpts1 = correspondencias["keypoints1"] mconf = correspondencias.get("confidence", Ninguno) print("Coincidencias sin formato:", mkpts0.shape[0]) si mkpts0.shape[0]< 8: aumente RuntimeError ("Muy pocas coincidencias para estimar la homografía") si mconf no es Ninguno: mconf = mconf.detach() topk = min(2000, mkpts0.shape[0]) idx = torch.topk(mconf, k=topk, mayor=True).indices mkpts0 = mkpts0[idx] mkpts1 = mkpts1[idx] print("Se mantienen las mejores coincidencias:", mkpts0.shape[0]) ransac = RANSAC( model_type="homografía", inl_th=3.0, lote_size=4096, max_iter=10, confianza=0.999, max_lo_iters=5 ).to(dispositivo) con torch.inference_mode(): H01, inliers = ransac(mkpts0, mkpts1) print("Forma H estimada:", tupla(H01.shape)) print("Inliers:", int(inliers.sum().item()), "/", int(inliers.numel())) vis = draw_matches( img0_rgb, img1_rgb, mkpts0.detach().cpu().numpy(), mkpts1.detach().cpu().numpy(), max_draw=250 ) plt.figure(figsize=(10,5)) plt.imshow(vis) plt.axis("off") plt.title("LoFTR coincide (subconjunto)") plt.show() H01 = H01.unsqueeze(0) if H01.ndim == 2 else H01 warped0 = KG.warp_perspective(t0, H01, dsize=(t1.shape[-2], t1.shape[-1]), align_corners=True) cosido = torch.max(warped0, t1) show(warped0, "Imagen0 deformada en el marco Imagen1 (vía homografía RANSAC)") show(stitched, "Mezcla cosida simple (max)")
Realizamos una comparación de características aprendidas utilizando LoFTR para establecer correspondencias densas entre dos imágenes, al tiempo que garantizamos la solidez a través de un mecanismo de respaldo seguro en la red. Luego aplicamos el RANSAC de Kornia para estimar una homografía estable a partir de estas coincidencias y deformamos una imagen en el marco de coordenadas de la otra. Visualizamos las correspondencias y producimos un resultado cosido simple para validar la alineación geométrica de un extremo a otro. Consulta los CÓDIGOS COMPLETOS aquí.
imprimir("n[4]Mini bucle de entrenamiento con aumentos de Kornia (subconjunto rápido)") cifar = torchvision.datasets.CIFAR10(root="/content/data", train=True, download=True) num_samples = 4096 indices = np.random.permutation(len(cifar))[:num_samples] subset = torch.utils.data.Subset(cifar, indices.tolist()) def intercalar (lote): imgs =[]etiquetas =[]para im, y en lote: imgs.append(TF.to_tensor(im)) etiquetas.append(y) devuelve torch.stack(imgs, 0), torch.tensor(labels) loader = torch.utils.data.DataLoader( subset, batch_size=256, shuffle=True, num_workers=2, pin_memory=True, collate_fn=collate ) aug_train = K.ImageSequential( K.RandomHorizontalFlip(p=0.5), K.RandomAffine(grados=12.0, traducir=(0.08, 0.08), escala=(0.9, 1.1), p=0.7), K.ColorJiggle(0.2, 0.2, 0.2, 0.1, p=0.8), K.RandomGaussianBlur((3, 3), (0.1, 1.5), p=0.3), ).to(dispositivo) clase TinyCifarNet(nn.Module): def __init__(self, num_classes=10): super().__init__() self.conv1 = nn.Conv2d(3, 48, 3, padding=1) self.conv2 = nn.Conv2d(48, 96, 3, padding=1) self.conv3 = nn.Conv2d(96, 128, 3, padding=1) self.head = nn.Linear(128, num_classes) def forward(self, x): x = F.relu(self.conv1(x)) x = F.max_pool2d(x, 2) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, 2) x = F.relu(self.conv3(x)) x = x.mean(dim=(-2, -1)) return self.head(x) modelo = TinyCifarNet().to(dispositivo) opt = torch.optim.AdamW(model.parameters(), lr=2e-3, peso_decay=1e-4) model.train() t_start = time.time() corriendo =[]para ello, (xb, yb) en enumerate(loader): xb = xb.to(device, non_blocking=True) yb = yb.to(device, non_blocking=True) xb = aug_train(xb) logits = model(xb) loss = F.cross_entropy(logits, yb) opt.zero_grad(set_to_none=True) loss.backward() opt.step() running.append(loss.item()) if (it + 1) % 10 == 0: print(f"iter {it+1:03d}/{len(loader)} | loss {np.mean(running[-10:]):.4f}") if >= 39: break print("Hecho en", round(time.time() – t_start, 2), "seg") plt.figure(figsize=(6,4)) plt.plot(running) plt.title("Pérdida de entrenamiento (demostración rápida)") plt.xlabel("iteración") plt.ylabel("pérdida") plt.show() xb0, yb0 = next(iter(loader)) xb0 = xb0[:8].to(dispositivo) xbA = aug_train(xb0) def mosaico8(x): x = x.detach().cpu().clamp(0,1) grid = torchvision.utils.make_grid(x, nrow=4) return grid.permute(1,2,0).numpy() plt.figure(figsize=(10,5)) plt.imshow(tile8(xb0)) plt.axis("off") plt.title("Lote CIFAR (original)") plt.show() plt.figure(figsize=(10,5)) plt.imshow(tile8(xbA)) plt.axis("off") plt.title("Lote CIFAR (Kornia-aumentado en GPU)") plt.show() print("n✅ Tutorial completo.") print("Próximas ideas:") print("- Costuras emplumadas (máscaras suaves) en lugar de mezcla máxima.") print("- Comparar LoFTR vs DISK/LightGlue usando kornia.feature.") print("- Optimización de homografía multiescala + pérdidas SSIM/Charbonnier.")
Demostramos cómo los aumentos basados en GPU de Kornia se integran directamente en un ciclo de entrenamiento estándar aplicándolos sobre la marcha a un subconjunto del conjunto de datos CIFAR-10. Entrenamos una red convolucional liviana de extremo a extremo, demostrando que los aumentos diferenciables generan una sobrecarga mínima al tiempo que mejoran la diversidad de datos. Por último, visualizamos lotes originales versus lotes aumentados para confirmar que las transformaciones se aplican de manera consistente y eficiente durante el aprendizaje.
En conclusión, demostramos que Kornia permite un flujo de trabajo de visión unificado donde el aumento de datos, el razonamiento geométrico, la coincidencia de características y el aprendizaje siguen siendo diferenciables y compatibles con GPU dentro de un único marco. Al combinar la coincidencia LoFTR, la estimación de homografía basada en RANSAC y la alineación impulsada por la optimización con un ciclo de entrenamiento práctico, mostramos cómo la visión clásica y el aprendizaje profundo se complementan entre sí en lugar de competir. Sirve como base para extendernos hacia uniones de grado de producción, una estimación de pose sólida o canales de capacitación a gran escala, y enfatizamos que los mismos patrones que usamos aquí se escalan naturalmente a sistemas de visión más complejos del mundo real.
Consulta los CÓDIGOS COMPLETOS aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 100.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.