Lección 5 de 18

Sistemas tradicionales

Jesús julio 20, 2026

Antes de llegar a LightGlue, vamos a construir un sistema de seguimiento de ROI usando los dos algoritmos clásicos de visión computacional más usados: ORB y SIFT. Esto nos dará el punto de referencia para comparar contra IA en la siguiente clase.

Indice de la lección

Objetivo

Al finalizar esta clase tendrás dos programas funcionando: uno que sigue un ROI seleccionado por el usuario usando ORB, y otro que hace lo mismo usando SIFT. Ambos comparten la misma estructura: selección de ROI al inicio, extracción de características de referencia, y comparación cuadro por cuadro contra el video en vivo.

Este ejercicio nos da una base real de comparación: en la siguiente clase usaremos exactamente el mismo concepto de seguimiento de ROI, pero con LightGlue, y podrás ver de primera mano las diferencias en robustez.

¿Qué es un descriptor de características?

Tanto ORB como SIFT funcionan bajo la misma idea general: encontrar puntos en una imagen que sean fáciles de reconocer de nuevo, esquinas, bordes marcados, texturas distintivas, y describir cada uno de esos puntos con un vector numérico llamado descriptor.

Si dos imágenes contienen el mismo objeto, aunque esté desplazado o rotado, varios de esos puntos tendrán descriptores muy similares en ambas imágenes. Y comparando descriptores podemos encontrar coincidencias (matches) entre la imagen de referencia y la imagen actual, y a partir de esas coincidencias inferir cómo se movió el objeto.

La diferencia entre ORB y SIFT está en cómo detectan esos puntos y cómo construyen el descriptor, lo cual afecta directamente su velocidad y su robustez.

ORB — Oriented FAST and Rotated BRIEF

ORB combina dos algoritmos: FAST para detectar puntos clave (esquinas) de forma muy rápida, y BRIEF para describir esos puntos como una cadena binaria (una secuencia de ceros y unos basada en comparaciones de intensidad entre píxeles vecinos).

Velocidad

Extremadamente rápido, ideal para aplicaciones en tiempo real con recursos limitados

Descriptor binario

Se compara con distancia Hamming, una operación muy ligera computacionalmente

Invariante a rotación

ORB calcula la orientación de cada punto y ajusta el descriptor en consecuencia

Sensible a escala e iluminación

Funciona peor cuando la imagen cambia mucho de tamaño o brillo respecto a la referencia

Libre de patentes

Fue creado específicamente como alternativa abierta a SIFT y SURF

Internamente, ORB combina tres ideas en un solo algoritmo:

1 – Detección de esquinas con FAST

FAST (Features from Accelerated Segment Test) recorre la imagen comparando cada píxel contra un anillo de píxeles vecinos. Si suficientes vecinos son notablemente más claros o más oscuros que el píxel central, ese punto se marca como esquina. Es una prueba muy simple (solo comparaciones de intensidad) lo que la hace extremadamente rápida de calcular para toda una imagen.

2 – Orientación mediante centroide de intensidad

Para que ORB sea invariante a rotación, cada punto detectado necesita una dirección de referencia. ORB calcula el centroide de intensidad de una región circular alrededor del punto, es decir, hacia dónde “se inclina” el brillo de esa zona, y usa el vector desde el centro del punto hacia ese centroide como su orientación. Así, sin importar cómo esté rotada la imagen, el punto siempre se describe relativo a su propia orientación.

3 – Descriptor con BRIEF orientado (rBRIEF)

BRIEF genera un descriptor binario comparando pares de píxeles alrededor del punto clave: si el píxel A es más brillante que el píxel B, ese bit es 1, si no, es 0. El problema es que BRIEF normal no es invariante a rotación, es decir, que los mismos pares de píxeles dan resultados distintos si la imagen rota. ORB resuelve esto “rotando” el patrón de comparación según la orientación calculada en el paso anterior, y además selecciona mediante una búsqueda los pares de píxeles que producen los bits más informativos y menos correlacionados entre sí. El resultado se llama rBRIEFo BRIEF rotado.

El resultado final es un descriptor binario de 256 bits por punto clave, que se compara entre imágenes usando distancia Hamming (contar cuántos bits son diferentes entre dos descriptores) una operación que el hardware ejecuta de forma casi instantánea incluso para miles de comparaciones.

blank

SIFT — Scale-Invariant Feature Transform

SIFT detecta puntos clave analizando la imagen en múltiples escalas,por lo que la procesa a distintos tamaños y niveles de desenfoque para encontrar puntos que sigan siendo reconocibles sin importar qué tan cerca o lejos esté la cámara. El descriptor que genera es un vector de 128 valores que describe la distribución de gradientes alrededor de cada punto.

