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