
RAG Multimodal y Visión por Computador con IA Local: Guía Paso a Paso (Proyecto Katie Bouman)
En la documentación técnica, científica y educativa, la información crítica rara vez existe solo como texto plano. Los esquemas eléctricos, los mapas conceptuales, las gráficas de laboratorio y los diagramas de flujo contienen conocimiento imprescindible que los sistemas RAG tradicionales (basados únicamente en texto) pasan por alto.
Para solucionar este desafío nace el Proyecto Katie Bouman —nombrado en honor a la científica de la computación que lideró el desarrollo del algoritmo para procesar e inferir la primera imagen de un agujero negro en la historia—. En esta guía construiremos una aplicación de Visión por Computador y RAG Multimodal 100% local, privada y sin costes de API utilizando FastAPI, modelos de visión local (VLMs) en LM Studio y Python.
1. ¿Qué hace este sistema?
El sistema permite subir cualquier archivo de imagen (fotografías de circuitos, capturas de esquemas, gráficas impresas, mapas de red) y realizar preguntas en lenguaje natural sobre su contenido visual.
A diferencia de un OCR convencional (que solo lee texto), un Modelo de Lenguaje y Visión (VLM) comprende la relación espacial y semántica de los elementos:
- Entiende conexiones: Sabe qué componente eléctrico está unido a qué pin en una protoboard.
- Interpreta tendencias: Lee ejes cartesianos, analiza curvas en gráficas de laboratorio e identifica valores máximos y mínimos.
- Sigue flujos: Analiza diagramas de bloques o algoritmos paso a paso.
2. Explicación de la Interfaz y sus Partes
La aplicación está diseñada con una arquitectura de panel dividido para ofrecer una experiencia pedagógica y transparente en el aula:
A. Panel Superior: Carga de Archivos e Instrucción Visual
- Cargador de Imagen Interactivo: Permite seleccionar o arrastrar archivos de imagen (
.png,.jpg,.jpeg). Al seleccionar un archivo, genera automáticamente una vista previa en miniatura para verificar el documento antes de enviarlo. - Caja de Consulta Multimodal: Un área de texto donde el usuario o alumno formula la pregunta concreta sobre el gráfico (ej. “¿Qué sensor está conectado en la entrada A0 y cuál es su función?”).
B. Panel Inferior Izquierdo: 1. Análisis del Modelo Multimodal (VLM)
- Muestra la interpretación estructurada generada por el modelo de visión local.
- Incluye un contador de tiempo total de inferencia visual, lo que permite comparar el rendimiento entre diferentes arquitecturas de modelos o hardware.
C. Panel Inferior Derecho: 2. Metadatos y Trazabilidad Visual
- Muestra los datos de auditoría de la petición en tiempo real: nombre del archivo, tipo MIME, tamaño procesado en KB y la instrucción enviada al servidor.
- Refleja cómo la imagen es convertida en memoria a una cadena en formato Base64 para integrarse directamente dentro del payload estándar de la API de OpenAI Vision.

