Trabajo Práctico 2 — Diseñando para la Revista “Edna”#
Introducción#
La revista Edna se enfrenta al desafío de comprimir sus imágenes para optimizar el espacio en sus publicaciones digitales e impresas, sin sacrificar los detalles visuales esenciales. La directora de la revista siempre exige que “¡no se use nada de menos que lo mejor!”, entonces ha decidido explorar dos métodos de cuantización de colores para mantener la alta calidad y el estilo inconfundible en cada número el filtro Halftone y la cuantización con K-Means.
La dirección de la revista busca que se implemente una solución que permita comparar visualmente ambos métodos, valorando cuál ofrece una mejor compresión y preservación de la calidad visual y teniendo en cuenta la estética de la imagen.
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 altura) con valores entre 0 y 255, donde el 0 representa el negro y el 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.
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
. Uno de los objetivos es profundizar en el manejo de estas herramientas.
Edición de Imágenes#
Cuando se edita una imagen, se modifican los valores de sus matrices de píxeles para conseguir el efecto deseado. Por ejemplo, se puede oscurecer la imagen disminuyendo los valores de la matriz o se puede modificar su tonalidad alterando los valores de un canal específico.
Filtro Halftone#
El filtro halftone modifica los pixeles de manera tal que la imagen resultante parece hecha por circulos de distintos colores primarios de diferentes tamaños, imitando una impresión en papel. Para cada canal de color (rojo, verde y azul), se aplica un patrón de puntos cuyo tamaño varía según la intensidad del píxel. Los pasos para implementar este filtro son:
- Dividir la imagen en sus canales de color.
-
Para cada canal:
- Partir de una matriz del mismo tamaño que la imagen original pero con un valor de 255 (máxima intensidad) en todos sus elementos.
- Obtener las coordenadas de los puntos a dibujar utilizando la función
get_grid_coords
. Esta función recibe como parámetros el alto y ancho de la imagen, el tamaño de los puntos y el ángulo de rotación, y devuelve una lista de puntos en los que deberán estar centrados los circulos. -
Para cada coordenada de la lista de puntos:
-
Calcular el radio del circulo en función de la intensidad del píxel correspondiente en la imagen original utilizando la siguiente formula:
\[ \text{radio} = \Bigl(1 - \frac{\text{intensidad}}{255}\Bigr) \times \text{dot_size} \times 0.7 \] -
Dibujar un círculo relleno de 0s en la matriz del radio calculado. El círculo estará centrado en la coordenada obtenida. Para dibujar un círculo de radio \(r\) centrado en \((c_x, c_y)\) se tienen que dibujar en los píxeles \((x, y)\) que cumplen la siguiente condición:
\[ (x - c_x)^2 + (y - c_y)^2 \leq r^2 \]
Figura 4: Círculo de pixeles centrado en \((7,8)\) con radio \(r=6\) -
-
Juntar los resultados de los tres canales en una sola imagen.
- Mostrar y guardar la imagen resultante.
La implementación de la función get_grid_coords
se presenta a continuación:
def get_grid_coords(h, w, dot_size, angle_deg):
positions = []
angle_rad = math.radians(angle_deg)
cx, cy = w / 2, h / 2 # centro de la imagen
# calcular la dimension de la grilla
diag = int(math.hypot(w, h))
num_x = diag // dot_size + 3
num_y = diag // dot_size + 3
# alinear el centro de la grilla con el centro de la imagen
offset_x = cx - (num_x * dot_size) / 2
offset_y = cy - (num_y * dot_size) / 2
# recorrer la grilla y calcular las posiciones (geometría 👻)
for i in range(num_y):
for j in range(num_x):
gx = offset_x + j * dot_size + dot_size / 2 - cx
gy = offset_y + i * dot_size + dot_size / 2 - cy
rx = gx * math.cos(angle_rad) - gy * math.sin(angle_rad) + cx
ry = gx * math.sin(angle_rad) + gy * math.cos(angle_rad) + cy
ix, iy = int(round(rx)), int(round(ry))
if 0 <= iy < h and 0 <= ix < w:
positions.append((ix, iy))
return positions
K-Means Quantization#
El algoritmo K-Means se utiliza para agrupar los colores de la imagen en un número reducido de clusters (o grupos). Los pasos para implementar esta técnica son:
- Inicializar de manera aleatoria los
k
pixeles que se utilizarán como centroides de los clusters (valor que representará el cluster entero). Los valores de los centroides deben ser valores RGB válidos (entre 0 y 255). -
Para cada píxel de la imagen, calcular cual es el centroide con color más cercano y asignar el píxel a ese cluster.
Cálculo de la distancia entre colores
Para calcular la distancia entre dos colores RGB, se puede utilizar la distancia euclidiana en el espacio RGB:
\[ d(c_1, c_2) = \sqrt{(R_1 - R_2)^2 + (G_1 - G_2)^2 + (B_1 - B_2)^2} \]Donde \((R_1, G_1, B_1)\) son los valores RGB del primer color y \((R_2, G_2, B_2)\) son los valores RGB del segundo color.
-
Recalcular los centroides de cada cluster como el promedio de los colores de los píxeles asignados a ese cluster.
Cálculo del nuevo centroide
Para calcular el nuevo centroide de un cluster, se suman los valores RGB de todos los píxeles asignados a ese cluster (cada canal por separado) y se divide por la cantidad de píxeles. El resultado debe ser redondeado al entero más cercano para obtener un valor RGB válido.
\[ c_{new} = \Bigl(\frac{R_{sum}}{N}, \frac{G_{sum}}{N}, \frac{B_{sum}}{N}\Bigr) \]Donde \(N\) es la cantidad de píxeles asignados al cluster.
-
Repetir los pasos 2 y 3 hasta que los centroides no cambien o se alcance un número máximo de iteraciones (por ejemplo, 100).
- Reemplazar cada píxel de la imagen original por el color del centroide al que pertenece.
- Mostrar y guardar la imagen resultante.
Requerimientos del Programa#
Se te pide escribir un script en Python que:
-
Solicite al usuario:
- La ruta de la imagen a procesar.
- El método de cuantización a aplicar: halftone o kmeans.
- En caso de elegir halftone:
- El tamaño de los puntos (
dot_size
). Si el usuario no especifica, puede dejar por default el valor5
. - Los ángulos de rotación para cada canal RGB. Si el usuario no especifica, puede dejar por default el valor
"15,45,0"
.
- El tamaño de los puntos (
- En caso de elegir kmeans:
- El número de colores deseados para la imagen resultante. Si el usuario no especifica, puede dejar por default el valor
8
.
- El número de colores deseados para la imagen resultante. Si el usuario no especifica, puede dejar por default el valor
-
Procese la imagen:
- Si el método de cuantización es halftone, aplique el filtro halftone a la imagen original utilizando los parámetros ingresados.
- Si el método de cuantización es kmeans, aplique el algoritmo K-Means a la imagen original utilizando el número de colores deseados.
-
Genere y muestre:
- La imagen original y la nueva imagen que refleje el método de cuantización seleccionado una al lado de la otra.
Ejemplos de ejecución del programa
Imágenes de prueba#
Se pueden descargar 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.
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.