Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2025
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to do so, subject to the
|
10 |
+
following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
app.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from PIL import Image
|
3 |
+
import numpy as np
|
4 |
+
import io, os, zipfile, tempfile, time
|
5 |
+
from spm import spm_augment
|
6 |
+
|
7 |
+
TITLE = "Shuffle PatchMix (SPM) Augmentation"
|
8 |
+
DESC = """
|
9 |
+
Upload an image, choose **number of patches (N×N)**, and generate SPM-augmented variants.
|
10 |
+
For batch processing, upload a .zip of images (PNG/JPG/JPEG), and download a .zip of outputs.
|
11 |
+
"""
|
12 |
+
|
13 |
+
def _parse_grid(grid_choice: str) -> int:
|
14 |
+
# Expect strings like "2x2", "4x4", "8x8", "16x16"
|
15 |
+
try:
|
16 |
+
n = int(grid_choice.lower().split("x")[0])
|
17 |
+
return max(1, n)
|
18 |
+
except Exception:
|
19 |
+
return 4
|
20 |
+
|
21 |
+
def run_single(image, grid_choice, mix_prob, beta_a, beta_b, num_augs, seed):
|
22 |
+
if image is None:
|
23 |
+
return []
|
24 |
+
outs = []
|
25 |
+
base_seed = int(seed) if seed is not None else None
|
26 |
+
N = _parse_grid(grid_choice)
|
27 |
+
for i in range(num_augs):
|
28 |
+
s = (base_seed + i) if base_seed is not None else None
|
29 |
+
out_img = spm_augment(
|
30 |
+
image,
|
31 |
+
num_patches=N,
|
32 |
+
mix_prob=float(mix_prob),
|
33 |
+
beta_a=float(beta_a),
|
34 |
+
beta_b=float(beta_b),
|
35 |
+
seed=s
|
36 |
+
)
|
37 |
+
outs.append(out_img)
|
38 |
+
return outs
|
39 |
+
|
40 |
+
def run_batch(zip_file, grid_choice, mix_prob, beta_a, beta_b, seed):
|
41 |
+
if zip_file is None:
|
42 |
+
return None, "Please upload a .zip file with images."
|
43 |
+
tempdir = tempfile.mkdtemp()
|
44 |
+
outdir = os.path.join(tempdir, "outputs")
|
45 |
+
os.makedirs(outdir, exist_ok=True)
|
46 |
+
# Extract
|
47 |
+
with zipfile.ZipFile(zip_file, 'r') as zf:
|
48 |
+
zf.extractall(tempdir)
|
49 |
+
# Collect images
|
50 |
+
valid_exts = {".png", ".jpg", ".jpeg"}
|
51 |
+
count_in, count_out = 0, 0
|
52 |
+
N = _parse_grid(grid_choice)
|
53 |
+
for root_dir, _, files in os.walk(tempdir):
|
54 |
+
for f in files:
|
55 |
+
if f.lower().endswith(tuple(valid_exts)):
|
56 |
+
in_path = os.path.join(root_dir, f)
|
57 |
+
try:
|
58 |
+
img = Image.open(in_path).convert("RGB")
|
59 |
+
except Exception:
|
60 |
+
continue
|
61 |
+
count_in += 1
|
62 |
+
out_img = spm_augment(
|
63 |
+
img,
|
64 |
+
num_patches=N,
|
65 |
+
mix_prob=float(mix_prob),
|
66 |
+
beta_a=float(beta_a),
|
67 |
+
beta_b=float(beta_b),
|
68 |
+
seed=int(seed) if seed is not None else None
|
69 |
+
)
|
70 |
+
rel = os.path.relpath(in_path, tempdir)
|
71 |
+
out_path = os.path.join(outdir, rel)
|
72 |
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
73 |
+
out_img.save(out_path)
|
74 |
+
count_out += 1
|
75 |
+
# Zip results
|
76 |
+
out_zip = os.path.join(tempdir, f"spm_outputs_{int(time.time())}.zip")
|
77 |
+
with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
78 |
+
for root_dir, _, files in os.walk(outdir):
|
79 |
+
for f in files:
|
80 |
+
p = os.path.join(root_dir, f)
|
81 |
+
arc = os.path.relpath(p, outdir)
|
82 |
+
zf.write(p, arcname=arc)
|
83 |
+
msg = f"Processed {count_out}/{count_in} files."
|
84 |
+
return out_zip, msg
|
85 |
+
|
86 |
+
with gr.Blocks() as demo:
|
87 |
+
gr.Markdown(f"# {TITLE}")
|
88 |
+
gr.Markdown(DESC)
|
89 |
+
with gr.Tabs():
|
90 |
+
with gr.TabItem("Single Image"):
|
91 |
+
with gr.Row():
|
92 |
+
with gr.Column(scale=1):
|
93 |
+
inp = gr.Image(label="Input image", type="pil")
|
94 |
+
grid_choice = gr.Radio(choices=["2x2","4x4","8x8","16x16"], value="4x4", label="Grid (N×N)")
|
95 |
+
mix_prob = gr.Slider(0, 1, value=0.5, step=0.05, label="Mix probability (per patch)")
|
96 |
+
with gr.Row():
|
97 |
+
beta_a = gr.Slider(0.1, 8, value=2.0, step=0.1, label="Beta α")
|
98 |
+
beta_b = gr.Slider(0.1, 8, value=2.0, step=0.1, label="Beta β")
|
99 |
+
num_augs = gr.Slider(1, 12, value=4, step=1, label="Number of variants")
|
100 |
+
seed = gr.Number(value=42, precision=0, label="Seed (int, optional)")
|
101 |
+
run_btn = gr.Button("Generate")
|
102 |
+
with gr.Column(scale=1):
|
103 |
+
gallery = gr.Gallery(label="Augmented outputs", columns=2, height="auto")
|
104 |
+
run_btn.click(
|
105 |
+
fn=run_single,
|
106 |
+
inputs=[inp, grid_choice, mix_prob, beta_a, beta_b, num_augs, seed],
|
107 |
+
outputs=[gallery]
|
108 |
+
)
|
109 |
+
with gr.TabItem("Batch (.zip)"):
|
110 |
+
with gr.Row():
|
111 |
+
with gr.Column(scale=1):
|
112 |
+
zip_in = gr.File(label="Upload a .zip of images", file_types=[".zip"])
|
113 |
+
grid_choice_b = gr.Radio(choices=["2x2","4x4","8x8","16x16"], value="4x4", label="Grid (N×N)")
|
114 |
+
mix_prob_b = gr.Slider(0, 1, value=0.5, step=0.05, label="Mix probability (per patch)")
|
115 |
+
with gr.Row():
|
116 |
+
beta_a_b = gr.Slider(0.1, 8, value=2.0, step=0.1, label="Beta α")
|
117 |
+
beta_b_b = gr.Slider(0.1, 8, value=2.0, step=0.1, label="Beta β")
|
118 |
+
seed_b = gr.Number(value=42, precision=0, label="Seed (int, optional)")
|
119 |
+
run_b = gr.Button("Process Zip")
|
120 |
+
with gr.Column(scale=1):
|
121 |
+
zip_out = gr.File(label="Download results (.zip)")
|
122 |
+
status = gr.Markdown()
|
123 |
+
run_b.click(
|
124 |
+
fn=run_batch,
|
125 |
+
inputs=[zip_in, grid_choice_b, mix_prob_b, beta_a_b, beta_b_b, seed_b],
|
126 |
+
outputs=[zip_out, status]
|
127 |
+
)
|
128 |
+
|
129 |
+
if __name__ == "__main__":
|
130 |
+
demo.launch()
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=5.0.0
|
2 |
+
pillow
|
3 |
+
numpy
|
spm.py
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
def _to_divisible_by(img, N):
|
5 |
+
"""Crop so width and height are divisible by N (top-left anchored)."""
|
6 |
+
w, h = img.size
|
7 |
+
W = (w // N) * N
|
8 |
+
H = (h // N) * N
|
9 |
+
if W == 0 or H == 0:
|
10 |
+
raise ValueError("N is larger than the image grid (image too small).")
|
11 |
+
if W != w or H != h:
|
12 |
+
img = img.crop((0, 0, W, H))
|
13 |
+
return img, W, H
|
14 |
+
|
15 |
+
def spm_augment(
|
16 |
+
image,
|
17 |
+
num_patches=4, # N for an N×N grid
|
18 |
+
mix_prob=0.5,
|
19 |
+
beta_a=2.0,
|
20 |
+
beta_b=2.0,
|
21 |
+
seed=None
|
22 |
+
):
|
23 |
+
"""
|
24 |
+
SPM-style augmentation using a global shuffle over an N×N patch grid.
|
25 |
+
1) Divide image into N×N patches (cropping to be divisible by N if needed).
|
26 |
+
2) Globally permute patch indices.
|
27 |
+
3) Per patch, with probability `mix_prob`, replace by a convex blend of
|
28 |
+
original and a shuffled patch using alpha~Beta(beta_a,beta_b) (one alpha per image).
|
29 |
+
"""
|
30 |
+
# Normalize input
|
31 |
+
if isinstance(image, np.ndarray):
|
32 |
+
img = Image.fromarray(image).convert("RGB")
|
33 |
+
else:
|
34 |
+
img = image.convert("RGB")
|
35 |
+
|
36 |
+
N = int(num_patches)
|
37 |
+
rng = np.random.default_rng(seed)
|
38 |
+
|
39 |
+
# Ensure divisibility and compute patch size
|
40 |
+
img, W, H = _to_divisible_by(img, N)
|
41 |
+
arr = np.array(img, dtype=np.uint8)
|
42 |
+
ph = H // N
|
43 |
+
pw = W // N
|
44 |
+
|
45 |
+
# Build patch list (row-major)
|
46 |
+
patches = []
|
47 |
+
for i in range(N):
|
48 |
+
for j in range(N):
|
49 |
+
y0 = i * ph
|
50 |
+
x0 = j * pw
|
51 |
+
patches.append(arr[y0:y0+ph, x0:x0+pw])
|
52 |
+
|
53 |
+
total = N * N
|
54 |
+
perm = rng.permutation(total)
|
55 |
+
|
56 |
+
# Sample one alpha for the whole image
|
57 |
+
if beta_a > 0 and beta_b > 0:
|
58 |
+
alpha = float(rng.beta(beta_a, beta_b))
|
59 |
+
else:
|
60 |
+
alpha = 1.0
|
61 |
+
|
62 |
+
# Patchwise mix
|
63 |
+
out = arr.copy()
|
64 |
+
mask = rng.random(total) < float(mix_prob)
|
65 |
+
idx = 0
|
66 |
+
for i in range(N):
|
67 |
+
for j in range(N):
|
68 |
+
y0 = i * ph
|
69 |
+
x0 = j * pw
|
70 |
+
if mask[idx]:
|
71 |
+
src = patches[idx].astype(np.float32)
|
72 |
+
shf = patches[perm[idx]].astype(np.float32)
|
73 |
+
if 0.0 < alpha < 1.0:
|
74 |
+
mixed = alpha * shf + (1.0 - alpha) * src
|
75 |
+
out[y0:y0+ph, x0:x0+pw] = np.clip(mixed, 0, 255).astype(np.uint8)
|
76 |
+
else:
|
77 |
+
out[y0:y0+ph, x0:x0+pw] = patches[perm[idx]]
|
78 |
+
else:
|
79 |
+
out[y0:y0+ph, x0:x0+pw] = patches[idx]
|
80 |
+
idx += 1
|
81 |
+
|
82 |
+
return Image.fromarray(out)
|