Name | crmark JSON |
Version |
1.1.0
JSON |
| download |
home_page | https://github.com/chenoly/crmark |
Summary | A robust reversible watermarking method that can robustly extract the watermark in lossy channels and perfectly recover both the cover image and the watermark in lossless channels. |
upload_time | 2025-07-23 03:09:33 |
maintainer | None |
docs_url | None |
author | chenoly |
requires_python | >=3.7 |
license | None |
keywords |
robust
reversible
watermarking
|
VCS |
 |
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
## 📝 Introduction
**CRMark: Cover-Recoverable Watermark**, a robust and reversible invisible image watermarking method. CRMark enables perfect reconstruction of the original cover image in lossless channels and robust watermark extraction in lossy channels.
CRMark leverages an **Integer Invertible Watermark Network (iIWN)** to achieve lossless and invertible mapping between cover-watermark pairs and stego images. It addresses the trade-off between robustness and reversibility in traditional robust reversible watermarking methods, offering significant improvements in robustness, visual quality, and computational efficiency.
Key features:
- **Robustness**: Enhanced against distortions through an Encoder-Noise Layer-Decoder framework.
- **Reversibility**: Ensures lossless recovery of both the cover image and the watermark in lossless channel.
- **Efficiency**: Reduces time complexity and auxiliary bitstream length.
---
## 🚀 Usage
```bash
pip install crmark
```
code
```bash
import os
import random
import string
import numpy as np
from PIL import Image
from crmark import CRMark
# Create output directory if not exists
os.makedirs("images", exist_ok=True)
# Initialize CRMark in color mode
crmark = CRMark(model_mode="color_256_64_complex")
# Generate a random string of length 3 (total 24 bits)
def generate_random_string(n: int) -> str:
characters = string.ascii_letters + string.digits
return ''.join(random.choices(characters, k=n))
# Calculate PSNR between two images
def calculate_psnr(img1, img2):
mse = np.mean((img1 - img2) ** 2)
if mse == 0:
return float('inf')
PIXEL_MAX = 255.0
psnr = 20 * np.log10(PIXEL_MAX / np.sqrt(mse))
return psnr
# Random string message
str_data = generate_random_string(5)
str_data = "hello"
print(str_data)
# Define image paths
cover_path = "images/cover_color.png"
rec_cover_path = "images/rec_color_cover.png"
attack_rec_cover_path = "images/attack_rec_color_cover.png"
stego_path_clean = "images/color_stego_clean.png"
stego_path_attacked = "images/color_stego_attacked.png"
residual_path_clean = "images/color_residual.png"
# === Case 1: Without attack ===
# Encode string into image
cover_image = np.float32(Image.open(cover_path).resize((256, 256)))
success, stego_image = crmark.encode(cover_image, str_data)
print("psnr:", calculate_psnr(np.float32(stego_image), cover_image))
if success:
stego_image.save(stego_path_clean)
stego_clean_image = np.float32(stego_image)
residual = np.abs((stego_clean_image - cover_image) * 10.) + 127.5
Image.fromarray(np.uint8(np.clip(residual, 0, 255))).save(residual_path_clean)
# Recover cover and message from clean image
stego_clean_image = np.float32(Image.open(stego_path_clean))
is_attacked_clean, rec_cover_clean, rec_message_clean = crmark.recover(stego_clean_image)
is_decoded, extracted_message_clean = crmark.decode(stego_clean_image)
rec_cover_clean.save(rec_cover_path)
# Compute pixel difference between original and recovered cover
cover = np.float32(Image.open(cover_path).resize((256, 256)))
rec_clean = np.float32(rec_cover_clean)
diff_clean = np.sum(np.abs(cover - rec_clean))
# === Case 2: With attack ===
# Slightly modify the image to simulate attack
stego = np.float32(Image.open(stego_path_clean))
H, W, C = stego.shape
rand_y = random.randint(0, H - 1)
rand_x = random.randint(0, W - 1)
rand_c = random.randint(0, C - 1)
# Apply a small perturbation (±1)
perturbation = random.choice([-1, 1])
stego[rand_y, rand_x, rand_c] = np.clip(stego[rand_y, rand_x, rand_c] + perturbation, 0, 255)
stego = np.clip(stego + np.random.randint(0, 20, size=stego.shape), 0, 255)
Image.fromarray(np.uint8(stego)).save(stego_path_attacked)
# Recover from attacked image
stego_attacked_image = np.float32(Image.open(stego_path_attacked))
is_attacked, rec_cover_attacked, rec_message_attacked = crmark.recover(stego_attacked_image)
is_attacked_flag, extracted_message_attacked = crmark.decode(stego_attacked_image)
rec_cover_attacked.save(attack_rec_cover_path)
rec_attacked = np.float32(rec_cover_attacked)
diff_attacked = np.sum(np.abs(cover - rec_attacked))
attack_rec_psnr = calculate_psnr(rec_attacked, cover)
# === Print results ===
print("=== Without Attack ===")
print("Original Message:", str_data)
print("Recovered Message:", rec_message_clean)
print("Extracted Message:", extracted_message_clean)
print("Was Attacked:", is_attacked_clean)
print("L1 Pixel Difference:", diff_clean)
print("\n=== With Attack ===")
print("Recovered Message:", rec_message_attacked)
print("Extracted Message:", extracted_message_attacked)
print("Was Attacked:", is_attacked)
print("L1 Pixel Difference:", diff_attacked)
print("L1 Pixel Psnr:", attack_rec_psnr)
else:
print("Emebdding failed!")
```
Raw data
{
"_id": null,
"home_page": "https://github.com/chenoly/crmark",
"name": "crmark",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": null,
"keywords": "robust reversible watermarking",
"author": "chenoly",
"author_email": "chenoly@outlook.com",
"download_url": "https://files.pythonhosted.org/packages/34/f8/5802bff2c93339e59eb6a8c2ca0aaac84d67fd1c61f51b93434336fa262e/crmark-1.1.0.tar.gz",
"platform": null,
"description": "## \ud83d\udcdd Introduction\r\n\r\n**CRMark: Cover-Recoverable Watermark**, a robust and reversible invisible image watermarking method. CRMark enables perfect reconstruction of the original cover image in lossless channels and robust watermark extraction in lossy channels.\r\n\r\nCRMark leverages an **Integer Invertible Watermark Network (iIWN)** to achieve lossless and invertible mapping between cover-watermark pairs and stego images. It addresses the trade-off between robustness and reversibility in traditional robust reversible watermarking methods, offering significant improvements in robustness, visual quality, and computational efficiency.\r\n\r\nKey features:\r\n- **Robustness**: Enhanced against distortions through an Encoder-Noise Layer-Decoder framework.\r\n- **Reversibility**: Ensures lossless recovery of both the cover image and the watermark in lossless channel.\r\n- **Efficiency**: Reduces time complexity and auxiliary bitstream length.\r\n\r\n---\r\n## \ud83d\ude80 Usage\r\n```bash\r\npip install crmark\r\n```\r\n\r\ncode\r\n```bash\r\nimport os\r\nimport random\r\nimport string\r\nimport numpy as np\r\nfrom PIL import Image\r\nfrom crmark import CRMark\r\n\r\n# Create output directory if not exists\r\nos.makedirs(\"images\", exist_ok=True)\r\n\r\n# Initialize CRMark in color mode\r\ncrmark = CRMark(model_mode=\"color_256_64_complex\")\r\n\r\n\r\n# Generate a random string of length 3 (total 24 bits)\r\ndef generate_random_string(n: int) -> str:\r\n characters = string.ascii_letters + string.digits\r\n return ''.join(random.choices(characters, k=n))\r\n\r\n\r\n# Calculate PSNR between two images\r\ndef calculate_psnr(img1, img2):\r\n mse = np.mean((img1 - img2) ** 2)\r\n if mse == 0:\r\n return float('inf')\r\n PIXEL_MAX = 255.0\r\n psnr = 20 * np.log10(PIXEL_MAX / np.sqrt(mse))\r\n return psnr\r\n\r\n\r\n# Random string message\r\nstr_data = generate_random_string(5)\r\nstr_data = \"hello\"\r\nprint(str_data)\r\n\r\n# Define image paths\r\n cover_path = \"images/cover_color.png\"\r\nrec_cover_path = \"images/rec_color_cover.png\"\r\nattack_rec_cover_path = \"images/attack_rec_color_cover.png\"\r\nstego_path_clean = \"images/color_stego_clean.png\"\r\nstego_path_attacked = \"images/color_stego_attacked.png\"\r\nresidual_path_clean = \"images/color_residual.png\"\r\n\r\n# === Case 1: Without attack ===\r\n# Encode string into image\r\ncover_image = np.float32(Image.open(cover_path).resize((256, 256)))\r\nsuccess, stego_image = crmark.encode(cover_image, str_data)\r\nprint(\"psnr:\", calculate_psnr(np.float32(stego_image), cover_image))\r\n\r\nif success:\r\n stego_image.save(stego_path_clean)\r\n stego_clean_image = np.float32(stego_image)\r\n residual = np.abs((stego_clean_image - cover_image) * 10.) + 127.5\r\n Image.fromarray(np.uint8(np.clip(residual, 0, 255))).save(residual_path_clean)\r\n\r\n # Recover cover and message from clean image\r\n stego_clean_image = np.float32(Image.open(stego_path_clean))\r\n is_attacked_clean, rec_cover_clean, rec_message_clean = crmark.recover(stego_clean_image)\r\n is_decoded, extracted_message_clean = crmark.decode(stego_clean_image)\r\n rec_cover_clean.save(rec_cover_path)\r\n\r\n # Compute pixel difference between original and recovered cover\r\n cover = np.float32(Image.open(cover_path).resize((256, 256)))\r\n rec_clean = np.float32(rec_cover_clean)\r\n diff_clean = np.sum(np.abs(cover - rec_clean))\r\n\r\n # === Case 2: With attack ===\r\n # Slightly modify the image to simulate attack\r\n stego = np.float32(Image.open(stego_path_clean))\r\n H, W, C = stego.shape\r\n rand_y = random.randint(0, H - 1)\r\n rand_x = random.randint(0, W - 1)\r\n rand_c = random.randint(0, C - 1)\r\n\r\n # Apply a small perturbation (\u00b11)\r\n perturbation = random.choice([-1, 1])\r\n stego[rand_y, rand_x, rand_c] = np.clip(stego[rand_y, rand_x, rand_c] + perturbation, 0, 255)\r\n\r\n stego = np.clip(stego + np.random.randint(0, 20, size=stego.shape), 0, 255)\r\n\r\n Image.fromarray(np.uint8(stego)).save(stego_path_attacked)\r\n\r\n # Recover from attacked image\r\n stego_attacked_image = np.float32(Image.open(stego_path_attacked))\r\n is_attacked, rec_cover_attacked, rec_message_attacked = crmark.recover(stego_attacked_image)\r\n is_attacked_flag, extracted_message_attacked = crmark.decode(stego_attacked_image)\r\n rec_cover_attacked.save(attack_rec_cover_path)\r\n\r\n rec_attacked = np.float32(rec_cover_attacked)\r\n diff_attacked = np.sum(np.abs(cover - rec_attacked))\r\n attack_rec_psnr = calculate_psnr(rec_attacked, cover)\r\n # === Print results ===\r\n print(\"=== Without Attack ===\")\r\n print(\"Original Message:\", str_data)\r\n print(\"Recovered Message:\", rec_message_clean)\r\n print(\"Extracted Message:\", extracted_message_clean)\r\n print(\"Was Attacked:\", is_attacked_clean)\r\n print(\"L1 Pixel Difference:\", diff_clean)\r\n\r\n print(\"\\n=== With Attack ===\")\r\n print(\"Recovered Message:\", rec_message_attacked)\r\n print(\"Extracted Message:\", extracted_message_attacked)\r\n print(\"Was Attacked:\", is_attacked)\r\n print(\"L1 Pixel Difference:\", diff_attacked)\r\n print(\"L1 Pixel Psnr:\", attack_rec_psnr)\r\n\r\nelse:\r\n print(\"Emebdding failed!\")\r\n\r\n```\r\n",
"bugtrack_url": null,
"license": null,
"summary": "A robust reversible watermarking method that can robustly extract the watermark in lossy channels and perfectly recover both the cover image and the watermark in lossless channels.",
"version": "1.1.0",
"project_urls": {
"Bug Reports": "https://github.com/chenoly/crmark/issues",
"Documentation": "https://crmark.readthedocs.io",
"Homepage": "https://github.com/chenoly/crmark",
"Source": "https://github.com/chenoly/crmark"
},
"split_keywords": [
"robust",
"reversible",
"watermarking"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "0718c25db6fa44855ce2d28c6b14f92d8906bfa4db5a9c1716cc19323db0b9cf",
"md5": "f069bf2288f0e1c728f6b3f53795c6e0",
"sha256": "9cc136fa8553b293986139069d02a6102a874da135a489c5321f8be1dc355f74"
},
"downloads": -1,
"filename": "crmark-1.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "f069bf2288f0e1c728f6b3f53795c6e0",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 43677,
"upload_time": "2025-07-23T03:09:31",
"upload_time_iso_8601": "2025-07-23T03:09:31.843131Z",
"url": "https://files.pythonhosted.org/packages/07/18/c25db6fa44855ce2d28c6b14f92d8906bfa4db5a9c1716cc19323db0b9cf/crmark-1.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "34f85802bff2c93339e59eb6a8c2ca0aaac84d67fd1c61f51b93434336fa262e",
"md5": "b015a910224ee5761a9ba711c14d9a44",
"sha256": "73365b8fe1595e09185d87bc6979a0b33b1bbeffc27010c205c5e88bf189d29a"
},
"downloads": -1,
"filename": "crmark-1.1.0.tar.gz",
"has_sig": false,
"md5_digest": "b015a910224ee5761a9ba711c14d9a44",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 42959,
"upload_time": "2025-07-23T03:09:33",
"upload_time_iso_8601": "2025-07-23T03:09:33.251350Z",
"url": "https://files.pythonhosted.org/packages/34/f8/5802bff2c93339e59eb6a8c2ca0aaac84d67fd1c61f51b93434336fa262e/crmark-1.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-23 03:09:33",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "chenoly",
"github_project": "crmark",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "crmark"
}