Robusto a escala

Su principal ventaja: detecta el mismo punto, aunque la imagen esté más grande o más pequeña

Descriptor de punto flotante

Más rico en información, pero se compara con distancia euclidiana, más costosa que Hamming

Más lento que ORB

El análisis multiescala tiene un costo computacional considerable

Mejor ante cambios de iluminación

El descriptor normaliza los gradientes, lo que lo hace menos sensible a cambios de brillo

Patente expirada

Durante años requirió licencia para uso comercial; actualmente está disponible libremente en OpenCV

SIFT funciona en cuatro etapas claramente diferenciadas:

1 – Detección de extremos en el espacio de escala

Un detector de esquinas normal usa una sola “ventana” de tamaño fijo, lo que funciona bien para esquinas pequeñas, pero falla con esquinas grandes, y viceversa. SIFT resuelve esto generando la misma imagen a múltiples niveles de desenfoque (escalas), formando lo que se llama una pirámide gaussiana. Luego calcula la diferencia entre escalas consecutivas (la Diferencia de Gaussianas) una aproximación mucho más eficiente al Laplaciano de Gaussiana. Los puntos donde esa diferencia alcanza un máximo o mínimo local, comparando con sus 8 vecinos en la misma escala y 9 vecinos en cada escala adyacente (26 comparaciones en total), se marcan como candidatos a puntos clave, y quedan asociados a la escala donde mejor se representan.

2 – Localización precisa de puntos clave

No todos los candidatos detectados en el paso anterior son útiles. SIFT refina la posición de cada candidato usando una expansión de Taylor para ubicarlo con mayor precisión, y descarta dos tipos de puntos problemáticos: los de bajo contraste (su respuesta es menor a un umbral, contrastThreshold en OpenCV) y los que caen sobre bordes en lugar de esquinas reales — esto se detecta con una matriz Hessiana similar a la usada en el detector de esquinas de Harris, controlada por edgeThreshold. Lo que sobrevive a este filtrado son puntos de interés realmente robustos.

3 – Asignación de orientación

Para cada punto clave que sobrevivió, SIFT analiza los gradientes de intensidad en su vecindad y construye un histograma de orientaciones con 36 divisiones (cada una cubriendo 10 grados). La dirección dominante de ese histograma se asigna como la orientación del punto, y si hay otra dirección con al menos el 80% de la fuerza de la dominante, se crea un punto clave adicional con esa segunda orientación. Esto es lo que hace a SIFT invariante a rotación: el descriptor siempre se construye relativo a esta orientación.

4 – Construcción del descriptor

Finalmente, SIFT toma una vecindad alrededor del punto clave, la divide en una cuadrícula de 4×4 regiones, y para cada región calcula un histograma de orientaciones de gradiente con 8 direcciones. Esto da 4×4×8 = 128 valores, el descriptor final de cada punto clave, normalizado para reducir el efecto de cambios de iluminación.

¿Cuándo usar cada uno?

Más allá de la teoría, en un proyecto real la elección depende de las condiciones físicas de tu estación de visión. Te presento algunos de los escenarios donde cada algoritmo tiene sentido:

ORB - Rápido y ligero

Mejor cuando la cámara está fija y la distancia al objeto no cambia. Ideal para sistemas con hardware limitado o que requieren muchos cuadros por segundo.

SIFT - Robusto y preciso

Mejor cuando puede haber variaciones de distancia, ángulo o iluminación. El costo es una menor velocidad de procesamiento.
Usa ORB cuando... Usa SIFT cuando...
La cámara está fija y la distancia al objeto no cambia entre ejecuciones
La distancia entre la cámara y la pieza puede variar de una ejecución a otra
Necesitas el mayor número de cuadros por segundo posible, por ejemplo en líneas de producción de alta velocidad
La iluminación del entorno cambia a lo largo del día o entre turnos
El hardware es limitado, una PC industrial con procesador de bajo poder o un equipo embebido
Tienes margen de procesamiento, no es una línea que requiera el máximo de velocidad
La iluminación del área está controlada y es constante
El objeto puede presentarse rotado en ángulos más variados
El objeto a seguir mantiene una orientación similar, con solo pequeñas rotaciones
La precisión del seguimiento importa más que la velocidad, por ejemplo, en estaciones de inspección o calibración
En la práctica

Muchos sistemas industriales con cámara fija y condiciones controladas funcionan perfectamente con ORB, por lo que no siempre se necesita la robustez extra de SIFT, y de hecho, la diferencia en velocidad es notable cuando se procesan muchos cuadros por segundo, por lo que SIFT vale la pena cuando las condiciones reales no son tan predecibles.

