Herramienta para comparar dos videos o imágenes de cámara trampa y proponer, a una persona revisora, los pares de fotogramas con más evidencia compartida para decidir si se trata del mismo individuo de Leopardus pardalis (u otro felino con patrón de rosetas). El sistema no decide la identidad: separa lo que la máquina puede medir de lo que sólo una persona puede juzgar.
"La tecnología estructura cómo los animales y los humanos comparten espacios, actúa como mediadora y 'conectora' de vidas a través del tiempo y el espacio. 'Facilita' formas de observar, medir, mover y matar, así como controlar, contener, conservar y cooperar con los animales. Configura las relaciones humanas con el mundo no humano, explorando a los animales no humanos como parientes, compañeros, alimento, transgresores, entretenimiento y herramientas." Tomado del libro "Compartiendo Espacios" para tener claro que eso que vemos con cámaras trampa genera también distanciamiento y una mirada desde el control, intrusiva y antropocéntrica.
¿Cómo preservar la alteridad radical de cada especie mientras sus individuos agencian dentro de redes amplias y complejas que no comprendemos? ¿Cómo y para qué realizar identificación de individuos sin simplificarlos a quasi-objetos sino como seres de una comunidad por venir?
Este repositorio hace parte de la tesis doctoral de Alejandro Duque en el programa D.A.C. (Diseño, Arte y Ciencia) de la Universidad Jorge Tadeo Lozano, con trabajo de campo en la Reserva MANAKAI.
Camera-trap re-identification tools usually collapse three different questions into one "similarity percentage": does a retrieval model relate the two images?, do the two images expose enough shared anatomy for a person to compare them responsibly?, and are they the same individual? Following the PF-ERI v2 framework (Shen, 2026), this repository separates those three layers explicitly. Version 3 replaces the earlier OpenCV-only heuristics with (1) MegaDetector v6 animal cropping, so that identical camera backgrounds cannot drive the match; (2) two frozen descriptors, MegaDescriptor-L-384 and DINOv2, aggregated over sampled video frames as retrieval support; (3) SIFT correspondences verified with MAGSAC++ as local pair evidence, reported with explicit failure states instead of silent zeros; and (4) a human review instrument (review_ready / not_review_ready / uncertain, structured reason codes, and a separate identity decision) appended to a CSV that can later calibrate — or refute — any automatic score. Controls on Creative-Commons ocelot camera-trap footage show the intended behaviour: two halves of the same video yield cosine ≈ 0.99 and 400–600 geometrically verified correspondences, while ocelots from different sites yield cosine ≈ 0.76–0.84 (the descriptors say "ocelot", not "this ocelot") and zero verified correspondences. No score in this repository is a calibrated probability of identity; identity remains a human decision.
# 1. Instalar (una sola vez; requiere conda — ver §4)
git clone https://github.com/alejoduque/ID_indv.git && cd ID_indv
conda create -n ocelot python=3.11 -y && conda activate ocelot
pip install -r requirements.txt
# 2. Comparar dos videos (o imágenes) de cámara trampa
python reid_compare.py videos/A.mov videos/B.mp4
# 3. Mirar la figura y decidir como persona revisora (se guarda en reviews.csv)
python reid_compare.py videos/A.mov videos/B.mp4 --review --reviewer tu_nombreSalida: outputs/A__B/review_figure.png (pares de fotogramas propuestos con sus correspondencias verificadas) y outputs/A__B/report.json. En consola verás tres capas: soporte de recuperación (descriptores), revisabilidad (evidencia local + pre-cribado) e identidad (siempre "decisión humana pendiente" hasta que la registres). Detalles y opciones en §5.
La primera ejecución descarga ~1.7 GB de pesos (MegaDescriptor, DINOv2, MegaDetector); después funciona sin conexión. Cada par de videos tarda ~25 s en un Mac Apple Silicon.
| v2 (baseline, OpenCV) | v3 (reid/ + reid_compare.py) |
|
|---|---|---|
| Recorte del animal | ninguno (el fondo de la cámara entraba en el matching) | MegaDetector v6 (pesos oficiales, inferencia con ultralytics) |
| Descriptores | SIFT/ORB sobre el fotograma completo | MegaDescriptor-L-384 + DINOv2 ViT-B/14, congelados, sobre el recorte |
| Emparejamiento de rosetas | por coordenadas absolutas en píxeles | correspondencias SIFT + prueba de Lowe + homografía MAGSAC++ |
| Resultado | "90-95 % de confianza – mismo individuo" (sin calibrar) | tres capas separadas: soporte de recuperación, revisabilidad, identidad (humana) |
| Fallos de medición | silenciados (score 0 o 100 %) | estados explícitos (no_detection, too_few_matches, ransac_failed, …) |
| Registro | ninguno | reviews.csv con la decisión de la persona revisora |
| Video | 12 fotogramas × 12 fotogramas con puntuación saturada | conjunto-a-conjunto: matriz coseno, media top-k, mejores pares por similitud × calidad |
Los scripts v2 se conservan con sus errores más graves corregidos (sección 8) para que los resultados anteriores puedan reinterpretarse.
Adaptado de PF-ERI v2 — Pair-level evidence admission separates retrieval support from human reviewability in wildlife re-identification (Shen, 2026; repositorio público). PF-ERI trabajó con lince euroasiático y cuatro revisores humanos; sus conclusiones son directamente aplicables a felinos con patrón.
soporte de recuperación → revisabilidad del par → verdad de identidad
(lo mide la máquina) (lo juzga una persona; (lo decide una persona,
la máquina pre-criba) sólo si el par es revisable)
| Capa | Pregunta | Quién responde | En este repositorio |
|---|---|---|---|
| 1. Soporte de recuperación | ¿Dos descriptores congelados e independientes relacionan estas dos fuentes? | la máquina | reid/descriptors.py, reid/compare.py (pairwise_support, rank_support) |
| 2. Revisabilidad del par | ¿Los dos fotogramas exponen suficiente anatomía compartida (mismo flanco, rosetas visibles, sin oclusión) para comparar responsablemente? | la persona revisora; la máquina sólo propone pares y pre-criba | reid/local_match.py, reviewability_prescreen, reid/review.py |
| 3. Verdad de identidad | ¿Es el mismo individuo? | la persona revisora, sólo cuando la capa 2 es review_ready |
campo identity en reviews.csv |
Tres resultados de PF-ERI que cambian cómo leer cualquier "score":
- Soporte de recuperación ≠ revisabilidad. En su cola externa, 494 de 637 pares sin soporte de descriptores fueron igualmente juzgados revisables por humanos. Un coseno bajo no significa "no mirar"; un coseno alto no significa "es el mismo".
- Lo que mejora en desarrollo puede fallar al transportarse. El modelo que añadía evidencia local (P5) ganó en validación cruzada y perdió de forma decisiva (Δ Brier = −0.20, IC 95 % completamente negativo) al ejecutarse congelado sobre datos externos. Por eso aquí ningún umbral se presenta como calibrado hasta que exista un conjunto de pares con decisión humana registrada (
reviews.csv). - El endpoint humano es parte del problema científico. El acuerdo binario entre revisores fue 53 % en desarrollo y >92 % después con un instrumento más claro. La revisabilidad es una medición producida conjuntamente por la imagen y por el instrumento de revisión, no una propiedad de la imagen.
Para la tesis, esta separación es también una postura: la máquina no "reconoce" al ocelote; ordena encuentros y devuelve a una persona la responsabilidad de mirar. La identidad individual queda como decisión situada, registrada con su razón y su incertidumbre, no como salida de un clasificador.
fuente A (video/imagen) fuente B (video/imagen)
│ 12 fotogramas uniformes │
▼ ▼
MegaDetector v6 ──► recorte del animal (fotogramas sin detección: excluidos y contados)
│ │
▼ ▼
calidad por recorte: píxeles, nitidez (var. Laplaciano), exposición/recorte de histograma
│ │
▼ ▼
MegaDescriptor-L-384 ─┐ ┌─ MegaDescriptor-L-384 CAPA 1
DINOv2 ViT-B/14 ──────┤ coseno ├─ DINOv2 ViT-B/14 matriz nA×nB por descriptor
└──────────┘ max · media top-k · mediana de máximos por fila
│
▼
selección de pares: similitud media × √(calidad_A · calidad_B), sin repetir fotogramas
│
▼
SIFT + Lowe 0.75 + homografía MAGSAC++ (6 px) ──► inliers, cobertura, estado CAPA 2
│
▼
pre-cribado: review_candidate | low_quality | no_pairs (+ razones)
│
▼
figura + report.json ──► --review: formulario en consola ──► reviews.csv CAPA 3
Detalles que importan:
- Recorte primero. Dos videos de la misma cámara comparten fondo, hojas, ramas y la franja de fecha/hora; sin recorte, SIFT "coincide" con el fondo aunque los animales sean distintos. Esa es la explicación más probable del "100 % SIFT, desviación 0" del análisis v2.
- Dos descriptores, no uno. Igual que PF-ERI, se exige acuerdo de MegaDescriptor y DINOv2. Con
--galleryel soporte se calcula por rangos (both_reciprocal/both_agreement/unsupported), que es la definición original y no depende de umbrales. - Evidencia local con verificación geométrica. Una correspondencia sólo cuenta si es distintiva (Lowe) y consistente con una transformación común (RANSAC). Los fallos se devuelven como categorías (
too_few_keypoints,too_few_matches,ransac_failed), no como cobertura 0. - Video como conjunto. Cada video es un conjunto de recortes; la similitud es una matriz, no un número. Se reporta el máximo (mejor par), la media de los k mejores (robusta a un fotograma casual) y la mediana de los máximos por fila (¿cuántos fotogramas de A tienen algún buen par en B?).
Probado en macOS (Apple M-series, aceleración MPS) con Python 3.11. torch no soporta aún Python ≥3.14 (el de Homebrew), así que se recomienda conda:
git clone https://github.com/alejoduque/ID_indv.git
cd ID_indv
conda create -n ocelot python=3.11 -y
conda activate ocelot
pip install -r requirements.txtPrimer uso: se descargan automáticamente los pesos de MegaDescriptor-L-384 (~1.2 GB, Hugging Face BVRA/MegaDescriptor-L-384), DINOv2 ViT-B/14 (~350 MB, timm) y MegaDetector MDV6-yolov9-c (~100 MB, Zenodo 15398270 → ~/.cache/megadetector/). Después todo corre sin conexión.
Sólo el baseline v2: pip install -r requirements-baseline.txt.
# Dos videos
python reid_compare.py videos/IMAG0033.AVI videos/IMAG0059.mp4
# Imagen fija contra video (o imagen contra imagen)
python reid_compare.py foto.jpg videos/IMAG0059.mp4
# Con galería: el soporte se calcula por rangos entre todas las fuentes de la carpeta (modo PF-ERI)
python reid_compare.py videos/A.mov videos/B.mp4 --gallery videos/ --top-k 5
# Con revisión humana al final (formulario en consola → reviews.csv)
python reid_compare.py videos/A.mov videos/B.mp4 --review --reviewer ad
# Opciones útiles
# --frames 20 más fotogramas por video (default 12)
# --pairs 5 más pares propuestos para revisión (default 3)
# --no-detector sin recorte (sólo si las imágenes ya vienen recortadas)
# --megadescriptor base --dinov2 small modelos más livianos
# --gray embeber en escala de grises (ver §6.3 antes de usarlo)
# --device cpuSalidas en outputs/<A>__<B>/:
review_figure.png— los pares propuestos (recorte A, recorte B, correspondencias verificadas en verde) y las matrices de similitud por descriptor.report.json— todo lo anterior en cifras: estados del detector, calidad por fotograma, agregados por descriptor, categoría de soporte, pares y su evidencia local, pre-cribado y sus razones, y"identity": "human_decision_pending".
Lectura de la consola:
CAPA 1 · Soporte de recuperación (descriptores congelados)
megadescriptor max=0.996 top5_mean=0.996 mediana_rowmax=0.989
dinov2 max=0.967 top5_mean=0.964 mediana_rowmax=0.936
categoría: both_agreement (umbrales NO calibrados)
CAPA 2 · Revisabilidad del par (pre-cribado automático)
review_candidate
par 1: A#162 (5.40s) vs B#61 (2.03s) | local=ok inliers=437 cobertura=0.310
CAPA 3 · Identidad: la decide la persona revisora (ver --review / reviews.csv)
Videos e imagen de prueba: Wikimedia Commons (ver créditos en §11). No hay verdad de identidad entre sitios; el control positivo es el mismo video partido en dos mitades.
| Caso | MegaDescriptor top-5 | DINOv2 top-5 | Evidencia local (SIFT+MAGSAC) |
|---|---|---|---|
| Positivo: mitades del mismo video (São Paulo) | 0.996 | 0.964 | ok, 395–624 inliers, cobertura 0.28–0.44 |
| Negativo: São Paulo vs Rubinéia (sitios distintos, ambos IR) | 0.760 | 0.836 | ransac_failed (4–5 inliers) / too_few_matches |
| Foto diurna a color (Rio Doce) vs video IR (Rubinéia) | 0.091 | 0.643 | too_few_matches |
Lectura: los descriptores globales dicen "esto es un ocelote con esta pose" (0.76–0.84 entre individuos de sitios distintos), y sólo la evidencia local verificada separa el mismo individuo del que no lo es. Es exactamente la separación entre capas de PF-ERI.
MDV6-yolov9-c está entrenado a 640 px. A 1280 px (el tamaño que PytorchWildlife fija por defecto) la confianza sobre un ocelote diurno nítido cayó de 0.94 a 0.05. reid/detect.py usa 640 para las variantes -c, 1280 para las -e, y reintenta a la otra escala si no hay detección. Además, se evitó la dependencia PytorchWildlife (arrastra yolov5, gradio y bioacústica, y falla al importar); los pesos oficiales se cargan directo con ultralytics.
MegaDescriptor cae a ≈0.09 entre una foto diurna a color y video IR nocturno; DINOv2 es menos sensible (0.64). Embeber en escala de grises reduce la brecha (0.09 → 0.33) pero produjo 0.93 entre la foto de Rio Doce y el video de São Paulo, que muestran flancos opuestos del cuerpo y no pueden ser evidencia de identidad; RANSAC dio 0 inliers en todos los pares. Conclusión provisional: en gris el descriptor global responde a pose y textura genérica. Se deja --gray como opción documentada, no por defecto, y se recomienda comparar color↔IR sólo con la evidencia local y la revisión humana.
DEFAULT_THRESHOLDS = {megadescriptor: 0.85, dinov2: 0.90} en reid/pipeline.py están entre el negativo (0.76/0.84) y el positivo (0.996/0.96) observados — con n = 1 en cada caso. Son marcadores de posición, etiquetados calibrated: false en cada reporte. El camino para calibrarlos es la sección 7.
Adaptado de la Tabla S3 de PF-ERI. Se registra con --review o programáticamente con reid.review.append_review.
| Campo | Valores | Notas |
|---|---|---|
reviewability |
review_ready · not_review_ready · uncertain |
¿Hay anatomía comparable? No es "¿es el mismo?" |
reason_codes |
opposite_side, no_overlap, partial_body, occlusion, blur, lighting, scale, pose, sufficient, other |
lista cerrada, separados por ; |
confidence |
low · medium · high |
certeza sobre la revisabilidad |
identity |
same · different · undetermined |
sólo se acepta si reviewability == review_ready |
identity_confidence |
low · medium · high |
|
technical_problem |
yes · no |
la revisión no pudo completarse (excluir del análisis) |
| contexto automático | megadescriptor_topk, dinov2_topk, support_category, local_match_status |
lo que la máquina vio, para calibrar después |
Reglas heredadas de PF-ERI que conviene mantener en MANAKAI:
- Dos revisiones por par, ciegas entre sí; si difieren en las tres categorías, una tercera persona adjudica.
reid.review.agreement_summarycalcula el acuerdo exacto y binario. - Un par puede ser revisable y de individuos distintos: la anatomía compartida basta para excluir con confianza. Y puede ser del mismo individuo y no revisable (flancos opuestos).
- Nunca reajustar umbrales mirando los resultados del par que se está revisando. La calibración se hace después, sobre el CSV acumulado, y se congela antes de aplicarse a pares nuevos.
Con unas decenas de pares registrados se puede: (a) estimar qué valores de coseno y de cobertura local corresponden a review_ready; (b) medir cuántos pares "sin soporte" resultan revisables (en PF-ERI: la mayoría); (c) reemplazar los umbrales provisionales por un modelo simple (regresión logística ridge, como P3 en PF-ERI) evaluado en pliegues disjuntos por individuo y video.
Los scripts ocelot_video_comparison.py, ocelot_pattern_comparison.py y ocelot_reid_interactive.py se conservan como referencia. Errores encontrados y corregidos:
| Problema | Efecto | Corrección |
|---|---|---|
Score ORB = min(100, top50/30·100) |
siempre 100 % (el JSON histórico lo muestra: media 100, desviación 0) | prueba de razón + RANSAC; score = fracción de keypoints verificados |
| SIFT sin verificación geométrica | contaba correspondencias espurias y de fondo | RANSAC (MAGSAC++) |
weighted_score sumaba conteos absolutos (hasta 108) contra umbrales 8/5/3 |
casi cualquier par salía "probablemente el mismo individuo" (histórico: 549.0) | normalizado por número de comparaciones |
| "Confianza 90-95 %" | porcentajes sin ninguna calibración | etiquetas ordinales marcadas uncalibrated, guiadas por la evidencia verificada |
extract_frames_from_video devolvía [] en error |
ValueError al desempaquetar |
lanza excepción con mensaje |
ocelot_pattern_comparison.py ignoraba los argumentos |
el README decía que los aceptaba | acepta <img1> <img2> |
dependencias scipy, scikit-image no documentadas |
ImportError |
requirements-baseline.txt |
Limitación que no se corrigió porque exige rediseño: rosetas y manchas (HoughCircles / blob) se emparejan por coordenadas absolutas de píxel entre videos distintos; el "score de patrones" sale 55 % en el control negativo y ya no manda la conclusión. El diseño v3 lo reemplaza.
Con las correcciones, el baseline discrimina los controles (ORB verificado: 1.4 % negativo vs 88.6 % positivo), pero sigue sin recorte del animal: úsese sólo para reinterpretar resultados antiguos.
- No produce probabilidades de identidad. Ninguna cifra es calibrada hasta que exista
reviews.csvcon pares suficientes (§7). - Los descriptores no fueron ajustados a ocelotes ni a IR; MegaDescriptor se entrenó con muchas especies (incluyendo felinos) pero su respuesta a IR es pobre (§6.3).
- La evidencia local (SIFT + homografía) supone que el flanco es aproximadamente planar y que la pose no cambia radicalmente; poses muy distintas darán
ransac_failedaunque sea el mismo individuo. Eso es una no-revisabilidad, no una exclusión. - Un fotograma por posición uniforme en el tiempo puede perder el instante en que el flanco es visible;
--frames 20o más ayuda a costa de tiempo. - No se separa flanco izquierdo/derecho automáticamente. Es el primer código de razón del instrumento (
opposite_side) y el candidato más claro para la siguiente iteración. - Pruebas:
pytest tests/cubre agregación, rangos, RANSAC sobre patrones sintéticos, calidad y el CSV; no descarga modelos.
Hoja de ruta sugerida: (1) acumular revisiones en MANAKAI con dos revisores; (2) galería de individuos conocidos → --gallery y soporte por rangos; (3) clasificador de flanco; (4) LightGlue/SuperPoint como matcher local alternativo, comparado contra SIFT con el mismo CSV; (5) sólo entonces, un modelo de calibración congelado y evaluado externamente, como P3 en PF-ERI.
reid_compare.py CLI principal (v3)
reid/
frames.py fotogramas de video/imagen + métricas de calidad
detect.py MegaDetector v6 vía ultralytics (recorte del animal)
descriptors.py MegaDescriptor / DINOv2 congelados (timm), coseno
local_match.py SIFT + Lowe + MAGSAC++, estados de fallo, dibujo
compare.py agregación conjunto-a-conjunto, soporte (umbral / rangos), pares, pre-cribado
review.py instrumento humano → reviews.csv, acuerdo entre revisores
report.py JSON + figura
pipeline.py featurize / compare_pair / gallery_support
tests/test_reid.py pruebas sin modelos
docs/ figuras de ejemplo
ocelot_video_comparison.py baseline v2 (corregido)
ocelot_pattern_comparison.py baseline v2, dos imágenes (corregido)
ocelot_reid_interactive.py baseline v2, menú interactivo
MANUAL_USO_SISTEMA_OCELOTE.md documentación v2 (histórica)
OCELOT_IDENTIFICATION_GUIDE.md documentación v2 (histórica)
RESUMEN_ANALISIS_OCELOTE.md resultados v2 (histórico; reinterpretar con §8)
*.png figuras v2 (históricas)
- Shen, D. (2026). Pair-level evidence admission separates retrieval support from human reviewability in wildlife re-identification (PF-ERI v2). https://github.com/Dean-999/pferi-reviewability — marco de tres capas, instrumento de revisión, diseño congelado/externo.
- Čermák, V., Picek, L., Adam, L., Papafitsoros, K. (2024). WildlifeDatasets: An open-source toolkit for animal re-identification. WACV. — MegaDescriptor (
BVRA/MegaDescriptor-L-384). - Oquab, M. et al. (2023). DINOv2: Learning Robust Visual Features without Supervision. —
vit_base_patch14_dinov2.lvd142m(timm). - Beery, S., Morris, D., Yang, S. (2019). Efficient pipeline for camera trap image review — MegaDetector; pesos v6 de Microsoft AI for Good / PytorchWildlife (Hernandez et al., 2024), Zenodo 15398270.
- Lowe, D. G. (2004). Distinctive image features from scale-invariant keypoints. IJCV. — SIFT y prueba de razón.
- Barath, D. et al. (2020). MAGSAC++, a fast, reliable and accurate robust estimator. CVPR. — verificación geométrica (
cv2.USAC_MAGSAC).
Medios de prueba (no incluidos en el repositorio; las figuras en docs/ son derivadas):
- Ocelot-foraging-Sao-Paulo-Brazil.webm, Miguelrangeljr, 2024, CC BY-SA 4.0, Wikimedia Commons.
- Ocelot-foraging-Rubineia-Brazil.webm, Miguelrangeljr, CC BY 4.0, Wikimedia Commons.
- Ocelot (Leopardus pardalis) at the Rio Doce state park 001.png, Richard Hatakeyama, 2014, CC BY-SA 4.0, Wikimedia Commons.
Código bajo licencia MIT. Las figuras derivadas de medios CC BY-SA en docs/ conservan esa licencia.
Proyecto MANAKAI — Monitoreo de fauna con cámaras trampa. Issues y contribuciones vía GitHub.
Última actualización: septiembre 2026 · Versión: 3.0 — separación de capas (PF-ERI), recorte, descriptores congelados, evidencia local verificada, instrumento de revisión humana.

