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 + + + +
+
+

🌥️ Cloud Removal Training

+

Train Deep Learning Models để khử mây từ ảnh Sentinel-2

+
+ + + +
+ +
+
+ 📚 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

+ +
+
+
+
+ + + Thư mục chứa dữ liệu SEN12MS-CR +
+ +
+ + + Tên model để lưu +
+ +
+ + + Giảm xuống 4 hoặc 2 nếu GPU hết RAM +
+ +
+ + + Số lượng epochs training +
+ +
+ + + Learning rate (default: 1e-4) +
+ +
+
+ + +
+ Sử dụng dữ liệu radar (VV, VH) để cải thiện kết quả +
+ +
+
+ + +
+ Sử dụng GPU để training nhanh hơn +
+
+ +
+ + +
+
+
+
+ + + + + +
+

🤖 Cloud Removal Models

+
+
+

Loading 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 @@
🏠 Trang Chủ (Active) 🎓 Training + 🌥️ Cloud Removal 🗺️ Prediction 🚀 Batch Processing 🌿 NDVI Analysis diff --git a/prediction_interface.html b/prediction_interface.html index d22452a..fc66767 100644 --- a/prediction_interface.html +++ b/prediction_interface.html @@ -295,6 +295,7 @@
🏠 Trang Chủ 🎓 Training + 🌥️ Cloud Removal 🗺️ Prediction (Active) 🚀 Batch Processing 🌿 NDVI Analysis @@ -427,6 +428,97 @@
+ +
+ + + + + + +
+ 💡 Hybrid method balances speed and quality. Deep learning provides best results but requires trained model. +
+
+