Más adelante, veremos cómo LightGlue cambia esta decisión: ya que nos ofrece una robustez mayor a SIFT con una velocidad más cercana a ORB cuando se ejecuta en GPU.

Estructura del programa

Ambos programas, el de ORB y el de SIFT, siguen exactamente la misma estructura, solo cambia el detector y el matcher utilizado:

1 - Inicializar la cámara

concv2.CAP_DSHOW, el backend recomendado para cámaras USB en Windows

2 - Capturar un primer cuadro

y permitir al usuario seleccionar el ROI con el mouse

3 - Extraer características de referencia

dentro del ROI seleccionado

4 - En cada cuadro nuevo

extraer características del cuadro completo y buscar coincidencias contra la referencia

5 - Dibujar las coincidencias

encontradas para visualizar el seguimiento en tiempo real

Seguimiento con ORB

Este programa selecciona el ROI, extrae los descriptores ORB de referencia y luego, cuadro por cuadro, busca coincidencias usando un BFMatcher con distancia Hamming, la distancia adecuada para descriptores binarios como los de ORB. Lo dividimos en cuatro bloques.

  import cv2 import numpy as np import sys captura = cv2.VideoCapture(0, cv2.CAP_DSHOW) if not captura.isOpened(): print("Error: no se pudo abrir la cámara") sys.exit() exito, cuadroInicial = captura.read() roi = cv2.selectROI("Selecciona el ROI - ENTER para confirmar", cuadroInicial, False) cv2.destroyWindow("Selecciona el ROI - ENTER para confirmar") x, y, ancho, alto = roi if ancho == 0 or alto == 0: print("Error: no se seleccionó un ROI válido") sys.exit() imagenReferencia = cuadroInicial[y:y+alto, x:x+ancho] imagenReferenciaGris = cv2.cvtColor(imagenReferencia, cv2.COLOR_BGR2GRAY)
Qué hace este bloque
 
  • import sys nos da acceso a sys.exit(), la función que usamos para terminar el programa de forma segura cuando ocurre un error.

  • cv2.VideoCapture(0, cv2.CAP_DSHOW) abre la cámara con índice 0 usando el backend DirectShow, recomendado en Windows para evitar demoras al iniciar.

  • captura.isOpened() verifica que la cámara respondió correctamente antes de continuar, si falla, el programa termina con sys.exit().

  • captura.read() devuelve dos valores: exito (booleano) y cuadroInicial (la imagen capturada). Usamos este primer cuadro para seleccionar el ROI.

  • cv2.selectROI(...) abre una ventana donde el usuario dibuja un rectángulo con el mouse. Devuelve una tupla (x, y, ancho, alto) con la posición y tamaño del rectángulo.

  • La validación ancho == 0 or alto == 0 detecta si el usuario cerró la ventana sin seleccionar nada, en ese caso, ambos valores son cero.

  • cuadroInicial[y:y+alto, x:x+ancho] recorta la imagen usando slicing, dejando solo la región seleccionada en imagenReferencia.

  • cv2.cvtColor(..., cv2.COLOR_BGR2GRAY) convierte la imagen a escala de grises, ORB trabaja sobre intensidad de píxeles, no necesita color.
  detectorOrb = cv2.ORB_create(nfeatures=1000) keypointsRef, descriptoresRef = detectorOrb.detectAndCompute(imagenReferenciaGris, None) if descriptoresRef is None: print("Error: no se encontraron características en el ROI") sys.exit() matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
Qué hace este bloque
  • cv2.ORB_create(nfeatures=1000) crea el detector ORB, limitado a un máximo de 1000 puntos clave por imagen, suficiente para seguimiento sin saturar el procesamiento.

  • detectAndCompute(imagen, None) hace dos cosas en una llamada: detecta los puntos clave (keypointsRef) y calcula su descriptor binario (descriptoresRef). El segundo argumento None indica que no se usa una máscara para limitar dónde buscar.

  • La validación descriptoresRef is None detecta si el ROI seleccionado era una zona sin textura suficiente, en ese caso ORB no encuentra ningún punto clave.

  • cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) crea el comparador de descriptores. NORM_HAMMING es la distancia correcta para descriptores binarios como los de ORB. crossCheck=True exige que la coincidencia sea mutua, el punto A debe ser el mejor candidato para B, y B el mejor candidato para A, lo que reduce falsos positivos.
  while True: #dentro del while exito, cuadroActual = captura.read() if not exito: print("Error: no se pudo leer el cuadro de la cámara") break cuadroGris = cv2.cvtColor(cuadroActual, cv2.COLOR_BGR2GRAY) keypointsActual, descriptoresActual = detectorOrb.detectAndCompute(cuadroGris, None) cuadroVisualizado = cuadroActual.copy() if descriptoresActual is not None and len(descriptoresActual) > 0: coincidencias = matcher.match(descriptoresRef, descriptoresActual) coincidencias = sorted(coincidencias, key=lambda m: m.distance) mejoresCoincidencias = coincidencias[:30] cuadroVisualizado = cv2.drawMatches( imagenReferencia, keypointsRef, cuadroActual, keypointsActual, mejoresCoincidencias, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) cv2.putText(cuadroVisualizado, f"Coincidencias: {len(mejoresCoincidencias)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("Seguimiento ORB", cuadroVisualizado) if cv2.waitKey(1) & 0xFF == ord('q'): break
