hoàn thành chức năng remove cloud train
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
"""
|
||||
Train Cloud Removal Model using SEN12MS-CR Dataset
|
||||
Huấn luyện model Deep Learning để khử mây từ ảnh Sentinel-2
|
||||
|
||||
Dataset: SEN12MS-CR (Sentinel-12 Multi-Seasonal Cloud Removal)
|
||||
- Input: S2 cloudy images (ảnh Sentinel-2 bị mây)
|
||||
- Target: S2 clean images (ảnh Sentinel-2 sạch)
|
||||
- Optional: S1 SAR data (radar data không bị ảnh hưởng bởi mây)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
from tqdm import tqdm
|
||||
|
||||
# Add winter_dataset to path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "winter_dataset"))
|
||||
from sen12ms_cr_dataLoader import SEN12MSCRDataset, Seasons, S1Bands, S2Bands
|
||||
|
||||
|
||||
# ============ DATASET WRAPPER ============
|
||||
|
||||
class CloudRemovalDataset(Dataset):
|
||||
"""
|
||||
PyTorch Dataset wrapper cho SEN12MS-CR
|
||||
Input: S2 cloudy + S1 (optional)
|
||||
Target: S2 clean
|
||||
"""
|
||||
|
||||
def __init__(self, base_dir, season=Seasons.WINTER, use_s1=True,
|
||||
s2_bands=S2Bands.ALL, normalize=True):
|
||||
"""
|
||||
Args:
|
||||
base_dir: Đường dẫn đến thư mục chứa dữ liệu
|
||||
season: Mùa (SPRING, SUMMER, FALL, WINTER)
|
||||
use_s1: Có sử dụng dữ liệu S1 (radar) không
|
||||
s2_bands: Các band S2 cần dùng
|
||||
normalize: Normalize dữ liệu về [0, 1]
|
||||
"""
|
||||
self.dataset = SEN12MSCRDataset(base_dir)
|
||||
self.season = season
|
||||
self.use_s1 = use_s1
|
||||
self.s2_bands = s2_bands
|
||||
self.normalize = normalize
|
||||
|
||||
# Lấy tất cả scene và patch IDs
|
||||
season_ids = self.dataset.get_season_ids(season)
|
||||
|
||||
# Tạo list of (scene_id, patch_id) pairs
|
||||
self.samples = []
|
||||
for scene_id, patch_ids in season_ids.items():
|
||||
for patch_id in patch_ids:
|
||||
self.samples.append((scene_id, patch_id))
|
||||
|
||||
# Get band count
|
||||
n_s2_bands = len(s2_bands.value) if hasattr(s2_bands, 'value') else len(s2_bands)
|
||||
|
||||
print(f"[DATASET] Loaded {len(self.samples)} samples from {season.value}")
|
||||
print(f"[DATASET] Use S1: {use_s1}, S2 bands: {n_s2_bands}")
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
scene_id, patch_id = self.samples[idx]
|
||||
|
||||
# Load triplet: S1, S2 clean, S2 cloudy
|
||||
s1, s2_clean, s2_cloudy, bounds = self.dataset.get_s1s2s2cloudy_triplet(
|
||||
self.season,
|
||||
scene_id,
|
||||
patch_id,
|
||||
s1_bands=S1Bands.ALL if self.use_s1 else S1Bands.NONE,
|
||||
s2_bands=self.s2_bands,
|
||||
s2cloudy_bands=self.s2_bands
|
||||
)
|
||||
|
||||
# Normalize to [0, 1] if needed
|
||||
if self.normalize:
|
||||
s2_clean = s2_clean.astype(np.float32) / 10000.0 # S2 values are in [0, 10000]
|
||||
s2_cloudy = s2_cloudy.astype(np.float32) / 10000.0
|
||||
if self.use_s1:
|
||||
# S1 values need different normalization (dB scale)
|
||||
s1 = (s1.astype(np.float32) + 30) / 50.0 # Normalize from [-30, 20] to [0, 1]
|
||||
s1 = np.clip(s1, 0, 1)
|
||||
|
||||
# Convert to torch tensors
|
||||
s2_clean = torch.from_numpy(s2_clean).float()
|
||||
s2_cloudy = torch.from_numpy(s2_cloudy).float()
|
||||
|
||||
# Input: S2 cloudy + S1 (if enabled)
|
||||
if self.use_s1:
|
||||
s1 = torch.from_numpy(s1).float()
|
||||
input_data = torch.cat([s2_cloudy, s1], dim=0)
|
||||
else:
|
||||
input_data = s2_cloudy
|
||||
|
||||
return input_data, s2_clean
|
||||
|
||||
|
||||
# ============ U-NET ARCHITECTURE ============
|
||||
|
||||
class DoubleConv(nn.Module):
|
||||
"""(Conv2d -> BatchNorm -> ReLU) x 2"""
|
||||
|
||||
def __init__(self, in_channels, out_channels):
|
||||
super().__init__()
|
||||
self.double_conv = nn.Sequential(
|
||||
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(out_channels),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(out_channels),
|
||||
nn.ReLU(inplace=True)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.double_conv(x)
|
||||
|
||||
|
||||
class UNet(nn.Module):
|
||||
"""
|
||||
U-Net architecture cho cloud removal
|
||||
Input: S2 cloudy (+ S1 optional) [B, C_in, H, W]
|
||||
Output: S2 clean [B, C_out, H, W]
|
||||
"""
|
||||
|
||||
def __init__(self, in_channels, out_channels, features=[64, 128, 256, 512]):
|
||||
super().__init__()
|
||||
self.encoder = nn.ModuleList()
|
||||
self.decoder = nn.ModuleList()
|
||||
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
|
||||
|
||||
# Encoder (downsampling)
|
||||
for feature in features:
|
||||
self.encoder.append(DoubleConv(in_channels, feature))
|
||||
in_channels = feature
|
||||
|
||||
# Bottleneck
|
||||
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
|
||||
|
||||
# Decoder (upsampling)
|
||||
for feature in reversed(features):
|
||||
self.decoder.append(
|
||||
nn.ConvTranspose2d(feature * 2, feature, kernel_size=2, stride=2)
|
||||
)
|
||||
self.decoder.append(DoubleConv(feature * 2, feature))
|
||||
|
||||
# Final output layer
|
||||
self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)
|
||||
|
||||
def forward(self, x):
|
||||
skip_connections = []
|
||||
|
||||
# Encoder
|
||||
for encode in self.encoder:
|
||||
x = encode(x)
|
||||
skip_connections.append(x)
|
||||
x = self.pool(x)
|
||||
|
||||
# Bottleneck
|
||||
x = self.bottleneck(x)
|
||||
|
||||
# Decoder
|
||||
skip_connections = skip_connections[::-1]
|
||||
|
||||
for idx in range(0, len(self.decoder), 2):
|
||||
x = self.decoder[idx](x) # Upsample
|
||||
skip_connection = skip_connections[idx // 2]
|
||||
|
||||
# Handle size mismatch
|
||||
if x.shape != skip_connection.shape:
|
||||
x = nn.functional.interpolate(x, size=skip_connection.shape[2:])
|
||||
|
||||
concat_skip = torch.cat((skip_connection, x), dim=1)
|
||||
x = self.decoder[idx + 1](concat_skip) # Double conv
|
||||
|
||||
return self.final_conv(x)
|
||||
|
||||
|
||||
# ============ TRAINING FUNCTIONS ============
|
||||
|
||||
def train_epoch(model, dataloader, criterion, optimizer, device):
|
||||
"""Train for one epoch"""
|
||||
model.train()
|
||||
total_loss = 0
|
||||
|
||||
pbar = tqdm(dataloader, desc="Training")
|
||||
for batch_idx, (inputs, targets) in enumerate(pbar):
|
||||
inputs = inputs.to(device)
|
||||
targets = targets.to(device)
|
||||
|
||||
# Forward pass
|
||||
optimizer.zero_grad()
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
pbar.set_postfix({'loss': loss.item()})
|
||||
|
||||
return total_loss / len(dataloader)
|
||||
|
||||
|
||||
def validate(model, dataloader, criterion, device):
|
||||
"""Validate model"""
|
||||
model.eval()
|
||||
total_loss = 0
|
||||
|
||||
with torch.no_grad():
|
||||
for inputs, targets in tqdm(dataloader, desc="Validation"):
|
||||
inputs = inputs.to(device)
|
||||
targets = targets.to(device)
|
||||
|
||||
outputs = model(inputs)
|
||||
loss = criterion(outputs, targets)
|
||||
total_loss += loss.item()
|
||||
|
||||
return total_loss / len(dataloader)
|
||||
|
||||
|
||||
def visualize_results(model, dataset, device, num_samples=3):
|
||||
"""Visualize cloud removal results"""
|
||||
model.eval()
|
||||
|
||||
fig, axes = plt.subplots(num_samples, 3, figsize=(15, 5 * num_samples))
|
||||
|
||||
with torch.no_grad():
|
||||
for i in range(num_samples):
|
||||
idx = np.random.randint(0, len(dataset))
|
||||
input_data, target = dataset[idx]
|
||||
|
||||
input_data = input_data.unsqueeze(0).to(device)
|
||||
output = model(input_data)
|
||||
|
||||
# Convert to numpy
|
||||
input_rgb = input_data[0, :3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
target_rgb = target[:3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
output_rgb = output[0, :3, :, :].cpu().numpy().transpose(1, 2, 0)
|
||||
|
||||
# Clip to [0, 1]
|
||||
input_rgb = np.clip(input_rgb * 3, 0, 1) # Enhance for visualization
|
||||
target_rgb = np.clip(target_rgb * 3, 0, 1)
|
||||
output_rgb = np.clip(output_rgb * 3, 0, 1)
|
||||
|
||||
if num_samples == 1:
|
||||
axes[0].imshow(input_rgb)
|
||||
axes[0].set_title("Input (Cloudy)")
|
||||
axes[0].axis('off')
|
||||
|
||||
axes[1].imshow(output_rgb)
|
||||
axes[1].set_title("Output (Predicted)")
|
||||
axes[1].axis('off')
|
||||
|
||||
axes[2].imshow(target_rgb)
|
||||
axes[2].set_title("Target (Clean)")
|
||||
axes[2].axis('off')
|
||||
else:
|
||||
axes[i, 0].imshow(input_rgb)
|
||||
axes[i, 0].set_title(f"Sample {i+1}: Input (Cloudy)")
|
||||
axes[i, 0].axis('off')
|
||||
|
||||
axes[i, 1].imshow(output_rgb)
|
||||
axes[i, 1].set_title(f"Sample {i+1}: Output (Predicted)")
|
||||
axes[i, 1].axis('off')
|
||||
|
||||
axes[i, 2].imshow(target_rgb)
|
||||
axes[i, 2].set_title(f"Sample {i+1}: Target (Clean)")
|
||||
axes[i, 2].axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
return fig
|
||||
|
||||
|
||||
# ============ MAIN TRAINING SCRIPT ============
|
||||
|
||||
def train_cloud_removal_model(
|
||||
data_dir="winter_dataset",
|
||||
use_s1=True,
|
||||
batch_size=8,
|
||||
num_epochs=50,
|
||||
learning_rate=1e-4,
|
||||
device="cuda" if torch.cuda.is_available() else "cpu",
|
||||
save_dir="model_train"
|
||||
):
|
||||
"""
|
||||
Train cloud removal model
|
||||
|
||||
Args:
|
||||
data_dir: Thư mục chứa dữ liệu SEN12MS-CR
|
||||
use_s1: Có sử dụng S1 radar data không
|
||||
batch_size: Batch size
|
||||
num_epochs: Số epochs
|
||||
learning_rate: Learning rate
|
||||
device: 'cuda' hoặc 'cpu'
|
||||
save_dir: Thư mục lưu model
|
||||
"""
|
||||
|
||||
print("=" * 70)
|
||||
print("🌥️ CLOUD REMOVAL MODEL TRAINING")
|
||||
print("=" * 70)
|
||||
print(f"Data directory: {data_dir}")
|
||||
print(f"Use S1 (SAR): {use_s1}")
|
||||
print(f"Device: {device}")
|
||||
print(f"Batch size: {batch_size}")
|
||||
print(f"Epochs: {num_epochs}")
|
||||
print(f"Learning rate: {learning_rate}")
|
||||
print("=" * 70)
|
||||
|
||||
# Create dataset
|
||||
print("\n📂 Loading dataset...")
|
||||
|
||||
# Use RGB + NIR bands for training (B02, B03, B04, B08)
|
||||
s2_bands = [S2Bands.B02, S2Bands.B03, S2Bands.B04, S2Bands.B08]
|
||||
|
||||
dataset = CloudRemovalDataset(
|
||||
base_dir=data_dir,
|
||||
season=Seasons.WINTER,
|
||||
use_s1=use_s1,
|
||||
s2_bands=s2_bands,
|
||||
normalize=True
|
||||
)
|
||||
|
||||
# Split train/val
|
||||
train_size = int(0.8 * len(dataset))
|
||||
val_size = len(dataset) - train_size
|
||||
train_dataset, val_dataset = torch.utils.data.random_split(
|
||||
dataset, [train_size, val_size]
|
||||
)
|
||||
|
||||
print(f"Train samples: {len(train_dataset)}")
|
||||
print(f"Val samples: {len(val_dataset)}")
|
||||
|
||||
# Create dataloaders
|
||||
train_loader = DataLoader(
|
||||
train_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
num_workers=4,
|
||||
pin_memory=True if device == "cuda" else False
|
||||
)
|
||||
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
num_workers=4,
|
||||
pin_memory=True if device == "cuda" else False
|
||||
)
|
||||
|
||||
# Create model
|
||||
print("\n🏗️ Creating U-Net model...")
|
||||
in_channels = len(s2_bands) + (2 if use_s1 else 0) # S2 + S1 (VV, VH)
|
||||
out_channels = len(s2_bands)
|
||||
|
||||
model = UNet(in_channels=in_channels, out_channels=out_channels)
|
||||
model = model.to(device)
|
||||
|
||||
print(f"Input channels: {in_channels}")
|
||||
print(f"Output channels: {out_channels}")
|
||||
print(f"Model parameters: {sum(p.numel() for p in model.parameters()):,}")
|
||||
|
||||
# Loss and optimizer
|
||||
criterion = nn.L1Loss() # MAE loss
|
||||
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
||||
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
|
||||
optimizer, mode='min', factor=0.5, patience=5
|
||||
)
|
||||
|
||||
# Training loop
|
||||
print("\n🚀 Starting training...")
|
||||
best_val_loss = float('inf')
|
||||
train_losses = []
|
||||
val_losses = []
|
||||
|
||||
for epoch in range(num_epochs):
|
||||
print(f"\n{'='*70}")
|
||||
print(f"Epoch {epoch + 1}/{num_epochs}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
# Train
|
||||
train_loss = train_epoch(model, train_loader, criterion, optimizer, device)
|
||||
train_losses.append(train_loss)
|
||||
|
||||
# Validate
|
||||
val_loss = validate(model, val_loader, criterion, device)
|
||||
val_losses.append(val_loss)
|
||||
|
||||
# Update learning rate
|
||||
scheduler.step(val_loss)
|
||||
|
||||
print(f"\nEpoch {epoch + 1} Summary:")
|
||||
print(f" Train Loss: {train_loss:.6f}")
|
||||
print(f" Val Loss: {val_loss:.6f}")
|
||||
|
||||
# Save best model
|
||||
if val_loss < best_val_loss:
|
||||
best_val_loss = val_loss
|
||||
save_path = Path(save_dir) / "cloud_removal_unet_best.pth"
|
||||
save_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
torch.save({
|
||||
'epoch': epoch,
|
||||
'model_state_dict': model.state_dict(),
|
||||
'optimizer_state_dict': optimizer.state_dict(),
|
||||
'train_loss': train_loss,
|
||||
'val_loss': val_loss,
|
||||
'use_s1': use_s1,
|
||||
'in_channels': in_channels,
|
||||
'out_channels': out_channels
|
||||
}, save_path)
|
||||
|
||||
print(f" 💾 Saved best model: {save_path}")
|
||||
|
||||
# Visualize every 10 epochs
|
||||
if (epoch + 1) % 10 == 0:
|
||||
print("\n📊 Generating visualizations...")
|
||||
fig = visualize_results(model, val_dataset, device, num_samples=3)
|
||||
|
||||
viz_path = Path(save_dir) / f"cloud_removal_epoch_{epoch+1}.png"
|
||||
fig.savefig(viz_path, dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f" 💾 Saved visualization: {viz_path}")
|
||||
|
||||
# Plot training curves
|
||||
print("\n📈 Plotting training curves...")
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.plot(train_losses, label='Train Loss')
|
||||
ax.plot(val_losses, label='Val Loss')
|
||||
ax.set_xlabel('Epoch')
|
||||
ax.set_ylabel('Loss (MAE)')
|
||||
ax.set_title('Cloud Removal Training Progress')
|
||||
ax.legend()
|
||||
ax.grid(True)
|
||||
|
||||
curve_path = Path(save_dir) / "training_curves.png"
|
||||
fig.savefig(curve_path, dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f" 💾 Saved training curves: {curve_path}")
|
||||
|
||||
# Final summary
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ TRAINING COMPLETED!")
|
||||
print("=" * 70)
|
||||
print(f"Best validation loss: {best_val_loss:.6f}")
|
||||
print(f"Model saved to: {Path(save_dir) / 'cloud_removal_unet_best.pth'}")
|
||||
print("=" * 70)
|
||||
|
||||
return model, train_losses, val_losses
|
||||
|
||||
|
||||
# ============ INFERENCE FUNCTION ============
|
||||
|
||||
def apply_cloud_removal(model_path, cloudy_image, s1_data=None, device="cuda"):
|
||||
"""
|
||||
Áp dụng model để khử mây cho một ảnh
|
||||
|
||||
Args:
|
||||
model_path: Đường dẫn đến model đã train
|
||||
cloudy_image: Ảnh S2 bị mây [C, H, W]
|
||||
s1_data: Dữ liệu S1 (optional) [2, H, W]
|
||||
device: 'cuda' hoặc 'cpu'
|
||||
|
||||
Returns:
|
||||
cleaned_image: Ảnh đã khử mây [C, H, W]
|
||||
"""
|
||||
# Load model
|
||||
checkpoint = torch.load(model_path, map_location=device)
|
||||
|
||||
model = UNet(
|
||||
in_channels=checkpoint['in_channels'],
|
||||
out_channels=checkpoint['out_channels']
|
||||
)
|
||||
model.load_state_dict(checkpoint['model_state_dict'])
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
|
||||
# Prepare input
|
||||
input_tensor = torch.from_numpy(cloudy_image).float().unsqueeze(0).to(device)
|
||||
|
||||
if checkpoint['use_s1'] and s1_data is not None:
|
||||
s1_tensor = torch.from_numpy(s1_data).float().unsqueeze(0).to(device)
|
||||
input_tensor = torch.cat([input_tensor, s1_tensor], dim=1)
|
||||
|
||||
# Inference
|
||||
with torch.no_grad():
|
||||
output = model(input_tensor)
|
||||
|
||||
cleaned_image = output[0].cpu().numpy()
|
||||
|
||||
return cleaned_image
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Train model
|
||||
model, train_losses, val_losses = train_cloud_removal_model(
|
||||
data_dir="winter_dataset",
|
||||
use_s1=True,
|
||||
batch_size=8,
|
||||
num_epochs=50,
|
||||
learning_rate=1e-4
|
||||
)
|
||||
Reference in New Issue
Block a user