Skip to content

SteganoGAN

SteganoGAN is a tool for creating steganographic images using adversarial training. In our implementation, we fixed the original code's memory leak during training and also refactored the model saving strategy so that pretrained models doesn't have to rely on the model's name. We provide pretrained weights on datasets DIV2K and MS-COCO, available to download at GitHub release - Pretrained weights.

stegobox.codec.SteganoGAN

Bases: BaseCodec

Source code in stegobox/codec/stegano_gan/steganogan.py
class SteganoGAN(BaseCodec):
    def __init__(
        self,
        arch: str = "basic",
        data_depth: int = 1,
        hidden_size: int = 32,
        weights_path: Optional[str] = None,
        cuda: bool = False,
        verbose: bool = False,
    ) -> None:
        """SteganoGAN: high capacity image steganography with GANs.

        Extensive work has been done to refactor the original implementation, so the
        weights provided in the original repo are not compatible with our version.

        Args:
            arch: Architecture, either `basic` or `dense`. Defaults to "basic".
            data_depth: Depth of payload data. Defaults to 1.
            hidden_size: Size of hidden layer. Defaults to 32.
            weights_path: Pretrained weights `/path/to/weights.pt`. Defaults to None.
            cuda: Whether use cuda or not. Defaults to False.
            verbose: Whether enable verbose logging or not. Defaults to False.

        Raises:
            ValueError: If architecture `arch` is not one of `basic` or `dense`.
        """
        device = torch.device("cuda" if cuda and torch.cuda.is_available() else "cpu")

        self.data_depth = data_depth
        self.hidden_size = hidden_size
        self.verbose = verbose

        if arch == "basic":
            self.encoder = BasicEncoder(data_depth, hidden_size).to(device)
            self.decoder = BasicDecoder(data_depth, hidden_size).to(device)
        elif arch == "dense":
            self.encoder = DenseEncoder(data_depth, hidden_size).to(device)
            self.decoder = DenseDecoder(data_depth, hidden_size).to(device)
        else:
            raise ValueError("Unknown architecture: {}".format(arch))
        self.critic = BasicCritic(hidden_size).to(device)
        self.device = device

        if weights_path:
            checkpoint = torch.load(weights_path)
            self.encoder.load_state_dict(checkpoint["encoder_state_dict"])
            self.decoder.load_state_dict(checkpoint["decoder_state_dict"])
        else:
            print("[red]Warning[/red]: no weights provided, we are in training mode.")

    def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
        """Encode payload string into carrier image.

        Args:
            carrier: PIL image, choose carefully according to the pretrained model used
            payload: Payload secret message

        Returns:
            Encoded steganographic image.
        """
        con = Console()

        with con.status("[bold green]Encoding..."):
            cover = np.array(carrier) / 127.5 - 1.0
            cover = torch.FloatTensor(cover).permute(2, 1, 0).unsqueeze(0)

            csize = cover.size()
            payload_t = self._make_payload(csize[3], csize[2], self.data_depth, payload)

            cover = cover.to(self.device)
            payload_t = payload_t.to(self.device)
            generated = self.encoder.forward(cover, payload_t)[0]
            generated = generated.clamp(-1.0, 1.0)
            generated = generated.permute(2, 1, 0).detach().cpu().numpy()
            generated = (generated + 1.0) * 127.5

            return Image.fromarray(generated.astype("uint8"))

    def decode(self, carrier: Image.Image) -> str:
        """Decode secret message from encoded steganographic image.

        Args:
            carrier: Encoded carrier image.

        Raises:
            ValueError: If failed to decode message from the input image.

        Returns:
            The decoded message if decode is successful.
        """
        con = Console()

        with con.status("[bold green]Decoding..."):
            image = np.array(carrier) / 255.0
            image = torch.as_tensor(image).permute(2, 1, 0).unsqueeze(0).float()
            image = image.to(self.device)

            image = self.decoder(image).view(-1) > 0

            # Split and decode messages
            candidates = Counter[str]()
            bits = image.data.int().cpu().numpy().tolist()
            for candidate in bits_to_bytearray(bits).split(b"\x00\x00\x00\x00"):
                candidate_text = bytearray_to_text(bytearray(candidate))
                if candidate_text:
                    candidates[candidate_text] += 1

            # Choose most common message
            if len(candidates) == 0:
                raise ValueError("Failed to find message.")

            candidate, _ = candidates.most_common(1)[0]
            return candidate

    def _make_payload(
        self, width: int, height: int, depth: int, text: str
    ) -> torch.Tensor:
        """This takes a piece of text and encodes it into a bit vector. It then fills a
        matrix of size (width, height) with copies of the bit vector.

        Args:
            width: Width of the matrix
            height: Height of the matrix
            depth: Data depth as used in the model (1 would suffice for text)
            text: The actual secret message payload

        Returns:
            A tensor of size (width, height, depth) containing the payload
        """
        message = text_to_bits(text) + [0] * 32

        payload = message
        while len(payload) < width * height * depth:
            payload += message

        payload = payload[: width * height * depth]

        return torch.as_tensor(payload).view(1, depth, height, width).float()

__init__(arch='basic', data_depth=1, hidden_size=32, weights_path=None, cuda=False, verbose=False)

SteganoGAN: high capacity image steganography with GANs.

Extensive work has been done to refactor the original implementation, so the weights provided in the original repo are not compatible with our version.

Parameters:

Name Type Description Default
arch str

Architecture, either basic or dense. Defaults to "basic".

'basic'
data_depth int

Depth of payload data. Defaults to 1.

1
hidden_size int

Size of hidden layer. Defaults to 32.

32
weights_path Optional[str]

Pretrained weights /path/to/weights.pt. Defaults to None.

