Tanut commited on
Commit
6ae079b
Β·
1 Parent(s): e20d060

upload from local

Browse files
Files changed (3) hide show
  1. README.md +6 -11
  2. app.py +272 -75
  3. requirements.txt +1 -1
README.md CHANGED
@@ -1,17 +1,12 @@
1
  ---
2
- title: GenImages ControlNet
3
- emoji: 🐒
4
- colorFrom: green
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- license: openrail
11
- short_description: Testing generate Images from Controlnet
12
  preload_from_hub:
13
  - runwayml/stable-diffusion-v1-5
14
  - monster-labs/control_v1p_sd15_qrcode_monster
 
 
15
  ---
16
-
17
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: QRBlend ZeroGPU
3
+ emoji: 🧩
 
 
4
  sdk: gradio
5
+ sdk_version: 4.44.0
6
+ python_version: 3.10.13
 
 
 
7
  preload_from_hub:
8
  - runwayml/stable-diffusion-v1-5
9
  - monster-labs/control_v1p_sd15_qrcode_monster
10
+ - latentcat/control_v1p_sd15_brightness
11
+ license: openrail++
12
  ---
 
 
app.py CHANGED
@@ -1,94 +1,291 @@
1
- import io, random, spaces
 
 
 
 
2
  import gradio as gr
3
- from PIL import Image
4
  import qrcode
5
  from qrcode.constants import ERROR_CORRECT_H
6
 
7
  import torch
8
- from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, DPMSolverMultistepScheduler
9
-
10
- SD15 = "runwayml/stable-diffusion-v1-5"
11
- QR_MONSTER = "monster-labs/control_v1p_sd15_qrcode_monster" # v2 lives in a subfolder
12
-
13
- # ----- Load models on first GPU call (saves ZeroGPU minutes) -----
14
- pipe = None
15
- def get_pipe(dtype=torch.float16):
16
- global pipe
17
- if pipe is not None:
18
- return pipe
19
- controlnet = ControlNetModel.from_pretrained(
20
- QR_MONSTER, subfolder="v2", torch_dtype=dtype
21
- )
22
- pipe = StableDiffusionControlNetPipeline.from_pretrained(
23
- SD15, controlnet=controlnet, torch_dtype=dtype, safety_checker=None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  )
25
- pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
26
- pipe.enable_model_cpu_offload() # better mem usage on H200 slice
27
- return pipe
28
-
29
- def make_qr_img(data: str, module_size: int = 16, border: int = 4) -> Image.Image:
30
- qr = qrcode.QRCode(
31
- version=None,
32
- error_correction=ERROR_CORRECT_H,
33
- box_size=module_size,
34
- border=border,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  )
36
- qr.add_data(data)
37
- qr.make(fit=True)
38
- # Gray background (#808080) improves blend/readability for v2
39
- img = qr.make_image(fill_color="black", back_color="#808080").convert("RGB")
 
 
 
 
 
40
  return img
41
 
42
- @spaces.GPU(duration=120)
43
- def generate(
44
- qr_text, prompt, negative_prompt, steps, guidance_scale,
45
- controlnet_scale, width, height, seed
 
 
 
 
 
 
 
 
 
 
46
  ):
47
- pipe = get_pipe()
48
- if seed is None or seed < 0:
49
- seed = random.randint(0, 2**31 - 1)
50
- generator = torch.Generator(device="cuda").manual_seed(seed)
51
 
52
- control_img = make_qr_img(qr_text, module_size=16, border=4).resize((width, height), Image.NEAREST)
 
 
53
 
