""" Cloud Removal Module - Hệ thống xử lý mây độc lập Cung cấp nhiều phương pháp khử mây cho dữ liệu Sentinel-2 """ import numpy as np import xarray as xr from typing import Tuple, Optional, Dict from sklearn.neighbors import KNeighborsRegressor from sklearn.ensemble import RandomForestRegressor import warnings warnings.filterwarnings('ignore') class CloudRemovalStrategy: """Base class cho các chiến lược xử lý mây""" def __init__(self, name: str, description: str): self.name = name self.description = description def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: """ Xử lý mây và trả về dữ liệu đã được làm sạch Returns: Tuple[xr.Dataset, Dict]: (cleaned_data, metadata) """ raise NotImplementedError class ClassicStrategy(CloudRemovalStrategy): """ Chiến lược cổ điển 3 bước: 1. Temporal interpolation (ffill + bfill) 2. Median compositing (nếu >= 3 scenes) 3. Spatial interpolation (nearest neighbor) """ def __init__(self): super().__init__( name="classic", description="3-step classical approach: temporal → median → spatial interpolation" ) def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'steps_applied': [] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) # Step 1: Temporal Interpolation for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time') metadata['steps_applied'].append('temporal_interpolation') # Step 2: Median Compositing (if >= 3 time steps) if len(s2_data.time) >= 3: for band in s2_data.data_vars: if band != "SCL": median_composite = s2_data[band].median(dim='time', skipna=True) s2_data[band] = s2_data[band].fillna(median_composite) metadata['steps_applied'].append('median_compositing') # Step 3: Spatial Interpolation for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest', fill_value='extrapolate') s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest', fill_value='extrapolate') metadata['steps_applied'].append('spatial_interpolation') # Final fallback for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata class NoRemovalStrategy(CloudRemovalStrategy): """Không xử lý mây - giữ nguyên dữ liệu gốc, chỉ fill NaN bằng 0""" def __init__(self): super().__init__( name="none", description="No cloud removal - keep original data with NaN filled as 0" ) def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'steps_applied': ['none'], 'note': 'No cloud removal applied, only NaN filling' } # Chỉ fill NaN bằng 0, không apply cloud mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata class TemporalOnlyStrategy(CloudRemovalStrategy): """Chỉ sử dụng temporal interpolation - nhanh nhất, phù hợp khi có nhiều time steps""" def __init__(self): super().__init__( name="temporal_only", description="Temporal interpolation only - fast, good for time series with many scenes" ) def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'steps_applied': ['temporal_interpolation'] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) # Temporal interpolation for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time') s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata class MedianCompositeStrategy(CloudRemovalStrategy): """Ưu tiên median composite - tốt nhất cho giảm noise""" def __init__(self): super().__init__( name="median_composite", description="Median composite priority - best for noise reduction" ) def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'steps_applied': ['median_compositing', 'spatial_interpolation'] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) # Direct median composite for band in s2_data.data_vars: if band != "SCL": median_composite = s2_data[band].median(dim='time', skipna=True) # Fill all NaN with median s2_data[band] = s2_data[band].fillna(median_composite) # Spatial interpolation for remaining gaps for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest') s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest') s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata class MLInpaintingStrategy(CloudRemovalStrategy): """ Machine Learning Inpainting - sử dụng KNN hoặc Random Forest Học từ pixels hợp lệ để dự đoán pixels bị mây """ def __init__(self, ml_model: str = "knn"): """ Args: ml_model: 'knn' hoặc 'rf' (random forest) """ super().__init__( name=f"ml_inpainting_{ml_model}", description=f"ML-based cloud removal using {ml_model.upper()} - learns from valid pixels" ) self.ml_model = ml_model def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'ml_model': self.ml_model, 'steps_applied': [] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) # ML inpainting cho từng time step for time_idx in range(len(s2_data.time)): # Get all bands for this time step bands_data = [] band_names = [] for band in s2_data.data_vars: if band != "SCL": band_data = s2_data[band].isel(time=time_idx).values bands_data.append(band_data.flatten()) band_names.append(band) if not bands_data: continue # Stack bands: shape (n_pixels, n_bands) X_all = np.column_stack(bands_data) # Find valid (non-NaN) and invalid (NaN) pixels valid_mask = ~np.isnan(X_all).any(axis=1) if valid_mask.sum() < 10: # Not enough training data continue X_valid = X_all[valid_mask] X_invalid_indices = np.where(~valid_mask)[0] if len(X_invalid_indices) == 0: # No clouds continue # Prepare features: use spatial coordinates + spectral values y_coords, x_coords = np.meshgrid( np.arange(s2_data.dims['y']), np.arange(s2_data.dims['x']), indexing='ij' ) coords_flat = np.column_stack([y_coords.flatten(), x_coords.flatten()]) # Train ML model on valid pixels X_train = coords_flat[valid_mask] y_train = X_valid try: if self.ml_model == "knn": model = KNeighborsRegressor(n_neighbors=min(5, len(X_train)), weights='distance') else: # random forest model = RandomForestRegressor(n_estimators=10, max_depth=10, random_state=42, n_jobs=-1) model.fit(X_train, y_train) # Predict invalid pixels X_test = coords_flat[X_invalid_indices] predictions = model.predict(X_test) # Fill predictions back X_all[X_invalid_indices] = predictions # Reshape and update dataset for band_idx, band in enumerate(band_names): filled_data = X_all[:, band_idx].reshape(s2_data.dims['y'], s2_data.dims['x']) s2_data[band].values[time_idx] = filled_data metadata['steps_applied'].append(f'ml_inpainting_time_{time_idx}') except Exception as e: print(f"[ML INPAINTING] Error at time {time_idx}: {e}") continue # Final cleanup for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata class DeepInpaintingStrategy(CloudRemovalStrategy): """ Deep Learning Inpainting - sử dụng U-Net CNN Phức tạp hơn nhưng cho kết quả tốt nhất với large cloud gaps Note: Yêu cầu pretrained model (train bằng train_cloud_removal.py) """ def __init__(self, model_path: Optional[str] = None): super().__init__( name="deep_inpainting", description="Deep Learning U-Net based cloud removal - best quality for large gaps" ) self.model_path = model_path or "model_train/cloud_removal_unet_best.pth" self.model = None self.device = None # Try to load model if provided if model_path or Path(self.model_path).exists(): try: import torch import torch.nn as nn # Load checkpoint checkpoint = torch.load(self.model_path, map_location='cpu') # Recreate U-Net architecture from train_cloud_removal import UNet self.model = UNet( in_channels=checkpoint.get('in_channels', 4), out_channels=checkpoint.get('out_channels', 4) ) self.model.load_state_dict(checkpoint['model_state_dict']) # Set device self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.model = self.model.to(self.device) self.model.eval() print(f"[DEEP INPAINTING] Loaded U-Net model from {self.model_path}") print(f"[DEEP INPAINTING] Using device: {self.device}") except Exception as e: print(f"[DEEP INPAINTING] Could not load model: {e}") self.model = None def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'has_model': self.model is not None, 'steps_applied': [] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) if self.model is None: # Fallback to classical method print("[DEEP INPAINTING] No model available, falling back to median composite") for band in s2_data.data_vars: if band != "SCL": median_composite = s2_data[band].median(dim='time', skipna=True) s2_data[band] = s2_data[band].fillna(median_composite) s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest') s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest') s2_data[band] = s2_data[band].fillna(0) metadata['steps_applied'].append('fallback_median') else: # Use U-Net for cloud removal print("[DEEP INPAINTING] Applying U-Net cloud removal...") import torch try: # Process each time step for time_idx in range(len(s2_data.time)): # Get bands for this time step (B02, B03, B04, B08) bands_to_process = ['B02', 'B03', 'B04', 'B08'] available_bands = [b for b in bands_to_process if b in s2_data.data_vars] if len(available_bands) < 4: print(f"[DEEP INPAINTING] Warning: Not all required bands available, skipping time {time_idx}") continue # Stack bands [C, H, W] input_bands = [] for band in available_bands: band_data = s2_data[band].isel(time=time_idx).values.astype(np.float32) # Normalize to [0, 1] (S2 values are typically 0-10000) band_data = np.clip(band_data / 10000.0, 0, 1) input_bands.append(band_data) input_array = np.stack(input_bands, axis=0) # [C, H, W] # Convert to tensor and add batch dimension input_tensor = torch.from_numpy(input_array).unsqueeze(0).to(self.device) # Run through U-Net with torch.no_grad(): output_tensor = self.model(input_tensor) # Convert back to numpy output_array = output_tensor[0].cpu().numpy() # [C, H, W] # Denormalize back to original scale output_array = output_array * 10000.0 # Update dataset with cleaned data for i, band in enumerate(available_bands): s2_data[band].values[time_idx] = output_array[i] metadata['steps_applied'].append(f'unet_time_{time_idx}') print(f"[DEEP INPAINTING] Processed {len(s2_data.time)} time steps with U-Net") except Exception as e: print(f"[DEEP INPAINTING] Error during inference: {e}") # Fallback to classical method for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time') s2_data[band] = s2_data[band].fillna(0) metadata['steps_applied'].append('unet_error_fallback') return s2_data, metadata class HybridStrategy(CloudRemovalStrategy): """ Hybrid Strategy - kết hợp Classical + ML 1. Classical temporal interpolation (nhanh) 2. ML inpainting cho gaps còn lại (chất lượng cao) 3. Spatial interpolation (cleanup) """ def __init__(self): super().__init__( name="hybrid", description="Hybrid classical + ML - balanced speed and quality" ) def remove_clouds(self, s2_data: xr.Dataset, cloud_mask: xr.DataArray) -> Tuple[xr.Dataset, Dict]: metadata = { 'method': self.name, 'steps_applied': [] } # Apply mask for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].where(~cloud_mask) # Step 1: Temporal interpolation (fast) for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time') metadata['steps_applied'].append('temporal_interpolation') # Step 2: Check remaining NaN percentage nan_count = 0 total_count = 0 for band in s2_data.data_vars: if band != "SCL": nan_count += np.isnan(s2_data[band].values).sum() total_count += s2_data[band].values.size nan_percentage = (nan_count / total_count * 100) if total_count > 0 else 0 # Step 3: ML inpainting if still significant gaps (>5%) if nan_percentage > 5.0: print(f"[HYBRID] {nan_percentage:.1f}% NaN remaining, applying ML inpainting...") ml_strategy = MLInpaintingStrategy(ml_model="knn") s2_data, ml_meta = ml_strategy.remove_clouds(s2_data, cloud_mask) metadata['steps_applied'].extend(['ml_inpainting_knn']) metadata['nan_before_ml'] = nan_percentage else: # Step 4: Spatial interpolation for small gaps for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest') s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest') metadata['steps_applied'].append('spatial_interpolation') # Final cleanup for band in s2_data.data_vars: if band != "SCL": s2_data[band] = s2_data[band].fillna(0) return s2_data, metadata # ============ FACTORY & UTILITIES ============ def get_available_methods() -> Dict[str, str]: """Trả về dictionary của tất cả methods có sẵn""" return { "none": "No cloud removal - keep original data (fastest, may have cloud artifacts)", "classic": "3-step classical: temporal → median → spatial (default, balanced)", "temporal_only": "Temporal interpolation only (fast, needs many scenes)", "median_composite": "Median composite priority (best noise reduction)", "ml_knn": "ML K-Nearest Neighbors inpainting (good quality, medium speed)", "ml_rf": "ML Random Forest inpainting (high quality, slower)", "deep": "Deep Learning CNN inpainting (best quality, requires model)", "hybrid": "Hybrid classical + ML (balanced speed & quality)" } def create_cloud_removal_strategy(method: str = "classic", **kwargs) -> CloudRemovalStrategy: """ Factory function để tạo strategy từ tên method Args: method: Tên method ("classic", "temporal_only", "median_composite", "ml_knn", "ml_rf", "deep", "hybrid") **kwargs: Additional parameters cho specific strategies Returns: CloudRemovalStrategy instance """ method = method.lower() if method == "none": return NoRemovalStrategy() elif method == "classic": return ClassicStrategy() elif method == "temporal_only": return TemporalOnlyStrategy() elif method == "median_composite": return MedianCompositeStrategy() elif method == "ml_knn": return MLInpaintingStrategy(ml_model="knn") elif method == "ml_rf": return MLInpaintingStrategy(ml_model="rf") elif method == "deep": model_path = kwargs.get('model_path', None) return DeepInpaintingStrategy(model_path=model_path) elif method == "hybrid": return HybridStrategy() else: print(f"[CLOUD REMOVAL] Unknown method '{method}', using 'classic'") return ClassicStrategy() def process_cloud_removal( s2_data: xr.Dataset, method: str = "classic", verbose: bool = True, **kwargs ) -> Tuple[xr.Dataset, Dict]: """ Main entry point cho cloud removal Args: s2_data: Sentinel-2 dataset với SCL band method: Cloud removal method name verbose: Print progress messages **kwargs: Additional parameters Returns: Tuple[xr.Dataset, Dict]: (cleaned_data, metadata) """ if verbose: print(f"[CLOUD REMOVAL] Using method: {method}") # Detect clouds from SCL if "SCL" not in s2_data: if verbose: print("[CLOUD REMOVAL] Warning: No SCL band, cannot mask clouds") return s2_data, {'method': 'none', 'warning': 'no_scl_band'} scl = s2_data["SCL"] # Create comprehensive cloud mask cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10) | (scl == 11) invalid_mask = (scl == 0) | (scl == 1) full_mask = cloud_mask | invalid_mask # Calculate coverage total_pixels = full_mask.size masked_pixels = int(full_mask.sum().values) cloud_coverage_percent = (masked_pixels / total_pixels * 100) if total_pixels > 0 else 0 if verbose: print(f"[CLOUD REMOVAL] Cloud coverage: {cloud_coverage_percent:.1f}%") print(f"[CLOUD REMOVAL] Masked pixels: {masked_pixels:,}/{total_pixels:,}") # Create strategy and process strategy = create_cloud_removal_strategy(method, **kwargs) cleaned_data, metadata = strategy.remove_clouds(s2_data.copy(deep=True), full_mask) # Add coverage info to metadata metadata['cloud_coverage_percent'] = float(cloud_coverage_percent) metadata['masked_pixels'] = masked_pixels metadata['total_pixels'] = total_pixels if verbose: print(f"[CLOUD REMOVAL] Completed using {metadata['method']}") print(f"[CLOUD REMOVAL] Steps: {', '.join(metadata['steps_applied'])}") return cleaned_data, metadata # ============ TESTING & COMPARISON ============ def compare_methods(s2_data: xr.Dataset, methods: list = None) -> Dict: """ So sánh các methods khác nhau trên cùng dữ liệu Args: s2_data: Sentinel-2 dataset methods: List of method names to compare (default: all) Returns: Dict: Comparison results """ if methods is None: methods = ["classic", "temporal_only", "median_composite", "ml_knn", "hybrid"] results = {} for method in methods: try: print(f"\n{'='*60}") print(f"Testing: {method}") print(f"{'='*60}") cleaned_data, metadata = process_cloud_removal(s2_data, method=method, verbose=True) # Calculate remaining NaN nan_count = sum(np.isnan(cleaned_data[band].values).sum() for band in cleaned_data.data_vars if band != "SCL") total_count = sum(cleaned_data[band].values.size for band in cleaned_data.data_vars if band != "SCL") results[method] = { 'metadata': metadata, 'remaining_nan_percent': (nan_count / total_count * 100) if total_count > 0 else 0, 'success': True } except Exception as e: results[method] = { 'error': str(e), 'success': False } print(f"[ERROR] {method}: {e}") return results