Skip to content

STCPNG

stegobox.codec.STCPNG

Bases: BaseCodec

Source code in stegobox/codec/stc_png.py
class STCPNG(BaseCodec):
    def __init__(self, stcode: list[int] = [71, 109], k: int = 8) -> None:
        """STCPNG: Hide a payload string into PNG images.

        Syndrome-Trellis Code (STC) is a near-optimal convolutional method for adaptive
        steganography.

        * To encode: This method depends on the carefully designed distortion cost
          function, which controls the embedding position of the message in the cover
          signal. It is based on syndrome coding using linear convolutional codes with
          the optimal binary quantizer implemented using the Viterbi algorithm run in
          the dual domain.
        * To decode: The secret message is extracted from the stego image with the
          checking matrix by calculating STC.

        Originally implemented in
        [daniellerch/stegola](https://github.com/daniellerch/stegolab/blob/master/codes/STC.py)

        Args:
            stcode: A list of integers, used to generate the checking matrix.
            k: Number of times repeated to create the checking matrix.
        """

        n_bits = len(bin(numpy.max(stcode))[2:])

        m = []
        for d in stcode:
            m.append(numpy.array([int(x) for x in list(bin(d)[2:].ljust(n_bits, "0"))]))
        m = numpy.array(m).T
        h_hat = m
        n, m = h_hat.shape

        h = numpy.zeros((k + n - 1, m * k))
        for i in range(k):
            h[i : i + n, m * i : m * (i + 1)] = h_hat

        self.code_n = m * k
        self.code_l = n_bits
        self.code_h = numpy.tile(stcode, k)
        self.code_shift = numpy.tile([0] * (m - 1) + [1], k)
        super().__init__()

    def hill(self, im):
        h = numpy.array([[-1, 2, -1], [2, -4, 2], [-1, 2, -1]])
        l1 = numpy.ones((3, 3)).astype("float32") / (3**2)
        l2 = numpy.ones((15, 15)).astype("float32") / (15**2)
        img = numpy.array(im)
        img.flags.writeable = True
        costs = signal.convolve2d(img[:8, :8, 0], h, mode="same")
        costs = abs(costs)
        costs = signal.convolve2d(costs, l1, mode="same")
        costs = 1 / costs
        costs = signal.convolve2d(costs, l2, mode="same")
        costs[costs == numpy.inf] = 1
        return costs

    def dual_viterbi(self, x, w, m):
        c = numpy.zeros((2**self.code_l, self.code_n))
        costs = numpy.infty * numpy.ones((2**self.code_l, 1))
        costs[0] = 0
        paths = numpy.zeros((2**self.code_l, self.code_n))

        m_id = 0  # message bit id
        y = numpy.zeros(x.shape)

        # Run forward
        for i in range(self.code_n):
            costs_old = costs.copy()
            hi = self.code_h[i]
            ji = 0
            for j in range(2**self.code_l):
                c1 = costs_old[ji] + x[i] * w[i]
                c2 = costs_old[(ji ^ hi)] + (1 - x[i]) * w[i]
                if c1 < c2:
                    costs[j] = c1
                    paths[j, i] = ji  # store index of the previous path
                else:
                    costs[j] = c2
                    paths[j, i] = ji ^ hi  # store index of the previous path
                ji = ji + 1

            for j in range(self.code_shift[i]):
                tail = numpy.infty * numpy.ones((2 ** (self.code_l - 1), 1))
                if m[m_id] == 0:
                    costs = numpy.vstack((costs[::2], tail))
                else:
                    costs = numpy.vstack((costs[1::2], tail))

                m_id = m_id + 1

            c[:, i] = costs[:, 0]

        # Backward run
        ind = numpy.argmin(costs)
        min_cost = costs[ind, 0]

        m_id -= 1

        for i in range(self.code_n - 1, -1, -1):
            for j in range(self.code_shift[i]):
                # invert the shift in syndrome trellis
                ind = 2 * ind + m[m_id, 0]
                m_id = m_id - 1

            y[i] = paths[ind, i] != ind
            ind = int(paths[ind, i])

        return y.astype("uint8"), min_cost, paths

    def bytes_to_bits(self, m):
        bits = []
        for b in m:
            for i in range(8):
                bits.append((b >> i) & 1)
        return bits

    def embed(self, cover, costs, message):
        shape = cover.shape
        x = cover.flatten()
        w = costs.flatten()
        ml = numpy.sum(self.code_shift)
        message_bits = numpy.array(self.bytes_to_bits(message))

        i = 0
        j = 0
        y = x.copy()
        while True:
            x_chunk = x[i : i + self.code_n][:, numpy.newaxis] % 2
            m_chunk = message_bits[j : j + ml][:, numpy.newaxis]
            w_chunk = w[i : i + self.code_n][:, numpy.newaxis]
            y_chunk, min_cost, _ = self.dual_viterbi(x_chunk, w_chunk, m_chunk)
            idx = x_chunk[:, 0] != y_chunk[:, 0]
            y[i : i + self.code_n][idx] += 1
            i += self.code_n
            j += ml
            if i + self.code_n > len(x) or j + ml > len(message_bits):
                break
        return numpy.vstack(y).reshape(shape)

    def calc_syndrome(self, x):
        m = numpy.zeros((numpy.sum(self.code_shift), 1))
        m_id = 0
        tmp = 0
        for i in range(self.code_n):
            hi = self.code_h[i]
            if x[i] == 1:
                tmp = hi ^ tmp
            for j in range(self.code_shift[i]):
                m[m_id] = tmp % 2
                tmp //= 2
                m_id += 1
        return m.astype("uint8")

    def bits_to_bytes(self, m):
        enc = bytearray()
        bitidx = 0
        bitval = 0
        for b in m:
            if bitidx == 8:
                enc.append(bitval)
                bitidx = 0
                bitval = 0
            bitval |= b << bitidx
            bitidx += 1
        if bitidx == 8:
            enc.append(bitval)

        return bytes(enc)

    def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
        """Encode the string payload into the carrier image in PNG format
        using STC and HILL steganagraphy.

        Args:
            carrier: The cover image in PNG format.
            payload: The secret message to embed.

        Returns:
            Image.Image: The container image embeded with the payload.
        """
        cover = numpy.array(carrier)
        cover.flags.writeable = True
        cost = self.hill(carrier)
        stego = cover.copy()
        stego[:8, :8, 0] = self.embed(cover[:8, :8, 0], cost, payload.encode("utf-8"))

        return Image.fromarray(numpy.uint8(stego))

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

        Args:
            carrier: The container image in format PNG with the secret message.

        Returns:
            str: The reveal_payload extracted from the container image.
        """
        stego = numpy.array(carrier)
        stego.flags.writeable = True
        stego = stego[:8, :8, 0]
        y = stego.flatten()
        message = []
        for i in range(0, len(y), self.code_n):
            y_chunk = y[i : i + self.code_n][:, numpy.newaxis] % 2
            if len(y_chunk) < self.code_n:
                break
            m_chunk = self.calc_syndrome(y_chunk)
            message += m_chunk[:, 0].tolist()

        message_bytes = self.bits_to_bytes(message)
        return message_bytes.decode("utf-8")

__init__(stcode=[71, 109], k=8)

STCPNG: Hide a payload string into PNG images.

Syndrome-Trellis Code (STC) is a near-optimal convolutional method for adaptive steganography.

  • To encode: This method depends on the carefully designed distortion cost function, which controls the embedding position of the message in the cover signal. It is based on syndrome coding using linear convolutional codes with the optimal binary quantizer implemented using the Viterbi algorithm run in the dual domain.
  • To decode: The secret message is extracted from the stego image with the checking matrix by calculating STC.

Originally implemented in daniellerch/stegola

Parameters:

Name Type Description Default
stcode list[int]

A list of integers, used to generate the checking matrix.

[71, 109]
k int

Number of times repeated to create the checking matrix.

8
Source code in stegobox/codec/stc_png.py
def __init__(self, stcode: list[int] = [71, 109], k: int = 8) -> None:
    """STCPNG: Hide a payload string into PNG images.

    Syndrome-Trellis Code (STC) is a near-optimal convolutional method for adaptive
    steganography.

    * To encode: This method depends on the carefully designed distortion cost
      function, which controls the embedding position of the message in the cover
      signal. It is based on syndrome coding using linear convolutional codes with
      the optimal binary quantizer implemented using the Viterbi algorithm run in
      the dual domain.
    * To decode: The secret message is extracted from the stego image with the
      checking matrix by calculating STC.

    Originally implemented in
    [daniellerch/stegola](https://github.com/daniellerch/stegolab/blob/master/codes/STC.py)

    Args:
        stcode: A list of integers, used to generate the checking matrix.
        k: Number of times repeated to create the checking matrix.
    """

    n_bits = len(bin(numpy.max(stcode))[2:])

    m = []
    for d in stcode:
        m.append(numpy.array([int(x) for x in list(bin(d)[2:].ljust(n_bits, "0"))]))
    m = numpy.array(m).T
    h_hat = m
    n, m = h_hat.shape

    h = numpy.zeros((k + n - 1, m * k))
    for i in range(k):
        h[i : i + n, m * i : m * (i + 1)] = h_hat

    self.code_n = m * k
    self.code_l = n_bits
    self.code_h = numpy.tile(stcode, k)
    self.code_shift = numpy.tile([0] * (m - 1) + [1], k)
    super().__init__()

encode(carrier, payload)

Encode the string payload into the carrier image in PNG format using STC and HILL steganagraphy.

Parameters:

Name Type Description Default
carrier Image

The cover image in PNG format.

required
payload str

The secret message to embed.

required

Returns:

Type Description
Image

Image.Image: The container image embeded with the payload.

Source code in stegobox/codec/stc_png.py
def encode(self, carrier: Image.Image, payload: str) -> Image.Image:
    """Encode the string payload into the carrier image in PNG format
    using STC and HILL steganagraphy.

    Args:
        carrier: The cover image in PNG format.
        payload: The secret message to embed.

    Returns:
        Image.Image: The container image embeded with the payload.
    """
    cover = numpy.array(carrier)
    cover.flags.writeable = True
    cost = self.hill(carrier)
    stego = cover.copy()
    stego[:8, :8, 0] = self.embed(cover[:8, :8, 0], cost, payload.encode("utf-8"))

    return Image.fromarray(numpy.uint8(stego))

decode(carrier)

Decode the secret message from the carrier image.

Parameters:

Name Type Description Default
carrier Image

The container image in format PNG with the secret message.

required

Returns:

Name Type Description
str str

The reveal_payload extracted from the container image.

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

    Args:
        carrier: The container image in format PNG with the secret message.

    Returns:
        str: The reveal_payload extracted from the container image.
    """
    stego = numpy.array(carrier)
    stego.flags.writeable = True
    stego = stego[:8, :8, 0]
    y = stego.flatten()
    message = []
    for i in range(0, len(y), self.code_n):
        y_chunk = y[i : i + self.code_n][:, numpy.newaxis] % 2
        if len(y_chunk) < self.code_n:
            break
        m_chunk = self.calc_syndrome(y_chunk)
        message += m_chunk[:, 0].tolist()

    message_bytes = self.bits_to_bytes(message)
    return message_bytes.decode("utf-8")