El otro día andaba por el planetario de Pamplona y vi un mosaico de fotos que formaban un satélite. Eso me dio una idea…
¿Qué es un mosaico de fotos?
Un mosaico de fotos es un grupo de imágenes unidas que vistas de cerca las veríamos como imágenes individuales muy juntas una al lado de la otra pero que vistas desde lejos forman una imagen nueva.
¿No lo he explicado muy bien? No te preocupes, vamos a hacer uno usando la biblioteca Pillow (versión moderna de PIL) en Python.
Preparativos
Además de Python, necesitamos instalar Pillow
. También usaré Pandas
para leer la información de Twitter que descargué el otro día. Vamos a crear un entorno virtual nuevo. Para ello podemos usar conda
o venv
. Con conda
se puede hacer así (desde una terminal o Anaconda Prompt dependiendo del sistema operativo que uses):
1 |
conda create -n mosaico_env pillow pandas |
Lo anterior creará un entorno virtual llamado ‘mosaico_env’ donde tendrás instalado pillow
y pandas
. Para activar el entorno virtual puedes usar:
1 |
conda activate mosaico_env |
Vamos a crear también una serie de carpetas para tenerlo todo más ordenado:
1 |
mkdir mosaico |
1 |
cd mosaico |
1 |
mkdir avatars |
Para crear un entorno virtual sin usar conda
podemos hacer lo siguiente (desde una terminal o Anaconda Prompt). Primero creamos las carpetas
1 |
mkdir mosaico |
1 |
cd mosaico |
1 |
mkdir avatars |
Y ahora creamos el entorno virtual dentro de la carpeta ‘mosaico’ usando venv
:
1 |
python -m venv mosaico_env |
Luego activamos el entorno virtual:
1 2 |
\mosaico_env\Scripts\activate.bat # WINDOWS . mosaico_env/bin/activate # LINUX/MAC |
E instalamos lo que necesitamos:
1 |
python -m pip install pillow pandas |
Con lo anterior deberíamos tener todo listo para proceder.
Materia prima
Para hacer el mosaico necesito materia prima, en este caso, la materia prima serán un montón de imágenes. ¿De dónde las puedo sacar? En mi caso usaré los avatares de nuestros seguidores en Twitter aprovechando la información que obtuvimos en el anterior artículo usando twint
. En ese artículo descargamos la información de las cuentas que nos siguen y creamos un fichero *.csv que ahora vamos a leer para obtener la url del avatar de esas cuentas. Imaginad que la información está en un fichero llamado “followers.csv” que hemos copiado en la carpeta mosaico. Vamos a empezar a descargar la información:
1 2 3 4 5 6 7 8 9 10 |
from urllib.request import urlretrieve from pathlib import Path import pandas as pd df = pd.read_csv( "followers_.csv", quotechar='"', usecols=[3,17] ) |
Lo anterior nos dará dos columnas, una con el nombre de usuario de la cuenta y otra con la url del avatar usado por el usuario de Twitter.
Vamos a iterar sobre esas columnas para descargar los avatares y llamarlos con el nombre del usuario. Los usuarios que no tienen un avatar personalizado tienen la imagen por defecto de Twitter (esa imagen que parece un huevo) y esas imágenes no las vamos a usar. Las imágenes descargadas las guardamos en la carpeta ‘avatars’ que hemos creado al principio:
1 2 3 4 5 6 7 8 9 |
path = Path("avatars") for name, url in zip(df.username, df.avatar): try: if "default_profile" not in url: urlretrieve(url, path / f"{name}.jpg") else: pass except: pass |
Lo anterior, en mi caso, me descarga casi 5000 imágenes. Ya tenemos la materia prima.
¿Cómo hago un mosaico?
Esta parte la he querido hacer sin usar algo ya hecho así que el código siguiente está hecho a medida para este artículo y sin refinar mucho. Servirá también para ver alguna funcionalidad de pillow
.
Primero importamos las bibliotecas que usaré:
1 2 3 4 |
from pathlib import Path # esta ya la habiamos importado antes from itertools import cycle from PIL import Image |
En el mosaico cada imagen de un avatar será un ‘pixel’ del mosaico final. El mosaico final estará compuesta por 100×100 avatares, es decir, 10000 avatares distribuidos en 100 filas y 100 columnas. Como tengo menos de 5000 avatares algunos avatares estarán repetidos en el mosaico final. El mosaico que vamos a formar es el logo de pybonacci:

