diff --git a/projects/steganography/README.md b/projects/steganography/README.md new file mode 100644 index 000000000..687dade6f --- /dev/null +++ b/projects/steganography/README.md @@ -0,0 +1,29 @@ +# Image steganography + +This project contains two algorithm (LSB and DCT), which can insert some secret but invisible. + +**LSB** insert message into Least Significant Bit of each pixels. + +**DCT** insert message into Middle Frequency. + +## Requirement + +Installation: + +```shell +$ pip install -r requirements.txt +``` + +## Usage + +Run LSB algorithm + +```shell +$ python3 lsb.py +``` + +Run DCT algorithm + +```shell +$ python3 dct.py +``` \ No newline at end of file diff --git a/projects/steganography/dct.py b/projects/steganography/dct.py new file mode 100644 index 000000000..8523ebc61 --- /dev/null +++ b/projects/steganography/dct.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# Copyright(C) 2021 wuyaoping +# +# DCT algorithm has great a robust but lower capacity. + +import numpy as np +import os.path as osp +import cv2 + +FLAG = '%' +# Select a part location from the middle frequency +LOC_MAX = (4, 1) +LOC_MIN = (3, 2) +# The difference between MAX and MIN, +# bigger to improve robust but make picture low quality. +ALPHA = 1 + +# Quantizer table +TABLE = np.array([ + [16, 11, 10, 16, 24, 40, 51, 61], + [12, 12, 14, 19, 26, 58, 60, 55], + [14, 13, 16, 24, 40, 57, 69, 56], + [14, 17, 22, 29, 51, 87, 80, 62], + [18, 22, 37, 56, 68, 109, 103, 77], + [24, 35, 55, 64, 81, 104, 113, 92], + [49, 64, 78, 87, 103, 121, 120, 101], + [72, 92, 95, 98, 112, 100, 103, 99] +]) + + +def insert(path, txt): + img = cv2.imread(path, cv2.IMREAD_ANYCOLOR) + txt = "{}{}{}".format(len(txt), FLAG, txt) + row, col = img.shape[:2] + max_bytes = (row // 8) * (col // 8) // 8 + assert max_bytes >= len( + txt), "Message overflow the capacity:{}".format(max_bytes) + img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) + # Just use the Y plane to store message, you can use all plane + y, u, v = cv2.split(img) + y = y.astype(np.float32) + blocks = [] + # Quantize blocks + for r_idx in range(0, 8 * (row // 8), 8): + for c_idx in range(0, 8 * (col // 8), 8): + quantized = cv2.dct(y[r_idx: r_idx+8, c_idx: c_idx+8]) / TABLE + blocks.append(quantized) + for idx in range(len(txt)): + encode(blocks[idx*8: (idx+1)*8], txt[idx]) + + idx = 0 + # Restore Y plane + for r_idx in range(0, 8 * (row // 8), 8): + for c_idx in range(0, 8 * (col // 8), 8): + y[r_idx: r_idx+8, c_idx: c_idx+8] = cv2.idct(blocks[idx] * TABLE) + idx += 1 + y = y.astype(np.uint8) + img = cv2.cvtColor(cv2.merge((y, u, v)), cv2.COLOR_YUV2BGR) + filename, _ = osp.splitext(path) + # DCT algorithm can save message even if jpg + filename += '_dct_embeded' + '.jpg' + cv2.imwrite(filename, img) + return filename + + +# Encode a char into the blocks +def encode(blocks, data): + data = ord(data) + for idx in range(len(blocks)): + bit_val = (data >> idx) & 1 + max_val = max(blocks[idx][LOC_MAX], blocks[idx][LOC_MIN]) + min_val = min(blocks[idx][LOC_MAX], blocks[idx][LOC_MIN]) + if max_val - min_val <= ALPHA: + max_val = min_val + ALPHA + 1e-3 + if bit_val == 1: + blocks[idx][LOC_MAX] = max_val + blocks[idx][LOC_MIN] = min_val + else: + blocks[idx][LOC_MAX] = min_val + blocks[idx][LOC_MIN] = max_val + + +# Decode a char from the blocks +def decode(blocks): + val = 0 + for idx in range(len(blocks)): + if blocks[idx][LOC_MAX] > blocks[idx][LOC_MIN]: + val |= 1 << idx + return chr(val) + + +def extract(path): + img = cv2.imread(path, cv2.IMREAD_ANYCOLOR) + row, col = img.shape[:2] + max_bytes = (row // 8) * (col // 8) // 8 + img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) + y, u, v = cv2.split(img) + y = y.astype(np.float32) + blocks = [] + for r_idx in range(0, 8 * (row // 8), 8): + for c_idx in range(0, 8 * (col // 8), 8): + quantized = cv2.dct(y[r_idx: r_idx+8, c_idx: c_idx+8]) / TABLE + blocks.append(quantized) + res = '' + idx = 0 + # Extract the length of the message + while idx < max_bytes: + ch = decode(blocks[idx*8: (idx+1)*8]) + idx += 1 + if ch == FLAG: + break + res += ch + end = int(res) + idx + assert end <= max_bytes, "Input image isn't correct." + res = '' + while idx < end: + res += decode(blocks[idx*8: (idx+1)*8]) + idx += 1 + return res + + +if __name__ == '__main__': + data = 'A collection of simple python mini projects to enhance your Python skills.' + res_path = insert('./example.png', data) + res = extract(res_path) + print(res) diff --git a/projects/steganography/example.png b/projects/steganography/example.png new file mode 100644 index 000000000..074292362 Binary files /dev/null and b/projects/steganography/example.png differ diff --git a/projects/steganography/lsb.py b/projects/steganography/lsb.py new file mode 100644 index 000000000..f079fb194 --- /dev/null +++ b/projects/steganography/lsb.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright(C) 2021 wuyaoping +# +# LSB algorithm has a great capacity but fragile. + +import cv2 +import math +import os.path as osp +import numpy as np + +# Insert data in the low bit. +# Lower make picture less loss but lower capacity. +BITS = 2 + +HIGH_BITS = 256 - (1 << BITS) +LOW_BITS = (1 << BITS) - 1 +BYTES_PER_BYTE = math.ceil(8 / BITS) +FLAG = '%' + + +def insert(path, txt): + img = cv2.imread(path, cv2.IMREAD_ANYCOLOR) + # Save origin shape to restore image + ori_shape = img.shape + max_bytes = ori_shape[0] * ori_shape[1] // BYTES_PER_BYTE + # Encode message with length + txt = '{}{}{}'.format(len(txt), FLAG, txt) + assert max_bytes >= len( + txt), "Message overflow the capacity:{}".format(max_bytes) + data = np.reshape(img, -1) + for (idx, val) in enumerate(txt): + encode(data[idx*BYTES_PER_BYTE: (idx+1) * BYTES_PER_BYTE], val) + + img = np.reshape(data, ori_shape) + filename, _ = osp.splitext(path) + # png is lossless encode that can restore message correctly + filename += '_lsb_embeded' + ".png" + cv2.imwrite(filename, img) + return filename + + +def extract(path): + img = cv2.imread(path, cv2.IMREAD_ANYCOLOR) + data = np.reshape(img, -1) + total = data.shape[0] + res = '' + idx = 0 + # Decode message length + while idx < total // BYTES_PER_BYTE: + ch = decode(data[idx*BYTES_PER_BYTE: (idx+1)*BYTES_PER_BYTE]) + idx += 1 + if ch == FLAG: + break + res += ch + end = int(res) + idx + assert end <= total // BYTES_PER_BYTE, "Input image isn't correct." + + res = '' + while idx < end: + res += decode(data[idx*BYTES_PER_BYTE: (idx+1)*BYTES_PER_BYTE]) + idx += 1 + return res + + +def encode(block, data): + data = ord(data) + for idx in range(len(block)): + block[idx] &= HIGH_BITS + block[idx] |= (data >> (BITS * idx)) & LOW_BITS + + +def decode(block): + val = 0 + for idx in range(len(block)): + val |= (block[idx] & LOW_BITS) << (idx * BITS) + return chr(val) + + +if __name__ == '__main__': + data = 'A collection of simple python mini projects to enhance your Python skills.' + input_path = "./example.png" + res_path = insert(input_path, data) + res = extract(res_path) + print(res) diff --git a/projects/steganography/requirements.txt b/projects/steganography/requirements.txt new file mode 100644 index 000000000..f9f14654c --- /dev/null +++ b/projects/steganography/requirements.txt @@ -0,0 +1,2 @@ +numpy==1.21.2 +opencv-python==4.5.3.56