- BACK_BACK/IMAGENES/: nueva pipeline para análisis de imágenes con VLM - image_analyzer.py: imagen → keywords via ollama - image_comparator.py: TF-IDF similitud keywords vs corpus - mongo_helper.py: CRUD colecciones imagenes y comparaciones - pipeline_pruebas.py: script end-to-end con CLI - requirements_imagenes.txt - BACK_BACK/FLUJOS_APP_PRUEBAS.js: servidor Express ligero (port 3001) sin MongoDB para testear los demos de visualización - VISUALIZACION/public/demos/: tres demos de 3d-force-graph - demo_text_nodes.html: nodos como texto (three-spritetext) - demo_img_nodes.html: nodos como imágenes (THREE.Sprite) - demo_mixed_nodes.html: cards HTML en 3D (CSS2DRenderer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
6 KiB
Python
158 lines
6 KiB
Python
"""
|
|
image_comparator.py
|
|
-------------------
|
|
Compara keywords de imágenes con documentos de texto (noticias, wikipedia, torrents)
|
|
usando similitud TF-IDF coseno.
|
|
|
|
Produce documentos para la colección 'comparaciones' de MongoDB,
|
|
con la misma estructura que los comparaciones texto-texto ya existentes:
|
|
{ noticia1, noticia2, porcentaje_similitud }
|
|
|
|
Ampliado con campos opcionales: source1_type, source2_type (para saber qué se comparó).
|
|
|
|
Uso:
|
|
comp = ImageComparator()
|
|
resultados = comp.compare_image_vs_collection(imagen_doc, lista_docs_texto)
|
|
top = comp.top_n(resultados, n=10)
|
|
"""
|
|
|
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
from sklearn.metrics.pairwise import cosine_similarity
|
|
import numpy as np
|
|
|
|
|
|
# ── Clase principal ─────────────────────────────────────────────────────────────
|
|
|
|
class ImageComparator:
|
|
|
|
def __init__(self, threshold: float = 5.0):
|
|
"""
|
|
Args:
|
|
threshold: porcentaje mínimo de similitud para incluir en resultados (0-100)
|
|
"""
|
|
self.threshold = threshold
|
|
self.vectorizer = TfidfVectorizer(
|
|
analyzer="word",
|
|
ngram_range=(1, 2),
|
|
min_df=1,
|
|
strip_accents="unicode",
|
|
lowercase=True,
|
|
)
|
|
|
|
# ── Conversión de documentos a texto ──────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def doc_to_text(doc: dict) -> str:
|
|
"""
|
|
Concatena los campos relevantes de un documento en un string para TF-IDF.
|
|
Compatible con estructura de noticias/wikipedia/torrents/imagenes.
|
|
"""
|
|
parts = []
|
|
# keywords de imágenes (lista) — los más informativos, se repiten para darles peso
|
|
if doc.get("keywords"):
|
|
kws = doc["keywords"] if isinstance(doc["keywords"], list) else []
|
|
parts.extend(kws * 3) # peso extra a keywords
|
|
# campos de texto estándar
|
|
for field in ("tema", "subtema", "texto"):
|
|
val = doc.get(field)
|
|
if val and isinstance(val, str):
|
|
parts.append(val)
|
|
# entidades
|
|
if doc.get("entidades"):
|
|
parts.extend(doc["entidades"])
|
|
return " ".join(parts)
|
|
|
|
# ── Comparación imagen vs lista de documentos ─────────────────────────────
|
|
|
|
def compare_image_vs_collection(
|
|
self,
|
|
image_doc: dict,
|
|
text_docs: list[dict],
|
|
) -> list[dict]:
|
|
"""
|
|
Compara una imagen contra una lista de documentos de texto.
|
|
|
|
Returns:
|
|
Lista de dicts ordenados por porcentaje_similitud desc, filtrados por threshold.
|
|
"""
|
|
if not text_docs:
|
|
return []
|
|
|
|
all_docs = [image_doc] + text_docs
|
|
texts = [self.doc_to_text(d) for d in all_docs]
|
|
|
|
try:
|
|
matrix = self.vectorizer.fit_transform(texts)
|
|
except ValueError:
|
|
return []
|
|
|
|
# Similitud de imagen (índice 0) contra todos los demás
|
|
sims = cosine_similarity(matrix[0:1], matrix[1:]).flatten()
|
|
|
|
comparaciones = []
|
|
for doc, sim in zip(text_docs, sims):
|
|
pct = round(float(sim) * 100, 2)
|
|
if pct < self.threshold:
|
|
continue
|
|
|
|
comparaciones.append({
|
|
# Campos compatibles con colección 'comparaciones' existente
|
|
"noticia1": image_doc.get("archivo", "imagen"),
|
|
"noticia2": doc.get("archivo", str(doc.get("_id", ""))),
|
|
"porcentaje_similitud": pct,
|
|
# Campos extendidos (opcionales — no rompen queries existentes)
|
|
"source1_type": "imagen",
|
|
"source2_type": doc.get("source_type", "texto"),
|
|
"tema_imagen": image_doc.get("tema", ""),
|
|
"tema_doc": doc.get("tema", ""),
|
|
})
|
|
|
|
comparaciones.sort(key=lambda x: x["porcentaje_similitud"], reverse=True)
|
|
return comparaciones
|
|
|
|
# ── Comparación muchas imágenes vs colección ───────────────────────────────
|
|
|
|
def compare_batch(
|
|
self,
|
|
image_docs: list[dict],
|
|
text_docs: list[dict],
|
|
) -> list[dict]:
|
|
"""
|
|
Compara múltiples imágenes contra una colección de documentos.
|
|
|
|
Returns:
|
|
Todos los pares con similitud >= threshold, sin duplicados.
|
|
"""
|
|
all_comparaciones = []
|
|
seen = set()
|
|
|
|
for img_doc in image_docs:
|
|
results = self.compare_image_vs_collection(img_doc, text_docs)
|
|
for r in results:
|
|
key = (r["noticia1"], r["noticia2"])
|
|
if key not in seen:
|
|
seen.add(key)
|
|
all_comparaciones.append(r)
|
|
|
|
all_comparaciones.sort(key=lambda x: x["porcentaje_similitud"], reverse=True)
|
|
return all_comparaciones
|
|
|
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def top_n(comparaciones: list[dict], n: int = 20) -> list[dict]:
|
|
return sorted(comparaciones, key=lambda x: x["porcentaje_similitud"], reverse=True)[:n]
|
|
|
|
@staticmethod
|
|
def stats(comparaciones: list[dict]) -> dict:
|
|
if not comparaciones:
|
|
return {"total": 0}
|
|
sims = [c["porcentaje_similitud"] for c in comparaciones]
|
|
return {
|
|
"total": len(sims),
|
|
"media": round(np.mean(sims), 2),
|
|
"max": round(max(sims), 2),
|
|
"min": round(min(sims), 2),
|
|
"sobre_50": sum(1 for s in sims if s >= 50),
|
|
"sobre_70": sum(1 for s in sims if s >= 70),
|
|
}
|