Skip to content

FNNS

Fixed Neural Network Steganography

The encoding process of FNNS first encodes a message into an image with SteganoGAN, then optimizes the encoded image itself with L-BFGS, which is inspired by adversarial attacks. The decoding procedure of FNNS is identical to the SteganoGAN decoding process.

This module depends on the stegobox.codec.SteganoGAN module.

stegobox.codec.FNNS

Bases: SteganoGAN

Source code in stegobox/codec/fnns.py
class FNNS(SteganoGAN):
    def __init__(
        self,
        mode: str = "fnns-de",
        steps: int = 2000,
        max_iter: int = 10,
        alpha: float = 0.1,
        eps: float = 0.3,
        num_bits: int = 1,
        stegano_gan_arch: str = "basic",
        stegano_gan_data_depth: int = 1,
        stegano_gan_hidden_size: int = 32,
        stegano_gan_weights_path: str = "ckpt/stegano_gan/basic_mscoco.pt",
        cuda: bool = True,
        verbose: bool = True,
    ) -> None:
        """FNNS - Fixed Neural Network Steganography

        Args:
            mode: FNNS mode, one of `fnns-d` or `fnns-de`. Defaults to "fnns-de".
            steps: Number of FNNS optimization steps. Defaults to 2000.
            max_iter: Max iterations of FNNS optimization. Defaults to 10.
            alpha: FNNS alpha. Defaults to 0.1.
            eps: FNNS epsilon. Defaults to 0.3.
            num_bits: Number of bits in payload. Defaults to 1.
            stegano_gan_arch: SteganoGAN architecture, must be one of `basic` or
                `dense`. Defaults to "basic".
            stegano_gan_data_depth: Data depth. Defaults to 1.
            stegano_gan_hidden_size: Hidden layer size. Defaults to 32.
            stegano_gan_weights_path: Pretrained SteganoGAN weights
                `/path/to/weights.pt`. Defaults to "ckpt/stegano_gan/basic_mscoco.pt".
            cuda: Whether use CUDA or not. Defaults to True.
            verbose: Whether enable verbose logging or not. Defaults to True.
        """
        super().__init__(
            stegano_gan_arch,
            stegano_gan_data_depth,
            stegano_gan_hidden_size,
            stegano_gan_weights_path,
            cuda,
            verbose,
        )
        device = torch.device("cuda" if cuda and torch.cuda.is_available() else "cpu")
        self.device = device
        self.verbose = verbose

        self.mode = mode
        self.steps = steps
        self.max_iter = max_iter
        self.alpha = alpha
        self.eps = eps
        self.num_bits = num_bits

    def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
        """Encode an image with payload string with SteganoGAN, then use FNNS to
        optimize the generated steganographic image.

        Args:
            carrier: Carrier PIL image.
            payload: Payload string.

        Raises:
            ValueError: If `mode` is not one of `fnns-d` or `fnns-de`.

        Returns:
            Encoded and FNNS optimized PIL image.
        """
        con = Console()

        with con.status("[bold green]Encoding...[/bold green]"):
            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)
            generated = (generated.clamp(-1.0, 1.0) + 1) / 2

        if self.verbose:
            print("[bold green]Encoding completed.[/bold green] FNNS optimizing...")

        def get_image_to_optimize():
            if self.mode == "fnns-de":
                return generated
            if self.mode == "fnns-d" or self.mode == "random":
                return (
                    torch.FloatTensor(np.array(carrier) / 255)
                    .permute(2, 1, 0)
                    .unsqueeze(0)
                    .to(self.device)
                )

            raise ValueError(f"Unknown mode: {self.mode}")

        image = get_image_to_optimize()

        model = self.decoder
        criterion = nn.BCEWithLogitsLoss(reduction="sum")
        target = payload_t.to(image.device)

        adv_image = image.clone().detach()

        # eps is dynamically adjusted during optimization
        eps = self.eps

        with track_metric(fields=["err", "eps"], disable=not self.verbose) as tracker:
            total = self.steps // self.max_iter
            t = tracker.add_task(description="FNNS", total=total, err=0.0, eps=eps)

            for _ in range(total):
                adv_image = adv_image.contiguous().requires_grad_(True)
                optimizer = LBFGS([adv_image], lr=self.alpha, max_iter=self.max_iter)

                def optimizer_closure():
                    outputs = model(adv_image)
                    loss = criterion(outputs, target)
                    optimizer.zero_grad()
                    loss.backward()
                    return loss

                optimizer.step(optimizer_closure)
                delta = torch.clamp(adv_image - image, min=-eps, max=eps)
                adv_image = torch.clamp(image + delta, min=0, max=1).detach()

                err = (
                    len(
                        torch.nonzero(
                            (model(adv_image) > 0).float().view(-1) != target.view(-1)
                        )
                    )
                    / target.numel()
                )

                if err < 1e-5:
                    eps = 0.7
                if err == 0:
                    eps = self.eps

                tracker.update(t, advance=1, err=err, eps=eps)

        generated_adv = adv_image.squeeze(0).permute(2, 1, 0).cpu().numpy() * 255
        return Image.fromarray(generated_adv.astype(np.uint8))

    def decode(self, carrier: Image.Image) -> str:
        """FNNS decoding process is identical to the one implemented in SteganoGAN.

        Args:
            carrier: Encoded PIL image.

        Returns:
            Decoded payload string.
        """
        return super().decode(carrier)

