|
|
import streamlit as st |
|
|
from PIL import Image, ImageEnhance |
|
|
from io import BytesIO |
|
|
from pathlib import Path |
|
|
|
|
|
def load_palette_file(path: Path): |
|
|
"""Load a .hex palette file into a list of RGB tuples.""" |
|
|
colors = [] |
|
|
with open(path, "r") as f: |
|
|
for line in f: |
|
|
hexcode = line.strip().lstrip("#") |
|
|
if len(hexcode) == 6: |
|
|
r = int(hexcode[0:2], 16) |
|
|
g = int(hexcode[2:4], 16) |
|
|
b = int(hexcode[4:6], 16) |
|
|
colors.append((r, g, b)) |
|
|
else: |
|
|
st.warning(f"Invalid hex code in palette file: {line}") |
|
|
return colors |
|
|
|
|
|
def apply_custom_palette(image: Image.Image, colors, dithering: bool): |
|
|
"""Quantize an image to a custom palette with optional dithering.""" |
|
|
|
|
|
palette_img = Image.new("P", (1, 1)) |
|
|
palette_data = [] |
|
|
for r, g, b in colors[:256]: |
|
|
palette_data.extend([r, g, b]) |
|
|
palette_data.extend([0] * (768 - len(palette_data))) |
|
|
palette_img.putpalette(palette_data) |
|
|
|
|
|
|
|
|
dither_mode = Image.FLOYDSTEINBERG if dithering else Image.NONE |
|
|
image = image.quantize(palette=palette_img, dither=dither_mode, method=Image.MEDIANCUT) |
|
|
return image.convert("RGB") |
|
|
|
|
|
st.set_page_config(page_title="Pixelizer", layout="wide") |
|
|
st.sidebar.title("Pixelizer") |
|
|
st.sidebar.header("Image Controls") |
|
|
uploaded_file = st.sidebar.file_uploader("Upload image", type=["png", "jpg", "jpeg"]) |
|
|
show_original = st.sidebar.toggle("Show original", value=False) |
|
|
dithering = st.sidebar.toggle("Enable dithering", value=True) |
|
|
pixel_size = st.sidebar.number_input("Pixel size", min_value=1, max_value=100, value=4, step=1) |
|
|
palette_dir = Path("palettes") |
|
|
palette_files = sorted(palette_dir.glob("*.hex")) |
|
|
palette_names = [f.stem for f in palette_files] |
|
|
palette = st.sidebar.selectbox("Palette", palette_names) |
|
|
|
|
|
contrast = st.sidebar.slider("Contrast", 0.5, 2.0, 1.0, 0.1) |
|
|
brightness = st.sidebar.slider("Brightness", 0.5, 2.0, 1.0, 0.1) |
|
|
|
|
|
if uploaded_file: |
|
|
original_image = Image.open(uploaded_file).convert("RGB") |
|
|
|
|
|
|
|
|
image = original_image.copy() |
|
|
enhancer_contrast = ImageEnhance.Contrast(image) |
|
|
image = enhancer_contrast.enhance(contrast) |
|
|
enhancer_brightness = ImageEnhance.Brightness(image) |
|
|
image = enhancer_brightness.enhance(brightness) |
|
|
|
|
|
|
|
|
processed_image = image.copy() |
|
|
w, h = processed_image.size |
|
|
processed_image = processed_image.resize( |
|
|
(max(1, w // pixel_size), max(1, h // pixel_size)), |
|
|
resample=Image.NEAREST |
|
|
) |
|
|
|
|
|
|
|
|
palette_file = palette_dir / f"{palette}.hex" |
|
|
processed_image = apply_custom_palette(processed_image, load_palette_file(palette_file), dithering) |
|
|
upscaled_processed_image = processed_image.resize((w, h), Image.NEAREST) |
|
|
|
|
|
|
|
|
if show_original: |
|
|
st.image(original_image, caption="Original", width="content") |
|
|
else: |
|
|
st.image(upscaled_processed_image, caption="Processed", width="content") |
|
|
|
|
|
|
|
|
buf = BytesIO() |
|
|
processed_image.save(buf, format="PNG") |
|
|
byte_im = buf.getvalue() |
|
|
|
|
|
st.sidebar.download_button( |
|
|
"Save image", |
|
|
data=byte_im, |
|
|
file_name="pixelized.png", |
|
|
mime="image/png" |
|
|
) |
|
|
|
|
|
|