Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 92: Proyecto de portafolio — Extractor de la paleta de colores de una imagen

100 Días de Código en Python — Día 92: Proyecto de portafolio — Extractor de la paleta de colores de una imagen

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 92, proyecto de portafolio profesional (categoría Image Processing & Data Science).

Este tutorial adapta pedagógicamente el Día 92. La sección 📝 Contenido original del curso proviene del único archivo de la carpeta: dia92-tarea11.md (enunciado y pregunta de reflexión, en inglés; aquí traducido). El curso no ofrece guía ni solución, y este tutorial no entrega el sitio terminado. Todo lo que no está en ese archivo (imágenes como arreglos de NumPy, conteo de colores, agrupación de colores parecidos, subida segura de archivos en Flask, retos 🟢🟡🟠🔴, proyecto extra y la etapa 11 de Mi Portafolio Profesional) es creación de esta adaptación.

Nota sobre las verificaciones: el 28 de septiembre de 2026, con Python 3.14, NumPy 2.5.3, Pillow 12.3.0 y Flask 3.1.3, se ejecutó colores_frecuentes (sección 4): en una imagen de prueba mitad roja, mitad azul, con un cuadro verde, devolvió rojo 50 %, azul 46 % y verde 4 %. En una "foto" con ruido (2169 colores distintos) sin agrupar, los 3 colores más comunes tenían solo 0,13 % cada uno; agrupando en intervalos de 32 se obtuvieron 2 colores con 84,43 % y 15,57 %. También se comprobó la ruta de subida con el cliente de pruebas de Flask: imagen válida → colores; archivo que no es imagen → mensaje de error; sin archivo → mensaje; archivo de 6 MB con límite de 5 MB → 413.


Tabla de contenidos

  1. La tarea: los 10 colores más comunes de una imagen
  2. Analizar el problema antes de programar
  3. 📖 Una imagen es un arreglo de NumPy
  4. 📖 Contar colores con np.unique
  5. 📖 El problema de los "casi iguales": agrupar colores
  6. 📖 Recibir la imagen en Flask de forma segura
  7. 🚀 Proyecto del día: construcción por versiones
  8. 🌟 Proyecto extra: paleta institucional para el periódico escolar
  9. 🏗️ Mi Portafolio Profesional — Etapa 11
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Observaciones sobre el material original
  13. 📋 Matriz de trazabilidad
  14. 📦 Evidencias de aprendizaje del Día 92

1. La tarea: los 10 colores más comunes de una imagen

(Fuente: dia92-tarea11.md)

📝 Contenido original del curso

Uno de los sitios favoritos de la instructora para diseñar es Flat UI Colors (flatuicolors.com), un sitio de paletas de colores. Es un sitio estático muy simple que muestra muchos colores y sus códigos HEX. Se puede copiar el código HEX y usarlo en el CSS o en cualquier programa de diseño.

En el Día 76 aprendiste procesamiento de imágenes con NumPy. Usando ese conocimiento y tus habilidades de desarrollador (es decir, buscar en Google), construye un sitio web donde un usuario pueda subir una imagen y tú le digas cuáles son los 10 colores más comunes de esa imagen.

Un buen ejemplo de esta funcionalidad: coolphptools.com/color_extract.

Pregunta de la tarea — Momento de reflexión: cómo abordaste el proyecto; qué fue difícil y qué fácil; cómo mejorarías; tu mayor aprendizaje; qué harías distinto.

Prerequisitos: NumPy e imágenes como arreglos (en esta copia del curso, Día 77; ver sección 12); códigos de color HEX en CSS (Día 43); Flask, plantillas y formularios POST (Días 54–60); Pillow (Día 85).


2. Analizar el problema antes de programar

(Sección creada para esta adaptación.)

🔍 Antes de programar

Pregunta Respuesta que debes construir tú
¿Qué entra? Un archivo de imagen subido desde el navegador
¿Qué sale? 10 colores, cada uno con su código HEX y su porcentaje, mostrados como muestras
¿Qué es "el mismo color"? ¿#c95f30 y #c96030 son el mismo? (sección 5)
¿Qué puede salir mal? Archivos enormes, archivos que no son imágenes, PNG transparentes, fotos de 12 megapíxeles lentas de procesar

Divide el problema

