| --- |
| language: |
| - en |
| base_model: |
| - answerdotai/ModernBERT-base |
| library_name: pytorch |
| pipeline_tag: image-to-image |
| tags: |
| - watermarking |
| - robustness |
| - image-processing |
| - torchscript |
| - autoencoder |
| - arxiv:2510.00799 |
| license: bsd-3-clause |
| paper: https://arxiv.org/abs/2510.00799 |
| --- |
| # LatentSeal |
|
|
| Fast, secure & high‑capacity **semantic image watermarking** with text‑autoencoded messages. |
|
|
| > Hide full sentences in images, survive heavy JPEG/crop/noise, decode in real time. |
| * [Model Paper](https://arxiv.org/abs/2510.00799) |
|
|
| ## Installation |
|
|
| ```bash |
| pip install latseal # use `pip install -e .` when developing locally |
| ``` |
|
|
| The first call to `latseal.embed(...)` or `latseal.encode(...)` will download |
| the required TorchScript models and autoencoder checkpoint into |
| `~/.cache/latseal` (override with `LATSEAL_CACHE_DIR`). The download is ~1.5 GB |
| and only happens once per machine. Set `LATSEAL_LOCAL_ONLY=1` to disable |
| network access and rely on locally provided weights via the `LATSEAL_*` |
| environment overrides documented in `latseal._resources`. By default the assets |
| are pulled from the Hugging Face repository [`Gevennou/lseal`](https://huggingface.co/Gevennou/lseal). |
|
|
|
|
| Watermarking and autoencoding can be used independently or chained, depending |
| on your workflow. |
|
|
| ## Quickstart Demo |
|
|
| Watermarking and text autoencoding demonstrated separately (requires the |
| `requests` package): |
|
|
| ```python |
| from pathlib import Path |
| |
| import requests |
| import torch |
| from PIL import Image |
| import latseal |
| |
| |
| img_url = "https://images.dog.ceo/breeds/frise-bichon/3.jpg" # 640x640 image |
| img_path = Path("latseal_demo_input.jpg") |
| secret_key = "demo-secret" |
| text_payload = ( |
| "Close-up photograph of a gourmet grilled cheese sandwich that has been artistically sliced in half. Each half reveals a gooey, white cheese center ith an enticing stringy, melted cheese bridge connecting them. The sandwich features double" |
| ) |
| |
| response = requests.get(img_url, timeout=30) |
| response.raise_for_status() |
| img_path.write_bytes(response.content) |
| image = Image.open(img_path).convert("RGB") |
| |
| # --- Watermark round-trip --- |
| message = latseal.random_message() |
| secret_message = latseal.secret_rotation(message, secret_key=secret_key) |
| wm_image = latseal.embed(secret_message, image, secret_key=None) |
| wm_image.save("latseal_demo_watermarked.jpg") |
| |
| recovered = latseal.detect(wm_image, secret_key=None) |
| recovered = latseal.secret_rotation(recovered, secret_key=secret_key, inverse=True) |
| |
| cos_sim = torch.dot(message, recovered) / (message.norm() * recovered.norm()) |
| print(f"Cosine similarity between original and recovered message: {float(cos_sim):.4f}") |
| |
| # --- Text autoencoder round-trip --- |
| latent = latseal.encode(text_payload) |
| decoded = latseal.decode(latent) |
| score = latseal.confidence(latent) |
| |
| print(f"Original text: {text_payload}") |
| print(f"Decoded text : {decoded}") |
| print(f"Confidence : {score:.3f}") |
| ``` |
|
|
| ## All-in-One Text Latent Demo |
|
|
| Prefer to hide a specific sentence directly? Encode it, embed the latent, and |
| report fidelity metrics (requires the `requests` and `numpy` packages): |
|
|
| ```python |
| from pathlib import Path |
| |
| import requests |
| import torch |
| from PIL import Image |
| import latseal |
| import numpy as np |
| |
| |
| def psnr(img_a, img_b, max_val=255.0): |
| """PSNR in dB for 8-bit RGB PIL images.""" |
| a = np.asarray(img_a, dtype=np.float32) |
| b = np.asarray(img_b, dtype=np.float32) |
| mse = np.mean((a - b) ** 2) |
| if mse == 0: |
| return float("inf") |
| return 20 * np.log10(max_val) - 10 * np.log10(mse) |
| |
| |
| img_url = "https://images.dog.ceo/breeds/frise-bichon/3.jpg" # 640x640 image |
| img_path = Path("latseal_demo_psnr_input.jpg") |
| secret_key = "demo-secret" |
| text_payload = ( |
| "Close-up photograph of a gourmet grilled cheese sandwich that has been artistically sliced in half. Each half reveals a gooey, white cheese center with an enticing stringy, melted cheese bridge connecting them. The sandwich features double" |
| ) |
| |
| response = requests.get(img_url, timeout=30) |
| response.raise_for_status() |
| img_path.write_bytes(response.content) |
| image = Image.open(img_path).convert("RGB") |
| |
| latent_message = latseal.encode(text_payload).squeeze(0) |
| secret_message = latseal.secret_rotation(latent_message, secret_key=secret_key) |
| wm_image = latseal.embed(secret_message, image, secret_key=None) |
| wm_image.save("latseal_text_latent_watermarked.jpg") |
| |
| detected = latseal.detect(wm_image, secret_key=None) |
| detected = latseal.secret_rotation(detected, secret_key=secret_key, inverse=True).squeeze(0) |
| |
| print(f"PSNR: {psnr(image, wm_image):.2f} dB") |
| cos_sim = torch.dot(latent_message, detected) / (latent_message.norm() * detected.norm()) |
| print(f"Cosine similarity: {float(cos_sim):.4f}") |
| print(f"Decoded text: {latseal.decode(detected)}") |
| print(f"Confidence: {latseal.confidence(detected)}") |
| ``` |
|
|
| ## Features |
| * **Content‑aware payload** – 256‑D unit‑norm latent vectors from a lightweight text autoencoder (TAE). |
| * **Robust embedding** – finetuned watermark model plus a secret invertible rotation (“spin”) boosts security. |
| * **High capacity** – > 256 bits (full sentences) per image. |
| * **Confidence score** – flags unreliable extractions via the confidence metric. |
|
|
| ## Citation |
|
|
| ``` |
| @misc{evennou2025fastsecurehighcapacityimage, |
| title={Fast, Secure, and High-Capacity Image Watermarking with Autoencoded Text Vectors}, |
| author={Gautier Evennou and Vivien Chappelier and Ewa Kijak}, |
| year={2025}, |
| eprint={2510.00799}, |
| archivePrefix={arXiv}, |
| primaryClass={cs.CR}, |
| url={https://arxiv.org/abs/2510.00799}, |
| } |
| ``` |
|
|
| ## License |
| BSD-3-Clause-Attribution — see `LICENSE`. |
|
|