[PROYECTO / 2022 → HOY]
Chess Scan: de una foto a jugar contra Stockfish
Cómo construí una app que fotografía un tablero de ajedrez físico, reconoce cada pieza con visión por computador e IA y te deja continuar la partida contra un motor. Empezó como app móvil en 2022 y hoy vive en el navegador.
Chess Scan nació al terminar mis prácticas: había aprendido un montón de tecnologías nuevas y quería juntarlas todas en un proyecto propio y, ya de paso, sobre algo que me gusta mucho: el ajedrez. La idea es sencilla de contar y entretenida de resolver — haces una foto a un tablero físico y la app reconstruye la posición en un tablero virtual para que sigas jugando contra el motor Stockfish, que siempre responde con la mejor jugada posible.
Por debajo, el proyecto tiene bastante más chicha de la que aparenta: un pipeline de visión por computador para encontrar el tablero, un modelo de deep learning para clasificar cada pieza, un servicio REST que orquesta todo y una app en Flutter como cara visible. Lo cuento en el mismo orden en que lo fui construyendo.
La arquitectura de un vistazo
El sistema se reparte en dos mundos: la app móvil (Flutter) y un backend en Python que hace el trabajo pesado. La app envía la foto, el backend la procesa y devuelve la posición; y para jugar, la app manda la posición actual y el backend responde con la jugada del rival.
App (Flutter) ──► Google Cloud Run ──► Contenedor Docker
└─► Backend Flask (Python)
├─ OpenCV → recorte del tablero
├─ TensorFlow → clasificación de piezas
└─ Stockfish → mejor jugada 1. Encontrar y recortar el tablero
Lo primero que hace el servidor al recibir la foto es aislar el tablero del resto de la imagen. Una foto real nunca viene perfecta: hay perspectiva, sombras, el móvil no está perfectamente perpendicular… así que apliqué una cadena de transformaciones con OpenCV hasta quedarme solo con el tablero, visto de frente:
- Paso a escala de grises y suavizado para quitar ruido.
- Detección de bordes con Canny para quedarme con las líneas.
- Transformada de Hough para extraer las rectas horizontales y verticales.
- Clustering de las intersecciones de esas rectas para localizar las esquinas reales del tablero.
- Transformación de perspectiva (
four_point_transform) para «enderezar» el tablero. - Y por último, dividirlo en 64 casillas idénticas.
Original → Grises + Blur → Canny → Hough → Clustering → Esquinas → Recorte 2. El dataset y la clasificación de piezas
Con el servicio de recorte funcionando pude generar mi propio dataset: 8.357 imágenes de casillas, repartidas en 13 clases — las 6 piezas de cada color más la casilla vacía. Etiquetar a mano tantas imágenes es tedioso, pero era la base para que el modelo aprendiera bien.
El modelo TensorFlow (transfer learning)
Para clasificar cada casilla entrené un modelo con TensorFlow y Keras, pero no desde cero. Usé transfer learning partiendo de MobileNet, una red ya preentrenada sobre ImageNet que sabe reconocer objetos cotidianos. La idea: aprovechar todo lo que esa red ya «sabe ver» y reentrenar solo la última capa para que distinga mis piezas de ajedrez. Los pasos clave:
- Data augmentation: rotaciones, zooms y distorsiones para simular fotos reales imperfectas.
- División 80 % entrenamiento / 20 % validación para medir la precisión de forma honesta.
- Congelar la base de MobileNet y entrenar únicamente la capa de clasificación final.
- 200 épocas de entrenamiento (varias horas de cómputo).
El resultado: alrededor de un 93 % de precisión en validación (y ~98 % en entrenamiento). El modelo
entrenado se exporta a un fichero .h5 con todo lo necesario para predecir en producción.
3. El servicio REST con Flask
El backend expone un servicio Flask con dos endpoints. El primero, POST /upload, recibe
la imagen en base64, la recorta, clasifica las 64 casillas y devuelve la posición codificada (por ejemplo
wp = peón blanco, bk = rey negro, em = casilla vacía):
@app.route("/upload", methods=["POST"])
def reciveImage():
data = request.json
image = base64.b64decode(data["image"])
image = cv2.imdecode(np.frombuffer(image, np.uint8), -1)
positions = Functions.predict_image(image)
return str(positions)
El segundo, POST /play, recibe la posición actual en notación FEN y le pide a
Stockfish la mejor jugada del rival (fijé su fuerza en 1500 de ELO para que sea un rival exigente
pero jugable):
@app.route("/play", methods=["POST"])
def reciveFen():
data = request.json
fen = data["fen"]
game = ChessGame(fen)
game.d_engine.set_elo_rating(1500)
move = game.d_engine.get_top_moves(1)[0]["Move"]
return move 4. La app en Flutter
La cara visible es una app en Flutter (mi primer contacto con el framework). El flujo para el usuario es directo: desde el menú puedes Jugar directamente o Escanear. Al escanear, eliges foto de cámara o galería, recortas para dejar solo el tablero y lo envías al servidor. La app te muestra una pantalla de confirmación de la posición detectada y, con un toque, empiezas a jugar contra Stockfish arrastrando tus piezas hasta que alguien gana.
5. Despliegue: Docker y Google Cloud Run
Para que la app funcione desde cualquier sitio, empaqueté el backend de Flask en un contenedor Docker y lo desplegué en Google Cloud Run, que mantiene el servicio a la escucha de las peticiones sin que yo tenga que gestionar servidores. Este mismo patrón —contenedor + Cloud Run— es el que sigo usando hoy en mis proyectos.
6. La segunda vida: de app móvil a web
Chess Scan podía haberse quedado como el proyecto con el que cerré mis prácticas, pero le tenía cariño y quería que cualquiera pudiera abrirlo sin instalar nada. Así que hace poco lo reconstruí entero para el navegador, con la misma filosofía de siempre —foto → recorte → detección → corrección → jugar— pero repensando cada capa con lo que he aprendido desde 2022.
La decisión de fondo fue clara: el cerebro que reconoce las piezas no se toca. El backend sigue
siendo Flask + TensorFlow + OpenCV en un contenedor Docker y reutiliza exactamente el
mismo modelo entrenado (modelo_sipr_v7_200_epocas.h5) de la app original. Lo que cambió por completo
fue todo lo demás:
- El front pasó de Flutter a la web: React 19 + TypeScript + Vite + Tailwind, con
recorte táctil de la foto (
react-easy-crop) que funciona igual en móvil que en escritorio. - Stockfish ahora corre en tu propio navegador (compilado a WebAssembly), no en el servidor. El rival vive en tu pestaña: cero latencia de red para pensar cada jugada.
- La confianza del modelo dejó de ser un número escondido: la web devuelve la confianza por casilla y resalta las dudosas para que las revises de un vistazo. Ahí es donde la colaboración persona-máquina se nota: el modelo hace el 95 %, tú corriges el 5 % que le cuesta.
- Un editor de tablero de verdad: arrastrar y soltar piezas, paleta, clic derecho para vaciar. Si la foto salió regular, arreglas la posición en segundos antes de jugar.
- Y una partida completa: dificultad ajustable, barra de evaluación, historial de jugadas con
exportación a PGN, deshacer y una flecha de pista con
chess.jsvalidando cada movimiento.
Es interesante ver el contraste: el mismo modelo .h5 que en 2022 vivía dentro de una app de móvil hoy
responde a peticiones desde una web, y el motor de ajedrez que antes consultaba al servidor ahora se ejecuta entero
en el cliente. El proyecto no cambió de idea; cambió de forma.
7. Dónde está hoy
La versión web está en producción y abierta a cualquiera en
chessscan.com (con el backend de IA en
api.chessscan.com). Como el resto de mis proyectos, está desplegada en Google Cloud Run
y escala a cero: no cuesta nada en reposo y arranca sola cuando alguien la abre.
Me gusta que Chess Scan haya pasado de ser «el proyecto con el que aprendí» a algo que cualquiera puede probar con una foto de su tablero. Es la mejor forma de enseñar lo que hago: no contarlo, sino dejar que lo uses.
Lo que me llevé del proyecto
Chess Scan fue mi primer proyecto de punta a punta: desde la investigación en visión por computador y el entrenamiento de un modelo de IA, hasta desarrollar una app en un lenguaje nuevo y desplegar todo en la nube. Tocar tantas piezas distintas y verlas encajar es justo lo que más me gusta de este trabajo — y, tres años después, reconstruirlo para la web me confirmó que aquella base estaba bien pensada.
Lo que en 2022 apunté como «ideas para el futuro» —una versión web, jugar contra el motor con más comodidad, poder corregir la posición— hoy está hecho. Los siguientes apuntes: modo multijugador y afinar aún más la detección en fotos difíciles.