| | 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) |
| | |
| | |
| | if orig_array.shape != recon_array.shape: |
| | |
| | recon_pil = reconstructed.resize(original.size, Image.LANCZOS) |
| | recon_array = pil_to_np(recon_pil) |
| | |
| | |
| | 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) |
| | |
| | |
| | if orig_array.shape != recon_array.shape: |
| | |
| | recon_pil = reconstructed.resize(original.size, Image.LANCZOS) |
| | recon_array = pil_to_np(recon_pil) |
| | |
| | |
| | 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 |
| | |
| | |
| | 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) |
| | |
| | |
| | if orig_array.shape != recon_array.shape: |
| | recon_pil = reconstructed.resize(original.size, Image.LANCZOS) |
| | recon_array = pil_to_np(recon_pil) |
| | |
| | |
| | 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) |
| | |
| | |
| | color_mse = np.mean(channel_diffs) |
| | |
| | |
| | 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] |
| | |
| | |
| | orig_hist = orig_hist / np.sum(orig_hist) |
| | recon_hist = recon_hist / np.sum(recon_hist) |
| | |
| | |
| | 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 = {} |
| | |
| | |
| | metrics['mse'] = calculate_mse(original, reconstructed) |
| | metrics['psnr'] = calculate_psnr(original, reconstructed) |
| | metrics['ssim'] = calculate_ssim(original, reconstructed) |
| | |
| | |
| | color_metrics = calculate_color_similarity(original, reconstructed) |
| | metrics.update(color_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) |
| | |
| | |
| | if orig_array.shape != recon_array.shape: |
| | recon_pil = reconstructed.resize(original.size, Image.LANCZOS) |
| | recon_array = pil_to_np(recon_pil) |
| | |
| | |
| | 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 = 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 = 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_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 |
| |
|