
Proyecto Rosalind Franklin: el comparador visual con IA local
El Proyecto Katie Bouman ya os enseñó a hacer preguntas libres sobre una imagen suelta —un circuito, una gráfica, un diagrama—. Pero hay una pregunta distinta, y en ciencia es casi siempre la más importante: no “¿qué es esto?”, sino “¿qué ha cambiado desde la última vez?”. Una planta el lunes y el viernes. Un circuito antes y después de una modificación. Un experimento al empezar y al terminar la reacción. Esa pregunta necesita dos imágenes, no una — y necesita comparar, no solo describir.
Para eso nace el Proyecto Rosalind Franklin, enfocado en la comparación visual con modelos de visión (VLM) locales, sobre el puerto 8049.
[ENLACE AL REPOSITORIO DE GITHUB]
1. ¿Quién es Rosalind Franklin y por qué da nombre a este proyecto?
Rosalind Franklin (1920–1958) fue una química y cristalógrafa británica que trabajó en el King’s College de Londres investigando la estructura del ADN mediante difracción de rayos X. En mayo de 1952, junto con su estudiante de doctorado Raymond Gosling, capturó la que se conoce como “Fotografía 51”: una imagen de difracción de rayos X de la forma húmeda del ADN, obtenida tras unas 62 horas de exposición, cuyo patrón en forma de cruz reveló que la molécula tenía una estructura helicoidal. Esa imagen —mostrada a James Watson y Francis Crick sin que Franklin lo supiera— fue una pieza de evidencia decisiva para el modelo de doble hélice que ellos publicaron. Franklin murió en 1958, antes de que en 1962 Watson, Crick y Maurice Wilkins recibieran el Premio Nobel por ese descubrimiento; su papel no fue reconocido públicamente hasta años después.
Franklin no adivinó la estructura del ADN mirando una sola foto: la encontró comparando patrones entre imágenes, con un rigor técnico extremo (fue ella quien perfeccionó la cámara de rayos X que hizo posible una imagen tan nítida). Es exactamente el espíritu de este proyecto: comparar dos imágenes con cuidado, en vez de describir una sola por encima.
2. ¿Qué hace este sistema y cómo funciona?
El sistema implementa una comparación visual 100% local sobre el puerto 8049:
- Carga de dos imágenes: El profesorado o el alumnado sube la Imagen A (“antes”) y la Imagen B (“después”) del mismo sujeto en dos momentos distintos.
- Codificación Base64: Cada imagen se convierte en memoria a una cadena Base64, siguiendo el mismo patrón que ya usa el Proyecto Katie Bouman.
- Un único mensaje con las dos imágenes: A diferencia de Bouman (una imagen por consulta), aquí las dos imágenes se envían dentro del mismo mensaje al modelo de visión, etiquetadas explícitamente como “IMAGEN A (antes)” e “IMAGEN B (después)”, para que el modelo las compare directamente en una sola pasada, en vez de describir cada una por separado.
- Veredicto estructurado: El modelo responde con un formato fijo de dos líneas — CAMBIO: Sí/No y una explicación breve — que la interfaz convierte en un aviso grande y visual (rojo si hay cambio, verde si no lo hay), para que el resultado se entienda de un vistazo antes de leer los detalles.
- Auditoría e Inspección: Panel con los metadatos de las dos imágenes (nombre, tamaño en KB, tipo MIME), el modelo usado, los tokens consumidos y la pregunta exacta enviada.

