Saltar a contenido

Trabajo Práctico 2 — Museo de Pantallas#

Introducción#

El Museo de Pantallas prepara una sala interactiva sobre tecnología de visualización. La idea es que un visitante suba una foto y el sistema le muestre cómo se habría visto esa imagen en dos pantallas de otra época:

  • la pantalla de una Game Boy, con sus 4 tonos de verde, y
  • el tubo de un televisor CRT, con sus líneas de barrido y sus colores apenas corridos.

El resultado no es solo la imagen filtrada: el museo quiere ver la imagen puesta dentro de una foto del aparato, ocupando exactamente la zona de la pantalla.

Game Boy CRT

Figura 1: El objetivo final: una foto procesada insertada en la pantalla de la Game Boy y en la de un televisor CRT.

Imágenes#

Las imágenes se representan como matrices de píxeles. En una imagen en escala de grises, esta matriz tiene dos dimensiones (ancho y alto) con valores entre 0 y 255, donde 0 representa el negro y 255 el blanco. En el caso de imágenes a color, se utilizan tres matrices correspondientes a los canales rojo, verde y azul (RGB), cada uno con valores en el mismo rango. La combinación de estos tres canales define el color final de cada píxel.

Escala de grises

Figura 2: Escala de grises

Representación matricial de una imagen a color

Figura 3: Representación matricial de una imagen a color

Rueda de colores RGB

Figura 4: Rueda de colores RGB

Para trabajar con imágenes en este proyecto se puede utilizar la función Image.open de la librería Pillow en conjunto con numpy. También se permite usar Image.resize para redimensionar imágenes. No se pueden usar funciones que resuelvan un ejercicio entero (por ejemplo, filtros ya hechos de Pillow o de otras librerías): la idea es implementar cada transformación a mano.

El filtro Game Boy#

La pantalla de la Game Boy (modelo original) mide 160×144 píxeles y solo puede mostrar 4 tonos. Este filtro lleva cualquier imagen a ese formato.

Paleta de la Game Boy (del tono más oscuro al más claro):

GAMEBOY = [(15, 56, 15), (48, 98, 48), (139, 172, 15), (155, 188, 15)]

Cada tupla representa un color RGB. Por ejemplo, (15, 56, 15) es Rojo=15, Verde=56, Azul=15.

Pasos para implementar este filtro:

  1. Ajustar la resolución. Redimensionar la imagen para que entre en una caja de 160×144 píxeles. Se puede usar Image.resize.

  2. Calcular el brillo de cada píxel como el promedio de sus tres canales:

    \[ \text{brillo}(x, y) = \frac{R + G + B}{3} \]
  3. Dithering ordenado. Como hay solo 4 tonos, para simular los valores intermedios se usa un patrón de puntos que depende de la posición del píxel (así se lograban los degradados en esa consola). Se usa la matriz de Bayer 4×4:

    BAYER_4 = [
        [ 0,  8,  2, 10],
        [12,  4, 14,  6],
        [ 3, 11,  1,  9],
        [15,  7, 13,  5],
    ]
    

    Con \(\text{paso} = 64\), a cada píxel se le suma un ajuste que depende de su posición:

    \[ \text{ajuste}(x, y) = \left( \frac{\text{BAYER_4}[\,y \bmod 4\,][\,x \bmod 4\,] + 0.5}{16} - 0.5 \right) \times \text{paso} \]
    \[ \text{brillo}(x, y) \leftarrow \text{brillo}(x, y) + \text{ajuste}(x, y) \]

    El ajuste vale aproximadamente entre \(-32\) y \(+32\): hace que píxeles cercanos con el mismo brillo caigan en tonos distintos según su posición, formando la trama.

    Nota

    El operador \(\bmod\) devuelve el resto de la división entera. Por ejemplo, \(7 \bmod 4 = 3\) y \(-1 \bmod 4 = 3\). En Python se puede usar el operador % para calcularlo.

  4. Cuantizar a 4 tonos. Calcular el índice del tono y recortarlo al rango \([0, 3]\):

    \[ \text{indice} = \left\lfloor \frac{\text{brillo}(x, y)}{\text{paso}} \right\rfloor \]

    Esto devuelve un número entero entre 0 y 3, que indica qué color de la paleta usar.

  5. Pintar el píxel con GAMEBOY[indice].

Con dithering Sin dithering

Figura 5: Filtro Game Boy con dithering (izquierda) y sin dithering (derecha). Sin dithering se ven bandas duras de color; con dithering la trama simula más tonos de los 4 reales.

El filtro CRT#

Este filtro simula cómo se veía una imagen en un televisor de tubo. Recibe un parámetro entero, desplazamiento (por defecto 3).

Pasos para implementar este filtro:

  1. Ajustar la resolución. Redimensionar la imagen para que entre en 320×240 píxeles (resolución típica de un televisor de la época). Para esto se puede usar Image.resize.

  2. Desplazamiento de canales. Los tubos no alineaban perfectamente los colores. Para cada píxel \((x, y)\):

    • el canal rojo toma el valor del píxel que está desplazamiento posiciones a la izquierda: imagen[y, x - desplazamiento, 0]
    • el canal azul toma el valor del píxel que está desplazamiento posiciones a la derecha: imagen[y, x + desplazamiento, 2]
    • el canal verde queda igual
    • si la posición de origen cae fuera de la imagen, ese canal se deja como estaba
  3. Scanlines. El haz del tubo dibujaba líneas horizontales con huecos entre ellas. Para simularlo, oscurecer una fila de cada dos: para toda fila con índice par (0, 2, 4, …), multiplicar todos los píxeles de esas filas por \(0.5\) y redondear los resultados para que queden enteros.

