Skip to content

HILL

stegobox.codec.HILL

Bases: BaseCodec

HILL is a steganography method for Image.

Source code derived from: https://github.com/daniellerch/stegolab/tree/master/HILL

Only encode function is available.

Source code in stegobox/codec/hill.py
class HILL(BaseCodec):
    """
    HILL is a steganography method for Image.

    Source code derived from: https://github.com/daniellerch/stegolab/tree/master/HILL

    Only encode function is available.
    """

    def __init__(self) -> None:
        super().__init__()
        np.set_printoptions(threshold=sys.maxsize)

    def encode(self, carrier: Image.Image, payload: float) -> Image.Image:
        """Merge secret_img into host image

        Args:
            carrier: host image
            payload: payload in bits per pixel

        Returns:
            Image: stego image.
        """

        if carrier.mode == "RGB":
            carrier = carrier.convert("L")
        cover = np.array(carrier)
        stego = self.hide(cover, payload)
        return Image.fromarray(stego)

    def decode(self, carrier: Image.Image) -> None:
        # TODO: decode
        raise NotImplementedError("Decode not implemented.")

    def hill_cost(self, cover: np.ndarray) -> np.ndarray:
        hf1 = np.array([[-1, 2, -1], [2, -4, 2], [-1, 2, -1]])
        h2 = np.ones((3, 3)).astype(np.float64) / 3**2
        hw = np.ones((15, 15)).astype(np.float64) / 15**2

        r1 = scipy.signal.convolve2d(cover, hf1, mode="same", boundary="symm")
        w1 = scipy.signal.convolve2d(np.abs(r1), h2, mode="same", boundary="symm")
        rho = 1.0 / (w1 + 10 ** (-10))
        cost = scipy.signal.convolve2d(rho, hw, mode="same", boundary="symm")
        return cost

    def ternary_entropyf(self, pp1: np.ndarray, pm1: np.ndarray) -> np.float64:
        eps = 2.2204e-16
        p0 = 1 - pp1 - pm1
        p = np.concatenate(
            [p0.flatten(order="F"), pp1.flatten(order="F"), pm1.flatten(order="F")]
        )
        p[p == 0] = 1e-16  # clear warning: divide by zero encountered in log2
        h = -(p * np.log2(p))
        h[np.logical_or(p < eps, p > (1 - eps))] = 0
        ht = sum(h)
        return np.float64(ht)

    def calc_lambda(
        self, rho_p1: np.ndarray, rho_m1: np.ndarray, message_length: float, n: int
    ) -> float:
        l3 = 1e3
        m3 = np.float64(message_length + 1)
        iterations = 0
        while m3 > message_length:
            l3 = l3 * 2
            pp1 = (np.exp(-l3 * rho_p1)) / (
                1 + np.exp(-l3 * rho_p1) + np.exp(-l3 * rho_m1)
            )
            pm1 = (np.exp(-l3 * rho_m1)) / (
                1 + np.exp(-l3 * rho_p1) + np.exp(-l3 * rho_m1)
            )
            m3 = self.ternary_entropyf(pp1, pm1)

            iterations += 1
            if iterations > 10:
                return l3
        l1 = 0.0
        m1 = np.float64(n)
        lamb = 0.0
        iterations = 0
        alpha = float(message_length) / n
        # limit search to 30 iterations and require that relative payload embedded
        # is roughly within 1/1000 of the required relative payload
        while float(m1 - m3) / n > alpha / 1000.0 and iterations < 30:
            lamb = l1 + (l3 - l1) / 2
            pp1 = (np.exp(-lamb * rho_p1)) / (
                1 + np.exp(-lamb * rho_p1) + np.exp(-lamb * rho_m1)
            )
            pm1 = (np.exp(-lamb * rho_m1)) / (
                1 + np.exp(-lamb * rho_p1) + np.exp(-lamb * rho_m1)
            )
            m2 = self.ternary_entropyf(pp1, pm1)
            if m2 < message_length:
                l3 = lamb
                m3 = m2
            else:
                l1 = lamb
                m1 = m2
            iterations = iterations + 1
        return lamb

    def embedding_simulator(
        self, x: np.ndarray, rho_p1: np.ndarray, rho_m1: np.ndarray, m: float
    ) -> np.ndarray:
        n = x.shape[0] * x.shape[1]
        lamb = self.calc_lambda(rho_p1, rho_m1, m, n)
        pchange_p1 = (np.exp(-lamb * rho_p1)) / (
            1 + np.exp(-lamb * rho_p1) + np.exp(-lamb * rho_m1)
        )
        pchange_m1 = (np.exp(-lamb * rho_m1)) / (
            1 + np.exp(-lamb * rho_p1) + np.exp(-lamb * rho_m1)
        )
        y = x.copy()
        rand_change = np.random.rand(y.shape[0], y.shape[1])
        y[rand_change < pchange_p1] = y[rand_change < pchange_p1] + 1
        y[(rand_change >= pchange_p1) & (rand_change < pchange_p1 + pchange_m1)] = (
            y[(rand_change >= pchange_p1) & (rand_change < pchange_p1 + pchange_m1)] - 1
        )
        return y

    def hide(self, cover: np.ndarray, payload: float) -> np.ndarray:
        rho = self.hill_cost(cover)
        wet_cost = 10**10
        rho[np.isnan(rho)] = wet_cost
        rho[rho > wet_cost] = wet_cost

        rho_m1 = rho.copy()
        rho_p1 = rho.copy()
        rho_p1[cover == 255] = wet_cost
        rho_m1[cover == 0] = wet_cost

        stego = self.embedding_simulator(
            cover, rho_p1, rho_m1, payload * cover.shape[0] * cover.shape[1]
        )
        # print(np.sum(np.abs(stego.astype('int16')-I.astype('int16'))))
        # imageio.imsave(stego_path, stego)
        return stego

encode(carrier, payload)

Merge secret_img into host image

Parameters:

Name Type Description Default
carrier Image

host image

required
payload float

payload in bits per pixel

required

Returns:

Name Type Description
Image Image

stego image.

Source code in stegobox/codec/hill.py
def encode(self, carrier: Image.Image, payload: float) -> Image.Image:
    """Merge secret_img into host image

    Args:
        carrier: host image
        payload: payload in bits per pixel

    Returns:
        Image: stego image.
    """

    if carrier.mode == "RGB":
        carrier = carrier.convert("L")
    cover = np.array(carrier)
    stego = self.hide(cover, payload)
    return Image.fromarray(stego)