import cv2 as cv
import numpy as np
from skimage.metrics import peak_signal_noise_ratio, structural_similarity
from typing import Callable, Optional, Tuple
def _align_shift(original: np.ndarray,
restored: np.ndarray,
max_shift: int = 8,
border: int = 4,
data_range: Optional[float] = None
) -> Tuple[np.ndarray, np.ndarray, Tuple[int, int]]:
"""
Находит целые (dy, dx) в диапозоне [-max_shift, max_shift],
которые максимизируют метрику между оригинальным изображением
и восстановленным на пересекающемся центральном участке (без граничных пикселей).
Возвращает 2 обрезанных массива и лучший сдвиг.
Работает для 2D и 3D массивов.
"""
a = np.asarray(original)
b = np.asarray(restored)
if a.shape != b.shape:
raise ValueError(
f"shape mismatch in _align_shift: {a.shape} vs {b.shape}")
if data_range is None:
data_range = 255.0 if a.dtype.kind in 'ui' else 1.0
H, W = a.shape[:2]
ms = int(max_shift)
bd = int(border)
inner_h = H - 2 * (ms + bd)
inner_w = W - 2 * (ms + bd)
if inner_h <= 0 or inner_w <= 0:
ms = 0
inner_h = H - 2 * bd
inner_w = W - 2 * bd
if inner_h <= 0 or inner_w <= 0:
return a, b, (0, 0)
y0 = ms + bd
x0 = ms + bd
a_win = a[y0:y0 + inner_h, x0:x0 + inner_w]
best_mse = np.inf
best_dy, best_dx = 0, 0
for dy in range(-ms, ms + 1):
for dx in range(-ms, ms + 1):
b_win = b[y0 + dy:y0 + dy + inner_h,
x0 + dx:x0 + dx + inner_w]
diff = a_win.astype(np.float64) - b_win.astype(np.float64)
mse = float(np.mean(diff * diff))
if mse < best_mse:
best_mse = mse
best_dy, best_dx = dy, dx
b_aligned = b[y0 + best_dy:y0 + best_dy + inner_h,
x0 + best_dx:x0 + best_dx + inner_w]
return a_win, b_aligned, (best_dy, best_dx)
[документация]
def PSNR(original: np.ndarray,
restored: np.ndarray,
aligned: bool = True,
max_shift: int = 8,
border: int = 4) -> float:
"""
Вычисляет отношение пикового сигнала к шуму (PSNR) между изображениями.
Авторы: Юров П.И.
Аргументы:
original (ndarray): Исходное изображение
restored (ndarray): Восстановленное/обработанное изображение
aligned (bool): Если True (по умолчанию), перед вычислением
PSNR выполняется поиск оптимального целочисленного сдвига
restored относительно original в окне
[-max_shift, +max_shift] по обеим осям; это компенсирует
translation-ambiguity слепой деконволюции.
Если False — стандартный попиксельный PSNR.
max_shift (int): Полуширина окна поиска сдвига (только при
aligned=True).
border (int): Дополнительный отступ от границы (px), который
выбрасывается из оценки, чтобы не считать на ringing-артефактах.
Возвращает:
Значение PSNR в децибелах (dB)
"""
if not aligned:
return peak_signal_noise_ratio(original, restored)
a_win, b_win, _ = _align_shift(original, restored,
max_shift=max_shift, border=border)
return peak_signal_noise_ratio(a_win, b_win)
[документация]
def SSIM(original: np.ndarray,
restored: np.ndarray,
data_range: Optional[float] = None,
aligned: bool = True,
max_shift: int = 8,
border: int = 4) -> float:
"""
Вычисляет индекс структурного сходства (SSIM) между изображениями.
Авторы: Юров П.И.
Аргументы:
original: Исходное изображение
restored: Восстановленное/обработанное изображение
data_range (Optional[float]): Верхний предел значений
aligned (bool): Если True (по умолчанию), перед вычислением
SSIM выполняется поиск оптимального целочисленного сдвига
restored относительно original в окне
[-max_shift, +max_shift] по обеим осям (сдвиг
подбирается по MSE, что эквивалентно максимизации PSNR).
Если False — стандартный SSIM.
max_shift (int): Полуширина окна поиска сдвига (только при
aligned=True).
border (int): Дополнительный отступ от границы (px).
Возвращает:
Значение SSIM в диапазоне от 0 до 1
"""
if not aligned:
return structural_similarity(original, restored, data_range=data_range)
a_win, b_win, _ = _align_shift(original, restored,
max_shift=max_shift, border=border,
data_range=data_range)
if a_win.ndim == 3:
return structural_similarity(a_win, b_win,
data_range=data_range,
channel_axis=-1)
return structural_similarity(a_win, b_win, data_range=data_range)
[документация]
def calculate_sml(image: np.ndarray) -> float:
"""
Вычисляет Sum of Modified Laplacian (SML) для изображения.
Авторы: Беззаборов А.А.
Мера общей резкости/количества краев.
Аргументы:
image (ndarray): Входное изображение (grayscale или color)
Возвращает:
Сумма модифицированного лапласиана
"""
if image.ndim == 3:
image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
kernel_x = np.array([[0, 0, 0], [-1, 2, -1], [0, 0, 0]], dtype=np.float32)
kernel_y = np.array([[0, -1, 0], [0, 2, 0], [0, -1, 0]], dtype=np.float32)
ml_x = cv.filter2D(image, -1, kernel_x)
ml_y = cv.filter2D(image, -1, kernel_y)
ml = np.abs(ml_x) + np.abs(ml_y)
return np.sum(ml)
[документация]
def Sharpness(image: np.ndarray):
"""
Подсчет резкости через дисперсию Лапласа.
Авторы: Юров П.И.
Более высокое значение указывает на большую резкость.
Аргументы:
image (ndarray): Входное изображение
"""
return cv.Laplacian(image, -1).var()
[документация]
def blur_complexity(original: np.ndarray,
blurred: np.ndarray) -> float:
"""
Вычисляет нормированную меру сложности смаза [0, 1] на основе SML.
Авторы: Беззаборов А.А.
Аргументы:
original: Исходное резкое изображение
blurred: Смазанное изображение
Возвращает:
Нормированная мера смаза:
0 - нет смаза (идеально резкое)
1 - максимальный смаз (полностью размытое)
"""
sml_orig = calculate_sml(original)
sml_blur = calculate_sml(blurred)
if sml_orig == 0:
return 1.0
blur_measure = 1.0 - (sml_blur / sml_orig)
return np.clip(blur_measure, 0.0, 1.0)
[документация]
def calculate_snr(signal: np.ndarray,
noise: np.ndarray) -> float:
"""
Вычисляет отношение сигнал-шум (SNR) в dB.
Авторы: Беззаборов А.А.
Аргументы:
signal: Сигнал (смазанное изображение без шума)
noise: Матрица добавленного шума
Возвращает:
SNR в децибелах (dB)
"""
signal_power = np.mean(signal**2)
noise_power = np.mean(noise**2)
if noise_power < 1e-10:
return float("inf")
return 10.0 * np.log10(signal_power / noise_power)
[документация]
def noise_complexity(signal: np.ndarray,
noise: np.ndarray,
min_snr: float = 0.0,
max_snr: float = 50.0) -> float:
"""
Вычисляет нормированную меру сложности шума [0, 1] на основе SNR.
Авторы: Беззаборов А.А.
Аргументы:
signal: Сигнал (смазанное изображение без шума)
noise: Матрица добавленного шума
min_snr: Минимальный SNR (соответствует сложности 1)
max_snr: Максимальный SNR (соответствует сложности 0)
Возвращает:
Нормированная мера шума:
0 - нет шума (SNR = ∞)
1 - максимальный шум (SNR → 0)
"""
snr_db = calculate_snr(signal, noise)
if np.isinf(snr_db):
return 0.0
noise_measure = 1.0 - ((snr_db - min_snr) / (max_snr - min_snr))
return np.clip(noise_measure, 0.0, 1.0)