3. El Código Completo con Imágenes de Prueba (main.py)
Crea un archivo llamado main.py. La interfaz incluye, al final de la página, un enlace directo a dos imágenes de dominio público (serie del mar de Aral, NASA Earth Observatory, satélite Terra) para que el alumnado pueda hacer su primera comparación sin tener que fotografiar nada previamente:
Python
import base64
import time
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import HTMLResponse
from openai import OpenAI
app = FastAPI()
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
def codificar_imagen_base64(contenido_bytes: bytes) -> str:
return base64.b64encode(contenido_bytes).decode("utf-8")
def parsear_veredicto(texto: str) -> dict:
resultado = {"cambio": None, "explicacion": None, "sin_parsear": False}
lineas = [l.strip() for l in texto.strip().split("\n") if l.strip()]
for linea in lineas:
if linea.upper().startswith("CAMBIO:"):
resultado["cambio"] = linea.split(":", 1)[1].strip()
elif linea.upper().startswith("EXPLICACION:") or linea.upper().startswith("EXPLICACIÓN:"):
resultado["explicacion"] = linea.split(":", 1)[1].strip()
if resultado["cambio"] is None:
resultado["sin_parsear"] = True
resultado["explicacion"] = texto.strip()
return resultado
@app.post("/comparar")
async def comparar(
pregunta: str = Form("¿Qué ha cambiado entre la imagen A y la imagen B?"),
archivo_a: UploadFile = File(...),
archivo_b: UploadFile = File(...),
):
if not archivo_a.content_type.startswith("image/") or not archivo_b.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Los dos archivos deben ser imágenes.")
tiempo_inicio = time.time()
bytes_a = await archivo_a.read()
bytes_b = await archivo_b.read()
b64_a = codificar_imagen_base64(bytes_a)
b64_b = codificar_imagen_base64(bytes_b)
prompt_instrucciones = f"""{pregunta}
Responde solo estas dos cosas, en este orden y en este formato exacto,
una por línea, sin nada más:
CAMBIO: Sí / No
EXPLICACION: una o dos frases describiendo el cambio (o su ausencia)"""
mensajes = [
{
"role": "system",
"content": (
"Eres el asistente del Proyecto Rosalind Franklin, especializado en "
"comparar dos imágenes y encontrar diferencias objetivas y verificables "
"entre ellas, igual que se compara un experimento antes y después."
),
},
{
"role": "user",
"content": [
{"type": "text", "text": "IMAGEN A (antes):"},
{"type": "image_url", "image_url": {"url": f"data:{archivo_a.content_type};base64,{b64_a}"}},
{"type": "text", "text": "IMAGEN B (después):"},
{"type": "image_url", "image_url": {"url": f"data:{archivo_b.content_type};base64,{b64_b}"}},
{"type": "text", "text": prompt_instrucciones},
],
},
]
respuesta = client.chat.completions.create(
model="qwen2-vl-2b-instruct",
messages=mensajes,
temperature=0.2,
max_tokens=400,
)
texto_respuesta = respuesta.choices[0].message.content
veredicto = parsear_veredicto(texto_respuesta)
tiempo_total = time.time() - tiempo_inicio
logs = {
"archivo_a": archivo_a.filename,
"archivo_b": archivo_b.filename,
"tamano_kb_a": f"{len(bytes_a) / 1024:.2f} KB",
"tamano_kb_b": f"{len(bytes_b) / 1024:.2f} KB",
"pregunta": pregunta,
}
return {"veredicto": veredicto, "tiempo": f"{tiempo_total:.2f} s", "logs": logs}
@app.get("/", response_class=HTMLResponse)
async def index():
return """<!DOCTYPE html><html>... interfaz con dos cargadores de imagen,
aviso grande Sí/No, y panel de metadatos (ver repositorio para el HTML completo) ...</html>"""
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="127.0.0.1", port=8049, reload=True)
El archivo completo, con la interfaz HTML entera (cargadores de imagen con vista previa, aviso de cambio en rojo/verde, y panel de metadatos), está en el repositorio enlazado arriba — aquí se ha resumido la parte HTML para que la guía no se haga interminable, pero el código Python de arriba es el corazón funcional completo, sin omitir nada.
4. Puesta en Marcha
- En LM Studio, carga un modelo de visión (Qwen2-VL-2B-Instruct o Moondream2) y mantén el servidor activo en el puerto
1234. - Instala las dependencias:
Bashpip install fastapi uvicorn openai - Arranca el servidor:
Bashuvicorn main:app --reload --port 8049 - Abre
http://127.0.0.1:8049en el navegador.
5. Imágenes de prueba para la primera ejecución
Para que el alumnado pueda probarlo sin tener que fotografiar nada de entrada, la propia interfaz enlaza a dos imágenes de dominio público de la NASA (Earth Observatory, satélite Terra), que muestran la misma zona del mar de Aral en dos años distintos:
- Imagen A (año 2000):
https://assets.science.nasa.gov/dynamicimage/assets/science/esd/eo/woc/images/aral/aralsea_tmo_2000238.jpg - Imagen B (año 2014):
https://assets.science.nasa.gov/dynamicimage/assets/science/esd/eo/woc/images/aral/aralsea_tmo_2014231.jpg
Descárgalas (clic derecho → guardar imagen) y súbelas como Imagen A e Imagen B en la aplicación. El resultado esperado es claro y contundente: entre 2000 y 2014 la parte oriental del mar de Aral se secó casi por completo, así que el comparador debería marcar CAMBIO: Sí y describir la reducción drástica de la superficie de agua. Es un buen primer caso porque el cambio es innegable a simple vista — sirve como prueba de que la instalación funciona antes de pasar a casos más sutiles con fotos propias del aula (una planta del huerto escolar, un circuito, una muestra de laboratorio).
Una vez comprobado ese caso de control, merece la pena probar también el caso contrario: sube la misma imagen dos veces, como A y como B. El comparador debería decir CAMBIO: No — y si en algún momento dice que sí hay cambio entre una imagen y ella misma, es la señal de que conviene bajar la temperature en el código antes de fiarse de él en clase, exactamente por la misma razón de consistencia que ya se explicó en el Proyecto RAJI.
Etiqueta: AIDARAC, comparación visual, educación STEM, FastAPI, IA local, IES Monterroso, LM Studio, montesteam, proyecto rosalind franklin, python, qwen2-vl, uvicorn, visión por computador, vlm


