top of page

Creando un mezclador de canales con Pillow


Este blog antiguamente se dedicaba al desarrollo de videojuegos en unity, una de las herramientas que usaba para ahorrar memoria en la GPU es la mezcla de canales en texturas en modelos 3D y en aquel tiempo hice un tutorial de cómo crear un mezclador usando las mismas herramientas de Unity. Esos tiempos ya han cambiado y ahora haremos un pequeño programa en Python que hace lo mismo, que te puede servir para cualquier entorno a parte de Unity.


Preparando las librerías

Este pequeño programa depende de las librerías #Tkinter que ya he hecho algún tutorial por aca y de la librería Pillow que es el corazón de este proyecto.


  • Instala estas librerías basta con abrir una terminal y escribir los siguientes comandos.

$pip install tk
$pip install pillow
$pip install pyinstaller # Este es opcional
  • Crea una carpeta para el proyecto y pon dentro 3 o 4 archivos de imágenes PNG.

  • Crea tu archivo de código Python en la carpeta del proyecto.

Abre el archivo con el editor de código favorito y escribe este código.

from PIL import Image
from tkinter import ttk
import tkinter as tk
from tkinter.filedialog import askopenfilename
import sys

def merge_channels(size:int, routes, save_filename):

    images = []

    for i in range(0,4):
        if routes[i] == '':
            images.append(Image.new('L',[size,size],'white'))
        else:
            img = Image.open(routes[i],'r')
            images.append(img.getchannel(0).resize([size,size]))
    
    result = Image.merge('RGBA',images)
    result.save(save_filename,'png')
    result.show()

class App():
    def __init__(self, window) -> None:
        self.win = window
        self.win.config(width=800, height=600)
        self.win.title('Texture channel mixer')
        
        # Etiqueta y botones
        instructions = ttk.Label(self.win,text='Este programa combina 3 o 4 imágenes grises en uno solo RGBA')
        instructions.grid(column= 0,row= 0, sticky='WE', columnspan=2)
        instructions1 = ttk.Label(self.win,text='** Se creará un canal blanco por cada ruta faltante')
        instructions1.grid(column= 0,row= 11, sticky='WE', columnspan=2)

        filetypes = [('Texture files',r'*.png *.tif'),('All files',r'*.*')]
        # Canal R

        r_filename = tk.StringVar()

        r_label = ttk.Label(self.win,text='R para rough o gloss')
        r_label.grid(column= 0,row= 1, sticky='WE')
        r_entry = ttk.Entry(self.win, textvariable=r_filename)
        r_entry.grid(column= 0, row= 2, sticky='WE',padx= 5)
        r_button = ttk.Button(self.win,text='Examine',command= lambda: r_filename.set(askopenfilename(filetypes=filetypes)))
        r_button.grid(column= 1, row= 2)
        
        # Canal G
        g_filename = tk.StringVar()
        g_label = ttk.Label(self.win,text='G para heigh o metallic')
        g_label.grid(column= 0, row= 3, sticky='WE')
        g_entry = ttk.Entry(self.win,textvariable=g_filename)
        g_entry.grid(column= 0, row= 4, sticky='WE',padx= 5)
        g_button = ttk.Button(self.win, text='Examine',command=lambda: g_filename.set(askopenfilename(filetypes=filetypes)))
        g_button.grid(column= 1, row= 4)
        
        # Canal B
        b_filename = tk.StringVar()
        b_label = ttk.Label(self.win,text='B para ao o specular si está en escala de grises')
        b_label.grid(column= 0, row= 5, sticky='WE')
        b_entry = ttk.Entry(self.win, textvariable=b_filename)
        b_entry.grid(column= 0, row= 6, sticky='WE',padx= 5)
        b_button = ttk.Button(self.win, text='Examine',command=lambda: b_filename.set(askopenfilename(filetypes=filetypes)))
        b_button.grid(column= 1, row= 6)
        
        # Canal A
        a_filename = tk.StringVar()
        a_label = ttk.Label(self.win,text='A para alpha o emission si el color depende de albedo')
        a_label.grid(column= 0, row= 7, sticky='WE')
        a_entry = ttk.Entry(self.win, textvariable=a_filename)
        a_entry.grid(column= 0, row= 8, sticky='WE',padx= 5)
        a_button = ttk.Button(self.win, text='Examine',command=lambda: a_filename.set(askopenfilename(filetypes=filetypes)))
        a_button.grid(column= 1, row= 8)
        
        # Aplicacion
        apply_label = ttk.Label(self.win,text='Ruta y nombre del nuevo archivo resultante')
        apply_label.grid(column= 0, row= 9, sticky='WE')
        apply_entry = ttk.Entry(self.win)
        apply_entry.grid(column= 0, row= 10, sticky='WE',padx= 5)
        # file_list = [r_filename.get(),g_filename.get(),b_filename.get(),a_filename.get()]
        apply_button = ttk.Button(self.win, text='Aplicar',command=lambda:merge_channels(512,[r_filename.get(),g_filename.get(),b_filename.get(),a_filename.get()],apply_entry.get())) # .save(f'{apply_entry.getvar()}.png', 'png')
        apply_button.grid(column= 1, row= 10)