54
- out = pipe(
55
- prompt=prompt,
56
- negative_prompt=negative_prompt,
57
- num_inference_steps=steps,
58
- guidance_scale=guidance_scale,
59
- controlnet_conditioning_scale=controlnet_scale, # higher => more readable
60
- image=control_img,
61
- width=width,
62
- height=height,
63
- generator=generator,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  img = out.images[0]
66
- return img, seed
67
 
 
 
 
 
 
 
 
68
  with gr.Blocks() as demo:
69
- gr.Markdown("# 🧩 QR‑Blended Image (ZeroGPU + ControlNet QR Monster v2)")
70
-
71
- with gr.Row():
72
- with gr.Column():
73
- qr_text = gr.Textbox(label="QR Content (URL or text)", value="https://example.com")
74
- prompt = gr.Textbox(
75
- label="Prompt",
76
- value="a detailed isometric cyberpunk city with neon lights, intricate geometry, depth, high detail, trending artstation"
77
- )
78
- negative = gr.Textbox(label="Negative Prompt", value="lowres, blurry, deformed, distorted, extra limbs, watermark, signature")
79
- steps = gr.Slider(10, 50, value=30, step=1, label="Steps")
80
- guidance = gr.Slider(1.0, 12.0, value=7.0, step=0.5, label="CFG Scale")
81
- cn_scale = gr.Slider(0.2, 2.0, value=1.0, step=0.05, label="ControlNet Guidance (↑=more scannable)")
82
- width = gr.Slider(512, 1024, value=768, step=64, label="Width")
83
- height = gr.Slider(512, 1024, value=768, step=64, label="Height")
84
- seed = gr.Number(value=-1, precision=0, label="Seed (-1=random)")
85
- go = gr.Button("Generate", variant="primary")
86
- with gr.Column():
87
- out_img = gr.Image(label="Result", interactive=False)
88
- out_seed = gr.Number(label="Used Seed", interactive=False)
89
-
90
- go.click(generate, inputs=[qr_text, prompt, negative, steps, guidance, cn_scale, width, height, seed],
91
- outputs=[out_img, out_seed])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  if __name__ == "__main__":
94
  demo.launch()
 
1
+ import os, re, gc, random
2
+ import numpy as np
3
+ from contextlib import nullcontext
4
+ from typing import Tuple
5
+
6
  import gradio as gr
7
+ from PIL import Image, ImageFilter
8
  import qrcode
9
  from qrcode.constants import ERROR_CORRECT_H
10
 
11
  import torch
12
+ from diffusers import (
13
+ StableDiffusionPipeline,
14
+ StableDiffusionControlNetImg2ImgPipeline,
15
+ ControlNetModel,
16
+ DPMSolverMultistepScheduler,
17
+ )
18
+ import spaces # ZeroGPU decorator
19
+
20
+ # =========================================================
21
+ # Auth (optional for private models)
22
+ # =========================================================
23
+ hf_token = os.getenv("HF_TOKEN")
24
+ AUTH_KW = {"token": hf_token} if hf_token else {}
25
+
26
+ # =========================================================
27
+ # Helpers (untouched logic)
28
+ # =========================================================
29
+ def normalize_color(c):
30
+ if c is None: return "white"
31
+ if isinstance(c, (tuple, list)):
32
+ r, g, b = (int(max(0, min(255, round(float(x))))) for x in c[:3]); return (r, g, b)
33
+ if isinstance(c, str):
34
+ s = c.strip()
35
+ if s.startswith("#"): return s
36
+ m = re.match(r"rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)", s, re.IGNORECASE)
37
+ if m:
38
+ r = int(max(0, min(255, round(float(m.group(1))))))
39
+ g = int(max(0, min(255, round(float(m.group(2))))))
40
+ b = int(max(0, min(255, round(float(m.group(3))))))
41
+ return (r, g, b)
42
+ return s
43
+ return "white"
44
+
45
+ def strengthen_qr_prompts(pos: str, neg: str) -> Tuple[str, str]:
46
+ # DON’T say β€œQR code” here – let ControlNet impose it
47
+ pos = (pos or "").strip()
48
+ neg = (neg or "").strip()
49
+ pos2 = f"{pos}, high contrast lighting, clean details, cohesive composition".strip(", ")
50
+ add_neg = "frame, border, ornate frame, watermark, text, numbers, checkerboard, mosaic, halftone, repeated pattern, glitch"
51
+ neg2 = (neg + (", " if neg else "") + add_neg).strip(", ").strip()
52
+ return pos2, neg2
53
+
54
+ def enforce_qr_contrast(stylized: Image.Image, qr_img: Image.Image, strength: float = 0.6, feather: float = 1.0) -> Image.Image:
55
+ if strength <= 0: return stylized
56
+ q = qr_img.convert("L")
57
+ black_mask = q.point(lambda p: 255 if p < 128 else 0).filter(ImageFilter.GaussianBlur(radius=float(feather)))
58
+ black = np.asarray(black_mask, dtype=np.float32) / 255.0
59
+ white = 1.0 - black
60
+ s = np.asarray(stylized.convert("RGB"), dtype=np.float32) / 255.0
61
+ s = s * (1.0 - float(strength) * black[..., None])
62
+ s = s + (1.0 - s) * (float(strength) * 0.85 * white[..., None])
63
+ s = np.clip(s, 0.0, 1.0)
64
+ return Image.fromarray((s * 255.0).astype(np.uint8), mode="RGB")
65
+
66
+ # =========================================================
67
+ # Models & loading (ZeroGPU-friendly lazy load)
68
+ # =========================================================
69
+ BASE_15 = "runwayml/stable-diffusion-v1-5"
70
+ QR_MONSTER_15 = "monster-labs/control_v1p_sd15_qrcode_monster" # v2 subfolder is handled by authors; base path is fine
71
+ BRIGHTNESS_15 = "latentcat/control_v1p_sd15_brightness" # optional helper
72
+
73
+ _sd = {"pipe": None}
74
+ _cn = {"pipe": None}
75
+
76
+ def _setup_scheduler(pipe):
77
+ pipe.scheduler = DPMSolverMultistepScheduler.from_config(
78
+ pipe.scheduler.config,
79
+ use_karras_sigmas=True,
80
+ algorithm_type="dpmsolver++"
81
  )
82
+
83
+ def _enable_memory_savers(pipe):
84
+ # Good defaults for Spaces/ZeroGPU
85
+ pipe.enable_attention_slicing()
86
+ pipe.enable_vae_slicing()
87
+ pipe.enable_vae_tiling()
88
+ pipe.enable_model_cpu_offload()
89
+
90
+ def _load_sd_txt2img():
91
+ if _sd["pipe"] is None:
92
+ pipe = StableDiffusionPipeline.from_pretrained(
93
+ BASE_15,
94
+ torch_dtype=torch.float16,
95
+ safety_checker=None,
96
+ use_safetensors=True,
97
+ low_cpu_mem_usage=True,
98
+ **AUTH_KW
99
+ )
100
+ _setup_scheduler(pipe)
101
+ _enable_memory_savers(pipe)
102
+ _sd["pipe"] = pipe
103
+ return _sd["pipe"]
104
+
105
+ def _load_cn_img2img():
106
+ if _cn["pipe"] is None:
107
+ qrnet = ControlNetModel.from_pretrained(
108
+ QR_MONSTER_15, torch_dtype=torch.float16, use_safetensors=True, **AUTH_KW
109
+ )
110
+ bright = ControlNetModel.from_pretrained(
111
+ BRIGHTNESS_15, torch_dtype=torch.float16, use_safetensors=True, **AUTH_KW
112
+ )
113
+ pipe = StableDiffusionControlNetImg2ImgPipeline.from_pretrained(
114
+ BASE_15,
115
+ controlnet=[qrnet, bright],
116
+ torch_dtype=torch.float16,
117
+ safety_checker=None,
118
+ use_safetensors=True,
119
+ low_cpu_mem_usage=True,
120
+ **AUTH_KW
121
+ )
122
+ _setup_scheduler(pipe)
123
+ _enable_memory_savers(pipe)
124
+ _cn["pipe"] = pipe
125
+ return _cn["pipe"]
126
+
127
+ # =========================================================
128
+ # Generation utilities (use inside @spaces.GPU)
129
+ # =========================================================
130
+ def sd_generate(prompt, negative, steps, guidance, seed, size=512):
131
+ pipe = _load_sd_txt2img()
132
+ # Reproducible generator β€” on GPU if available
133
+ gen = torch.Generator(device="cuda" if torch.cuda.is_available() else "cpu")
134
+ if int(seed) != 0:
135
+ gen = gen.manual_seed(int(seed))
136
+ else:
137
+ gen = gen.manual_seed(random.randint(0, 2**31 - 1))
138
+
139
+ if torch.cuda.is_available():
140
+ torch.cuda.empty_cache()
141
+ gc.collect()
142
+
143
+ out = pipe(
144
+ prompt=prompt,
145
+ negative_prompt=negative or "",
146
+ num_inference_steps=int(steps),
147
+ guidance_scale=float(guidance),
148
+ width=int(size), height=int(size),
149
+ generator=gen
150
  )
151
+ return out.images[0]
152
+
153
+ def make_qr(url="http://www.mybirdfire.com", size=512, border=10, back_color="#808080", blur_radius=0.0):
154
+ qr = qrcode.QRCode(version=None, error_correction=ERROR_CORRECT_H, box_size=10, border=int(border))
155
+ qr.add_data(url.strip()); qr.make(fit=True)
156
+ bg = normalize_color(back_color)
157
+ img = qr.make_image(fill_color="black", back_color=bg).convert("RGB").resize((size, size), Image.NEAREST)
158
+ if blur_radius and blur_radius > 0:
159
+ img = img.filter(ImageFilter.GaussianBlur(radius=float(blur_radius)))
160
  return img
161
 
162
+ NEG_DEFAULT = "lowres, low contrast, blurry, jpeg artifacts, worst quality, bad anatomy, extra digits"
163
+
164
+ # =========================================================
165
+ # Main two-stage generator (ZeroGPU-guarded)
166
+ # =========================================================
167
+ @spaces.GPU(duration=120) # allocate GPU only while generating
168
+ def qr_art_two_stage(
169
+ prompt, negative,
170
+ base_steps, base_cfg, base_seed,
171
+ stylize_steps, stylize_cfg, stylize_seed,
172
+ size, url, border, back_color,
173
+ denoise, qr_weight, bright_weight,
174
+ qr_start, qr_end, bright_start, bright_end,
175
+ control_blur, repair_strength, feather_px
176
  ):
177
+ size = max(384, int(size) // 8 * 8)
 
 
 
178
 
179
+ # Stage A: base art (txt2img)
180
+ p_pos, p_neg = strengthen_qr_prompts(prompt, negative)
181
+ base_img = sd_generate(p_pos, p_neg, base_steps, base_cfg, base_seed, size=size)
182
 
183
+ # Stage B: img2img + ControlNet
184
+ qr_img = make_qr(url=url, size=size, border=border, back_color=back_color, blur_radius=control_blur)
185
+ pipe = _load_cn_img2img()
186
+
187
+ if torch.cuda.is_available():
188
+ torch.cuda.empty_cache()
189
+ gc.collect()
190
+
191
+ gen = torch.Generator(device="cuda" if torch.cuda.is_available() else "cpu")
192
+ if int(stylize_seed) != 0:
193
+ gen = gen.manual_seed(int(stylize_seed))
194
+ else:
195
+ gen = gen.manual_seed(random.randint(0, 2**31 - 1))
196
+
197
+ kwargs = dict(
198
+ prompt=p_pos,
199
+ negative_prompt=p_neg or NEG_DEFAULT,
200
+ image=base_img, # init image for img2img
201
+ control_image=[qr_img, qr_img], # Monster + Brightness
202
+ strength=float(denoise), # how much we allow change
203
+ num_inference_steps=int(stylize_steps),
204
+ guidance_scale=float(stylize_cfg),
205
+ generator=gen,
206
+ controlnet_conditioning_scale=[float(qr_weight), float(bright_weight)],
207
+ width=size, height=size, # (diffusers uses init image size; harmless here)
208
  )
209
+
210
+ try:
211
+ out = pipe(
212
+ **kwargs,
213
+ control_guidance_start=[float(qr_start), float(bright_start)],
214
+ control_guidance_end=[float(qr_end), float(bright_end)],
215
+ )
216
+ except TypeError:
217
+ out = pipe(
218
+ **kwargs,
219
+ controlnet_start=[float(qr_start), float(bright_start)],
220
+ controlnet_end=[float(qr_end), float(bright_end)],
221
+ )
222
+
223
  img = out.images[0]
 
224
 
225
+ # Optional post repair to push blacks/whites where modules demand
226
+ img = enforce_qr_contrast(img, qr_img, strength=float(repair_strength), feather=float(feather_px))
227
+ return img, base_img, qr_img
228
+
229
+ # =========================================================
230
+ # UI (Gradio Space)
231
+ # =========================================================
232
  with gr.Blocks() as demo:
233
+ gr.Markdown("## 🧩 QR-Code Monster β€” Two-Stage (txt2img β†’ img2img + ControlNet) β€” ZeroGPU")
234
+
235
+ with gr.Tab("Two-Stage QR Art"):
236
+ with gr.Row():
237
+ with gr.Column():
238
+ url = gr.Textbox(label="URL/Text", value="http://www.mybirdfire.com")
239
+ prompt = gr.Textbox(
240
+ label="Style prompt (no 'QR code' here)",
241
+ value="baroque palace interior with intricate roots, cinematic, dramatic lighting, ultra detailed"
242
+ )
243
+ negative = gr.Textbox(label="Negative", value="")
244
+ size = gr.Slider(512, 1024, value=768, step=64, label="Canvas (px)")
245
+
246
+ gr.Markdown("**Stage A β€” Base art (txt2img)**")
247
+ base_steps = gr.Slider(10, 60, value=26, step=1, label="Base steps")
248
+ base_cfg = gr.Slider(1.0, 12.0, value=6.0, step=0.1, label="Base CFG")
249
+ base_seed = gr.Number(value=0, precision=0, label="Base seed (0=random)")
250
+
251
+ gr.Markdown("**Stage B β€” ControlNet img2img**")
252
+ stylize_steps = gr.Slider(10, 60, value=28, step=1, label="Stylize steps")
253
+ stylize_cfg = gr.Slider(1.0, 12.0, value=6.0, step=0.1, label="Stylize CFG")
254
+ stylize_seed = gr.Number(value=0, precision=0, label="Stylize seed (0=random)")
255
+ denoise = gr.Slider(0.1, 0.8, value=0.48, step=0.01, label="Denoising strength (keep composition lower)")
256
+
257
+ qr_weight = gr.Slider(0.5, 1.7, value=1.2, step=0.05, label="QR Monster weight")
258
+ bright_weight = gr.Slider(0.0, 1.0, value=0.20, step=0.05, label="Brightness weight")
259
+
260
+ qr_start = gr.Slider(0.0, 1.0, value=0.05, step=0.01, label="QR start")
261
+ qr_end = gr.Slider(0.0, 1.0, value=0.95, step=0.01, label="QR end")
262
+ bright_start = gr.Slider(0.0, 1.0, value=0.40, step=0.01, label="Brightness start")
263
+ bright_end = gr.Slider(0.0, 1.0, value=0.85, step=0.01, label="Brightness end")
264
+
265
+ border = gr.Slider(4, 20, value=12, step=1, label="QR border (quiet zone)")
266
+ back_color = gr.ColorPicker(value="#808080", label="QR background (mid-gray blends better)")
267
+ control_blur = gr.Slider(0.0, 3.0, value=1.2, step=0.1, label="Soften control (Gaussian blur radius)")
268
+ repair_strength = gr.Slider(0.0, 1.0, value=0.65, step=0.05, label="Post repair strength")
269
+ feather_px = gr.Slider(0.0, 3.0, value=1.0, step=0.1, label="Repair feather (px)")
270
+
271
+ go = gr.Button("Generate QR Art", variant="primary")
272
+
273
+ with gr.Column():
274
+ final_img = gr.Image(label="Final stylized QR")
275
+ base_img = gr.Image(label="Base art (Stage A)")
276
+ ctrl_img = gr.Image(label="Control image (QR used)")
277
+
278
+ go.click(
279
+ qr_art_two_stage,
280
+ inputs=[prompt, negative,
281
+ base_steps, base_cfg, base_seed,
282
+ stylize_steps, stylize_cfg, stylize_seed,
283
+ size, url, border, back_color,
284
+ denoise, qr_weight, bright_weight,
285
+ qr_start, qr_end, bright_start, bright_end,
286
+ control_blur, repair_strength, feather_px],
287
+ outputs=[final_img, base_img, ctrl_img]
288
+ )
289
 
290
  if __name__ == "__main__":
291
  demo.launch()
requirements.txt CHANGED
@@ -3,9 +3,9 @@ diffusers>=0.27.2
3
  transformers>=4.42.0
4
  accelerate>=0.31.0
5
  safetensors
6
- xformers
7
  gradio>=4.29.0
8
  qrcode[pil]
9
  Pillow
10
  huggingface-hub
11
  spaces
 
 
3
  transformers>=4.42.0
4
  accelerate>=0.31.0
5
  safetensors
 
6
  gradio>=4.29.0
7
  qrcode[pil]
8
  Pillow
9
  huggingface-hub
10
  spaces
11
+ numpy