None
cuda bool

Whether use cuda or not. Defaults to False.

False
verbose bool

Whether enable verbose logging or not. Defaults to False.

False

Raises:

Type Description
ValueError

If architecture arch is not one of basic or dense.

Source code in stegobox/codec/stegano_gan/steganogan.py
def __init__(
    self,
    arch: str = "basic",
    data_depth: int = 1,
    hidden_size: int = 32,
    weights_path: Optional[str] = None,
    cuda: bool = False,
    verbose: bool = False,
) -> None:
    """SteganoGAN: high capacity image steganography with GANs.

    Extensive work has been done to refactor the original implementation, so the
    weights provided in the original repo are not compatible with our version.

    Args:
        arch: Architecture, either `basic` or `dense`. Defaults to "basic".
        data_depth: Depth of payload data. Defaults to 1.
        hidden_size: Size of hidden layer. Defaults to 32.
        weights_path: Pretrained weights `/path/to/weights.pt`. Defaults to None.
        cuda: Whether use cuda or not. Defaults to False.
        verbose: Whether enable verbose logging or not. Defaults to False.

    Raises:
        ValueError: If architecture `arch` is not one of `basic` or `dense`.
    """
    device = torch.device("cuda" if cuda and torch.cuda.is_available() else "cpu")

    self.data_depth = data_depth
    self.hidden_size = hidden_size
    self.verbose = verbose

    if arch == "basic":
        self.encoder = BasicEncoder(data_depth, hidden_size).to(device)
        self.decoder = BasicDecoder(data_depth, hidden_size).to(device)
    elif arch == "dense":
        self.encoder = DenseEncoder(data_depth, hidden_size).to(device)
        self.decoder = DenseDecoder(data_depth, hidden_size).to(device)
    else:
        raise ValueError("Unknown architecture: {}".format(arch))
    self.critic = BasicCritic(hidden_size).to(device)
    self.device = device

    if weights_path:
        checkpoint = torch.load(weights_path)
        self.encoder.load_state_dict(checkpoint["encoder_state_dict"])
        self.decoder.load_state_dict(checkpoint["decoder_state_dict"])
    else:
        print("[red]Warning[/red]: no weights provided, we are in training mode.")

encode(carrier, payload)

Encode payload string into carrier image.

Parameters:

Name Type Description Default
carrier Image

PIL image, choose carefully according to the pretrained model used

required
payload str

Payload secret message

required

Returns:

Type Description
Image

Encoded steganographic image.

Source code in stegobox/codec/stegano_gan/steganogan.py
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
    """Encode payload string into carrier image.

    Args:
        carrier: PIL image, choose carefully according to the pretrained model used
        payload: Payload secret message

    Returns:
        Encoded steganographic image.
    """
    con = Console()

    with con.status("[bold green]Encoding..."):
        cover = np.array(carrier) / 127.5 - 1.0
        cover = torch.FloatTensor(cover).permute(2, 1, 0).unsqueeze(0)

        csize = cover.size()
        payload_t = self._make_payload(csize[3], csize[2], self.data_depth, payload)

        cover = cover.to(self.device)
        payload_t = payload_t.to(self.device)
        generated = self.encoder.forward(cover, payload_t)[0]
        generated = generated.clamp(-1.0, 1.0)
        generated = generated.permute(2, 1, 0).detach().cpu().numpy()
        generated = (generated + 1.0) * 127.5

        return Image.fromarray(generated.astype("uint8"))

decode(carrier)

Decode secret message from encoded steganographic image.

Parameters:

Name Type Description Default
carrier Image

Encoded carrier image.

required

Raises:

Type Description
ValueError

If failed to decode message from the input image.

Returns:

Type Description
str

The decoded message if decode is successful.

Source code in stegobox/codec/stegano_gan/steganogan.py
def decode(self, carrier: Image.Image) -> str:
    """Decode secret message from encoded steganographic image.

    Args:
        carrier: Encoded carrier image.

    Raises:
        ValueError: If failed to decode message from the input image.

    Returns:
        The decoded message if decode is successful.
    """
    con = Console()

    with con.status("[bold green]Decoding..."):
        image = np.array(carrier) / 255.0
        image = torch.as_tensor(image).permute(2, 1, 0).unsqueeze(0).float()
        image = image.to(self.device)

        image = self.decoder(image).view(-1) > 0

        # Split and decode messages
        candidates = Counter[str]()
        bits = image.data.int().cpu().numpy().tolist()
        for candidate in bits_to_bytearray(bits).split(b"\x00\x00\x00\x00"):
            candidate_text = bytearray_to_text(bytearray(candidate))
            if candidate_text:
                candidates[candidate_text] += 1

        # Choose most common message
        if len(candidates) == 0:
            raise ValueError("Failed to find message.")

        candidate, _ = candidates.most_common(1)[0]
        return candidate

Training

Tip

You can train your own model following the process in stegobox.codec.stegano_gan.fit.

stegobox.codec.stegano_gan.fit

Call this module if you are training a SteganoGAN model from scratch. From the root directory of the project, call:

python stegobox/codec/stegano_gan/fit.py --arch=basic --epoch=12 --batch-size=32 \
    --dataset-root=data/mscoco/ --save-path=basic_mscoco.pt --cuda

Two datasets are available for training the model:

  1. DIV2K:
    • train: http://data.vision.ee.ethz.ch/cvl/DIV2K/DIV2K_train_HR.zip
    • validate: http://data.vision.ee.ethz.ch/cvl/DIV2K/DIV2K_valid_HR.zip
  2. MS-COCO:
    • train: http://images.cocodataset.org/zips/train2017.zip
    • validate: http://images.cocodataset.org/zips/test2017.zip