Aproveitando que tópico passado usei uma foto dos integrantes do Metallica para ensinar reconhecimento facial, hoje cedo assistindo um vídeo no Youtube me foi sugerido o vídeo abaixo:
Achei bonita a arte feita de forma pixelada no album Ride the Lightning da banda e pensei em procurar como fazer algo parecido com programação, tentando pixelar uma imagem.
Dando uma pesquisada, descobri rapidamente uma forma de fazer isso e resolvi incrementar a forma criando um gif da mesma imagem cada vez mais pixelada. Vou aproveitar o tópico pra inserir mais conceitos de funções, aproveitando que estamos vendo funções nos tópicos teóricos, e outros conceitos importantes da linguagem.
Então, iremos gifs assim:




(tá, não ficou nem de longe perto da pixel art feita no vídeo do Youtube mas valeu a tentativa rs)
Hoje, novamente iremos usar a lib Pillow para criar nosso script pixelator.py:
pip install Pillow
Primeiramente, irei criar uma função que irá converter a imagem numa forma mais pixelada dela:
# Pixelator.py
from PIL import Image
def img_to_pixel(image, output_image, pixel_size):
img = Image.open(image)
img = img.resize((img.size[0] // pixel_size, img.size[1] // pixel_size), Image.NEAREST)
img = img.resize((img.size[0] * pixel_size, img.size[1] * pixel_size), Image.NEAREST)
img = img.save(output_image)
return img
Da lib Pillow importei Image e usei essa classe para abrir a imagem dentro da função (que recebe 3 parâmetros, image, outpug_image e pixel_size) e realizar seu redimensionamento. Quem acompanha os tópicos desde o início deve lembrar de um algoritmo semelhante, que foi usado no tópico de redimensionamento de imagens (
Extra [2] - Python - Criando uma Graphic User Interface (GUI) de redimensionamento de imagens - Tecnologia - Bastter.com) mas dessa vez fiz de uma forma diferente. Inicialmente, redimensionei a imagem dividindo altura e largura pela quantidade de pixels (pixel_size) desejada, e usei como filtro o algoritmo NEAREST em vez do algoritmo ANTIALIAS que foi usado no tópico Extra[2]. Neste caso, o NEAREST realiza um efeito mais pixelado na imagem, que é o que queremos.
Em seguida, multipliquei altura e largura novamente por pixel_size, usando o mesmo filtro NEAREST. Resumindo, eu diminuí a imagem por um valor x e depois aumentei a imagem novamente pelo mesmo valor x, mas agora a imagem irá manter o formato mais pixelado.
Em seguida, salvei a imagem e fiz a função retornar a imagem. Isso vai ser importante lá na frente.
Testando o código acima na logo do site, pedindo tamanho de pixels de 30:
img_to_pixel('bastter_logo.png', 'logo_out.png', 30)

Se eu quiser só pixelar qualquer imagem, o código acima já basta, mas decidi complicar um pouco mais as coisas pra ensinar mais conceitos de programação funcional e outros conceitos de programação. Decidi criar os gifs acima.
Então já sei como pixelar, agora eu preciso pixelar várias imagens variando a "perda de qualidade" de cada imagem, coletar todas essas imagens pixeladas e transformá-las num gif.
Vou criar outra função que irá criar várias imagens pixeladas da mesma imagem base. Vou deixar flexível a quantidade de imagens que o usuário quer criar, então vou passar essa variável como parâmetro da minha segunda função. Então basicamente o que essa minha nova função vai fazer é uma iteração sobre a quantidade de imagens que quero criar e ir criando essas imagens, usando minha primeira função:
def img_to_pixel(image, output_image, pixel_size):
img = Image.open(image)
img = img.resize((img.size[0] // pixel_size, img.size[1] // pixel_size), Image.NEAREST)
img = img.resize((img.size[0] * pixel_size, img.size[1] * pixel_size), Image.NEAREST)
img = img.save(output_image)
return img
def create_frames(image, img2pixel, number_of_images):
for i in range(2, number_of_images + 1):
output_image = img2pixel(image,
f'logo{i}.png',
i)
O que foi feito acima, na função create_frames: ela recebe 3 parâmetros, o primeiro é a imagem que eu quero pixelar, o segundo é a nossa primeira função (sim, posso passar uma função como argumento de funções), já que irei usar a função img_to_pixel dentro de create_frames, e o terceiro é a quantidade de imagens que quero gerar.
Obs: eu poderia não passar a função como argumento já que, como visto no tópico anterior, caso nada seja dito dentro da função, o interpretador Python irá procurar a função no escopo global, irá achá-la e o resultado será o mesmo, mas decidi passar como argumento para mostrar que isso também acontece.
Portanto, create_frames irá realizar uma iteração de 2 até a quantidade de imagens que quero gerar. O 2 é porque eu já tenho a primeira imagem, que é a imagem base, e as outras imagens serão geradas a partir dela até a quantidade desejada de imagens.
output_image é o retorno de cada chamada da função image_to_pixel, que recebe os argumentos imagem, o nome da imagem de saída e a quantidade de tamanho de pixels (pixel_size), que foi definido como parâmetro na primeira função.
O resultado prático do nosso código até agora é o seguinte:
# Chamando a função create_frames, usando bastter_logo.png como base
# passando a primeira função como parâmetro e pedindo 10 imagens
create_frames('bastter_logo.png', img_to_pixel, 10)
Iremos gerar 9 imagens além da imagem base:

Mas tem algo que não tou gostando, que é eu informar dentro da função o nome do arquivo de saída. Quero poder usar o nome da imagem de base como referência e só ir adicionando de forma automática a numeração em seguida. Para isso, irei importar uma lib chamada "os", que simula nosso sistema operacional, que extrair o nome da imagem base e sua extensão. Vamos fazer um teste na lib antes para ficar mais claro:
>>> from os import path
>>> path.splitext('bastter_logo.png')
('bastter_logo', '.png')
>>>
Consegui com um comando extrair o nome do meu arquivo e sua extensão, e o resultado foi uma tupla de dois valores. Então vou automatizar a geração dos arquivos de saída usando esse princípio. Vamos mudar nossa função create_frames:
def create_frames(image, img2pixel, number_of_images):
for i in range(2, number_of_images + 1):
output_image = img2pixel(image, f'{path.splitext(image)[0]}{i}{path.splitext(image)[1]}', i)
Percebam que agora meu laço cria imagens pegando o nome do arquivo de base em "path.splitext(image)[0], acrescenta o número "i" ao nome do arquivo e por fim sua extensão usando "path.splitext(image)[1]". Fazendo dessa forma, nosso resultado agora fica:
...
from os import path
...
def create_frames(image, img2pixel, number_of_images):
for i in range(2, number_of_images + 1):
output_image = img2pixel(image, f'{path.splitext(image)[0]}{i}{path.splitext(image)[1]}', i)
create_frames('bastter_logo.png', img_to_pixel, 10)

Notaram como agora o nome de saída ficou automatizado?
Agora eu preciso de uma função que irá capturar todos esses arquivos e transformar num gif. Lembram do projeto de geração de mapas com dados eleitorais (
Projeto 1 (Parte 6) - Python - Muitos mapas e um gif - Tecnologia - Bastter.com), no qual criamos um gif lá? Irei usar o mesmo algoritmo pra criar nosso gif:
...
from glob import glob
...
def create_gif(base_image, output_gif):
frames = []
images = glob(f'{path.splitext(base_image)[0]}*.{path.splitext(base_image)[1][1:]}')
images.sort(key=path.getmtime)
for image in images:
new_frame = Image.open(image)
frames.append(new_frame)
frames[0].save(output_gif, format='GIF', append_images=frames[1:],
save_all=True, duration=300, loop=0)
Aqui, novamente irei usar a lib glob para resgatar todos os arquivos numa lista. Da forma explicitada acima, glob irá resgatar todos os arquivos com meu nome base, o que vier de excedente e o seu formato. Vejamos o retorno de "images":
print(images)
# ['bastter_logo.png', 'bastter_logo2.png', 'bastter_logo3.png', 'bastter_logo4.png', 'bastter_logo5.png', 'bastter_logo6.png', 'bastter_logo7.png', 'bastter_logo8.png', 'bastter_logo9.png', 'bastter_logo10.png']
Em seguida, novamente usando a lib "os", ordenei os arquivos pela data de última modificação neles, usando a função "getmtime". Dessa forma, asseguro que meu gif terá as imagens ordenadas na ordem de sua criação, evitando que eu pegue os arquivos de forma aleatória como primeiro a logo1, depois a logo4, depois a logo7, depois a 2, etc.
Por fim, itero sobre todas as imagens e crio o gif como foi feito no tópico dos mapas. Nada de novo aqui.
Até o momento, nosso programa está assim:
from PIL import Image
from os import path
from glob import glob
def img_to_pixel(image, output_image, pixel_size):
img = Image.open(image)
img = img.resize((img.size[0] // pixel_size, img.size[1] // pixel_size), Image.NEAREST)
img = img.resize((img.size[0] * pixel_size, img.size[1] * pixel_size), Image.NEAREST)
img = img.save(output_image)
return img
def create_frames(image, img2pixel, number_of_images):
for i in range(2, number_of_images + 1):
output_image = img2pixel(image, f'{path.splitext(image)[0]}{i}{path.splitext(image)[1]}', i)
def create_gif(base_image, output_gif):
frames = []
images = glob(f'{path.splitext(base_image)[0]}*.{path.splitext(base_image)[1][1:]}')
images.sort(key=path.getmtime)
print(images)
for image in images:
new_frame = Image.open(image)
frames.append(new_frame)
frames[0].save(output_gif, format='GIF', append_images=frames[1:],
save_all=True, duration=300, loop=0)
create_frames('bastter_logo.png', img_to_pixel, 10)
create_gif('bastter_logo.png', 'logo.gif')
E rodando o código acima, já consigo criar um gif, chamado logo.gif:

Entretanto, além do gif, fiquei com vários arquivos que são lixo, já que eu quero somente o gif:

Eu posso automatizar o delete dessas imagens usadas para criar o gif dentro de nosso script, novamente usando a lib "os":
...
from os import path, remove
...
def create_gif(base_image, output_gif):
frames = []
images = glob(f'{path.splitext(base_image)[0]}*.{path.splitext(base_image)[1][1:]}')
images.sort(key=path.getmtime)
for image in images:
new_frame = Image.open(image)
frames.append(new_frame)
frames[0].save(output_gif, format='GIF', append_images=frames[1:],
save_all=True, duration=300, loop=0)
print(images)
# Remove as imagens criadas deixando apenas a primeira, a imagem base
for img in images[1:]:
remove(img)
No fim do código acima, notem o último laço, no qual itero sobre todas as imagens excluindo a primeira e vou apagando todas elas. Ao final, só ficarei com meu gif e com a imagem de base.
Mas toda vida que eu quiser criar um gif usando uma imagem base diferente, preciso ir lá no meu código e mudá-lo. Isso pode se tornar bem chato se quisermos criar vários gifs a partir de várias imagens diferentes. Então, pra finalizar o tópico, irei introduzir a lib "sys". Vou mostrar um programa simples usando a lib sys:
# sys_script.py
import sys
print(f'{sys.argv[1]} {sys.argv[2]}')
Agora irei rodar o código acima digitando no terminal do meu sistema operacional:
$ python sys_script.py Hello World
E o resultado é:
Hello World
Percebam que eu inseri dados no meu programa ao rodar o script em linha de comando. Então eu posso selecionar as imagens que eu quero pixelar dessa forma. Irei importar sys e irei pedir que o usuário já digite a imagem de entrada e o gif de saída, dessa forma:
...
from sys import argv
...
base_image = argv[1]
output_gif = argv[2]
create_frames(base_image, img_to_pixel, 10)
create_gif(base_image, output_gif)
Agora, rodando o script usando a linha de comando:
$ python pixelator.py bastter_logo.png logo.gif
Temos o gif criado da forma como pedimos, apagando os arquivos excedentes e deixando somente nosso gif e o arquivo de base.
Agora, para criar outros gifs, basta mudar na linha de comando o nome do arquivo base e o nome do gif que irá sair:
$ python pixelator.py bastter_logo.png logo.gif
$ python pixelator.py bastter1.jpg bastter1.gif
$ python pixelator.py bastter2.jpg bastter2.gif
$ python pixelator.py mario64.jpeg mario.gif
$ python pixelator.py ride.jpg ride.gif
E assim criamos:





Nosso código completo:
# pixelator.py
from PIL import Image
from glob import glob
from os import path, remove
from sys import argv
def img_to_pixel(image, output_image, pixel_size):
img = Image.open(image)
img = img.resize((img.size[0] // pixel_size, img.size[1] // pixel_size), Image.NEAREST)
img = img.resize((img.size[0] * pixel_size, img.size[1] * pixel_size), Image.NEAREST)
img = img.save(output_image)
return img
def create_frames(image, img2pixel, number_of_images):
for i in range(2, number_of_images + 1):
output_image = img2pixel(image, f'{path.splitext(image)[0]}{i}{path.splitext(image)[1]}', i)
def create_gif(base_image, output_gif):
frames = []
images = glob(f'{path.splitext(base_image)[0]}*.{path.splitext(base_image)[1][1:]}')
images.sort(key=path.getmtime)
for image in images:
new_frame = Image.open(image)
frames.append(new_frame)
frames[0].save(output_gif, format='GIF', append_images=frames[1:],
save_all=True, duration=300, loop=0)
for img in images[1:]:
remove(img)
base_image = argv[1]
output_gif = argv[2]
create_frames(base_image, img_to_pixel, 10)
create_gif(base_image, output_gif)
O tópico foi mais pra inserir conceitos novos como a lib "os", a lib "sys" e uma pincelada de leve de programação funcional. Os gifs foram só pra dar uma brincada em cima desses conceitos.