| --- |
| language: |
| - en |
| tags: |
| - image-watermarking |
| - onnx |
| - computer-vision |
| --- |
| |
| # Watermark Anything Model (WAM) - ONNX |
|
|
| This repository contains the ONNX conversion of the **Watermark Anything Model (WAM)**. To ensure optimal performance and flexibility, the monolithic PyTorch model has been decomposed into three distinct ONNX computational graphs. |
|
|
| ## Model Components |
|
|
| The watermarking pipeline is divided into three `.onnx` files, ensuring that image scaling and heavy computations (like Just Noticeable Difference - JND attenuation) can be performed optimally. |
|
|
| 1. **`embedder.onnx`** |
| - **Inputs:** |
| - `image`: Float32 `[batch_size, 3, 256, 256]` (Normalized image) |
| - `message`: Float32 `[batch_size, nbits]` (Binary payload to embed) |
| - **Outputs:** |
| - `watermark`: Float32 `[batch_size, 3, 256, 256]` (Watermark signal/deltas) |
|
|
| 2. **`blender.onnx`** |
| - **Inputs:** |
| - `imgs`: Float32 `[batch_size, 3, height, width]` (Original normalized image at any resolution) |
| - `deltas`: Float32 `[batch_size, 3, height, width]` (Watermark signal resized to original image dimensions) |
| - **Outputs:** |
| - `watermarked_image`: Float32 `[batch_size, 3, height, width]` (Final normalized image with JND applied) |
|
|
| 3. **`extractor.onnx`** |
| - **Inputs:** |
| - `image`: Float32 `[batch_size, 3, 256, 256]` (Watermarked image, resized and normalized) |
| - **Outputs:** |
| - `mask`: Float32 `[batch_size, 1 + nbits, 256, 256]` (Detected watermark mask and extracted message bits) |
|
|
| --- |
|
|
| ## Inference Guide (Python) |
|
|
| You will need `onnxruntime`, `numpy`, `torch`, `torchvision`, and `Pillow`. |
|
|
| ### 1. Installation |
|
|
| ```bash |
| pip install onnxruntime numpy torch torchvision pillow |
| ``` |
|
|
| ### 2. End-to-End Example |
|
|
| Below is a complete Python script to load an image, embed a watermark, save the result, and extract the watermark mask. |
|
|
| ```python |
| import numpy as np |
| import onnxruntime as ort |
| import torch |
| from torchvision import transforms |
| from PIL import Image |
| |
| # --------------------------------------------------------- |
| # 1. Initialization |
| # --------------------------------------------------------- |
| # Load ONNX Runtime sessions |
| embedder_sess = ort.InferenceSession("embedder.onnx") |
| blender_sess = ort.InferenceSession("blender.onnx") |
| extractor_sess = ort.InferenceSession("extractor.onnx") |
| |
| # Define ImageNet normalization stats |
| mean = [0.485, 0.456, 0.406] |
| std = [0.229, 0.224, 0.225] |
| |
| normalize = transforms.Normalize(mean=mean, std=std) |
| unnormalize = transforms.Normalize( |
| mean=[-m/s for m, s in zip(mean, std)], |
| std=[1/s for s in std] |
| ) |
| resize_256 = transforms.Resize((256, 256), antialias=True) |
| to_tensor = transforms.ToTensor() |
| |
| # Configure watermark bits (0 means only localizer mask, no payload) |
| nbits = 0 |
| |
| # --------------------------------------------------------- |
| # 2. Image Preprocessing |
| # --------------------------------------------------------- |
| img_path = "input_image.jpg" |
| pil_img = Image.open(img_path).convert("RGB") |
| img_tensor = to_tensor(pil_img).unsqueeze(0) # [1, 3, H, W] |
| |
| # Normalize for the model |
| img_normalized = normalize(img_tensor) |
| original_h, original_w = img_tensor.shape[2], img_tensor.shape[3] |
| |
| # --------------------------------------------------------- |
| # 3. Embedding the Watermark Signal |
| # --------------------------------------------------------- |
| img_resized = resize_256(img_normalized) |
| msg = np.random.randint(0, 2, (1, nbits)).astype(np.float32) |
| |
| embedder_inputs = { |
| 'image': img_resized.numpy(), |
| 'message': msg |
| } |
| # Output is [1, 3, 256, 256] |
| watermark_deltas = embedder_sess.run(None, embedder_inputs)[0] |
| |
| # --------------------------------------------------------- |
| # 4. Blending & JND Attenuation |
| # --------------------------------------------------------- |
| # Resize the watermark signal back to the original image resolution |
| inverse_resize = transforms.Resize((original_h, original_w), antialias=True) |
| watermark_deltas_full = inverse_resize(torch.from_numpy(watermark_deltas)).numpy() |
| |
| blender_inputs = { |
| 'imgs': img_normalized.numpy(), |
| 'deltas': watermark_deltas_full |
| } |
| # Output is [1, 3, H, W] normalized |
| watermarked_img_normalized = blender_sess.run(None, blender_inputs)[0] |
| |
| # --------------------------------------------------------- |
| # 5. Postprocessing and Saving |
| # --------------------------------------------------------- |
| watermarked_img_tensor = torch.from_numpy(watermarked_img_normalized) |
| watermarked_img_unnorm = unnormalize(watermarked_img_tensor) |
| watermarked_img_clamped = torch.clamp(watermarked_img_unnorm, 0, 1) |
| |
| output_img = transforms.ToPILImage()(watermarked_img_clamped.squeeze(0)) |
| output_img.save("watermarked_image.png") |
| print("✅ Watermarked image saved!") |
| |
| # --------------------------------------------------------- |
| # 6. Extraction Detection |
| # --------------------------------------------------------- |
| # Resize the watermarked image to 256x256 before feeding into the extractor |
| extractor_input = resize_256(watermarked_img_tensor).numpy() |
| |
| extractor_inputs = { |
| 'image': extractor_input |
| } |
| extracted_mask = extractor_sess.run(None, extractor_inputs)[0] |
| |
| print(f"✅ Extracted mask shape: {extracted_mask.shape}") |
| ``` |
|
|
| ## Performance & Optimization |
| Replacing the native monolithic implementation with these ONNX models offers massive portability, meaning it can be run seamlessly on edge devices using ONNX Runtime (CPU, CUDA, TensorRT, CoreML). |
|
|
| *Note: The `blender.onnx` relies on **dynamic axes** for image resolution, allowing you to avoid resizing your pristine original images. The `embedder` and `extractor` graphs operate strictly at `256x256` to align with their training resolutions.* |
|
|
|
|
| --- |
| license: mit |
| --- |
|
|