Caso de estudio · CUDA · Computer Vision
CudaSift: keypoints en GPU que conservan los mejores, no los primeros
Forké CudaSift para seleccionar keypoints por score en vez de truncar por orden de detección, con un tope opcional por octava y un wrapper de Python con pybind11.
Top-k
por score, no los primeros encontrados
Tope por octava
entre todas las octavas, no solo las primeras
3–5 ms
de overhead del filtrado
SIFT sigue siendo uno de los detectores de features más usados en visión por computador, y CudaSift es una implementación en CUDA que lo corre en GPU a gran velocidad. Pero al integrarlo me topé con un detalle que degrada la calidad de los keypoints: cuando una imagen genera más respuestas que el buffer, CudaSift se queda con las primeras que encuentra y descarta el resto.
Forké CudaSift para arreglar eso: ahora conserva los keypoints más fuertes por score, reparte el presupuesto entre octavas de forma opcional, y se puede llamar desde Python con un wrapper de pybind11. El detector original es de Mårten Björkman; mis cambios están sobre ese trabajo.
El problema: el buffer trunca por orden, no por calidad#
CudaSift reserva un buffer fijo de maxPts keypoints. La detección añade candidatos con atomicInc y, cuando el buffer se llena, el kernel fija el índice de escritura al último slot: cada candidato extra a partir de ahí sobreescribe el mismo último hueco. El resultado es que el conjunto que se devuelve son los keypoints que se escribieron primero (orden de detección), y un feature fuerte se puede perder solo por detectarse tarde.
Para un front end de matching o de SLAM esto importa: querés los keypoints más repetibles y con más respuesta, no un subconjunto arbitrario sesgado por el orden de barrido del kernel.
Mi contribución: selección por score, tope por octava y wrapper#
Separé la capacidad de trabajo del tope final. Ahora el dispositivo almacena hasta maxWorkPts = 4 × maxPts candidatos sin sobreescribir (lo que excede se descarta, no pisa el último slot). Después de la detección, cada octava se filtra en host a los top-k candidatos por abs(sharpness), la respuesta DoG de extracción, con nth_element y sort, y se devuelven solo esos. El modo legacy original sigue disponible con un flag.
Añadí un tope opcional por octava: el presupuesto de maxPts se reparte de forma pareja entre octavas (el resto va a las más finas), así cada escala queda representada en vez de que una sola octava llene el buffer. Y escribí un wrapper de pybind11 (cudasift_py) que recibe una imagen NumPy y devuelve keypoints, escalas, orientaciones, scores y descriptores como arrays, sin pasar por el ejecutable de C++.
La idea es simple: dejar que la detección encuentre de más, y después quedarse con lo mejor por score en vez de con lo primero. Tres modos: legacy, score global y score por octava.
Cómo se usa y dónde lo probé#
Desde Python es una sola llamada: cudasift_py.extract(img, ...) con los flags use_score_filter y use_per_octave_cap. Devuelve keypoints (N,2), escalas, orientaciones, scores (la sharpness de extracción) y descriptores (N,128). La arquitectura de CUDA es parametrizable desde CMake, así que compila para la GPU que tengas.
Lo probé dentro de EndoCartoScope, el sistema de SLAM en colonoscopia donde investigo, como extractor del front end. Conservar los keypoints más fuertes y repartirlos entre escalas le da al matcher una base más estable que un truncado por orden.
Selección top-k por sharpness en vez de truncado por orden de detección.
Tope opcional por octava para cubrir todas las escalas.
Wrapper de pybind11: imagen NumPy entra, keypoints y descriptores salen.
Stack#
Preguntas frecuentes#
¿Qué es CudaSift?
Una implementación en CUDA del detector y descriptor SIFT, originalmente de Mårten Björkman, que corre en GPU a gran velocidad. Forké ese repo para mejorar la selección de keypoints y añadir un wrapper de Python.
¿Qué cambiaste exactamente?
Tres cosas: la selección de keypoints por score (top-k por sharpness) en vez de truncar por orden de detección, un tope opcional por octava para cubrir todas las escalas, y un wrapper de pybind11 para llamarlo desde Python con NumPy.
¿Por qué importa quedarse con los keypoints por score?
Porque el buffer original, al llenarse, sobreescribe el último slot con cada candidato extra: te quedás con los primeros que se detectan, no con los mejores. Para matching o SLAM querés los más fuertes y repetibles.
¿Se puede usar el comportamiento original?
Sí. El modo legacy sigue disponible con un flag (use_score_filter=False), y también hay un modo de score global sin tope por octava.
¿Hablamos de visión por computador?
Si trabajás en CUDA, matching o cualquier problema duro de visión, escribime.