prism-steganography

prism

prism models

prism-steganography hides a recoverable 64-bit message inside a cover image imperceptibly β€” not a fixed watermark, but a full trained encoder/decoder pair. Built as two jointly-trained networks: a U-Net encoder that embeds the message as bounded, near-invisible pixel perturbations, and a CNN decoder that recovers it. A differentiable noise layer sits between them at train time (blur, sensor noise, JPEG-like compression, pixel dropout), so the decoder learns to recover the message even after the image is distorted β€” not just from a pristine copy.

Production-usable for short recoverable messages (up to 8 ASCII characters) where the message needs to survive real-world image handling, not just a clean file transfer.

[TEST SCREENSHOT HERE] Screenshot 2026-07-11 at 00.19.29

πŸ—’οΈ Model Details

Architecture U-Net encoder (message tiled + concatenated with image) + CNN decoder
Message capacity 64 bits (8 bytes / 8 ASCII characters, UTF-8, null-padded)
Input RGB image, 128x128
Training data PD12M, pxhere, cc0-textures, ambientcg (Apache/CC0-licensed)
Training Mixed precision, differentiable noise layer (blur/noise/JPEG-approx/dropout), early stopping on validation bit-accuracy

πŸƒ Usage

Unlike a single-model checkpoint, this repo bundles two networks β€” an encoder and a decoder β€” trained together:

from huggingface_hub import hf_hub_download
import torch, importlib.util, json

model_file = hf_hub_download(repo_id="olaverse/prism-steganography", filename="model.py")
ckpt_file = hf_hub_download(repo_id="olaverse/prism-steganography", filename="pytorch_model.pt")
config_file = hf_hub_download(repo_id="olaverse/prism-steganography", filename="config.json")

spec = importlib.util.spec_from_file_location("model", model_file)
model_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(model_module)

config = json.load(open(config_file))
checkpoint = torch.load(ckpt_file, map_location="cpu")

encoder = model_module.StegEncoder(**config)
encoder.load_state_dict(checkpoint["encoder"])
encoder.eval()

decoder = model_module.StegDecoder(**config)
decoder.load_state_dict(checkpoint["decoder"])
decoder.eval()

Hides a 64-bit message (up to 8 ASCII characters, UTF-8 byte-encoded and null-padded) inside a cover image, recoverable even after blur, noise, JPEG re-compression, or pixel dropout.

# encoder(cover_image_tensor, message_bits_tensor) -> stego_image_tensor
# decoder(stego_image_tensor) -> recovered_bit_logits  (threshold at 0)

Full example: hide and recover a message

from PIL import Image
import torchvision.transforms.functional as TF

MSG_BITS = config["msg_bits"]  # 64

def text_to_bits(text: str, num_bits: int = MSG_BITS) -> torch.Tensor:
    num_bytes = num_bits // 8
    raw = text.encode("utf-8")[:num_bytes]
    raw = raw + b"\x00" * (num_bytes - len(raw))
    bits = []
    for byte in raw:
        bits.extend([(byte >> i) & 1 for i in range(7, -1, -1)])
    return torch.tensor(bits, dtype=torch.float32)

def bits_to_text(bits: torch.Tensor) -> str:
    bits = bits.round().long().tolist()
    byte_vals = []
    for i in range(0, len(bits), 8):
        val = 0
        for b in bits[i:i + 8]:
            val = (val << 1) | b
        byte_vals.append(val)
    raw = bytes(byte_vals).rstrip(b"\x00")
    return raw.decode("utf-8", errors="replace")

# load a cover image and resize to the model's trained resolution
img = Image.open("cover.jpg").convert("RGB").resize((128, 128))
cover = TF.to_tensor(img).unsqueeze(0)

# encode a message (truncated/padded to 8 bytes -- this model's fixed capacity)
message = "hi there"[: MSG_BITS // 8]
msg_bits = text_to_bits(message).unsqueeze(0)

with torch.no_grad():
    stego = encoder(cover, msg_bits)          # near-identical image with the message hidden
    recovered_logits = decoder(stego)         # decode straight from the stego image
    recovered_bits = (recovered_logits > 0).float()

recovered_text = bits_to_text(recovered_bits[0])

print("Original message: ", message)
print("Recovered message:", recovered_text)

TF.to_pil_image(stego[0]).save("stego.jpg")   # save the image with the hidden message

Output:

Original message: hi there

Recovered message: hi there

Message capacity is exactly msg_bits // 8 ASCII characters (8, for this model) β€” longer input is silently truncated by text_to_bits. To test robustness against real-world distortion (compression, re-uploading, etc.), pass stego through your own noise/degradation of choice before decoding β€” recovery accuracy will drop somewhat under distortion (see Benchmarks above), so consider adding error-correction coding on top of the raw bits for applications that need near-perfect reliability.

πŸ“Š Benchmarks

Multi-trial testing (20 random messages/distortion draws on a single cover image, not a scored benchmark against a standard dataset): clean recovery (no distortion) averaged 99.9% bit-accuracy with a 98.4% worst case β€” essentially flawless. After distortion (blur/noise/JPEG-approx/dropout, randomly sampled per trial), average bit-accuracy was 93.7%, with a worst-case single trial as low as 62.5% under a severe distortion draw β€” reliability depends meaningfully on distortion severity, not uniform across all conditions.

Known Limitations

  • Message capacity is small β€” 64 bits (8 characters) by design for this release; longer messages require retraining with a larger bit budget.
  • Worst-case robustness under severe distortion is meaningfully lower than the average β€” the 93.7% mean bit-accuracy under distortion can drop well below that (observed as low as 62.5% in testing) on particularly harsh distortion draws. No error-correction coding is applied on top of the raw bits in this release, so a real deployment wanting near-100% message reliability should add redundancy (e.g. a repetition or Hamming code) on top of the raw bit channel.
  • Trained and tested at 128x128 resolution; not evaluated at other input sizes.
  • Not evaluated against standard steganography benchmarks or attack suites.

Training data & licensing

Trained on PD12M (Spawning/PD12M, CDLA-Permissive-2.0), pxhere (nyuuzyou/pxhere, CC0), cc0-textures (nyuuzyou/cc0-textures, CC0), and ambientcg (nyuuzyou/ambientcg, CC0). Released under Apache-2.0.

Citation

@misc{prism-steganography,
  title  = {prism-steganography},
  author = {Olaverse},
  year   = {2026},
  url    = {https://huggingface.co/olaverse/prism-steganography}
}
Downloads last month
85
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Collection including olaverse/prism-steganography