def main():
    # Ventana
    root = tk.Tk()
    application = App(root)
    root.mainloop()

if __name__ == '__main__':
    main()
    
    
Creando la interfaz.

El código de la interfaz es bastante repetitivo, porque simplemente repetimos el mismo formulario usando tkinter para cada uno de los canales, este código está dividido en 6 partes contenidas dentro de una clase para mantener unas mínimas buenas prácticas de desarrollo.

  • Etiqueta - contiene 2 etiquetas con instrucciones.

  • Canal R - Un Entry y un botón de examinar.

  • Canal G - una copia del R

  • Canal B - una copia del R

  • Canal A - una copia del R

  • Aplicación - contiene la ruta de guardado y el botón para invocar la mezcla.

Todos los widgets usados para el formulario usan una distribución #grid porque es más fácil de usar y queda más estética.

r_filename = tk.StringVar()

r_entry = ttk.Entry(self.win, textvariable=r_filename)
r_entry.grid(column= 0, row= 2, sticky='WE',padx= 5)

r_button = ttk.Button(self.win,text='Examine',command= lambda: r_filename.set(askopenfilename(filetypes=filetypes)))

r_button.grid(column= 1, row= 2)

Cada canal contiene 2 widget, un entry y un botón de examinar, que abre un cuadro de dialogo solo para obtener el texto de la ruta completa de la imagen que se va a mezclar y guardarlo en la variable $filename.

def __init__(self, window) -> None:
        self.win = window
        self.win.config(width=800, height=600)
        self.win.title('Texture channel mixer')
        
        # Etiqueta y botones
        instructions = ttk.Label(self.win,text='Este programa combina 3 o 4 imágenes grises en uno solo RGBA')
        instructions.grid(column= 0,row= 0, sticky='WE', columnspan=2)
        instructions1 = ttk.Label(self.win,text='** Se creará un canal blanco por cada ruta faltante')
        instructions1.grid(column= 0,row= 11, sticky='WE', columnspan=2)

        filetypes = [('Texture files',r'*.png *.tif'),('All files',r'*.*')]
        # Canal R

        r_filename = tk.StringVar()

        r_label = ttk.Label(self.win,text='R para rough o gloss')
        r_label.grid(column= 0,row= 1, sticky='WE')
        r_entry = ttk.Entry(self.win, textvariable=r_filename)
        r_entry.grid(column= 0, row= 2, sticky='WE',padx= 5)
        r_button = ttk.Button(self.win,text='Examine',command= lambda: r_filename.set(askopenfilename(filetypes=filetypes)))
        r_button.grid(column= 1, row= 2)
        
        # Canal G
        # Canal B
        # Canal A
        ...
        # Aplicacion
        apply_label = ttk.Label(self.win,text='Ruta y nombre del nuevo archivo resultante')
        apply_label.grid(column= 0, row= 9, sticky='WE')
        apply_entry = ttk.Entry(self.win)
        apply_entry.grid(column= 0, row= 10, sticky='WE',padx= 5)
        # file_list = [r_filename.get(),g_filename.get(),b_filename.get(),a_filename.get()]
        apply_button = ttk.Button(self.win, text='Aplicar',command=lambda:merge_channels(512,[r_filename.get(),g_filename.get(),b_filename.get(),a_filename.get()],apply_entry.get())) # .save(f'{apply_entry.getvar()}.png', 'png')
        apply_button.grid(column= 1, row= 10)

Por último, está la función que hace toda la magia.


def merge_channels(size:int, routes, save_filename): images = [] for i in range(0,4): if routes[i] == '': images.append(Image.new('L',[size,size],'white')) else: img = Image.open(routes[i],'r') images.append(img.getchannel(0).resize([size,size])) result = Image.merge('RGBA',images) result.save(save_filename,'png') result.show()


Si la ruta no existe, crea una nueva imagen blanca y lo agrega a una lista de imágenes, si existe toma el canal rojo y lo guarda en la lista de imágenes, para luego combinarlo con $Image.merge(), posteriormente guarda los cambios y muestra el resultado en el navegador.


Eso es todo por ahora, nos vemos en un proximo post.

4 visualizaciones0 comentarios

Entradas Recientes

Ver todo
bottom of page