Presentación
En el trabajo que vengo realizando en el desarrollo de la librería PantallaE213 surgio la necesidad de contar con archivos de imagenes binarios para poder subirlos a la placa Vision Master E213 (HT-VME213).
Con ese objetivo se desarrollo el programa que les presento a continuacion el cual preve ademas de la conversión a binario manteniendo el tamaño original de la imagen, convertir al tamaño de la pantalla Vision Master E213 (HT-VME213) (122 x 250 píxeles) y tambien convertirla a un tamaño personalizado (puedes indicar ancho y alto) .
Descripción del programa
Es una herramienta de escritorio con interfaz gráfica que permite convertir imágenes comunes (PNG, JPG, BMP, etc.) en archivos binarios de 1 bit por píxel, listos para ser enviados directamente a pantallas e-ink monocromas.¿Cómo funciona?
Selección de imagen:
– El usuario carga cualquier imagen desde su computadora mediante un explorador de archivos intuitivo.
Opciones de tamaño:
– Mantener tamaño original: conserva las dimensiones de la imagen fuente.
– 122 × 250 píxeles: resolución optimizada para pantallas e-ink típicas.
– Tamaño personalizado: permite definir ancho y alto manualmente.
Procesamiento automático:
– La imagen pasa por una serie de mejoras automáticas:
• Conversión a escala de grises.
• Ajuste automático de contraste.
• Ligero realce de brillo y contraste.
• Suavizado para reducir ruido.
• Dithering Stucki: un algoritmo avanzado que simula niveles de gris usando solo blanco y negro, ideal para pantallas e-ink.
Generación del binario:
– Cada píxel se codifica como un bit: 0 = blanco, 1 = negro.
– Los bits se empaquetan en bytes, ordenados de izquierda a derecha, con el bit más significativo primero.
– Cuando se elige la resolución 122 × 250, el programa genera un archivo de exactamente 4000 bytes, compatible con buffers de memoria que exigen este tamaño fijo (incluyendo el padding necesario para alineación).
Previsualización integrada:
– Muestra en tiempo real cómo se verá la imagen convertida en la pantalla e-ink, junto con información técnica: dimensiones y tamaño del archivo binario resultante.
Código del programa
A continuacion coloco el archivo convertir.py que contiene el codigo del programa.
Archivo: convertir.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Conversor de imágenes a formato 1-bit para e-ink con interfaz gráfica
Permite convertir imágenes a formato binario con diferentes opciones de tamaño
Incluye previsualización de la imagen convertida
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, Canvas
from PIL import Image, ImageOps, ImageEnhance, ImageFilter, ImageTk
import os
from pathlib import Path
# -----------------------------
# DITHERING STUCKI
# -----------------------------
STUCKI_KERNEL = [
(1, 0, 8/42), (2, 0, 4/42),
(-2, 1, 2/42), (-1, 1, 4/42), (0, 1, 8/42), (1, 1, 4/42), (2, 1, 2/42),
(-2, 2, 1/42), (-1, 2, 2/42), (0, 2, 4/42), (1, 2, 2/42), (2, 2, 1/42),
]
def dither_stucki(img):
"""Aplica dithering Stucki manualmente"""
w, h = img.size
pix = img.load()
for y in range(h):
for x in range(w):
old = pix[x, y]
new = 255 if old > 127 else 0
pix[x, y] = new
err = old - new
for dx, dy, factor in STUCKI_KERNEL:
nx = x + dx
ny = y + dy
if 0 <= nx < w and 0 <= ny < h:
pix[nx, ny] = max(0, min(255, pix[nx, ny] + int(err * factor)))
return img
def convertir_imagen(input_path, output_path, ancho=None, alto=None):
"""
Convierte imagen a formato 1-bit para e-ink
Si ancho y alto son None, mantiene dimensiones originales
Retorna la imagen procesada además de las estadísticas
"""
# Abrir imagen
img = Image.open(input_path)
# Determinar dimensiones finales
if ancho is None or alto is None:
ANCHO, ALTO = img.size
else:
ANCHO, ALTO = ancho, alto
# Convertir a escala de grises
img = ImageOps.grayscale(img)
# Auto-contraste moderado
img = ImageOps.autocontrast(img, cutoff=2)
# Ajuste de brillo/contraste
img = ImageEnhance.Brightness(img).enhance(1.05)
img = ImageEnhance.Contrast(img).enhance(1.15)
# Reescalar si es necesario
if img.size != (ANCHO, ALTO):
img = img.resize((ANCHO, ALTO), Image.LANCZOS)
# Suavizado leve
img = img.filter(ImageFilter.GaussianBlur(radius=0.4))
# Dithering
img = dither_stucki(img)
# Convertir a 1-bit
pix = img.load()
data = bytearray()
for y in range(ALTO):
for x_byte in range((ANCHO + 7) // 8):
byte = 0
for bit in range(8):
x = x_byte * 8 + bit
if x < ANCHO:
if pix[x, y] < 128:
byte |= (0x80 >> bit)
data.append(byte)
# Guardar archivo binario
with open(output_path, "wb") as f:
f.write(data)
return img, ANCHO, ALTO, len(data)
# -----------------------------
# INTERFAZ GRÁFICA
# -----------------------------
class ConversorEInkGUI:
def __init__(self, root):
self.root = root
self.root.title("Conversor de Imágenes E-Ink")
self.root.geometry("1000x500")
# Variables
self.archivo_seleccionado = tk.StringVar()
self.opcion_tamaño = tk.IntVar(value=1)
self.ancho_custom = tk.StringVar(value="122")
self.alto_custom = tk.StringVar(value="250")
self.imagen_preview = None
# Crear carpetas si no existen
self.crear_carpetas()
# Construir interfaz
self.construir_interfaz()
def crear_carpetas(self):
"""Crea las carpetas originales y binarios si no existen"""
Path("originales").mkdir(exist_ok=True)
Path("binarios").mkdir(exist_ok=True)
def construir_interfaz(self):
# Frame principal dividido en dos columnas
frame_principal = ttk.Frame(self.root)
frame_principal.pack(fill="both", expand=True, padx=5, pady=5)
# Columna izquierda (controles)
frame_izquierda = ttk.Frame(frame_principal)
frame_izquierda.pack(side="left", fill="both", expand=False, padx=5)
# Columna derecha (preview)
frame_derecha = ttk.LabelFrame(frame_principal, text="Vista Previa", padding=10)
frame_derecha.pack(side="right", fill="both", expand=True, padx=5)
# === COLUMNA IZQUIERDA ===
# Título
titulo = tk.Label(frame_izquierda, text="Conversor E-Ink",
font=("Arial", 14, "bold"))
titulo.pack(pady=10)
# Frame de selección de archivo
frame_archivo = ttk.LabelFrame(frame_izquierda, text="Archivo de Entrada", padding=10)
frame_archivo.pack(pady=10, fill="x")
self.label_archivo = ttk.Label(frame_archivo, text="Ningún archivo seleccionado",
foreground="gray", wraplength=350)
self.label_archivo.pack(pady=5)
ttk.Button(frame_archivo, text="Seleccionar imagen",
command=self.seleccionar_archivo).pack(pady=5)
# Frame de opciones de tamaño
frame_opciones = ttk.LabelFrame(frame_izquierda, text="Opciones de Tamaño", padding=10)
frame_opciones.pack(pady=10, fill="x")
# Opción 1: Mantener tamaño original
ttk.Radiobutton(frame_opciones, text="Mantener tamaño original",
variable=self.opcion_tamaño, value=1,
command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
# Opción 2: Tamaño fijo 122x250
ttk.Radiobutton(frame_opciones, text="122 × 250 píxeles",
variable=self.opcion_tamaño, value=2,
command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
# Opción 3: Tamaño personalizado
ttk.Radiobutton(frame_opciones, text="Tamaño personalizado:",
variable=self.opcion_tamaño, value=3,
command=self.actualizar_estado_campos).pack(anchor="w", pady=3)
# Frame para entrada personalizada
frame_custom = ttk.Frame(frame_opciones)
frame_custom.pack(anchor="w", padx=20, pady=3)
ttk.Label(frame_custom, text="Ancho:").grid(row=0, column=0, padx=3)
self.entry_ancho = ttk.Entry(frame_custom, textvariable=self.ancho_custom, width=8)
self.entry_ancho.grid(row=0, column=1, padx=3)
ttk.Label(frame_custom, text="Alto:").grid(row=0, column=2, padx=3)
self.entry_alto = ttk.Entry(frame_custom, textvariable=self.alto_custom, width=8)
self.entry_alto.grid(row=0, column=3, padx=3)
# Botón de conversión
self.btn_convertir = ttk.Button(frame_izquierda, text="Convertir Imagen",
command=self.convertir, state="disabled")
self.btn_convertir.pack(pady=15)
# Área de estado
frame_estado = ttk.LabelFrame(frame_izquierda, text="Estado", padding=10)
frame_estado.pack(pady=5, fill="x")
self.label_estado = tk.Label(frame_estado, text="Seleccione una imagen",
justify="left", anchor="w", wraplength=350)
self.label_estado.pack(fill="x")
# === COLUMNA DERECHA (PREVIEW) ===
# Canvas para mostrar la imagen
self.canvas = Canvas(frame_derecha, bg="white", highlightthickness=1,
highlightbackground="gray")
self.canvas.pack(fill="both", expand=True)
# Label para información de la imagen
self.label_info_preview = ttk.Label(frame_derecha, text="",
justify="center", font=("Arial", 9))
self.label_info_preview.pack(pady=5)
# Inicializar estado de campos
self.actualizar_estado_campos()
def actualizar_estado_campos(self):
"""Habilita/deshabilita campos según la opción seleccionada"""
if self.opcion_tamaño.get() == 3:
self.entry_ancho.config(state="normal")
self.entry_alto.config(state="normal")
else:
self.entry_ancho.config(state="disabled")
self.entry_alto.config(state="disabled")
def seleccionar_archivo(self):
"""Abre diálogo para seleccionar archivo """
archivo = filedialog.askopenfilename(
title="Seleccionar imagen",
initialdir="originales",
filetypes=[
("Imágenes", "*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp"),
("Archivos PNG", "*.png"),
("Archivos JPG", "*.jpg *.jpeg"),
("Todos los archivos", "*.*")
]
)
if archivo:
self.archivo_seleccionado.set(archivo)
self.btn_convertir.config(state="normal")
nombre_archivo = os.path.basename(archivo)
self.label_archivo.config(text=nombre_archivo, foreground="blue")
self.label_estado.config(text=f"Listo para convertir: {nombre_archivo}")
def mostrar_preview(self, img, ancho, alto, tamaño_bytes):
"""Muestra la imagen convertida en el canvas"""
# Limpiar canvas
self.canvas.delete("all")
# Calcular escala para ajustar al canvas
canvas_ancho = self.canvas.winfo_width()
canvas_alto = self.canvas.winfo_height()
# Esperar a que el canvas tenga dimensiones válidas
if canvas_ancho <= 1 or canvas_alto <= 1:
self.root.update()
canvas_ancho = self.canvas.winfo_width()
canvas_alto = self.canvas.winfo_height()
# Calcular escala manteniendo proporción
escala_ancho = (canvas_ancho - 40) / ancho
escala_alto = (canvas_alto - 40) / alto
escala = min(escala_ancho, escala_alto, 1.0) # No ampliar más allá del tamaño original
# Redimensionar para preview si es necesario
if escala < 1.0:
nuevo_ancho = int(ancho * escala)
nuevo_alto = int(alto * escala)
img_preview = img.resize((nuevo_ancho, nuevo_alto), Image.NEAREST)
else:
img_preview = img
# Convertir para mostrar en Tkinter
self.imagen_preview = ImageTk.PhotoImage(img_preview)
# Centrar en canvas
x_pos = canvas_ancho // 2
y_pos = canvas_alto // 2
self.canvas.create_image(x_pos, y_pos, image=self.imagen_preview)
# Actualizar información
info = f"{ancho} × {alto} px | {tamaño_bytes} bytes"
self.label_info_preview.config(text=info)
def convertir(self):
"""Ejecuta la conversión de la imagen"""
archivo_entrada = self.archivo_seleccionado.get()
if not archivo_entrada:
messagebox.showwarning("Advertencia", "Debe seleccionar un archivo primero")
return
try:
# Determinar dimensiones según opción
if self.opcion_tamaño.get() == 1:
# Mantener original
ancho, alto = None, None
elif self.opcion_tamaño.get() == 2:
# Tamaño fijo
ancho, alto = 122, 250
else:
# Personalizado
try:
ancho = int(self.ancho_custom.get())
alto = int(self.alto_custom.get())
if ancho <= 0 or alto <= 0:
raise ValueError
except ValueError:
messagebox.showerror("Error", "Las dimensiones deben ser números enteros positivos")
return
# Generar nombre de archivo de salida
nombre_base = os.path.splitext(os.path.basename(archivo_entrada))[0]
archivo_salida = os.path.join("binarios", f"{nombre_base}.bin")
# Convertir
self.label_estado.config(text="Convirtiendo imagen...")
self.root.update()
img_resultado, ancho_final, alto_final, tamaño_bytes = convertir_imagen(
archivo_entrada, archivo_salida, ancho, alto
)
# Mostrar preview
self.mostrar_preview(img_resultado, ancho_final, alto_final, tamaño_bytes)
# Actualizar estado
self.label_estado.config(
text=f"✓ Conversión exitosa\n{archivo_salida}\n{ancho_final}×{alto_final} px, {tamaño_bytes} bytes"
)
messagebox.showinfo("Éxito", f"Imagen convertida correctamente:\n\n{archivo_salida}")
except Exception as e:
messagebox.showerror("Error", f"Error durante la conversión:\n{str(e)}")
self.label_estado.config(text="Error en la conversión")
# -----------------------------
# PUNTO DE ENTRADA
# -----------------------------
if __name__ == "__main__":
root = tk.Tk()
app = ConversorEInkGUI(root)
root.mainloop()
Observacion
Este programa se puede hacer funcionar en un entorno venv con python 3 instalando la librería Pillow (o PIL, Python Imaging Library) necesario para: abrir y manipular imágenes (Image, ImageOps, ImageEnhance, ImageFilter, etc.); aplicar dithering, redimensionamiento, conversión a escala de grises y a 1-bit; generar la previsualización en la interfaz.