Texto
[Formulario HTML] ──archivo──► [Flask: recibir y validar] ──imagen──► [NumPy: contar colores]
                                                                          │
[Plantilla: muestras de color] ◄────────── lista de (hex, %) ─────────────┘

La función que cuenta colores no sabe nada de Flask: recibe una imagen de Pillow y devuelve una lista. Se prueba sin navegador.


3. 📖 Una imagen es un arreglo de NumPy

(Sección creada para esta adaptación, repasando el Día 77.)

🎯 Objetivo de aprendizaje

Convertir una imagen en un arreglo y transformar su forma para contar píxeles.

📖 Explicación conceptual

Una imagen RGB de 100 × 100 píxeles se convierte en un arreglo de forma (100, 100, 3): 100 filas, 100 columnas y 3 valores (R, G, B) por píxel, con tipo uint8 (enteros de 0 a 255). Verificado.

Para contar colores no importa dónde está cada píxel, solo cuál es. Por eso se "aplana" la cuadrícula en una lista de píxeles con reshape:

Python
import numpy as np
from PIL import Image

pixeles = np.array(imagen.convert("RGB")).reshape(-1, 3)
# forma (100, 100, 3)  ─►  (10000, 3): 10 000 filas, cada una [R, G, B]
  • .convert("RGB") asegura 3 canales (un PNG puede tener 4, con alfa; una imagen en gris, 1).
  • reshape(-1, 3): "3 columnas y las filas que hagan falta" (-1 pide a NumPy calcularlo).

⚠️ Cuidado con uint8

Python
np.array([250], dtype=np.uint8) + 10    # → array([4]), ¡no 260!

Verificado: los uint8 "dan la vuelta" al pasar de 255 (desbordamiento). Si haces cuentas con píxeles (promedios, sumas), conviértelos antes: pixeles.astype(int).


4. 📖 Contar colores con np.unique

(Sección creada para esta adaptación. Función verificada.)

