Image_Combine / app.py
PSNbst's picture
Update app.py
99c0e27 verified
import gradio as gr
from PIL import Image
import io
import zipfile
import random
import tempfile
import os
def random_black_or_white():
return (0, 0, 0, 255) if random.random() < 0.5 else (255, 255, 255, 255)
def random_non_black_white():
while True:
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
# 不要纯黑纯白
if not (r == g == b == 0 or r == g == b == 255):
return (r, g, b, 255)
def limit_2048(img: Image.Image):
w, h = img.size
if w > 2048 or h > 2048:
scale = min(2048 / w, 2048 / h)
nw = int(w * scale)
nh = int(h * scale)
img = img.resize((nw, nh), Image.Resampling.LANCZOS)
return img
def resize_to_64_multiple(img: Image.Image):
w, h = img.size
w64 = max(64, round(w / 64) * 64)
h64 = max(64, round(h / 64) * 64)
scale = min(w64 / w, h64 / h)
nw = int(w * scale)
nh = int(h * scale)
bg_color = random_black_or_white()
background = Image.new("RGBA", (w64, h64), bg_color)
scaled = img.resize((nw, nh), Image.Resampling.LANCZOS)
ox = (w64 - nw) // 2
oy = (h64 - nh) // 2
background.paste(scaled, (ox, oy), scaled)
return background
def make_collage_2x2(images_4):
w, h = images_4[0].size
collage = Image.new("RGBA", (2*w, 2*h), (0,0,0,255))
collage.paste(images_4[0], (0,0), images_4[0])
collage.paste(images_4[1], (w,0), images_4[1])
collage.paste(images_4[2], (0,h), images_4[2])
collage.paste(images_4[3], (w,h), images_4[3])
return limit_2048(collage)
def make_collage_leftover(images_leftover):
n = len(images_leftover)
if n < 1 or n > 3:
return None
resized_list = [resize_to_64_multiple(img) for img in images_leftover]
max_w = max(im.size[0] for im in resized_list)
max_h = max(im.size[1] for im in resized_list)
uniformed = []
for rimg in resized_list:
w, h = rimg.size
if (w,h) == (max_w, max_h):
uniformed.append(rimg)
else:
bg_color = rimg.getpixel((0,0))
bg = Image.new("RGBA", (max_w, max_h), bg_color)
offx = (max_w - w)//2
offy = (max_h - h)//2
bg.paste(rimg, (offx, offy), rimg)
uniformed.append(bg)
if n == 1:
possible_layouts = [(1,1), (1,2), (2,1), (2,2)]
elif n == 2:
possible_layouts = [(1,2), (2,1), (2,2)]
else: # n == 3
possible_layouts = [(2,2)]
rows, cols = random.choice(possible_layouts)
big_w = cols * max_w
big_h = rows * max_h
collage = Image.new("RGBA", (big_w, big_h), (0,0,0,255))
cells = [(r, c) for r in range(rows) for c in range(cols)]
random.shuffle(cells)
for i, img_ in enumerate(uniformed):
r, c = cells[i]
ox = c * max_w
oy = r * max_h
collage.paste(img_, (ox, oy), img_)
leftover_cells = cells[n:]
for (r, c) in leftover_cells:
color_ = random_non_black_white()
rect = Image.new("RGBA", (max_w, max_h), color_)
collage.paste(rect, (c*max_w, r*max_h), rect)
return limit_2048(collage)
def process_images(uploaded_files):
pil_images = []
for f in uploaded_files:
if f is not None:
img = Image.open(f.name).convert("RGBA")
pil_images.append(img)
results = []
total = len(pil_images)
groups_4 = total // 4
leftover = total % 4
idx = 0
for _ in range(groups_4):
group_4 = pil_images[idx:idx+4]
idx += 4
resized_4 = [resize_to_64_multiple(im) for im in group_4]
max_w = max(im.size[0] for im in resized_4)
max_h = max(im.size[1] for im in resized_4)
final_4 = []
for rimg in resized_4:
w, h = rimg.size
if (w,h) == (max_w, max_h):
final_4.append(rimg)
else:
bg_color = rimg.getpixel((0,0))
bg = Image.new("RGBA", (max_w, max_h), bg_color)
offx = (max_w - w)//2
offy = (max_h - h)//2
bg.paste(rimg, (offx, offy), rimg)
final_4.append(bg)
collage_2x2 = make_collage_2x2(final_4)
results.append(collage_2x2)
if leftover > 0:
leftover_imgs = pil_images[idx:]
collage_left = make_collage_leftover(leftover_imgs)
if collage_left is not None:
results.append(collage_left)
return results
def make_zip(uploaded_files):
"""
真正构建ZIP,并返回临时文件路径(不是BytesIO),
以防 Gradio 把 BytesIO 当成字符串解析而报错。
"""
collages = process_images(uploaded_files)
if not collages:
return None # 让前端做判断
# 在内存里打包
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
for i, img in enumerate(collages, start=1):
img_bytes = io.BytesIO()
img.save(img_bytes, format="PNG")
img_bytes.seek(0)
zf.writestr(f"collage_{i}.png", img_bytes.read())
zip_buffer.seek(0)
# 将 BytesIO 写入临时文件,再返回临时文件路径
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
tmp.write(zip_buffer.getvalue())
tmp_path = tmp.name
return tmp_path
def on_zip_click(files):
"""
返回 (zip_file_path, message) 给两个 output:
1. gr.File 要求返回一个str路径或None
2. gr.Textbox 用来输出提示信息
"""
path = make_zip(files)
if path is None:
return (None, "无可下载内容 - 可能没上传图片或无法拼接")
else:
return (path, "打包完成!点击上方链接下载ZIP")
with gr.Blocks() as demo:
gr.Markdown("## 2×2 拼接小工具(兼容不足4张、随机填充、保留透明)")
with gr.Row():
with gr.Column():
file_input = gr.Files(label="上传多张图片(可多选)", file_types=["image"])
preview_btn = gr.Button("生成预览")
zip_btn = gr.Button("打包下载 ZIP")
with gr.Column():
gallery_out = gr.Gallery(label="拼接结果预览", columns=2)
# 注意:必须 visible=True,这样点击后可以直接更新
zip_file_out = gr.File(label="下载拼接结果 ZIP", visible=True, interactive=False)
msg_out = gr.Textbox(label="提示信息", interactive=False)
preview_btn.click(
fn=process_images,
inputs=[file_input],
outputs=[gallery_out]
)
zip_btn.click(
fn=on_zip_click,
inputs=[file_input],
outputs=[zip_file_out, msg_out]
)
demo.launch()