Qué hace este bloque
  • El bucle while True se repite indefinidamente, lee un cuadro nuevo de la cámara en cada iteración hasta que se presione q.

  • detectAndCompute se aplica ahora sobre el cuadro completo de la cámara, generando keypointsActual y descriptoresActual para ese instante.

  • matcher.match(descriptoresRef, descriptoresActual) compara cada descriptor de referencia contra todos los del cuadro actual y devuelve la mejor coincidencia para cada uno, junto con su distancia.

  • sorted(..., key=lambda m: m.distance) ordena las coincidencias de menor a mayor distancia, las primeras son las más confiables.

  • coincidencias[:30] se queda solo con las 30 mejores, para no saturar la visualización con coincidencias débiles.

  • cv2.drawMatches(...) genera una imagen combinada mostrando la referencia y el cuadro actual lado a lado, con líneas conectando los puntos que coinciden. El flag NOT_DRAW_SINGLE_POINTS oculta los keypoints que no tienen coincidencia.

  • cv2.putText(...) escribe en la esquina superior izquierda el número de coincidencias encontradas, útil para evaluar qué tan bien está funcionando el seguimiento en tiempo real.

  • cv2.waitKey(1) & 0xFF == ord('q') espera 1 milisegundo por una tecla, si es q, se sale del bucle.
  captura.release() cv2.destroyAllWindows()
Qué hace este bloque
  • captura.release() libera la cámara para que otros programas puedan usarla.

  • cv2.destroyAllWindows() cierra todas las ventanas de OpenCV abiertas por el programa.

Seguimiento con SIFT

La estructura es idéntica a la de ORB. Los cambios son: usamos cv2.SIFT_create() como detector, cv2.NORM_L2 (distancia euclidiana) en el matcher porque los descriptores SIFT son vectores de punto flotante, y aplicamos knnMatch con prueba de razón (ratio test) en lugar de match simple. El bloque 1 y el bloque 4 son exactamente iguales que en ORB.

  import cv2 import numpy as np import sys captura = cv2.VideoCapture(0, cv2.CAP_DSHOW) if not captura.isOpened(): print("Error: no se pudo abrir la cámara") sys.exit() exito, cuadroInicial = captura.read() roi = cv2.selectROI("Selecciona el ROI - ENTER para confirmar", cuadroInicial, False) cv2.destroyWindow("Selecciona el ROI - ENTER para confirmar") x, y, ancho, alto = roi if ancho == 0 or alto == 0: print("Error: no se seleccionó un ROI válido") sys.exit() imagenReferencia = cuadroInicial[y:y+alto, x:x+ancho] imagenReferenciaGris = cv2.cvtColor(imagenReferencia, cv2.COLOR_BGR2GRAY)
  detectorSift = cv2.SIFT_create() keypointsRef, descriptoresRef = detectorSift.detectAndCompute(imagenReferenciaGris, None) if descriptoresRef is None: print("Error: no se encontraron características en el ROI") sys.exit() matcher = cv2.BFMatcher(cv2.NORM_L2) umbralRatio = 0.75