CRT

Figura 6: Filtro CRT con desplazamiento = 3.

Insertar la imagen en la pantalla#

Junto con el enunciado se entregan cuatro archivos (ver Materiales):

Foto del aparato Máscara de la pantalla
gameboy.png gameboy_mascara.png
tv.png tv_mascara.png

La máscara es una imagen en escala de grises, del mismo tamaño que la foto, que vale blanco (255) donde está la pantalla y negro (0) en el resto.

Máscara de la TV Máscara sobre la foto

Figura 7: La máscara de la TV (izquierda) y la zona que marca sobre la foto (derecha).

Pasos para insertar imagen (el resultado de un filtro) en foto usando mascara:

  1. Encontrar el rectángulo de la pantalla. Buscar la primera y la última fila que tengan algún píxel blanco (\(= 255\)) en la máscara, y lo mismo para las columnas. Eso da las esquinas \((x_0, y_0)\) y \((x_1, y_1)\) del rectángulo que contiene a la pantalla.

Máscara de la TV

Figura 8: Esquinas del rectángulo que contiene a la pantalla.
  1. Estirar la imagen filtrada al tamaño exacto de ese rectángulo: \(\text{ancho} = x_1 - x_0 + 1\), \(\text{alto} = y_1 - y_0 + 1\) (con Image.resize).

  2. Copiar. Armar una imagen nueva cuyos pixeles serán:

    1. Si la máscara es blanca (\(= 255\)), tomar el píxel de la imagen después del filtro.
    2. Si la máscara es negra (\(= 0\)), tomar el píxel de la foto original (de la TV o de la Game Boy).

    Para esto se puede copiar la foto de la TV o de la Game Boy y luego reemplazar solamente los píxeles donde la máscara es blanca.

Requerimientos del Programa#

Se te pide escribir un programa en Python que:

  1. Solicite al usuario:

    1. La ruta de la imagen a procesar.
    2. La pantalla a usar: gameboy o crt.
    3. Si eligió crt, el desplazamiento de canales (entero positivo, por defecto 3).
    4. La ruta donde guardar el resultado.
  2. Procese la imagen:

    • Si la pantalla es gameboy, aplicar el filtro Game Boy (con dithering) e insertar el resultado en la pantalla de gameboy.png usando gameboy_mascara.png.
    • Si la pantalla es crt, aplicar el filtro CRT e insertar el resultado en la pantalla de tv.png usando tv_mascara.png.
  3. Guarde la imagen resultante en la ruta indicada.

El programa debe validar los errores:

  • Si no se encuentra la imagen de entrada.
  • Si la pantalla elegida no es gameboy ni crt.
  • Si el desplazamiento no es un número entero positivo.

Se espera que la salida por pantalla sea un texto legible, claro y bien formateado.

Ejemplos de ejecución del programa

Ingrese la ruta de la imagen: img/statue_of_liberty.jpg
Seleccione la pantalla (gameboy/crt): gameboy
Seleccione la ruta para guardar la imagen procesada: out/estatua_gameboy.png

Imagen guardada en out/estatua_gameboy.png
Ingrese la ruta de la imagen: img/statue_of_liberty.jpg
Seleccione la pantalla (gameboy/crt): crt
Ingrese el desplazamiento de canales (default=3): 3
Seleccione la ruta para guardar la imagen procesada: out/estatua_crt.png

Imagen guardada en out/estatua_crt.png
Ingrese la ruta de la imagen: img/desconocida.png

No se encontró la imagen. Por favor, verifique la ruta e intente nuevamente.
Ingrese la ruta de la imagen: img/statue_of_liberty.jpg
Seleccione la pantalla (gameboy/crt): psp

Pantalla inválida. Las opciones son 'gameboy' o 'crt'.
Ingrese la ruta de la imagen: img/statue_of_liberty.jpg
Seleccione la pantalla (gameboy/crt): crt
Ingrese el desplazamiento de canales (default=3): -2

El desplazamiento debe ser un número entero positivo.

Materiales#

Se pueden descargar las fotos de los aparatos, sus máscaras y algunas imágenes de prueba de esta carpeta.

Entrega#

El trabajo se realizará en grupos de hasta dos estudiantes. El desarrollo del trabajo se llevará a cabo en un repositorio privado de Github, el cual debe ser compartido con los docentes de la materia (se pueden encontrar los usuarios de los docentes aquí). La entrega del trabajo se realizará subiendo el link del repositorio a la tarea correspondiente en el campus del curso. La fecha de entrega se encuentra en la misma tarea y en el calendario del curso.

El código debe estar modularizado en varios archivos y utilizar únicamente las herramientas vistas en clase junto con Pillow y numpy.

Importante: Se recuerda a los estudiantes que las entregas deben ser un producto original de cada grupo, por lo que se les pide revisar la sección 6 del programa de la materia y el Código de Honor y Ética. En caso de sospecha de copia, se citará a los grupos involucrados para una defensa oral para verificar la autoría de la entrega. En caso de no poder defender la autoría, los profesores estarán obligados a elevar el caso al comité de ética de la universidad.