Coverage for markdiffusion / watermark / tr / tr.py: 91.72%
145 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:17 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:17 +0000
1from ..base import BaseWatermark, BaseConfig
2from markdiffusion.utils.media_utils import *
3import torch
4from typing import Dict, Union, List, Optional
5from markdiffusion.utils.utils import set_random_seed, inherit_docstring
6from markdiffusion.utils.diffusion_config import DiffusionConfig
7import copy
8import numpy as np
9from PIL import Image
10from markdiffusion.visualize.data_for_visualization import DataForVisualization
11from markdiffusion.detection.tr.tr_detection import TRDetector
13class TRConfig(BaseConfig):
14 """Config class for TR algorithm, load config file and initialize parameters."""
16 def initialize_parameters(self) -> None:
17 """Initialize algorithm-specific parameters."""
18 self.w_seed = self.config_dict['w_seed']
19 self.w_channel = self.config_dict['w_channel']
20 self.w_pattern = self.config_dict['w_pattern']
21 # self.w_mask_shape = self.config_dict['w_mask_shape']
22 self.w_radius = self.config_dict['w_radius']
23 self.w_pattern_const = self.config_dict['w_pattern_const']
24 self.threshold = self.config_dict['threshold']
25 self.threshold_p_value = self.config_dict.get('threshold_p_value', 0.01)
27 @property
28 def algorithm_name(self) -> str:
29 """Return the algorithm name."""
30 return 'TR'
32class TRUtils:
33 """Utility class for TR algorithm, contains helper functions."""
35 def __init__(self, config: TRConfig, *args, **kwargs) -> None:
36 """
37 Initialize the Tree-Ring watermarking algorithm.
39 Parameters:
40 config (TRConfig): Configuration for the Tree-Ring algorithm.
41 """
42 self.config = config
43 self.gt_patch = self._get_watermarking_pattern()
44 self.watermarking_mask = self._get_watermarking_mask(self.config.init_latents)
46 def _circle_mask(self, size: int=64, r: int=10, x_offset: int=0, y_offset: int=0) -> np.ndarray:
47 """Generate a circular mask."""
48 x0 = y0 = size // 2
49 x0 += x_offset
50 y0 += y_offset
51 y, x = np.ogrid[:size, :size]
52 y = y[::-1]
54 return ((x - x0)**2 + (y-y0)**2)<= r**2
56 def _get_watermarking_pattern(self) -> torch.Tensor:
57 """Get the ground truth watermarking pattern."""
58 set_random_seed(self.config.w_seed)
60 gt_init = get_random_latents(pipe=self.config.pipe, height=self.config.image_size[0], width=self.config.image_size[1])
62 if 'seed_ring' in self.config.w_pattern:
63 gt_patch = gt_init
65 gt_patch_tmp = copy.deepcopy(gt_patch)
66 for i in range(self.config.w_radius, 0, -1):
67 tmp_mask = self._circle_mask(gt_init.shape[-1], r=i)
68 tmp_mask = torch.tensor(tmp_mask).to(self.config.device)
70 for j in range(gt_patch.shape[1]):
71 gt_patch[:, j, tmp_mask] = gt_patch_tmp[0, j, 0, i].item()
72 elif 'seed_zeros' in self.config.w_pattern:
73 gt_patch = gt_init * 0
74 elif 'seed_rand' in self.config.w_pattern:
75 gt_patch = gt_init
76 elif 'rand' in self.config.w_pattern:
77 gt_patch = torch.fft.fftshift(torch.fft.fft2(gt_init), dim=(-1, -2))
78 gt_patch[:] = gt_patch[0]
79 elif 'zeros' in self.config.w_pattern:
80 gt_patch = torch.fft.fftshift(torch.fft.fft2(gt_init), dim=(-1, -2)) * 0
81 elif 'const' in self.config.w_pattern:
82 gt_patch = torch.fft.fftshift(torch.fft.fft2(gt_init), dim=(-1, -2)) * 0
83 gt_patch += self.config.w_pattern_const
84 elif 'ring' in self.config.w_pattern:
85 gt_patch = torch.fft.fftshift(torch.fft.fft2(gt_init), dim=(-1, -2))
87 gt_patch_tmp = copy.deepcopy(gt_patch)
88 for i in range(self.config.w_radius, 0, -1):
89 tmp_mask = self._circle_mask(gt_init.shape[-1], r=i)
90 tmp_mask = torch.tensor(tmp_mask).to(self.config.device)
92 for j in range(gt_patch.shape[1]):
93 gt_patch[:, j, tmp_mask] = gt_patch_tmp[0, j, 0, i].item()
95 return gt_patch
97 def _get_watermarking_mask(self, init_latents: torch.Tensor) -> torch.Tensor:
98 """Get the watermarking mask."""
99 watermarking_mask = torch.zeros(init_latents.shape, dtype=torch.bool).to(self.config.device)
101 # if self.config.w_mask_shape == 'circle':
102 np_mask = self._circle_mask(init_latents.shape[-1], r=self.config.w_radius)
103 torch_mask = torch.tensor(np_mask).to(self.config.device)
105 if self.config.w_channel == -1:
106 # all channels
107 watermarking_mask[:, :] = torch_mask
108 else:
109 watermarking_mask[:, self.config.w_channel] = torch_mask
110 # elif self.config.w_mask_shape == 'square':
111 # anchor_p = init_latents.shape[-1] // 2
112 # if self.config.w_channel == -1:
113 # # all channels
114 # watermarking_mask[:, :, anchor_p-self.config.w_radius:anchor_p+self.config.w_radius, anchor_p-self.config.w_radius:anchor_p+self.config.w_radius] = True
115 # else:
116 # watermarking_mask[:, self.config.w_channel, anchor_p-self.config.w_radius:anchor_p+self.config.w_radius, anchor_p-self.config.w_radius:anchor_p+self.config.w_radius] = True
117 # elif self.config.w_mask_shape == 'no':
118 # pass
119 # else:
120 # raise NotImplementedError(f'w_mask_shape: {self.config.w_mask_shape}')
122 return watermarking_mask
124 def inject_watermark(self, init_latents: torch.Tensor) -> torch.Tensor:
125 init_latents_w_fft = torch.fft.fftshift(torch.fft.fft2(init_latents), dim=(-1, -2))
126 target_patch = self.gt_patch
128 if not torch.is_complex(target_patch):
129 real = target_patch.to(torch.float32)
130 imag = torch.zeros_like(real)
131 target_patch = torch.complex(real, imag)
132 target_patch = target_patch.to(init_latents_w_fft.dtype)
134 init_latents_w_fft[self.watermarking_mask] = target_patch[self.watermarking_mask].clone()
136 init_latents_w = torch.fft.ifft2(torch.fft.ifftshift(init_latents_w_fft, dim=(-1, -2))).real
137 return init_latents_w
139@inherit_docstring
140class TR(BaseWatermark):
141 def __init__(self,
142 watermark_config: TRConfig,
143 *args, **kwargs):
144 """
145 Initialize the TR watermarking algorithm.
147 Parameters:
148 watermark_config (TRConfig): Configuration instance of the Tree-Ring algorithm.
149 """
150 self.config = watermark_config
151 self.utils = TRUtils(self.config)
153 self.detector = TRDetector(
154 watermarking_mask=self.utils.watermarking_mask,
155 gt_patch=self.utils.gt_patch,
156 threshold=self.config.threshold,
157 device=self.config.device,
158 threshold_p_value=self.config.threshold_p_value,
159 )
161 def _generate_watermarked_image(self, prompt: str, *args, **kwargs) -> Image.Image:
162 """Internal method to generate a watermarked image."""
163 watermarked_latents = self.utils.inject_watermark(self.config.init_latents)
165 # save watermarked latents
166 self.set_orig_watermarked_latents(watermarked_latents)
168 # Construct generation parameters
169 generation_params = {
170 "num_images_per_prompt": self.config.num_images,
171 "guidance_scale": self.config.guidance_scale,
172 "num_inference_steps": self.config.num_inference_steps,
173 "height": self.config.image_size[0],
174 "width": self.config.image_size[1],
175 "latents": watermarked_latents,
176 }
178 # Add parameters from config.gen_kwargs
179 if hasattr(self.config, "gen_kwargs") and self.config.gen_kwargs:
180 for key, value in self.config.gen_kwargs.items():
181 if key not in generation_params:
182 generation_params[key] = value
184 # Use kwargs to override default parameters
185 for key, value in kwargs.items():
186 generation_params[key] = value
188 # Ensure latents parameter is not overridden
189 generation_params["latents"] = watermarked_latents
191 return self.config.pipe(
192 prompt,
193 **generation_params
194 ).images[0]
196 def _detect_watermark_in_image(self,
197 image: Image.Image,
198 prompt: str = "",
199 *args,
200 **kwargs) -> Dict[str, float]:
201 """Detect the watermark in the image."""
202 # Use config values as defaults if not explicitly provided
203 guidance_scale_to_use = kwargs.get('guidance_scale', self.config.guidance_scale)
204 num_steps_to_use = kwargs.get('num_inference_steps', self.config.num_inference_steps)
206 # Step 1: Get Text Embeddings
207 do_classifier_free_guidance = (guidance_scale_to_use > 1.0)
208 prompt_embeds, negative_prompt_embeds = self.config.pipe.encode_prompt(
209 prompt=prompt,
210 device=self.config.device,
211 do_classifier_free_guidance=do_classifier_free_guidance,
212 num_images_per_prompt=1, # TODO: Multiple image generation to be supported
213 )
215 if do_classifier_free_guidance:
216 text_embeddings = torch.cat([negative_prompt_embeds, prompt_embeds])
217 else:
218 text_embeddings = prompt_embeds
220 # Step 2: Preprocess Image
221 image = transform_to_model_format(image, target_size=self.config.image_size[0]).unsqueeze(0).to(text_embeddings.dtype).to(self.config.device)
223 # Step 3: Get Image Latents
224 image_latents = get_media_latents(pipe=self.config.pipe, media=image, sample=False, decoder_inv=kwargs.get('decoder_inv', False))
226 # Step 4: Reverse Image Latents
227 # Pass only known parameters to forward_diffusion, and let kwargs handle any additional parameters
228 inversion_kwargs = {k: v for k, v in kwargs.items() if k not in ['decoder_inv', 'guidance_scale', 'num_inference_steps']}
230 reversed_latents = self.config.inversion.forward_diffusion(
231 latents=image_latents,
232 text_embeddings=text_embeddings,
233 guidance_scale=guidance_scale_to_use,
234 num_inference_steps=num_steps_to_use,
235 **inversion_kwargs
236 )[-1]
238 # Step 5: Evaluate Watermark
239 if 'detector_type' in kwargs:
240 return self.detector.eval_watermark(reversed_latents, detector_type=kwargs['detector_type'])
241 else:
242 return self.detector.eval_watermark(reversed_latents)
244 def get_data_for_visualize(self,
245 image: Image.Image,
246 prompt: str="",
247 guidance_scale: Optional[float]=None,
248 decoder_inv: bool=False,
249 *args,
250 **kwargs) -> DataForVisualization:
251 """Get data for visualization including detection inversion - similar to GS logic."""
252 # Use config values as defaults if not explicitly provided
253 guidance_scale_to_use = guidance_scale if guidance_scale is not None else self.config.guidance_scale
255 # Step 1: Generate watermarked latents (generation process)
256 set_random_seed(self.config.gen_seed)
257 watermarked_latents = self.utils.inject_watermark(self.config.init_latents)
259 # Step 2: Generate actual watermarked image using the same process as _generate_watermarked_image
260 generation_params = {
261 "num_images_per_prompt": self.config.num_images,
262 "guidance_scale": self.config.guidance_scale,
263 "num_inference_steps": self.config.num_inference_steps,
264 "height": self.config.image_size[0],
265 "width": self.config.image_size[1],
266 "latents": watermarked_latents,
267 }
269 # Add parameters from config.gen_kwargs
270 if hasattr(self.config, "gen_kwargs") and self.config.gen_kwargs:
271 for key, value in self.config.gen_kwargs.items():
272 if key not in generation_params:
273 generation_params[key] = value
275 # Generate the actual watermarked image
276 watermarked_image = self.config.pipe(
277 prompt,
278 **generation_params
279 ).images[0]
281 # Step 3: Perform watermark detection to get inverted latents (detection process)
282 inverted_latents = None
283 try:
284 # Get Text Embeddings for detection
285 do_classifier_free_guidance = (guidance_scale_to_use > 1.0)
286 prompt_embeds, negative_prompt_embeds = self.config.pipe.encode_prompt(
287 prompt=prompt,
288 device=self.config.device,
289 do_classifier_free_guidance=do_classifier_free_guidance,
290 num_images_per_prompt=1, # TODO: Multiple image generation to be supported
291 )
293 if do_classifier_free_guidance:
294 text_embeddings = torch.cat([negative_prompt_embeds, prompt_embeds])
295 else:
296 text_embeddings = prompt_embeds
298 # Preprocess watermarked image for detection
299 processed_image = transform_to_model_format(
300 watermarked_image,
301 target_size=self.config.image_size[0]
302 ).unsqueeze(0).to(text_embeddings.dtype).to(self.config.device)
304 # Get Image Latents
305 image_latents = get_media_latents(
306 pipe=self.config.pipe,
307 media=processed_image,
308 sample=False,
309 decoder_inv=decoder_inv
310 )
312 # Reverse Image Latents to get inverted noise
313 inversion_kwargs = {k: v for k, v in kwargs.items() if k not in ['prompt', 'decoder_inv', 'guidance_scale', 'num_inference_steps']}
315 reversed_latents_list = self.config.inversion.forward_diffusion(
316 latents=image_latents,
317 text_embeddings=text_embeddings,
318 guidance_scale=guidance_scale_to_use,
319 num_inference_steps=self.config.num_inference_steps,
320 **inversion_kwargs
321 )
323 inverted_latents = reversed_latents_list[-1]
325 except Exception as e:
326 print(f"Warning: Could not perform inversion for visualization: {e}")
327 inverted_latents = None
329 # Step 4: Prepare visualization data
330 return DataForVisualization(
331 config=self.config,
332 utils=self.utils,
333 reversed_latents=reversed_latents_list,
334 orig_watermarked_latents=self.orig_watermarked_latents,
335 image=image,
336 )
337# try tr