191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
"""
|
||
Модуль DCT стеганографии с квантованием (JPEG-style)
|
||
"""
|
||
|
||
import numpy as np
|
||
from PIL import Image
|
||
from scipy.fftpack import dct, idct
|
||
from .utils import text_to_bits, bits_to_text
|
||
|
||
BLOCK_SIZE = 8
|
||
|
||
# Стандартная таблица квантования JPEG для качества 50%
|
||
QUANT_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 _get_zigzag_order() -> list:
|
||
"""Zigzag порядок для матрицы 8x8."""
|
||
zigzag = []
|
||
for s in range(15):
|
||
if s % 2 == 0:
|
||
for i in range(min(s, 7), max(0, s - 7) - 1, -1):
|
||
j = s - i
|
||
if 0 <= i < 8 and 0 <= j < 8:
|
||
zigzag.append(i * 8 + j)
|
||
else:
|
||
for i in range(max(0, s - 7), min(s, 7) + 1):
|
||
j = s - i
|
||
if 0 <= i < 8 and 0 <= j < 8:
|
||
zigzag.append(i * 8 + j)
|
||
return zigzag
|
||
|
||
|
||
ZIGZAG_ORDER = _get_zigzag_order()
|
||
MID_FREQ_INDICES = ZIGZAG_ORDER[1:40]
|
||
|
||
|
||
def _dct_quantize(block: np.ndarray) -> np.ndarray:
|
||
"""DCT + квантование."""
|
||
dct_block = dct(dct(block, axis=0, norm='ortho'), axis=1, norm='ortho')
|
||
quantized = np.round(dct_block / QUANT_TABLE)
|
||
return quantized.astype(np.int32)
|
||
|
||
|
||
def _idct_dequantize(quantized: np.ndarray) -> np.ndarray:
|
||
"""Обратное квантование + IDCT."""
|
||
dct_block = quantized * QUANT_TABLE
|
||
block = idct(idct(dct_block, axis=0, norm='ortho'), axis=1, norm='ortho')
|
||
block = np.round(block)
|
||
return np.clip(block, 0, 255).astype(np.uint8)
|
||
|
||
|
||
def _extract_bits_from_block(block_quant: np.ndarray, max_bits: int) -> list:
|
||
"""Извлекает биты из квантованных коэффициентов."""
|
||
extracted = []
|
||
coeff_flat = block_quant.flatten()
|
||
|
||
for idx in MID_FREQ_INDICES:
|
||
if len(extracted) >= max_bits:
|
||
break
|
||
if idx >= len(coeff_flat):
|
||
break
|
||
bit = int(coeff_flat[idx]) & 1
|
||
extracted.append(bit)
|
||
return extracted
|
||
|
||
|
||
def _embed_bits_in_block(block_quant: np.ndarray, bits: list, bit_index: int) -> tuple:
|
||
"""Внедряет биты в квантованные коэффициенты."""
|
||
modified = block_quant.copy()
|
||
current_idx = bit_index
|
||
coeff_flat = modified.flatten()
|
||
|
||
for idx in MID_FREQ_INDICES:
|
||
if current_idx >= len(bits):
|
||
break
|
||
if idx >= len(coeff_flat):
|
||
break
|
||
new_val = (int(coeff_flat[idx]) & 0xFE) | bits[current_idx]
|
||
coeff_flat[idx] = new_val
|
||
current_idx += 1
|
||
|
||
return coeff_flat.reshape(BLOCK_SIZE, BLOCK_SIZE), current_idx
|
||
|
||
|
||
def encode_dct(image_path: str, message: str, output_path: str) -> bool:
|
||
"""Скрывает сообщение в изображении методом DCT."""
|
||
img = Image.open(image_path).convert('L')
|
||
pixels = np.array(img, dtype=np.float64)
|
||
height, width = pixels.shape
|
||
|
||
# Кодируем сообщение в байты
|
||
message_bytes = message.encode('utf-8')
|
||
msg_length = len(message_bytes)
|
||
|
||
# Формат: [длина: 32 бита] + [сообщение]
|
||
# 32 бита = 4 байта, достаточно для сообщений до 4 ГБ
|
||
length_bits = format(msg_length, '032b')
|
||
message_bits = text_to_bits(message)
|
||
|
||
# Собираем всё вместе
|
||
all_bits = length_bits + message_bits
|
||
bit_list = [int(b) for b in all_bits]
|
||
total_bits = len(bit_list)
|
||
|
||
# Проверяем вместимость
|
||
blocks_per_row = width // BLOCK_SIZE
|
||
blocks_per_col = height // BLOCK_SIZE
|
||
max_bits = blocks_per_row * blocks_per_col * len(MID_FREQ_INDICES)
|
||
|
||
if total_bits > max_bits:
|
||
print(f"Ошибка: сообщение слишком большое.")
|
||
print(f"Доступно: {max_bits}, требуется: {total_bits}")
|
||
return False
|
||
|
||
modified_pixels = pixels.copy()
|
||
bit_index = 0
|
||
|
||
for i in range(0, height - BLOCK_SIZE + 1, BLOCK_SIZE):
|
||
for j in range(0, width - BLOCK_SIZE + 1, BLOCK_SIZE):
|
||
if bit_index >= total_bits:
|
||
break
|
||
|
||
block = pixels[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
|
||
quant_block = _dct_quantize(block)
|
||
quant_block, bit_index = _embed_bits_in_block(quant_block, bit_list, bit_index)
|
||
new_block = _idct_dequantize(quant_block)
|
||
modified_pixels[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE] = new_block
|
||
|
||
if bit_index >= total_bits:
|
||
break
|
||
|
||
result_img = Image.fromarray(modified_pixels.astype(np.uint8), mode='L')
|
||
result_img.save(output_path)
|
||
print(f"Успешно! Спрятано {total_bits} бит ({total_bits // 8} байт)")
|
||
return True
|
||
|
||
|
||
def decode_dct(image_path: str) -> str:
|
||
"""Извлекает сообщение из изображения методом DCT."""
|
||
img = Image.open(image_path).convert('L')
|
||
pixels = np.array(img, dtype=np.float64)
|
||
height, width = pixels.shape
|
||
|
||
# Извлекаем все биты
|
||
all_bits = []
|
||
|
||
for i in range(0, height - BLOCK_SIZE + 1, BLOCK_SIZE):
|
||
for j in range(0, width - BLOCK_SIZE + 1, BLOCK_SIZE):
|
||
block = pixels[i:i+BLOCK_SIZE, j:j+BLOCK_SIZE]
|
||
quant_block = _dct_quantize(block)
|
||
bits_from_block = _extract_bits_from_block(quant_block, len(MID_FREQ_INDICES))
|
||
all_bits.extend(bits_from_block)
|
||
|
||
# Читаем длину сообщения (первые 32 бита)
|
||
if len(all_bits) < 32:
|
||
return ""
|
||
|
||
length_bits = all_bits[:32]
|
||
length_str = ''.join(str(b) for b in length_bits)
|
||
msg_length = int(length_str, 2)
|
||
|
||
# Проверяем, что длина разумная
|
||
if msg_length > len(all_bits) // 8:
|
||
return ""
|
||
|
||
# Читаем сообщение
|
||
message_bits = all_bits[32:32 + msg_length * 8]
|
||
|
||
# Дополняем до кратности 8 (хотя уже должно быть кратно)
|
||
remainder = len(message_bits) % 8
|
||
if remainder != 0:
|
||
message_bits.extend([0] * (8 - remainder))
|
||
|
||
if len(message_bits) == 0:
|
||
return ""
|
||
|
||
bits_string = ''.join(str(b) for b in message_bits)
|
||
try:
|
||
return bits_to_text(bits_string)
|
||
except Exception as e:
|
||
print(f"Ошибка при декодировании: {e}")
|
||
return "" |