Cuaderno de apoyo para hacer las prácticas por programa
Opcional. Todo el curso se puede hacer con una hoja de cálculo, el navegador y la interfaz web de un modelo. Este cuaderno es para quien quiera que la recolección, el hash, el catálogo y las descripciones queden en archivos y se puedan repetir. Está pensado para alguien sin experiencia previa en Python, copiando bloque a bloque. Cada bloque se explica antes de mostrarse.
Preparación
Instale Python 3.11 o superior y, en una terminal:
pip install requests pillow imagehash pandas python-dotenv openai
Cree una carpeta de trabajo con esta estructura:
mi-corpus/
paginas.csv lista de páginas fuente (la escribe usted antes de descargar)
img/ imágenes descargadas
descartadas/ lo que no es del tema
catalogo.csv el catálogo
descripciones.jsonl
.env la clave del modelo; nunca va en el código ni en el repositorio
paginas.csv tiene cuatro columnas: fuente, escena, url, periodo. Una fila por página.
Comprobar robots
Antes de descargar nada, el programa pregunta al sitio si permite visitar la ruta. La biblioteca estándar trae un lector de robots.txt. Si el archivo no existe, la función devuelve permitido; anótelo igual, porque "no dijo que no" no es "dijo que sí".
from urllib import robotparser
from urllib.parse import urlparse
AGENTE = "corpus-visual-curso3 (contacto: su-correo@ejemplo.org)"
def permitido(url):
"""True si robots.txt del sitio permite que AGENTE visite la url."""
base = "{0.scheme}://{0.netloc}".format(urlparse(url))
rp = robotparser.RobotFileParser()
rp.set_url(base + "/robots.txt")
try:
rp.read()
except Exception:
return True # sin robots.txt: permitido, y se anota
return rp.can_fetch(AGENTE, url)
Descargar una página
Explicación. El bloque pide la página, busca todas las etiquetas de imagen, resuelve la URL de cada una, la descarga con una pausa entre peticiones, lee las dimensiones con Pillow y escribe una fila por imagen con la URL de la imagen, la URL de la página, el texto alternativo y las medidas. Se identifica con el agente y espera dos segundos entre imágenes. No filtra: el filtrado es un paso aparte y registrado.
import csv, os, re, time
import requests
from io import BytesIO
from urllib.parse import urljoin
from PIL import Image
def descargar_pagina(url, fuente, escena, periodo, carpeta="img", catalogo="catalogo.csv", pausa=2.0):
if not permitido(url):
print("robots.txt no permite:", url)
return
os.makedirs(carpeta, exist_ok=True)
html = requests.get(url, headers={"User-Agent": AGENTE}, timeout=30).text
etiquetas = re.findall(r'<img[^>]+>', html, flags=re.I)
nuevo = not os.path.exists(catalogo)
with open(catalogo, "a", newline="", encoding="utf-8") as f:
w = csv.writer(f)
if nuevo:
w.writerow(["id", "periodo", "fuente", "pagina_fuente", "url_imagen", "ancho", "alto",
"extension", "descripcion_heredada", "origen_descripcion", "derechos",
"escena", "ruta_local", "grupo_duplicados", "canonica", "sensible"])
n = sum(1 for _ in open(catalogo, encoding="utf-8")) - 1
for et in etiquetas:
src = re.search(r'src="([^"]+)"', et)
if not src:
continue
url_img = urljoin(url, src.group(1))
alt = re.search(r'alt="([^"]*)"', et)
alt = alt.group(1) if alt else ""
try:
r = requests.get(url_img, headers={"User-Agent": AGENTE}, timeout=30)
im = Image.open(BytesIO(r.content))
except Exception as e:
print("no se pudo:", url_img, e)
continue
n += 1
ext = (im.format or "jpg").lower()
ruta = os.path.join(carpeta, f"{n:04d}.{ext}")
with open(ruta, "wb") as g:
g.write(r.content)
w.writerow([n, periodo, fuente, url, url_img, im.width, im.height, "." + ext,
alt, "alt" if alt else "ninguna", "", escena, ruta, "", "", ""])
time.sleep(pausa)
Para recorrer la lista:
import pandas as pd
paginas = pd.read_csv("paginas.csv")
for _, p in paginas.iterrows():
descargar_pagina(p["url"], p["fuente"], p["escena"], p["periodo"])
Filtrar lo que no es del tema
Explicación. El filtrado se hace mirando, pero el programa ayuda a encontrar candidatos: dimensiones muy pequeñas, dimensiones repetidas muchas veces en la misma página, nombres de archivo con "logo", "icon" o "banner". El bloque solo lista candidatos; usted decide y mueve.
cat = pd.read_csv("catalogo.csv")
cat["dims"] = cat["ancho"].astype(str) + "x" + cat["alto"].astype(str)
repetidas = cat.groupby(["pagina_fuente", "dims"]).size()
candidatas = cat[(cat["ancho"] < 150) | (cat["alto"] < 150) |
cat["url_imagen"].str.contains("logo|icon|banner|sprite", case=False, na=False)]
print(repetidas[repetidas > 5])
print(candidatas[["id", "url_imagen", "dims"]])
Hash perceptual
Explicación. imagehash.dhash reduce cada imagen a 9 por 8 píxeles en gris y anota si cada píxel es más claro que el siguiente de su fila: 64 bits. La distancia entre dos hashes es el número de bits distintos. El bloque calcula el hash de cada imagen del catálogo, compara todos los pares y lista los que quedan bajo el umbral. Mire con cuidado los pares entre 8 y 14: ahí se decide.
import imagehash
from itertools import combinations
UMBRAL = 10
cat = pd.read_csv("catalogo.csv")
hashes = {}
for _, fila in cat.iterrows():
try:
hashes[fila["id"]] = imagehash.dhash(Image.open(fila["ruta_local"]))
except Exception as e:
print("sin hash:", fila["id"], e)
pares = []
for a, b in combinations(hashes, 2):
d = hashes[a] - hashes[b]
if d <= UMBRAL + 4:
pares.append((a, b, d, "bajo umbral" if d <= UMBRAL else "en el límite"))
pares.sort(key=lambda t: t[2])
for p in pares:
print(p)
Para asignar grupos, una pasada simple: cada par bajo el umbral une a los dos ids en el mismo grupo.
grupo = {}
siguiente = 1
for a, b, d, _ in pares:
if d > UMBRAL:
continue
ga, gb = grupo.get(a), grupo.get(b)
if ga and gb:
for k, v in grupo.items():
if v == gb:
grupo[k] = ga
elif ga:
grupo[b] = ga
elif gb:
grupo[a] = gb
else:
grupo[a] = grupo[b] = f"G{siguiente}"
siguiente += 1
cat["grupo_duplicados"] = cat["id"].map(grupo).fillna("")
cat.to_csv("catalogo.csv", index=False)
El ejemplar canónico lo elige usted en la columna canonica, con la razón en la plantilla.
Validar
Explicación. Dos comprobaciones: cada fila tiene archivo, cada archivo tiene fila. El bloque imprime las discrepancias y no corrige nada solo.
import glob
cat = pd.read_csv("catalogo.csv")
en_disco = set(glob.glob("img/*"))
en_catalogo = set(cat["ruta_local"].astype(str))
print("filas sin archivo:", sorted(en_catalogo - en_disco))
print("archivos sin fila:", sorted(en_disco - en_catalogo))
print("filas:", len(cat), "archivos:", len(en_disco))
Bloque opcional para describir con un modelo multimodal
Explicación. El bloque envía una imagen y el prompt por capas a un modelo compatible con la interfaz de openai, y guarda prompt, salida y metadatos en descripciones.jsonl. La clave está en .env y se lee con python-dotenv; nunca se escribe en el código ni se sube a un repositorio. Antes de correrlo sobre una imagen, compruebe en el catálogo que no está marcada como sensible. Varios proveedores ofrecen cuota gratuita; cambie BASE_URL y MODEL según el suyo.
Contenido de .env:
API_KEY=su_clave
BASE_URL=https://api.openai.com/v1
MODEL=nombre_del_modelo_con_vision
import os, json, base64, datetime
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL"))
MODEL = os.getenv("MODEL")
PROMPT_CAPAS = open("prompt_capas.txt", encoding="utf-8").read() # el de la práctica 2
def describir(id_imagen, ruta, prompt=PROMPT_CAPAS, registro="descripciones.jsonl"):
"""Envía la imagen y el prompt; guarda todo con fecha. Devuelve el texto."""
with open(ruta, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
ext = ruta.rsplit(".", 1)[-1].lower().replace("jpg", "jpeg")
r = client.chat.completions.create(
model=MODEL,
temperature=0.2,
messages=[{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/{ext};base64,{b64}"}},
]}],
)
texto = r.choices[0].message.content
uso = getattr(r, "usage", None)
with open(registro, "a", encoding="utf-8") as f:
f.write(json.dumps({
"id": id_imagen, "ruta": ruta, "fecha": datetime.datetime.now().isoformat(),
"modelo": MODEL, "prompt": prompt, "respuesta": texto,
"tokens": getattr(uso, "total_tokens", None),
}, ensure_ascii=False) + "\n")
return texto
Para la muestra de 20:
cat = pd.read_csv("catalogo.csv")
muestra = cat[(cat["sensible"] != "sí")].head(20)
for _, fila in muestra.iterrows():
print(fila["id"]); print(describir(fila["id"], fila["ruta_local"])); print("=====")
Separar las capas de la salida
Explicación. Si el modelo respetó los títulos del prompt, el bloque corta la salida en cuatro columnas para la tabla de la semana 6.
import re
TITULOS = [("denotacion", "DENOTACIÓN"), ("indices", "ÍNDICES MATERIALES"),
("suposiciones", "SUPOSICIONES"), ("no_sabe", "LO QUE NO PUEDES SABER")]
def capas(texto):
"""Corta la salida en cuatro capas por sus títulos; vacío si falta un título."""
out = {}
for i, (clave, titulo) in enumerate(TITULOS):
inicio = texto.find(titulo)
if inicio < 0:
out[clave] = ""
continue
fin = len(texto)
for _, t2 in TITULOS[i + 1:]:
j = texto.find(t2, inicio + len(titulo))
if j >= 0:
fin = min(fin, j)
out[clave] = texto[inicio + len(titulo):fin].strip(" .:\n")
return out
desc = pd.read_json("descripciones.jsonl", lines=True)
tabla = pd.DataFrame([{"id": d["id"], **capas(d["respuesta"])} for _, d in desc.iterrows()])
tabla.to_csv("descripciones_por_capas.csv", index=False)
Si el modelo no respetó los títulos, la separación falla y se anota: es un dato sobre el modelo.
Registro de frontera por programa
Mantenga un archivo con sus decisiones, una por afirmación, código o grupo de exhibición:
def decidir(tipo, id_imagen, propuesta, decision, criterio, responde, archivo="frontera.jsonl"):
with open(archivo, "a", encoding="utf-8") as f:
f.write(json.dumps({"tipo": tipo, "id": id_imagen, "propuso_el_modelo": propuesta,
"decidi": decision, "criterio": criterio, "responde": responde},
ensure_ascii=False) + "\n")
decidir("descripcion", 17, "cuatro personas de pie frente a un muro",
"corregir: son tres; la cuarta es una sombra",
"miré la imagen a tamaño completo", "yo")
Al final, frontera.jsonl se convierte en las tablas de la plantilla:
pd.read_json("frontera.jsonl", lines=True).to_csv("registro-de-frontera-visual.csv", index=False)
Nota sobre costos y privacidad
Use cuotas gratuitas y, hasta la semana 7, solo imágenes sin personas identificables. Si su corpus es sensible, considere un modelo local de visión: menor calidad, control total, y la decisión queda en el registro. Exterioridad y gobernanza de la IA en ciencias sociales trata estos problemas en detalle: derechos sobre el material, datos personales e imágenes sensibles, consentimiento en plataformas y la opción de describir en lugar de exhibir; concluye que esas decisiones se escriben en un protocolo de gobernanza que dice quién responde por cada una.
Práctica 1. Catálogo reproducible con deduplicación perceptual
Duración estimada: 4 horas, de las cuales 1 en la sesión sincrónica de la semana 2.
Requisitos: una lista de páginas fuente del proyecto propio, una carpeta de trabajo y la plantilla plantillas/catalogo-visual.md. Para la vía por programa, Python 3.11 o superior con requests, Pillow, imagehash y pandas (ver cuaderno-apoyo.md). Para la vía manual, una hoja de cálculo y la mini app de deduplicación.
No requiere programar: cada paso tiene una vía manual.
Objetivo
Construir el primer catálogo de 20 imágenes del corpus propio con las diez columnas de procedencia, detectar duplicados por hash perceptual con un umbral declarado, y validar la integridad entre catálogo y disco en ambas direcciones.
Antes de la sesión
- Escriba la pregunta con la que reúne el corpus. Una frase. Va en la cabecera de la plantilla y se escribe antes de descargar, porque después ya no es la misma.
- Haga la lista de páginas fuente: al menos cinco páginas de al menos dos escenas distintas (museo o archivo, prensa, plataforma). Una fila por página en la primera tabla de la plantilla.
- Para cada sitio, abra
https://<sitio>/robots.txten el navegador y anote si la ruta que va a visitar está permitida. Si el archivo no existe, anótelo. Si está prohibida, no la use y anote por qué. - Si va por programa, instale las bibliotecas y pruebe que el bloque "comprobar robots" del cuaderno corre.
Cuatro pasos durante la sesión
Paso 1. Descargar con registro
Vía manual. Para cada imagen que quiera incorporar, guarde el archivo con un nombre numerado (0001.jpg, 0002.jpg) y anote en la hoja de cálculo la URL de la imagen (clic derecho, copiar dirección de la imagen), la URL de la página, la fecha y el texto alternativo o pie si lo hay. Las dimensiones se obtienen de las propiedades del archivo.
Vía por programa. Use el bloque "descargar una página" del cuaderno. Descarga las imágenes de una página respetando robots.txt, con pausa entre peticiones y un agente identificado, y escribe una fila por imagen con URL, página, dimensiones y texto alternativo.
En las dos vías, no filtre todavía: descargue todo lo que la página ofrezca como imagen. El filtrado es el paso siguiente y debe quedar registrado.
Paso 2. Filtrar lo que no es del tema
Mire las imágenes descargadas y marque las que no pertenecen al corpus: logotipos, banderas de navegación, íconos, miniaturas de otras secciones. En el caso demostrativo, una fracción visible de lo descargado era de ese tipo, con dimensiones repetidas (236 por 350 píxeles) que las delataban antes de abrirlas. Anote el criterio de descarte en la plantilla. No borre los archivos: muévalos a una carpeta descartadas/.
Paso 3. Deduplicar por hash perceptual
Vía por programa. El bloque "hash perceptual" del cuaderno calcula el dHash de cada imagen y la distancia de Hamming entre todos los pares. Fije el umbral en 10 para empezar y mire los pares con distancia entre 8 y 14: ahí está la decisión.
Vía manual. Con la mini app apps/dedup-perceptual.html vea cómo se calcula el hash y qué hace el umbral. Luego, en su carpeta, busque a ojo los pares sospechosos (misma escena, distinto recorte o brillo) y anótelos como grupo. Es menos preciso y se declara así.
Para cada grupo, elija el ejemplar canónico y diga por qué: mayor resolución, procedencia más clara, sin recorte. Los demás se conservan con el campo canonica = no.
Paso 4. Validar la integridad
Confronte el catálogo con la carpeta en ambas direcciones. Vía por programa: bloque "validar". Vía manual: cuente los archivos de la carpeta, cuente las filas, y compare nombre por nombre. Anote cada discrepancia y qué hizo con ella. Dé la cifra del corpus con su historia: cuántas bajó, cuántas descartó, cuántos grupos, cuántas quedan.
Después de la sesión
- Amplíe el catálogo a 40 imágenes como mínimo siguiendo los mismos cuatro pasos. Es la entrega de la semana 3.
- Consulte la página fuente de al menos la mitad de las imágenes para llenar la columna de derechos. "Desconocido" solo vale después de mirar.
- Marque en la columna
sensibletodas las imágenes con personas identificables, menores o muerte. Es la entrada del protocolo de la semana 7. - Escriba la declaración: qué hizo un programa, qué hizo un asistente de código si lo usó, qué hizo usted, qué no pudo descargar.
Qué se evalúa
La trazabilidad, no el tamaño. Un catálogo de 40 imágenes en que cada fila tiene fuente, página, derechos consultados y grupo de duplicados vale más que uno de 400 con columnas vacías. La cifra del corpus debe poder defenderse con la validación.
Variante para grupos
Dos estudiantes descargan desde la misma lista de páginas con procedimientos distintos (uno manual, uno por programa) y comparan catálogos. Las filas que uno tiene y el otro no son el mejor material de discusión sobre reproducibilidad.
Práctica 2. Descripción por capas con un modelo multimodal y auditoría de una muestra
Duración estimada: 5 horas, repartidas entre las sesiones de las semanas 4 y 5 y el trabajo autónomo.
Requisitos: 20 imágenes del catálogo que se puedan enviar a un modelo (públicas, de dominio público o sin personas identificables); acceso a un modelo multimodal de uso gratuito en su versión web, o el cuaderno de apoyo con la clave en .env; la plantilla plantillas/ficha-semiotica.md, sección "auditoría".
No requiere programar.
Objetivo
Obtener descripciones de 20 imágenes con dos prompts, uno plano y uno por capas; guardar cada salida con sus metadatos; auditar una muestra de 15 descripciones por capas con la tabla de tres columnas; y escribir el resumen del sesgo del modelo sobre el corpus propio.
Antes de la sesión de la semana 4
- Elija las 20 imágenes. Compruebe en el catálogo que ninguna está marcada como sensible. Si su corpus entero es sensible, use para esta práctica una muestra de imágenes públicas del mismo tema o de un tema vecino, y dígalo.
- Para tres de las 20, escriba su lectura propia: diez afirmaciones etiquetadas como en el taller de la semana 3. Guárdela sin mostrarla al modelo.
- Abra una cuenta en un servicio con modelo multimodal gratuito, o configure el
.envdel cuaderno. Pruebe con una imagen.
Describir en la semana 4
Prompt plano
Describe esta imagen en detalle.
Prompt por capas
Describe esta imagen en cuatro partes separadas, con estos títulos exactos y en este orden.
1. DENOTACIÓN. Solo lo que se ve: personas (cuántas, dónde en el cuadro, postura, vestimenta), objetos, lugar, disposición, texto visible transcrito literal. Sin interpretar, sin adjetivos de valor, sin nombrar instituciones ni lugares que no estén escritos en la imagen.
2. ÍNDICES MATERIALES. Tipo de imagen (fotografía, grabado, captura de pantalla, ilustración), señales de época o de proceso (grano, color, desenfoque, compresión), marcas de agua, bordes, recortes, interfaz visible.
3. SUPOSICIONES. Lo que supones y no ves: época probable, lugar probable, género de la imagen, función, qué está pasando. Para cada suposición, di en qué te basas y tu grado de confianza (alta, media, baja).
4. LO QUE NO PUEDES SABER. Lista lo que un lector local o el contexto de la fuente sabrían y tú no puedes inferir de la imagen.
Responde en español.
Adapte el prompt a su corpus en dos frases como máximo (por ejemplo, pida transcribir texto en una lengua específica). No añada en el prompt información sobre la imagen: el modelo debe describir sin pistas.
Registro de cada salida
Por imagen y por prompt, guarde: id de la imagen, modelo y versión, fecha y hora, prompt literal, salida literal, y lo que pueda del costo (tokens, llamadas, tiempo). Vía manual: una fila por salida en una hoja de cálculo, con la salida pegada. Vía por programa: el bloque "describir" del cuaderno lo hace y escribe descripciones.jsonl.
Comparar los dos prompts
Para las tres imágenes con lectura propia, llene la tabla de la semana 4: afirmaciones en la capa de denotación, cuántas habría escrito usted igual, suposiciones declaradas, cuántas rechazaría, qué declaró el modelo no saber. Anote en la bitácora qué cambió entre prompts: no la cantidad de detalle sino su clasificación.
Presupuesto
Con el costo de las 40 salidas, extrapole al corpus completo: dinero, tiempo de ejecución, tiempo de lectura. Decida y escriba qué parte del corpus describirá con el presupuesto que tiene.
Auditar en la semana 5
Diseñar la muestra
Quince descripciones por capas, estratificadas por escena o periodo (cinco por estrato si hay tres estratos), incluyendo a propósito las imágenes que usted considera más locales. Escriba el criterio antes de auditar.
El orden obligatorio
Para cada imagen de la muestra: primero mire la imagen y escriba tres frases propias; después lea la descripción del modelo. Nunca al revés. La auditoría hecha leyendo primero es una aprobación.
La tabla de tres columnas
Una fila por afirmación del modelo. Las cuatro columnas de destino:
- No está: el modelo lo afirma y la imagen no lo muestra.
- No describe: la imagen lo muestra y el modelo no lo dice (se añade como fila con la afirmación que falta).
- Contexto local: está en la imagen, el modelo lo describe en parte o mal, y solo se juzga sabiendo del lugar, el periodo o la comunidad. Diga qué contexto haría falta.
- Aceptada: está y el modelo lo dice bien.
| Imagen | Afirmación del modelo | Capa en que la puso | Columna | Qué sé yo que el modelo no | Cómo lo sé |
|---|---|---|---|---|---|
Cifras y resumen
Cuente las afirmaciones por columna sobre el total auditado. Declare que son cifras sobre la muestra, no tasas del corpus. Escriba el resumen del sesgo en un párrafo: qué tipo de cosas afirma el modelo sin base, qué tipo de cosas calla, qué tipo de cosas describe sin entender, con un ejemplo de cada una tomado de su corpus. Ese párrafo acompaña a todas las descripciones del corpus desde ahora.
Variante con dos modelos o dos lenguas
Si tiene acceso a dos modelos, o repite con el prompt en inglés, audite cinco imágenes con ambos y compare la tercera columna. Ananthram et al. (2025) encuentran que la lengua del prompt cambia el sesgo sin eliminarlo; compruebe si pasa en su corpus.
Después de la sesión de la semana 5
- Describa con el prompt por capas la parte del corpus que el presupuesto permitió, por programa o a mano, y consolide todo en una tabla con una fila por imagen y una columna por capa.
- Adjunte a la tabla el resumen del sesgo y las cifras de la muestra.
- Marque en el registro de frontera visual las afirmaciones que aceptó sin poder decir cómo lo sabía.
Qué se evalúa
La precisión de la auditoría, no la calidad de la descripción. Una descripción mediocre auditada afirmación por afirmación vale más que una brillante aprobada en bloque. La tercera columna se evalúa por si nombra el contexto que falta, no por su tamaño.