Python
def colores_frecuentes(imagen, cantidad=10, paso=0):
    pixeles = np.array(imagen.convert("RGB")).reshape(-1, 3)
    if paso:
        pixeles = (pixeles // paso) * paso + paso // 2
    colores, conteos = np.unique(pixeles, axis=0, return_counts=True)
    orden = np.argsort(conteos)[::-1][:cantidad]
    total = len(pixeles)
    return [("#{:02x}{:02x}{:02x}".format(*colores[i]), float(conteos[i] / total * 100))
            for i in orden]

Qué hace cada parte:

  • np.unique(pixeles, axis=0, return_counts=True): con axis=0 compara filas completas (colores [R, G, B]), no números sueltos. Devuelve los colores distintos y cuántas veces aparece cada uno.
  • np.argsort(conteos) da los índices que ordenarían los conteos de menor a mayor; [::-1] los invierte (mayor a menor) y [:cantidad] toma los primeros 10.
  • "#{:02x}{:02x}{:02x}".format(*colores[i]) convierte [255, 0, 0] en "#ff0000": x = hexadecimal, 02 = dos dígitos con cero a la izquierda. El * "desempaqueta" los tres valores.
  • float(...): NumPy devuelve sus propios tipos (np.float64); convertirlos a float evita sorpresas al mostrarlos o pasarlos a JSON.
  • El parámetro paso se explica en la sección 5.

Verificado: en una imagen mitad roja y mitad azul con un cuadro verde de 20 × 20, el resultado fue #ff0000 50 %, #0000ff 46 %, #008000 4 %.

Alternativa: collections.Counter(map(tuple, pixeles)).most_common(10) también funciona y es fácil de leer, pero es mucho más lento con millones de píxeles porque recorre en Python puro. Mídelo tú con time.perf_counter() y una foto real.


5. 📖 El problema de los "casi iguales": agrupar colores

(Sección creada para esta adaptación. Verificado con una imagen con ruido.)

En una foto real, un cielo azul no es un azul: son miles de tonos que difieren en 1 o 2 unidades por el ruido del sensor y la compresión JPEG. Resultado (verificado): en una imagen de prueba con ruido había 2169 colores distintos, y los "más comunes" tenían solo 0,13 % cada uno, casi idénticos entre sí. Una paleta así no sirve.

Solución: cuantizar (agrupar valores en intervalos). Con paso=32, cada canal se redondea al centro de su intervalo: 0–31 → 16, 32–63 → 48, etc.

Python
pixeles = (pixeles // paso) * paso + paso // 2

Con paso=32 se obtuvo una paleta útil: 2 colores con 84,43 % y 15,57 % (verificado).

Método Idea Ventaja Desventaja
Cuantización por intervalos (arriba) Redondear cada canal Simple, rápido, lo entiendes todo Los intervalos son arbitrarios; un color justo en el borde se parte en dos
Pillow quantize(colors=10) Pillow reduce la imagen a 10 colores con un algoritmo propio Una línea; buenos resultados Menos control; hay que entender qué hace (documentación)
K-means (scikit-learn) Encuentra 10 "centros" que mejor representan los píxeles Paletas de alta calidad Requiere aprender un algoritmo de aprendizaje automático

Tu decisión: prueba al menos dos métodos con 3 fotos distintas y justifica cuál usas.

⚠️ PNG transparentes: verificado: un PNG totalmente transparente convertido a RGB da 100 % negro (#000000), porque los píxeles transparentes suelen guardar RGB = (0, 0, 0). Solución: con la imagen en RGBA, descarta los píxeles con alfa 0 antes de contar.


6. 📖 Recibir la imagen en Flask de forma segura

(Sección creada para esta adaptación. Verificado con el cliente de pruebas de Flask.)

El formulario necesita enctype="multipart/form-data"; sin él, el archivo no se envía:

HTML
<form method="post" enctype="multipart/form-data">
  <input type="file" name="imagen" accept="image/*" required>
  <button>Extraer colores</button>
</form>

La ruta (fragmento):

Python
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024   # 5 MB

@app.route("/", methods=["GET", "POST"])
def inicio():
    resultado, error = None, None
    if request.method == "POST":
        archivo = request.files.get("imagen")
        if not archivo or archivo.filename == "":
            error = "Elige una imagen."
        else:
            try:
                with Image.open(archivo.stream) as imagen:
                    imagen.thumbnail((300, 300))
                    resultado = colores_frecuentes(imagen, paso=24)
            except Exception:
                error = "El archivo no es una imagen válida."
    return render_template("index.html", resultado=resultado, error=error)

Puntos clave:

  • MAX_CONTENT_LENGTH: Flask rechaza archivos más grandes con 413 (verificado). Sin límite, alguien podría subir un archivo gigante y agotar la memoria del servidor.
  • Image.open(archivo.stream) lee la imagen sin guardarla en disco: más simple y más seguro. Si decides guardarla, usa werkzeug.utils.secure_filename para el nombre (un nombre como ../../app.py podría sobrescribir archivos).
  • thumbnail((300, 300)): contar colores en 90 000 píxeles es casi instantáneo; en una foto de 12 megapíxeles, mucho más lento, y la paleta es prácticamente la misma.
  • except Exception: Pillow lanza distintos errores si el archivo no es una imagen; aquí se muestra un mensaje amable (verificado). En tu versión, puedes capturar PIL.UnidentifiedImageError para ser más específico.
  • Privacidad: si no guardas las imágenes, dilo en la página ("Tu imagen no se almacena").

En la plantilla, cada color puede mostrarse como un cuadro:

HTML
{% for color, porcentaje in resultado %}
  <div class="muestra" style="background: {{ color }}"></div>
  <code>{{ color }}</code> {{ "%.1f"|format(porcentaje) }} %
{% endfor %}

7. 🚀 Proyecto del día: construcción por versiones

(Desarrolla la tarea dia92-tarea11.md. Las versiones son una propuesta de esta adaptación.)

Problema

Construir un sitio web que reciba una imagen y muestre sus 10 colores más comunes.

Contexto

El grupo de diseño del periódico escolar quiere sacar paletas de colores de fotografías del colegio y de la región para usarlas en sus publicaciones.

Objetivo

Un sitio Flask que muestre la paleta con muestras de color, códigos HEX copiables y porcentajes.

Requisitos funcionales

  1. Formulario de subida de imagen (JPG, PNG).
  2. Los 10 colores más comunes, agrupando tonos casi iguales.
  3. Cada color con muestra visual, código HEX y porcentaje.
  4. Mensajes claros ante archivos inválidos o demasiado grandes.

Restricciones técnicas

  • Conteo con NumPy en una función separada y probada.
  • Límite de tamaño de archivo.
  • La imagen no se guarda en el servidor (o, si se guarda, con nombre seguro y borrado posterior).

Planificación

  1. Comprender (prueba el sitio de ejemplo y Flat UI Colors). 2. Entradas: imagen. 3. Procesos: validar → reducir → contar → formatear. 4. Salidas: la paleta. 5. Dividir en funciones y rutas. 6. Bocetar la página. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1 — Consola: colores_frecuentes con una imagen de tu computador; imprime la lista.
  • Versión 2 — Agrupar: compara sin agrupar, con cuantización y con quantize; elige.
  • Versión 3 — Flask: formulario, subida y lista de códigos.
  • Versión 4 — Paleta visual: muestras de color al estilo Flat UI Colors, con Bootstrap o CSS propio.
  • Versión 5 — Robustez: límite de tamaño, errores, PNG transparentes.
  • Versión final: botón "copiar" (JavaScript navigator.clipboard.writeText), README.md, diario.

Pruebas

Python
from PIL import Image
img = Image.new("RGB", (100, 100), (255, 0, 0))
img.paste((0, 0, 255), (0, 0, 50, 100))
img.paste((0, 128, 0), (0, 0, 20, 20))
resultado = colores_frecuentes(img)
assert resultado[0] == ("#ff0000", 50.0)
assert [c for c, _ in resultado] == ["#ff0000", "#0000ff", "#008000"]
# Caso Resultado esperado
1 Imagen de prueba de 3 colores Los 3 colores con 50 %, 46 %, 4 %
2 Imagen de un solo color 1 color con 100 % (no 10 colores)
3 Foto real 10 colores distinguibles a simple vista
4 PNG transparente No aparece negro al 100 %
5 Archivo de texto renombrado .png Mensaje de error
6 Archivo mayor que el límite 413 o una página de error amable
7 Enviar sin elegir archivo "Elige una imagen."

🧪 Prueba y depuración

Python
archivo = request.files["imagen"]
archivo.save("static/" + archivo.filename)
pixeles = np.array(Image.open("static/" + archivo.filename))
colores = np.unique(pixeles, return_counts=True)

Encuentra cuatro problemas: request.files["imagen"] lanza error si no se envió archivo (mejor .get); guardar con el nombre original es inseguro (y llena el disco); no se hace reshape(-1, 3) ni se convierte a RGB; y np.unique sin axis=0 cuenta números sueltos, no colores.

⭐ Extensión del proyecto

  • 🟢 Reto 1 — Aplicación: el usuario elige cuántos colores ver (5, 10, 15).
  • 🟡 Reto 2 — Ampliación: mostrar también el código RGB y el nombre aproximado del color (con un diccionario de colores con nombre y la distancia más corta).
  • 🟠 Reto 3 — Desafío: descargar la paleta como archivo CSS con variables (--color-1: #...;) o como imagen PNG generada con Pillow.
  • 🔴 Reto 4 — Pensamiento computacional: "paleta accesible": para cada par de colores de la paleta, calcula el contraste según la fórmula WCAG (búscala en la documentación del W3C) e indica qué combinaciones sirven para texto sobre fondo. Diseña cómo presentar esa información sin abrumar al usuario.

8. 🌟 Proyecto extra: paleta institucional para el periódico escolar

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

El periódico escolar quiere una identidad visual coherente. Tienen 20 fotos representativas del colegio (fachada, uniformes, zonas verdes, eventos) y quieren una paleta de 6 colores que las represente a todas.

Requisitos

  1. Procesar una carpeta de imágenes y combinar sus colores (no solo una imagen).
  2. Elegir 6 colores representativos y distintos entre sí (define "distintos" con una distancia entre colores).
  3. Generar una página HTML con la paleta, sus códigos y ejemplos de uso (titular, fondo, botón) con buen contraste.

Decisiones que debes tomar (y justificar)

  • ¿Todas las fotos pesan igual en la paleta final, o las más grandes pesan más?
  • ¿Cómo evitas que el blanco del cielo o el gris del cemento dominen?

Casos de prueba

  • Con 3 imágenes de un solo color cada una (rojo, verde, azul), la paleta contiene esos 3 colores.
  • Ningún par de colores de la paleta está a menos de la distancia mínima que definiste.

9. 🏗️ Mi Portafolio Profesional — Etapa 11

(Proyecto integrador creado para esta adaptación; etapas anteriores en los Días 82 a 91.)

Etapa 11 (hoy):

  1. Repositorio del extractor con colores.py (y pruebas), la app Flask y README.md con capturas y la comparación de métodos de agrupación (sección 5) con ejemplos visuales.
  2. Usa tu propio extractor para obtener una paleta para tu sitio de portafolio (Día 83) y aplícala. Documenta el antes y el después.
  3. DIARIO.md del Día 92 y ficha en proyectos.json.

10. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué forma tiene el arreglo de una imagen RGB de 640 × 480? ¿Y después de reshape(-1, 3)?
  2. ¿Por qué en una foto real los colores "más comunes" sin agrupar son poco útiles?
  3. ¿Para qué sirve MAX_CONTENT_LENGTH?
  4. ¿Por qué el formulario necesita enctype="multipart/form-data"?

Análisis de código

Python
"#{:02x}{:02x}{:02x}".format(12, 200, 5)

¿Qué cadena resulta? ¿Qué pasaría sin el 02?

Predicción

Con paso=32, ¿en qué valor se convierte un canal de 100? ¿Y uno de 255?

Depuración

"Mi sitio dice que el color más común de mi logo es negro, pero el logo es azul sobre fondo transparente." Explica la causa y la solución.

Modificación

Cambia colores_frecuentes para que ignore los píxeles casi blancos (los tres canales > 240).

Creación

Escribe hex_a_rgb("#ff8000") que devuelva (255, 128, 0) (pista: int(texto, 16)).


11. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú primero → consulta a la IA sobre un punto concreto → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Explicar una línea. Pide a una IA que explique np.unique(..., axis=0, return_counts=True). Comprueba cada afirmación con la documentación de NumPy y con un arreglo pequeño hecho por ti.

Actividad 2 — Comparar métodos. Pide a una IA código de K-means para extraer colores. Antes de usarlo, compara su resultado con tu cuantización en 3 fotos y mide los tiempos. ¿Vale la pena la complejidad?

Actividad 3 — Seguridad. Pregunta qué riesgos tiene permitir subir archivos a un servidor. Verifica los que apliquen a tu sitio con pruebas del cliente de Flask.

Declara el uso: sección "Uso de IA" en el README.md.


12. 🛠️ Observaciones sobre el material original

Archivo Observación Tratamiento
dia92-tarea11.md Dice que el procesamiento de imágenes con NumPy se vio en el Día 76; en esta copia del curso, el Día 76 es de Plotly y el de NumPy es el 77 Se remite al Día 77
dia92-tarea11.md Pide "los 10 colores más comunes" sin considerar los tonos casi iguales Sección 5: agrupación y comparación de métodos
dia92-tarea11.md El sitio de ejemplo (coolphptools.com) es antiguo pero sigue en línea Se conserva como referencia
dia92-tarea11.md No menciona límites de tamaño ni seguridad en la subida Sección 6

13. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Flat UI Colors: paletas con códigos HEX dia92-tarea11.md Secciones 4 y 6 (formato HEX, muestras) — Retos 2 y 3 Sección 7 (versión 4)
Procesamiento de imágenes con NumPy dia92-tarea11.md Secciones 3 a 5 Extractor de colores Reto 4 Secciones 7 y 9
Sitio web: subir imagen y mostrar los 10 colores más comunes dia92-tarea11.md Sección 6 Extractor de colores Reto 1 Sección 7
Ejemplo: coolphptools color extract dia92-tarea11.md Sección 7 (planificación) — — —
Momento de reflexión dia92-tarea11.md Sección 7 (versión final) Pregunta de reflexión — Sección 9
(No existe en el curso original) — Cuantización, desbordamiento uint8, subida segura — Paleta institucional Sección 8

14. 📦 Evidencias de aprendizaje del Día 92

  • El sitio funcionando: capturas con 3 fotos distintas y sus paletas.
  • Las pruebas de colores_frecuentes superadas.
  • La comparación escrita de métodos de agrupación.
  • Tu portafolio con la paleta obtenida por tu propio programa.
  • DIARIO.md del Día 92 y ficha en el portafolio.
  • Cuaderno de errores: np.unique sin axis=0, falta de reshape, desbordamiento de uint8, PNG transparente como negro, formulario sin enctype, archivos sin límite de tamaño, nombres de archivo inseguros.