bened commited on
Commit
da0cc60
·
verified ·
1 Parent(s): fc9663d

Delete watermark/engine_v3.py

Browse files
Files changed (1) hide show
  1. watermark/engine_v3.py +0 -455
watermark/engine_v3.py DELETED
@@ -1,455 +0,0 @@
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
- MODEL_CACHE = None
10
- FLORENCE_MODEL = None
11
- FLORENCE_PROCESSOR = None
12
- LAMA_MODEL = None
13
-
14
-
15
- def _get_device():
16
- return torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
-
18
-
19
- def _get_model_dtype():
20
- if not torch.cuda.is_available():
21
- return torch.float32
22
- try:
23
- if torch.cuda.is_bf16_supported():
24
- return torch.bfloat16
25
- except Exception:
26
- pass
27
- return torch.float16
28
-
29
-
30
- def _get_writable_cache():
31
- candidates = []
32
- if os.getenv("WATERMARK_CACHE_DIR"):
33
- candidates.append(Path(os.environ["WATERMARK_CACHE_DIR"]))
34
- candidates += [
35
- Path("/data/.cache/watermark"),
36
- Path("/tmp/watermark"),
37
- Path.home() / ".cache" / "watermark",
38
- ]
39
- for p in candidates:
40
- try:
41
- p.mkdir(parents=True, exist_ok=True)
42
- test = p / ".write_test"
43
- test.write_text("ok", encoding="utf-8")
44
- try:
45
- test.unlink()
46
- except Exception:
47
- pass
48
- print(f"[Watermark] Cache directory: {p}")
49
- return p
50
- except (PermissionError, OSError):
51
- pass
52
- raise RuntimeError("No writable watermark cache directory available.")
53
-
54
-
55
- def _cache_dir():
56
- global MODEL_CACHE
57
- if MODEL_CACHE is None:
58
- MODEL_CACHE = _get_writable_cache()
59
- return MODEL_CACHE
60
-
61
-
62
- def _patch_florence_compatibility():
63
- """Compatibility patches for Florence-2 remote code + newer Transformers."""
64
- try:
65
- from transformers import PretrainedConfig, PreTrainedModel
66
-
67
- if not hasattr(PretrainedConfig, "forced_bos_token_id"):
68
- PretrainedConfig.forced_bos_token_id = None
69
- print("[Florence] Applied forced_bos_token_id compatibility patch.")
70
-
71
- if not hasattr(PreTrainedModel, "_supports_sdpa"):
72
- PreTrainedModel._supports_sdpa = False
73
- print("[Florence] Applied _supports_sdpa compatibility patch.")
74
- except Exception as e:
75
- print(f"[Florence] Compatibility patch skipped: {e}")
76
-
77
-
78
- def _load_models():
79
- global FLORENCE_MODEL, FLORENCE_PROCESSOR, LAMA_MODEL
80
-
81
- if (
82
- FLORENCE_MODEL is not None
83
- and FLORENCE_PROCESSOR is not None
84
- and LAMA_MODEL is not None
85
- ):
86
- return
87
-
88
- _patch_florence_compatibility()
89
-
90
- from transformers import AutoProcessor, AutoModelForCausalLM
91
-
92
- cache = _cache_dir()
93
- model_id = os.getenv(
94
- "FLORENCE_MODEL_ID",
95
- "microsoft/Florence-2-large",
96
- )
97
- device = _get_device()
98
- dtype = _get_model_dtype()
99
-
100
- print(f"[Florence] Loading model: {model_id}")
101
- print(f"[Florence] device={device}, dtype={dtype}")
102
-
103
- FLORENCE_PROCESSOR = AutoProcessor.from_pretrained(
104
- model_id,
105
- cache_dir=str(cache / "hf"),
106
- trust_remote_code=True,
107
- )
108
-
109
- model_kwargs = {
110
- "cache_dir": str(cache / "hf"),
111
- "trust_remote_code": True,
112
- "torch_dtype": dtype,
113
- }
114
-
115
- # Force eager attention so Transformers does not try to negotiate
116
- # SDPA with older Florence remote-code revisions.
117
- try:
118
- model_kwargs["attn_implementation"] = "eager"
119
- except Exception:
120
- pass
121
-
122
- FLORENCE_MODEL = AutoModelForCausalLM.from_pretrained(
123
- model_id,
124
- **model_kwargs,
125
- ).to(device).eval()
126
-
127
- # Patch the actual custom model instance as well.
128
- try:
129
- if not hasattr(FLORENCE_MODEL, "_supports_sdpa"):
130
- FLORENCE_MODEL._supports_sdpa = False
131
- except Exception:
132
- pass
133
-
134
- try:
135
- actual_dtype = next(FLORENCE_MODEL.parameters()).dtype
136
- print(f"[Florence] model dtype={actual_dtype}")
137
- except Exception:
138
- pass
139
-
140
- lama_path = cache / "big-lama.pt"
141
- if not lama_path.exists():
142
- url = os.getenv(
143
- "LAMA_URL",
144
- "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt",
145
- )
146
- print("[LaMa] Downloading model...")
147
- urllib.request.urlretrieve(url, lama_path)
148
-
149
- print("[LaMa] Loading model...")
150
- LAMA_MODEL = torch.jit.load(
151
- str(lama_path),
152
- map_location=device,
153
- ).eval()
154
-
155
- print("[Watermark] All watermark models loaded.")
156
-
157
-
158
- def _boxes(image, prompt):
159
- """Detect watermark bounding boxes using Florence-2."""
160
- _load_models()
161
-
162
- if image is None:
163
- return []
164
-
165
- image = image.convert("RGB")
166
- device = _get_device()
167
-
168
- try:
169
- model_dtype = next(FLORENCE_MODEL.parameters()).dtype
170
- except Exception:
171
- model_dtype = _get_model_dtype()
172
-
173
- task = "<OPEN_VOCABULARY_DETECTION>"
174
- text = task + (prompt or "watermark")
175
-
176
- inputs = FLORENCE_PROCESSOR(
177
- text=text,
178
- images=image,
179
- return_tensors="pt",
180
- )
181
-
182
- prepared = {}
183
- for key, value in inputs.items():
184
- if hasattr(value, "to"):
185
- value = value.to(device)
186
- prepared[key] = value
187
-
188
- # Florence vision tower Conv2D requires input dtype to match model weights.
189
- if "pixel_values" in prepared:
190
- prepared["pixel_values"] = prepared["pixel_values"].to(
191
- device=device,
192
- dtype=model_dtype,
193
- )
194
-
195
- if "input_ids" in prepared:
196
- prepared["input_ids"] = prepared["input_ids"].long()
197
- if "attention_mask" in prepared:
198
- prepared["attention_mask"] = prepared["attention_mask"].long()
199
-
200
- print(
201
- f"[Florence] model_dtype={model_dtype}, "
202
- f"pixel_dtype={prepared.get('pixel_values').dtype if 'pixel_values' in prepared else None}, "
203
- f"device={device}"
204
- )
205
-
206
- generation_kwargs = {
207
- "input_ids": prepared.get("input_ids"),
208
- "pixel_values": prepared.get("pixel_values"),
209
- "max_new_tokens": 1024,
210
- "num_beams": 3,
211
- "do_sample": False,
212
- "use_cache": False,
213
- }
214
- generation_kwargs = {
215
- k: v for k, v in generation_kwargs.items() if v is not None
216
- }
217
-
218
- with torch.inference_mode():
219
- generated = FLORENCE_MODEL.generate(**generation_kwargs)
220
-
221
- decoded = FLORENCE_PROCESSOR.batch_decode(
222
- generated,
223
- skip_special_tokens=False,
224
- )[0]
225
-
226
- print("[Florence] decoded:", decoded[:500])
227
-
228
- parsed = FLORENCE_PROCESSOR.post_process_generation(
229
- decoded,
230
- task=task,
231
- image_size=image.size,
232
- )
233
- data = parsed.get(task, parsed)
234
-
235
- if not isinstance(data, dict):
236
- return []
237
-
238
- raw_boxes = data.get("bboxes", []) or []
239
- valid_boxes = []
240
-
241
- for box in raw_boxes:
242
- if not isinstance(box, (list, tuple)) or len(box) != 4:
243
- continue
244
- try:
245
- x1, y1, x2, y2 = map(float, box)
246
- except (TypeError, ValueError):
247
- continue
248
-
249
- x1 = max(0.0, min(float(image.width), x1))
250
- y1 = max(0.0, min(float(image.height), y1))
251
- x2 = max(0.0, min(float(image.width), x2))
252
- y2 = max(0.0, min(float(image.height), y2))
253
-
254
- if x2 <= x1 or y2 <= y1:
255
- continue
256
-
257
- valid_boxes.append([x1, y1, x2, y2])
258
-
259
- print(f"[Florence] detected {len(valid_boxes)} bounding boxes")
260
- return valid_boxes
261
-
262
-
263
- def _mask(image, boxes, max_pct):
264
- mask = Image.new("L", image.size, 0)
265
- draw = ImageDraw.Draw(mask)
266
- total = image.width * image.height
267
-
268
- for box in boxes or []:
269
- if len(box) != 4:
270
- continue
271
-
272
- x1, y1, x2, y2 = map(float, box)
273
- x1 = max(0, min(image.width, x1))
274
- x2 = max(0, min(image.width, x2))
275
- y1 = max(0, min(image.height, y1))
276
- y2 = max(0, min(image.height, y2))
277
-
278
- area = max(0, x2 - x1) * max(0, y2 - y1)
279
-
280
- if total and area / total * 100 <= max_pct:
281
- draw.rectangle((x1, y1, x2, y2), fill=255)
282
-
283
- # Small expansion around the detected watermark.
284
- return mask.filter(ImageFilter.MaxFilter(7))
285
-
286
-
287
- def _to_lama_tensors(image, mask, device, multiple=8):
288
- """Convert an image/mask crop to LaMa tensors with safe dimensions."""
289
- image = image.convert("RGB")
290
- mask = mask.convert("L")
291
-
292
- w, h = image.size
293
- pad_w = (multiple - (w % multiple)) % multiple
294
- pad_h = (multiple - (h % multiple)) % multiple
295
-
296
- image_np = np.asarray(image, dtype=np.float32) / 255.0
297
- mask_np = np.asarray(mask, dtype=np.float32) / 255.0
298
-
299
- if pad_w or pad_h:
300
- # Edge padding keeps the context natural and avoids black borders.
301
- image_np = np.pad(
302
- image_np,
303
- ((0, pad_h), (0, pad_w), (0, 0)),
304
- mode="edge",
305
- )
306
- mask_np = np.pad(
307
- mask_np,
308
- ((0, pad_h), (0, pad_w)),
309
- mode="constant",
310
- constant_values=0,
311
- )
312
-
313
- image_t = torch.from_numpy(image_np).permute(2, 0, 1).unsqueeze(0).to(device)
314
- image_t = image_t * 2.0 - 1.0
315
-
316
- mask_t = torch.from_numpy(mask_np).unsqueeze(0).unsqueeze(0).to(device)
317
- mask_t = (mask_t > 0.5).float()
318
-
319
- return image_t, mask_t, (w, h)
320
-
321
-
322
- def _run_lama(image, mask):
323
- """Run LaMa on a crop and return a crop-sized PIL image."""
324
- device = _get_device()
325
- image_t, mask_t, original_size = _to_lama_tensors(
326
- image,
327
- mask,
328
- device,
329
- multiple=8,
330
- )
331
-
332
- with torch.inference_mode():
333
- out = LAMA_MODEL(image_t, mask_t)
334
-
335
- if isinstance(out, (tuple, list)):
336
- out = out[0]
337
-
338
- out = (
339
- (out.clamp(-1, 1) + 1.0) / 2.0
340
- ).squeeze(0).permute(1, 2, 0).float().cpu().numpy()
341
-
342
- restored = Image.fromarray(
343
- (out * 255).clip(0, 255).astype(np.uint8)
344
- )
345
-
346
- # Remove any safety padding added for LaMa.
347
- restored = restored.crop(
348
- (0, 0, original_size[0], original_size[1])
349
- )
350
- return restored
351
-
352
-
353
- def _lama(image, mask, boxes=None):
354
- """
355
- Run LaMa only on watermark regions.
356
-
357
- Important V2/V3 fixes:
358
- 1. Pad crop dimensions to a multiple of 8 to avoid LaMa tensor-size
359
- mismatches such as 245 vs 248.
360
- 2. Composite the LaMa result through the actual mask. The old code pasted
361
- the entire restored crop back into the frame, which could create a
362
- bright/white rectangular patch around the watermark.
363
- 3. Keep the original pixels outside the mask untouched.
364
- """
365
- _load_models()
366
-
367
- image = image.convert("RGB")
368
- mask = mask.convert("L")
369
-
370
- if not boxes:
371
- restored = _run_lama(image, mask)
372
- blend_mask = mask.filter(ImageFilter.GaussianBlur(1.0))
373
- return Image.composite(restored, image, blend_mask)
374
-
375
- result = image.copy()
376
- pad = max(16, int(min(image.size) * 0.02))
377
-
378
- for box in boxes:
379
- if not isinstance(box, (list, tuple)) or len(box) != 4:
380
- continue
381
-
382
- x1, y1, x2, y2 = map(float, box)
383
- left = max(0, int(x1) - pad)
384
- top = max(0, int(y1) - pad)
385
- right = min(image.width, int(x2) + pad)
386
- bottom = min(image.height, int(y2) + pad)
387
-
388
- if right <= left or bottom <= top:
389
- continue
390
-
391
- crop = result.crop((left, top, right, bottom))
392
- crop_mask = mask.crop((left, top, right, bottom))
393
-
394
- if not np.asarray(crop_mask).any():
395
- continue
396
-
397
- restored = _run_lama(crop, crop_mask)
398
-
399
- # Feather only the transition edge. Do NOT paste the entire LaMa crop.
400
- blend_mask = crop_mask.filter(ImageFilter.GaussianBlur(1.2))
401
- blended = Image.composite(
402
- restored,
403
- crop,
404
- blend_mask,
405
- )
406
-
407
- result.paste(
408
- blended,
409
- (left, top),
410
- )
411
-
412
- return result
413
-
414
-
415
- def preview(image, max_bbox_percent=30, detection_prompt="watermark"):
416
- if image is None:
417
- raise ValueError("Please upload an image.")
418
-
419
- boxes = _boxes(image, detection_prompt)
420
- out = image.convert("RGB").copy()
421
- draw = ImageDraw.Draw(out)
422
-
423
- for box in boxes:
424
- if len(box) == 4:
425
- draw.rectangle(
426
- tuple(map(float, box)),
427
- outline="red",
428
- width=4,
429
- )
430
-
431
- return out
432
-
433
-
434
- def remove_image(
435
- image,
436
- max_bbox_percent=30,
437
- detection_prompt="watermark",
438
- transparent=False,
439
- ):
440
- if image is None:
441
- raise ValueError("Please upload an image.")
442
-
443
- boxes = _boxes(image, detection_prompt)
444
- mask = _mask(image, boxes, max_bbox_percent)
445
-
446
- if not boxes:
447
- return image.convert("RGBA" if transparent else "RGB"), mask
448
-
449
- result = _lama(image, mask, boxes=boxes)
450
-
451
- if transparent:
452
- result = result.convert("RGBA")
453
- result.putalpha(mask.point(lambda p: 255 - p))
454
-
455
- return result, mask