PSNbst commited on
Commit
1898bb9
·
verified ·
1 Parent(s): 4bbef93

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -81
app.py CHANGED
@@ -2,121 +2,248 @@ import gradio as gr
2
  from PIL import Image
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)
43
- new_ch = int(ch * scale)
44
- collage = collage.resize((new_cw, new_ch), Image.Resampling.LANCZOS)
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()
 
2
  from PIL import Image
3
  import io
4
  import zipfile
5
+ import random
6
 
7
+ def random_black_or_white():
8
+ """返回 (r,g,b,a) = 黑 or 白,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),其中 (r,g,b) != (0,0,0) 且 != (255,255,255)。
14
+ 用于填充最后拼图时剩余空格,使之是“非黑非白”的纯色。
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 resize_to_64_multiple(img: Image.Image):
24
+ """
25
+ 将单张 RGBA 图片就近缩放到 (w', h'),其中 w'、h' 均为 64 的倍数(至少64)。
26
+ 背景随机填充黑/白。原图保持居中(若有空余,四周即背景色;透明度保留)。
27
+ """
28
  w, h = img.size
29
+ # 找到最接近的 64 倍数(至少 64)
30
  w64 = max(64, round(w / 64) * 64)
31
  h64 = max(64, round(h / 64) * 64)
32
+
33
+ # 算缩放比:保证原图能放入 w64*h64
34
  scale = min(w64 / w, h64 / h)
35
+ new_w = int(w * scale)
36
+ new_h = int(h * scale)
37
+
38
+ # 随机黑或白做背景
39
+ bg_color = random_black_or_white()
40
+ background = Image.new("RGBA", (w64, h64), bg_color)
41
+
42
+ # 缩放
43
+ scaled = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
44
+
45
+ # 居中贴到背景
46
+ offset_x = (w64 - new_w) // 2
47
+ offset_y = (h64 - new_h) // 2
48
+ # 注意第三个参数 scaled:保持其透明度
49
+ background.paste(scaled, (offset_x, offset_y), scaled)
50
  return background
51
 
52
+ def limit_2048(img: Image.Image):
53
+ """若图片宽或高 > 2048,则等比例缩小到不超过 2048。"""
54
+ w, h = img.size
55
+ if w > 2048 or h > 2048:
56
+ scale = min(2048 / w, 2048 / h)
57
+ new_w = int(w * scale)
58
+ new_h = int(h * scale)
59
+ img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
60
+ return img
61
+
62
+ def make_collage_2x2(images_4):
63
+ """传入 4 张同尺寸 RGBA,做 2×2 拼接,再限制不超过 2048。"""
64
+ w, h = images_4[0].size
65
+ collage = Image.new("RGBA", (2 * w, 2 * h), (0,0,0,255))
66
+ collage.paste(images_4[0], (0, 0), images_4[0])
67
+ collage.paste(images_4[1], (w, 0), images_4[1])
68
+ collage.paste(images_4[2], (0, h), images_4[2])
69
+ collage.paste(images_4[3], (w, h), images_4[3])
70
+ return limit_2048(collage)
71
 
72
+ def make_collage_leftover(images_leftover):
73
+ """
74
+ 对剩余的 1~3 张图做“兼容性拼接”。
75
+ 1) 首先统一尺寸(同宽高);
76
+ 2) 随机选择 (rows, cols) 布局(如 1x1/1x2/2x1/2x2 等)能容纳所有图;
77
+ 3) 将图随机放到网格单元,剩余格子用“非黑非白”的纯色填充;
78
+ 4) 最后若超出 2048,则缩小。
79
+ """
80
+ n = len(images_leftover)
81
+ if n < 1 or n > 3:
82
+ return None # 保险
83
+
84
+ # 统一每张图片的尺寸:按64倍数策略后,找出最大 w,h
85
+ resized_list = []
86
+ max_w = 0
87
+ max_h = 0
88
+ for img in images_leftover:
89
+ rimg = resize_to_64_multiple(img)
90
+ resized_list.append(rimg)
91
+ if rimg.size[0] > max_w:
92
+ max_w = rimg.size[0]
93
+ if rimg.size[1] > max_h:
94
+ max_h = rimg.size[1]
95
+
96
+ # 再次处理,使它们都成为 (max_w, max_h)
97
+ # (若小于max_w或max_h,就在背景再“��中贴图”)
98
+ uniformed = []
99
+ for rimg in resized_list:
100
+ w, h = rimg.size
101
+ if w == max_w and h == max_h:
102
+ uniformed.append(rimg)
103
+ else:
104
+ bg = Image.new("RGBA", (max_w, max_h), rimg.getpixel((0,0)))
105
+ # 取自身背景色(黑或白)进行填充,这样保持一致
106
+ offx = (max_w - w)//2
107
+ offy = (max_h - h)//2
108
+ bg.paste(rimg, (offx, offy), rimg)
109
+ uniformed.append(bg)
110
+
111
+ # 现在 uniformed 每张都是 (max_w, max_h),按 n ∈ [1,2,3]
112
+ # 决定随机布局
113
+ possible_layouts = []
114
+ if n == 1:
115
+ # 可以放在 (1x1), (1x2), (2x1), (2x2)
116
+ possible_layouts = [(1,1), (1,2), (2,1), (2,2)]
117
+ elif n == 2:
118
+ # (1x2), (2x1), (2x2)
119
+ possible_layouts = [(1,2), (2,1), (2,2)]
120
+ else: # n == 3
121
+ # (2x2)
122
+ possible_layouts = [(2,2)]
123
+
124
+ rows, cols = random.choice(possible_layouts)
125
+ # 拼接画布
126
+ big_w = cols * max_w
127
+ big_h = rows * max_h
128
+ collage = Image.new("RGBA", (big_w, big_h), (0,0,0,255))
129
+
130
+ # 网格坐标
131
+ cells = []
132
+ for r in range(rows):
133
+ for c in range(cols):
134
+ cells.append((r,c))
135
+
136
+ random.shuffle(cells) # 打乱单元格顺序
137
+ # 将 n 张图放前 n 个格子
138
+ for i, img_ in enumerate(uniformed):
139
+ r, c = cells[i]
140
+ offset_x = c * max_w
141
+ offset_y = r * max_h
142
+ collage.paste(img_, (offset_x, offset_y), img_)
143
+
144
+ # 剩余单元用“非黑非白”随机色填充
145
+ leftover_cells = cells[n:]
146
+ for (r, c) in leftover_cells:
147
+ fill_col = random_non_black_white()
148
+ rect = Image.new("RGBA", (max_w, max_h), fill_col)
149
+ offset_x = c * max_w
150
+ offset_y = r * max_h
151
+ collage.paste(rect, (offset_x, offset_y), rect)
152
+
153
+ return limit_2048(collage)
154
+
155
+ def process_images(uploaded_files):
156
+ """
157
+ 1) 把文件读成 RGBA;
158
+ 2) 分成若干 4 张组 => each 2×2 拼接;
159
+ 3) 若最后有 1~3 张剩余,则调用 make_collage_leftover();
160
+ 4) 返回多张结果图(列表)
161
+ """
162
  pil_images = []
163
  for f in uploaded_files:
164
  if f is not None:
 
165
  img = Image.open(f.name).convert("RGBA")
166
  pil_images.append(img)
167
+
168
  results = []
169
+ # 每 4 张一组
170
+ full_groups = len(pil_images) // 4
171
+ leftover_count = len(pil_images) % 4
172
+
173
+ # 处理完整的 4 张组
174
+ idx = 0
175
+ for _ in range(full_groups):
176
+ group_4 = pil_images[idx : idx+4]
177
+ idx += 4
178
+
179
+ # 先统一尺寸 => 找 max_w, max_h
180
+ temp = [resize_to_64_multiple(im) for im in group_4]
181
+ # 再次统一(可能有不同64倍数)
182
+ max_w = max([im.size[0] for im in temp])
183
+ max_h = max([im.size[1] for im in temp])
184
+
185
+ uniformed = []
186
+ for rimg in temp:
187
+ w, h = rimg.size
188
+ if w == max_w and h == max_h:
189
+ uniformed.append(rimg)
190
+ else:
191
+ bg = Image.new("RGBA", (max_w, max_h), rimg.getpixel((0,0)))
192
+ offx = (max_w - w)//2
193
+ offy = (max_h - h)//2
194
+ bg.paste(rimg, (offx, offy), rimg)
195
+ uniformed.append(bg)
196
+
197
+ # 2x2 拼接
198
+ collage_4 = make_collage_2x2(uniformed)
199
+ results.append(collage_4)
200
+
201
+ # 处理剩余 1~3 张
202
+ if leftover_count > 0:
203
+ leftover_images = pil_images[idx:]
204
+ collage_left = make_collage_leftover(leftover_images)
205
+ if collage_left is not None:
206
+ results.append(collage_left)
207
 
208
  return results
209
 
210
+ def make_zip(uploaded_files):
211
+ """打包所有拼接结果为 ZIP (PNG 格式),若无结果则返回 None。"""
212
+ collages = process_images(uploaded_files)
213
  if not collages:
 
214
  return None
215
 
216
  buf = io.BytesIO()
217
  with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
218
+ for i, img in enumerate(collages, start=1):
 
219
  img_bytes = io.BytesIO()
 
220
  img.save(img_bytes, format="PNG")
221
  img_bytes.seek(0)
222
+ zf.writestr(f"collage_{i}.png", img_bytes.read())
223
  buf.seek(0)
224
  return buf
225
 
226
  with gr.Blocks() as demo:
227
+ gr.Markdown("## 图片 2×2 拼接小工具(含随机填充、兼容不足4张)")
228
  gr.Markdown(
229
+ "1. 一次可上传多张图片,每 4 张为一组严格 2×2 拼接;\n"
230
+ "2. 若最后不足 4 (1~3),会随机选择网格大小 (1x1,1x2,2x1,2x2),并随机分配位置;\n"
231
+ " 剩余空格用“非黑非白”的随机颜色填充;\n"
232
+ "3. 每张图先按 64 的倍数就近缩放,空余处随机黑/白 (50% 概率);\n"
233
+ "4. 拼出的图若任一边超 2048,则等比例缩小到不超 2048;\n"
234
+ "5. 保留 PNG 透明度,背景填充只在超出区域;\n"
235
+ "6. 生成结果可预览,也可打包下载成 ZIP。"
236
  )
 
237
  with gr.Row():
238
  with gr.Column():
239
+ input_files = gr.Files(label="上传图片(可多选)", file_types=["image"])
240
+ btn_preview = gr.Button("生成预览")
241
+ btn_zip = gr.Button("打包下载 ZIP")
242
  with gr.Column():
243
+ gallery = gr.Gallery(label="拼接结果预览", columns=2)
244
+ zipfile_output = gr.File(label="下载拼接结果 ZIP", visible=False, interactive=False)
 
 
 
 
 
 
 
 
245
 
246
+ btn_preview.click(fn=process_images, inputs=[input_files], outputs=[gallery])
247
+ btn_zip.click(fn=make_zip, inputs=[input_files], outputs=[zipfile_output])
 
 
 
 
248
 
249
  demo.launch()