
Encontrar dos veces la misma imagen
Pregunta a una biblioteca de 200.000 fotografías si ya contiene la imagen que tienes en la mano y la respuesta dependerá de lo que cuente como la misma imagen. El archivo pudo haberse recodificado, redimensionado, corregido de color o recortado, de modo que sus bytes ya no coinciden aunque una persona reconozca la fotografía original.
Este es el segundo artículo de una serie sobre la búsqueda de imágenes. El primero trató la ruta de texto: construir un documento buscable, fusionar BM25 con pgvector y manejar los topes de candidatos. Este artículo deja el texto fuera del problema y usa otro índice, en la misma instancia de PostgreSQL, con otra definición de lo que es correcto.
El algoritmo básico funciona bien con fotografías ordinarias, pero un estilo de catálogo muy común deja al descubierto un supuesto que importa en producción.
«La misma imagen» necesita una definición
Antes de elegir un algoritmo hace falta una política. Estas son seis cosas que le pasan a una imagen dentro de una organización, y ninguna técnica sola las resuelve todas.
Qué filas importan no es una cuestión técnica. Dentro de un DAM, el mismo motor responde a tres tareas con tres definiciones distintas de lo correcto.
«¿Ya importamos esto?» se ejecuta de forma automática durante la importación. Un falso positivo interrumpe la importación, así que esta ruta es deliberadamente conservadora y cubre solo las filas 1 a 3.
«Busca el resto de esta sesión de fotos.» lo pide una persona ante el teclado que quiere recall y descartará el ruido por su cuenta. Filas 1 a 5, con generosidad.
«¿Ya tenemos licencia de esto?» es la tarea cara. Alguien está a punto de pagar por una imagen de stock, y la copia que ya está en la biblioteca puede haber sido recortada, reetalonada y reexportada hasta quedar irreconocible. Un fallo aquí cuesta dinero real, así que exige el máximo recall y un paso de confirmación humana.
Las tres tareas pueden compartir índice y motor de consulta, pero necesitan umbrales distintos.
La tabla también sugiere dos distinciones útiles.
Un hash de bytes responde otra pregunta. MD5 indica si se trata del mismo archivo, algo útil al importar pero que no cubre las copias transformadas.
Los embeddings solos no sirven bien para deduplicar. Un embedding visual se entrena para situar cerca imágenes semánticamente parecidas, de modo que «la misma fotografía, recodificada» y «otra fotografía del mismo motivo» pueden parecer buenas coincidencias. Esa generalización ayuda en la búsqueda normal, pero genera falsos positivos entre SKU distintos fotografiados sobre el mismo fondo.
Por eso usamos un método estructural barato, casi insensible al significado, para generar candidatos, y reservamos la información semántica para la verificación.
Un hash que sobrevive a la recodificación
El hash perceptual resuelve de la fila 1 a la 3 de esa tabla con una idea tomada de la compresión de imágenes: descartar todo lo que un codificador JPEG también descartaría y aplicar el hash a lo que queda.
Usamos una DCT porque los dos hashes perceptuales más simples son menos robustos para esta tarea. Un hash de promedio (average hash) compara los píxeles con el brillo medio, así que los ajustes de contraste lo afectan con facilidad. Un hash de diferencias (difference hash) compara píxeles vecinos, lo que maneja mejor el brillo pero tiene problemas con los degradados. La DCT cuesta más aritmética, pero separa de forma explícita la estructura gruesa del detalle fino y permite elegir qué coeficientes conservar.
El resultado es un entero de 63 bits en el que imágenes parecidas producen enteros parecidos, y «parecido» tiene un significado preciso: la distancia de Hamming, es decir, el número de posiciones de bit en las que dos hashes difieren. Una recodificación lo mueve uno o dos bits. Un cambio de tamaño lo mueve unos pocos. Un recorte lo mueve muchísimo, y ese es el siguiente problema.
Aplicar el hash a regiones y no solo a la imagen completa
Si recortas el 20 % de una imagen, todos los coeficientes de esa DCT cambian, porque los píxeles que la alimentan se han desplazado. El hash queda irreconocible, aunque cuatro quintas partes de la imagen sean idénticas.
La solución es dejar de tratar la imagen como una sola cosa. freedam calcula el hash de seis regiones por separado: el encuadre completo recortado en cuadrado desde el centro, el 70 % central y los cuatro cuadrantes al 60 % cada uno.
Buscar en un espacio sin índice
Dado un hash de consulta y seis filas por recurso, necesitamos todas las filas a una distancia de Hamming como 12 bits. Un árbol B no puede responder eso directamente, porque el orden numérico no guarda ninguna relación útil con la distancia de Hamming, así que WHERE hamming(phash, $1) <= 12 obliga a recorrer secuencialmente todas las teselas.
El multi-index hashing convierte esto en búsquedas de igualdad con índice. Divide cada hash de 64 bits en cuatro fragmentos (chunks) de 16 bits guardados en columnas indexadas. Si dos hashes difieren en tres bits como máximo, esas diferencias no pueden tocar los cuatro fragmentos, así que al menos uno es idéntico. La garantía termina más allá de esa distancia, y por eso esta sigue siendo solo una fuente de candidatos.
Tres detalles de implementación importaron en la práctica.
Calcula la distancia en la base de datos, una sola vez. Una función SQL mínima mantiene la aritmética junto a los datos: bit_count((a # b)::bit(64)), declarada IMMUTABLE STRICT PARALLEL SAFE para que el planificador pueda paralelizarla e integrarla en un filtro en lugar de materializar filas en tu aplicación.
Abre en abanico sobre las teselas con UNION ALL y luego agrupa por recurso. Seis teselas producen seis búsquedas por fragmento; se unen los resultados, se calculan las distancias y se agrupan hasta una fila por recurso con MIN(hd) y la tesela que lo produjo. Un recurso que coincide en cuatro teselas debe ser un resultado fuerte, no cuatro débiles.
Entiende lo que cuesta de verdad una búsqueda por fragmento. Con dieciséis bits hay 65.536 valores posibles por columna de fragmento. En una biblioteca de hashes aleatorios uniformes, una búsqueda de igualdad sobre un fragmento devuelve aproximadamente una fila de cada 65.536, y por eso esto funciona. Los hashes reales no son uniformes. Un corpus con muchas regiones planas o casi idénticas produce valores de fragmento que se repiten miles de veces, y una consulta que cae en uno de ellos paga por cada fila que devuelve antes de haber calculado una sola distancia. Es el mismo problema de forma del corpus que veremos en la sección siguiente, que llega por otra puerta: el modelo de costos del algoritmo supone una distribución que tu biblioteca puede no tener. Si las búsquedas son lentas, mide la distribución de valores de c0 a c3 antes de tocar nada más.
Registra el tope de candidatos. La etapa A se detiene en 2000 candidatos ordenados por distancia exacta. La etapa de verificación solo reduce ese conjunto, pero el tope sigue teniendo un costo en recall y debe incluirse en el informe de completitud, como se describe en el primer artículo.
Dónde la fotografía de catálogo rompe el supuesto
El hash perceptual supone que las imágenes tienen estructura suficiente para compararse. La fotografía de catálogo sobre fondo blanco limpio debilita ese supuesto, porque la mayor parte de cada encuadre es uniforme. La DCT de «casi todo blanco con un objeto pequeño en el centro» está dominada por coeficientes cercanos a cero, así que el umbral de la mediana puede dividir ruido en lugar de estructura útil y dejar productos distintos a pocos bits unos de otros.
Esto no produce una excepción. Aparece como un catálogo que informa de malas sugerencias de duplicados mientras otras bibliotecas y las pruebas se comportan con normalidad. La distancia se calcula bien; simplemente la distribución de imágenes ha violado un supuesto en el que se apoya la métrica. Por eso las pruebas necesitan ejemplos de los estilos fotográficos presentes en el corpus real, no solo fotografías genéricas.
Bajar el umbral global reduciría el recall en las fotografías ordinarias, así que clasificamos este tipo de imagen y endurecemos los umbrales solo para él.
freedam mide dos estadísticas baratas sobre la miniatura en grises de 16 por 16, antes de normalizarla: la varianza de los valores de los píxeles y la densidad de bordes a partir de la magnitud del gradiente.
La lección más general es que un umbral de similitud depende del par de imágenes y del corpus. Cuando una biblioteca tiene un estilo fotográfico dominante, es poco probable que una única constante global ofrezca la misma precisión en todos los recursos. Medir la varianza y la densidad de bordes al importar nos da una forma barata de compensar ese estilo.
Tres fuentes de candidatos, un resultado
Como la garantía del principio del palomar termina en el radio 3 y los recortes derrotan por completo a la estructura, el hash por sí solo deja huecos. freedam ejecuta tres generadores de candidatos en paralelo, y cada uno existe por un fallo concreto y conocido de los otros.
Conviene explicitar tres detalles.
El vector de miniatura acota el embedding visual. La fuente C es la única que sobrevive a un recorte fuerte, pero también puede devolver otra fotografía del mismo motivo. Por eso un candidato debe superar tanto un umbral semántico de coseno como un umbral estructural de distancia de miniatura. Un recorte genuino suele seguir siendo estructuralmente cercano, mientras que una fotografía nueva a menudo no.
Mantén el límite visible para el escaneo ANN. La consulta visual usa ORDER BY clip_vector <=> $1 LIMIT 50, y el filtro de distancia se aplica después. Añadir AND distance <= x parece más estricto, pero impide el escaneo del índice HNSW en esta forma de consulta. Igual que con el límite de filas de BM25 del primer artículo, el ejecutor necesita ver el límite para usar bien el índice aproximado.
Registra qué comparaciones se ejecutaron. Una coincidencia solo vectorial lleva una distancia de Hamming centinela de -1 y una coincidencia solo visual lleva -2, en lugar de un valor verosímil. Así la confianza puede usar una fórmula adecuada a la evidencia disponible, y quien depure después puede distinguir una comparación omitida de una distancia medida.
La tolerancia depende del caso de uso
El mismo motor de detección atiende tareas con perfiles de riesgo distintos. Durante la importación, un falso positivo interrumpe la importación, así que el sistema solo debe señalar copias casi exactas. En la búsqueda interactiva, las personas pueden revisar por su cuenta resultados con ruido, así que el umbral puede favorecer el recall. Un solo umbral no sirve bien para los dos casos.
Por eso hay una única tolerancia en una escala de 0 a 50, y todo lo demás se deriva de ella. Cinco familias de umbrales se mueven juntas a lo largo de una curva lineal por tramos con anclajes en 0, 10 y 50.
El quiebre en 10 conserva exactamente las antiguas constantes de importación. Al sustituir valores fijos en el código por una curva ajustable, un anclaje en los valores anteriores da un punto de compatibilidad conocido y facilita evaluar los cambios en el resto del rango.
Costo y límites actuales
La mayor parte del costo se paga al importar. ImageMagick abre la imagen, crea seis teselas de 256 píxeles, las convierte en entradas de 32 por 32 en escala de grises para las DCT y produce una miniatura de 16 por 16 para el vector y las estadísticas de la imagen. El trabajo depende de la CPU y se ejecuta en una cola, no dentro de la petición de importación. Lo que se guarda es relativamente pequeño: seis filas de ocho enteros, un vector de 256 dimensiones y un vector opcional de 768 dimensiones.
Hacer más trabajo una sola vez al importar deja las consultas en cuatro búsquedas de igualdad con índice, dos escaneos de vecinos más cercanos aproximados y unos miles de llamadas a bit_count. Eso hace viable «¿ya está esto en la biblioteca?» como consulta síncrona en una biblioteca de 200.000 recursos.
Los límites:
La rotación y el espejo lo derrotan. El hash DCT de una imagen volteada no guarda relación con el original. Las rotaciones pequeñas de uno o dos grados sobreviven; los 90 grados no, ni tampoco un volteo horizontal. Los sistemas que lo necesitan calculan también el hash de las variantes transformadas, a un múltiplo del costo de indexación. Nosotros no lo hacemos, porque en una biblioteca de marca un recurso reflejado suele ser un recurso deliberadamente distinto.
Los compuestos complejos quedan fuera de alcance. Una fotografía original colocada dentro de un diseño, con texto encima y un tratamiento de color, no es una casi copia de la fotografía según ninguna de estas medidas. Encontrarla requiere una correspondencia a nivel de región, que es una arquitectura distinta y mucho más cara.
Opera por imagen, no por región. Las teselas son una cuadrícula fija, no un detector. Aproximan bien la tolerancia a recortes y no aproximan en absoluto la correspondencia a nivel de objeto.
El video se trata de forma indirecta, no directa. Se puede aplicar el hash a los fotogramas, pero la forma de la pregunta cambia, porque «el mismo video» implica tiempo.
Cada límite se puede resolver con más indexación y más cálculo, pero el sistema actual informa solo de las transformaciones que está diseñado para cubrir, para que nadie considere la detección de duplicados más sólida de lo que es.
Lo que le diríamos a cualquiera que construya esto
- Decide qué significa «la misma imagen» antes de elegir un algoritmo. Escribe la tabla de transformaciones. La fila que decides que no debe coincidir es la que determina la arquitectura.
- No uses embeddings solos para deduplicar. Se entrenan para generalizar justo sobre la distinción que intentas hacer. Úsalos para verificar o para rescatar recortes, acotados por una comprobación estructural.
- Aplica el hash a regiones, no a imágenes. Las teselas convierten una comparación frágil de todo o nada en una votación, y la tesela que coincidió indica cómo se relacionan las dos imágenes.
- Sabe dónde termina tu garantía del principio del palomar. El multi-index hashing es exacto por debajo del radio m y heurístico por encima. Si tus umbrales están por encima, dilo y añade una segunda fuente en lugar de fingir.
- Un umbral es una propiedad del corpus. Dos estadísticas por imagen al importar valen más que una constante global ajustada con una muestra que no incluía el catálogo de tu cliente.
- Deja que el índice tenga su forma. Un escaneo ANN necesita un límite visible; un filtro que parece un endurecimiento puede ser un escaneo completo disfrazado.
- Nunca inventes una distancia que no mediste. Los valores centinela y una puntuación adecuada a la evidencia mantienen depurable un conjunto de resultados.
- Expón un solo control y deriva el resto. Las personas tienen una pregunta («¿cómo de estricto debe ser esto?») y cada etapa del pipeline necesita un número distinto para responderla.
Dónde vive esto en el producto
La detección de casi duplicados se ejecuta al importar, donde señala las copias probables antes de que entren en la biblioteca, y bajo demanda como búsqueda por similitud visual y detección de duplicados. Todo lo anterior se ejecuta dentro de la misma instancia de PostgreSQL que almacena los recursos, con pgvector y cuatro índices B-tree ordinarios, y eso es parte de lo que hace práctico autoalojar freedam.
Siguiente en esta serie: cómo la búsqueda atiende veinte idiomas, incluido por qué el nombre del índice, el predicado de idioma y la configuración de texto deben recurrir juntos al mismo valor alternativo.
Si prefieres verlo antes que leerlo, prueba la demo e importa una copia recortada y guardada de nuevo de una imagen que ya esté en la biblioteca.



