Spaces:
Running on Zero
Running on Zero
File size: 7,002 Bytes
8d928e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """
Phase 5 -- Scientific Validation (PSNR / SSIM / RMSE)
satellite_sr_deploy/validation.py
We use the standard Super-Resolution evaluation protocol since true
higher-resolution ground truth (e.g. 2.5m WorldView) is unavailable
for this specific Sentinel-2 scene.
Protocol:
1. Load original 10m Sentinel-2 image (Ground Truth).
2. Crop to dimensions divisible by 4.
3. Downsample by 4x using Bicubic interpolation (simulating 40m input).
4. Upsample the 40m image back to 10m using Bicubic, ESRGAN, and HATSAT.
5. Compute PSNR, SSIM, and RMSE between the upsampled images and the GT.
"""
import sys
import math
import time
from pathlib import Path
import torch
import numpy as np
from PIL import Image
# Ensure satellite_sr_deploy is in path
ROOT = Path(__file__).resolve().parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from model_router import SatelliteSRRouter
def calculate_psnr(img1, img2):
"""Calculate Peak Signal-to-Noise Ratio (PSNR) in RGB space."""
# img1 and img2 can be numpy arrays
img1 = img1.astype(np.float64)
img2 = img2.astype(np.float64)
mse = np.mean((img1 - img2) ** 2)
if mse == 0:
return float('inf')
return 20 * math.log10(255.0 / math.sqrt(mse))
def calculate_rmse(img1, img2):
"""Calculate Root Mean Square Error (RMSE)."""
img1 = img1.astype(np.float64)
img2 = img2.astype(np.float64)
return np.sqrt(np.mean((img1 - img2) ** 2))
def ssim_core(img1, img2, C1=6.5025, C2=58.5225):
"""
Simplified SSIM over the entire image.
Standard SSIM uses a gaussian window, but for a quick numpy implementation
we can approximate or compute global/block-based SSIM.
For robust SSIM without adding scikit-image dependency, we'll compute
global mean/variance which gives a rough structural similarity.
(Note: True SSIM uses local sliding windows, so we'll implement a simple
11x11 sliding window in numpy if possible, or just use the global version
for demonstration since this is a local metric).
"""
# For strict SSIM, we should ideally use torchvision or skimage.
# Let's see if skimage is available.
pass
try:
from skimage.metrics import structural_similarity as ssim
HAS_SKIMAGE = True
except ImportError:
HAS_SKIMAGE = False
print("skimage not found. SSIM will be calculated using a global approximation.")
def calculate_ssim(img1, img2):
if HAS_SKIMAGE:
# skimage expects channel_axis=-1 for RGB
return ssim(img1, img2, channel_axis=-1, data_range=255)
else:
# Global approximation (Not strictly standard SSIM, but gives a relative metric)
img1 = img1.astype(np.float64)
img2 = img2.astype(np.float64)
mu1 = img1.mean()
mu2 = img2.mean()
sigma1 = img1.var()
sigma2 = img2.var()
cov = np.cov(img1.flatten(), img2.flatten())[0, 1]
c1 = (0.01 * 255)**2
c2 = (0.03 * 255)**2
num = (2 * mu1 * mu2 + c1) * (2 * cov + c2)
den = (mu1**2 + mu2**2 + c1) * (sigma1 + sigma2 + c2)
return num / den
def run_validation():
print("=" * 60)
print("PHASE 5: SCIENTIFIC VALIDATION")
print("=" * 60)
gt_path = ROOT.parent / "data" / "processed" / "sentinel2_rgb_10m.png"
if not gt_path.exists():
print(f"Error: Could not find GT image at {gt_path}")
sys.exit(1)
gt_img = Image.open(gt_path).convert("RGB")
orig_w, orig_h = gt_img.size
print(f"Original Ground Truth (GT) size: {orig_w}x{orig_h}")
# 1. Crop GT to a multiple of 4
w = (orig_w // 4) * 4
h = (orig_h // 4) * 4
gt_img = gt_img.crop((0, 0, w, h))
print(f"Cropped GT size (divisible by 4): {w}x{h}")
# 2. Downsample by 4x to simulate 40m input
lr_w, lr_h = w // 4, h // 4
lr_img = gt_img.resize((lr_w, lr_h), Image.BICUBIC)
print(f"Low-Resolution (LR) 40m simulated size: {lr_w}x{lr_h}")
# 3. Upsample using Bicubic
print("\n--- Running Baseline: Bicubic Interpolation ---")
start_time = time.time()
bicubic_img = lr_img.resize((w, h), Image.BICUBIC)
bicubic_time = time.time() - start_time
# Initialize Models
print("\n--- Initializing Deep Learning Models ---")
router = SatelliteSRRouter()
# 4. Upsample using ESRGAN
print("\n--- Running ESRGAN ---")
start_time = time.time()
esrgan_img = router.predict(lr_img, "esrgan")
esrgan_time = time.time() - start_time
# 5. Upsample using HATSAT
print("\n--- Running HATSAT ---")
start_time = time.time()
hatsat_img = router.predict(lr_img, "hatsat")
hatsat_time = time.time() - start_time
# 6. Evaluation
print("\n" + "=" * 60)
print("EVALUATION RESULTS (vs Ground Truth)")
print("=" * 60)
gt_np = np.array(gt_img)
bicubic_np = np.array(bicubic_img)
esrgan_np = np.array(esrgan_img)
hatsat_np = np.array(hatsat_img)
results = {}
for name, img_np, t in [("Bicubic", bicubic_np, bicubic_time),
("ESRGAN", esrgan_np, esrgan_time),
("HATSAT", hatsat_np, hatsat_time)]:
# Ensure dimensions match (in case of any cropping/padding discrepancy)
if img_np.shape != gt_np.shape:
# Crop to match GT exactly
img_np = img_np[:h, :w, :]
psnr_val = calculate_psnr(gt_np, img_np)
ssim_val = calculate_ssim(gt_np, img_np)
rmse_val = calculate_rmse(gt_np, img_np)
results[name] = {
"PSNR": psnr_val,
"SSIM": ssim_val,
"RMSE": rmse_val,
"Time": t
}
print(f"{name.upper()}:")
print(f" PSNR: {psnr_val:.2f} dB (higher is better)")
print(f" SSIM: {ssim_val:.4f} (higher is better)")
print(f" RMSE: {rmse_val:.2f} (lower is better)")
print(f" Time: {t:.2f} s")
print("-" * 40)
print("SUMMARY CONCLUSION:")
best_psnr = max(results, key=lambda k: results[k]["PSNR"])
best_ssim = max(results, key=lambda k: results[k]["SSIM"])
best_rmse = min(results, key=lambda k: results[k]["RMSE"])
print(f"Highest PSNR: {best_psnr} ({results[best_psnr]['PSNR']:.2f} dB)")
print(f"Highest SSIM: {best_ssim} ({results[best_ssim]['SSIM']:.4f})")
print(f"Lowest RMSE: {best_rmse} ({results[best_rmse]['RMSE']:.2f})")
# Save the synthetic LR and upsampled variants for visual inspection
out_dir = ROOT / "outputs" / "validation"
out_dir.mkdir(parents=True, exist_ok=True)
gt_img.save(out_dir / "01_GroundTruth.png")
lr_img.save(out_dir / "02_SimulatedLR.png")
bicubic_img.save(out_dir / "03_Bicubic.png")
esrgan_img.save(out_dir / "04_ESRGAN.png")
hatsat_img.save(out_dir / "05_HATSAT.png")
print(f"\nSaved visual comparison images to: {out_dir}")
print("=" * 60)
if __name__ == "__main__":
run_validation()
|