3. El Código Completo (app_bouman.py)
Crea un archivo llamado app_bouman.py e incluye el siguiente código:
Python
import base64
import time
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import HTMLResponse
from openai import OpenAI
app = FastAPI()
# Conexión al servidor local de LM Studio
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
def codificar_imagen_base64(contenido_bytes: bytes) -> str:
"""Convierte los bytes de una imagen subida a cadena Base64."""
return base64.b64encode(contenido_bytes).decode("utf-8")
@app.post("/analizar")
async def analizar_imagen(
pregunta: str = Form(...), archivo: UploadFile = File(...)
):
tiempo_inicio = time.time()
# Validar que sea una imagen
if not archivo.content_type.startswith("image/"):
raise HTTPException(
status_code=400, detail="El archivo subido debe ser una imagen."
)
try:
# Leer la imagen y convertir a Base64
contenido_bytes = await archivo.read()
base64_img = codificar_imagen_base64(contenido_bytes)
mime_type = archivo.content_type
# Formato estándar OpenAI Vision Payload
mensajes = [
{
"role": "system",
"content": (
"Eres el asistente del Proyecto Katie Bouman especializado en visión por computador "
"y análisis de esquemas técnicos, diagramas, gráficas y documentos visuales. "
"Analiza minuciosamente la imagen proporcionada y responde en español con precisión."
),
},
{
"role": "user",
"content": [
{"type": "text", "text": pregunta},
{
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_img}"
},
},
],
},
]
# Petición al LLM de visión local
respuesta = client.chat.completions.create(
model="qwen2-vl-2b-instruct",
messages=mensajes,
temperature=0.2,
max_tokens=800,
)
texto_respuesta = respuesta.choices[0].message.content
tiempo_total = time.time() - tiempo_inicio
# Trazabilidad / Logs
logs = {
"nombre_archivo": archivo.filename,
"tipo_mime": mime_type,
"tamano_kb": f"{len(contenido_bytes) / 1024:.2f} KB",
"prompt_usuario": pregunta,
}
return {
"respuesta": texto_respuesta,
"tiempo": f"{tiempo_total:.2f} s",
"logs": logs,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/", response_class=HTMLResponse)
async def index():
return """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>PROYECTO KATIE BOUMAN - RAG Multimodal</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f6f9; margin: 0; padding: 25px; color: #333; }
.container { max-width: 1050px; margin: auto; }
h1 { color: #1a365d; margin-bottom: 2px; font-size: 26px; }
.subtitle { color: #4a5568; font-size: 14px; margin-top: 0; margin-bottom: 25px; }
.upload-section { background: white; border: 1px dashed #cbd5e0; border-radius: 8px; padding: 20px; margin-bottom: 25px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.grid-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.file-box { border: 2px dashed #4299e1; background: #ebf8ff; border-radius: 6px; padding: 20px; text-align: center; cursor: pointer; }
.preview-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin-top: 10px; display: none; border: 1px solid #e2e8f0; }
textarea { width: 100%; height: 100px; padding: 10px; border: 1px solid #cbd5e0; border-radius: 6px; font-family: inherit; font-size: 14px; box-sizing: border-box; }
button { background: #2b6cb0; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 15px; cursor: pointer; margin-top: 10px; width: 100%; }
button:hover { background: #2c5282; }
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.panel { background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; min-height: 250px; }
.panel-header { font-size: 15px; font-weight: bold; color: #2b6cb0; border-bottom: 1px solid #edf2f7; padding-bottom: 10px; margin-bottom: 15px; }
.tiempo { font-size: 12px; color: #718096; margin-top: 15px; border-top: 1px dotted #e2e8f0; padding-top: 8px; }
.log-box { background: #f7fafc; border: 1px solid #e2e8f0; border-radius: 5px; padding: 10px; margin-bottom: 10px; font-family: monospace; font-size: 12px; color: #2d3748; }
footer { text-align: center; margin-top: 30px; font-size: 12px; color: #a0aec0; }
</style>
</head>
<body>
<div class="container">
<h1>PROYECTO KATIE BOUMAN</h1>
<p class="subtitle">Análisis Multimodal de Esquemas, Gráficas y Documentación Visual con VLM Local · MonteSteam</p>
<div class="upload-section">
<div class="grid-inputs">
<div>
<label><strong>1. Selecciona o arrastra una imagen (Circuito, Gráfica, Diagrama):</strong></label>
<div class="file-box" onclick="document.getElementById('archivo').click()">
📁 Haz clic para subir imagen
<input type="file" id="archivo" accept="image/*" style="display:none;" onchange="mostrarVistaPrevia(event)">
</div>
<img id="vista-previa" class="preview-img">
</div>
<div>
<label><strong>2. Pregunta o instrucción sobre el elemento visual:</strong></label>
<textarea id="pregunta" placeholder="Ejemplo: Describe los componentes de este circuito y dime qué sensor está conectado en la entrada A0."></textarea>
<button onclick="procesarImagen()">Analizar Documento Visual</button>
</div>
</div>
</div>
<div class="results-grid">
<div class="panel">
<div class="panel-header">1. ANÁLISIS DEL MODELO MULTIMODAL (VLM)</div>
<div id="respuesta-agente">Sube una imagen y realiza una consulta para obtener el análisis.</div>
<div id="tiempo-box"></div>
</div>
<div class="panel">
<div class="panel-header">2. METADATOS Y TRAZABILIDAD VISUAL</div>
<div id="logs-agente">Sin ejecución previa.</div>
</div>
</div>
<footer>PROYECTO KATIE BOUMAN · Puerto 8043 · Laboratorio MonteSteam</footer>
</div>
<script>
function mostrarVistaPrevia(event) {
const archivo = event.target.files[0];
if (archivo) {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.getElementById('vista-previa');
img.src = e.target.result;
img.style.display = 'block';
}
reader.readAsDataURL(archivo);
}
}
async function procesarImagen() {
const archivoInput = document.getElementById('archivo');
const preguntaInput = document.getElementById('pregunta');
if (!archivoInput.files[0]) {
alert('Por favor, selecciona una imagen primero.');
return;
}
if (!preguntaInput.value.trim()) {
alert('Escribe una consulta sobre la imagen.');
return;
}
const respBox = document.getElementById('respuesta-agente');
const timeBox = document.getElementById('tiempo-box');
const logsBox = document.getElementById('logs-agente');
respBox.innerHTML = '<em>Procesando imagen con el modelo visual en LM Studio...</em>';
timeBox.innerHTML = '';
logsBox.innerHTML = '<em>Codificando imagen en Base64...</em>';
const formData = new FormData();
formData.append('archivo', archivoInput.files[0]);
formData.append('pregunta', preguntaInput.value.trim());
try {
const res = await fetch('/analizar', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
respBox.innerHTML = `<p>${data.respuesta.replace(/\\n/g, '<br>')}</p>`;
timeBox.innerHTML = `<div class="tiempo">⏱️ Tiempo total de inferencia visual: <strong>${data.tiempo}</strong></div>`;
logsBox.innerHTML = `
<div class="log-box">📄 <strong>Archivo:</strong> ${data.logs.nombre_archivo}</div>
<div class="log-box">📐 <strong>Tipo MIME:</strong> ${data.logs.tipo_mime}</div>
<div class="log-box">💾 <strong>Tamaño:</strong> ${data.logs.tamano_kb}</div>
<div class="log-box">❓ <strong>Prompt:</strong> ${data.logs.prompt_usuario}</div>
`;
} else {
respBox.innerHTML = `<span style="color:red;">Error: ${data.detail}</span>`;
logsBox.innerHTML = '-';
}
} catch (e) {
respBox.innerHTML = '<span style="color:red;">Error de conexión con el servidor FastAPI.</span>';
logsBox.innerHTML = '-';
}
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
import uvicorn
uvicorn.run("app_bouman:app", host="127.0.0.1", port=8043, reload=True)
4. Puesta en Marcha
- En LM Studio, asegúrate de tener cargado un modelo visual (como
Qwen2-VL-2B-InstructoMoondream2) y mantén activo el servidor en el puerto1234. - Inicia la aplicación utilizando Uvicorn directamente desde la consola para contar con recarga automática (hot-reload):
Bashuvicorn app_bouman:app --reload --port 8043 - Accede desde tu navegador web a:
[http://127.0.0.1:8043](http://127.0.0.1:8043)



