Initial commit
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user