| """ |
| Reward computation for the shader environment. |
| |
| Provides SSIM (structural similarity) between two raw RGBA pixel buffers. |
| Uses scipy windowed SSIM when available, falls back to global-stats SSIM. |
| """ |
|
|
| import numpy as np |
|
|
| |
| _K1 = 0.01 |
| _K2 = 0.03 |
| _L = 1.0 |
| _C1 = (_K1 * _L) ** 2 |
| _C2 = (_K2 * _L) ** 2 |
|
|
| try: |
| from scipy.ndimage import uniform_filter |
| _HAS_SCIPY = True |
| except ImportError: |
| _HAS_SCIPY = False |
|
|
|
|
| def _channel(a: np.ndarray, b: np.ndarray, window: int = 11) -> float: |
| """SSIM for a single channel (H, W) float array in [0, 1].""" |
| if _HAS_SCIPY: |
| mu_a = uniform_filter(a, size=window) |
| mu_b = uniform_filter(b, size=window) |
| sigma_aa = uniform_filter(a * a, size=window) - mu_a * mu_a |
| sigma_bb = uniform_filter(b * b, size=window) - mu_b * mu_b |
| sigma_ab = uniform_filter(a * b, size=window) - mu_a * mu_b |
| else: |
| mu_a = np.mean(a) |
| mu_b = np.mean(b) |
| sigma_aa = np.var(a) |
| sigma_bb = np.var(b) |
| sigma_ab = np.mean((a - mu_a) * (b - mu_b)) |
|
|
| num = (2 * mu_a * mu_b + _C1) * (2 * sigma_ab + _C2) |
| den = (mu_a ** 2 + mu_b ** 2 + _C1) * (sigma_aa + sigma_bb + _C2) |
|
|
| ssim_map = num / den |
|
|
| if isinstance(ssim_map, np.ndarray): |
| return float(np.mean(ssim_map)) |
| return float(ssim_map) |
|
|
|
|
| def ssim(ref: bytes | None, agent: bytes | None, width: int, height: int) -> float: |
| """ |
| Mean SSIM between two raw RGBA byte buffers (top-left origin). |
| |
| Drops alpha channel, computes per-channel SSIM on RGB, returns mean. |
| Returns 0.0 on malformed input. |
| """ |
| if ref is None or agent is None: |
| return 0.0 |
|
|
| expected = width * height * 4 |
| if len(ref) != expected or len(agent) != expected: |
| return 0.0 |
|
|
| ref_arr = np.frombuffer(ref, dtype=np.uint8).reshape(height, width, 4) |
| agent_arr = np.frombuffer(agent, dtype=np.uint8).reshape(height, width, 4) |
|
|
| |
| ref_rgb = ref_arr[:, :, :3].astype(np.float64) / 255.0 |
| agent_rgb = agent_arr[:, :, :3].astype(np.float64) / 255.0 |
|
|
| |
| scores = [] |
| for c in range(3): |
| scores.append(_channel(ref_rgb[:, :, c], agent_rgb[:, :, c])) |
|
|
| return float(np.clip(np.mean(scores), 0.0, 1.0)) |
|
|