Initial commit

This commit is contained in:
user
2026-07-12 14:06:45 +04:00
commit a75028d68f
148 changed files with 3275 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
__all__ = [
"Label64",
"Sequence8",
"ColorSpace",
"BlockGrid8",
"ChannelView",
"RgbImage512",
"PsnrMetric",
"YcbcrImage512",
"LsbRow8",
]
from .label64 import Label64
from .sequence8 import Sequence8
from .color_space import ColorSpace
from .block_grid8 import BlockGrid8
from .channel_view import ChannelView
from .rgb_image512 import RgbImage512
from .psnr_metric import PsnrMetric
from .ycbcr_image512 import YcbcrImage512
from .lsb_row8 import LsbRow8
@@ -0,0 +1,40 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class BlockGrid8:
"""Разбиение и сборка блоков 8×8."""
@staticmethod
def split(component: np.ndarray) -> np.ndarray:
"""Разбить 2D-компоненту на блоки (N,8,8)."""
if component.ndim != 2:
raise ValueError("Ожидается 2D-массив компоненты.")
h, w = component.shape
if h % 8 != 0 or w % 8 != 0:
raise ValueError("Размеры должны быть кратны 8.")
# Перестановка осей даёт (nH, nW, 8, 8) -> (N, 8, 8)
arr = np.asarray(component)
nH, nW = h // 8, w // 8
blocks = arr.reshape(nH, 8, nW, 8).transpose(
0, 2, 1, 3).reshape(nH * nW, 8, 8)
return blocks
@staticmethod
def merge(blocks: np.ndarray, height: int, width: int) -> np.ndarray:
"""Собрать 2D-компоненту из блоков (N,8,8)."""
if blocks.ndim != 3 or blocks.shape[1:] != (8, 8):
raise ValueError("Ожидается массив блоков формы (N,8,8).")
if height % 8 != 0 or width % 8 != 0:
raise ValueError("Размеры должны быть кратны 8.")
nH, nW = height // 8, width // 8
if blocks.shape[0] != nH * nW:
raise ValueError(
"Количество блоков не соответствует размеру выходной матрицы.")
# Обратная перестановка из (N,8,8) -> (nH, nW, 8, 8) -> (H, W)
arr = np.asarray(blocks)
comp = arr.reshape(nH, nW, 8, 8).transpose(
0, 2, 1, 3).reshape(height, width)
return comp
@@ -0,0 +1,39 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class ChannelView:
"""Доступ к компонентам Y, Cb, Cr в массиве YCbCr."""
_MAP = {"y": 0, "cb": 1, "cr": 2}
@staticmethod
def _idx(name: str) -> int:
"""Вернуть индекс канала."""
k = name.strip().lower()
if k not in ChannelView._MAP:
raise ValueError("Канал должен быть Y, Cb или Cr.")
return ChannelView._MAP[k]
@staticmethod
def get(ycbcr: np.ndarray, channel: str) -> np.ndarray:
"""Вернуть 2D-компоненту выбранного канала."""
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
raise ValueError("Ожидается массив HxWx3.")
i = ChannelView._idx(channel)
return ycbcr[..., i]
@staticmethod
def set(ycbcr: np.ndarray, channel: str, component: np.ndarray) -> np.ndarray:
"""Вернуть копию YCbCr с заменённым каналом."""
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
raise ValueError("Ожидается массив HxWx3.")
if component.shape != ycbcr.shape[:2]:
raise ValueError("Размер компоненты не совпадает с HxW.")
out = ycbcr.copy()
i = ChannelView._idx(channel)
# Короткая отсечка и приведение типа
out[..., i] = np.clip(component, 0, 255).astype(np.uint8)
return out
@@ -0,0 +1,44 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class ColorSpace:
"""Конвертация RGB↔YCbCr с отсечкой диапазона."""
# Матрица прямого преобразования (используются коэффициенты из задания)
_M = np.array([
[0.299, 0.587, 0.144],
[-0.168736, -0.331264, 0.5],
[0.5, -0.418688, -0.081312],
], dtype=np.float64)
_M_INV = np.linalg.inv(_M)
@staticmethod
def _clip_u8(arr: np.ndarray) -> np.ndarray:
"""Округлить и отсечь к uint8."""
return np.clip(np.rint(arr), 0, 255).astype(np.uint8)
@classmethod
def rgb_to_ycbcr(cls, rgb: np.ndarray) -> np.ndarray:
"""RGB uint8 -> YCbCr uint8."""
if rgb.ndim != 3 or rgb.shape[-1] != 3:
raise ValueError("Ожидается массив HxWx3.")
x = rgb.astype(np.float64).reshape(-1, 3)
ycbcr = x @ cls._M.T
ycbcr[:, 1:] += 128.0 # смещения Cb/Cr
ycbcr = ycbcr.reshape(rgb.shape)
return cls._clip_u8(ycbcr)
@classmethod
def ycbcr_to_rgb(cls, ycbcr: np.ndarray) -> np.ndarray:
"""YCbCr uint8 -> RGB uint8."""
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
raise ValueError("Ожидается массив HxWx3.")
x = ycbcr.astype(np.float64).reshape(-1, 3)
x[:, 1:] -= 128.0 # убрать смещения Cb/Cr
rgb = x @ cls._M_INV.T
rgb = rgb.reshape(ycbcr.shape)
return cls._clip_u8(rgb)
+46
View File
@@ -0,0 +1,46 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
import numpy as np
from PIL import Image
@dataclass(frozen=True)
class Label64:
"""Бинарная метка 64x64."""
data: np.ndarray # uint8, shape (64, 64), значения {0,1}
def __post_init__(self):
arr = np.asarray(self.data, dtype=np.uint8)
if arr.shape != (64, 64):
raise ValueError("Метка должна быть 64x64.")
# Нормализация к {0,1}
arr = (arr > 0).astype(np.uint8)
object.__setattr__(self, "data", arr)
@classmethod
def from_image(cls, path: str, threshold: int = 128) -> "Label64":
"""Создать из изображения 64x64."""
img = Image.open(path).convert("L")
if img.size != (64, 64):
raise ValueError("Изображение метки должно быть 64x64.")
arr = np.array(img, dtype=np.uint8)
arr = (arr >= threshold).astype(np.uint8)
return cls(arr)
@classmethod
def from_vector(cls, vec: Iterable[int]) -> "Label64":
"""Создать из вектора длиной 4096."""
v = np.fromiter(vec, dtype=np.uint8, count=4096)
if v.size != 4096:
raise ValueError("Длина вектора должна быть 4096.")
return cls(v.reshape(64, 64))
def to_vector(self) -> np.ndarray:
"""Вернуть вектор длиной 4096."""
return self.data.reshape(-1).astype(np.uint8)
def to_image(self, path: str) -> None:
"""Сохранить PNG 64x64."""
Image.fromarray(self.data * 255, mode="L").save(path)
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class LsbRow8:
"""Встраивание/извлечение 8 бит в первый ряд блока 8×8 через LSB."""
@staticmethod
def embed(block: np.ndarray, bits: np.ndarray) -> np.ndarray:
"""Вернуть копию блока с вшитыми 8 битами."""
if block.shape != (8, 8):
raise ValueError("Нужен блок 8x8.")
b = np.asarray(block, dtype=np.uint8).copy()
v = np.asarray(bits, dtype=np.uint8).reshape(-1)
if v.size != 8 or not np.isin(v, [0, 1]).all():
raise ValueError("Нужно 8 бит 0/1.")
# Меняем только младшие биты первых 8 пикселей верхней строки
row = b[0, :8]
row = (row & 0xFE) | v
b[0, :8] = row
return b
@staticmethod
def extract(block: np.ndarray) -> np.ndarray:
"""Извлечь 8 бит из LSB первого ряда блока."""
if block.shape != (8, 8):
raise ValueError("Нужен блок 8x8.")
b = np.asarray(block, dtype=np.uint8)
return (b[0, :8] & 1).astype(np.uint8)
@@ -0,0 +1,22 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class PsnrMetric:
"""Расчёт PSNR для двух массивов одинаковой формы."""
max_value: float = 255.0
def psnr(self, a: np.ndarray, b: np.ndarray) -> float:
"""Вычислить PSNR."""
if a.shape != b.shape:
raise ValueError("Массивы должны иметь одинаковую форму.")
x = a.astype(np.float64)
y = b.astype(np.float64)
# MSE по всем элементам (каналы включены)
mse = np.mean((x - y) ** 2)
if mse == 0.0:
return float("inf")
return 20.0 * np.log10(self.max_value) - 10.0 * np.log10(mse)
@@ -0,0 +1,38 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from PIL import Image
@dataclass(frozen=True)
class RgbImage512:
"""RGB-изображение 512x512."""
data: np.ndarray # uint8, shape (512, 512, 3)
def __post_init__(self):
arr = np.asarray(self.data, dtype=np.uint8)
if arr.shape != (512, 512, 3):
raise ValueError("Изображение должно быть 512x512x3 (RGB).")
object.__setattr__(self, "data", arr)
@classmethod
def from_file(cls, path: str) -> "RgbImage512":
"""Загрузить из файла."""
img = Image.open(path).convert("RGB")
if img.size != (512, 512):
raise ValueError("Размер изображения должен быть 512x512.")
return cls(np.array(img, dtype=np.uint8))
@classmethod
def from_array(cls, arr: np.ndarray) -> "RgbImage512":
"""Создать из массива 512x512x3."""
return cls(arr)
def to_array(self) -> np.ndarray:
"""Вернуть массив uint8 512x512x3."""
return self.data
def save(self, path: str) -> None:
"""Сохранить в файл."""
Image.fromarray(self.data, mode="RGB").save(path)
+55
View File
@@ -0,0 +1,55 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
import numpy as np
@dataclass(frozen=True)
class Sequence8:
"""Пара 8-битных последовательностей: seq0 и инверсная seq1."""
_seq0: np.ndarray # uint8, shape (8,)
def __post_init__(self):
arr = np.asarray(self._seq0, dtype=np.uint8).reshape(-1)
if arr.size != 8:
raise ValueError("seq0 должна содержать 8 бит.")
if not np.isin(arr, [0, 1]).all():
raise ValueError("seq0 может состоять только из 0 и 1.")
object.__setattr__(self, "_seq0", arr)
@property
def seq0(self) -> np.ndarray:
"""Последовательность для бита 0."""
return self._seq0
@property
def seq1(self) -> np.ndarray:
"""Инверсия seq0 для бита 1."""
return 1 - self._seq0
def for_bit(self, bit: int) -> np.ndarray:
"""Вернуть последовательность по значению бита."""
if bit not in (0, 1):
raise ValueError("bit должен быть 0 или 1.")
return self._seq0 if bit == 0 else (1 - self._seq0)
@classmethod
def from_string(cls, bits: str) -> "Sequence8":
"""Создать из строки вида '01010101'."""
if len(bits) != 8 or any(ch not in "01" for ch in bits):
raise ValueError("Строка должна быть из 8 символов 0/1.")
arr = np.fromiter((int(ch) for ch in bits), dtype=np.uint8, count=8)
return cls(arr)
@classmethod
def from_iterable(cls, bits: Iterable[int]) -> "Sequence8":
"""Создать из итерируемого набора из 8 бит."""
arr = np.fromiter(bits, dtype=np.uint8, count=8)
if arr.size != 8:
raise ValueError("Нужно 8 бит.")
return cls(arr)
def __len__(self) -> int:
"""Длина последовательности."""
return 8
@@ -0,0 +1,50 @@
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
# Локальные импорты внутри методов уменьшают риск циклов зависимостей
@dataclass(frozen=True)
class YcbcrImage512:
"""YCbCr-изображение 512x512."""
data: np.ndarray # uint8, shape (512, 512, 3)
def __post_init__(self):
arr = np.asarray(self.data, dtype=np.uint8)
if arr.shape != (512, 512, 3):
raise ValueError("Изображение должно быть 512x512x3 (YCbCr).")
object.__setattr__(self, "data", arr)
@classmethod
def from_rgb(cls, rgb_img) -> "YcbcrImage512":
"""Создать из RgbImage512."""
from .color_space import ColorSpace
from .rgb_image512 import RgbImage512
if not isinstance(rgb_img, RgbImage512):
raise TypeError("Ожидается RgbImage512.")
ycbcr = ColorSpace.rgb_to_ycbcr(rgb_img.to_array())
return cls(ycbcr)
def to_rgb(self):
"""Преобразовать в RgbImage512."""
from .color_space import ColorSpace
from .rgb_image512 import RgbImage512
rgb = ColorSpace.ycbcr_to_rgb(self.data)
return RgbImage512.from_array(rgb)
def get_channel(self, name: str) -> np.ndarray:
"""Вернуть 2D-компоненту канала."""
from .channel_view import ChannelView
return ChannelView.get(self.data, name)
def with_channel(self, name: str, component: np.ndarray) -> "YcbcrImage512":
"""Вернуть копию с заменённым каналом."""
from .channel_view import ChannelView
new_ycbcr = ChannelView.set(self.data, name, component)
return YcbcrImage512(new_ycbcr)
def to_array(self) -> np.ndarray:
"""Вернуть массив uint8 512x512x3."""
return self.data