Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -3,38 +3,31 @@ from PIL import Image
|
|
3 |
import io
|
4 |
import zipfile
|
5 |
import random
|
|
|
|
|
6 |
|
7 |
def random_black_or_white():
|
8 |
-
"""返回 RGBA (黑 或 白),50% 概率"""
|
9 |
return (0, 0, 0, 255) if random.random() < 0.5 else (255, 255, 255, 255)
|
10 |
|
11 |
def random_non_black_white():
|
12 |
-
"""
|
13 |
-
返回 (r,g,b,a),且不是纯黑(0,0,0)也不是纯白(255,255,255)。
|
14 |
-
用于最后不足4张时的空格颜色填充。
|
15 |
-
"""
|
16 |
while True:
|
17 |
r = random.randint(0, 255)
|
18 |
g = random.randint(0, 255)
|
19 |
b = random.randint(0, 255)
|
|
|
20 |
if not (r == g == b == 0 or r == g == b == 255):
|
21 |
return (r, g, b, 255)
|
22 |
|
23 |
def limit_2048(img: Image.Image):
|
24 |
-
"""若宽或高大于2048,则等比例缩小到不超过2048。"""
|
25 |
w, h = img.size
|
26 |
if w > 2048 or h > 2048:
|
27 |
scale = min(2048 / w, 2048 / h)
|
28 |
-
|
29 |
-
|
30 |
-
img = img.resize((
|
31 |
return img
|
32 |
|
33 |
def resize_to_64_multiple(img: Image.Image):
|
34 |
-
"""
|
35 |
-
将图像就近缩放到(w', h'),其中 w'、h'均为64倍数(至少64)。
|
36 |
-
空余区域随机黑/白填充,保留透明度。
|
37 |
-
"""
|
38 |
w, h = img.size
|
39 |
w64 = max(64, round(w / 64) * 64)
|
40 |
h64 = max(64, round(h / 64) * 64)
|
@@ -43,7 +36,7 @@ def resize_to_64_multiple(img: Image.Image):
|
|
43 |
nw = int(w * scale)
|
44 |
nh = int(h * scale)
|
45 |
|
46 |
-
bg_color = random_black_or_white()
|
47 |
background = Image.new("RGBA", (w64, h64), bg_color)
|
48 |
scaled = img.resize((nw, nh), Image.Resampling.LANCZOS)
|
49 |
ox = (w64 - nw) // 2
|
@@ -52,75 +45,57 @@ def resize_to_64_multiple(img: Image.Image):
|
|
52 |
return background
|
53 |
|
54 |
def make_collage_2x2(images_4):
|
55 |
-
"""传入4张同尺寸RGBA,以2×2拼接,并限制在2048以内。"""
|
56 |
w, h = images_4[0].size
|
57 |
-
collage = Image.new("RGBA", (2*w, 2*h), (0,
|
58 |
-
collage.paste(images_4[0], (0,
|
59 |
-
collage.paste(images_4[1], (w,
|
60 |
-
collage.paste(images_4[2], (0,
|
61 |
-
collage.paste(images_4[3], (w,
|
62 |
return limit_2048(collage)
|
63 |
|
64 |
def make_collage_leftover(images_leftover):
|
65 |
-
"""
|
66 |
-
针对不足4张(1~3)的情况,随机布局(1x1,1x2,2x1,2x2等)容纳这些图片,
|
67 |
-
其余空格以“非黑非白”的纯色填充,并最终限制2048以内。
|
68 |
-
"""
|
69 |
n = len(images_leftover)
|
70 |
if n < 1 or n > 3:
|
71 |
return None
|
72 |
|
73 |
-
# 先做“64倍数随机黑/白填充”缩放
|
74 |
resized_list = [resize_to_64_multiple(img) for img in images_leftover]
|
75 |
-
# 找最大宽、高,统一尺寸
|
76 |
max_w = max(im.size[0] for im in resized_list)
|
77 |
max_h = max(im.size[1] for im in resized_list)
|
78 |
|
79 |
-
# 再居中贴到背景 (max_w, max_h) 保证每张图一致
|
80 |
uniformed = []
|
81 |
for rimg in resized_list:
|
82 |
w, h = rimg.size
|
83 |
-
if (w,
|
84 |
uniformed.append(rimg)
|
85 |
else:
|
86 |
-
# 用本身的背景色(左上像素)来填充
|
87 |
bg_color = rimg.getpixel((0,0))
|
88 |
bg = Image.new("RGBA", (max_w, max_h), bg_color)
|
89 |
-
offx = (max_w - w)
|
90 |
-
offy = (max_h - h)
|
91 |
bg.paste(rimg, (offx, offy), rimg)
|
92 |
uniformed.append(bg)
|
93 |
|
94 |
-
# 选定随机布局
|
95 |
-
# n=1: (1x1),(1x2),(2x1),(2x2)
|
96 |
-
# n=2: (1x2),(2x1),(2x2)
|
97 |
-
# n=3: (2x2)
|
98 |
-
possible_layouts = []
|
99 |
if n == 1:
|
100 |
possible_layouts = [(1,1), (1,2), (2,1), (2,2)]
|
101 |
elif n == 2:
|
102 |
possible_layouts = [(1,2), (2,1), (2,2)]
|
103 |
-
else:
|
104 |
possible_layouts = [(2,2)]
|
105 |
|
106 |
rows, cols = random.choice(possible_layouts)
|
107 |
-
|
108 |
big_w = cols * max_w
|
109 |
big_h = rows * max_h
|
110 |
collage = Image.new("RGBA", (big_w, big_h), (0,0,0,255))
|
111 |
|
112 |
-
# 网格坐标
|
113 |
cells = [(r, c) for r in range(rows) for c in range(cols)]
|
114 |
random.shuffle(cells)
|
115 |
|
116 |
-
# 把uniformed中的图贴到前n个格子
|
117 |
for i, img_ in enumerate(uniformed):
|
118 |
r, c = cells[i]
|
119 |
-
|
120 |
-
|
121 |
-
collage.paste(img_, (
|
122 |
|
123 |
-
# 剩余格子(空格)用“非黑非白”的随机色填充
|
124 |
leftover_cells = cells[n:]
|
125 |
for (r, c) in leftover_cells:
|
126 |
color_ = random_non_black_white()
|
@@ -130,15 +105,9 @@ def make_collage_leftover(images_leftover):
|
|
130 |
return limit_2048(collage)
|
131 |
|
132 |
def process_images(uploaded_files):
|
133 |
-
"""
|
134 |
-
1) 分成若干4张组 => each 2x2;
|
135 |
-
2) 对最后剩余(1~3张),make_collage_leftover 处理。
|
136 |
-
3) 返回所有结果图
|
137 |
-
"""
|
138 |
pil_images = []
|
139 |
for f in uploaded_files:
|
140 |
if f is not None:
|
141 |
-
# 保留透明
|
142 |
img = Image.open(f.name).convert("RGBA")
|
143 |
pil_images.append(img)
|
144 |
|
@@ -148,23 +117,19 @@ def process_images(uploaded_files):
|
|
148 |
leftover = total % 4
|
149 |
|
150 |
idx = 0
|
151 |
-
# 先拼满4张的组
|
152 |
for _ in range(groups_4):
|
153 |
group_4 = pil_images[idx:idx+4]
|
154 |
idx += 4
|
155 |
-
# 每张先resize
|
156 |
resized_4 = [resize_to_64_multiple(im) for im in group_4]
|
157 |
-
# 再统一max_w, max_h
|
158 |
max_w = max(im.size[0] for im in resized_4)
|
159 |
max_h = max(im.size[1] for im in resized_4)
|
160 |
|
161 |
final_4 = []
|
162 |
for rimg in resized_4:
|
163 |
w, h = rimg.size
|
164 |
-
if (w,
|
165 |
final_4.append(rimg)
|
166 |
else:
|
167 |
-
# 补背景居中
|
168 |
bg_color = rimg.getpixel((0,0))
|
169 |
bg = Image.new("RGBA", (max_w, max_h), bg_color)
|
170 |
offx = (max_w - w)//2
|
@@ -175,71 +140,74 @@ def process_images(uploaded_files):
|
|
175 |
collage_2x2 = make_collage_2x2(final_4)
|
176 |
results.append(collage_2x2)
|
177 |
|
178 |
-
# 再拼 leftover 1~3张
|
179 |
if leftover > 0:
|
180 |
-
|
181 |
-
collage_left = make_collage_leftover(
|
182 |
if collage_left is not None:
|
183 |
results.append(collage_left)
|
184 |
|
185 |
return results
|
186 |
|
187 |
def make_zip(uploaded_files):
|
188 |
-
"""
|
|
|
|
|
|
|
189 |
collages = process_images(uploaded_files)
|
190 |
-
# 若无生成任何拼图
|
191 |
if not collages:
|
192 |
-
|
193 |
-
return None
|
194 |
|
195 |
-
|
196 |
-
|
|
|
197 |
for i, img in enumerate(collages, start=1):
|
198 |
img_bytes = io.BytesIO()
|
199 |
img.save(img_bytes, format="PNG")
|
200 |
img_bytes.seek(0)
|
201 |
zf.writestr(f"collage_{i}.png", img_bytes.read())
|
202 |
-
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
|
205 |
def on_zip_click(files):
|
206 |
"""
|
207 |
-
|
208 |
-
1
|
209 |
-
2
|
210 |
"""
|
211 |
-
|
212 |
-
if
|
213 |
-
return (None, "无可下载内容 -
|
214 |
else:
|
215 |
-
return (
|
216 |
|
217 |
with gr.Blocks() as demo:
|
218 |
-
gr.Markdown("## 2×2
|
219 |
with gr.Row():
|
220 |
with gr.Column():
|
221 |
-
file_input = gr.Files(label="上传多张图片", file_types=["image"])
|
222 |
preview_btn = gr.Button("生成预览")
|
223 |
zip_btn = gr.Button("打包下载 ZIP")
|
224 |
with gr.Column():
|
225 |
-
# 不使用 .style() 以兼容老Gradio
|
226 |
gallery_out = gr.Gallery(label="拼接结果预览", columns=2)
|
227 |
-
#
|
228 |
-
zip_file_out = gr.File(label="
|
229 |
-
|
230 |
|
231 |
-
# 生成预览
|
232 |
preview_btn.click(
|
233 |
fn=process_images,
|
234 |
inputs=[file_input],
|
235 |
outputs=[gallery_out]
|
236 |
)
|
237 |
-
|
238 |
-
# 打包下载ZIP,额外给一个文本提示
|
239 |
zip_btn.click(
|
240 |
fn=on_zip_click,
|
241 |
inputs=[file_input],
|
242 |
-
outputs=[zip_file_out,
|
243 |
)
|
244 |
|
245 |
demo.launch()
|
|
|
3 |
import io
|
4 |
import zipfile
|
5 |
import random
|
6 |
+
import tempfile
|
7 |
+
import os
|
8 |
|
9 |
def random_black_or_white():
|
|
|
10 |
return (0, 0, 0, 255) if random.random() < 0.5 else (255, 255, 255, 255)
|
11 |
|
12 |
def random_non_black_white():
|
|
|
|
|
|
|
|
|
13 |
while True:
|
14 |
r = random.randint(0, 255)
|
15 |
g = random.randint(0, 255)
|
16 |
b = random.randint(0, 255)
|
17 |
+
# 不要纯黑纯白
|
18 |
if not (r == g == b == 0 or r == g == b == 255):
|
19 |
return (r, g, b, 255)
|
20 |
|
21 |
def limit_2048(img: Image.Image):
|
|
|
22 |
w, h = img.size
|
23 |
if w > 2048 or h > 2048:
|
24 |
scale = min(2048 / w, 2048 / h)
|
25 |
+
nw = int(w * scale)
|
26 |
+
nh = int(h * scale)
|
27 |
+
img = img.resize((nw, nh), Image.Resampling.LANCZOS)
|
28 |
return img
|
29 |
|
30 |
def resize_to_64_multiple(img: Image.Image):
|
|
|
|
|
|
|
|
|
31 |
w, h = img.size
|
32 |
w64 = max(64, round(w / 64) * 64)
|
33 |
h64 = max(64, round(h / 64) * 64)
|
|
|
36 |
nw = int(w * scale)
|
37 |
nh = int(h * scale)
|
38 |
|
39 |
+
bg_color = random_black_or_white()
|
40 |
background = Image.new("RGBA", (w64, h64), bg_color)
|
41 |
scaled = img.resize((nw, nh), Image.Resampling.LANCZOS)
|
42 |
ox = (w64 - nw) // 2
|
|
|
45 |
return background
|
46 |
|
47 |
def make_collage_2x2(images_4):
|
|
|
48 |
w, h = images_4[0].size
|
49 |
+
collage = Image.new("RGBA", (2*w, 2*h), (0,0,0,255))
|
50 |
+
collage.paste(images_4[0], (0,0), images_4[0])
|
51 |
+
collage.paste(images_4[1], (w,0), images_4[1])
|
52 |
+
collage.paste(images_4[2], (0,h), images_4[2])
|
53 |
+
collage.paste(images_4[3], (w,h), images_4[3])
|
54 |
return limit_2048(collage)
|
55 |
|
56 |
def make_collage_leftover(images_leftover):
|
|
|
|
|
|
|
|
|
57 |
n = len(images_leftover)
|
58 |
if n < 1 or n > 3:
|
59 |
return None
|
60 |
|
|
|
61 |
resized_list = [resize_to_64_multiple(img) for img in images_leftover]
|
|
|
62 |
max_w = max(im.size[0] for im in resized_list)
|
63 |
max_h = max(im.size[1] for im in resized_list)
|
64 |
|
|
|
65 |
uniformed = []
|
66 |
for rimg in resized_list:
|
67 |
w, h = rimg.size
|
68 |
+
if (w,h) == (max_w, max_h):
|
69 |
uniformed.append(rimg)
|
70 |
else:
|
|
|
71 |
bg_color = rimg.getpixel((0,0))
|
72 |
bg = Image.new("RGBA", (max_w, max_h), bg_color)
|
73 |
+
offx = (max_w - w)//2
|
74 |
+
offy = (max_h - h)//2
|
75 |
bg.paste(rimg, (offx, offy), rimg)
|
76 |
uniformed.append(bg)
|
77 |
|
|
|
|
|
|
|
|
|
|
|
78 |
if n == 1:
|
79 |
possible_layouts = [(1,1), (1,2), (2,1), (2,2)]
|
80 |
elif n == 2:
|
81 |
possible_layouts = [(1,2), (2,1), (2,2)]
|
82 |
+
else: # n == 3
|
83 |
possible_layouts = [(2,2)]
|
84 |
|
85 |
rows, cols = random.choice(possible_layouts)
|
|
|
86 |
big_w = cols * max_w
|
87 |
big_h = rows * max_h
|
88 |
collage = Image.new("RGBA", (big_w, big_h), (0,0,0,255))
|
89 |
|
|
|
90 |
cells = [(r, c) for r in range(rows) for c in range(cols)]
|
91 |
random.shuffle(cells)
|
92 |
|
|
|
93 |
for i, img_ in enumerate(uniformed):
|
94 |
r, c = cells[i]
|
95 |
+
ox = c * max_w
|
96 |
+
oy = r * max_h
|
97 |
+
collage.paste(img_, (ox, oy), img_)
|
98 |
|
|
|
99 |
leftover_cells = cells[n:]
|
100 |
for (r, c) in leftover_cells:
|
101 |
color_ = random_non_black_white()
|
|
|
105 |
return limit_2048(collage)
|
106 |
|
107 |
def process_images(uploaded_files):
|
|
|
|
|
|
|
|
|
|
|
108 |
pil_images = []
|
109 |
for f in uploaded_files:
|
110 |
if f is not None:
|
|
|
111 |
img = Image.open(f.name).convert("RGBA")
|
112 |
pil_images.append(img)
|
113 |
|
|
|
117 |
leftover = total % 4
|
118 |
|
119 |
idx = 0
|
|
|
120 |
for _ in range(groups_4):
|
121 |
group_4 = pil_images[idx:idx+4]
|
122 |
idx += 4
|
|
|
123 |
resized_4 = [resize_to_64_multiple(im) for im in group_4]
|
|
|
124 |
max_w = max(im.size[0] for im in resized_4)
|
125 |
max_h = max(im.size[1] for im in resized_4)
|
126 |
|
127 |
final_4 = []
|
128 |
for rimg in resized_4:
|
129 |
w, h = rimg.size
|
130 |
+
if (w,h) == (max_w, max_h):
|
131 |
final_4.append(rimg)
|
132 |
else:
|
|
|
133 |
bg_color = rimg.getpixel((0,0))
|
134 |
bg = Image.new("RGBA", (max_w, max_h), bg_color)
|
135 |
offx = (max_w - w)//2
|
|
|
140 |
collage_2x2 = make_collage_2x2(final_4)
|
141 |
results.append(collage_2x2)
|
142 |
|
|
|
143 |
if leftover > 0:
|
144 |
+
leftover_imgs = pil_images[idx:]
|
145 |
+
collage_left = make_collage_leftover(leftover_imgs)
|
146 |
if collage_left is not None:
|
147 |
results.append(collage_left)
|
148 |
|
149 |
return results
|
150 |
|
151 |
def make_zip(uploaded_files):
|
152 |
+
"""
|
153 |
+
真正构建ZIP,并返回临时文件路径(不是BytesIO),
|
154 |
+
以防 Gradio 把 BytesIO 当成字符串解析而报错。
|
155 |
+
"""
|
156 |
collages = process_images(uploaded_files)
|
|
|
157 |
if not collages:
|
158 |
+
return None # 让前端做判断
|
|
|
159 |
|
160 |
+
# 在内存里打包
|
161 |
+
zip_buffer = io.BytesIO()
|
162 |
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
163 |
for i, img in enumerate(collages, start=1):
|
164 |
img_bytes = io.BytesIO()
|
165 |
img.save(img_bytes, format="PNG")
|
166 |
img_bytes.seek(0)
|
167 |
zf.writestr(f"collage_{i}.png", img_bytes.read())
|
168 |
+
zip_buffer.seek(0)
|
169 |
+
|
170 |
+
# 将 BytesIO 写入临时文件,再返回临时文件路径
|
171 |
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
172 |
+
tmp.write(zip_buffer.getvalue())
|
173 |
+
tmp_path = tmp.name
|
174 |
+
|
175 |
+
return tmp_path
|
176 |
|
177 |
def on_zip_click(files):
|
178 |
"""
|
179 |
+
返回 (zip_file_path, message) 给两个 output:
|
180 |
+
1. gr.File 要求返回一个str路径或None
|
181 |
+
2. gr.Textbox 用来输出提示信息
|
182 |
"""
|
183 |
+
path = make_zip(files)
|
184 |
+
if path is None:
|
185 |
+
return (None, "无可下载内容 - 可能没上传图片或无法拼接")
|
186 |
else:
|
187 |
+
return (path, "打包完成!点击上方链接下载ZIP")
|
188 |
|
189 |
with gr.Blocks() as demo:
|
190 |
+
gr.Markdown("## 2×2 拼接小工具(兼容不足4张、随机填充、保留透明)")
|
191 |
with gr.Row():
|
192 |
with gr.Column():
|
193 |
+
file_input = gr.Files(label="上传多张图片(可多选)", file_types=["image"])
|
194 |
preview_btn = gr.Button("生成预览")
|
195 |
zip_btn = gr.Button("打包下载 ZIP")
|
196 |
with gr.Column():
|
|
|
197 |
gallery_out = gr.Gallery(label="拼接结果预览", columns=2)
|
198 |
+
# 注意:必须 visible=True,这样点击后可以直接更新
|
199 |
+
zip_file_out = gr.File(label="下载拼接结果 ZIP", visible=True, interactive=False)
|
200 |
+
msg_out = gr.Textbox(label="提示信息", interactive=False)
|
201 |
|
|
|
202 |
preview_btn.click(
|
203 |
fn=process_images,
|
204 |
inputs=[file_input],
|
205 |
outputs=[gallery_out]
|
206 |
)
|
|
|
|
|
207 |
zip_btn.click(
|
208 |
fn=on_zip_click,
|
209 |
inputs=[file_input],
|
210 |
+
outputs=[zip_file_out, msg_out]
|
211 |
)
|
212 |
|
213 |
demo.launch()
|