Mosaic_Generator / src /metrics.py
Teoman21's picture
-done mosaic generator
4376584
from __future__ import annotations
import numpy as np
from PIL import Image
from typing import Dict, Tuple
from .utils import pil_to_np
from skimage.metrics import structural_similarity as ssim
def calculate_mse(original: Image.Image, reconstructed: Image.Image) -> float:
"""
Calculate Mean Squared Error between original and reconstructed images.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
MSE value
"""
orig_array = pil_to_np(original)
recon_array = pil_to_np(reconstructed)
# Ensure same size
if orig_array.shape != recon_array.shape:
# Resize reconstructed to match original
recon_pil = reconstructed.resize(original.size, Image.LANCZOS)
recon_array = pil_to_np(recon_pil)
# Calculate MSE
mse = np.mean((orig_array - recon_array) ** 2)
return float(mse)
def calculate_psnr(original: Image.Image, reconstructed: Image.Image) -> float:
"""
Calculate Peak Signal-to-Noise Ratio.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
PSNR value in dB
"""
mse = calculate_mse(original, reconstructed)
if mse == 0:
return float('inf')
psnr = 20 * np.log10(1.0 / np.sqrt(mse))
return float(psnr)
def calculate_ssim(original: Image.Image, reconstructed: Image.Image) -> float:
"""
Calculate Structural Similarity Index.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
SSIM value between 0 and 1
"""
orig_array = pil_to_np(original)
recon_array = pil_to_np(reconstructed)
# Ensure same size
if orig_array.shape != recon_array.shape:
# Resize reconstructed to match original
recon_pil = reconstructed.resize(original.size, Image.LANCZOS)
recon_array = pil_to_np(recon_pil)
# Convert to grayscale for SSIM calculation
if len(orig_array.shape) == 3:
orig_gray = np.mean(orig_array, axis=2)
recon_gray = np.mean(recon_array, axis=2)
else:
orig_gray = orig_array
recon_gray = recon_array
# Calculate SSIM
ssim_value = ssim(orig_gray, recon_gray, data_range=1.0)
return float(ssim_value)
def calculate_color_similarity(original: Image.Image, reconstructed: Image.Image) -> Dict[str, float]:
"""
Calculate color-based similarity metrics.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
Dictionary with color similarity metrics
"""
orig_array = pil_to_np(original)
recon_array = pil_to_np(reconstructed)
# Ensure same size
if orig_array.shape != recon_array.shape:
recon_pil = reconstructed.resize(original.size, Image.LANCZOS)
recon_array = pil_to_np(recon_pil)
# Calculate per-channel differences
channel_diffs = []
for channel in range(3):
orig_channel = orig_array[:, :, channel]
recon_channel = recon_array[:, :, channel]
channel_mse = np.mean((orig_channel - recon_channel) ** 2)
channel_diffs.append(channel_mse)
# Calculate overall color difference
color_mse = np.mean(channel_diffs)
# Calculate color histogram similarity
orig_hist = np.histogram(orig_array.flatten(), bins=256, range=(0, 1))[0]
recon_hist = np.histogram(recon_array.flatten(), bins=256, range=(0, 1))[0]
# Normalize histograms
orig_hist = orig_hist / np.sum(orig_hist)
recon_hist = recon_hist / np.sum(recon_hist)
# Calculate histogram correlation
hist_correlation = np.corrcoef(orig_hist, recon_hist)[0, 1]
return {
'color_mse': float(color_mse),
'red_channel_mse': float(channel_diffs[0]),
'green_channel_mse': float(channel_diffs[1]),
'blue_channel_mse': float(channel_diffs[2]),
'histogram_correlation': float(hist_correlation) if not np.isnan(hist_correlation) else 0.0
}
def calculate_comprehensive_metrics(original: Image.Image, reconstructed: Image.Image) -> Dict[str, float]:
"""
Calculate comprehensive similarity metrics.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
Dictionary with all similarity metrics
"""
metrics = {}
# Basic metrics
metrics['mse'] = calculate_mse(original, reconstructed)
metrics['psnr'] = calculate_psnr(original, reconstructed)
metrics['ssim'] = calculate_ssim(original, reconstructed)
# Color metrics
color_metrics = calculate_color_similarity(original, reconstructed)
metrics.update(color_metrics)
# Additional derived metrics
metrics['rmse'] = np.sqrt(metrics['mse'])
metrics['mae'] = calculate_mae(original, reconstructed)
return metrics
def calculate_mae(original: Image.Image, reconstructed: Image.Image) -> float:
"""
Calculate Mean Absolute Error.
Args:
original: Original PIL Image
reconstructed: Reconstructed PIL Image
Returns:
MAE value
"""
orig_array = pil_to_np(original)
recon_array = pil_to_np(reconstructed)
# Ensure same size
if orig_array.shape != recon_array.shape:
recon_pil = reconstructed.resize(original.size, Image.LANCZOS)
recon_array = pil_to_np(recon_pil)
# Calculate MAE
mae = np.mean(np.abs(orig_array - recon_array))
return float(mae)
def interpret_metrics(metrics: Dict[str, float]) -> Dict[str, str]:
"""
Provide human-readable interpretations of metrics.
Args:
metrics: Dictionary of metric values
Returns:
Dictionary with interpretations
"""
interpretations = {}
# MSE interpretation
mse = metrics.get('mse', 0)
if mse < 0.01:
interpretations['mse'] = "Excellent similarity"
elif mse < 0.05:
interpretations['mse'] = "Good similarity"
elif mse < 0.1:
interpretations['mse'] = "Moderate similarity"
else:
interpretations['mse'] = "Poor similarity"
# PSNR interpretation
psnr = metrics.get('psnr', 0)
if psnr > 40:
interpretations['psnr'] = "Excellent quality"
elif psnr > 30:
interpretations['psnr'] = "Good quality"
elif psnr > 20:
interpretations['psnr'] = "Acceptable quality"
else:
interpretations['psnr'] = "Poor quality"
# SSIM interpretation
ssim_val = metrics.get('ssim', 0)
if ssim_val > 0.9:
interpretations['ssim'] = "Very similar structure"
elif ssim_val > 0.7:
interpretations['ssim'] = "Similar structure"
elif ssim_val > 0.5:
interpretations['ssim'] = "Moderately similar structure"
else:
interpretations['ssim'] = "Different structure"
return interpretations