Qué hace este bloque
  • cv2.SIFT_create() crea el detector SIFT con su configuración por defecto, no requiere un límite de características como ORB.

  • detectAndCompute funciona igual que con ORB, pero aquí descriptoresRef es un arreglo de vectores de 128 valores en punto flotante, no cadenas binarias.

  • cv2.BFMatcher(cv2.NORM_L2) crea el comparador usando distancia euclidiana (L2), la métrica correcta para descriptores de punto flotante. Aquí no usamos crossCheck porque vamos a aplicar la prueba de razón en su lugar.

  • umbralRatio = 0.75 define el umbral que se usará en el bucle para decidir si una coincidencia es confiable, es el valor estándar recomendado en el paper de SIFT.
  while True: #dentro de while exito, cuadroActual = captura.read() if not exito: print("Error: no se pudo leer el cuadro de la cámara") break cuadroGris = cv2.cvtColor(cuadroActual, cv2.COLOR_BGR2GRAY) keypointsActual, descriptoresActual = detectorSift.detectAndCompute(cuadroGris, None) cuadroVisualizado = cuadroActual.copy() if descriptoresActual is not None and len(descriptoresActual) >= 2: coincidenciasKnn = matcher.knnMatch(descriptoresRef, descriptoresActual, k=2) mejoresCoincidencias = [] for par in coincidenciasKnn: if len(par) == 2: mejor, segundoMejor = par if mejor.distance < umbralRatio * segundoMejor.distance: mejoresCoincidencias.append(mejor) cuadroVisualizado = cv2.drawMatches( imagenReferencia, keypointsRef, cuadroActual, keypointsActual, mejoresCoincidencias, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) cv2.putText(cuadroVisualizado, f"Coincidencias: {len(mejoresCoincidencias)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("Seguimiento SIFT", cuadroVisualizado) if cv2.waitKey(1) & 0xFF == ord('q'): break
Qué hace este bloque
  • La lectura del cuadro y la detección de características son iguales que en ORB, solo que ahora usamos detectorSift.

  • matcher.knnMatch(descriptoresRef, descriptoresActual, k=2) es la diferencia clave: para cada descriptor de referencia, busca las 2 mejores coincidencias en el cuadro actual, no solo la mejor.

  • El bucle for par in coincidenciasKnn recorre cada par de candidatos. mejor es la coincidencia más cercana y segundoMejor la segunda más cercana.

  • La condición mejor.distance < umbralRatio * segundoMejor.distance es la prueba de razón: si el mejor candidato es claramente mejor que el segundo (su distancia es menor al 75% de la del segundo), se acepta como coincidencia válida. Si ambos están muy cerca, la coincidencia es ambigua y se descarta.

  • El resto: drawMatchesputTextimshow y la condición de salida es idéntico al programa de ORB.
  captura.release() cv2.destroyAllWindows()

¿Por qué SIFT no usa crossCheck?

crossCheck y knnMatch no son compatibles entre sí en OpenCV, crossCheck requiere match simple. Como con SIFT preferimos la prueba de razón (más efectiva para descriptores de punto flotante), usamos knnMatch sin crossCheck.

Limitaciones que vas a poder observar

Al probar ambos programas, vas a notar diferencias muy claras en cómo falla cada algoritmo, las cuales no van tan solo de que uno encuentre “más o menos” coincidencias, sino que fallan de formas distintas. Fallas que podemos mitigar con la implementación de LightGlue.

Comportamiento de ORB
  • Para el mismo objeto, encuentra menos coincidencias que SIFT en general.

  • Cuando apuntas la cámara a otra escena sin el objeto, sigue marcando gran cantidad de coincidencias, las cuales son falsos positivos ya que se encuentran sobre objetos que no tienen relación con el objeto original.

  • Con cambios fuertes de iluminación o sobreexposición, también aparecen gran cantidad de falsos positivos en lugar de simplemente perder coincidencias.

  • Si rotas el objeto 180°, las coincidencias reales se pierden casi por completo y aumentan los falsos positivos.
Comportamiento de SIFT
  • Para el mismo objeto, encuentra más coincidencias que ORB.

  • Cuando el objeto no está presente en la escena, simplemente no encuentra coincidencias o encuentra mucho menos que ORB, es decir que este no inventa matches con otros objetos.

  • Con cambios de iluminación tiende a perder coincidencias en lugar de generar alta cantidad de falsos positivos.

  • Si rotas el objeto 180°, las coincidencias del objeto original se mantienen.

  • Consume entre un 10% y un 20% más de CPU que ORB.

ORB

SIFT

Para comprobarlo tú mismo, prueba lo siguiente con ambos programas:

Apunta a otra escena

Apunta a otra escena sin el objeto, observa si ORB sigue mostrando líneas de coincidencia y hacia dónde.

Cambia la iluminación

Cambia la iluminación drásticamente, enciende una luz directa sobre el objeto y observa el comportamiento de cada uno.

Rota el objeto 180°

Rota el objeto 180° y compara cuántas coincidencias reales sobreviven en cada algoritmo,

Observa la fluidez

Observa la fluidez del video, SIFT debería notarse ligeramente más lento que ORB. También puedes abrir el administrador de tareas y ver el rendimiento del CPU.

Respuestas