Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -3,48 +3,40 @@ from PIL import Image
|
|
3 |
import io
|
4 |
import zipfile
|
5 |
|
6 |
-
def
|
7 |
-
"""
|
8 |
-
将单张图片缩放到 (w', h'),其中 w'、h' 均为 64 的整数倍。
|
9 |
-
若原图宽高比与 w'/h' 不完全一致,则在空余处用 fill_color 填充(不裁切)。
|
10 |
-
"""
|
11 |
w, h = img.size
|
12 |
-
|
13 |
-
# 计算最接近的 64 倍数
|
14 |
-
# 若 round(...) 结果为 0,至少保底 64
|
15 |
w64 = max(64, round(w / 64) * 64)
|
16 |
h64 = max(64, round(h / 64) * 64)
|
17 |
|
18 |
-
#
|
19 |
scale = min(w64 / w, h64 / h)
|
20 |
new_width = int(w * scale)
|
21 |
new_height = int(h * scale)
|
22 |
|
23 |
-
#
|
24 |
-
|
25 |
-
#
|
26 |
scaled_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
27 |
-
|
|
|
28 |
offset_x = (w64 - new_width) // 2
|
29 |
offset_y = (h64 - new_height) // 2
|
30 |
-
|
31 |
-
return
|
32 |
|
33 |
-
def
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
""
|
38 |
-
|
39 |
-
w,
|
40 |
-
collage
|
41 |
-
|
42 |
-
collage.paste(four_images[0], (0, 0))
|
43 |
-
collage.paste(four_images[1], (w, 0))
|
44 |
-
collage.paste(four_images[2], (0, h))
|
45 |
-
collage.paste(four_images[3], (w, h))
|
46 |
|
47 |
cw, ch = collage.size
|
|
|
48 |
if cw > 2048 or ch > 2048:
|
49 |
scale = min(2048 / cw, 2048 / ch)
|
50 |
new_cw = int(cw * scale)
|
@@ -53,99 +45,78 @@ def make_collage(four_images, fill_color=(0, 0, 0)):
|
|
53 |
|
54 |
return collage
|
55 |
|
56 |
-
def
|
57 |
-
"""
|
58 |
-
处理用户上传的多张图片:
|
59 |
-
1. 每组 4 张图 -> 统一到 64 的倍数尺寸 -> 拼成 2×2 的大图
|
60 |
-
2. 大于 2048×2048 则缩小
|
61 |
-
3. 返回多张拼接图(以列表的形式)供 Gradio 显示
|
62 |
-
4. 若最后一组不足 4 张,则跳过
|
63 |
-
"""
|
64 |
-
# 读取成 PIL 对象
|
65 |
pil_images = []
|
66 |
-
for
|
67 |
-
if
|
68 |
-
|
|
|
69 |
pil_images.append(img)
|
70 |
-
|
71 |
results = []
|
72 |
-
#
|
73 |
for i in range(0, len(pil_images), 4):
|
74 |
group = pil_images[i : i + 4]
|
75 |
if len(group) < 4:
|
76 |
-
# 不足 4 张,跳过
|
77 |
break
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
collage = make_collage(resized, fill_color=(0,0,0))
|
83 |
results.append(collage)
|
84 |
|
85 |
return results
|
86 |
|
87 |
-
def
|
88 |
-
|
89 |
-
在内存中创建 Zip,将所有生成的拼接图打包。
|
90 |
-
返回一个 BytesIO 给 Gradio 的 File 输出,用于下载。
|
91 |
-
"""
|
92 |
-
# 先按需求生成拼接结果
|
93 |
-
collages = process_images(uploaded_files)
|
94 |
-
|
95 |
-
# 如果没有生成任何拼接图,则返回 None,让前端不显示下载
|
96 |
if not collages:
|
|
|
97 |
return None
|
98 |
|
99 |
-
# 在内存中创建 zip
|
100 |
buf = io.BytesIO()
|
101 |
-
with zipfile.ZipFile(buf, "w") as zf:
|
102 |
for idx, img in enumerate(collages):
|
103 |
-
#
|
104 |
img_bytes = io.BytesIO()
|
|
|
105 |
img.save(img_bytes, format="PNG")
|
106 |
img_bytes.seek(0)
|
107 |
zf.writestr(f"collage_{idx+1}.png", img_bytes.read())
|
108 |
buf.seek(0)
|
109 |
return buf
|
110 |
|
111 |
-
|
112 |
-
# 构建 Gradio 界面
|
113 |
-
########################################
|
114 |
-
css_custom = """
|
115 |
-
/* 你可根据需要,在此自定义或覆盖 Gradio 的 CSS 样式 */
|
116 |
-
"""
|
117 |
-
|
118 |
-
with gr.Blocks(css=css_custom) as demo:
|
119 |
gr.Markdown("## 图片 2×2 拼接小工具")
|
120 |
gr.Markdown(
|
121 |
-
"1.
|
122 |
-
"2.
|
123 |
-
"3.
|
124 |
-
"4.
|
|
|
125 |
)
|
126 |
|
127 |
with gr.Row():
|
128 |
with gr.Column():
|
129 |
file_input = gr.Files(label="上传图片(可多选)", file_types=["image"])
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
with gr.Column():
|
134 |
-
|
135 |
-
|
|
|
136 |
|
137 |
-
# 点击“生成预览” ->
|
138 |
-
|
139 |
-
fn=
|
140 |
inputs=[file_input],
|
141 |
outputs=[gallery_output]
|
142 |
)
|
143 |
|
144 |
-
# 点击“打包下载 ZIP” -> 生成 zip
|
145 |
-
|
146 |
-
fn=
|
147 |
inputs=[file_input],
|
148 |
-
outputs=[
|
149 |
)
|
150 |
|
151 |
demo.launch()
|
|
|
3 |
import io
|
4 |
import zipfile
|
5 |
|
6 |
+
def resize_image_to_multiple_of_64_rgba(img: Image.Image):
|
|
|
|
|
|
|
|
|
7 |
w, h = img.size
|
8 |
+
# 计算最接近的 64 倍数(保底64)
|
|
|
|
|
9 |
w64 = max(64, round(w / 64) * 64)
|
10 |
h64 = max(64, round(h / 64) * 64)
|
11 |
|
12 |
+
# 按最小缩放比,使原图能贴进 w64 x h64
|
13 |
scale = min(w64 / w, h64 / h)
|
14 |
new_width = int(w * scale)
|
15 |
new_height = int(h * scale)
|
16 |
|
17 |
+
# 用 RGBA、黑色背景(全不透明)
|
18 |
+
background = Image.new("RGBA", (w64, h64), (0, 0, 0, 255))
|
19 |
+
# 缩放原图到 new_width, new_height
|
20 |
scaled_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
21 |
+
|
22 |
+
# 贴到背景中央,保留其透明度
|
23 |
offset_x = (w64 - new_width) // 2
|
24 |
offset_y = (h64 - new_height) // 2
|
25 |
+
background.paste(scaled_img, (offset_x, offset_y), scaled_img)
|
26 |
+
return background
|
27 |
|
28 |
+
def make_collage_2x2(four_rgba_images):
|
29 |
+
# 4 张图,均已是 RGBA、同尺寸
|
30 |
+
w, h = four_rgba_images[0].size
|
31 |
+
# 新画布:2x2 => 宽度 2*w,高度 2*h,背景黑
|
32 |
+
collage = Image.new("RGBA", (2 * w, 2 * h), (0, 0, 0, 255))
|
33 |
+
collage.paste(four_rgba_images[0], (0, 0), four_rgba_images[0])
|
34 |
+
collage.paste(four_rgba_images[1], (w, 0), four_rgba_images[1])
|
35 |
+
collage.paste(four_rgba_images[2], (0, h), four_rgba_images[2])
|
36 |
+
collage.paste(four_rgba_images[3], (w, h), four_rgba_images[3])
|
|
|
|
|
|
|
|
|
37 |
|
38 |
cw, ch = collage.size
|
39 |
+
# 若合并后任意边 > 2048,则等比例缩小
|
40 |
if cw > 2048 or ch > 2048:
|
41 |
scale = min(2048 / cw, 2048 / ch)
|
42 |
new_cw = int(cw * scale)
|
|
|
45 |
|
46 |
return collage
|
47 |
|
48 |
+
def process_images_for_preview(uploaded_files):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
pil_images = []
|
50 |
+
for f in uploaded_files:
|
51 |
+
if f is not None:
|
52 |
+
# 以 RGBA 读图,保证可保留透明通道
|
53 |
+
img = Image.open(f.name).convert("RGBA")
|
54 |
pil_images.append(img)
|
55 |
+
|
56 |
results = []
|
57 |
+
# 每 4 张为一组,不足 4 张跳过
|
58 |
for i in range(0, len(pil_images), 4):
|
59 |
group = pil_images[i : i + 4]
|
60 |
if len(group) < 4:
|
|
|
61 |
break
|
62 |
+
# 每张做64倍数 resize + 黑色背景填充
|
63 |
+
resized = [resize_image_to_multiple_of_64_rgba(im) for im in group]
|
64 |
+
# 再 2×2 拼接
|
65 |
+
collage = make_collage_2x2(resized)
|
|
|
66 |
results.append(collage)
|
67 |
|
68 |
return results
|
69 |
|
70 |
+
def process_and_zip_for_download(uploaded_files):
|
71 |
+
collages = process_images_for_preview(uploaded_files)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
if not collages:
|
73 |
+
# 没有任何拼接图,就返回 None,让界面不显示可下载链接
|
74 |
return None
|
75 |
|
|
|
76 |
buf = io.BytesIO()
|
77 |
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
78 |
for idx, img in enumerate(collages):
|
79 |
+
# 转字节并写入 zip
|
80 |
img_bytes = io.BytesIO()
|
81 |
+
# 以 PNG 格式保存,带 RGBA
|
82 |
img.save(img_bytes, format="PNG")
|
83 |
img_bytes.seek(0)
|
84 |
zf.writestr(f"collage_{idx+1}.png", img_bytes.read())
|
85 |
buf.seek(0)
|
86 |
return buf
|
87 |
|
88 |
+
with gr.Blocks() as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
gr.Markdown("## 图片 2×2 拼接小工具")
|
90 |
gr.Markdown(
|
91 |
+
"1. 一次可上传多张图片,每 4 张为一组。\n"
|
92 |
+
"2. 每组里 4 张图会先缩放到 64 的倍数、空余处用黑色填充,然后 2×2 拼接。\n"
|
93 |
+
"3. 拼接结果若超过 2048×2048,则缩小到不超过 2048。\n"
|
94 |
+
"4. 不足 4 张的剩余图片忽略。\n"
|
95 |
+
"5. 支持保留 PNG 透明度,拼图时使用黑色背景。"
|
96 |
)
|
97 |
|
98 |
with gr.Row():
|
99 |
with gr.Column():
|
100 |
file_input = gr.Files(label="上传图片(可多选)", file_types=["image"])
|
101 |
+
preview_btn = gr.Button("生成预览")
|
102 |
+
download_btn = gr.Button("打包下载 ZIP")
|
|
|
103 |
with gr.Column():
|
104 |
+
# Gallery 构造上不使用 .style() 以兼容老版本
|
105 |
+
gallery_output = gr.Gallery(label="拼接结果预览", columns=2)
|
106 |
+
zip_output = gr.File(label="下载拼接结果 ZIP", interactive=False, visible=False)
|
107 |
|
108 |
+
# 点击“生成预览” -> 返回拼接图列表
|
109 |
+
preview_btn.click(
|
110 |
+
fn=process_images_for_preview,
|
111 |
inputs=[file_input],
|
112 |
outputs=[gallery_output]
|
113 |
)
|
114 |
|
115 |
+
# 点击“打包下载 ZIP” -> 生成 zip
|
116 |
+
download_btn.click(
|
117 |
+
fn=process_and_zip_for_download,
|
118 |
inputs=[file_input],
|
119 |
+
outputs=[zip_output]
|
120 |
)
|
121 |
|
122 |
demo.launch()
|