En esta imagen tenemos 8 colores que, definidas usando RGB, son:
1 2 3 4 5 6 7 8 9 10 |
colors = ( (255, 255, 255), (255, 211, 51), (255, 231, 113), (112, 164, 203), (83, 144, 193), (67, 133, 187), (61, 121, 170), (48, 106, 153), ) |
Están ordenados por el número de píxeles usados por cada color. Por tanto, el primero es el blanco puesto que el mayor número de píxeles son blancos.
Lo que tenemos que hacer ahora es clasificar cada avatar con un color del logo de pybonacci. Una forma sencilla sería viendo cual es el color más repetido en cada avatar y calcular la distancia de ese color predominante en el avatar a los 8 colores del logo de pybonacci. Supongo que habrá formas precisas de calcular el color predominante y la distancia a otro color pero no he investigado mucho. Para obtener el color predominante he adaptado una función encontrada en este post:
1 2 3 4 5 6 7 8 9 10 11 12 |
def most_frequent_color(image): w, h = image.size pixels = image.getcolors(w * h) most_frequent_pixel = pixels[0] for count, color in pixels: if count > most_frequent_pixel[0]: most_frequent_pixel = color if len(most_frequent_pixel) == 2: return most_frequent_pixel[1] else: return most_frequent_pixel |
La anterior función nos devolverá el pixel más repetido en RGB o RGBA (una tupla con 3 o 4 valores). El parámetro que acepta es un objeto Image
de pillow
. Este objeto nos devuelve su tamaño en píxeles en una tupla si usamos Image.size
. La tupla está ordenada por ancho (o número de columnas) y alto (o número de filas). Para obtener todos los píxeles usamos el método Image.getcolors
que nos devolverá una lista de tuplas donde cada tupla indica el número de veces que se repite un color y el color en sí.
Una vez que tenemos una función para obtener el color más repetido de una imagen necesito otra función para ver si ese color predominante está más cerca de uno u otro de los colores disponibles en el logo de pybonacci. Como he comentado, lo de estar ‘mas cerca’ lo vamos a calcular simplemente con la distancia entre colores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def closer_to(rgb, colors): closer = None _dist = None for i, color in enumerate(colors): dist = ( (rgb[0] - color[0])**2 + (rgb[1] - color[1])**2 + (rgb[2] - color[2])**2 ) if _dist is None: closer = i _dist = dist if dist < _dist: closer = i _dist = dist return closer |
En la anterior función no estamos usando nada en especial de pillow
. Como he comentado, la distancia la medimos simplemente como la distancia euclídea entre colores considerando el valor del píxel R, G o B como un punto en un espacio tridimensional.
Con las anteriores funciones tenemos el color predominante de cada avatar y a qué color del logo de pybonacci corresponderá.
Vamos a leer la imagen de referencia, el logo de pybonacci, y la vamos a reescalar al tamaño que he comentado más arriba, 100×100 y a crear otra imagen que será 25 veces mayor en cada lado que contendrá los avatares:
1 2 3 4 |
ref_im = Image.open("pybonacci.png") # original image w, h = 100, 100 ref_im = ref_im.resize((w, h)) bg_im = ref_im.resize((w * 25, h * 25)) # original image 25 times bigger |
Para leer una imagen con pillow
usamos el método Image.open
con la ruta a la imagen. En este caso tengo la imagen ‘pybonacci.png’ dentro de la carpeta ‘mosaico’.
Para cambiar el tamaño usamos el método Image.resize
que acepta una tupla con el ancho y el alto de la nueva imagen.
Nuestra imagen de referencia es el logo de pybonacci con un tamaño de 100×100 píxeles. La imagen que contendrá el mosaico será una imagen que será 25 veces más grande en cada lado (ancho y altura). 1 píxel de la imagen original corresponderá a 25×25 píxeles del mosaico y en esos 25×25 píxeles colocaremos un avatar.
Ahora usamos las dos funciones definidas anteriormente para clasificar los avatares en cada uno de los colores del logo de pybonacci:
1 2 3 4 5 6 7 8 9 10 11 12 |
# check closest color avatar to one of the pybonacci logo results = {i: [] for i in range(len(colors))} for path in Path("avatars").glob("*"): if "default_profile" in str(path): continue im = Image.open(path) w, h = im.size print(w, h, str(path)) rgb = most_frequent_color(im) results[closer_to(rgb, colors)].append(path) for k in results.keys(): results[k] = cycle(results[k]) |
Creamos un diccionario donde cada clave será el orden de cada uno de los colores definidos en colors
más arriba (los colores del logo de pybonacci).
Cada clave contendrá una lista con la ruta de las imágenes más cercanas a ese color. Por tanto, la clave 0
contendrá las rutas de los avatares que tienen menos distancia al color blanco, la clave 1
contendrá las rutas de los avatares que tienen menos distancia al color del primer triángulo más grande del logo, etc.
Por último, en lugar de tener una lista en cada valor del par clave:valor
del diccionario, usamos cycle
para crear un iterador infinito para cada lista de rutas.
Ahora ya tenemos casi todo en orden. Nos queda el último paso, crear el mosaico final. Con el siguiente código lo consigo:
1 2 3 4 5 6 7 8 9 10 |
# fill new big image pixels = ref_im.load() w, h = 100, 100 for x in range(w): for y in range(h): rgb = pixels[x, y] i = closer_to(rgb, colors) im = Image.open(next(results[i])).resize((25, 25)) bg_im.paste(im, (x * 25, y * 25)) bg_im.save("pybo_mosaic.png") |
Obtenemos, primero, un objeto PixelAccess
(en pixels
) mediante el uso del método Image.load
. Este nuevo objeto nos permite acceder a cada píxel por su posición. Luego, iteramos en cada píxel de la imagen de referencia (el logo de pybonacci en tamaño 100×100, ref_im
) para saber qué color tenemos de la imagen original (el blanco del fondo o alguno de los colores de los triángulos). Si el píxel es, por ejemplo, blanco iremos al diccionario y obtendremos una ruta en la clave 0
que es la que corresponde al blanco. Leemos el avatar correspondiente y le cambiamos el tamaño a 25×25. Esta imagen la metemos en la imagen final del mosaico usando el método Image.paste
donde metemos la imagen de 25×25 (el avatar) en la imagen mayor, el logo de pybonacci con el tamaño de cada lado multiplicado por 25, en la posición que le toca. Los iteradores se usan para ir recorriendo rutas y recorrer el mayor número posible o volver al principio si se han acabado las rutas.
Por último, guardamos la imagen final del mosaico usando el método Image.save
.
El resultado
Después de todo este duro trabajo, el resultado final es el siguiente:

No es maravilloso pero el resultado queda aceptable. Se aceptan sugerencias de mejora.
Prueba a encontrar tu avatar
Si eres seguidor de pybonacci en twitter puedes probar a buscar tu avatar simulando el juego de ‘¿Dónde está Wally?‘.
Espero que os haya gustado.
Saludos.