__init__(mode='fnns-de', steps=2000, max_iter=10, alpha=0.1, eps=0.3, num_bits=1, stegano_gan_arch='basic', stegano_gan_data_depth=1, stegano_gan_hidden_size=32, stegano_gan_weights_path='ckpt/stegano_gan/basic_mscoco.pt', cuda=True, verbose=True)

FNNS - Fixed Neural Network Steganography

Parameters:

Name Type Description Default
mode str

FNNS mode, one of fnns-d or fnns-de. Defaults to "fnns-de".

'fnns-de'
steps int

Number of FNNS optimization steps. Defaults to 2000.

2000
max_iter int

Max iterations of FNNS optimization. Defaults to 10.

10
alpha float

FNNS alpha. Defaults to 0.1.

0.1
eps float

FNNS epsilon. Defaults to 0.3.

0.3
num_bits int

Number of bits in payload. Defaults to 1.

1
stegano_gan_arch str

SteganoGAN architecture, must be one of basic or dense. Defaults to "basic".

'basic'
stegano_gan_data_depth int

Data depth. Defaults to 1.

1
stegano_gan_hidden_size int

Hidden layer size. Defaults to 32.

32
stegano_gan_weights_path str

Pretrained SteganoGAN weights /path/to/weights.pt. Defaults to "ckpt/stegano_gan/basic_mscoco.pt".

'ckpt/stegano_gan/basic_mscoco.pt'
cuda bool

Whether use CUDA or not. Defaults to True.

True
verbose bool

Whether enable verbose logging or not. Defaults to True.

True
Source code in stegobox/codec/fnns.py
def __init__(
    self,
    mode: str = "fnns-de",
    steps: int = 2000,
    max_iter: int = 10,
    alpha: float = 0.1,
    eps: float = 0.3,
    num_bits: int = 1,
    stegano_gan_arch: str = "basic",
    stegano_gan_data_depth: int = 1,
    stegano_gan_hidden_size: int = 32,
    stegano_gan_weights_path: str = "ckpt/stegano_gan/basic_mscoco.pt",
    cuda: bool = True,
    verbose: bool = True,
) -> None:
    """FNNS - Fixed Neural Network Steganography

    Args:
        mode: FNNS mode, one of `fnns-d` or `fnns-de`. Defaults to "fnns-de".
        steps: Number of FNNS optimization steps. Defaults to 2000.
        max_iter: Max iterations of FNNS optimization. Defaults to 10.
        alpha: FNNS alpha. Defaults to 0.1.
        eps: FNNS epsilon. Defaults to 0.3.
        num_bits: Number of bits in payload. Defaults to 1.
        stegano_gan_arch: SteganoGAN architecture, must be one of `basic` or
            `dense`. Defaults to "basic".
        stegano_gan_data_depth: Data depth. Defaults to 1.
        stegano_gan_hidden_size: Hidden layer size. Defaults to 32.
        stegano_gan_weights_path: Pretrained SteganoGAN weights
            `/path/to/weights.pt`. Defaults to "ckpt/stegano_gan/basic_mscoco.pt".
        cuda: Whether use CUDA or not. Defaults to True.
        verbose: Whether enable verbose logging or not. Defaults to True.
    """
    super().__init__(
        stegano_gan_arch,
        stegano_gan_data_depth,
        stegano_gan_hidden_size,
        stegano_gan_weights_path,
        cuda,
        verbose,
    )
    device = torch.device("cuda" if cuda and torch.cuda.is_available() else "cpu")
    self.device = device
    self.verbose = verbose

    self.mode = mode
    self.steps = steps
    self.max_iter = max_iter
    self.alpha = alpha
    self.eps = eps
    self.num_bits = num_bits

