diff --git a/CLOUD_PROCESSING.md b/CLOUD_PROCESSING.md
new file mode 100644
index 0000000..6a824fa
--- /dev/null
+++ b/CLOUD_PROCESSING.md
@@ -0,0 +1,346 @@
+# Xử lý mây (Cloud Processing) — Hệ thống Land Classification
+
+Tài liệu chi tiết về các phương pháp xử lý mây cho dữ liệu Sentinel-2. Module độc lập `cloud_removal.py` cung cấp nhiều chiến lược có thể chọn.
+
+## Tổng quan
+
+Hệ thống cung cấp **7 phương pháp xử lý mây** khác nhau, từ cổ điển đến hiện đại (ML/DL):
+
+1. **Classic** - 3 bước cổ điển (temporal → median → spatial) - mặc định
+2. **Temporal Only** - Chỉ temporal interpolation (nhanh nhất)
+3. **Median Composite** - Ưu tiên median composite (giảm nhiễu tốt nhất)
+4. **ML KNN** - Machine Learning K-Nearest Neighbors inpainting
+5. **ML RF** - Machine Learning Random Forest inpainting
+6. **Deep Inpainting** - Deep Learning CNN inpainting (yêu cầu model)
+7. **Hybrid** - Kết hợp classical + ML (cân bằng tốc độ và chất lượng)
+
+---
+
+## Cách sử dụng
+
+### API Endpoint
+
+Lấy danh sách các methods:
+```bash
+GET /api/cloud-removal/methods
+```
+
+Response:
+```json
+{
+ "success": true,
+ "methods": {
+ "classic": "3-step classical: temporal → median → spatial (default, balanced)",
+ "temporal_only": "Temporal interpolation only (fastest, 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)"
+ },
+ "default": "classic"
+}
+```
+
+### Config trong Prediction
+
+Thêm `cloud_removal_method` vào config:
+
+```python
+config = {
+ "model_filename": "model_odc.joblib",
+ "min_lon": 105.5,
+ "max_lon": 105.6,
+ "min_lat": 10.0,
+ "max_lat": 10.1,
+ "start_date": "2024-01-01",
+ "end_date": "2024-12-31",
+ "max_scenes": 12,
+ "cloud_cover": 30,
+ "resolution": 20,
+ "use_gpu": false,
+ "cloud_removal_method": "hybrid" # Chọn method tại đây
+}
+```
+
+### Programmatic Usage
+
+```python
+from cloud_removal import process_cloud_removal
+
+# Load Sentinel-2 data with SCL band
+s2_data = load(...)
+
+# Process clouds with selected method
+cleaned_data, metadata = process_cloud_removal(
+ s2_data=s2_data,
+ method="hybrid", # or "classic", "ml_knn", etc.
+ verbose=True
+)
+
+print(f"Cloud coverage: {metadata['cloud_coverage_percent']:.1f}%")
+print(f"Steps applied: {metadata['steps_applied']}")
+```
+
+---
+
+## Chi tiết các phương pháp
+
+### 1. Classic (Mặc định)
+
+**Mô tả:** 3 bước cổ điển kết hợp temporal, median, và spatial interpolation.
+
+**Quy trình:**
+1. Temporal interpolation (ffill + bfill)
+2. Median compositing (nếu >= 3 scenes)
+3. Spatial interpolation (nearest neighbor)
+4. Fallback fillna(0)
+
+**Ưu điểm:**
+- Cân bằng tốc độ và chất lượng
+- Đã được test kỹ, ổn định
+- Phù hợp hầu hết trường hợp
+
+**Nhược điểm:**
+- Không tối ưu cho các gaps lớn
+- Có thể tạo artifacts ở biên
+
+**Khi nào dùng:** Default choice, phù hợp cho production
+
+---
+
+### 2. Temporal Only
+
+**Mô tả:** Chỉ sử dụng temporal interpolation (ffill + bfill).
+
+**Ưu điểm:**
+- Nhanh nhất
+- Giữ xu hướng thời gian tốt
+- Ít tạo artifacts
+
+**Nhược điểm:**
+- Yêu cầu nhiều time steps
+- Không xử lý được gaps liên tục
+- Chất lượng kém nếu ít scenes
+
+**Khi nào dùng:** Khi có nhiều scenes (>10) và cần tốc độ
+
+---
+
+### 3. Median Composite
+
+**Mô tả:** Ưu tiên median composite, sau đó spatial interpolation.
+
+**Ưu điểm:**
+- Giảm nhiễu tốt nhất
+- Chống outliers hiệu quả
+- Tạo composite trơn
+
+**Nhược điểm:**
+- Mất thông tin temporal
+- Yêu cầu >= 3 scenes
+- Chậm hơn temporal only
+
+**Khi nào dùng:** Khi cần giảm nhiễu, không quan tâm temporal dynamics
+
+---
+
+### 4. ML KNN Inpainting
+
+**Mô tả:** Sử dụng K-Nearest Neighbors để học từ pixels hợp lệ và dự đoán pixels bị mây.
+
+**Quy trình:**
+1. Xác định valid pixels (không có mây)
+2. Train KNN model với spatial coordinates + spectral values
+3. Predict invalid pixels
+4. Fill predictions vào dataset
+
+**Ưu điểm:**
+- Chất lượng cao hơn classical
+- Học spatial patterns
+- Không cần pretrained model
+
+**Nhược điểm:**
+- Chậm hơn classical
+- Yêu cầu đủ valid pixels (>10)
+- Tốn RAM nếu ảnh lớn
+
+**Hyperparameters:**
+- n_neighbors: 5
+- weights: 'distance'
+
+**Khi nào dùng:** Khi cần chất lượng cao và có đủ valid pixels
+
+---
+
+### 5. ML Random Forest Inpainting
+
+**Mô tả:** Sử dụng Random Forest để inpainting, tương tự KNN nhưng phức tạp hơn.
+
+**Ưu điểm:**
+- Chất lượng cao nhất trong ML methods
+- Xử lý non-linear patterns tốt
+- Robust với outliers
+
+**Nhược điểm:**
+- Chậm nhất trong ML methods
+- Tốn nhiều RAM
+- Có thể overfit với ít data
+
+**Hyperparameters:**
+- n_estimators: 10
+- max_depth: 10
+- n_jobs: -1 (parallel)
+
+**Khi nào dùng:** Khi cần chất lượng tối đa và không quan tâm tốc độ
+
+---
+
+### 6. Deep Inpainting (CNN)
+
+**Mô tả:** Sử dụng CNN autoencoder để reconstruct pixels bị mây.
+
+**Trạng thái:** **Đang phát triển** - yêu cầu pretrained model
+
+**Quy trình (planned):**
+1. Stack bands thành multi-channel image
+2. Tạo binary mask (1=cloud, 0=valid)
+3. Run through CNN autoencoder
+4. Blend predictions với valid pixels
+
+**Ưu điểm (khi có model):**
+- Chất lượng tốt nhất
+- Xử lý large gaps hiệu quả
+- Học global context
+
+**Nhược điểm:**
+- Yêu cầu pretrained model
+- Chậm nhất (GPU recommended)
+- Phức tạp để deploy
+
+**Khi nào dùng:** Khi có GPU và pretrained model, cần chất lượng tối đa
+
+---
+
+### 7. Hybrid (Khuyến nghị)
+
+**Mô tả:** Kết hợp classical + ML để cân bằng tốc độ và chất lượng.
+
+**Quy trình:**
+1. Temporal interpolation (nhanh)
+2. Check remaining NaN percentage
+3. Nếu > 5%: Apply ML KNN inpainting
+4. Nếu <= 5%: Apply spatial interpolation
+5. Fallback fillna(0)
+
+**Ưu điểm:**
+- Cân bằng tốc độ và chất lượng
+- Adaptive - chỉ dùng ML khi cần
+- Hiệu quả với mọi cloud coverage
+
+**Nhược điểm:**
+- Phức tạp hơn classic
+- Khó debug
+
+**Khi nào dùng:** **Khuyến nghị cho production** - tự động chọn strategy phù hợp
+
+---
+
+## So sánh Performance
+
+| Method | Tốc độ | Chất lượng | RAM | Yêu cầu |
+|--------|--------|------------|-----|---------|
+| classic | ⭐⭐⭐⭐ | ⭐⭐⭐ | Thấp | Không |
+| temporal_only | ⭐⭐⭐⭐⭐ | ⭐⭐ | Thấp | Nhiều scenes |
+| median_composite | ⭐⭐⭐ | ⭐⭐⭐⭐ | Thấp | >= 3 scenes |
+| ml_knn | ⭐⭐ | ⭐⭐⭐⭐ | Trung bình | Đủ valid pixels |
+| ml_rf | ⭐ | ⭐⭐⭐⭐⭐ | Cao | Đủ valid pixels |
+| deep | ⭐ | ⭐⭐⭐⭐⭐ | Rất cao | Pretrained model + GPU |
+| hybrid | ⭐⭐⭐ | ⭐⭐⭐⭐ | Trung bình | Không |
+
+---
+
+## Phát hiện mây (SCL)
+
+Tất cả methods đều sử dụng SCL (Scene Classification Layer):
+
+```python
+# SCL values:
+# 0: No data, 1: Saturated/Defective, 2: Dark Area Pixels
+# 3: Cloud shadows, 4: Vegetation, 5: Not vegetated, 6: Water
+# 7: Unclassified, 8: Cloud medium probability, 9: Cloud high probability
+# 10: Thin cirrus, 11: Snow/Ice
+
+cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10) | (scl == 11)
+invalid_mask = (scl == 0) | (scl == 1)
+full_mask = cloud_mask | invalid_mask
+```
+
+---
+
+## Testing & Comparison
+
+So sánh nhiều methods trên cùng dữ liệu:
+
+```python
+from cloud_removal import compare_methods
+
+results = compare_methods(
+ s2_data=s2_data,
+ methods=["classic", "temporal_only", "ml_knn", "hybrid"]
+)
+
+for method, result in results.items():
+ print(f"{method}: {result['remaining_nan_percent']:.2f}% NaN remaining")
+```
+
+---
+
+## Khuyến nghị sử dụng
+
+### Production (General)
+```
+cloud_removal_method: "hybrid"
+```
+- Cân bằng tốc độ và chất lượng
+- Adaptive theo cloud coverage
+
+### High Quality (Research)
+```
+cloud_removal_method: "ml_rf"
+```
+- Chất lượng tối đa
+- Chấp nhận tốc độ chậm
+
+### Fast Processing (Monitoring)
+```
+cloud_removal_method: "temporal_only"
+```
+- Cần nhiều scenes (>10)
+- Ưu tiên tốc độ
+
+### Low Cloud Coverage (<10%)
+```
+cloud_removal_method: "classic"
+```
+- Đơn giản, hiệu quả
+- Ổn định, đã test kỹ
+
+---
+
+## Vị trí code
+
+- **Module:** `cloud_removal.py` - Standalone cloud removal module
+- **API Integration:** `api_server.py` - API endpoints và config
+- **Documentation:** `CLOUD_PROCESSING.md` - Tài liệu này
+
+---
+
+## Phát triển tiếp
+
+- [ ] Implement CNN autoencoder cho deep inpainting
+- [ ] Add quality scoring system
+- [ ] Optimize ML methods với Dask
+- [ ] Add weighted temporal interpolation
+- [ ] Support custom ML models
+
diff --git a/CLOUD_REMOVAL_UPLOAD_GUIDE.md b/CLOUD_REMOVAL_UPLOAD_GUIDE.md
new file mode 100644
index 0000000..bf0d48a
--- /dev/null
+++ b/CLOUD_REMOVAL_UPLOAD_GUIDE.md
@@ -0,0 +1,200 @@
+# Cloud Removal Model Upload Feature
+
+## Overview
+Added functionality to upload and use custom deep learning cloud removal models (.pth files) during prediction.
+
+## Features Implemented
+
+### 1. API Endpoints
+
+#### Upload Cloud Removal Model
+```
+POST /api/cloud-removal/upload
+```
+- Upload `.pth` cloud removal model files
+- Validates file extension (.pth only)
+- Security checks for filename
+- Returns file info (name, size)
+
+**Example:**
+```bash
+curl -X POST -F "file=@cloud_removal_unet_best.pth" \
+ http://localhost:8000/api/cloud-removal/upload
+```
+
+#### List Cloud Removal Models
+```
+GET /api/cloud-removal/models
+```
+Already existing - lists all `.pth` models in `model_train/` directory
+
+#### Delete Cloud Removal Model
+```
+DELETE /api/cloud-removal/models/{filename}
+```
+Already existing - deletes a specific cloud removal model
+
+### 2. Prediction Configuration Updates
+
+#### PredictionConfig
+Added new optional field:
+```python
+cloud_removal_model: Optional[str] = None # .pth filename
+```
+
+#### PredictionWithNDVIConfig
+Added new optional field:
+```python
+cloud_removal_model: Optional[str] = None # .pth filename
+```
+
+### 3. Prediction Function Integration
+
+The `run_prediction()` function now:
+1. Accepts `cloud_removal_model` parameter
+2. Passes model path to `process_cloud_removal()`
+3. Logs which model is being used
+
+**Code:**
+```python
+cloud_removal_method = config.cloud_removal_method
+cloud_removal_model = config.cloud_removal_model
+
+s2_data, cloud_metadata = process_cloud_removal(
+ s2_data=s2_data,
+ method=cloud_removal_method,
+ model_path=f"model_train/{cloud_removal_model}" if cloud_removal_model else None,
+ verbose=True
+)
+```
+
+### 4. Web Interface Updates
+
+#### Upload Button
+- Added file input in "Deep Learning" cloud removal section
+- Upload button appears when "Deep Learning" method is selected
+- Real-time upload status feedback
+- Auto-refreshes model list after successful upload
+
+#### Model Selection
+- Dropdown shows all available `.pth` models
+- Auto-selects newly uploaded model
+- Shows model metadata (epoch, loss)
+
+## Usage Guide
+
+### Step 1: Train or Obtain a Cloud Removal Model
+Train using the cloud training interface or obtain a pre-trained `.pth` model.
+
+### Step 2: Upload Model
+1. Go to Prediction Interface
+2. Scroll to "Cloud Removal Method" section
+3. Select "Deep Learning (U-Net)" from dropdown
+4. Model upload section appears
+5. Click "📤 Upload Cloud Removal Model (.pth)"
+6. Select your `.pth` file
+7. Wait for upload confirmation
+
+### Step 3: Use Model in Prediction
+1. The uploaded model is automatically selected
+2. Configure other prediction parameters (bbox, dates, etc.)
+3. Click "🚀 Start Prediction (với NDVI)"
+4. The system will use your custom model for cloud removal
+
+## File Structure
+```
+model_train/
+├── cloud_removal_unet_best.pth # User uploaded
+├── cloud_removal_unet_epoch_10.pth # User uploaded
+├── model_mobilenet-lraspp_*.joblib # Land classification models
+└── ...
+```
+
+## API Request Example
+
+### Using Uploaded Model
+```json
+{
+ "model_filename": "model_mobilenet-lraspp_20260105_225459.joblib",
+ "min_lon": 105.80,
+ "min_lat": 10.00,
+ "max_lon": 105.82,
+ "max_lat": 10.02,
+ "start_date": "2024-01-15",
+ "end_date": "2024-01-17",
+ "max_scenes": 3,
+ "cloud_cover": 30,
+ "resolution": 20,
+ "use_gpu": true,
+ "export_ndvi": true,
+ "export_classification": true,
+ "cloud_removal_method": "deep",
+ "cloud_removal_model": "cloud_removal_unet_best.pth"
+}
+```
+
+### Without Custom Model (Classical Methods)
+```json
+{
+ ...
+ "cloud_removal_method": "hybrid",
+ "cloud_removal_model": null
+}
+```
+
+## Security Features
+- Filename validation (no path traversal)
+- File extension validation (.pth only)
+- File existence checks
+- Duplicate filename detection
+
+## Error Handling
+- Invalid file type → 400 Bad Request
+- Duplicate filename → 400 Bad Request
+- Upload failure → 500 Internal Server Error
+- Missing model when "deep" selected → Falls back to "hybrid" method
+
+## Notes
+- Uploaded models are stored in `model_train/` directory
+- Models must be PyTorch `.pth` files
+- Compatible with `cloud_removal.py` module
+- Works with both `/api/prediction/start` and `/api/predict/with-ndvi` endpoints
+
+## Testing
+
+### Test Upload
+```bash
+# Upload a model
+curl -X POST -F "file=@my_cloud_model.pth" \
+ http://localhost:8000/api/cloud-removal/upload
+
+# List models
+curl http://localhost:8000/api/cloud-removal/models
+
+# Delete model
+curl -X DELETE \
+ http://localhost:8000/api/cloud-removal/models/my_cloud_model.pth
+```
+
+### Test Prediction
+```bash
+curl -X POST http://localhost:8000/api/predict/with-ndvi \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model_filename": "model_mobilenet-lraspp_20260105_225459.joblib",
+ "min_lon": 105.80, "min_lat": 10.00,
+ "max_lon": 105.82, "max_lat": 10.02,
+ "start_date": "2024-01-15", "end_date": "2024-01-17",
+ "max_scenes": 2, "cloud_cover": 30, "resolution": 20,
+ "use_gpu": false, "export_ndvi": true,
+ "cloud_removal_method": "deep",
+ "cloud_removal_model": "cloud_removal_unet_best.pth"
+ }'
+```
+
+## Future Enhancements
+- Model metadata display (architecture, training date)
+- Model validation on upload
+- Multiple model format support (.pt, .onnx)
+- Model performance metrics
+- Batch upload support
diff --git a/CLOUD_TRAINING_GUIDE.md b/CLOUD_TRAINING_GUIDE.md
new file mode 100644
index 0000000..6888af0
--- /dev/null
+++ b/CLOUD_TRAINING_GUIDE.md
@@ -0,0 +1,227 @@
+# Cloud Removal Training với SEN12MS-CR Dataset
+
+Hướng dẫn train Deep Learning model để khử mây từ ảnh Sentinel-2 sử dụng dataset SEN12MS-CR.
+
+## 📂 Cấu trúc dữ liệu
+
+```
+winter_dataset/
+├── ROIs2017_winter_s1/ # Sentinel-1 SAR data (VV, VH)
+│ ├── s1_8/
+│ ├── s1_9/
+│ └── ...
+├── ROIs2017_winter_s2/ # Sentinel-2 CLEAN (ground truth)
+│ ├── s2_8/
+│ ├── s2_9/
+│ └── ...
+├── ROIs2017_winter_s2_cloudy/ # Sentinel-2 CLOUDY (input)
+│ ├── s2_cloudy_8/
+│ ├── s2_cloudy_9/
+│ └── ...
+└── sen12ms_cr_dataLoader.py # Data loader
+```
+
+## 🚀 Quick Start
+
+### 1. Training Model
+
+```bash
+# Activate environment
+conda activate env_01
+
+# Train cloud removal model
+python train_cloud_removal.py
+```
+
+**Hyperparameters mặc định:**
+- Use S1: `True` (sử dụng radar data)
+- Batch size: `8`
+- Epochs: `50`
+- Learning rate: `1e-4`
+- Model: U-Net
+- Loss: MAE (L1 Loss)
+
+### 2. Test Training (Quick)
+
+```bash
+# Test với 5 epochs
+python test_cloud_training.py
+```
+
+### 3. Sử dụng Model đã train
+
+```python
+from cloud_removal import process_cloud_removal
+
+# Load Sentinel-2 data
+s2_data = load(...) # Your S2 data with SCL band
+
+# Apply deep learning cloud removal
+cleaned_data, metadata = process_cloud_removal(
+ s2_data=s2_data,
+ method="deep", # Use deep learning method
+ verbose=True
+)
+```
+
+## 🎯 Model Architecture
+
+**U-Net** với cấu trúc:
+- **Input:** S2 cloudy (4 bands: B02, B03, B04, B08) + S1 (2 bands: VV, VH) = 6 channels
+- **Output:** S2 clean (4 bands) = 4 channels
+- **Features:** [64, 128, 256, 512]
+- **Skip connections:** Encoder → Decoder
+- **Activation:** ReLU + BatchNorm
+
+## 📊 Dataset Info
+
+**SEN12MS-CR** (Sentinel-12 Multi-Seasonal Cloud Removal):
+- **Scenes:** ~2000+ patches
+- **Size:** 256x256 pixels
+- **Bands:**
+ - S1: VV, VH (2 channels)
+ - S2: 13 bands (chọn B02, B03, B04, B08 cho training)
+- **Seasons:** Spring, Summer, Fall, Winter
+- **Source:** [https://github.com/PatrickTUM/SEN12MS-CR](https://github.com/PatrickTUM/SEN12MS-CR)
+
+## 🔧 Customization
+
+### Thay đổi hyperparameters
+
+```python
+from train_cloud_removal import train_cloud_removal_model
+
+model, train_losses, val_losses = train_cloud_removal_model(
+ data_dir="winter_dataset",
+ use_s1=True, # Có dùng S1 không
+ batch_size=16, # Tăng nếu có GPU mạnh
+ num_epochs=100, # Số epochs
+ learning_rate=5e-5, # Learning rate
+ device="cuda", # "cuda" hoặc "cpu"
+ save_dir="model_train" # Thư mục lưu model
+)
+```
+
+### Chỉ dùng S2 (không dùng S1)
+
+```python
+model, train_losses, val_losses = train_cloud_removal_model(
+ use_s1=False, # Không dùng radar data
+ # ... other params
+)
+```
+
+### Thay đổi S2 bands
+
+Sửa trong `train_cloud_removal.py`:
+
+```python
+# Thay vì RGB + NIR
+s2_bands = [S2Bands.B02, S2Bands.B03, S2Bands.B04, S2Bands.B08]
+
+# Có thể dùng tất cả bands
+s2_bands = S2Bands.ALL
+```
+
+## 📈 Monitoring Training
+
+Model tự động lưu:
+- **Best model:** `model_train/cloud_removal_unet_best.pth`
+- **Training curves:** `model_train/training_curves.png`
+- **Visualizations:** `model_train/cloud_removal_epoch_*.png` (mỗi 10 epochs)
+
+## 🌐 Tích hợp vào API
+
+Model đã được tích hợp vào `cloud_removal.py`:
+
+```python
+# API endpoint
+GET /api/cloud-removal/methods
+
+# Response
+{
+ "methods": {
+ "deep": "Deep Learning U-Net inpainting (best quality, requires model)"
+ }
+}
+```
+
+Sử dụng trong prediction:
+
+```json
+{
+ "model_filename": "model_odc.joblib",
+ "cloud_removal_method": "deep",
+ "..."
+}
+```
+
+## 📝 Notes
+
+### GPU Requirements
+- **Recommended:** NVIDIA GPU với >= 6GB VRAM
+- **Minimum:** CPU (chậm hơn ~10x)
+
+### Training Time
+- **GPU (RTX 3060):** ~2-3 hours cho 50 epochs
+- **CPU:** ~20-30 hours cho 50 epochs
+
+### Data Download
+Nếu chưa có dữ liệu, download từ:
+```bash
+# Download SEN12MS-CR dataset
+wget https://mediatum.ub.tum.de/download/1554803/1554803.zip
+unzip 1554803.zip -d winter_dataset/
+```
+
+## 🐛 Troubleshooting
+
+### 1. CUDA out of memory
+```python
+# Giảm batch size
+batch_size=4 # hoặc 2
+```
+
+### 2. Import error
+```bash
+# Kiểm tra dependencies
+pip install torch torchvision tqdm matplotlib
+```
+
+### 3. Model không load được
+```python
+# Kiểm tra path
+model_path = "model_train/cloud_removal_unet_best.pth"
+assert Path(model_path).exists()
+```
+
+## 📚 References
+
+- **Paper:** SEN12MS-CR: A Dataset for Cloud Removal in Sentinel-2 Imagery
+- **GitHub:** https://github.com/PatrickTUM/SEN12MS-CR
+- **U-Net:** Ronneberger et al., "U-Net: Convolutional Networks for Biomedical Image Segmentation"
+
+## ✅ Checklist
+
+- [x] Data loader cho SEN12MS-CR
+- [x] U-Net architecture
+- [x] Training script
+- [x] Visualization
+- [x] Model saving/loading
+- [x] Tích hợp vào cloud_removal.py
+- [x] API integration
+- [x] Test script
+- [x] Documentation
+
+## 🎓 Next Steps
+
+1. **Train model:** `python train_cloud_removal.py`
+2. **Evaluate:** Xem visualizations trong `model_train/`
+3. **Test inference:** Dùng `test_cloud_removal.py`
+4. **Deploy:** Model tự động được dùng khi chọn `cloud_removal_method="deep"`
+
+---
+
+**Tác giả:** AI Assistant
+**Ngày tạo:** 2026-01-21
+**Version:** 1.0
diff --git a/PLANETARY_COMPUTER_TIPS.md b/PLANETARY_COMPUTER_TIPS.md
new file mode 100644
index 0000000..09e3458
--- /dev/null
+++ b/PLANETARY_COMPUTER_TIPS.md
@@ -0,0 +1,204 @@
+# Microsoft Planetary Computer - Giải pháp Timeout
+
+## ❌ Vấn đề
+```
+The request exceeded the maximum allowed time
+```
+
+## ✅ Giải pháp
+
+### 1. **Giảm Parameters** (Quan trọng nhất)
+
+**Thử theo thứ tự:**
+
+```python
+# ❌ QUÁ LỚN - Dễ timeout
+bbox = [105.48, 9.77, 106.14, 10.35] # ~70km x 60km
+start_date = "2023-01-01"
+end_date = "2023-12-31" # 12 months
+max_scenes = 12
+```
+
+```python
+# ✅ VỪA PHẢI - Tốt
+bbox = [105.8, 10.0, 105.9, 10.1] # ~10km x 10km
+start_date = "2024-01-01"
+end_date = "2024-01-31" # 1 month
+max_scenes = 5
+```
+
+```python
+# ✅ RẤT NHỎ - Luôn work
+bbox = [105.85, 10.05, 105.87, 10.07] # ~2km x 2km
+start_date = "2024-01-15"
+end_date = "2024-01-22" # 1 week
+max_scenes = 3
+```
+
+### 2. **Chiến lược Progressive Loading**
+
+Thay vì load toàn bộ vùng lớn 1 lúc, chia nhỏ:
+
+```python
+# Ví dụ: Chia bbox lớn thành 4 phần nhỏ
+original_bbox = [105.48, 9.77, 106.14, 10.35]
+
+# Tính mid points
+min_lon, min_lat, max_lon, max_lat = original_bbox
+mid_lon = (min_lon + max_lon) / 2
+mid_lat = (min_lat + max_lat) / 2
+
+# 4 sub-regions
+sub_regions = [
+ [min_lon, min_lat, mid_lon, mid_lat], # Bottom-left
+ [mid_lon, min_lat, max_lon, mid_lat], # Bottom-right
+ [min_lon, mid_lat, mid_lon, max_lat], # Top-left
+ [mid_lon, mid_lat, max_lon, max_lat], # Top-right
+]
+
+# Load từng region riêng, sau đó merge
+```
+
+### 3. **Tăng Timeout trong Code**
+
+Sửa `fetch_sentinel_items_with_retry`:
+
+```python
+# Thử với timeout dài hơn và ít items hơn
+for attempt in range(max_retries):
+ try:
+ # Giảm target xuống còn 2-3 items cho lần đầu
+ target_items = min(3, max_scenes) if attempt == 0 else 2
+
+ search = catalog.search(
+ collections=["sentinel-2-l2a"],
+ bbox=bbox,
+ datetime=time_range,
+ query={"eo:cloud_cover": {"lt": cloud_cover}},
+ limit=10 # Giảm từ 20-50 xuống 10
+ )
+
+ # Set timeout cho iterator
+ items = []
+ import signal
+
+ def timeout_handler(signum, frame):
+ raise TimeoutError("Item fetch timeout")
+
+ signal.signal(signal.SIGALRM, timeout_handler)
+ signal.alarm(30) # 30 giây timeout
+
+ try:
+ for item in search.items():
+ items.append(item)
+ if len(items) >= target_items:
+ break
+ finally:
+ signal.alarm(0) # Cancel alarm
+```
+
+### 4. **Alternative: Dùng Dữ liệu Local**
+
+Nếu Planetary Computer liên tục timeout:
+
+#### **a) Download trước (Recommended)**
+
+```bash
+# Dùng sentinelsat để download
+pip install sentinelsat
+
+# Download Sentinel-2 về máy
+python download_sentinel2.py --bbox 105.8,10.0,105.9,10.1 \
+ --start 2024-01-01 --end 2024-01-31
+```
+
+#### **b) Dùng Google Earth Engine** (Nếu có account)
+
+```python
+import ee
+ee.Initialize()
+
+# Load Sentinel-2 từ GEE thay vì Planetary Computer
+image = ee.ImageCollection('COPERNICUS/S2_SR') \
+ .filterBounds(ee.Geometry.Rectangle(bbox)) \
+ .filterDate(start_date, end_date) \
+ .median()
+```
+
+### 5. **Cache Aggressive**
+
+Khi đã load được data, cache ngay:
+
+```python
+# Trong prediction interface, enable cache by default
+use_cache = True # ALWAYS
+
+# Khi load thành công, lưu cache ngay
+if items and len(items) > 0:
+ cache_file = f"cache_{bbox_hash}_{date_hash}.joblib"
+ joblib.dump({
+ 'items': items,
+ 's2_data': s2_data,
+ 'timestamp': datetime.now()
+ }, cache_file)
+```
+
+## 🎯 **Action Plan Ngay Bây Giờ**
+
+### **Bước 1: Test với bbox CỰC NHỎ**
+
+Web interface → Prediction:
+- Min Lon: **105.80**
+- Min Lat: **10.00**
+- Max Lon: **105.82** (chỉ 0.02 độ = ~2km)
+- Max Lat: **10.02**
+- Start: **2024-01-15**
+- End: **2024-01-17** (3 ngày)
+- Max Scenes: **2**
+- Cloud Cover: 50%
+
+→ Nếu vẫn timeout → Vấn đề là internet/firewall/server PC quá tải
+
+### **Bước 2: Nếu Step 1 OK → Tăng dần**
+
+- Tăng bbox lên 0.05 độ (~5km)
+- Tăng time range lên 1 tuần
+- Tăng max_scenes lên 5
+
+### **Bước 3: Dùng Batch Processing**
+
+Thay vì 1 query lớn:
+- Chia thành nhiều queries nhỏ
+- Dùng `/api/batch/start`
+- Mỗi job = 1 vùng nhỏ
+- Merge results sau
+
+## 🔧 **Debug Commands**
+
+```bash
+# Check internet
+ping -c 3 planetarycomputer.microsoft.com
+
+# Check DNS
+nslookup planetarycomputer.microsoft.com
+
+# Test với curl
+curl -I https://planetarycomputer.microsoft.com/api/stac/v1
+
+# Monitor network
+sudo tcpdump -i any host planetarycomputer.microsoft.com
+```
+
+## 📝 **Token Info** (FYI)
+
+Microsoft Planetary Computer **KHÔNG CẦN** manual token:
+- ✅ SAS tokens tự động gen bởi `planetary_computer.sign()`
+- ✅ Auto-refresh khi cần
+- ✅ Không cần API key/registration (public access)
+- ❌ KHÔNG có "hết token" - chỉ có timeout/rate limit
+
+Nếu thấy authentication error:
+```python
+# Cài lại thư viện
+pip install --upgrade planetary-computer pystac-client
+```
diff --git a/api_server.py b/api_server.py
index b1bd909..73ab2ac 100644
--- a/api_server.py
+++ b/api_server.py
@@ -3,7 +3,7 @@ API Server for Land Classification Model Training
Cho phép chọn dữ liệu và cấu hình training qua giao diện web
"""
-from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File
+from fastapi import FastAPI, BackgroundTasks, HTTPException, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse
@@ -38,6 +38,9 @@ from vietnam_provinces_merged import (
search_province_32, get_merged_info, get_provinces_statistics
)
+# Import cloud removal module
+from cloud_removal import process_cloud_removal, get_available_methods
+
# Import planetary computer libraries (conditional)
try:
from pystac_client import Client
@@ -217,6 +220,10 @@ class PredictionConfig(BaseModel):
# GPU support for deep learning models
use_gpu: bool
+
+ # Cloud removal strategy
+ cloud_removal_method: str = "classic"
+ cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
class TrainingStatus(BaseModel):
@@ -236,6 +243,7 @@ class NDVIConfig(BaseModel):
end_date: str
max_cloud_cover: int = 30
resolution: int = 20
+ cloud_removal_method: str = "classic"
class ChangeDetectionWorkflowRequest(BaseModel):
@@ -258,6 +266,7 @@ class ComparePeriodsPredictionConfig(BaseModel):
resolution: int = 20
export_ndvi: bool = True
export_classification: bool = True
+ cloud_removal_method: str = "classic"
class PredictionWithNDVIConfig(BaseModel):
@@ -275,6 +284,19 @@ class PredictionWithNDVIConfig(BaseModel):
use_gpu: bool = False # Use GPU for deep learning models
export_ndvi: bool = True # Export NDVI raster
export_classification: bool = True # Export classification raster
+ cloud_removal_method: str = "classic"
+ cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
+
+
+class CloudRemovalTrainingConfig(BaseModel):
+ """Cấu hình train cloud removal model"""
+ data_dir: str = "winter_dataset"
+ use_s1: bool = True # Sử dụng Sentinel-1 radar data
+ batch_size: int = 8
+ num_epochs: int = 50
+ learning_rate: float = 1e-4
+ use_gpu: bool = True
+ model_name: str = "cloud_removal_unet" # Tên model để lưu
# Serve change detection interface page (moved here after app is defined)
@@ -356,6 +378,16 @@ async def training_page():
raise HTTPException(status_code=404, detail="Training interface không tồn tại")
+@app.get("/cloud-training", response_class=HTMLResponse)
+async def cloud_training_page():
+ """Serve cloud removal training interface"""
+ html_file = Path(__file__).parent / "cloud_training_interface.html"
+ if html_file.exists():
+ return FileResponse(html_file)
+ else:
+ raise HTTPException(status_code=404, detail="Cloud training interface không tồn tại")
+
+
@app.get("/prediction", response_class=HTMLResponse)
async def prediction_page():
"""Serve prediction interface"""
@@ -463,6 +495,298 @@ async def validate_model(model_filename: str):
}
+@app.get("/api/cloud-removal/methods")
+async def get_cloud_removal_methods():
+ """Lấy danh sách các phương pháp xử lý mây có sẵn"""
+ methods = get_available_methods()
+ return {
+ "success": True,
+ "methods": methods,
+ "default": "classic",
+ "description": "Cloud removal strategies for Sentinel-2 data processing"
+ }
+
+
+@app.get("/api/cloud-removal/models")
+async def list_cloud_removal_models():
+ """Liệt kê các cloud removal models đã train"""
+ model_dir = Path("model_train")
+ if not model_dir.exists():
+ return {"models": [], "count": 0}
+
+ models = []
+ # Search for ALL .pth files in model_train and subdirectories
+ for model_file in model_dir.rglob("*.pth"):
+ # Skip non-cloud-removal models (keep land classification models separate)
+ if any(x in model_file.name.lower() for x in ['mobilenet', 'cnn_', 'swin', 'xgboost', 'random_forest']):
+ continue
+
+ try:
+ import torch
+ import json
+
+ # Try to load metadata from .json sidecar file first
+ metadata_file = model_file.with_suffix('.json')
+ if metadata_file.exists():
+ try:
+ with open(metadata_file, 'r') as f:
+ metadata = json.load(f)
+
+ models.append({
+ "filename": model_file.name,
+ "path": str(model_file),
+ "relative_path": str(model_file.relative_to(model_dir)),
+ "epoch": metadata.get('epoch', 0),
+ "train_loss": metadata.get('train_loss', 0),
+ "val_loss": metadata.get('val_loss', 0),
+ "use_s1": metadata.get('use_s1', True),
+ "in_channels": metadata.get('in_channels', 6),
+ "out_channels": metadata.get('out_channels', 4),
+ "description": metadata.get('description', ''),
+ "created": model_file.stat().st_mtime,
+ "size_mb": model_file.stat().st_size / (1024 * 1024),
+ "has_metadata": True
+ })
+ continue
+ except Exception as e:
+ print(f"[Cloud Models] Failed to read metadata file {metadata_file}: {e}")
+
+ # Try to load checkpoint metadata from .pth file
+ try:
+ checkpoint = torch.load(model_file, map_location='cpu')
+ epoch = checkpoint.get('epoch', 0) if isinstance(checkpoint, dict) else 0
+ train_loss = checkpoint.get('train_loss', 0) if isinstance(checkpoint, dict) else 0
+ val_loss = checkpoint.get('val_loss', 0) if isinstance(checkpoint, dict) else 0
+ use_s1 = checkpoint.get('use_s1', True) if isinstance(checkpoint, dict) else True
+ in_channels = checkpoint.get('in_channels', 6) if isinstance(checkpoint, dict) else 6
+ out_channels = checkpoint.get('out_channels', 4) if isinstance(checkpoint, dict) else 4
+ except:
+ # If checkpoint format is different or corrupted, use defaults
+ epoch = 0
+ train_loss = 0
+ val_loss = 0
+ use_s1 = True
+ in_channels = 3
+ out_channels = 3
+
+ models.append({
+ "filename": model_file.name,
+ "path": str(model_file),
+ "relative_path": str(model_file.relative_to(model_dir)),
+ "epoch": epoch,
+ "train_loss": train_loss,
+ "val_loss": val_loss,
+ "use_s1": use_s1,
+ "in_channels": in_channels,
+ "out_channels": out_channels,
+ "description": "",
+ "created": model_file.stat().st_mtime,
+ "size_mb": model_file.stat().st_size / (1024 * 1024),
+ "has_metadata": False
+ })
+ except Exception as e:
+ print(f"[Cloud Models] Error loading {model_file}: {e}")
+ # Still add the file even if we can't load metadata
+ models.append({
+ "filename": model_file.name,
+ "path": str(model_file),
+ "relative_path": str(model_file.relative_to(model_dir)),
+ "epoch": 0,
+ "train_loss": 0,
+ "val_loss": 0,
+ "use_s1": False,
+ "in_channels": 3,
+ "out_channels": 3,
+ "description": "",
+ "created": model_file.stat().st_mtime,
+ "size_mb": model_file.stat().st_size / (1024 * 1024),
+ "has_metadata": False
+ })
+
+ models.sort(key=lambda x: x['created'], reverse=True)
+ return {"models": models, "count": len(models)}
+
+
+@app.post("/api/cloud-removal/train")
+async def train_cloud_removal(config: CloudRemovalTrainingConfig, background_tasks: BackgroundTasks):
+ """Bắt đầu train cloud removal model"""
+
+ # Check if data directory exists
+ data_dir = Path(config.data_dir)
+ if not data_dir.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Data directory not found: {config.data_dir}"
+ )
+
+ # Create status tracking
+ training_id = datetime.now().strftime("%Y%m%d_%H%M%S")
+
+ async def run_cloud_training():
+ try:
+ from train_cloud_removal import train_cloud_removal_model
+
+ print(f"[CLOUD REMOVAL TRAINING] Starting training {training_id}")
+
+ model, train_losses, val_losses = train_cloud_removal_model(
+ data_dir=config.data_dir,
+ use_s1=config.use_s1,
+ batch_size=config.batch_size,
+ num_epochs=config.num_epochs,
+ learning_rate=config.learning_rate,
+ device="cuda" if config.use_gpu else "cpu",
+ save_dir="model_train"
+ )
+
+ print(f"[CLOUD REMOVAL TRAINING] Completed {training_id}")
+
+ return {
+ "success": True,
+ "training_id": training_id,
+ "final_train_loss": train_losses[-1],
+ "final_val_loss": val_losses[-1],
+ "epochs": len(train_losses)
+ }
+
+ except Exception as e:
+ print(f"[CLOUD REMOVAL TRAINING ERROR] {e}")
+ import traceback
+ traceback.print_exc()
+ return {
+ "success": False,
+ "error": str(e),
+ "training_id": training_id
+ }
+
+ # Run in background
+ background_tasks.add_task(run_cloud_training)
+
+ return {
+ "message": "Cloud removal training started",
+ "training_id": training_id,
+ "config": {
+ "data_dir": config.data_dir,
+ "use_s1": config.use_s1,
+ "batch_size": config.batch_size,
+ "num_epochs": config.num_epochs,
+ "learning_rate": config.learning_rate,
+ "use_gpu": config.use_gpu
+ }
+ }
+
+
+@app.post("/api/cloud-removal/upload")
+async def upload_cloud_removal_model(
+ file: UploadFile = File(...),
+ epoch: int = Form(0),
+ train_loss: float = Form(0.0),
+ val_loss: float = Form(0.0),
+ in_channels: int = Form(6),
+ out_channels: int = Form(4),
+ use_s1: bool = Form(True),
+ description: str = Form("")
+):
+ """Upload cloud removal .pth model with optional metadata"""
+
+ # Debug logging
+ print(f"[Upload] Received parameters:")
+ print(f" File: {file.filename}")
+ print(f" Epoch: {epoch} (type: {type(epoch)})")
+ print(f" Train Loss: {train_loss} (type: {type(train_loss)})")
+ print(f" Val Loss: {val_loss} (type: {type(val_loss)})")
+ print(f" In Channels: {in_channels} (type: {type(in_channels)})")
+ print(f" Out Channels: {out_channels} (type: {type(out_channels)})")
+ print(f" Use S1: {use_s1} (type: {type(use_s1)})")
+ print(f" Description: {description}")
+
+ # Validate file extension
+ if not file.filename.endswith('.pth'):
+ raise HTTPException(status_code=400, detail="Only .pth files are allowed")
+
+ # Security check
+ if ".." in file.filename or "/" in file.filename or "\\" in file.filename:
+ raise HTTPException(status_code=400, detail="Invalid filename")
+
+ try:
+ model_dir = Path("model_train")
+ model_dir.mkdir(exist_ok=True)
+
+ # Save uploaded file
+ file_path = model_dir / file.filename
+
+ # Check if file already exists
+ if file_path.exists():
+ raise HTTPException(status_code=400, detail=f"Model {file.filename} already exists")
+
+ # Write file
+ with open(file_path, "wb") as f:
+ content = await file.read()
+ f.write(content)
+
+ file_size = file_path.stat().st_size
+
+ # Save metadata as JSON sidecar file
+ import json
+ metadata_file = file_path.with_suffix('.json')
+
+ metadata_dict = {
+ "filename": file.filename,
+ "epoch": epoch,
+ "train_loss": train_loss,
+ "val_loss": val_loss,
+ "in_channels": in_channels,
+ "out_channels": out_channels,
+ "use_s1": use_s1,
+ "description": description,
+ "uploaded_at": datetime.now().isoformat()
+ }
+
+ with open(metadata_file, 'w') as f:
+ json.dump(metadata_dict, f, indent=2)
+
+ print(f"[Upload] Saved model: {file_path}")
+ print(f"[Upload] Saved metadata: {metadata_file}")
+ print(f"[Upload] Metadata: {metadata_dict}")
+
+ return {
+ "message": f"Successfully uploaded {file.filename}",
+ "filename": file.filename,
+ "size_mb": round(file_size / 1024 / 1024, 2),
+ "path": str(file_path),
+ "metadata": metadata_dict
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ print(f"[Upload] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
+
+
+@app.delete("/api/cloud-removal/models/{filename}")
+async def delete_cloud_removal_model(filename: str):
+ """Xóa cloud removal model"""
+ model_dir = Path("model_train")
+ model_path = model_dir / filename
+
+ # Security check
+ if ".." in filename or "/" in filename or "\\" in filename:
+ raise HTTPException(status_code=400, detail="Invalid filename")
+
+ if not model_path.exists():
+ raise HTTPException(status_code=404, detail=f"Model not found: {filename}")
+
+ try:
+ model_path.unlink()
+ return {
+ "success": True,
+ "message": f"Deleted cloud removal model: {filename}"
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to delete: {str(e)}")
+
+
@app.delete("/api/models/{model_filename}")
async def delete_model(model_filename: str):
"""Xóa model"""
@@ -1304,18 +1628,14 @@ async def run_prediction(config: PredictionConfig):
)
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
- s2_search = catalog.search(
- collections=["sentinel-2-l2a"],
+ s2_items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
)
- s2_items = list(s2_search.items())
-
- if not s2_items:
- raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
-
- s2_items = s2_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý dữ liệu Sentinel-2..."
@@ -1417,87 +1737,27 @@ async def run_prediction(config: PredictionConfig):
# ============ ADVANCED CLOUD MASKING & REMOVAL ============
prediction_status["progress"] = "Đang xử lý mây nâng cao..."
- cloud_coverage_percent = 0
- if "SCL" in s2_data:
- scl = s2_data["SCL"]
-
- # SCL classification values (Sentinel-2 Scene Classification):
- # 0: No data, 1: Saturated/Defective, 2: Dark Area Pixels
- # 3: Cloud shadows, 4: Vegetation, 5: Not vegetated, 6: Water
- # 7: Unclassified, 8: Cloud medium probability, 9: Cloud high probability
- # 10: Thin cirrus, 11: Snow/Ice
-
- # Comprehensive cloud mask (clouds, shadows, cirrus, snow)
- cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10) | (scl == 11)
-
- # Also mask no-data and saturated pixels
- invalid_mask = (scl == 0) | (scl == 1)
- full_mask = cloud_mask | invalid_mask
-
- # Calculate cloud coverage percentage
- 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
-
- print(f"[CLOUD MASK] Cloud coverage: {cloud_coverage_percent:.1f}%")
- print(f"[CLOUD MASK] Masked pixels: {masked_pixels}/{total_pixels}")
-
- # Apply mask to all bands
- for band in s2_data.data_vars:
- if band != "SCL":
- s2_data[band] = s2_data[band].where(~full_mask)
-
- # ============ CLOUD REMOVAL STRATEGIES ============
-
- # Strategy 1: Temporal Interpolation (fill gaps between time steps)
- prediction_status["progress"] = "Đang khử mây bằng temporal interpolation..."
- for band in s2_data.data_vars:
- if band != "SCL":
- # Forward fill then backward fill along time dimension
- s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time')
-
- print(f"[CLOUD REMOVAL] Applied temporal interpolation")
-
- # Strategy 2: Median Compositing (if multiple time steps available)
- if len(s2_data.time) >= 3:
- prediction_status["progress"] = "Đang tạo median composite để giảm nhiễu mây..."
-
- # Create median composite for each band
- for band in s2_data.data_vars:
- if band != "SCL":
- # Median reduces cloud noise better than mean
- median_composite = s2_data[band].median(dim='time', skipna=True)
-
- # Fill remaining NaN with median
- s2_data[band] = s2_data[band].fillna(median_composite)
-
- print(f"[CLOUD REMOVAL] Applied median compositing from {len(s2_data.time)} scenes")
-
- # Strategy 3: Spatial Interpolation (fill small gaps)
- prediction_status["progress"] = "Đang khử mây bằng spatial interpolation..."
- for band in s2_data.data_vars:
- if band != "SCL":
- # Use nearest neighbor interpolation for remaining small gaps
- 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')
-
- print(f"[CLOUD REMOVAL] Applied spatial interpolation")
-
- # Final check: replace any remaining NaN with 0
- for band in s2_data.data_vars:
- if band != "SCL":
- s2_data[band] = s2_data[band].fillna(0)
-
- print(f"[CLOUD REMOVAL] Completed - all NaN values handled")
-
- # Quality warning if cloud coverage too high
- if cloud_coverage_percent > 30:
- print(f"[WARNING] High cloud coverage ({cloud_coverage_percent:.1f}%) - prediction quality may be affected")
- prediction_status["progress"] = f"⚠️ Cảnh báo: Độ phủ mây cao ({cloud_coverage_percent:.1f}%)"
-
- else:
- print("[WARNING] No SCL band available - skipping cloud masking")
- prediction_status["progress"] = "⚠️ Không có SCL band - bỏ qua khử mây"
+ # Use cloud removal module with user-selected method
+ cloud_removal_method = config.cloud_removal_method if hasattr(config, 'cloud_removal_method') else "classic"
+ cloud_removal_model = config.cloud_removal_model if hasattr(config, 'cloud_removal_model') else None
+
+ print(f"[CLOUD REMOVAL] Using method: {cloud_removal_method}")
+ if cloud_removal_model:
+ print(f"[CLOUD REMOVAL] Using custom model: {cloud_removal_model}")
+
+ s2_data, cloud_metadata = process_cloud_removal(
+ s2_data=s2_data,
+ method=cloud_removal_method,
+ model_path=f"model_train/{cloud_removal_model}" if cloud_removal_model else None,
+ verbose=True
+ )
+
+ cloud_coverage_percent = cloud_metadata.get('cloud_coverage_percent', 0)
+
+ # Quality warning if cloud coverage too high
+ if cloud_coverage_percent > 30:
+ print(f"[WARNING] High cloud coverage ({cloud_coverage_percent:.1f}%) - prediction quality may be affected")
+ prediction_status["progress"] = f"⚠️ Cảnh báo: Độ phủ mây cao ({cloud_coverage_percent:.1f}%)"
# ============ EXTRACT FEATURES ============
prediction_status["progress"] = f"Đang trích xuất features (mode={feature_mode})..."
@@ -2458,20 +2718,16 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
job["progress"] = 25
- # Search Sentinel-2
- s2_search = catalog.search(
- collections=["sentinel-2-l2a"],
+ # Search Sentinel-2 with retry
+ s2_items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
)
- s2_items = list(s2_search.items())
- if not s2_items:
- raise ValueError("Không tìm thấy dữ liệu Sentinel-2")
-
- s2_items = s2_items[:config.max_scenes]
-
job["progress"] = 35
# Load Sentinel-2 data
@@ -2768,14 +3024,15 @@ async def change_detection_predict_workflow(
modifier=planetary_computer.sign_inplace
)
- search = catalog.search(
- collections=["sentinel-2-l2a"],
+ # Use retry logic for fetching items
+ items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": cloud_cover}}
+ time_range=time_range,
+ cloud_cover=cloud_cover,
+ max_scenes=max_scenes,
+ max_retries=3
)
-
- items = list(search.items())[:max_scenes]
print(f"[CHANGE DETECTION] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -3403,6 +3660,86 @@ async def change_detection_api(
# ============ PREDICTION WITH NDVI API ============
+def fetch_sentinel_items_with_retry(catalog, bbox, time_range, cloud_cover, max_scenes, max_retries=3):
+ """
+ Fetch Sentinel-2 items with retry logic and exponential backoff
+
+ Optimizations:
+ - Reduce page size on retry
+ - Use shorter timeouts for each attempt
+ - Fetch fewer items initially and expand if successful
+ """
+ import time
+
+ for attempt in range(max_retries):
+ try:
+ # Reduce target items on each retry to minimize timeout risk
+ target_items = max_scenes if attempt == 0 else min(max_scenes, 20 // (attempt + 1) * 10)
+ page_limit = 50 if attempt == 0 else 20 # Smaller pages on retry
+
+ print(f"[FETCH ATTEMPT {attempt + 1}/{max_retries}] Searching Sentinel-2...")
+ print(f" → Target items: {target_items}, Page limit: {page_limit}")
+
+ # Search with reduced limit on retries
+ search = catalog.search(
+ collections=["sentinel-2-l2a"],
+ bbox=bbox,
+ datetime=time_range,
+ query={"eo:cloud_cover": {"lt": cloud_cover}},
+ limit=page_limit
+ )
+
+ # Try to get items with timeout protection
+ items = []
+ page_count = 0
+ max_pages = 3 if attempt > 0 else 5 # Fewer pages on retry
+
+ for item in search.items():
+ items.append(item)
+ if len(items) >= target_items:
+ print(f"[FETCH] Reached target ({target_items} items)")
+ break
+
+ # Track pagination to prevent hanging
+ if len(items) % page_limit == 0:
+ page_count += 1
+ if page_count >= max_pages:
+ print(f"[FETCH] Max pages reached ({max_pages}), got {len(items)} items")
+ break
+
+ if items:
+ print(f"[FETCH SUCCESS] Retrieved {len(items)} items")
+ # Return up to max_scenes, but accept fewer if that's all we got
+ return items[:min(len(items), max_scenes)]
+ else:
+ raise ValueError("No Sentinel-2 scenes found for the specified criteria")
+
+ except Exception as e:
+ error_msg = str(e)
+ print(f"[FETCH ERROR] Attempt {attempt + 1} failed: {error_msg}")
+
+ if attempt < max_retries - 1:
+ # Longer exponential backoff: 3, 6, 12 seconds
+ wait_time = 3 * (2 ** attempt)
+ print(f"[RETRY] Waiting {wait_time}s before retry...")
+ time.sleep(wait_time)
+ else:
+ # Final attempt failed
+ if "exceeded the maximum allowed time" in error_msg or "timeout" in error_msg.lower():
+ raise HTTPException(
+ status_code=504,
+ detail=f"Microsoft Planetary Computer request timed out after {max_retries} attempts. "
+ f"Please try: (1) Reduce date range (2) Reduce max_scenes to 5-10 (3) Use smaller bbox area"
+ )
+ elif "no sentinel-2 scenes found" in error_msg.lower():
+ raise HTTPException(
+ status_code=404,
+ detail="No Sentinel-2 data found. Try: (1) Different date range (2) Higher cloud_cover threshold (3) Different location"
+ )
+ else:
+ raise
+
+
@app.post("/api/predict/with-ndvi")
async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks: BackgroundTasks):
"""Predict land classification và NDVI cho một khu vực"""
@@ -3454,15 +3791,15 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
time_range = f"{config.start_date}/{config.end_date}"
- # Search for Sentinel-2 data
- search = catalog.search(
- collections=["sentinel-2-l2a"],
+ # Search for Sentinel-2 data with retry logic
+ items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
)
-
- items = list(search.items())[:config.max_scenes]
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -3546,13 +3883,17 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
modifier=planetary_computer.sign_inplace
)
time_range = f"{config.start_date}/{config.end_date}"
- search = catalog.search(
- collections=["sentinel-2-l2a"],
+
+ # Use retry logic for B11 band
+ b11_items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
)
- signed_items = [planetary_computer.sign(item) for item in list(search.items())[:config.max_scenes]]
+ signed_items = [planetary_computer.sign(item) for item in b11_items]
print(f"[PREDICT+NDVI] Fetched {len(signed_items)} scenes for B11")
b11_data = odc.stac.load(
@@ -3911,7 +4252,10 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
"bbox": bbox,
"change_detection": change_summary
}
-
+
+ except HTTPException:
+ # Re-raise HTTPException with original status code (e.g., 504 for timeout)
+ raise
except Exception as e:
print(f"[PREDICT+NDVI ERROR] {str(e)}")
import traceback
@@ -3942,15 +4286,15 @@ async def calculate_ndvi_timeseries(config: NDVIConfig):
bbox = config.bbox
time_range = f"{config.start_date}/{config.end_date}"
- # Search for Sentinel-2 data
- search = catalog.search(
- collections=["sentinel-2-l2a"],
+ # Search for Sentinel-2 data with retry logic
+ items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.max_cloud_cover,
+ max_scenes=1000, # Get all available scenes for time series
+ max_retries=3
)
-
- items = list(search.items())
print(f"[NDVI] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0:
@@ -4137,20 +4481,15 @@ async def ndvi_predict_timeseries(config: NDVIPredictionConfig):
modifier=planetary_computer.sign_inplace,
)
- s2_search = catalog.search(
- collections=["sentinel-2-l2a"],
+ # Search for Sentinel-2 data with retry
+ s2_items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
+ time_range=time_range,
+ cloud_cover=config.max_cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
)
- s2_items = list(s2_search.items())
-
- if not s2_items:
- raise HTTPException(status_code=404, detail="No Sentinel-2 data found")
-
- # Limit scenes to max_scenes
- if len(s2_items) > config.max_scenes:
- s2_items = s2_items[:config.max_scenes]
print(f"✅ Found {len(s2_items)} Sentinel-2 scenes (limited to {config.max_scenes})")
@@ -4623,15 +4962,18 @@ async def ndvi_forecast(config: NDVIForecastConfig):
modifier=planetary_computer.sign_inplace,
)
- s2_search = catalog.search(
- collections=["sentinel-2-l2a"],
- bbox=bbox,
- datetime=time_range,
- query={"eo:cloud_cover": {"lt": config.max_cloud_cover}}
- )
- s2_items = list(s2_search.items())
-
- if not s2_items:
+ # Search for historical Sentinel-2 data with retry
+ try:
+ s2_items = fetch_sentinel_items_with_retry(
+ catalog=catalog,
+ bbox=bbox,
+ time_range=time_range,
+ cloud_cover=config.max_cloud_cover,
+ max_scenes=config.max_scenes,
+ max_retries=3
+ )
+ except ValueError:
+ # No items found - provide helpful error message
raise HTTPException(
status_code=404,
detail=f"⚠️ Không tìm thấy dữ liệu Sentinel-2 cho khu vực này!\n\n"
@@ -4646,8 +4988,7 @@ async def ndvi_forecast(config: NDVIForecastConfig):
f"5. Đảm bảo forecast_start_date không quá xa trong tương lai"
)
- if len(s2_items) > config.max_scenes:
- s2_items = s2_items[:config.max_scenes]
+ print(f"✅ Found {len(s2_items)} historical Sentinel-2 scenes")
print(f"✅ Found {len(s2_items)} historical scenes")
diff --git a/cloud_removal.py b/cloud_removal.py
new file mode 100644
index 0000000..5191e28
--- /dev/null
+++ b/cloud_removal.py
@@ -0,0 +1,628 @@
+"""
+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
diff --git a/cloud_removal_train.ipynb b/cloud_removal_train.ipynb
new file mode 100644
index 0000000..21c2679
--- /dev/null
+++ b/cloud_removal_train.ipynb
@@ -0,0 +1,10 @@
+{
+ "cells": [],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/cloud_training_interface.html b/cloud_training_interface.html
new file mode 100644
index 0000000..4144f0e
--- /dev/null
+++ b/cloud_training_interface.html
@@ -0,0 +1,605 @@
+
+
+
+
+
+ Cloud Removal Training - Deep Learning
+
+
+
+
+
+
+
+
+
+
+
+
+ 📚 Dataset: SEN12MS-CR (Sentinel-12 Multi-Seasonal Cloud Removal)
+ 🏗️ Architecture: U-Net với skip connections
+ 📊 Input: S2 cloudy (4 bands) + S1 radar (2 bands) = 6 channels
+ 🎯 Output: S2 clean (4 bands)
+ ⏱️ Training time: ~2-3 hours (GPU) / ~20-30 hours (CPU)
+
+
+
+
+
+
⚙️ Cấu hình Training
+
+
+
+
+
+
+
📊 Training Status
+
+
+
+
+
Training logs will appear here...
+
+
+
+
+
+
+
🤖 Cloud Removal Models
+
+
+
+
+
+
📖 Cloud Removal Methods
+
+
+
🔹 Classic (Default)
+
3-step approach: temporal → median → spatial interpolation
+
Fast
+
+
+
+
🔹 Hybrid
+
Classical + ML KNN - balanced speed & quality
+
Recommended
+
+
+
+
🔹 ML KNN
+
K-Nearest Neighbors inpainting - good quality
+
Medium Speed
+
+
+
+
🔹 Deep Learning
+
U-Net CNN - best quality for large gaps
+
Requires Model
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
index e95b2fc..aec3acc 100644
--- a/index.html
+++ b/index.html
@@ -430,6 +430,7 @@