bened commited on
Commit
7485f4a
·
verified ·
1 Parent(s): e8af2ea

Update watermark/engine.py

Browse files
Files changed (1) hide show
  1. watermark/engine.py +1058 -124
watermark/engine.py CHANGED
@@ -1,226 +1,1125 @@
1
  from pathlib import Path
2
  import os
3
  import urllib.request
 
4
  import numpy as np
5
  import torch
6
  from PIL import Image, ImageDraw, ImageFilter
7
 
 
 
 
 
 
8
  MODEL_CACHE = None
 
9
  FLORENCE_MODEL = None
10
  FLORENCE_PROCESSOR = None
 
11
  LAMA_MODEL = None
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def _get_writable_cache():
 
 
 
 
 
 
 
14
  candidates = []
15
- if os.getenv("WATERMARK_CACHE_DIR"):
16
- candidates.append(Path(os.environ["WATERMARK_CACHE_DIR"]))
17
- candidates += [
18
- Path("/data/.cache/watermark"),
19
- Path("/tmp/watermark"),
20
- Path.home() / ".cache" / "watermark",
21
- ]
22
- for p in candidates:
 
 
 
 
 
 
 
23
  try:
24
- p.mkdir(parents=True, exist_ok=True)
25
- test = p / ".write_test"
26
- test.write_text("ok", encoding="utf-8")
27
- test.unlink(missing_ok=True)
28
- print(f"Watermark cache: {p}")
29
- return p
30
- except (PermissionError, OSError):
31
- pass
32
- raise RuntimeError("No writable watermark cache directory available.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  def _cache_dir():
35
  global MODEL_CACHE
 
36
  if MODEL_CACHE is None:
37
  MODEL_CACHE = _get_writable_cache()
 
38
  return MODEL_CACHE
39
 
 
 
 
 
 
40
  def _patch_florence_config():
41
- # Florence-2 custom configuration from older revisions accesses
42
- # self.forced_bos_token_id directly. Newer Transformers may not
43
- # materialize that attribute on PretrainedConfig subclasses.
 
 
 
 
 
 
 
 
 
44
  try:
 
45
  from transformers import PretrainedConfig
46
- if not hasattr(PretrainedConfig, "forced_bos_token_id"):
 
 
 
 
 
47
  PretrainedConfig.forced_bos_token_id = None
48
- print("Applied Florence-2 forced_bos_token_id compatibility patch.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  except Exception as e:
50
- print(f"Florence compatibility patch skipped: {e}")
 
 
 
 
 
 
 
 
 
 
51
 
52
  def _load_models():
53
- global FLORENCE_MODEL, FLORENCE_PROCESSOR, LAMA_MODEL
54
- if FLORENCE_MODEL is not None and LAMA_MODEL is not None:
 
 
 
 
 
 
 
 
 
55
  return
56
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  _patch_florence_config()
58
 
59
- from transformers import AutoProcessor, AutoModelForCausalLM
 
 
 
 
 
 
 
60
 
61
  cache = _cache_dir()
 
 
 
 
 
 
 
 
62
  model_id = os.getenv(
63
  "FLORENCE_MODEL_ID",
64
  "microsoft/Florence-2-large",
65
  )
66
 
67
- print(f"Loading Florence-2: {model_id}")
 
 
 
 
 
 
68
 
69
- FLORENCE_PROCESSOR = AutoProcessor.from_pretrained(
70
- model_id,
71
- cache_dir=str(cache / "hf"),
72
- trust_remote_code=True,
 
 
73
  )
74
 
75
- FLORENCE_MODEL = AutoModelForCausalLM.from_pretrained(
76
- model_id,
77
- torch_dtype=torch.bfloat16,
78
- cache_dir=str(cache / "hf"),
79
- trust_remote_code=True,
80
- ).to("cuda").eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  lama_path = cache / "big-lama.pt"
 
83
  if not lama_path.exists():
84
- url = os.getenv(
 
85
  "LAMA_URL",
86
  "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt",
87
  )
88
- print("Downloading LaMa model...")
89
- urllib.request.urlretrieve(url, lama_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  LAMA_MODEL = torch.jit.load(
92
  str(lama_path),
93
- map_location="cuda",
94
  ).eval()
95
 
96
- print("Watermark models loaded.")
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  def _boxes(image, prompt):
 
99
  _load_models()
100
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  task = "<OPEN_VOCABULARY_DETECTION>"
 
102
  inputs = FLORENCE_PROCESSOR(
103
  text=task + prompt,
104
  images=image,
105
  return_tensors="pt",
106
  )
107
- inputs = {
108
- k: (v.to("cuda") if hasattr(v, "to") else v)
109
- for k, v in inputs.items()
110
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  with torch.inference_mode():
 
113
  generated = FLORENCE_MODEL.generate(
114
- input_ids=inputs.get("input_ids"),
115
- pixel_values=inputs.get("pixel_values"),
116
- max_new_tokens=1024,
117
- num_beams=3,
 
 
 
 
 
 
 
 
 
 
 
 
118
  )
119
 
120
- decoded = FLORENCE_PROCESSOR.batch_decode(
121
- generated,
122
- skip_special_tokens=False,
123
- )[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- parsed = FLORENCE_PROCESSOR.post_process_generation(
126
- decoded,
127
- task=task,
128
- image_size=image.size,
129
  )
130
- data = parsed.get(task, parsed)
131
 
132
  if not isinstance(data, dict):
133
  return []
134
 
135
- return data.get("bboxes", []) or []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- def _mask(image, boxes, max_pct):
138
- mask = Image.new("L", image.size, 0)
139
  draw = ImageDraw.Draw(mask)
140
- total = image.width * image.height
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  for box in boxes:
 
143
  if len(box) != 4:
144
  continue
145
- x1, y1, x2, y2 = map(float, box)
146
- x1 = max(0, min(image.width, x1))
147
- x2 = max(0, min(image.width, x2))
148
- y1 = max(0, min(image.height, y1))
149
- y2 = max(0, min(image.height, y2))
150
- area = max(0, x2 - x1) * max(0, y2 - y1)
151
 
152
- if total and area / total * 100 <= max_pct:
153
- draw.rectangle((x1, y1, x2, y2), fill=255)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- return mask.filter(ImageFilter.MaxFilter(7))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- def _lama(image, mask, boxes=None):
158
- """Run LaMa on watermark regions rather than the whole frame when boxes exist."""
159
  _load_models()
160
 
161
  image = image.convert("RGB")
162
  mask = mask.convert("L")
163
 
164
- # Region-based inpainting is much cheaper for video than sending a full
165
- # 1080p/4K frame through LaMa. Use a modest padding around each box.
 
 
166
  if boxes:
 
167
  result = image.copy()
168
- pad = max(16, int(min(image.size) * 0.02))
 
 
 
 
 
 
 
 
 
169
  for box in boxes:
 
170
  if len(box) != 4:
171
  continue
172
- x1, y1, x2, y2 = map(float, box)
173
- left = max(0, int(x1) - pad)
174
- top = max(0, int(y1) - pad)
175
- right = min(image.width, int(x2) + pad)
176
- bottom = min(image.height, int(y2) + pad)
177
- if right <= left or bottom <= top:
 
 
 
178
  continue
179
- crop = image.crop((left, top, right, bottom))
180
- crop_mask = mask.crop((left, top, right, bottom))
181
- crop_np = np.asarray(crop, dtype=np.float32) / 255.0
182
- mask_np = np.asarray(crop_mask, dtype=np.float32) / 255.0
183
- image_t = torch.from_numpy(crop_np).permute(2,0,1).unsqueeze(0).cuda() * 2.0 - 1.0
184
- mask_t = torch.from_numpy(mask_np).unsqueeze(0).unsqueeze(0).cuda()
185
- mask_t = (mask_t > 0.5).float()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  with torch.inference_mode():
187
- out = LAMA_MODEL(image_t, mask_t)
188
- if isinstance(out, (tuple, list)):
189
- out = out[0]
190
- out = ((out.clamp(-1, 1) + 1.0) / 2.0).squeeze(0).permute(1,2,0).float().cpu().numpy()
191
- restored = Image.fromarray((out * 255).clip(0,255).astype(np.uint8))
192
- result.paste(restored, (left, top))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  return result
194
 
195
- image_np = np.asarray(image, dtype=np.float32) / 255.0
196
- mask_np = np.asarray(mask, dtype=np.float32) / 255.0
197
- image_t = torch.from_numpy(image_np).permute(2,0,1).unsqueeze(0).cuda() * 2.0 - 1.0
198
- mask_t = torch.from_numpy(mask_np).unsqueeze(0).unsqueeze(0).cuda()
199
- mask_t = (mask_t > 0.5).float()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  with torch.inference_mode():
201
- result = LAMA_MODEL(image_t, mask_t)
202
- if isinstance(result, (tuple, list)):
203
- result = result[0]
204
- result = ((result.clamp(-1,1)+1.0)/2.0).squeeze(0).permute(1,2,0).float().cpu().numpy()
205
- return Image.fromarray((result*255).clip(0,255).astype(np.uint8))
206
 
207
- def preview(image, max_bbox_percent=30, detection_prompt="watermark"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  if image is None:
209
- raise ValueError("Please upload an image.")
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- boxes = _boxes(image, detection_prompt)
212
- out = image.convert("RGB").copy()
213
- draw = ImageDraw.Draw(out)
 
 
 
 
 
 
 
 
214
 
215
  for box in boxes:
216
- if len(box) == 4:
217
- draw.rectangle(
218
- tuple(map(float, box)),
219
- outline="red",
220
- width=4,
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
- return out
 
 
 
224
 
225
  def remove_image(
226
  image,
@@ -228,19 +1127,54 @@ def remove_image(
228
  detection_prompt="watermark",
229
  transparent=False,
230
  ):
 
231
  if image is None:
232
- raise ValueError("Please upload an image.")
 
 
233
 
234
- boxes = _boxes(image, detection_prompt)
235
- mask = _mask(image, boxes, max_bbox_percent)
 
 
 
 
236
 
 
 
 
 
 
 
 
237
  if not boxes:
238
- return image.convert("RGBA" if transparent else "RGB"), mask
239
 
240
- result = _lama(image, mask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  if transparent:
243
- result = result.convert("RGBA")
244
- result.putalpha(mask.point(lambda p: 255 - p))
 
 
 
 
 
 
 
 
245
 
246
  return result, mask
 
1
  from pathlib import Path
2
  import os
3
  import urllib.request
4
+
5
  import numpy as np
6
  import torch
7
  from PIL import Image, ImageDraw, ImageFilter
8
 
9
+
10
+ # ============================================================
11
+ # Global model cache
12
+ # ============================================================
13
+
14
  MODEL_CACHE = None
15
+
16
  FLORENCE_MODEL = None
17
  FLORENCE_PROCESSOR = None
18
+
19
  LAMA_MODEL = None
20
 
21
+
22
+ # ============================================================
23
+ # Device / dtype
24
+ # ============================================================
25
+
26
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
27
+
28
+ if DEVICE == "cuda":
29
+ DTYPE = torch.bfloat16
30
+ else:
31
+ DTYPE = torch.float32
32
+
33
+
34
+ # ============================================================
35
+ # Writable cache directory
36
+ # ============================================================
37
+
38
  def _get_writable_cache():
39
+ """
40
+ Find a writable directory.
41
+
42
+ HF Spaces may not allow writing to /data, so never assume
43
+ /data exists or is writable.
44
+ """
45
+
46
  candidates = []
47
+
48
+ env_cache = os.getenv("WATERMARK_CACHE_DIR")
49
+
50
+ if env_cache:
51
+ candidates.append(Path(env_cache))
52
+
53
+ candidates.extend(
54
+ [
55
+ Path("/tmp/watermark"),
56
+ Path.home() / ".cache" / "watermark",
57
+ ]
58
+ )
59
+
60
+ for path in candidates:
61
+
62
  try:
63
+ path.mkdir(
64
+ parents=True,
65
+ exist_ok=True,
66
+ )
67
+
68
+ test_file = path / ".write_test"
69
+
70
+ test_file.write_text(
71
+ "ok",
72
+ encoding="utf-8",
73
+ )
74
+
75
+ try:
76
+ test_file.unlink()
77
+ except OSError:
78
+ pass
79
+
80
+ print(f"[Watermark] Cache directory: {path}")
81
+
82
+ return path
83
+
84
+ except (PermissionError, OSError) as e:
85
+
86
+ print(
87
+ f"[Watermark] Cache unavailable: "
88
+ f"{path} ({e})"
89
+ )
90
+
91
+ raise RuntimeError(
92
+ "No writable watermark cache directory available."
93
+ )
94
+
95
 
96
  def _cache_dir():
97
  global MODEL_CACHE
98
+
99
  if MODEL_CACHE is None:
100
  MODEL_CACHE = _get_writable_cache()
101
+
102
  return MODEL_CACHE
103
 
104
+
105
+ # ============================================================
106
+ # Florence-2 compatibility
107
+ # ============================================================
108
+
109
  def _patch_florence_config():
110
+ """
111
+ Compatibility patch for Florence-2 custom code.
112
+
113
+ Florence-2 was originally written against older versions
114
+ of Transformers.
115
+
116
+ Newer Transformers may access forced_bos_token_id before
117
+ the attribute exists.
118
+
119
+ This patch is harmless when the attribute already exists.
120
+ """
121
+
122
  try:
123
+
124
  from transformers import PretrainedConfig
125
+
126
+ if not hasattr(
127
+ PretrainedConfig,
128
+ "forced_bos_token_id",
129
+ ):
130
+
131
  PretrainedConfig.forced_bos_token_id = None
132
+
133
+ print(
134
+ "[Florence] Applied "
135
+ "forced_bos_token_id compatibility patch."
136
+ )
137
+
138
+ except Exception as e:
139
+
140
+ print(
141
+ f"[Florence] Config patch skipped: {e}"
142
+ )
143
+
144
+
145
+ def _patch_florence_model_class():
146
+ """
147
+ Fix Florence-2 _supports_sdpa compatibility.
148
+
149
+ Some Florence-2 remote-code revisions define
150
+ _supports_sdpa as a property that accesses
151
+ self.language_model.
152
+
153
+ Transformers may query _supports_sdpa during
154
+ Florence2ForConditionalGeneration.__init__(),
155
+ before language_model has been created.
156
+
157
+ Therefore we force class-level attributes.
158
+ """
159
+
160
+ try:
161
+
162
+ from transformers.models.auto.modeling_auto import (
163
+ AutoModelForCausalLM,
164
+ )
165
+
166
+ # Importing the model through the dynamic module system
167
+ # happens only after from_pretrained(), so this patch is
168
+ # additionally applied after model class creation below.
169
+
170
+ return AutoModelForCausalLM
171
+
172
  except Exception as e:
173
+
174
+ print(
175
+ f"[Florence] Model compatibility setup skipped: {e}"
176
+ )
177
+
178
+ return None
179
+
180
+
181
+ # ============================================================
182
+ # Load Florence + LaMa
183
+ # ============================================================
184
 
185
  def _load_models():
186
+
187
+ global FLORENCE_MODEL
188
+ global FLORENCE_PROCESSOR
189
+ global LAMA_MODEL
190
+
191
+ # Already loaded
192
+ if (
193
+ FLORENCE_MODEL is not None
194
+ and FLORENCE_PROCESSOR is not None
195
+ and LAMA_MODEL is not None
196
+ ):
197
  return
198
 
199
+ print(
200
+ f"[Watermark] Device: {DEVICE}"
201
+ )
202
+
203
+ print(
204
+ f"[Watermark] Dtype: {DTYPE}"
205
+ )
206
+
207
+ # --------------------------------------------------------
208
+ # Florence compatibility
209
+ # --------------------------------------------------------
210
+
211
  _patch_florence_config()
212
 
213
+ # --------------------------------------------------------
214
+ # Transformers
215
+ # --------------------------------------------------------
216
+
217
+ from transformers import (
218
+ AutoProcessor,
219
+ AutoModelForCausalLM,
220
+ )
221
 
222
  cache = _cache_dir()
223
+
224
+ hf_cache = cache / "hf"
225
+
226
+ hf_cache.mkdir(
227
+ parents=True,
228
+ exist_ok=True,
229
+ )
230
+
231
  model_id = os.getenv(
232
  "FLORENCE_MODEL_ID",
233
  "microsoft/Florence-2-large",
234
  )
235
 
236
+ print(
237
+ f"[Florence] Loading model: {model_id}"
238
+ )
239
+
240
+ # --------------------------------------------------------
241
+ # Processor
242
+ # --------------------------------------------------------
243
 
244
+ FLORENCE_PROCESSOR = (
245
+ AutoProcessor.from_pretrained(
246
+ model_id,
247
+ cache_dir=str(hf_cache),
248
+ trust_remote_code=True,
249
+ )
250
  )
251
 
252
+ print(
253
+ "[Florence] Processor loaded."
254
+ )
255
+
256
+ # --------------------------------------------------------
257
+ # Florence model
258
+ # --------------------------------------------------------
259
+
260
+ model_kwargs = {
261
+ "cache_dir": str(hf_cache),
262
+ "trust_remote_code": True,
263
+ }
264
+
265
+ if DEVICE == "cuda":
266
+ model_kwargs["torch_dtype"] = DTYPE
267
+ else:
268
+ model_kwargs["torch_dtype"] = torch.float32
269
+
270
+ # IMPORTANT:
271
+ #
272
+ # eager avoids the SDPA compatibility path.
273
+ #
274
+ # This is particularly useful if the environment contains
275
+ # a newer Transformers version or newer Torch.
276
+ #
277
+ model_kwargs["attn_implementation"] = "eager"
278
+
279
+ print(
280
+ "[Florence] Loading model weights..."
281
+ )
282
+
283
+ try:
284
+
285
+ FLORENCE_MODEL = (
286
+ AutoModelForCausalLM.from_pretrained(
287
+ model_id,
288
+ **model_kwargs,
289
+ )
290
+ )
291
+
292
+ except TypeError as e:
293
+
294
+ # Some older Transformers versions may not accept
295
+ # attn_implementation.
296
+
297
+ if "attn_implementation" in str(e):
298
+
299
+ print(
300
+ "[Florence] "
301
+ "attn_implementation unsupported; "
302
+ "retrying without it."
303
+ )
304
+
305
+ model_kwargs.pop(
306
+ "attn_implementation",
307
+ None,
308
+ )
309
+
310
+ FLORENCE_MODEL = (
311
+ AutoModelForCausalLM.from_pretrained(
312
+ model_id,
313
+ **model_kwargs,
314
+ )
315
+ )
316
+
317
+ else:
318
+ raise
319
+
320
+ # --------------------------------------------------------
321
+ # Explicit Florence _supports_sdpa compatibility
322
+ # --------------------------------------------------------
323
+
324
+ try:
325
+
326
+ model_cls = FLORENCE_MODEL.__class__
327
+
328
+ # Force class-level values.
329
+
330
+ if (
331
+ not hasattr(model_cls, "_supports_sdpa")
332
+ or isinstance(
333
+ getattr(
334
+ model_cls,
335
+ "_supports_sdpa",
336
+ None,
337
+ ),
338
+ property,
339
+ )
340
+ ):
341
+
342
+ try:
343
+ model_cls._supports_sdpa = True
344
+ except Exception:
345
+ pass
346
+
347
+ if (
348
+ not hasattr(
349
+ model_cls,
350
+ "_supports_flash_attn_2",
351
+ )
352
+ or isinstance(
353
+ getattr(
354
+ model_cls,
355
+ "_supports_flash_attn_2",
356
+ None,
357
+ ),
358
+ property,
359
+ )
360
+ ):
361
+
362
+ try:
363
+ model_cls._supports_flash_attn_2 = False
364
+ except Exception:
365
+ pass
366
+
367
+ except Exception as e:
368
+
369
+ print(
370
+ f"[Florence] "
371
+ f"Class compatibility patch skipped: {e}"
372
+ )
373
+
374
+ # --------------------------------------------------------
375
+ # Move to GPU
376
+ # --------------------------------------------------------
377
+
378
+ FLORENCE_MODEL = (
379
+ FLORENCE_MODEL
380
+ .to(DEVICE)
381
+ .eval()
382
+ )
383
+
384
+ print(
385
+ "[Florence] Model loaded."
386
+ )
387
+
388
+ # --------------------------------------------------------
389
+ # LaMa
390
+ # --------------------------------------------------------
391
 
392
  lama_path = cache / "big-lama.pt"
393
+
394
  if not lama_path.exists():
395
+
396
+ lama_url = os.getenv(
397
  "LAMA_URL",
398
  "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt",
399
  )
400
+
401
+ print(
402
+ "[LaMa] Downloading model..."
403
+ )
404
+
405
+ print(
406
+ f"[LaMa] URL: {lama_url}"
407
+ )
408
+
409
+ urllib.request.urlretrieve(
410
+ lama_url,
411
+ lama_path,
412
+ )
413
+
414
+ print(
415
+ "[LaMa] Download complete."
416
+ )
417
+
418
+ print(
419
+ "[LaMa] Loading model..."
420
+ )
421
 
422
  LAMA_MODEL = torch.jit.load(
423
  str(lama_path),
424
+ map_location=DEVICE,
425
  ).eval()
426
 
427
+ print(
428
+ "[LaMa] Model loaded."
429
+ )
430
+
431
+ print(
432
+ "[Watermark] All watermark models loaded."
433
+ )
434
+
435
+
436
+ # ============================================================
437
+ # Florence watermark detection
438
+ # ============================================================
439
 
440
  def _boxes(image, prompt):
441
+
442
  _load_models()
443
 
444
+ if image is None:
445
+ return []
446
+
447
+ image = image.convert("RGB")
448
+
449
+ prompt = (
450
+ str(prompt).strip()
451
+ if prompt
452
+ else "watermark"
453
+ )
454
+
455
+ # Florence-2 open vocabulary detection
456
  task = "<OPEN_VOCABULARY_DETECTION>"
457
+
458
  inputs = FLORENCE_PROCESSOR(
459
  text=task + prompt,
460
  images=image,
461
  return_tensors="pt",
462
  )
463
+
464
+ # Move tensors to device
465
+ prepared_inputs = {}
466
+
467
+ for key, value in inputs.items():
468
+
469
+ if hasattr(value, "to"):
470
+
471
+ if (
472
+ DEVICE == "cuda"
473
+ and torch.is_floating_point(value)
474
+ ):
475
+ value = value.to(
476
+ DEVICE,
477
+ dtype=DTYPE,
478
+ )
479
+ else:
480
+ value = value.to(DEVICE)
481
+
482
+ prepared_inputs[key] = value
483
+
484
+ # --------------------------------------------------------
485
+ # Florence generation
486
+ # --------------------------------------------------------
487
 
488
  with torch.inference_mode():
489
+
490
  generated = FLORENCE_MODEL.generate(
491
+
492
+ input_ids=prepared_inputs.get(
493
+ "input_ids"
494
+ ),
495
+
496
+ pixel_values=prepared_inputs.get(
497
+ "pixel_values"
498
+ ),
499
+
500
+ max_new_tokens=512,
501
+
502
+ # Beam search is unnecessary for this task.
503
+ # Reducing to 1 significantly lowers ZeroGPU cost.
504
+ num_beams=1,
505
+
506
+ do_sample=False,
507
  )
508
 
509
+ # --------------------------------------------------------
510
+ # Decode
511
+ # --------------------------------------------------------
512
+
513
+ decoded = (
514
+ FLORENCE_PROCESSOR.batch_decode(
515
+ generated,
516
+ skip_special_tokens=False,
517
+ )[0]
518
+ )
519
+
520
+ # --------------------------------------------------------
521
+ # Parse Florence result
522
+ # --------------------------------------------------------
523
+
524
+ parsed = (
525
+ FLORENCE_PROCESSOR.post_process_generation(
526
+ decoded,
527
+ task=task,
528
+ image_size=image.size,
529
+ )
530
+ )
531
 
532
+ data = parsed.get(
533
+ task,
534
+ parsed,
 
535
  )
 
536
 
537
  if not isinstance(data, dict):
538
  return []
539
 
540
+ boxes = data.get(
541
+ "bboxes",
542
+ [],
543
+ )
544
+
545
+ if not boxes:
546
+ return []
547
+
548
+ # --------------------------------------------------------
549
+ # Validate boxes
550
+ # --------------------------------------------------------
551
+
552
+ valid_boxes = []
553
+
554
+ for box in boxes:
555
+
556
+ if not isinstance(
557
+ box,
558
+ (list, tuple),
559
+ ):
560
+ continue
561
+
562
+ if len(box) != 4:
563
+ continue
564
+
565
+ try:
566
+
567
+ x1, y1, x2, y2 = map(
568
+ float,
569
+ box,
570
+ )
571
+
572
+ except Exception:
573
+ continue
574
+
575
+ if x2 <= x1 or y2 <= y1:
576
+ continue
577
+
578
+ valid_boxes.append(
579
+ [
580
+ x1,
581
+ y1,
582
+ x2,
583
+ y2,
584
+ ]
585
+ )
586
+
587
+ return valid_boxes
588
+
589
+
590
+ # ============================================================
591
+ # Create watermark mask
592
+ # ============================================================
593
+
594
+ def _mask(
595
+ image,
596
+ boxes,
597
+ max_pct,
598
+ ):
599
+
600
+ image = image.convert("RGB")
601
+
602
+ mask = Image.new(
603
+ "L",
604
+ image.size,
605
+ 0,
606
+ )
607
 
 
 
608
  draw = ImageDraw.Draw(mask)
609
+
610
+ total_area = (
611
+ image.width
612
+ * image.height
613
+ )
614
+
615
+ try:
616
+ max_pct = float(max_pct)
617
+ except Exception:
618
+ max_pct = 30.0
619
+
620
+ max_pct = max(
621
+ 0.1,
622
+ min(
623
+ max_pct,
624
+ 100.0,
625
+ ),
626
+ )
627
 
628
  for box in boxes:
629
+
630
  if len(box) != 4:
631
  continue
 
 
 
 
 
 
632
 
633
+ try:
634
+
635
+ x1, y1, x2, y2 = map(
636
+ float,
637
+ box,
638
+ )
639
+
640
+ except Exception:
641
+ continue
642
+
643
+ x1 = max(
644
+ 0,
645
+ min(
646
+ image.width,
647
+ x1,
648
+ ),
649
+ )
650
+
651
+ x2 = max(
652
+ 0,
653
+ min(
654
+ image.width,
655
+ x2,
656
+ ),
657
+ )
658
+
659
+ y1 = max(
660
+ 0,
661
+ min(
662
+ image.height,
663
+ y1,
664
+ ),
665
+ )
666
+
667
+ y2 = max(
668
+ 0,
669
+ min(
670
+ image.height,
671
+ y2,
672
+ ),
673
+ )
674
+
675
+ area = (
676
+ max(
677
+ 0,
678
+ x2 - x1,
679
+ )
680
+ *
681
+ max(
682
+ 0,
683
+ y2 - y1,
684
+ )
685
+ )
686
+
687
+ if (
688
+ total_area > 0
689
+ and area / total_area * 100
690
+ <= max_pct
691
+ ):
692
+
693
+ draw.rectangle(
694
+ (
695
+ x1,
696
+ y1,
697
+ x2,
698
+ y2,
699
+ ),
700
+ fill=255,
701
+ )
702
+
703
+ # Slight expansion to ensure watermark edges
704
+ # are also included.
705
+ try:
706
+
707
+ mask = mask.filter(
708
+ ImageFilter.MaxFilter(7)
709
+ )
710
+
711
+ except Exception:
712
+ pass
713
+
714
+ return mask
715
+
716
 
717
+ # ============================================================
718
+ # LaMa inpainting
719
+ # ============================================================
720
+
721
+ def _lama(
722
+ image,
723
+ mask,
724
+ boxes=None,
725
+ ):
726
+ """
727
+ Run LaMa inpainting.
728
+
729
+ If bounding boxes are available, process only local
730
+ regions. This is substantially cheaper for video.
731
+ """
732
 
 
 
733
  _load_models()
734
 
735
  image = image.convert("RGB")
736
  mask = mask.convert("L")
737
 
738
+ # --------------------------------------------------------
739
+ # Region-based processing
740
+ # --------------------------------------------------------
741
+
742
  if boxes:
743
+
744
  result = image.copy()
745
+
746
+ # Padding around watermark region.
747
+ pad = max(
748
+ 16,
749
+ int(
750
+ min(image.size)
751
+ * 0.02
752
+ ),
753
+ )
754
+
755
  for box in boxes:
756
+
757
  if len(box) != 4:
758
  continue
759
+
760
+ try:
761
+
762
+ x1, y1, x2, y2 = map(
763
+ float,
764
+ box,
765
+ )
766
+
767
+ except Exception:
768
  continue
769
+
770
+ left = max(
771
+ 0,
772
+ int(x1) - pad,
773
+ )
774
+
775
+ top = max(
776
+ 0,
777
+ int(y1) - pad,
778
+ )
779
+
780
+ right = min(
781
+ image.width,
782
+ int(x2) + pad,
783
+ )
784
+
785
+ bottom = min(
786
+ image.height,
787
+ int(y2) + pad,
788
+ )
789
+
790
+ if (
791
+ right <= left
792
+ or bottom <= top
793
+ ):
794
+ continue
795
+
796
+ crop = image.crop(
797
+ (
798
+ left,
799
+ top,
800
+ right,
801
+ bottom,
802
+ )
803
+ )
804
+
805
+ crop_mask = mask.crop(
806
+ (
807
+ left,
808
+ top,
809
+ right,
810
+ bottom,
811
+ )
812
+ )
813
+
814
+ # ------------------------------------------------
815
+ # Convert image
816
+ # ------------------------------------------------
817
+
818
+ crop_np = (
819
+ np.asarray(
820
+ crop,
821
+ dtype=np.float32,
822
+ )
823
+ / 255.0
824
+ )
825
+
826
+ mask_np = (
827
+ np.asarray(
828
+ crop_mask,
829
+ dtype=np.float32,
830
+ )
831
+ / 255.0
832
+ )
833
+
834
+ image_t = (
835
+ torch.from_numpy(
836
+ crop_np
837
+ )
838
+ .permute(
839
+ 2,
840
+ 0,
841
+ 1,
842
+ )
843
+ .unsqueeze(0)
844
+ .to(DEVICE)
845
+ * 2.0
846
+ - 1.0
847
+ )
848
+
849
+ mask_t = (
850
+ torch.from_numpy(
851
+ mask_np
852
+ )
853
+ .unsqueeze(0)
854
+ .unsqueeze(0)
855
+ .to(DEVICE)
856
+ )
857
+
858
+ mask_t = (
859
+ mask_t > 0.5
860
+ ).float()
861
+
862
+ # ------------------------------------------------
863
+ # LaMa inference
864
+ # ------------------------------------------------
865
+
866
  with torch.inference_mode():
867
+
868
+ output = LAMA_MODEL(
869
+ image_t,
870
+ mask_t,
871
+ )
872
+
873
+ if isinstance(
874
+ output,
875
+ (tuple, list),
876
+ ):
877
+ output = output[0]
878
+
879
+ # ------------------------------------------------
880
+ # Convert back to PIL
881
+ # ------------------------------------------------
882
+
883
+ output = (
884
+ (
885
+ output.clamp(
886
+ -1,
887
+ 1,
888
+ )
889
+ + 1.0
890
+ )
891
+ / 2.0
892
+ )
893
+
894
+ output = (
895
+ output
896
+ .squeeze(0)
897
+ .permute(
898
+ 1,
899
+ 2,
900
+ 0,
901
+ )
902
+ .float()
903
+ .cpu()
904
+ .numpy()
905
+ )
906
+
907
+ restored = Image.fromarray(
908
+ (
909
+ output * 255
910
+ )
911
+ .clip(
912
+ 0,
913
+ 255,
914
+ )
915
+ .astype(
916
+ np.uint8
917
+ )
918
+ )
919
+
920
+ result.paste(
921
+ restored,
922
+ (
923
+ left,
924
+ top,
925
+ ),
926
+ )
927
+
928
  return result
929
 
930
+ # ========================================================
931
+ # Full-frame fallback
932
+ # ========================================================
933
+
934
+ image_np = (
935
+ np.asarray(
936
+ image,
937
+ dtype=np.float32,
938
+ )
939
+ / 255.0
940
+ )
941
+
942
+ mask_np = (
943
+ np.asarray(
944
+ mask,
945
+ dtype=np.float32,
946
+ )
947
+ / 255.0
948
+ )
949
+
950
+ image_t = (
951
+ torch.from_numpy(
952
+ image_np
953
+ )
954
+ .permute(
955
+ 2,
956
+ 0,
957
+ 1,
958
+ )
959
+ .unsqueeze(0)
960
+ .to(DEVICE)
961
+ * 2.0
962
+ - 1.0
963
+ )
964
+
965
+ mask_t = (
966
+ torch.from_numpy(
967
+ mask_np
968
+ )
969
+ .unsqueeze(0)
970
+ .unsqueeze(0)
971
+ .to(DEVICE)
972
+ )
973
+
974
+ mask_t = (
975
+ mask_t > 0.5
976
+ ).float()
977
+
978
  with torch.inference_mode():
 
 
 
 
 
979
 
980
+ output = LAMA_MODEL(
981
+ image_t,
982
+ mask_t,
983
+ )
984
+
985
+ if isinstance(
986
+ output,
987
+ (tuple, list),
988
+ ):
989
+ output = output[0]
990
+
991
+ output = (
992
+ (
993
+ output.clamp(
994
+ -1,
995
+ 1,
996
+ )
997
+ + 1.0
998
+ )
999
+ / 2.0
1000
+ )
1001
+
1002
+ output = (
1003
+ output
1004
+ .squeeze(0)
1005
+ .permute(
1006
+ 1,
1007
+ 2,
1008
+ 0,
1009
+ )
1010
+ .float()
1011
+ .cpu()
1012
+ .numpy()
1013
+ )
1014
+
1015
+ return Image.fromarray(
1016
+ (
1017
+ output * 255
1018
+ )
1019
+ .clip(
1020
+ 0,
1021
+ 255,
1022
+ )
1023
+ .astype(
1024
+ np.uint8
1025
+ )
1026
+ )
1027
+
1028
+
1029
+ # ============================================================
1030
+ # Preview
1031
+ # ============================================================
1032
+
1033
+ def preview(
1034
+ image,
1035
+ max_bbox_percent=30,
1036
+ detection_prompt="watermark",
1037
+ ):
1038
+
1039
  if image is None:
1040
+ raise ValueError(
1041
+ "Please upload an image."
1042
+ )
1043
+
1044
+ image = image.convert("RGB")
1045
+
1046
+ boxes = _boxes(
1047
+ image,
1048
+ detection_prompt,
1049
+ )
1050
+
1051
+ # Apply area filtering for preview as well.
1052
+ filtered_boxes = []
1053
 
1054
+ total_area = (
1055
+ image.width
1056
+ * image.height
1057
+ )
1058
+
1059
+ try:
1060
+ max_pct = float(
1061
+ max_bbox_percent
1062
+ )
1063
+ except Exception:
1064
+ max_pct = 30.0
1065
 
1066
  for box in boxes:
1067
+
1068
+ if len(box) != 4:
1069
+ continue
1070
+
1071
+ x1, y1, x2, y2 = map(
1072
+ float,
1073
+ box,
1074
+ )
1075
+
1076
+ area = (
1077
+ max(
1078
+ 0,
1079
+ x2 - x1,
1080
+ )
1081
+ *
1082
+ max(
1083
+ 0,
1084
+ y2 - y1,
1085
  )
1086
+ )
1087
+
1088
+ if (
1089
+ total_area > 0
1090
+ and area / total_area * 100
1091
+ <= max_pct
1092
+ ):
1093
+
1094
+ filtered_boxes.append(
1095
+ box
1096
+ )
1097
+
1098
+ output = image.copy()
1099
+
1100
+ draw = ImageDraw.Draw(
1101
+ output
1102
+ )
1103
+
1104
+ for box in filtered_boxes:
1105
+
1106
+ draw.rectangle(
1107
+ tuple(
1108
+ map(
1109
+ float,
1110
+ box,
1111
+ )
1112
+ ),
1113
+ outline="red",
1114
+ width=4,
1115
+ )
1116
+
1117
+ return output
1118
 
1119
+
1120
+ # ============================================================
1121
+ # Image watermark removal
1122
+ # ============================================================
1123
 
1124
  def remove_image(
1125
  image,
 
1127
  detection_prompt="watermark",
1128
  transparent=False,
1129
  ):
1130
+
1131
  if image is None:
1132
+ raise ValueError(
1133
+ "Please upload an image."
1134
+ )
1135
 
1136
+ image = image.convert("RGB")
1137
+
1138
+ boxes = _boxes(
1139
+ image,
1140
+ detection_prompt,
1141
+ )
1142
 
1143
+ mask = _mask(
1144
+ image,
1145
+ boxes,
1146
+ max_bbox_percent,
1147
+ )
1148
+
1149
+ # Nothing detected
1150
  if not boxes:
 
1151
 
1152
+ output = image.convert(
1153
+ "RGBA"
1154
+ if transparent
1155
+ else "RGB"
1156
+ )
1157
+
1158
+ return output, mask
1159
+
1160
+ # IMPORTANT:
1161
+ # Pass boxes so LaMa performs local inpainting.
1162
+ result = _lama(
1163
+ image,
1164
+ mask,
1165
+ boxes,
1166
+ )
1167
 
1168
  if transparent:
1169
+
1170
+ result = result.convert(
1171
+ "RGBA"
1172
+ )
1173
+
1174
+ result.putalpha(
1175
+ mask.point(
1176
+ lambda p: 255 - p
1177
+ )
1178
+ )
1179
 
1180
  return result, mask