encode(carrier, payload)

Encode an image with payload string with SteganoGAN, then use FNNS to optimize the generated steganographic image.

Parameters:

Name Type Description Default
carrier Image

Carrier PIL image.

required
payload str

Payload string.

required

Raises:

Type Description
ValueError

If mode is not one of fnns-d or fnns-de.

Returns:

Type Description
Image

Encoded and FNNS optimized PIL image.

Source code in stegobox/codec/fnns.py
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
    """Encode an image with payload string with SteganoGAN, then use FNNS to
    optimize the generated steganographic image.

    Args:
        carrier: Carrier PIL image.
        payload: Payload string.

    Raises:
        ValueError: If `mode` is not one of `fnns-d` or `fnns-de`.

    Returns:
        Encoded and FNNS optimized PIL image.
    """
    con = Console()

    with con.status("[bold green]Encoding...[/bold green]"):
        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)
        generated = (generated.clamp(-1.0, 1.0) + 1) / 2

    if self.verbose:
        print("[bold green]Encoding completed.[/bold green] FNNS optimizing...")

    def get_image_to_optimize():
        if self.mode == "fnns-de":
            return generated
        if self.mode == "fnns-d" or self.mode == "random":
            return (
                torch.FloatTensor(np.array(carrier) / 255)
                .permute(2, 1, 0)
                .unsqueeze(0)
                .to(self.device)
            )

        raise ValueError(f"Unknown mode: {self.mode}")

    image = get_image_to_optimize()

    model = self.decoder
    criterion = nn.BCEWithLogitsLoss(reduction="sum")
    target = payload_t.to(image.device)

    adv_image = image.clone().detach()

    # eps is dynamically adjusted during optimization
    eps = self.eps

    with track_metric(fields=["err", "eps"], disable=not self.verbose) as tracker:
        total = self.steps // self.max_iter
        t = tracker.add_task(description="FNNS", total=total, err=0.0, eps=eps)

        for _ in range(total):
            adv_image = adv_image.contiguous().requires_grad_(True)
            optimizer = LBFGS([adv_image], lr=self.alpha, max_iter=self.max_iter)

            def optimizer_closure():
                outputs = model(adv_image)
                loss = criterion(outputs, target)
                optimizer.zero_grad()
                loss.backward()
                return loss

            optimizer.step(optimizer_closure)
            delta = torch.clamp(adv_image - image, min=-eps, max=eps)
            adv_image = torch.clamp(image + delta, min=0, max=1).detach()

            err = (
                len(
                    torch.nonzero(
                        (model(adv_image) > 0).float().view(-1) != target.view(-1)
                    )
                )
                / target.numel()
            )

            if err < 1e-5:
                eps = 0.7
            if err == 0:
                eps = self.eps

            tracker.update(t, advance=1, err=err, eps=eps)

    generated_adv = adv_image.squeeze(0).permute(2, 1, 0).cpu().numpy() * 255
    return Image.fromarray(generated_adv.astype(np.uint8))

decode(carrier)

FNNS decoding process is identical to the one implemented in SteganoGAN.

Parameters:

Name Type Description Default
carrier Image

Encoded PIL image.

required

Returns:

Type Description
str

Decoded payload string.

Source code in stegobox/codec/fnns.py
def decode(self, carrier: Image.Image) -> str:
    """FNNS decoding process is identical to the one implemented in SteganoGAN.

    Args:
        carrier: Encoded PIL image.

    Returns:
        Decoded payload string.
    """
    return super().decode(carrier)