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
- La tarea: los 10 colores más comunes de una imagen
- Analizar el problema antes de programar
- 📖 Una imagen es un arreglo de NumPy
- 📖 Contar colores con
np.unique - 📖 El problema de los "casi iguales": agrupar colores
- 📖 Recibir la imagen en Flask de forma segura
- 🚀 Proyecto del día: construcción por versiones
- 🌟 Proyecto extra: paleta institucional para el periódico escolar
- 🏗️ Mi Portafolio Profesional — Etapa 11
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Observaciones sobre el material original
- 📋 Matriz de trazabilidad
- 📦 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
[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:
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" (-1pide a NumPy calcularlo).
⚠️ Cuidado con uint8
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.)
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): conaxis=0compara 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 afloatevita sorpresas al mostrarlos o pasarlos a JSON.- El parámetro
pasose 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.
pixeles = (pixeles // paso) * paso + paso // 2Con 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:
<form method="post" enctype="multipart/form-data">
<input type="file" name="imagen" accept="image/*" required>
<button>Extraer colores</button>
</form>La ruta (fragmento):
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, usawerkzeug.utils.secure_filenamepara el nombre (un nombre como../../app.pypodrí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 capturarPIL.UnidentifiedImageErrorpara 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:
{% 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
- Formulario de subida de imagen (JPG, PNG).
- Los 10 colores más comunes, agrupando tonos casi iguales.
- Cada color con muestra visual, código HEX y porcentaje.
- 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
- 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_frecuentescon 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
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
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
- Procesar una carpeta de imágenes y combinar sus colores (no solo una imagen).
- Elegir 6 colores representativos y distintos entre sí (define "distintos" con una distancia entre colores).
- 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):
- Repositorio del extractor con
colores.py(y pruebas), la app Flask yREADME.mdcon capturas y la comparación de métodos de agrupación (sección 5) con ejemplos visuales. - 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.
DIARIO.mddel Día 92 y ficha enproyectos.json.
10. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué forma tiene el arreglo de una imagen RGB de 640 × 480? ¿Y después de
reshape(-1, 3)? - ¿Por qué en una foto real los colores "más comunes" sin agrupar son poco útiles?
- ¿Para qué sirve
MAX_CONTENT_LENGTH? - ¿Por qué el formulario necesita
enctype="multipart/form-data"?
Análisis de código
"#{: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_frecuentessuperadas. - La comparación escrita de métodos de agrupación.
- Tu portafolio con la paleta obtenida por tu propio programa.
DIARIO.mddel Día 92 y ficha en el portafolio.- Cuaderno de errores:
np.uniquesinaxis=0, falta dereshape, desbordamiento deuint8, PNG transparente como negro, formulario sinenctype, archivos sin límite de tamaño, nombres de archivo inseguros.