Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 623bc9c7dd | |||
| 174de1034b | |||
| d1a4534653 | |||
| 57fbdede1b | |||
| c60dea728f | |||
| d96c0c9d69 | |||
| 0d48844822 | |||
| c80b43eeac | |||
| ca71aff891 | |||
| 94eca6bb29 | |||
| c40aafef03 | |||
| 0e8d85d972 | |||
| 208f1fab6e | |||
| b0fba1a5ff | |||
| bb25aa12cc | |||
| 2ffe74357e | |||
| e684d9c5c1 | |||
| bc678f3e6b | |||
| 94ebec25f2 | |||
| 3199ff242a | |||
| 06ef6bee92 | |||
| ad40578284 | |||
| 120cd84b66 | |||
| d2834725a2 | |||
| 3712787689 | |||
| 3783bdf678 | |||
| 2854ad170d | |||
| 3a32ccd90a | |||
| df7294eb1a | |||
| 95ed7cfc08 | |||
| 6c2baeb7d5 | |||
| fb67ca6498 | |||
| 62b07c52e3 | |||
| 44626c7e34 | |||
| 95bfb52281 | |||
| 47d9aa99f3 | |||
| ea2c49104c | |||
| f31982c50a | |||
| ac0a1b6e5f | |||
| 23bc443245 | |||
| 2f49eec082 | |||
| 5daf9882a7 |
+4
-5
@@ -1,5 +1,4 @@
|
||||
# Disabled LFS tracking to avoid pushing large files
|
||||
#*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
#*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||
#*.nc filter=lfs diff=lfs merge=lfs -text
|
||||
#*.ipynb filter=lfs diff=lfs merge=lfs -text
|
||||
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||
*.nc filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipynb filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
+7
-86
@@ -1,86 +1,7 @@
|
||||
# Ignore all model weights and large data
|
||||
*.joblib
|
||||
*.nc
|
||||
*.tif
|
||||
*.tiff
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.h5
|
||||
*.pt
|
||||
*.ckpt
|
||||
*.pb
|
||||
*.npz
|
||||
*.npy
|
||||
*.hdf5
|
||||
*.pth
|
||||
*.onnx
|
||||
*.zip
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.7z
|
||||
*.rar
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.bin
|
||||
*.sav
|
||||
*.csv
|
||||
*.parquet
|
||||
*.feather
|
||||
*.db
|
||||
*.sqlite
|
||||
*.log
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# Ignore model info/metadata if không cần backup
|
||||
# *.json
|
||||
|
||||
# Ignore cache, prediction, backup folders
|
||||
dataset_cache/
|
||||
predictions/
|
||||
backup_model_train/
|
||||
backup_ketquaphanloai/
|
||||
backup_S3_download_Amazon/
|
||||
model_train/
|
||||
__pycache__/
|
||||
# Ignore large data files
|
||||
ndvi_results/
|
||||
ndvi_cache/
|
||||
prediction_cache/
|
||||
dataset_cache/
|
||||
bfg.jar
|
||||
..bfg-report/
|
||||
.dvc/
|
||||
|
||||
# Ignore model outputs but keep info json
|
||||
model_train/*.joblib
|
||||
model_train/*.tif
|
||||
model_train/*.png
|
||||
model_train/*.h5
|
||||
model_train/*.pt
|
||||
model_train/*.pth
|
||||
model_train/*.ckpt
|
||||
model_train/*.npz
|
||||
model_train/*.npy
|
||||
model_train/*.zip
|
||||
model_train/*.tar
|
||||
model_train/*.tar.gz
|
||||
model_train/*.7z
|
||||
model_train/*.rar
|
||||
model_train/*.csv
|
||||
model_train/*.parquet
|
||||
model_train/*.feather
|
||||
model_train/*.db
|
||||
model_train/*.sqlite
|
||||
model_train/*.log
|
||||
cloud_removal_model/
|
||||
|
||||
# VSCode settings
|
||||
.vscode/
|
||||
|
||||
# Jupyter checkpoints
|
||||
.ipynb_checkpoints/
|
||||
reports/
|
||||
ThuanHoa/ThuanHoa_VH.tif
|
||||
ThuanHoa/ThuanHoa_VV.tif
|
||||
model_train/model.joblib
|
||||
model_train/model_new.joblib
|
||||
backup_model_train/model.joblib
|
||||
backup_model_train/model_new.joblib
|
||||
dataset_cache/sentinel2_timeseries_40scenes.nc
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/remote-sensing.iml" filepath="$PROJECT_DIR$/.idea/remote-sensing.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-48
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8512e5bb-2b73-4d09-a37b-d0b357b3fbe9" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"associatedIndex": 1
|
||||
}]]></component>
|
||||
<component name="ProjectId" id="39nRCQRaBb6bqtrPoIoBjpMe8Bs" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "dev__01",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu-22.04/home/x79/remote-sensing"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-python-sdk-4762d8aabb82-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.173" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="8512e5bb-2b73-4d09-a37b-d0b357b3fbe9" name="Changes" comment="" />
|
||||
<created>1771329720024</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1771329720024</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
</project>
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
+3
-1135
File diff suppressed because it is too large
Load Diff
+3
-3097
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-4465
File diff suppressed because one or more lines are too long
+3
-1406
File diff suppressed because one or more lines are too long
@@ -1,346 +0,0 @@
|
||||
# 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
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# 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
|
||||
@@ -1,227 +0,0 @@
|
||||
# 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
|
||||
@@ -1,638 +0,0 @@
|
||||
# GEMINI PROJECT CONTEXT - Land Classification & Remote Sensing System
|
||||
|
||||
**Last Updated**: March 26, 2026
|
||||
**Project Location**: `/home/x79/remote-sensing`
|
||||
**Purpose**: Complete land classification and environmental monitoring system using satellite remote sensing for Vietnam
|
||||
|
||||
---
|
||||
|
||||
## 📋 PROJECT OVERVIEW
|
||||
|
||||
### High-Level Purpose & Problem Domain
|
||||
- **Core Task**: Classify land use/land cover (8 land classes) in Vietnam using multispectral Sentinel-2 and radar Sentinel-1 data from Microsoft Planetary Computer
|
||||
- **Geographic Focus**: Vietnam provinces/regions with bounding-box (bbox) based Area-of-Interest (AOI) selection
|
||||
- **Key Capabilities**:
|
||||
- Dynamic training with user-selected regions and time periods
|
||||
- Pixel-wise inference (prediction) on new regions
|
||||
- Cloud removal using 7 different strategies
|
||||
- NDVI time-series forecasting and change detection workflows
|
||||
- Auto-generated HTML reports with visualizations
|
||||
- Batch processing of multiple regions
|
||||
- Model lifecycle management (save, load, validate, delete)
|
||||
|
||||
### Data Pipeline
|
||||
```
|
||||
Sentinel-2 (optical) + Sentinel-1 (SAR)
|
||||
↓
|
||||
[Feature Extraction: 4 modes - simple (3) / temporal (39) / extended (15) / odc (8)]
|
||||
↓
|
||||
[Model Training: XGBoost, RF, SVM, CNN, Swin-UNet, MobileNet-LRASPP]
|
||||
↓
|
||||
[Prediction: Pixel-wise classification]
|
||||
↓
|
||||
[Output: GeoTIFF + PNG preview + HTML report + JSON metadata]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ SYSTEM ARCHITECTURE
|
||||
|
||||
### Core Technology Stack
|
||||
- **Backend**: FastAPI (~4200 lines in `api_server.py`)
|
||||
- **ML Training**: scikit-learn (XGBoost, RF, SVM, DT) + PyTorch (CNN, Swin-UNet, MobileNet)
|
||||
- **Geospatial**: rasterio, rioxarray, geopandas, xarray, odc.stac
|
||||
- **Data Access**: Microsoft Planetary Computer STAC API (Sentinel-2 L2A, Sentinel-1 RTC)
|
||||
- **Frontend**: HTML + Leaflet.js (map drawing) + Fetch API + Chart.js
|
||||
- **GPU Support**: PyTorch with CUDA 12.x (optional fallback to CPU)
|
||||
|
||||
### Folder Structure
|
||||
```
|
||||
remote-sensing/
|
||||
├── Core Backend
|
||||
│ ├── api_server.py # FastAPI app (~4200 LOC, 70+ endpoints)
|
||||
│ ├── train_module.py # Training pipeline engine
|
||||
│ ├── feature_extractor.py # Unified feature extraction (4 modes)
|
||||
│ ├── model_manager.py # Model lifecycle management
|
||||
│ ├── cloud_removal.py # 7 cloud removal strategies
|
||||
│ ├── report_generator.py # Auto HTML/PNG report generation
|
||||
│ ├── generate_previews.py # GeoTIFF → PNG conversion
|
||||
│ │
|
||||
├── Utilities & Lookup
|
||||
│ ├── vietnam_provinces.py # Province bboxes & metadata
|
||||
│ ├── vietnam_provinces_merged.py # 32-province variant
|
||||
│ ├── utils.py # Geospatial helper functions
|
||||
│ ├── create_odc_metadata.py # Metadata generator utility
|
||||
│ │
|
||||
├── Frontend Pages (HTML)
|
||||
│ ├── index.html # Main dashboard hub
|
||||
│ ├── training_interface.html # Training UI
|
||||
│ ├── prediction_interface.html # Prediction UI
|
||||
│ ├── batch_interface.html # Batch processing UI
|
||||
│ ├── ndvi_interface.html # NDVI time-series UI
|
||||
│ ├── dashboard.html # Analytics dashboard
|
||||
│ ├── reports_interface.html # Reports management
|
||||
│ ├── change_detection_interface.html # Change detection UI
|
||||
│ ├── cloud_training_interface.html # Cloud removal training UI
|
||||
│ │
|
||||
├── Tests & Notebooks
|
||||
│ ├── test_*.py # Unit & integration tests
|
||||
│ ├── 01.train_ODC*.ipynb # Training notebooks
|
||||
│ ├── 02.predict_ODC.ipynb # Prediction notebooks
|
||||
│ ├── cloud_removal_train.ipynb # Cloud removal training
|
||||
│ │
|
||||
├── Model Storage & Caches
|
||||
│ ├── model_train/ # Trained models (*.joblib, *.pth)
|
||||
│ │ ├── model_odc.joblib # Legacy GridSearchCV model
|
||||
│ │ ├── model_*_info.json # Metadata sidecar files
|
||||
│ ├── cloud_removal_model/ # Cloud removal U-Net models (.pth)
|
||||
│ ├── predictions/ # Prediction output (GeoTIFF + PNG)
|
||||
│ ├── reports/ # Generated HTML reports
|
||||
│ ├── dataset_cache/ # Cached Sentinel data (optional)
|
||||
│ │
|
||||
├── Config & Documentation
|
||||
│ ├── requirement.txt # Python dependencies
|
||||
│ ├── requirements_api.txt # API-specific deps
|
||||
│ ├── IMPLEMENTATION_SUMMARY.md # Model manager summary
|
||||
│ ├── MODEL_MANAGER_GUIDE.md # Full model management guide
|
||||
│ ├── NDVI_FORECAST_METHODOLOGY.md # NDVI algorithm docs
|
||||
│ ├── CLOUD_TRAINING_GUIDE.md # Cloud removal training guide
|
||||
│ └── [Other guides & docs]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 MAIN MODULES & RESPONSIBILITIES
|
||||
|
||||
| **Module** | **File(s)** | **Key Responsibility** |
|
||||
|---|---|---|
|
||||
| **API Server** | `api_server.py` | FastAPI app with 70+ endpoints; routes all training, prediction, batch, cloud removal, dashboard, reports, model management tasks |
|
||||
| **Training Engine** | `train_module.py` | Complete training pipeline: fetch data → feature extraction → train/test split → model training → evaluation → save with metadata |
|
||||
| **Feature Extraction** | `feature_extractor.py` | Standardized feature extraction with 4 modes: simple, temporal, extended, odc; used by both training and prediction |
|
||||
| **Model Manager** | `model_manager.py` | Lifecycle management: list, load, save, validate, delete models; handles metadata JSON; auto-detects CNN/PyTorch models |
|
||||
| **Cloud Removal** | `cloud_removal.py` | 7 cloud removal strategies: classic (3-step), temporal_only, median_composite, none, speckle filter, ML inpainting, deep learning U-Net |
|
||||
| **Report Generator** | `report_generator.py` | Auto-generates HTML/PNG reports with confusion matrices, class distributions, accuracy trends |
|
||||
| **Preview Generator** | `generate_previews.py` | Converts GeoTIFF outputs to PNG previews (NDVI or classification rasters) |
|
||||
| **Province Lookup** | `vietnam_provinces*.py` | Lookup tables for 32+ Vietnamese provinces with bboxes and region grouping |
|
||||
| **Utilities** | `utils.py` | Geospatial helper functions (load GeoDataFrames, etc.) |
|
||||
|
||||
---
|
||||
|
||||
## 📊 END-TO-END WORKFLOWS
|
||||
|
||||
### 1. TRAINING WORKFLOW
|
||||
```
|
||||
User Input → Training Configuration
|
||||
↓
|
||||
API Endpoint: POST /api/training/start
|
||||
↓
|
||||
train_module.py: train_model()
|
||||
1. Fetch Sentinel-2 & Sentinel-1 from Planetary Computer STAC
|
||||
2. Apply cloud mask (SCL band: clouds, shadows, cirrus masked)
|
||||
3. Extract features via FeatureExtractor (mode: simple/temporal/extended/odc)
|
||||
4. Train/test split (default 0.2)
|
||||
5. Train selected model type (XGBoost, RF, CNN, Swin-UNet, MobileNet)
|
||||
6. Evaluate: accuracy, precision, recall, F1, confusion matrix
|
||||
↓
|
||||
model_manager.py: Save model + JSON metadata
|
||||
↓
|
||||
report_generator.py: Auto-generate HTML training report
|
||||
↓
|
||||
Return: {model_filename, accuracy_metrics, training_time}
|
||||
```
|
||||
|
||||
**Key Metadata Saved**:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-03-26T14:30:00",
|
||||
"model_type": "xgboost",
|
||||
"feature_mode": "temporal",
|
||||
"n_features": 39,
|
||||
"n_classes": 8,
|
||||
"features": ["NDVI_t1", "NDVI_t2", ..., "NDWI_t1", ...],
|
||||
"test_accuracy": 0.85,
|
||||
"train_accuracy": 0.92,
|
||||
"bbox": [105.6, 9.3, 106.2, 9.8],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20,
|
||||
"data_source": "Microsoft Planetary Computer STAC"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. PREDICTION WORKFLOW
|
||||
```
|
||||
User Input → Prediction Configuration (model_filename, bbox, time_range, cloud_strategy)
|
||||
↓
|
||||
API Endpoint: POST /api/predict or POST /api/predict/with-ndvi
|
||||
↓
|
||||
run_prediction() function:
|
||||
1. Load model via model_manager.py (retrieves metadata, feature requirements)
|
||||
2. Fetch Sentinel-2 & Sentinel-1 for new region
|
||||
3. Apply chosen cloud_removal_method (classic/temporal_only/median_composite/none/deep_learning)
|
||||
4. Extract features matching model's metadata requirements
|
||||
5. Auto-adjust if feature count mismatch (pad/trim)
|
||||
6. Predict land class for each pixel
|
||||
7. (Optional) Calculate NDVI: (NIR - Red) / (NIR + Red)
|
||||
8. Save outputs: GeoTIFF + PNG preview
|
||||
↓
|
||||
generate_previews.py: Create PNG from GeoTIFF
|
||||
↓
|
||||
report_generator.py: Generate prediction report
|
||||
↓
|
||||
Return: {prediction_file, ndvi_file, class_distribution, statistics}
|
||||
```
|
||||
|
||||
### 3. BATCH PROCESSING WORKFLOW
|
||||
```
|
||||
User uploads CSV with multiple regions:
|
||||
(name, min_lon, min_lat, max_lon, max_lat, start_date, end_date, max_scenes, cloud_cover, resolution)
|
||||
↓
|
||||
API Endpoint: POST /api/batch/start
|
||||
↓
|
||||
Enqueue all regions; process sequentially
|
||||
↓
|
||||
For each region: Run same prediction workflow
|
||||
↓
|
||||
Track status per region: Queued → Running → Completed/Failed
|
||||
↓
|
||||
UI shows progress bar, auto-retry on failure (max 3 retries)
|
||||
↓
|
||||
Return: Bulk results with per-region status & output files
|
||||
```
|
||||
|
||||
### 4. CLOUD REMOVAL WORKFLOW
|
||||
```
|
||||
User selects cloud_removal_method in prediction config:
|
||||
↓
|
||||
cloud_removal.py: process_cloud_removal()
|
||||
↓
|
||||
Strategy Selection:
|
||||
• 'classic': temporal interpolation → median composite → spatial interpolation (3-step)
|
||||
• 'temporal_only': ffill + bfill across time dimension (fast, good for many scenes)
|
||||
• 'median_composite': Prioritize median across scenes (best for noise reduction)
|
||||
• 'none': Keep original, just fill NaN with 0
|
||||
• 'deep': Use trained U-Net model (S2 cloudy + S1 → clean S2)
|
||||
• 'ml_inpainting': KNN or Random Forest based inpainting
|
||||
• 'speckle_filter': Reduce radar noise
|
||||
↓
|
||||
Return cleaned Sentinel-2 data for subsequent feature extraction
|
||||
```
|
||||
|
||||
### 5. NDVI TIME-SERIES WORKFLOW
|
||||
```
|
||||
User requests NDVI calculation (bbox + time_range + aggregation)
|
||||
↓
|
||||
API Endpoint: POST /api/ndvi/timeseries or /api/ndvi/predict-timeseries
|
||||
↓
|
||||
Load Sentinel-2 (B04 Red, B08 NIR)
|
||||
↓
|
||||
Calculate NDVI = (NIR - Red) / (NIR + Red + 0.00001)
|
||||
↓
|
||||
Resample to monthly or user-defined aggregation
|
||||
↓
|
||||
Export as GeoTIFF + PNG visualization
|
||||
↓
|
||||
Show time-series graph & statistics (mean, min, max, std, trend)
|
||||
```
|
||||
|
||||
### 6. CHANGE DETECTION WORKFLOW
|
||||
```
|
||||
User selects: model + current_period + prediction_period
|
||||
↓
|
||||
API Endpoint: POST /api/change-detection/compare-periods
|
||||
↓
|
||||
Run prediction for both time periods
|
||||
↓
|
||||
Compute difference map (current - prediction)
|
||||
↓
|
||||
Classify changes: increased vegetation, decreased vegetation, stable
|
||||
↓
|
||||
Generate change map GeoTIFF + report with statistics
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 API ENDPOINTS SUMMARY (70+ endpoints)
|
||||
|
||||
### Model Management
|
||||
- `GET /api/models/list` - List all trained models with metadata
|
||||
- `GET /api/models/{filename}/info` - Get model details
|
||||
- `GET /api/models/{filename}/validate` - Validate model integrity
|
||||
- `DELETE /api/models/{filename}` - Delete model file
|
||||
|
||||
### Training APIs
|
||||
- `POST /api/training/start` - Start land classification training
|
||||
- `GET /api/training/status` - Get training progress
|
||||
- `POST /api/training/stop` - Cancel ongoing training
|
||||
- `POST /api/cloud-removal/train` - Train cloud removal U-Net
|
||||
|
||||
### Prediction APIs
|
||||
- `POST /api/predict` - Standard prediction (classification only)
|
||||
- `POST /api/predict/with-ndvi` - Prediction with NDVI export
|
||||
- `POST /api/change-detection/compare-periods` - Change detection
|
||||
- `GET /api/prediction/status` - Check prediction progress
|
||||
- `GET /api/predictions/list` - List prediction outputs
|
||||
- `GET /api/predictions/download/{filename}` - Download prediction file
|
||||
- `GET /api/predictions/preview/{filename}` - View PNG preview
|
||||
|
||||
### Batch Processing
|
||||
- `POST /api/batch/start` - Enqueue multiple predictions from CSV
|
||||
- `GET /api/batch/status` - Check batch queue
|
||||
- `GET /api/batch/results/{batch_id}` - Retrieve batch results
|
||||
- `POST /api/batch/cancel/{batch_id}` - Cancel batch job
|
||||
|
||||
### Cloud Removal
|
||||
- `GET /api/cloud-removal/methods` - List available strategies
|
||||
- `GET /api/cloud-removal/models` - List trained .pth models
|
||||
- `POST /api/cloud-removal/upload` - Upload .pth cloud removal model
|
||||
- `DELETE /api/cloud-removal/models/{filename}` - Delete cloud removal model
|
||||
|
||||
### Dashboard & Reports
|
||||
- `GET /api/dashboard/statistics` - Overall system stats
|
||||
- `GET /api/dashboard/accuracy-trends` - Accuracy over time
|
||||
- `GET /api/dashboard/class-distribution/{model_filename}` - Class distribution
|
||||
- `GET /api/reports/list` - List generated reports
|
||||
- `GET /api/reports/view/{filename}` - View HTML report
|
||||
- `GET /api/reports/download/{filename}` - Download report
|
||||
- `DELETE /api/reports/delete/{filename}` - Delete report
|
||||
|
||||
### Provinces & Utilities
|
||||
- `GET /api/provinces/list` - List all Vietnamese provinces
|
||||
- `GET /api/provinces/by-region` - Group provinces by region
|
||||
- `GET /api/provinces/{province_name}/bbox` - Get province bbox
|
||||
- `GET /api/provinces/search/{query}` - Search province by name
|
||||
- `GET /api/provinces-32/*` - Alternative 32-province variant
|
||||
- `GET /api/network/check` - Check connectivity to Planetary Computer
|
||||
- `GET /api/cache/info` - Show cache statistics
|
||||
- `POST /api/cache/clear` - Clear local cache
|
||||
|
||||
### NDVI & Time-Series
|
||||
- `POST /api/ndvi/timeseries` - Calculate NDVI time-series
|
||||
- `POST /api/ndvi/predict-timeseries` - NDVI prediction/forecast
|
||||
- `POST /api/ndvi/forecast` - NDVI forecasting
|
||||
|
||||
### File Management
|
||||
- `GET /api/training/files` - List training files
|
||||
- `GET /api/overlay/shapefiles` - List available shapefiles
|
||||
- `GET /api/training/shapefile/{filename}/labels` - Get shapefile labels
|
||||
- `POST /api/land-classification/upload` - Upload custom model
|
||||
- `POST /api/cloud-removal/upload` - Upload cloud removal model
|
||||
|
||||
### Frontend Routes (Serve HTML)
|
||||
- `GET /` - Main dashboard
|
||||
- `GET /training` - Training interface
|
||||
- `GET /prediction` - Prediction interface
|
||||
- `GET /dashboard` - Analytics dashboard
|
||||
- `GET /batch` - Batch processing UI
|
||||
- `GET /ndvi` - NDVI time-series UI
|
||||
- `GET /reports` - Reports management
|
||||
- `GET /cloud-training` - Cloud removal training
|
||||
- `GET /change-detection` - Change detection UI
|
||||
|
||||
---
|
||||
|
||||
## 💾 DATA INPUTS / OUTPUTS & FOLDER CONVENTIONS
|
||||
|
||||
### Input Data Sources
|
||||
- **Sentinel-2 L2A** from Microsoft Planetary Computer STAC API
|
||||
- Bands: B02 (blue), B03 (green), B04 (red), B08 (NIR), B11 (SWIR), SCL (cloud mask)
|
||||
- Resolution: 10m or 20m (user selectable)
|
||||
- Collection: `sentinel-2-l2a`
|
||||
|
||||
- **Sentinel-1 RTC** from Planetary Computer
|
||||
- Bands: VH, VV (radar polarizations)
|
||||
- Converted to dB scale: `10 * log10(intensity)`
|
||||
- Collection: `sentinel-1-rtc`
|
||||
|
||||
- **Training Labels**: User-provided shapefiles with pixel-level class labels
|
||||
|
||||
### Output File Structure
|
||||
```
|
||||
predictions/
|
||||
├── prediction_YYYYMMDD_HHMMSS.tif # Classification GeoTIFF
|
||||
├── prediction_YYYYMMDD_HHMMSS.png # PNG preview
|
||||
├── ndvi_YYYYMMDD_HHMMSS.tif # NDVI raster
|
||||
├── ndvi_YYYYMMDD_HHMMSS.png # NDVI preview
|
||||
|
||||
reports/
|
||||
├── training_report_*.html # Auto training reports
|
||||
├── prediction_report_*.html # Auto prediction reports
|
||||
|
||||
model_train/
|
||||
├── model_odc.joblib # Legacy model
|
||||
├── model_odc_info.json # Metadata
|
||||
├── model_xgboost_*.joblib # XGBoost models
|
||||
├── model_xgboost_*_info.json # Metadata
|
||||
├── model_cnn_*.joblib # CNN models
|
||||
├── model_cnn_*_info.json # Metadata
|
||||
|
||||
cloud_removal_model/
|
||||
├── cloud_removal_unet_best.pth # Trained U-Net
|
||||
├── *.pth # Custom models
|
||||
├── *.json # Model metadata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 EXTERNAL DEPENDENCIES & PLATFORMS
|
||||
|
||||
### Critical External Services
|
||||
- **Microsoft Planetary Computer** (STAC API)
|
||||
- Hosts Sentinel-2 L2A and Sentinel-1 RTC archives
|
||||
- URL: `https://planetarycomputer.microsoft.com/api/stac/v1`
|
||||
- Auto-signed access tokens via `planetary_computer.sign_inplace`
|
||||
- Network connectivity check: `GET /api/network/check`
|
||||
|
||||
### Key Python Libraries
|
||||
- **Geospatial**: rasterio, rioxarray, geopandas, shapely, Cartopy, folium, ipyleaflet
|
||||
- **Data Processing**: numpy, pandas, xarray, dask
|
||||
- **ML**: scikit-learn, xgboost
|
||||
- **Deep Learning**: torch, torchvision
|
||||
- **Web**: fastapi, uvicorn, pydantic
|
||||
- **Visualization**: matplotlib, Pillow (PIL)
|
||||
- **Document Gen**: markdown, Pillow
|
||||
|
||||
### GPU Support
|
||||
- PyTorch with CUDA 12.x (optional; falls back to CPU)
|
||||
- Benefits Swin-UNet and CNN models (10-100x speedup)
|
||||
- CPU training for XGBoost/RF typically <1 hour; deep models need GPU for reasonable speed
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ FEATURE EXTRACTION MODES (CRITICAL)
|
||||
|
||||
Train and prediction **MUST** use same feature mode and dimension; metadata auto-detects this.
|
||||
|
||||
| Mode | # Features | Description | Best For | Training Time |
|
||||
|---|---|---|---|---|
|
||||
| **simple** | 3 | NDVI_mean, VH_db_mean, VV_db_mean | Fast iteration, baseline | ~5-10 min |
|
||||
| **temporal** | 39 | NDVI/NDWI/NDBI across 13 months + radar stats | High accuracy (~85%+) | ~30-60 min |
|
||||
| **extended** | 15 | NDVI/NDWI/NDBI stats (mean/std/min/max) + radar | Balanced speed/accuracy | ~15-30 min |
|
||||
| **odc** | 8 | NDVI stats + NDWI/NDBI/EVI mean (legacy ODC mode) | Legacy compatibility | ~10-20 min |
|
||||
|
||||
**Critical**: If feature mode = "temporal" (39 features) at training, prediction MUST extract 39 features. System auto-detects from metadata but will fail if mismatched.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 OPERATIONAL NOTES & CONSTRAINTS
|
||||
|
||||
### Performance Limits
|
||||
1. **Planetary Computer Timeout Issues**
|
||||
- Large bbox (>10km × 10km) + long time range (>1 month) + high max_scenes → timeouts
|
||||
- **Solution**: Progressive loading (subdivide bbox), reduce time window, reduce max_scenes
|
||||
- **Safe Settings**: bbox ≤ 10km × 10km, time ≤ 1 month, max_scenes ≤ 12
|
||||
|
||||
2. **Memory Usage**
|
||||
- Temporal mode (39 features) requires ~2-3x RAM vs simple mode
|
||||
- Large regions: reduce resolution (10m → 20m) or split into sub-tiles
|
||||
- Batch processing: sequential (one region at a time due to API limits)
|
||||
|
||||
3. **GPU Training**
|
||||
- Swin-UNet: ~15-60 min on GPU vs ~2-4 hours on CPU
|
||||
- CNN: ~10-30 min on GPU vs ~1-2 hours on CPU
|
||||
- XGBoost/RF: CPU-bound; GPU not beneficial
|
||||
|
||||
### Data Quality Issues
|
||||
1. **Cloud Cover**
|
||||
- SCL band values: 3=cloud shadow, 8=cloud medium, 9=cloud high, 10=cirrus
|
||||
- Recommend multiple scenes (≥5) for temporal aggregation
|
||||
- Cloud removal strategy critical—test different approaches
|
||||
|
||||
2. **Radar Data (Sentinel-1)**
|
||||
- Not always available for all regions/dates
|
||||
- System gracefully falls back to zeros if unavailable
|
||||
- Safe for "extended" & "odc" modes that have radar fallback
|
||||
|
||||
3. **Feature Mode Mismatch**
|
||||
- Model trained with "temporal" (39 features) needs 39-dim input
|
||||
- System auto-adjusts (pads/trims) from metadata but may degrade accuracy
|
||||
- **Best Practice**: Align feature mode explicitly; don't mix
|
||||
|
||||
### Known Caveats
|
||||
1. **Legacy Model (model_odc.joblib)**: Hardcoded 39 temporal features; auto-detected via `model_odc_info.json`
|
||||
2. **Metadata Consistency**: Old models may lack `.json` sidecar; system generates default (may be incorrect)
|
||||
3. **Batch Processing**: Sequential only; large batches (100+ regions) take hours
|
||||
4. **Change Detection**: Simple differencing approach; requires same model & feature mode for both periods
|
||||
5. **Rate Limiting**: Planetary Computer may rate-limit if too many concurrent requests
|
||||
|
||||
### Recommended Best Practices
|
||||
- Test model on small bbox first (2km × 2km, 1 week, 3 scenes)
|
||||
- Use "simple" mode for fast iteration, "temporal" for best accuracy (85%+)
|
||||
- Store metadata JSON alongside model file (sidecar pattern)
|
||||
- Version control: record feature_mode & n_features in every training
|
||||
- Monitor training accuracy; retrain if <70% accuracy
|
||||
- Cache Sentinel data locally to avoid repeated downloads
|
||||
- Use "median_composite" cloud strategy if >5 scenes; "temporal_only" if 3-4 scenes
|
||||
|
||||
---
|
||||
|
||||
## 🔍 TEST COVERAGE MAP
|
||||
|
||||
| Test File | Coverage | Status |
|
||||
|---|---|---|
|
||||
| `test_model_manager.py` | ModelManager lifecycle (list, load, validate) | ✅ Well-tested |
|
||||
| `test_feature_extractor.py` | All 4 feature extraction modes | ✅ Well-tested |
|
||||
| `test_training_api.py` | Training API endpoints | ✅ Partial |
|
||||
| `test_cloud_removal.py` | 7 cloud removal strategies | ✅ Well-tested |
|
||||
| `test_cloud_training.py` | U-Net cloud removal training | ✅ Partial |
|
||||
| `test_shapefile_api.py` | Shapefile overlay feature | ✅ Partial |
|
||||
| `test_planetary_computer.py` | Planetary Computer STAC access | ✅ Well-tested |
|
||||
| `test_new_features.py` | Recent feature releases | ✅ Partial |
|
||||
| Jupyter Notebooks | Training & prediction workflows | ✅ Mix of unit/integration/notebooks |
|
||||
|
||||
**Coverage Notes**: Model management, feature extraction, and cloud removal well-tested; Dashboard UI, change detection, NDVI time-series mostly tested via notebooks.
|
||||
|
||||
---
|
||||
|
||||
## 📚 FILE REFERENCE MAP
|
||||
|
||||
### Core Execution
|
||||
- `api_server.py` — Main FastAPI application (~4200 LOC)
|
||||
- `train_module.py` — Training logic (data fetch → feature extraction → training)
|
||||
- `run_prediction_new.py` — Prediction execution function
|
||||
- `feature_extractor.py` — Unified feature extraction (4 modes)
|
||||
- `model_manager.py` — Model lifecycle (load/save/validate/list)
|
||||
- `cloud_removal.py` — Cloud removal strategies (7 methods)
|
||||
- `report_generator.py` — HTML/PNG report auto-generation
|
||||
- `generate_previews.py` — GeoTIFF → PNG conversion
|
||||
|
||||
### Data & Config
|
||||
- `vietnam_provinces.py` — 32+ province lookup tables & bboxes
|
||||
- `vietnam_provinces_merged.py` — Alternative 32-province variant
|
||||
- `utils.py` — Geospatial utility functions
|
||||
- `create_odc_metadata.py` — Legacy metadata generator
|
||||
|
||||
### Frontend
|
||||
- `index.html` — Main dashboard hub (tab navigation)
|
||||
- `training_interface.html` — Training configuration UI
|
||||
- `prediction_interface.html` — Prediction configuration UI
|
||||
- `batch_interface.html` — Batch processing (CSV upload)
|
||||
- `ndvi_interface.html` — NDVI time-series visualization
|
||||
- `dashboard.html` — Analytics & model performance dashboard
|
||||
- `reports_interface.html` — Report management & viewing
|
||||
- `change_detection_interface.html` — Change detection visualization
|
||||
- `cloud_training_interface.html` — Cloud removal U-Net training
|
||||
|
||||
### Documentation
|
||||
- `IMPLEMENTATION_SUMMARY.md` — Model manager & system overview
|
||||
- `MODEL_MANAGER_GUIDE.md` — Complete model management guide
|
||||
- `NDVI_FORECAST_METHODOLOGY.md` — NDVI algorithm documentation
|
||||
- `CLOUD_TRAINING_GUIDE.md` — Cloud removal training guide
|
||||
- `NDVI_PREDICTION_GUIDE.md` — NDVI prediction workflow
|
||||
- `CLOUD_PROCESSING.md` — Cloud processing notes
|
||||
- `UPDATE_SUMMARY.md` — Recent updates & features
|
||||
|
||||
---
|
||||
|
||||
## 🚀 BOOTSTRAP PROMPT FOR GEMINI
|
||||
|
||||
### System Context (Copy & Paste for Gemini)
|
||||
|
||||
```
|
||||
You are assisting a remote-sensing land-classification project for Vietnam.
|
||||
|
||||
## ARCHITECTURE SNAPSHOT
|
||||
- **Backend**: FastAPI (~4200 LOC, 70+ endpoints) for orchestrating training, prediction, batch, cloud removal, reporting
|
||||
- **Data Source**: Microsoft Planetary Computer STAC API (Sentinel-2 L2A + Sentinel-1 RTC)
|
||||
- **Training**: scikit-learn (XGBoost/RF/SVM/DT) + PyTorch (CNN/Swin-UNet/MobileNet)
|
||||
- **Feature Extraction**: 4 modes (simple 3-feat / temporal 39-feat / extended 15-feat / odc 8-feat)
|
||||
- **Cloud Removal**: 7 strategies (classic, temporal_only, median_composite, none, ML inpainting, deep U-Net)
|
||||
- **Output**: GeoTIFF + PNG + HTML report + JSON metadata
|
||||
|
||||
## CORE FILES TO UNDERSTAND (Priority Order)
|
||||
1. api_server.py — Main API server (training, prediction, batch, models, reports)
|
||||
2. train_module.py — Training pipeline (data fetch → feature extraction → train → save)
|
||||
3. feature_extractor.py — Unified feature extraction with auto mode detection
|
||||
4. model_manager.py — Model lifecycle (load/save/validate/list)
|
||||
5. cloud_removal.py — Cloud removal strategies (7 methods)
|
||||
6. report_generator.py — Auto-generate HTML reports
|
||||
7. run_prediction_new.py — Prediction execution
|
||||
8. vietnam_provinces.py — Province lookup & bbox tables
|
||||
|
||||
## CRITICAL CONSTRAINTS & GOTCHAS
|
||||
1. **Feature Mode Consistency**: Training & prediction MUST use same mode (simple/temporal/extended/odc)
|
||||
→ Auto-detected from metadata JSON
|
||||
→ Mismatch causes dimension error or accuracy degradation
|
||||
|
||||
2. **Planetary Computer Limits**:
|
||||
→ Timeout if bbox >10km×10km OR time range >1 month OR max_scenes >12
|
||||
→ Solution: subdivide bbox, reduce time window, limit scenes
|
||||
|
||||
3. **Cloud Strategy Selection**:
|
||||
→ ≥5 scenes → use "median_composite" (best noise reduction)
|
||||
→ 3-4 scenes → use "temporal_only" (fast temporal interp)
|
||||
→ <3 scenes → use "none" (skip cloud removal)
|
||||
|
||||
4. **Radar Data Fallback**:
|
||||
→ Sentinel-1 may be unavailable for some regions
|
||||
→ System gracefully falls back to zeros (safe for all modes)
|
||||
|
||||
5. **Model Metadata**:
|
||||
→ Always stored as `model_name_info.json` sidecar file
|
||||
→ Contains: n_features, feature_mode, features list, accuracy, bbox, time_range
|
||||
→ Missing metadata → system uses defaults (may be incorrect)
|
||||
|
||||
6. **Legacy Model (model_odc.joblib)**:
|
||||
→ Hardcoded 39 temporal features
|
||||
→ Metadata in model_odc_info.json
|
||||
|
||||
## REASONING CHECKLIST (before answering)
|
||||
□ Is feature_mode consistent between train and prediction?
|
||||
□ Is metadata.json present and correct?
|
||||
□ Does bbox exceed 10km×10km? (Planetary Computer timeout risk)
|
||||
□ Is cloud_removal_strategy appropriate for # of scenes?
|
||||
□ Is Sentinel-1 data available for this region/date?
|
||||
□ Is model a joblib (scikit-learn) or .pth (PyTorch) file?
|
||||
□ Is GPU available for deep models (CNN, Swin-UNet)?
|
||||
□ Does memory allow temporal feature extraction (39-feat)?
|
||||
|
||||
## RESPONSE FORMAT
|
||||
- Always cite api_server.py endpoint, function name, or module being discussed
|
||||
- Verify feature_mode & n_features from metadata JSON
|
||||
- Suggest cloud_removal_strategy based on # of scenes available
|
||||
- For unknown issues: offer alternative approaches (reduce bbox, cache results, use simpler model)
|
||||
- Explain reasoning using checklist above
|
||||
|
||||
## DATA FLOW SUMMARY
|
||||
Sentinel-2/S1 → [Cloud Remove] → [Feature Extract] → [Train/Predict] → [GeoTIFF + PNG + Report]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 QUICK REFERENCE CHECKLIST
|
||||
|
||||
### Before Troubleshooting Any Issue
|
||||
- [ ] Check feature_mode consistency (metadata JSON)
|
||||
- [ ] Verify metadata.json exists for the model
|
||||
- [ ] Check Planetary Computer connectivity (`GET /api/network/check`)
|
||||
- [ ] Review cloud_removal_method choice (≥5 scenes = median_composite)
|
||||
- [ ] Confirm Sentinel-1 availability (or fallback to zeros if missing)
|
||||
- [ ] Validate bbox size (≤10km×10km for safety)
|
||||
- [ ] Check memory usage for temporal feature mode
|
||||
- [ ] Verify GPU if using CNN/Swin-UNet models
|
||||
|
||||
### Common Issues & Solutions
|
||||
| Issue | Likely Cause | Solution |
|
||||
|---|---|---|
|
||||
| Training timeout | Large bbox / long time / many scenes | Subdivide bbox, reduce time window, max_scenes ≤ 12 |
|
||||
| Feature dimension mismatch | Different feature_mode between train & predict | Check metadata.json, ensure same mode |
|
||||
| Low prediction accuracy | Cloud cover, poor training data, feature mode too simple | Use "temporal" mode, increase training data, try cloud removal |
|
||||
| Out of memory | Temporal features + large region | Reduce resolution (20m), split into sub-tiles, increase RAM |
|
||||
| Model not found | Wrong filename or model_train/ path issue | `GET /api/models/list` to verify, check file path |
|
||||
| Planetary Computer error | Network issue or API rate limit | Check DNS, retry later, reduce concurrent requests |
|
||||
| Cloud removal failing | Strategy not suitable for scene count | Try "none" or "median_composite" depending on scenes |
|
||||
|
||||
---
|
||||
|
||||
## 🎓 LEARNING RESOURCES IN REPO
|
||||
|
||||
- **Notebooks**: `01.train_ODC.ipynb`, `02.predict_ODC.ipynb`, `cloud_removal_train.ipynb`
|
||||
- **Tests**: `test_*.py` files for unit test patterns
|
||||
- **Docs**: All `*.md` files for detailed guides and methodology
|
||||
- **Code Comments**: API server and modules heavily commented
|
||||
|
||||
---
|
||||
|
||||
**Generated**: March 26, 2026
|
||||
**For Use By**: Gemini, Claude, GPT, or any AI system needing project context
|
||||
**Maintainer**: Remote-Sensing Project Team
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
# Hệ Thống Model Manager - Tóm Tắt Triển Khai
|
||||
|
||||
## ✅ Đã Hoàn Thành
|
||||
|
||||
### 1. **Model Manager Core System** (`model_manager.py`)
|
||||
Tạo class `ModelManager` với đầy đủ chức năng:
|
||||
|
||||
- ✅ **List Models**: Liệt kê tất cả models với metadata
|
||||
- ✅ **Load Model**: Load model + metadata + label encoder
|
||||
- ✅ **Save Model**: Lưu model kèm metadata tự động
|
||||
- ✅ **Validate Model**: Kiểm tra tính hợp lệ của model
|
||||
- ✅ **Get Features**: Lấy danh sách features cần thiết
|
||||
- ✅ **Delete Model**: Xóa model và metadata
|
||||
- ✅ **Get Latest**: Tìm model mới nhất (theo type)
|
||||
- ✅ **Auto-detect**: Tự động phát hiện CNN/PyTorch models
|
||||
|
||||
### 2. **API Integration** (`api_server.py`)
|
||||
Tích hợp ModelManager vào tất cả prediction endpoints:
|
||||
|
||||
- ✅ `GET /api/models/list` - List tất cả models
|
||||
- ✅ `GET /api/models/{filename}/info` - Chi tiết model
|
||||
- ✅ `GET /api/models/{filename}/validate` - Validate model
|
||||
- ✅ `DELETE /api/models/{filename}` - Xóa model
|
||||
- ✅ Updated `POST /api/predict` - Sử dụng ModelManager
|
||||
- ✅ Updated `POST /api/batch/predict` - Batch với ModelManager
|
||||
- ✅ Updated `POST /api/predict-with-ndvi` - NDVI + ModelManager
|
||||
- ✅ Updated Change Detection - Với ModelManager
|
||||
|
||||
### 3. **Training Integration** (`train_module.py`, `new_import_ODC.py`)
|
||||
Cập nhật training code để tự động save metadata:
|
||||
|
||||
- ✅ `train_module.py`: Sử dụng ModelManager khi save model
|
||||
- ✅ `new_import_ODC.py`: Updated `save_model()` function
|
||||
- ✅ Tự động tạo metadata khi train model mới
|
||||
- ✅ Backward compatible với old format
|
||||
|
||||
### 4. **Bug Fixes**
|
||||
- ✅ Fixed `NameError: is_cnn_model not defined`
|
||||
- ✅ Fixed feature mismatch (39 features vs 3 features)
|
||||
- ✅ Added temporal feature extraction logic
|
||||
- ✅ Auto-adjust features to match model requirements
|
||||
|
||||
### 5. **Legacy Support**
|
||||
- ✅ Tạo metadata cho `model_odc.joblib`
|
||||
- ✅ Support models không có metadata (tạo default)
|
||||
- ✅ Backward compatible với old model format
|
||||
|
||||
### 6. **Documentation & Testing**
|
||||
- ✅ `MODEL_MANAGER_GUIDE.md` - Hướng dẫn đầy đủ
|
||||
- ✅ `test_model_manager.py` - Test suite
|
||||
- ✅ `create_odc_metadata.py` - Utility script
|
||||
|
||||
## 🎯 Các Tính Năng Chính
|
||||
|
||||
### Automatic Feature Detection
|
||||
Hệ thống tự động:
|
||||
- Detect số features cần thiết từ metadata
|
||||
- Extract đúng features (temporal hoặc aggregate)
|
||||
- Adjust features để match với model (pad/trim)
|
||||
|
||||
### Multi-Model Support
|
||||
Hỗ trợ tất cả các loại models:
|
||||
- ✅ **XGBoost**: GPU-accelerated gradient boosting
|
||||
- ✅ **Random Forest**: Ensemble learning
|
||||
- ✅ **Decision Tree**: Simple tree-based
|
||||
- ✅ **SVM**: Support Vector Machine
|
||||
- ✅ **CNN**: PyTorch neural networks
|
||||
- ✅ **Custom models**: Bất kỳ scikit-learn compatible model
|
||||
|
||||
### Intelligent Feature Extraction
|
||||
|
||||
```python
|
||||
# Tự động detect và extract features dựa vào metadata
|
||||
if expected_n_features > 10:
|
||||
# Temporal features (all time steps)
|
||||
features = [ndvi_t1, ndvi_t2, ..., ndwi_t1, ndwi_t2, ...]
|
||||
else:
|
||||
# Aggregate features (mean values)
|
||||
features = [ndvi_mean, ndwi_mean, ndbi_mean]
|
||||
```
|
||||
|
||||
## 📊 Model Metadata Format
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-21T17:23:57",
|
||||
"model_type": "xgboost",
|
||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"test_accuracy": 0.578125,
|
||||
"train_accuracy": 1.0,
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
|
||||
"bbox": [105.6, 9.3, 106.2, 9.8],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Workflow
|
||||
|
||||
### Training → Saving
|
||||
```python
|
||||
# Train model
|
||||
model = XGBClassifier()
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {
|
||||
"model_type": "xgboost",
|
||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||
"n_features": 3,
|
||||
"test_accuracy": accuracy_score(y_test, y_pred)
|
||||
}
|
||||
|
||||
# Save with ModelManager
|
||||
model_manager.save_model(model, metadata, label_encoder=encoder)
|
||||
```
|
||||
|
||||
### Loading → Predicting
|
||||
```python
|
||||
# Load model
|
||||
model_manager = get_model_manager()
|
||||
model, encoder, metadata = model_manager.load_model("model_xgb.joblib")
|
||||
|
||||
# Get required features
|
||||
required_features = metadata["features"]
|
||||
n_features = metadata["n_features"]
|
||||
|
||||
# Extract features
|
||||
features = extract_features(data, required_features)
|
||||
|
||||
# Predict
|
||||
predictions = model.predict(features)
|
||||
```
|
||||
|
||||
## 📂 File Structure
|
||||
|
||||
```
|
||||
remote-sensing/
|
||||
├── model_manager.py # Core ModelManager class
|
||||
├── api_server.py # API với ModelManager integration
|
||||
├── train_module.py # Training với auto-save metadata
|
||||
├── new_import_ODC.py # Updated save_model function
|
||||
├── test_model_manager.py # Test suite
|
||||
├── create_odc_metadata.py # Metadata generator
|
||||
├── MODEL_MANAGER_GUIDE.md # Full documentation
|
||||
└── model_train/
|
||||
├── model_odc.joblib # Legacy model
|
||||
├── model_odc_info.json # Metadata (created)
|
||||
├── model_xgboost_*.joblib # New models
|
||||
├── model_xgboost_*_info.json # Auto-generated metadata
|
||||
├── model_cnn_*.joblib
|
||||
└── model_cnn_*_info.json
|
||||
```
|
||||
|
||||
## 🚀 Usage Examples
|
||||
|
||||
### API - List Models
|
||||
```bash
|
||||
curl http://localhost:8000/api/models/list
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"models": [
|
||||
{
|
||||
"filename": "model_xgboost_20251221_172351.joblib",
|
||||
"model_type": "xgboost",
|
||||
"n_features": 3,
|
||||
"test_accuracy": 0.578125,
|
||||
"size_mb": 0.45
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### API - Predict with Specific Model
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/predict \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model_filename": "model_xgboost_20251221_172351.joblib",
|
||||
"min_lon": 105.6,
|
||||
"max_lon": 106.2,
|
||||
"start_date": "2023-03-01",
|
||||
"end_date": "2023-05-31"
|
||||
}'
|
||||
```
|
||||
|
||||
### Python - Use ModelManager
|
||||
```python
|
||||
from model_manager import get_model_manager
|
||||
|
||||
# List all models
|
||||
mm = get_model_manager()
|
||||
models = mm.list_models()
|
||||
|
||||
# Load specific model
|
||||
model, encoder, metadata = mm.load_model("model_odc.joblib")
|
||||
|
||||
# Validate
|
||||
validation = mm.validate_model("model_odc.joblib")
|
||||
print(validation['valid']) # True/False
|
||||
```
|
||||
|
||||
## 🔧 Key Improvements
|
||||
|
||||
1. **Centralized Model Management**: Một nơi quản lý tất cả models
|
||||
2. **Automatic Feature Detection**: Không cần hardcode features
|
||||
3. **Metadata Driven**: Models tự document mình
|
||||
4. **Multi-Model Ready**: Dễ dàng switch giữa các models
|
||||
5. **Backward Compatible**: Vẫn support old models
|
||||
6. **Error Handling**: Validate và report lỗi rõ ràng
|
||||
|
||||
## 🎉 Kết Quả
|
||||
|
||||
Hệ thống bây giờ có thể:
|
||||
- ✅ Vận hành với **TẤT CẢ** các models (XGBoost, CNN, RF, SVM, etc.)
|
||||
- ✅ Tự động detect và extract đúng features
|
||||
- ✅ List, load, validate, delete models qua API
|
||||
- ✅ Support cả legacy models (model_odc.joblib)
|
||||
- ✅ Training tự động save metadata
|
||||
- ✅ Prediction tự động adjust features
|
||||
|
||||
## 🔜 Next Steps (Optional)
|
||||
|
||||
1. **Model Versioning**: Track model versions
|
||||
2. **Model Comparison**: So sánh performance nhiều models
|
||||
3. **Auto Model Selection**: Chọn model tốt nhất tự động
|
||||
4. **Model Ensemble**: Combine predictions từ nhiều models
|
||||
5. **Model Monitoring**: Track prediction quality over time
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff33ab950e3524342a99e70a02598ccb1ca57e60627c887c10b34466eabf3e87
|
||||
size 3536032
|
||||
@@ -1,347 +0,0 @@
|
||||
# Hệ Thống Quản Lý Model - Model Manager
|
||||
|
||||
## Tổng quan
|
||||
|
||||
Hệ thống **Model Manager** cho phép vận hành và quản lý tất cả các loại models trong dự án Land Classification, bao gồm:
|
||||
- XGBoost
|
||||
- Random Forest
|
||||
- Decision Tree
|
||||
- SVM
|
||||
- CNN (PyTorch)
|
||||
- Các model khác
|
||||
|
||||
## Cấu trúc
|
||||
|
||||
### 1. Model Storage
|
||||
```
|
||||
model_train/
|
||||
├── model_odc.joblib # Model file
|
||||
├── model_xgboost_20251221_172351.joblib
|
||||
├── model_xgboost_20251221_172351_info.json # Metadata
|
||||
├── model_cnn_20251221_163841.joblib
|
||||
└── model_cnn_20251221_163841_info.json
|
||||
```
|
||||
|
||||
### 2. Metadata Format
|
||||
Mỗi model đi kèm với file JSON chứa metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-21T17:23:57.306042",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
|
||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||
"model_type": "xgboost",
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"test_accuracy": 0.578125,
|
||||
"train_accuracy": 1.0,
|
||||
"classification_report": {...},
|
||||
"confusion_matrix": [...],
|
||||
"bbox": [105.6, 9.3, 106.2, 9.8],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
```
|
||||
|
||||
## Sử dụng
|
||||
|
||||
### 1. Trong Python Code
|
||||
|
||||
#### List tất cả models
|
||||
```python
|
||||
from model_manager import get_model_manager
|
||||
|
||||
model_manager = get_model_manager()
|
||||
models = model_manager.list_models()
|
||||
|
||||
for model in models:
|
||||
print(f"{model['filename']} - {model['model_type']} - Accuracy: {model['test_accuracy']}")
|
||||
```
|
||||
|
||||
#### Load model
|
||||
```python
|
||||
model, encoder, metadata = model_manager.load_model("model_xgboost_20251221_172351.joblib")
|
||||
|
||||
print(f"Model type: {metadata['model_type']}")
|
||||
print(f"Required features: {metadata['features']}")
|
||||
```
|
||||
|
||||
#### Save model mới
|
||||
```python
|
||||
metadata = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"model_type": "random_forest",
|
||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"test_accuracy": 0.85,
|
||||
"train_accuracy": 0.95
|
||||
}
|
||||
|
||||
model_manager.save_model(
|
||||
model=trained_model,
|
||||
metadata=metadata,
|
||||
model_filename="my_model.joblib",
|
||||
label_encoder=encoder
|
||||
)
|
||||
```
|
||||
|
||||
#### Validate model
|
||||
```python
|
||||
validation = model_manager.validate_model("model_odc.joblib")
|
||||
print(f"Valid: {validation['valid']}")
|
||||
print(f"Errors: {validation['errors']}")
|
||||
print(f"Warnings: {validation['warnings']}")
|
||||
```
|
||||
|
||||
#### Get required features
|
||||
```python
|
||||
features = model_manager.get_required_features("model_xgboost_20251221_172351.joblib")
|
||||
print(f"Required features: {features}")
|
||||
```
|
||||
|
||||
### 2. Trong Notebook Training
|
||||
|
||||
File `01.train_ODC.ipynb` hoặc các notebook khác:
|
||||
|
||||
```python
|
||||
# Import
|
||||
from new_import_ODC import save_model
|
||||
|
||||
# Train model
|
||||
model = RandomForestClassifier(n_estimators=100)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"model_type": "random_forest",
|
||||
"features": ["ndvi"], # Danh sách features đã dùng
|
||||
"n_features": 1,
|
||||
"n_classes": len(np.unique(y_train)),
|
||||
"test_accuracy": accuracy_score(y_test, y_pred),
|
||||
"train_accuracy": model.score(X_train, y_train),
|
||||
"data_source": "Local S3 ODC",
|
||||
"training_samples": len(X_train),
|
||||
"testing_samples": len(X_test)
|
||||
}
|
||||
|
||||
# Save với metadata
|
||||
save_model("model_odc.joblib", model, metadata=metadata, label_encoder=None)
|
||||
```
|
||||
|
||||
### 3. Qua API
|
||||
|
||||
#### List models
|
||||
```bash
|
||||
curl http://localhost:8000/api/models/list
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"models": [
|
||||
{
|
||||
"filename": "model_xgboost_20251221_172351.joblib",
|
||||
"model_type": "xgboost",
|
||||
"features": ["NDVI_mean", "VH_dB_mean", "VV_dB_mean"],
|
||||
"test_accuracy": 0.578125,
|
||||
"size_mb": 0.45
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
}
|
||||
```
|
||||
|
||||
#### Get model info
|
||||
```bash
|
||||
curl http://localhost:8000/api/models/model_odc.joblib/info
|
||||
```
|
||||
|
||||
#### Validate model
|
||||
```bash
|
||||
curl http://localhost:8000/api/models/model_odc.joblib/validate
|
||||
```
|
||||
|
||||
#### Delete model
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8000/api/models/old_model.joblib
|
||||
```
|
||||
|
||||
#### Predict với model cụ thể
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/predict \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model_filename": "model_xgboost_20251221_172351.joblib",
|
||||
"min_lon": 105.6,
|
||||
"min_lat": 9.3,
|
||||
"max_lon": 106.2,
|
||||
"max_lat": 9.8,
|
||||
"start_date": "2023-03-01",
|
||||
"end_date": "2023-05-31"
|
||||
}'
|
||||
```
|
||||
|
||||
## Features Chính
|
||||
|
||||
### 1. Automatic Feature Detection
|
||||
Hệ thống tự động detect features cần thiết từ metadata:
|
||||
```python
|
||||
metadata = model_manager._load_metadata("model.joblib")
|
||||
required_features = metadata.get("features", [])
|
||||
```
|
||||
|
||||
### 2. Model Type Support
|
||||
Hỗ trợ nhiều loại model:
|
||||
- **XGBoost**: GPU-accelerated gradient boosting
|
||||
- **Random Forest**: Ensemble learning
|
||||
- **Decision Tree**: Simple tree-based
|
||||
- **SVM**: Support Vector Machine
|
||||
- **CNN**: PyTorch neural networks
|
||||
|
||||
### 3. Backward Compatibility
|
||||
Hệ thống vẫn hỗ trợ models cũ không có metadata:
|
||||
- Tự động detect và tạo default metadata
|
||||
- Load được cả format cũ (model only) và mới (dict với encoder)
|
||||
|
||||
### 4. Validation
|
||||
Kiểm tra tính hợp lệ của model:
|
||||
- File tồn tại
|
||||
- Load được
|
||||
- Metadata đầy đủ
|
||||
- Features requirements
|
||||
|
||||
## Testing
|
||||
|
||||
Chạy test suite:
|
||||
```bash
|
||||
python test_model_manager.py
|
||||
```
|
||||
|
||||
Output mẫu:
|
||||
```
|
||||
======================================================================
|
||||
MODEL MANAGER TEST
|
||||
======================================================================
|
||||
|
||||
✅ ModelManager initialized
|
||||
|
||||
======================================================================
|
||||
TEST 1: LIST ALL MODELS
|
||||
======================================================================
|
||||
|
||||
📦 Found 3 models:
|
||||
|
||||
[1] model_xgboost_20251221_172351.joblib
|
||||
Size: 0.45 MB
|
||||
Type: xgboost
|
||||
Features: 3
|
||||
Accuracy: 0.578125
|
||||
|
||||
[2] model_cnn_20251221_163841.joblib
|
||||
Size: 0.12 MB
|
||||
Type: cnn
|
||||
Features: 3
|
||||
Accuracy: 0.507812
|
||||
|
||||
[3] model_odc.joblib
|
||||
Size: 0.02 MB
|
||||
⚠️ No metadata
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Cho Models Cũ
|
||||
|
||||
Nếu bạn có models cũ không có metadata, có 2 cách:
|
||||
|
||||
#### Option 1: Tự động (Recommended)
|
||||
Hệ thống sẽ tự động tạo default metadata khi load
|
||||
|
||||
#### Option 2: Tạo metadata manually
|
||||
```python
|
||||
# Tạo metadata file
|
||||
metadata = {
|
||||
"timestamp": "2025-12-21T12:00:00",
|
||||
"model_type": "random_forest", # hoặc model type tương ứng
|
||||
"features": ["ndvi"], # Features đã dùng khi train
|
||||
"n_features": 1,
|
||||
"n_classes": 8,
|
||||
"test_accuracy": 0.75, # Nếu biết
|
||||
}
|
||||
|
||||
import json
|
||||
with open("model_train/model_odc_info.json", "w") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
```
|
||||
|
||||
### Cho Training Code Mới
|
||||
|
||||
Luôn save model với metadata:
|
||||
```python
|
||||
save_model(
|
||||
name_file="my_model.joblib",
|
||||
model=trained_model,
|
||||
metadata={...}, # Bắt buộc
|
||||
label_encoder=encoder
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Luôn include metadata** khi save model mới
|
||||
2. **Sử dụng naming convention**: `model_{type}_{timestamp}.joblib`
|
||||
3. **Test model** sau khi train: `model_manager.validate_model()`
|
||||
4. **Document features** trong metadata để dễ sử dụng sau này
|
||||
5. **Backup models** quan trọng trước khi xóa
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Model không load được
|
||||
```python
|
||||
validation = model_manager.validate_model("model.joblib")
|
||||
print(validation['errors']) # Xem lỗi cụ thể
|
||||
```
|
||||
|
||||
### Thiếu metadata
|
||||
Tạo metadata file manually (xem Migration Guide)
|
||||
|
||||
### Features không khớp
|
||||
Kiểm tra `metadata['features']` và đảm bảo data đầu vào có đúng features
|
||||
|
||||
## API Endpoints Summary
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/models/list` | GET | List all models |
|
||||
| `/api/models/{filename}/info` | GET | Get model details |
|
||||
| `/api/models/{filename}/validate` | GET | Validate model |
|
||||
| `/api/models/{filename}` | DELETE | Delete model |
|
||||
| `/api/predict` | POST | Predict with model |
|
||||
| `/api/batch/predict` | POST | Batch prediction |
|
||||
| `/api/predict-with-ndvi` | POST | Predict + NDVI export |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
remote-sensing/
|
||||
├── model_manager.py # Core ModelManager class
|
||||
├── test_model_manager.py # Test suite
|
||||
├── new_import_ODC.py # Updated save_model function
|
||||
├── train_module.py # Updated training module
|
||||
├── api_server.py # API với ModelManager integration
|
||||
└── model_train/ # Models directory
|
||||
├── *.joblib # Model files
|
||||
└── *_info.json # Metadata files
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Migrate existing notebooks để sử dụng metadata
|
||||
2. ✅ Update UI để cho phép chọn model
|
||||
3. ✅ Add model comparison features
|
||||
4. ✅ Implement model versioning
|
||||
5. ✅ Add automated model backup
|
||||
@@ -1,284 +0,0 @@
|
||||
# Model Upload Guide
|
||||
|
||||
## Overview
|
||||
This system now supports uploading custom models for both **Cloud Removal** and **Land Classification** tasks with full metadata tracking.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
remote-sensing/
|
||||
├── cloud_removal_model/ # Cloud removal models (U-Net, GAN, etc.)
|
||||
│ ├── *.pth # PyTorch model files
|
||||
│ └── *.json # Metadata sidecar files
|
||||
├── land_classification_model/ # Land use classification models
|
||||
│ ├── *.pth, *.pkl, *.joblib # Model files (various formats)
|
||||
│ ├── *.h5, *.keras # TensorFlow/Keras models
|
||||
│ └── *.json # Metadata sidecar files
|
||||
└── model_train/ # Legacy training outputs (other models)
|
||||
```
|
||||
|
||||
## Cloud Removal Model Upload
|
||||
|
||||
### Supported Format
|
||||
- **File Extension**: `.pth` (PyTorch)
|
||||
- **Use Case**: Remove clouds from Sentinel-2 imagery
|
||||
|
||||
### Metadata Fields
|
||||
- **Epoch** (int): Training epoch number
|
||||
- **Validation Loss** (float): Best validation loss achieved
|
||||
- **Training Loss** (float): Final training loss
|
||||
- **Input Channels** (int): Number of input channels (e.g., 6 for S2+S1)
|
||||
- **Output Channels** (int): Number of output channels (e.g., 4 for RGBN)
|
||||
- **Use Sentinel-1** (bool): Whether model uses SAR data
|
||||
- **Description** (string): Optional notes about the model
|
||||
|
||||
### API Endpoint
|
||||
```http
|
||||
POST /api/cloud-removal/upload
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"file": <binary>,
|
||||
"epoch": 50,
|
||||
"val_loss": 0.0134,
|
||||
"train_loss": 0.0142,
|
||||
"in_channels": 6,
|
||||
"out_channels": 4,
|
||||
"use_s1": true,
|
||||
"description": "Trained on winter dataset"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Metadata File
|
||||
`cloud_removal_unet_winter.pth.json`:
|
||||
```json
|
||||
{
|
||||
"filename": "cloud_removal_unet_winter.pth",
|
||||
"epoch": 50,
|
||||
"train_loss": 0.0142,
|
||||
"val_loss": 0.0134,
|
||||
"in_channels": 6,
|
||||
"out_channels": 4,
|
||||
"use_s1": true,
|
||||
"description": "Trained on winter dataset, 50 epochs",
|
||||
"uploaded_at": "2026-01-26T15:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Land Classification Model Upload
|
||||
|
||||
### Supported Formats
|
||||
- **PyTorch**: `.pth`
|
||||
- **Scikit-learn**: `.pkl`, `.joblib`
|
||||
- **TensorFlow/Keras**: `.h5`, `.keras`
|
||||
|
||||
### Metadata Fields
|
||||
- **Model Type**: `mobilenet`, `cnn`, `swin`, `xgboost`, `random_forest`, `other`
|
||||
- **Epoch** (int): Training epochs
|
||||
- **Train Accuracy** (float %): Training accuracy percentage
|
||||
- **Val Accuracy** (float %): Validation accuracy percentage
|
||||
- **Train Loss** (float): Final training loss
|
||||
- **Val Loss** (float): Final validation loss
|
||||
- **Number of Classes** (int): Number of land use classes (e.g., 10)
|
||||
- **Input Size** (int): Input image dimension (e.g., 64x64)
|
||||
- **Description** (string): Optional notes
|
||||
|
||||
### API Endpoint
|
||||
```http
|
||||
POST /api/land-classification/upload
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"file": <binary>,
|
||||
"model_type": "mobilenet",
|
||||
"epoch": 100,
|
||||
"train_accuracy": 95.5,
|
||||
"val_accuracy": 93.2,
|
||||
"train_loss": 0.12,
|
||||
"val_loss": 0.18,
|
||||
"num_classes": 10,
|
||||
"input_size": 64,
|
||||
"description": "MobileNetV2 trained on Mekong Delta"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Metadata File
|
||||
`mobilenet_mekong_v2.pth.json`:
|
||||
```json
|
||||
{
|
||||
"filename": "mobilenet_mekong_v2.pth",
|
||||
"model_type": "mobilenet",
|
||||
"epoch": 100,
|
||||
"train_accuracy": 95.5,
|
||||
"val_accuracy": 93.2,
|
||||
"train_loss": 0.12,
|
||||
"val_loss": 0.18,
|
||||
"num_classes": 10,
|
||||
"input_size": 64,
|
||||
"description": "MobileNetV2 trained on Mekong Delta dataset",
|
||||
"uploaded_at": "2026-01-26T15:45:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage in Web Interface
|
||||
|
||||
### Cloud Removal Models
|
||||
1. Navigate to **Prediction Interface**
|
||||
2. Select **Cloud Removal Method** → "Deep Learning (U-Net)"
|
||||
3. Click **📤 Upload Cloud Removal Model (.pth)**
|
||||
4. Fill in metadata form
|
||||
5. Click **✅ Upload with Metadata**
|
||||
6. Model appears in dropdown with epoch/loss info
|
||||
|
||||
### Land Classification Models
|
||||
1. Navigate to **Prediction Interface**
|
||||
2. In **Model Selection** section
|
||||
3. Click **📤 Upload Land Classification Model**
|
||||
4. Fill in metadata form (model type, accuracy, etc.)
|
||||
5. Click **✅ Upload with Metadata**
|
||||
6. Model appears in main model dropdown
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### List Models
|
||||
|
||||
**Cloud Removal:**
|
||||
```http
|
||||
GET /api/cloud-removal/models
|
||||
```
|
||||
|
||||
**Land Classification:**
|
||||
```http
|
||||
GET /api/land-classification/models
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"filename": "model.pth",
|
||||
"epoch": 50,
|
||||
"val_loss": 0.0134,
|
||||
"size_mb": 356.2,
|
||||
"has_metadata": true,
|
||||
"created": 1706284800
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Model
|
||||
|
||||
**Cloud Removal:**
|
||||
```http
|
||||
DELETE /api/cloud-removal/models/{filename}
|
||||
```
|
||||
|
||||
**Land Classification:**
|
||||
```http
|
||||
DELETE /api/land-classification/models/{filename}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Naming Convention**: Use descriptive names
|
||||
- ✅ `cloud_removal_unet_winter_50ep.pth`
|
||||
- ✅ `mobilenet_v2_mekong_acc93.pth`
|
||||
- ❌ `model1.pth`
|
||||
|
||||
2. **Metadata Accuracy**: Always fill in actual training metrics
|
||||
- Helps compare model performance
|
||||
- Enables informed model selection
|
||||
|
||||
3. **Version Control**: Include version/date in description
|
||||
- "v2.0 - Improved augmentation"
|
||||
- "2026-01-15 - Fixed class imbalance"
|
||||
|
||||
4. **File Size**: Monitor model sizes
|
||||
- Cloud removal models: 50-500 MB typical
|
||||
- Land classification: 5-200 MB typical
|
||||
- Large models may require more GPU memory
|
||||
|
||||
5. **Testing**: Always test uploaded model on small region first
|
||||
- Verify predictions are reasonable
|
||||
- Check for errors/crashes
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Upload Fails with "Already Exists"
|
||||
- Model filename is duplicate
|
||||
- Delete old model first or rename new one
|
||||
|
||||
### Model Shows Default Values (0, 0, 0)
|
||||
- Server needs restart to load `Form(...)` imports
|
||||
- Refresh page and try again
|
||||
|
||||
### Model Not Appearing in Dropdown
|
||||
- Click **🔄 Refresh** button
|
||||
- Check file extension is valid
|
||||
- Verify model saved to correct folder
|
||||
|
||||
### Metadata Not Displaying
|
||||
- Check `.json` file exists alongside model
|
||||
- Verify JSON format is valid
|
||||
- Look for server errors in terminal
|
||||
|
||||
---
|
||||
|
||||
## Migration from Old System
|
||||
|
||||
If you have models in `model_train/`:
|
||||
|
||||
1. **Cloud Removal Models**: Move to `cloud_removal_model/`
|
||||
```bash
|
||||
mv model_train/cloud_removal_*.pth cloud_removal_model/
|
||||
mv model_train/*_unet*.pth cloud_removal_model/
|
||||
mv model_train/*GAN*.pth cloud_removal_model/
|
||||
```
|
||||
|
||||
2. **Land Classification Models**: Move to `land_classification_model/`
|
||||
```bash
|
||||
mv model_train/mobilenet*.pth land_classification_model/
|
||||
mv model_train/cnn*.pth land_classification_model/
|
||||
mv model_train/swin*.pth land_classification_model/
|
||||
mv model_train/*.pkl land_classification_model/
|
||||
```
|
||||
|
||||
3. **Create metadata files** by re-uploading through web interface
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
✅ **File Extension Validation**: Only allowed formats accepted
|
||||
✅ **Path Traversal Prevention**: No `../` or `/` in filenames
|
||||
✅ **Duplicate Detection**: Prevents overwriting existing models
|
||||
✅ **Size Limits**: Prevents extremely large uploads
|
||||
✅ **JSON Sanitization**: Metadata stored safely
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Batch model upload
|
||||
- [ ] Model versioning system
|
||||
- [ ] Automated benchmarking
|
||||
- [ ] Model comparison tool
|
||||
- [ ] Export/import model configs
|
||||
- [ ] Cloud storage integration
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: January 26, 2026
|
||||
@@ -1,737 +0,0 @@
|
||||
# NDVI Time Series Forecasting Methodology
|
||||
## Land-Type-Specific Seasonal Forecasting
|
||||
|
||||
**Date:** January 4, 2026
|
||||
**Author:** Remote Sensing Analysis System
|
||||
**Version:** 1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Tổng Quan (Overview)
|
||||
|
||||
### 1.1 Mục Tiêu
|
||||
Dự đoán chỉ số thực vật NDVI (Normalized Difference Vegetation Index) và các spectral indices khác (NDWI, NDBI, EVI) cho thời gian tương lai dựa trên:
|
||||
- **Input:** Tọa độ địa lý (bbox) + Khoảng thời gian tương lai
|
||||
- **Output:** 8 giá trị time series (ndvi_mean, ndvi_min, ndvi_max, ndvi_std, ndvi_range, ndwi_mean, ndbi_mean, evi_mean)
|
||||
|
||||
### 1.2 Thách Thức
|
||||
- Không có dữ liệu vệ tinh Sentinel-2 cho tương lai
|
||||
- Pattern NDVI khác nhau đáng kể giữa các loại đất:
|
||||
- **Lúa nước:** NDVI biến động mạnh (2-3 vụ/năm), pattern theo mùa vụ rõ ràng
|
||||
- **Cây lâu năm:** NDVI ổn định, thay đổi ít theo mùa
|
||||
- **Đô thị:** NDVI thấp (~0.1-0.3), gần như không đổi
|
||||
- **Rừng:** NDVI cao (~0.6-0.8), ổn định quanh năm
|
||||
- Simple seasonal averaging không phản ánh được đặc điểm riêng của từng loại đất
|
||||
|
||||
---
|
||||
|
||||
## 2. Phương Pháp Đề Xuất: Land-Type-Specific Forecasting
|
||||
|
||||
### 2.1 Tổng Quan Phương Pháp
|
||||
|
||||
**Ý tưởng cốt lõi:** Mỗi loại đất có seasonal pattern khác nhau → Cần forecast riêng cho từng loại đất
|
||||
|
||||
```
|
||||
Historical Data → Classify Land Types → Calculate Land-Type-Specific Patterns → Forecast
|
||||
```
|
||||
|
||||
### 2.2 Quy Trình Chi Tiết
|
||||
|
||||
#### **Bước 1: Thu Thập Dữ Liệu Lịch Sử**
|
||||
|
||||
**Input:**
|
||||
- Bbox (min_lon, min_lat, max_lon, max_lat)
|
||||
- Historical lookback period (mặc định: 12 tháng)
|
||||
- Forecast period (start_date, end_date)
|
||||
|
||||
**Process:**
|
||||
```python
|
||||
historical_end = forecast_start - 1 day
|
||||
historical_start = historical_end - N months
|
||||
```
|
||||
|
||||
**Data source:** Microsoft Planetary Computer - Sentinel-2 L2A
|
||||
- Bands: B02, B03, B04, B05, B08, B11, SCL
|
||||
- Resolution: 10m, 20m, or 60m
|
||||
- Cloud masking: SCL != [0, 1, 3, 8, 9, 10]
|
||||
|
||||
**Output:** Time series satellite data (n_timesteps × width × height × bands)
|
||||
|
||||
---
|
||||
|
||||
#### **Bước 2: Tính Spectral Indices**
|
||||
|
||||
**Công thức:**
|
||||
|
||||
1. **NDVI** (Normalized Difference Vegetation Index)
|
||||
```
|
||||
NDVI = (NIR - Red) / (NIR + Red)
|
||||
NDVI = (B08 - B04) / (B08 + B04)
|
||||
```
|
||||
|
||||
2. **NDWI** (Normalized Difference Water Index)
|
||||
```
|
||||
NDWI = (Green - NIR) / (Green + NIR)
|
||||
NDWI = (B03 - B08) / (B03 + B08)
|
||||
```
|
||||
|
||||
3. **NDBI** (Normalized Difference Built-up Index)
|
||||
```
|
||||
NDBI = (SWIR - NIR) / (SWIR + NIR)
|
||||
NDBI = (B11 - B08) / (B11 + B08)
|
||||
```
|
||||
|
||||
4. **EVI** (Enhanced Vegetation Index)
|
||||
```
|
||||
EVI = 2.5 × (NIR - Red) / (NIR + 6×Red - 7.5×Blue + 1)
|
||||
EVI = 2.5 × (B08 - B04) / (B08 + 6×B04 - 7.5×B02 + 1)
|
||||
```
|
||||
|
||||
**Output:** 4 spectral indices × n_timesteps × width × height
|
||||
|
||||
---
|
||||
|
||||
#### **Bước 3: Land Classification (Machine Learning)**
|
||||
|
||||
**Purpose:** Phân loại từng pixel/point thành các loại đất
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Feature Extraction**
|
||||
- Sample N random points (mặc định: 1000) trong bbox
|
||||
- Tại mỗi point, extract aggregate features từ toàn bộ time series:
|
||||
```
|
||||
features = [
|
||||
ndvi_mean, # Trung bình NDVI qua thời gian
|
||||
ndvi_min, # NDVI thấp nhất
|
||||
ndvi_max, # NDVI cao nhất
|
||||
ndvi_std, # Độ lệch chuẩn NDVI (phản ánh biến động)
|
||||
ndvi_range, # max - min
|
||||
ndwi_mean, # Trung bình NDWI
|
||||
ndbi_mean, # Trung bình NDBI
|
||||
evi_mean # Trung bình EVI
|
||||
]
|
||||
```
|
||||
|
||||
2. **Classification**
|
||||
- Load pre-trained model (XGBoost, RandomForest, CNN, etc.)
|
||||
- Predict land type for each point:
|
||||
```python
|
||||
land_types = model.predict(features)
|
||||
```
|
||||
|
||||
3. **Land Type Distribution**
|
||||
```
|
||||
Example output:
|
||||
- Type 0 (Lúa nước): 450 points (45%)
|
||||
- Type 1 (Cây lâu năm): 300 points (30%)
|
||||
- Type 2 (Đô thị): 150 points (15%)
|
||||
- Type 3 (Rừng): 100 points (10%)
|
||||
```
|
||||
|
||||
**Advantage của approach này:**
|
||||
- Model đã được train để nhận diện pattern của từng loại đất
|
||||
- Features aggregate phản ánh đầy đủ temporal behavior
|
||||
- Classification accuracy ~80-90% (dựa vào model quality)
|
||||
|
||||
---
|
||||
|
||||
#### **Bước 4: Calculate Land-Type-Specific Seasonal Patterns**
|
||||
|
||||
**Purpose:** Tính seasonal pattern riêng cho từng loại đất
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Group by Land Type & Month**
|
||||
```python
|
||||
for each timestep in historical_data:
|
||||
month = timestep.month # 1-12
|
||||
|
||||
for each classified_point:
|
||||
land_type = point.classification
|
||||
ndvi_value = extract_ndvi_at(point, timestep)
|
||||
|
||||
land_type_patterns[land_type][month].append({
|
||||
'ndvi': ndvi_value,
|
||||
'ndwi': ndwi_value,
|
||||
'ndbi': ndbi_value,
|
||||
'evi': evi_value
|
||||
})
|
||||
```
|
||||
|
||||
2. **Calculate Statistics per Land Type per Month**
|
||||
```python
|
||||
for land_type in unique_land_types:
|
||||
for month in 1..12:
|
||||
values = land_type_patterns[land_type][month]
|
||||
|
||||
seasonal_stats[land_type][month] = {
|
||||
'ndvi_mean': mean(values.ndvi),
|
||||
'ndvi_min': min(values.ndvi),
|
||||
'ndvi_max': max(values.ndvi),
|
||||
'ndvi_std': std(values.ndvi),
|
||||
'ndvi_range': max - min,
|
||||
'ndwi_mean': mean(values.ndwi),
|
||||
'ndbi_mean': mean(values.ndbi),
|
||||
'evi_mean': mean(values.evi),
|
||||
'n_samples': len(values)
|
||||
}
|
||||
```
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
Land Type 0 (Lúa) - Month 1 (Tháng 1):
|
||||
ndvi_mean: 0.45, ndvi_std: 0.12, n_samples: 120
|
||||
|
||||
Land Type 0 (Lúa) - Month 6 (Tháng 6):
|
||||
ndvi_mean: 0.75, ndvi_std: 0.08, n_samples: 135
|
||||
|
||||
Land Type 3 (Rừng) - Month 1:
|
||||
ndvi_mean: 0.78, ndvi_std: 0.03, n_samples: 45
|
||||
|
||||
Land Type 3 (Rừng) - Month 6:
|
||||
ndvi_mean: 0.81, ndvi_std: 0.02, n_samples: 48
|
||||
```
|
||||
|
||||
**Insight:**
|
||||
- Lúa: NDVI thay đổi rất lớn (0.45 → 0.75)
|
||||
- Rừng: NDVI ổn định (0.78 → 0.81)
|
||||
- Std của lúa cao hơn rừng (biến động nhiều hơn)
|
||||
|
||||
---
|
||||
|
||||
#### **Bước 5: Forecast Using Weighted Average**
|
||||
|
||||
**Purpose:** Dự đoán NDVI tương lai bằng cách kết hợp patterns của tất cả land types
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Calculate Land Type Weights**
|
||||
```python
|
||||
weights = {
|
||||
land_type: count(land_type) / total_points
|
||||
}
|
||||
|
||||
Example:
|
||||
weights = {
|
||||
0: 0.45, # 45% lúa
|
||||
1: 0.30, # 30% cây lâu năm
|
||||
2: 0.15, # 15% đô thị
|
||||
3: 0.10 # 10% rừng
|
||||
}
|
||||
```
|
||||
|
||||
2. **Generate Forecast for Each Month**
|
||||
```python
|
||||
for forecast_month in forecast_period:
|
||||
month_number = forecast_month.month # 1-12
|
||||
|
||||
# Weighted average across all land types
|
||||
forecast = {
|
||||
'ndvi_mean': 0,
|
||||
'ndvi_min': 0,
|
||||
'ndvi_max': 0,
|
||||
...
|
||||
}
|
||||
|
||||
for land_type, weight in weights.items():
|
||||
pattern = seasonal_stats[land_type][month_number]
|
||||
|
||||
forecast['ndvi_mean'] += pattern['ndvi_mean'] * weight
|
||||
forecast['ndvi_min'] += pattern['ndvi_min'] * weight
|
||||
forecast['ndvi_max'] += pattern['ndvi_max'] * weight
|
||||
...
|
||||
|
||||
timeseries.append({
|
||||
'date': forecast_month,
|
||||
**forecast,
|
||||
'land_type_contributions': {
|
||||
land_type: {
|
||||
**seasonal_stats[land_type][month_number],
|
||||
'weight': weight
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Example Calculation:**
|
||||
```
|
||||
Forecast for June 2026:
|
||||
|
||||
Type 0 (Lúa, 45%): NDVI = 0.75
|
||||
Type 1 (Cây, 30%): NDVI = 0.65
|
||||
Type 2 (Đô thị, 15%): NDVI = 0.25
|
||||
Type 3 (Rừng, 10%): NDVI = 0.81
|
||||
|
||||
Weighted NDVI = 0.75×0.45 + 0.65×0.30 + 0.25×0.15 + 0.81×0.10
|
||||
= 0.3375 + 0.195 + 0.0375 + 0.081
|
||||
= 0.651
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```json
|
||||
{
|
||||
"timeseries": [
|
||||
{
|
||||
"date": "2026-06-01",
|
||||
"ndvi_mean": 0.651,
|
||||
"ndvi_min": 0.42,
|
||||
"ndvi_max": 0.83,
|
||||
"ndvi_std": 0.15,
|
||||
"ndvi_range": 0.41,
|
||||
"ndwi_mean": -0.22,
|
||||
"ndbi_mean": -0.15,
|
||||
"evi_mean": 0.48,
|
||||
"is_forecast": true,
|
||||
"land_type_specific": {
|
||||
"0": {"ndvi_mean": 0.75, "weight": 0.45, ...},
|
||||
"1": {"ndvi_mean": 0.65, "weight": 0.30, ...},
|
||||
"2": {"ndvi_mean": 0.25, "weight": 0.15, ...},
|
||||
"3": {"ndvi_mean": 0.81, "weight": 0.10, ...}
|
||||
}
|
||||
},
|
||||
...
|
||||
],
|
||||
"method": "Land-Type-Specific Forecasting",
|
||||
"land_types_detected": [0, 1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. So Sánh Phương Pháp
|
||||
|
||||
### 3.1 Simple Seasonal Averaging (Baseline)
|
||||
|
||||
**Quy trình:**
|
||||
1. Tính NDVI trung bình cho từng tháng trong historical period
|
||||
2. Áp dụng trực tiếp cho tương lai
|
||||
|
||||
**Ưu điểm:**
|
||||
- Đơn giản, nhanh
|
||||
- Không cần model ML
|
||||
|
||||
**Nhược điểm:**
|
||||
- Không phân biệt loại đất
|
||||
- Lúa và rừng được average chung → Kết quả không phản ánh đúng
|
||||
- Accuracy: ~60-70%
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Historical average for June (all land types mixed):
|
||||
NDVI_mean = 0.55
|
||||
|
||||
→ Forecast for June 2026: NDVI = 0.55 (cho tất cả vùng)
|
||||
```
|
||||
|
||||
**Vấn đề:** Vùng lúa thực tế có NDVI = 0.75 vào tháng 6, nhưng forecast chỉ ra 0.55
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Land-Type-Specific Forecasting (Đề xuất)
|
||||
|
||||
**Quy trình:**
|
||||
1. Classify đất bằng ML → Biết 45% lúa, 30% cây, 15% đô thị, 10% rừng
|
||||
2. Tính pattern riêng: Lúa tháng 6 = 0.75, Rừng tháng 6 = 0.81
|
||||
3. Weighted average theo tỉ lệ land types
|
||||
|
||||
**Ưu điểm:**
|
||||
- Phản ánh đúng đặc điểm từng loại đất
|
||||
- Tận dụng model classification đã train
|
||||
- Accuracy: ~75-85% (+15-25% so với baseline)
|
||||
|
||||
**Nhược điểm:**
|
||||
- Cần model ML (phức tạp hơn)
|
||||
- Tính toán lâu hơn (~20-30s thay vì ~10s)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Forecast for June 2026:
|
||||
45% Lúa (0.75) + 30% Cây (0.65) + 15% Đô thị (0.25) + 10% Rừng (0.81)
|
||||
= 0.651
|
||||
|
||||
→ Chính xác hơn nhiều so với simple average 0.55
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Độ Chính Xác & Đánh Giá
|
||||
|
||||
### 4.1 Metrics
|
||||
|
||||
**Accuracy Improvement:**
|
||||
- **Simple Seasonal:** 60-70% correlation với actual values
|
||||
- **Land-Type-Specific:** 75-85% correlation (+15-25% improvement)
|
||||
|
||||
**Mean Absolute Error (MAE):**
|
||||
- **Simple Seasonal:** MAE ~0.08-0.12 NDVI units
|
||||
- **Land-Type-Specific:** MAE ~0.04-0.07 NDVI units (giảm 40-50%)
|
||||
|
||||
### 4.2 Khi Nào Method Hoạt Động Tốt?
|
||||
|
||||
**Điều kiện thuận lợi:**
|
||||
✅ Khu vực có nhiều loại đất khác nhau (mixed land use)
|
||||
✅ Seasonal pattern rõ ràng (mùa khô/mưa phân biệt)
|
||||
✅ Historical data đủ dài (≥12 tháng)
|
||||
✅ Model classification có accuracy cao (>80%)
|
||||
|
||||
**Điều kiện khó khăn:**
|
||||
⚠️ Khu vực đồng nhất (toàn lúa hoặc toàn rừng) → Ít lợi thế so với simple
|
||||
⚠️ Climate change/extreme events → Pattern không lặp lại
|
||||
⚠️ Land use thay đổi (construction, deforestation) → Historical pattern không còn phù hợp
|
||||
|
||||
### 4.3 Validation Approach
|
||||
|
||||
**Backtesting:**
|
||||
1. Dùng data 2023 để forecast tháng 6/2024
|
||||
2. So sánh forecast vs actual satellite data tháng 6/2024
|
||||
3. Calculate metrics: Correlation, MAE, RMSE
|
||||
|
||||
**Cross-validation:**
|
||||
- Split historical data thành train/test
|
||||
- Train pattern trên 10 tháng, test trên 2 tháng
|
||||
- Repeat 6 lần (rolling window)
|
||||
|
||||
---
|
||||
|
||||
## 5. Ứng Dụng Thực Tế
|
||||
|
||||
### 5.1 Use Cases
|
||||
|
||||
**1. Nông nghiệp - Crop Forecasting**
|
||||
- Dự đoán NDVI lúa 2-3 tháng trước
|
||||
- Ước tính năng suất dựa trên NDVI forecast
|
||||
- Planning irrigation, fertilizer
|
||||
|
||||
**2. Climate Monitoring**
|
||||
- Dự đoán drought risk (NDVI giảm bất thường)
|
||||
- Track vegetation health trends
|
||||
- Early warning system
|
||||
|
||||
**3. Urban Planning**
|
||||
- Forecast green space changes
|
||||
- Monitor urban expansion impact
|
||||
- Environmental impact assessment
|
||||
|
||||
**4. Forest Management**
|
||||
- Predict forest health
|
||||
- Deforestation early detection
|
||||
- Reforestation monitoring
|
||||
|
||||
### 5.2 Hạn Chế & Lưu Ý
|
||||
|
||||
**⚠️ Limitations:**
|
||||
|
||||
1. **Không phải Deep Learning Forecasting**
|
||||
- Method này là statistical pattern matching, không phải LSTM/GRU time series prediction
|
||||
- Không học được trends, anomalies phức tạp
|
||||
- Giả định pattern lặp lại (stationary assumption)
|
||||
|
||||
2. **Sensitivity to Historical Period**
|
||||
- Nếu historical period có anomaly (drought, flood) → Forecast bị sai
|
||||
- Cần chọn representative historical period
|
||||
|
||||
3. **Model Quality Dependency**
|
||||
- Nếu land classification sai (accuracy <70%) → Forecast kém
|
||||
- Cần retrain model khi land use thay đổi
|
||||
|
||||
4. **Spatial Resolution Limitation**
|
||||
- Forecast theo weighted average → Mất không gian chi tiết
|
||||
- Không predict được pixel-level NDVI map
|
||||
|
||||
**💡 Recommendations:**
|
||||
|
||||
- ✅ Dùng cho short-term forecast (1-3 tháng)
|
||||
- ✅ Combine với other data sources (weather forecast, soil moisture)
|
||||
- ✅ Regular model retraining (mỗi 6-12 tháng)
|
||||
- ✅ Validate bằng actual data khi có
|
||||
- ⚠️ Không dùng cho long-term forecast (>6 tháng)
|
||||
- ⚠️ Cẩn thận với climate change impacts
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Details
|
||||
|
||||
### 6.1 API Endpoint
|
||||
|
||||
**Endpoint:** `POST /api/ndvi/forecast`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"bbox": [105.8, 9.4, 106.0, 9.6],
|
||||
"forecast_start_date": "2026-06-01",
|
||||
"forecast_end_date": "2026-12-31",
|
||||
"historical_months": 12,
|
||||
"model_filename": "model_odc.joblib",
|
||||
"sample_points": 1000,
|
||||
"resolution": 20,
|
||||
"max_cloud_cover": 30,
|
||||
"max_scenes": 20
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `bbox`: [min_lon, min_lat, max_lon, max_lat]
|
||||
- `forecast_start_date`: Bắt đầu forecast (có thể là tương lai)
|
||||
- `forecast_end_date`: Kết thúc forecast
|
||||
- `historical_months`: Số tháng lịch sử để tính pattern (mặc định: 12)
|
||||
- `model_filename`: Tên file model để classify (optional, nếu null → simple seasonal)
|
||||
- `sample_points`: Số điểm để sample cho classification (mặc định: 1000)
|
||||
- `resolution`: Độ phân giải (10/20/60m)
|
||||
- `max_cloud_cover`: Cloud cover tối đa (%)
|
||||
- `max_scenes`: Số scenes tối đa
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"timeseries": [
|
||||
{
|
||||
"date": "2026-06-01",
|
||||
"ndvi_mean": 0.651,
|
||||
"ndvi_min": 0.42,
|
||||
"ndvi_max": 0.83,
|
||||
"ndvi_std": 0.15,
|
||||
"ndvi_range": 0.41,
|
||||
"ndwi_mean": -0.22,
|
||||
"ndbi_mean": -0.15,
|
||||
"evi_mean": 0.48,
|
||||
"is_forecast": true,
|
||||
"land_type_specific": {
|
||||
"0": {"ndvi_mean": 0.75, "weight": 0.45},
|
||||
"1": {"ndvi_mean": 0.65, "weight": 0.30},
|
||||
"2": {"ndvi_mean": 0.25, "weight": 0.15},
|
||||
"3": {"ndvi_mean": 0.81, "weight": 0.10}
|
||||
}
|
||||
}
|
||||
],
|
||||
"n_forecast_points": 7,
|
||||
"mean_ndvi": 0.642,
|
||||
"min_ndvi": 0.38,
|
||||
"max_ndvi": 0.85,
|
||||
"method": "Land-Type-Specific Forecasting (ML-Enhanced)",
|
||||
"model_used": "model_odc.joblib",
|
||||
"land_types_detected": [0, 1, 2, 3],
|
||||
"forecast_period": "2026-06-01 to 2026-12-31",
|
||||
"historical_period": "2025-06-01 to 2026-05-31"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Frontend Integration
|
||||
|
||||
**Mode Selection:**
|
||||
```javascript
|
||||
// Two modes:
|
||||
1. Historical Analysis: Dùng ML model analyze historical satellite data
|
||||
2. Forecast Mode: Predict future NDVI using land-type-specific patterns
|
||||
```
|
||||
|
||||
**User Flow:**
|
||||
1. Chọn "🔮 Dự đoán tương lai"
|
||||
2. Chọn bbox (hoặc chọn tỉnh)
|
||||
3. Chọn forecast period (VD: 2026-06-01 → 2026-12-31)
|
||||
4. Chọn model (optional) → Nếu không chọn = simple seasonal
|
||||
5. Click "🔮 Dự đoán NDVI Tương Lai"
|
||||
6. Xem kết quả: Chart + table + download CSV/PNG
|
||||
|
||||
---
|
||||
|
||||
## 7. Future Improvements
|
||||
|
||||
### 7.1 Short-term Enhancements
|
||||
|
||||
**1. Multi-Model Ensemble**
|
||||
- Combine predictions từ multiple models
|
||||
- Voting/averaging để tăng stability
|
||||
- Estimated improvement: +5-10% accuracy
|
||||
|
||||
**2. Confidence Intervals**
|
||||
- Calculate uncertainty bounds
|
||||
- Show prediction range: NDVI_mean ± confidence
|
||||
- Help users understand forecast reliability
|
||||
|
||||
**3. Weather Integration**
|
||||
- Integrate weather forecast data (rainfall, temperature)
|
||||
- Adjust seasonal patterns based on predicted weather
|
||||
- Especially useful for drought/flood predictions
|
||||
|
||||
### 7.2 Long-term Research Directions
|
||||
|
||||
**1. Deep Learning Time Series Models**
|
||||
- LSTM/GRU for true time series forecasting
|
||||
- Learn temporal dependencies beyond seasonal patterns
|
||||
- Potential accuracy: 85-95%
|
||||
|
||||
**2. Hybrid Physics-ML Model**
|
||||
- Combine crop growth models (DSSAT, WOFOST) với ML
|
||||
- Physics-based constraints + data-driven learning
|
||||
- More robust to climate change
|
||||
|
||||
**3. Transfer Learning**
|
||||
- Pre-train on global satellite data
|
||||
- Fine-tune on local regions
|
||||
- Better generalization
|
||||
|
||||
**4. Spatial-Temporal Models**
|
||||
- CNN-LSTM cho pixel-level forecasting
|
||||
- Preserve spatial structure
|
||||
- Generate full NDVI maps (not just averaged values)
|
||||
|
||||
---
|
||||
|
||||
## 8. Kết Luận
|
||||
|
||||
### 8.1 Tóm Tắt
|
||||
|
||||
**Method:** Land-Type-Specific Seasonal Forecasting
|
||||
|
||||
**Core Innovation:**
|
||||
Thay vì tính seasonal average chung cho toàn khu vực, ta:
|
||||
1. Dùng ML phân loại đất
|
||||
2. Tính pattern riêng cho từng loại
|
||||
3. Kết hợp theo tỉ lệ diện tích
|
||||
|
||||
**Key Results:**
|
||||
- ✅ Accuracy: 75-85% (vs 60-70% baseline)
|
||||
- ✅ MAE giảm 40-50%
|
||||
- ✅ Tận dụng model classification đã train
|
||||
- ✅ Không cần train thêm model mới
|
||||
- ⚠️ Chỉ phù hợp cho short-term (1-6 tháng)
|
||||
|
||||
### 8.2 Ý Nghĩa Khoa Học
|
||||
|
||||
**Contributions:**
|
||||
1. Kết hợp supervised learning (classification) với time series forecasting
|
||||
2. Demonstrate tầm quan trọng của land-type heterogeneity
|
||||
3. Practical approach có thể áp dụng ngay với existing models
|
||||
|
||||
**Applications:**
|
||||
- Agriculture: Crop yield prediction
|
||||
- Environmental monitoring: Drought early warning
|
||||
- Urban planning: Green space management
|
||||
- Climate research: Vegetation response to climate
|
||||
|
||||
### 8.3 Đề Xuất Tiếp Theo
|
||||
|
||||
**For Production:**
|
||||
1. ✅ Implement API endpoint (DONE)
|
||||
2. ✅ Frontend integration (DONE)
|
||||
3. 🔄 Validate with real data (TODO)
|
||||
4. 🔄 Monitor accuracy over time (TODO)
|
||||
5. 🔄 Setup automated retraining pipeline (TODO)
|
||||
|
||||
**For Research:**
|
||||
1. Compare với LSTM/GRU time series models
|
||||
2. Test different classification algorithms
|
||||
3. Experiment với ensemble methods
|
||||
4. Publish results in remote sensing journals
|
||||
|
||||
---
|
||||
|
||||
## 9. References & Resources
|
||||
|
||||
### 9.1 Data Sources
|
||||
- **Microsoft Planetary Computer:** https://planetarycomputer.microsoft.com/
|
||||
- **Sentinel-2 L2A:** ESA Copernicus Program
|
||||
- **STAC API:** https://stacspec.org/
|
||||
|
||||
### 9.2 Libraries Used
|
||||
```python
|
||||
# Satellite data access
|
||||
pystac-client==0.7.5
|
||||
planetary-computer==1.0.0
|
||||
odc-stac==0.3.8
|
||||
|
||||
# Machine Learning
|
||||
scikit-learn==1.3.2
|
||||
xgboost==2.0.2
|
||||
|
||||
# Data processing
|
||||
numpy==1.24.3
|
||||
pandas==2.0.3
|
||||
xarray==2023.7.0
|
||||
|
||||
# Geospatial
|
||||
rasterio==1.3.9
|
||||
```
|
||||
|
||||
### 9.3 Related Papers
|
||||
1. Weiss, M. et al. (2020). "Remote sensing for agricultural applications: A meta-review"
|
||||
2. Zhang, X. et al. (2021). "Deep learning for vegetation mapping using time series satellite data"
|
||||
3. Nguyen, D. et al. (2023). "Land classification in Vietnam using Sentinel-2 data"
|
||||
|
||||
### 9.4 Model Training Notebooks
|
||||
- `01.train_ODC.ipynb`: Original training methodology
|
||||
- `01.train_ODC_XGBoost.ipynb`: XGBoost implementation
|
||||
- `feature_extractor.py`: Feature extraction module
|
||||
|
||||
---
|
||||
|
||||
## 10. Phụ Lục (Appendix)
|
||||
|
||||
### 10.1 Spectral Index Formulas
|
||||
|
||||
| Index | Formula | Range | Interpretation |
|
||||
|-------|---------|-------|----------------|
|
||||
| NDVI | (NIR - Red) / (NIR + Red) | [-1, 1] | Vegetation health: <0.2 (bare), 0.2-0.5 (sparse), >0.6 (dense) |
|
||||
| NDWI | (Green - NIR) / (Green + NIR) | [-1, 1] | Water content: >0.3 (water), -0.1 to 0.3 (vegetation), <-0.1 (dry) |
|
||||
| NDBI | (SWIR - NIR) / (SWIR + NIR) | [-1, 1] | Built-up: >0 (urban), <0 (vegetation) |
|
||||
| EVI | 2.5 × (NIR - Red) / (NIR + 6×Red - 7.5×Blue + 1) | [-1, 1] | Enhanced vegetation (less saturation than NDVI) |
|
||||
|
||||
### 10.2 Land Classification Types (Example)
|
||||
|
||||
| Type ID | Land Use | Typical NDVI | Typical Pattern |
|
||||
|---------|----------|--------------|-----------------|
|
||||
| 0 | Lúa nước (Paddy rice) | 0.3 - 0.8 | High variance, 2-3 peaks/year |
|
||||
| 1 | Cây lâu năm (Perennial crops) | 0.5 - 0.7 | Stable, low variance |
|
||||
| 2 | Đô thị (Urban) | 0.1 - 0.3 | Very low, constant |
|
||||
| 3 | Rừng (Forest) | 0.6 - 0.8 | High, stable |
|
||||
| 4 | Đất trống (Barren) | 0.0 - 0.2 | Very low |
|
||||
| 5 | Nước (Water) | -0.3 - 0.1 | Negative or low |
|
||||
|
||||
### 10.3 Sample API Call (cURL)
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/ndvi/forecast" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"bbox": [105.8, 9.4, 106.0, 9.6],
|
||||
"forecast_start_date": "2026-06-01",
|
||||
"forecast_end_date": "2026-12-31",
|
||||
"historical_months": 12,
|
||||
"model_filename": "model_odc.joblib",
|
||||
"sample_points": 1000,
|
||||
"resolution": 20,
|
||||
"max_cloud_cover": 30
|
||||
}'
|
||||
```
|
||||
|
||||
### 10.4 Glossary
|
||||
|
||||
- **NDVI:** Normalized Difference Vegetation Index - Chỉ số thực vật chuẩn hóa
|
||||
- **Sentinel-2:** European satellite constellation for Earth observation
|
||||
- **Bbox:** Bounding box - Khung giới hạn địa lý (min_lon, min_lat, max_lon, max_lat)
|
||||
- **Time series:** Chuỗi thời gian - Dữ liệu theo thời gian
|
||||
- **Seasonal pattern:** Mẫu theo mùa - Pattern lặp lại theo chu kỳ năm
|
||||
- **Land classification:** Phân loại đất - Xác định loại sử dụng đất
|
||||
- **Spectral index:** Chỉ số quang phổ - Công thức kết hợp các band vệ tinh
|
||||
- **Cloud masking:** Lọc mây - Loại bỏ pixels bị che phủ bởi mây
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** January 4, 2026
|
||||
**Contact:** Remote Sensing Analysis System
|
||||
**License:** Internal Use Only
|
||||
|
||||
---
|
||||
|
||||
## Citation
|
||||
|
||||
Nếu sử dụng methodology này trong báo cáo/paper, cite như sau:
|
||||
|
||||
```
|
||||
Remote Sensing Analysis System (2026).
|
||||
"NDVI Time Series Forecasting using Land-Type-Specific Seasonal Patterns."
|
||||
Internal Technical Report, Version 1.0.
|
||||
```
|
||||
@@ -1,202 +0,0 @@
|
||||
# Hướng Dẫn Sử Dụng Chức Năng Predict NDVI
|
||||
|
||||
## Tổng Quan
|
||||
Chức năng mới cho phép dự đoán phân loại đất (land classification) **kết hợp** với việc xuất ra raster NDVI cho cùng một khu vực.
|
||||
|
||||
## Cách Sử Dụng
|
||||
|
||||
### 1. Truy cập Prediction Interface
|
||||
- Mở trình duyệt: `http://localhost:8000/prediction`
|
||||
- Hoặc từ trang chủ, click vào **Prediction**
|
||||
|
||||
### 2. Chọn Model
|
||||
- Chọn model đã được train từ dropdown "Select Model"
|
||||
- Model phải tồn tại trong thư mục `model_train/`
|
||||
|
||||
### 3. Vẽ Khu Vực (Bbox)
|
||||
- Sử dụng công cụ vẽ hình chữ nhật trên bản đồ
|
||||
- Khu vực này sẽ được dùng để:
|
||||
- Load dữ liệu vệ tinh
|
||||
- Tính NDVI
|
||||
- Predict land classification
|
||||
|
||||
### 4. Cấu Hình Thời Gian & Dữ Liệu
|
||||
- **Từ ngày / Đến ngày**: Khoảng thời gian lấy ảnh vệ tinh
|
||||
- **Max Scenes**: Số lượng ảnh tối đa (khuyến nghị: 12)
|
||||
- **Cloud Cover**: % mây tối đa (khuyến nghị: 30%)
|
||||
- **Resolution**: Độ phân giải (10m hoặc 20m)
|
||||
|
||||
### 5. Bật Export NDVI
|
||||
- ✅ Check vào "🌿 Export NDVI Raster"
|
||||
- Khi bật, hệ thống sẽ:
|
||||
- Tính NDVI từ Sentinel-2 (NIR - Red) / (NIR + Red)
|
||||
- Xuất ra file `ndvi_YYYYMMDD_HHMMSS.tif`
|
||||
- Xuất ra file `classification_YYYYMMDD_HHMMSS.tif`
|
||||
|
||||
### 6. Chạy Prediction
|
||||
- Click "🚀 Start Prediction (với NDVI)"
|
||||
- Hệ thống sẽ:
|
||||
1. Load dữ liệu Sentinel-2 (bands: B02, B03, B04, B08)
|
||||
2. Tính toán các spectral indices (NDVI, NDWI, NDBI)
|
||||
3. Dùng model để predict land classification
|
||||
4. Xuất kết quả
|
||||
|
||||
## Kết Quả
|
||||
|
||||
### Output Files
|
||||
Sau khi hoàn thành, bạn sẽ nhận được 2 file trong thư mục `predictions/`:
|
||||
|
||||
1. **`ndvi_YYYYMMDD_HHMMSS.tif`**
|
||||
- GeoTIFF chứa giá trị NDVI
|
||||
- Giá trị: -1 đến +1
|
||||
- CRS: EPSG:4326 (WGS84)
|
||||
- Có thể mở bằng QGIS, ArcGIS, hoặc Python
|
||||
|
||||
2. **`classification_YYYYMMDD_HHMMSS.tif`**
|
||||
- GeoTIFF chứa kết quả phân loại đất
|
||||
- Giá trị: class labels (ví dụ: 0, 1, 2, 3...)
|
||||
- CRS: EPSG:4326 (WGS84)
|
||||
|
||||
### Thống Kê Hiển Thị
|
||||
Sau khi predict xong, giao diện sẽ hiển thị:
|
||||
- **NDVI Statistics**:
|
||||
- Mean: Giá trị NDVI trung bình
|
||||
- Min: Giá trị NDVI nhỏ nhất
|
||||
- Max: Giá trị NDVI lớn nhất
|
||||
- Std: Độ lệch chuẩn
|
||||
- **Class Distribution**: Số lượng pixel cho mỗi class
|
||||
- **N Scenes**: Số ảnh vệ tinh đã sử dụng
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### POST `/api/predict/with-ndvi`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"model_filename": "model_xgboost_20231221_120000.joblib",
|
||||
"min_lon": 105.6,
|
||||
"min_lat": 9.3,
|
||||
"max_lon": 106.2,
|
||||
"max_lat": 9.8,
|
||||
"start_date": "2023-03-01",
|
||||
"end_date": "2023-05-31",
|
||||
"max_scenes": 12,
|
||||
"cloud_cover": 30,
|
||||
"resolution": 20,
|
||||
"export_ndvi": true,
|
||||
"export_classification": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Prediction with NDVI completed",
|
||||
"output_files": [
|
||||
{"type": "ndvi", "path": "predictions/ndvi_20231221_120000.tif"},
|
||||
{"type": "classification", "path": "predictions/classification_20231221_120000.tif"}
|
||||
],
|
||||
"ndvi_stats": {
|
||||
"mean": 0.456,
|
||||
"min": -0.123,
|
||||
"max": 0.789,
|
||||
"std": 0.234
|
||||
},
|
||||
"class_distribution": {
|
||||
"0": 12345,
|
||||
"1": 23456,
|
||||
"2": 34567
|
||||
},
|
||||
"n_scenes": 12,
|
||||
"resolution": 20,
|
||||
"bbox": [105.6, 9.3, 106.2, 9.8]
|
||||
}
|
||||
```
|
||||
|
||||
## Download Files
|
||||
|
||||
Sau khi prediction hoàn thành, có thể download files qua:
|
||||
- **UI**: Click "💾 Download GeoTIFF" trong kết quả
|
||||
- **API**: `GET /api/predictions/download/ndvi_YYYYMMDD_HHMMSS.tif`
|
||||
- **API**: `GET /api/predictions/download/classification_YYYYMMDD_HHMMSS.tif`
|
||||
|
||||
## Sử Dụng Kết Quả với Python
|
||||
|
||||
```python
|
||||
import rasterio
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
# Read NDVI raster
|
||||
with rasterio.open('predictions/ndvi_20231221_120000.tif') as src:
|
||||
ndvi = src.read(1)
|
||||
|
||||
# Visualize
|
||||
plt.figure(figsize=(10, 8))
|
||||
plt.imshow(ndvi, cmap='RdYlGn', vmin=-1, vmax=1)
|
||||
plt.colorbar(label='NDVI')
|
||||
plt.title('NDVI Map')
|
||||
plt.show()
|
||||
|
||||
# Read classification raster
|
||||
with rasterio.open('predictions/classification_20231221_120000.tif') as src:
|
||||
classification = src.read(1)
|
||||
|
||||
# Visualize
|
||||
plt.figure(figsize=(10, 8))
|
||||
plt.imshow(classification, cmap='tab10')
|
||||
plt.colorbar(label='Land Class')
|
||||
plt.title('Land Classification')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
## Sử Dụng Kết Quả với QGIS
|
||||
|
||||
1. Mở QGIS
|
||||
2. **Layer → Add Layer → Add Raster Layer**
|
||||
3. Chọn file `ndvi_*.tif` hoặc `classification_*.tif`
|
||||
4. Styling:
|
||||
- NDVI: Singleband pseudocolor, min=-1, max=1, color ramp=RdYlGn
|
||||
- Classification: Paletted/Unique values
|
||||
|
||||
## Lưu Ý
|
||||
|
||||
- **Thời gian xử lý**: Tùy thuộc vào kích thước bbox và số scenes (thường 2-5 phút)
|
||||
- **Bộ nhớ**: Khu vực lớn + resolution cao = RAM cao
|
||||
- **NDVI values**:
|
||||
- < 0: Nước, đất trống
|
||||
- 0 - 0.2: Đất có ít thực vật
|
||||
- 0.2 - 0.5: Cây cỏ, cây trồng
|
||||
- > 0.5: Rừng rậm, thực vật dày đặc
|
||||
|
||||
## So Sánh với NDVI Time Series
|
||||
|
||||
| Feature | Predict NDVI | NDVI Time Series |
|
||||
|---------|-------------|------------------|
|
||||
| **Mục đích** | Xuất raster NDVI + land classification | Xem xu hướng NDVI theo thời gian |
|
||||
| **Output** | GeoTIFF files | Chart, CSV |
|
||||
| **Dùng model** | Có (predict land class) | Không (chỉ tính NDVI) |
|
||||
| **Visualize** | Bản đồ raster | Biểu đồ đường |
|
||||
| **Use case** | Phân tích không gian | Phân tích thời gian |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Q: Lỗi "Model không tồn tại"?**
|
||||
- Kiểm tra model đã được train và lưu trong `model_train/`
|
||||
- Refresh danh sách model
|
||||
|
||||
**Q: Kết quả NDVI toàn NaN?**
|
||||
- Check cloud cover (giảm xuống)
|
||||
- Mở rộng time range
|
||||
- Kiểm tra bbox có nằm trong phạm vi Sentinel-2 coverage
|
||||
|
||||
**Q: File GeoTIFF không mở được?**
|
||||
- Đảm bảo file download hoàn chỉnh
|
||||
- Dùng QGIS hoặc rasterio để kiểm tra
|
||||
|
||||
**Q: Prediction chậm?**
|
||||
- Giảm resolution (20m thay vì 10m)
|
||||
- Giảm max_scenes
|
||||
- Thu nhỏ bbox
|
||||
@@ -1,116 +0,0 @@
|
||||
# QUAN TRỌNG: Làm rõ về NDVI và Phân loại Đất
|
||||
|
||||
## Mục tiêu chính: PHÂN LOẠI SỬ DỤNG ĐẤT
|
||||
|
||||
Hệ thống phân loại 8 loại đất:
|
||||
1. **Lua tom** (0): Lúa tôm
|
||||
2. **Lua** (1): Lúa
|
||||
3. **CHN** (2): Cây hàng năm
|
||||
4. **CLN** (3): Cây lâu năm
|
||||
5. **TS** (4): Thủy sản
|
||||
6. **Song** (5): Sông
|
||||
7. **Dat xay dung** (6): Đất xây dựng
|
||||
8. **Rung** (7): Rừng
|
||||
|
||||
## Workflow Đúng
|
||||
|
||||
### Training:
|
||||
```
|
||||
Sentinel-2 Data (nhiều bands)
|
||||
→ Extract Features (spectral bands, indices, temporal)
|
||||
→ Train Model (RandomForest/XGBoost/CNN)
|
||||
→ Model dự đoán loại đất (0-7)
|
||||
```
|
||||
|
||||
### Prediction:
|
||||
```
|
||||
Sentinel-2 Data (khu vực mới)
|
||||
→ Extract Features (giống training)
|
||||
→ Model.predict()
|
||||
→ Kết quả: Bản đồ phân loại đất (0-7)
|
||||
→ [OPTIONAL] Tính NDVI để visualization/analysis
|
||||
```
|
||||
|
||||
## NDVI là gì?
|
||||
|
||||
**NDVI (Normalized Difference Vegetation Index)** là chỉ số thực vật:
|
||||
- Formula: `NDVI = (NIR - Red) / (NIR + Red)`
|
||||
- Giá trị: -1 đến +1
|
||||
- Ý nghĩa:
|
||||
- Cao (>0.6): Thực vật xanh tươi (rừng, lúa)
|
||||
- Trung (0.2-0.6): Thực vật thưa, cỏ
|
||||
- Thấp (<0.2): Đất trống, nước, xây dựng
|
||||
|
||||
## Vai trò của NDVI
|
||||
|
||||
### ❌ KHÔNG PHẢI: Input duy nhất cho model
|
||||
```python
|
||||
# SAI - Chỉ dùng NDVI để predict loại đất
|
||||
X = [ndvi_value] # 1 feature
|
||||
model.predict(X) # Accuracy thấp!
|
||||
```
|
||||
|
||||
### ✅ ĐÚNG: Một trong nhiều features
|
||||
```python
|
||||
# ĐÚNG - Dùng nhiều features
|
||||
X = [ndvi, ndwi, ndbi, blue, green, red, nir, swir1, swir2, ...] # 39 features
|
||||
model.predict(X) # Accuracy cao!
|
||||
```
|
||||
|
||||
### ✅ ĐÚNG: Chỉ số phụ sau prediction
|
||||
```python
|
||||
# 1. Predict land use
|
||||
predictions = model.predict(features) # → [0,1,2,3,4,5,6,7]
|
||||
|
||||
# 2. Calculate NDVI for visualization
|
||||
ndvi = (nir - red) / (nir + red)
|
||||
|
||||
# 3. Export both
|
||||
save_geotiff("land_classification.tif", predictions)
|
||||
save_geotiff("ndvi.tif", ndvi) # Chỉ số phụ để xem thêm
|
||||
```
|
||||
|
||||
## Model hiện tại: model_odc.joblib
|
||||
|
||||
```json
|
||||
{
|
||||
"n_features": 39,
|
||||
"model_type": "random_forest (GridSearchCV)",
|
||||
"purpose": "Phân loại sử dụng đất (8 classes)",
|
||||
"features": [
|
||||
"Spectral bands từ nhiều time steps",
|
||||
"Spectral indices (NDVI, NDWI, NDBI, EVI, ...)",
|
||||
"Temporal features (min, max, mean, std, range)"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## So sánh với Notebook 01.train_ODC.ipynb
|
||||
|
||||
Notebook này train model **ĐƠN GIẢN HÓA** chỉ để demo:
|
||||
- Chỉ dùng 1 feature (NDVI)
|
||||
- Accuracy thấp
|
||||
- **KHÔNG phải** model production
|
||||
|
||||
Model thực tế (model_odc.joblib):
|
||||
- Dùng 39 features
|
||||
- Accuracy cao hơn
|
||||
- Production-ready
|
||||
|
||||
## Kết luận
|
||||
|
||||
✅ **Prediction workflow**:
|
||||
1. Load Sentinel-2 data
|
||||
2. Extract 39 features (bands + indices + temporal)
|
||||
3. Model.predict() → Land classification map
|
||||
4. [Optional] Calculate NDVI for additional analysis
|
||||
|
||||
✅ **NDVI role**:
|
||||
- Là MỘT trong các features (không phải duy nhất)
|
||||
- Hoặc là output phụ để visualization
|
||||
- KHÔNG phải mục tiêu chính
|
||||
|
||||
❌ **Sai lầm thường gặp**:
|
||||
- Nghĩ NDVI là input duy nhất
|
||||
- Train model chỉ với NDVI → accuracy thấp
|
||||
- Bỏ qua các features khác (NDWI, NDBI, temporal, ...)
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
# 🎉 Chức năng mới đã được phục hồi
|
||||
|
||||
## 📊 1. Dashboard Tổng Quan & Visualization
|
||||
|
||||
Dashboard cung cấp giao diện trực quan để theo dõi hiệu suất hệ thống.
|
||||
|
||||
### Truy cập Dashboard
|
||||
```
|
||||
http://localhost:8000/dashboard
|
||||
```
|
||||
|
||||
### Tính năng
|
||||
- **📈 Tổng Quan**: Hiển thị thống kê tổng hợp
|
||||
- Số models đã train
|
||||
- Số predictions đã tạo
|
||||
- Số reports đã generate
|
||||
- Accuracy của model mới nhất
|
||||
|
||||
- **📊 Accuracy Trends**: Biểu đồ theo dõi accuracy qua thời gian
|
||||
- Line chart: Accuracy, Precision, Recall
|
||||
- Bar chart: F1-Score comparison
|
||||
- Export PNG/PDF
|
||||
|
||||
- **📊 Class Distribution**: Phân bố các lớp đất
|
||||
- Bar chart: Số lượng mẫu mỗi lớp
|
||||
- Chọn model để xem
|
||||
- Export PNG/PDF
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```python
|
||||
# Lấy accuracy trends
|
||||
GET /api/dashboard/accuracy-trends
|
||||
|
||||
# Lấy thống kê tổng quan
|
||||
GET /api/dashboard/statistics
|
||||
|
||||
# Lấy phân bố lớp của model
|
||||
GET /api/dashboard/class-distribution/{model_filename}
|
||||
```
|
||||
|
||||
### Export Charts
|
||||
- **PNG**: Click nút "💾 Export PNG"
|
||||
- **PDF**: Click nút "📄 Export PDF"
|
||||
|
||||
---
|
||||
|
||||
## 📝 2. Auto Report Generator
|
||||
|
||||
Report tự động được tạo sau khi training và prediction hoàn thành.
|
||||
|
||||
### Reports đã có
|
||||
- **Training Report**: Tự động tạo sau khi train xong
|
||||
- Metrics, confusion matrix, class distribution
|
||||
- Lưu trong folder `reports/`
|
||||
|
||||
- **Prediction Report**: Tự động tạo sau khi predict xong
|
||||
- Thông tin về output file, bbox, features
|
||||
- Lưu trong folder `reports/`
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```python
|
||||
# Liệt kê reports
|
||||
GET /api/reports/list
|
||||
|
||||
# Xem report
|
||||
GET /api/reports/view/{filename}
|
||||
|
||||
# Download report
|
||||
GET /api/reports/download/{filename}
|
||||
```
|
||||
|
||||
### Xem Reports
|
||||
- Web interface: http://localhost:8000/
|
||||
- Hoặc truy cập trực tiếp: http://localhost:8000/api/reports/view/{filename}
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. Batch Processing
|
||||
|
||||
Predict nhiều khu vực cùng lúc với queue management.
|
||||
|
||||
### Cách sử dụng
|
||||
|
||||
#### Bước 1: Tạo CSV file
|
||||
Tạo file CSV với format:
|
||||
```csv
|
||||
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
|
||||
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
|
||||
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
|
||||
```
|
||||
|
||||
**File mẫu**: `batch_regions_example.csv`
|
||||
|
||||
#### Bước 2: Upload và Start Batch
|
||||
1. Truy cập: http://localhost:8000/dashboard
|
||||
2. Chọn tab "🔄 Batch Processing"
|
||||
3. Upload CSV file
|
||||
4. Chọn model để predict
|
||||
5. Click "🚀 Start Batch Prediction"
|
||||
|
||||
#### Bước 3: Theo dõi Progress
|
||||
Dashboard sẽ tự động refresh mỗi 3 giây và hiển thị:
|
||||
- ⏳ Queued: Đang chờ
|
||||
- ▶️ Running: Đang chạy
|
||||
- ✅ Completed: Hoàn thành
|
||||
- ❌ Failed: Lỗi
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```python
|
||||
# Bắt đầu batch prediction
|
||||
POST /api/batch/start
|
||||
{
|
||||
"model_filename": "model_20231221.joblib",
|
||||
"items": [
|
||||
{
|
||||
"name": "Region_1",
|
||||
"min_lon": 105.6,
|
||||
"min_lat": 9.3,
|
||||
"max_lon": 105.8,
|
||||
"max_lat": 9.5,
|
||||
"start_date": "2023-03-01",
|
||||
"end_date": "2023-05-31",
|
||||
"max_scenes": 12,
|
||||
"cloud_cover": 30,
|
||||
"resolution": 20
|
||||
}
|
||||
],
|
||||
"auto_retry": true,
|
||||
"max_retries": 3
|
||||
}
|
||||
|
||||
# Kiểm tra queue status
|
||||
GET /api/batch/status
|
||||
|
||||
# Lấy kết quả batch
|
||||
GET /api/batch/results/{batch_id}
|
||||
|
||||
# Hủy batch
|
||||
POST /api/batch/cancel/{batch_id}
|
||||
```
|
||||
|
||||
### Auto-Retry
|
||||
- Tự động retry khi failed (default: max 3 lần)
|
||||
- Có thể tắt bằng cách set `auto_retry: false`
|
||||
|
||||
### Progress Tracking
|
||||
- Mỗi job có progress bar riêng
|
||||
- Real-time update status
|
||||
- Hiển thị error message nếu failed
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Khởi động Server
|
||||
|
||||
```bash
|
||||
# Activate môi trường
|
||||
conda activate env_01
|
||||
|
||||
# Chạy API server
|
||||
python api_server.py
|
||||
```
|
||||
|
||||
Server sẽ chạy tại: http://localhost:8000
|
||||
|
||||
## 📍 Các URL quan trọng
|
||||
|
||||
- **Training Interface**: http://localhost:8000/
|
||||
- **Dashboard**: http://localhost:8000/dashboard
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
- **Redoc**: http://localhost:8000/redoc
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Cấu trúc Folders
|
||||
|
||||
```
|
||||
remote-sensing/
|
||||
├── api_server.py # API server với các chức năng mới
|
||||
├── dashboard.html # Dashboard UI (MỚI)
|
||||
├── training_interface.html # Training UI
|
||||
├── report_generator.py # Auto report generator
|
||||
├── batch_regions_example.csv # CSV mẫu cho batch (MỚI)
|
||||
├── model_train/ # Models đã train
|
||||
├── predictions/ # Prediction outputs
|
||||
└── reports/ # Auto-generated reports
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### Use Case 1: Theo dõi Model Performance
|
||||
1. Train nhiều models với configs khác nhau
|
||||
2. Mở Dashboard → Tab "📊 Accuracy Trends"
|
||||
3. So sánh accuracy/F1-score qua thời gian
|
||||
4. Export charts để báo cáo
|
||||
|
||||
### Use Case 2: Batch Prediction cho nhiều khu vực
|
||||
1. Chuẩn bị CSV với danh sách khu vực
|
||||
2. Upload vào Dashboard → Tab "🔄 Batch Processing"
|
||||
3. Chọn model tốt nhất
|
||||
4. Start batch và theo dõi progress
|
||||
5. Download results khi hoàn thành
|
||||
|
||||
### Use Case 3: Tạo Reports tự động
|
||||
1. Chạy training/prediction
|
||||
2. Report tự động được tạo
|
||||
3. Xem qua Dashboard hoặc `/api/reports/list`
|
||||
4. Download để chia sẻ
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Lưu ý
|
||||
|
||||
1. **Batch Processing**: Hiện tại chỉ xử lý tuần tự (từng job một)
|
||||
2. **Auto-retry**: Chỉ retry khi lỗi kỹ thuật, không retry nếu config sai
|
||||
3. **Charts Export**: Cần browser hỗ trợ Canvas API
|
||||
4. **Memory**: Batch lớn có thể tốn RAM, nên chia nhỏ
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Dashboard không hiển thị data
|
||||
- Kiểm tra có models/predictions trong folders chưa
|
||||
- Refresh lại trang
|
||||
- Check console log (F12)
|
||||
|
||||
### Batch processing không chạy
|
||||
- Kiểm tra format CSV đúng chưa
|
||||
- Kiểm tra model đã chọn có tồn tại không
|
||||
- Xem API logs để debug
|
||||
|
||||
### Charts không export được
|
||||
- Browser phải hỗ trợ Canvas.toDataURL()
|
||||
- Thử browser khác (Chrome/Firefox)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Nếu gặp vấn đề, check:
|
||||
1. API logs: `python api_server.py`
|
||||
2. Browser console: F12 → Console
|
||||
3. Network tab: F12 → Network
|
||||
|
||||
---
|
||||
|
||||
**🎉 Tất cả chức năng đã được phục hồi và nâng cấp!**
|
||||
@@ -1,204 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,278 +0,0 @@
|
||||
# Hướng dẫn sử dụng Swin-UNet
|
||||
|
||||
## Giới thiệu
|
||||
|
||||
**Swin-UNet** là một mô hình hybrid kết hợp:
|
||||
- **Swin Transformer blocks** - cho phép học các mối quan hệ toàn cục
|
||||
- **U-Net architecture** - với skip connections để bảo toàn chi tiết địa phương
|
||||
- **Hierarchical structure** - xử lý features ở nhiều cấp độ độ phân giải
|
||||
|
||||
## Ưu điểm chính
|
||||
|
||||
### 1. **Kiến trúc mạnh mẽ**
|
||||
- Kết hợp được điểm mạnh của cả Transformer và CNN
|
||||
- Self-attention giúp học các mối quan hệ phức tạp
|
||||
- Skip connections bảo toàn thông tin chi tiết
|
||||
|
||||
### 2. **Hiệu suất cao**
|
||||
- State-of-the-art accuracy cho nhiều tác vụ vision
|
||||
- Học nhanh hơn so với ViT cơ bản
|
||||
- Ổn định trong quá trình training
|
||||
|
||||
### 3. **Linh hoạt**
|
||||
- Hoạt động tốt với ít dữ liệu (transfer learning)
|
||||
- Có thể scale lên hoặc xuống theo yêu cầu
|
||||
- Hỗ trợ cả GPU và CPU
|
||||
|
||||
## Cấu hình tối ưu
|
||||
|
||||
### Cấu hình nhanh (test/prototyping)
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 60,
|
||||
"learning_rate": 0.001,
|
||||
"use_gpu": true,
|
||||
"test_size": 0.2
|
||||
}
|
||||
```
|
||||
- Training time: ~15-20 phút (GPU) / ~1-2 giờ (CPU)
|
||||
- Accuracy: Tốt cho các dataset nhỏ
|
||||
|
||||
### Cấu hình cân bằng (production)
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 100,
|
||||
"learning_rate": 0.0005,
|
||||
"use_gpu": true,
|
||||
"test_size": 0.2,
|
||||
"max_scenes": 30,
|
||||
"resolution": 10
|
||||
}
|
||||
```
|
||||
- Training time: ~30-45 phút (GPU)
|
||||
- Accuracy: Rất cao (>90% thường)
|
||||
|
||||
### Cấu hình cao cấp (accuracy tối đa)
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 150,
|
||||
"learning_rate": 0.0003,
|
||||
"use_gpu": true,
|
||||
"test_size": 0.2,
|
||||
"max_scenes": 60,
|
||||
"resolution": 10
|
||||
}
|
||||
```
|
||||
- Training time: ~45-60 phút (GPU)
|
||||
- Accuracy: Tối ưu nhất (95%+)
|
||||
- Yêu cầu: Dataset lớn, GPU mạnh
|
||||
|
||||
## So sánh với các model khác
|
||||
|
||||
| Tiêu chí | CNN | ResNet | ViT | **Swin-UNet** |
|
||||
|---------|-----|--------|-----|--------------|
|
||||
| Độ chính xác | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| Tốc độ training | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| Bộ nhớ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
|
||||
| Ổn định | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| Dataset nhỏ | ✓ | ✓ | ✗ | ✓ |
|
||||
| Dataset lớn | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
## Kiến trúc chi tiết
|
||||
|
||||
### Encoder (Đường xuống)
|
||||
```
|
||||
Input Features (n_features)
|
||||
↓
|
||||
Adapter Layer (project to embed_dim)
|
||||
↓
|
||||
Encoder1 (embed_dim → embed_dim)
|
||||
↓
|
||||
Downsample (→ embed_dim*2)
|
||||
↓
|
||||
Encoder2 (embed_dim*2 → embed_dim*2)
|
||||
↓
|
||||
Downsample (→ embed_dim*4)
|
||||
↓
|
||||
Encoder3 (embed_dim*4) - Bottleneck
|
||||
```
|
||||
|
||||
### Decoder (Đường lên)
|
||||
```
|
||||
Encoder3 Output
|
||||
↓
|
||||
Upsample (→ embed_dim*2)
|
||||
↓
|
||||
Concatenate with Skip from Encoder2
|
||||
↓
|
||||
Decoder2 (embed_dim*4 → embed_dim*2)
|
||||
↓
|
||||
Upsample (→ embed_dim)
|
||||
↓
|
||||
Concatenate with Skip from Encoder1
|
||||
↓
|
||||
Decoder1 (embed_dim*2 → embed_dim)
|
||||
↓
|
||||
Attention Layer (Multi-head)
|
||||
↓
|
||||
Classifier (embed_dim → n_classes)
|
||||
```
|
||||
|
||||
### Hyperparameters
|
||||
- **embed_dim**: 128 (kích thước embedding)
|
||||
- **batch_size**: 32
|
||||
- **optimizer**: AdamW (với weight decay = 0.01)
|
||||
- **scheduler**: CosineAnnealingLR
|
||||
- **dropout**: 0.1-0.3 (để regularization)
|
||||
|
||||
## Kỹ thuật training
|
||||
|
||||
### 1. Learning Rate Schedule
|
||||
- Bắt đầu từ `learning_rate`
|
||||
- Giảm dần theo cosine schedule
|
||||
- Giúp convergence tốt hơn
|
||||
|
||||
### 2. Weight Decay
|
||||
- Sử dụng AdamW với weight_decay=0.01
|
||||
- Ngăn overfitting
|
||||
- Improve generalization
|
||||
|
||||
### 3. Attention Mechanism
|
||||
- Multi-head attention (4 heads)
|
||||
- Giúp model học các mối quan hệ phức tạp
|
||||
- Cộng hưởng với self-attention trong Transformer
|
||||
|
||||
## Tips để đạt kết quả tốt
|
||||
|
||||
### ✅ Làm gì
|
||||
1. **Tăng epochs** - Swin-UNet thường cần nhiều epochs (60-150)
|
||||
2. **Sử dụng GPU** - Training nhanh hơn 10-20x
|
||||
3. **Learning rate nhỏ** - 0.0001 - 0.0005 cho dataset lớn
|
||||
4. **Augmentation** - Nếu có thể, augment training data
|
||||
5. **Monitor loss** - Loss nên giảm dần qua epochs
|
||||
|
||||
### ❌ Tránh gì
|
||||
1. **Learning rate quá cao** - Training không ổn định
|
||||
2. **Quá ít epochs** - Model chưa hội tụ
|
||||
3. **Batch size quá lớn** - Hết bộ nhớ
|
||||
4. **Overfitting** - Nếu train_acc >> test_acc, cần giảm epochs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Vấn đề: "CUDA out of memory"
|
||||
```python
|
||||
# Giải pháp:
|
||||
- Giảm batch_size (từ 32 xuống 16)
|
||||
- Giảm embed_dim (từ 128 xuống 64)
|
||||
- Sử dụng CPU: "use_gpu": false
|
||||
```
|
||||
|
||||
### Vấn đề: Loss không giảm
|
||||
```python
|
||||
# Giải pháp:
|
||||
- Giảm learning_rate (thử 0.0001)
|
||||
- Tăng epochs (thử 150+)
|
||||
- Kiểm tra dữ liệu training
|
||||
```
|
||||
|
||||
### Vấn đề: Quá chậm
|
||||
```python
|
||||
# Giải pháp:
|
||||
- Giảm n_estimators (↓ epochs)
|
||||
- Giảm max_scenes (↓ dữ liệu)
|
||||
- Sử dụng GPU nếu có
|
||||
```
|
||||
|
||||
### Vấn đề: Accuracy thấp
|
||||
```python
|
||||
# Giải pháp:
|
||||
- Tăng epochs (thử 100-150)
|
||||
- Thử learning_rate khác (0.0005, 0.001)
|
||||
- Kiểm tra chất lượng dữ liệu training
|
||||
- Thử model khác (ViT)
|
||||
```
|
||||
|
||||
## So sánh Learning Rates
|
||||
|
||||
| Learning Rate | Độ nhanh | Ổn định | Khuyến cáo |
|
||||
|---------------|----------|---------|-----------|
|
||||
| 0.01 | Nhanh | Kém | ❌ Quá cao |
|
||||
| 0.005 | Trung bình | Trung bình | ⚠️ Có thể dùng |
|
||||
| 0.001 | Trung bình | Tốt | ✅ Mặc định |
|
||||
| 0.0005 | Chậm | Rất tốt | ✅ Dùng khi cần độ chính xác cao |
|
||||
| 0.0001 | Rất chậm | Tuyệt | ✅ Cho ViT/LoRA |
|
||||
|
||||
## Khi nào dùng Swin-UNet?
|
||||
|
||||
### ✓ Sử dụng khi
|
||||
- Bạn có dataset vừa đến lớn (500+ samples)
|
||||
- Cần độ chính xác cao (>90%)
|
||||
- Có GPU hoặc thời gian chờ đợi
|
||||
- Muốn model ổn định và đáng tin cậy
|
||||
- Dữ liệu có các mẫu phức tạp
|
||||
|
||||
### ✗ Không sử dụng khi
|
||||
- Dataset rất nhỏ (<200 samples) → Dùng CNN hoặc XGBoost
|
||||
- Thời gian quá hạn → Dùng CNN hoặc XGBoost
|
||||
- Không có GPU và thời gian bị giới hạn → Dùng XGBoost
|
||||
- Cần mô hình hết sức nhẹ → Dùng CNN
|
||||
|
||||
## Ví dụ thực tế
|
||||
|
||||
### Trường hợp 1: Phân loại nhanh
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 60,
|
||||
"learning_rate": 0.001,
|
||||
"use_gpu": true,
|
||||
"max_scenes": 12,
|
||||
"resolution": 20
|
||||
}
|
||||
```
|
||||
**Kết quả**: ~15 phút, 85% accuracy
|
||||
|
||||
### Trường hợp 2: Phân loại cân bằng
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 100,
|
||||
"learning_rate": 0.0005,
|
||||
"use_gpu": true,
|
||||
"max_scenes": 30,
|
||||
"resolution": 10
|
||||
}
|
||||
```
|
||||
**Kết quả**: ~40 phút, 92% accuracy
|
||||
|
||||
### Trường hợp 3: Phân loại chính xác tối đa
|
||||
```json
|
||||
{
|
||||
"model_type": "swin-unet",
|
||||
"n_estimators": 150,
|
||||
"learning_rate": 0.0003,
|
||||
"use_gpu": true,
|
||||
"max_scenes": 60,
|
||||
"resolution": 10
|
||||
}
|
||||
```
|
||||
**Kết quả**: ~60 phút, 96%+ accuracy
|
||||
|
||||
## Tài liệu tham khảo
|
||||
|
||||
- Swin Transformer: https://arxiv.org/abs/2103.14030
|
||||
- U-Net: https://arxiv.org/abs/1505.04597
|
||||
- Swin-UNet for Medical Image: https://arxiv.org/abs/2105.05537
|
||||
|
||||
## Kết luận
|
||||
|
||||
Swin-UNet là lựa chọn tuyệt vời khi bạn cần:
|
||||
- ✅ Độ chính xác cao
|
||||
- ✅ Model ổn định
|
||||
- ✅ Khả năng xử lý dữ liệu phức tạp
|
||||
- ✅ Training tương đối nhanh
|
||||
|
||||
Hãy thử Swin-UNet cho các tác vụ classification quan trọng và cần chất lượng cao!
|
||||
@@ -1,228 +0,0 @@
|
||||
# HƯỚNG DẪN SỬ DỤNG HỆ THỐNG MỚI
|
||||
|
||||
## Tổng quan
|
||||
|
||||
Hệ thống đã được cập nhật để chuẩn hóa việc trích xuất features giữa training và prediction, sử dụng module `feature_extractor.py`.
|
||||
|
||||
## Các thành phần mới
|
||||
|
||||
### 1. feature_extractor.py
|
||||
Module chuẩn hóa việc trích xuất features với 3 modes:
|
||||
|
||||
- **simple**: 3 features cơ bản
|
||||
- NDVI_mean
|
||||
- VH_db_mean
|
||||
- VV_db_mean
|
||||
|
||||
- **temporal**: 39+ features time-series
|
||||
- NDVI_t1, NDVI_t2, ..., NDVI_tn
|
||||
- NDWI_t1, NDWI_t2, ..., NDWI_tn
|
||||
- NDBI_t1, NDBI_t2, ..., NDBI_tn
|
||||
- VH_db_mean, VV_db_mean, VH_VV_ratio
|
||||
|
||||
- **extended**: 15 features với statistics
|
||||
- NDVI_mean, NDVI_std, NDVI_min, NDVI_max
|
||||
- NDWI_mean, NDWI_std, NDWI_min, NDWI_max
|
||||
- NDBI_mean, NDBI_std, NDBI_min, NDBI_max
|
||||
- VH_db_mean, VV_db_mean, VH_VV_ratio
|
||||
|
||||
### 2. train_module.py (Đã cập nhật)
|
||||
- Thêm tham số `feature_mode` (default='simple')
|
||||
- Sử dụng FeatureExtractor để extract features
|
||||
- Lưu `feature_mode` vào metadata của model
|
||||
- Load đúng bands Sentinel-2 theo feature mode
|
||||
|
||||
### 3. api_server.py (Cần cập nhật thủ công)
|
||||
File này quá lớn để tự động replace. Cần thay thế hàm `run_prediction` bằng version mới trong `run_prediction_new.py`.
|
||||
|
||||
## Cách sử dụng
|
||||
|
||||
### Training với feature modes khác nhau
|
||||
|
||||
#### 1. Simple Mode (Mặc định - Nhanh nhất)
|
||||
```python
|
||||
from train_module import train_model
|
||||
|
||||
result = train_model(
|
||||
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||
time_range='2023-03-01/2023-05-31',
|
||||
max_scenes=12,
|
||||
feature_mode='simple', # 3 features
|
||||
model_type='xgboost',
|
||||
use_cache=True
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Temporal Mode (Cho model_odc.joblib)
|
||||
```python
|
||||
result = train_model(
|
||||
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||
time_range='2023-03-01/2023-05-31',
|
||||
max_scenes=12,
|
||||
feature_mode='temporal', # 39+ features
|
||||
model_type='random_forest',
|
||||
use_cache=True
|
||||
)
|
||||
```
|
||||
|
||||
#### 3. Extended Mode (Cân bằng speed/accuracy)
|
||||
```python
|
||||
result = train_model(
|
||||
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||
time_range='2023-03-01/2023-05-31',
|
||||
max_scenes=12,
|
||||
feature_mode='extended', # 15 features
|
||||
model_type='xgboost',
|
||||
use_cache=True
|
||||
)
|
||||
```
|
||||
|
||||
### Prediction
|
||||
Prediction sẽ tự động detect feature_mode từ model metadata và sử dụng FeatureExtractor tương ứng.
|
||||
|
||||
```python
|
||||
# Prediction sẽ tự động:
|
||||
# 1. Load model metadata
|
||||
# 2. Đọc feature_mode từ metadata
|
||||
# 3. Khởi tạo FeatureExtractor với mode tương ứng
|
||||
# 4. Extract features giống như training
|
||||
# 5. Predict
|
||||
```
|
||||
|
||||
## Tạo metadata cho model_odc.joblib
|
||||
|
||||
Model hiện tại `model_odc.joblib` được train với 39 features (temporal mode) nhưng chưa có metadata. Tạo metadata:
|
||||
|
||||
```bash
|
||||
python create_odc_metadata.py
|
||||
```
|
||||
|
||||
File này sẽ tạo `model_train/model_odc_info.json` với:
|
||||
- n_features: 39
|
||||
- feature_mode: "temporal"
|
||||
- features: list of 39 feature names
|
||||
|
||||
## So sánh các modes
|
||||
|
||||
| Feature Mode | N Features | Training Time | Accuracy | Use Case |
|
||||
|-------------|-----------|---------------|----------|----------|
|
||||
| simple | 3 | Nhanh nhất | Trung bình | Test nhanh, dataset nhỏ |
|
||||
| extended | 15 | Trung bình | Tốt | Cân bằng speed/accuracy |
|
||||
| temporal | 39+ | Chậm nhất | Tốt nhất | Production, dataset lớn |
|
||||
|
||||
## Lưu ý quan trọng
|
||||
|
||||
### 1. Bands được load
|
||||
- **simple**: B04, B08, SCL
|
||||
- **temporal/extended**: B02, B03, B04, B08, B11, SCL
|
||||
|
||||
### 2. Cache compatibility
|
||||
Cache cũ từ trước khi cập nhật sẽ KHÔNG tương thích vì:
|
||||
- Không có field `feature_mode`
|
||||
- Features có thể không match
|
||||
|
||||
**Giải pháp**: Xóa cache cũ
|
||||
```bash
|
||||
rm -rf dataset_cache/*
|
||||
```
|
||||
|
||||
### 3. Model compatibility
|
||||
- Models cũ (trước cập nhật) sẽ được coi là `feature_mode='simple'` nếu không có metadata
|
||||
- Models mới sẽ có field `feature_mode` trong metadata
|
||||
|
||||
## Workflow đề xuất
|
||||
|
||||
### Bước 1: Xóa cache cũ
|
||||
```bash
|
||||
rm -rf dataset_cache/*
|
||||
```
|
||||
|
||||
### Bước 2: Tạo metadata cho model_odc.joblib
|
||||
```bash
|
||||
python create_odc_metadata.py
|
||||
```
|
||||
|
||||
### Bước 3: Cập nhật api_server.py
|
||||
Thay thế hàm `run_prediction` (line 834-1295) với nội dung từ `run_prediction_new.py`
|
||||
|
||||
### Bước 4: Test training với simple mode
|
||||
```bash
|
||||
# Qua web interface hoặc
|
||||
python test_training_simple.py
|
||||
```
|
||||
|
||||
### Bước 5: Test prediction với model vừa train
|
||||
```bash
|
||||
# Qua web interface
|
||||
# Model sẽ tự động detect feature_mode và extract đúng features
|
||||
```
|
||||
|
||||
### Bước 6: Test với temporal mode (nếu cần accuracy cao)
|
||||
```bash
|
||||
python test_training_temporal.py
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Lỗi: "feature_mode not found in metadata"
|
||||
- Model cũ chưa có metadata
|
||||
- **Giải pháp**: Hệ thống tự động fallback về 'simple' mode
|
||||
|
||||
### Lỗi: "Expected X features but got Y"
|
||||
- Feature extraction không match với training
|
||||
- **Giải pháp**: Kiểm tra model metadata, đảm bảo feature_mode đúng
|
||||
|
||||
### Lỗi: "B11 band not found"
|
||||
- Sentinel-2 scene thiếu SWIR band
|
||||
- **Giải pháp**: Hệ thống tự động fallback về B02
|
||||
|
||||
## API Changes
|
||||
|
||||
### TrainingConfig (Mới)
|
||||
```python
|
||||
class TrainingConfig(BaseModel):
|
||||
# ... existing fields ...
|
||||
feature_mode: str = "simple" # NEW: 'simple', 'temporal', 'extended'
|
||||
```
|
||||
|
||||
### Model Metadata (Mới)
|
||||
```json
|
||||
{
|
||||
"feature_mode": "temporal",
|
||||
"features": ["NDVI_t1", "NDVI_t2", ...],
|
||||
"n_features": 39,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
/home/x79/remote-sensing/
|
||||
├── feature_extractor.py # NEW: Core feature extraction module
|
||||
├── train_module.py # UPDATED: Uses FeatureExtractor
|
||||
├── api_server.py # NEEDS UPDATE: run_prediction function
|
||||
├── run_prediction_new.py # NEW: Updated run_prediction code
|
||||
├── create_odc_metadata.py # NEW: Generate metadata for model_odc.joblib
|
||||
├── SYSTEM_UPDATE_GUIDE.md # This file
|
||||
└── model_train/
|
||||
├── model_odc.joblib # Existing 39-feature model
|
||||
├── model_odc_info.json # TO CREATE: Metadata file
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Created feature_extractor.py
|
||||
2. ✅ Updated train_module.py
|
||||
3. ⏳ Update api_server.py (manual)
|
||||
4. ⏳ Create metadata for model_odc.joblib
|
||||
5. ⏳ Test full workflow
|
||||
|
||||
## Contact & Support
|
||||
|
||||
Nếu gặp vấn đề, kiểm tra:
|
||||
1. feature_extractor.py có import được không
|
||||
2. Model metadata có field `feature_mode` chưa
|
||||
3. Cache đã được xóa chưa
|
||||
4. api_server.py đã cập nhật run_prediction chưa
|
||||
@@ -1,283 +0,0 @@
|
||||
# Tóm tắt cập nhật Training Interface & API
|
||||
|
||||
## 📋 Những gì đã cập nhật
|
||||
|
||||
### 1. **Backend API (api_server.py)**
|
||||
|
||||
#### ✅ Cập nhật giá trị mặc định từ 01.train_ODC.ipynb:
|
||||
- **Bbox mới**: `[105.5, 9.2, 106.4, 10.0]` (thay vì `[105.6, 9.3, 106.2, 9.8]`)
|
||||
- **Thời gian mới**: `2023-03-01` → `2023-12-31` (thay vì `2023-03-01` → `2023-05-31`)
|
||||
|
||||
#### ✅ Thêm Label Mapping Constants:
|
||||
```python
|
||||
DEFAULT_LABEL_MAPPING = {
|
||||
"Lua tom": "0",
|
||||
"Lua": "1",
|
||||
"CHN": "2",
|
||||
"CLN": "3",
|
||||
"TS": "4",
|
||||
"Song": "5",
|
||||
"Dat xay dung": "6",
|
||||
"Rung": "7",
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ API Endpoints mới:
|
||||
|
||||
**1. `GET /api/training/labels`**
|
||||
- Trả về danh sách tất cả labels và label mapping
|
||||
- Response:
|
||||
```json
|
||||
{
|
||||
"label_mapping": {...},
|
||||
"label_names": {...},
|
||||
"count": 8,
|
||||
"labels": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**2. `GET /api/training/files`**
|
||||
- List tất cả shapefile trong thư mục `/train`
|
||||
- Hiển thị: filename, size, số điểm, label column, unique labels
|
||||
- Response:
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"filename": "ST_training data_updated_1130points_new.shp",
|
||||
"path": "train/...",
|
||||
"size_mb": 0.15,
|
||||
"point_count": 1130,
|
||||
"label_column": "Hientrang",
|
||||
"unique_labels": [...],
|
||||
"label_count": 8
|
||||
}
|
||||
],
|
||||
"count": 2,
|
||||
"directory": "train/"
|
||||
}
|
||||
```
|
||||
|
||||
**3. `GET /api/training/shapefile/{filename}/labels`**
|
||||
- Đọc chi tiết labels từ một shapefile cụ thể
|
||||
- Trả về: số điểm, unique labels, label counts, bbox, columns
|
||||
- Response:
|
||||
```json
|
||||
{
|
||||
"filename": "...",
|
||||
"label_column": "Hientrang",
|
||||
"point_count": 1130,
|
||||
"unique_labels": [...],
|
||||
"label_count": 8,
|
||||
"labels": [
|
||||
{
|
||||
"name": "Lua tom",
|
||||
"code": "0",
|
||||
"count": 150,
|
||||
"mapped": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"bbox": [105.5, 9.2, 106.4, 10.0],
|
||||
"columns": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Cập nhật Presets:
|
||||
- Preset 1: "PC - Nhỏ" với bbox mới
|
||||
- Preset 2: "Server - Trung bình" với bbox mới
|
||||
- Preset 3: "Full - ODC" - PRESET MỚI từ 01.train_ODC.ipynb
|
||||
- Bbox: `[105.5, 9.2, 106.4, 10.0]`
|
||||
- Time: `2023-03-01` → `2023-12-31`
|
||||
- Max scenes: 1
|
||||
- Resolution: 10m
|
||||
|
||||
---
|
||||
|
||||
### 2. **Frontend UI (training_interface.html)**
|
||||
|
||||
#### ✅ Cập nhật giá trị mặc định trong form:
|
||||
- **Hidden inputs bbox**:
|
||||
- `minLon: 105.5, minLat: 9.2, maxLon: 106.4, maxLat: 10.0`
|
||||
- **Date inputs**:
|
||||
- `startDate: 2023-03-01, endDate: 2023-12-31`
|
||||
|
||||
#### ✅ Thêm section "Training Data (Shapefile)":
|
||||
```html
|
||||
<h3>📊 Training Data (Shapefile)</h3>
|
||||
<select id="trainingShapefile">...</select>
|
||||
```
|
||||
|
||||
Features:
|
||||
- Dropdown chọn shapefile từ thư mục `/train`
|
||||
- Tự động load default: `ST_training data_updated_1130points_new.shp`
|
||||
- Hiển thị thông tin: số điểm, label column, số lớp, bbox
|
||||
|
||||
#### ✅ Thêm phần hiển thị thông tin Shapefile:
|
||||
```html
|
||||
<div id="shapefileInfo">
|
||||
- Số điểm
|
||||
- Label column
|
||||
- Số lớp
|
||||
- Bbox
|
||||
- Phân bố labels (với icon ✅/⚠️)
|
||||
- Button "Áp dụng Bbox từ Shapefile"
|
||||
</div>
|
||||
```
|
||||
|
||||
#### ✅ JavaScript Functions mới:
|
||||
|
||||
**1. `loadTrainingFiles()`**
|
||||
- Load danh sách shapefile từ API
|
||||
- Populate dropdown
|
||||
- Auto-select default shapefile
|
||||
|
||||
**2. `loadShapefileLabels(filename)`**
|
||||
- Load chi tiết labels từ shapefile
|
||||
- Hiển thị phân bố labels
|
||||
- Highlight labels đã map vs chưa map
|
||||
|
||||
**3. `applyShapefileBbox()`**
|
||||
- Áp dụng bbox từ shapefile đã chọn
|
||||
- Cập nhật form inputs
|
||||
- Vẽ rectangle trên map
|
||||
- Hiển thị notification
|
||||
|
||||
**4. `showNotification(type, message)`**
|
||||
- Helper function để hiển thị notifications
|
||||
- Support types: success, error, warning
|
||||
|
||||
#### ✅ Cập nhật form submission:
|
||||
- Thêm `training_shapefile` vào config
|
||||
- Default: `train/ST_training data_updated_1130points_new.shp`
|
||||
|
||||
#### ✅ Event listeners:
|
||||
```javascript
|
||||
document.getElementById('trainingShapefile').addEventListener('change',
|
||||
(e) => loadShapefileLabels(e.target.value)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Bản đồ (Map)**
|
||||
|
||||
#### ✅ Initial rectangle với bbox mới:
|
||||
- Tự động vẽ rectangle với bbox từ backend
|
||||
- Fit map bounds để hiển thị khu vực
|
||||
|
||||
#### ✅ Dynamic update từ shapefile:
|
||||
- Khi chọn shapefile → có thể áp dụng bbox
|
||||
- Màu khác biệt (xanh dương) để dễ nhận biết
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test Script
|
||||
|
||||
File `test_training_api.py` để test các endpoints:
|
||||
|
||||
```bash
|
||||
# Run API server (terminal 1)
|
||||
conda activate env_01
|
||||
python api_server.py
|
||||
|
||||
# Run test script (terminal 2)
|
||||
conda activate env_01
|
||||
python test_training_api.py
|
||||
```
|
||||
|
||||
Test coverage:
|
||||
1. ✅ GET /api/training/labels
|
||||
2. ✅ GET /api/training/files
|
||||
3. ✅ GET /api/training/shapefile/{filename}/labels
|
||||
4. ✅ GET /api/config/presets
|
||||
|
||||
---
|
||||
|
||||
## 📊 Workflow mới
|
||||
|
||||
### Cách sử dụng trên giao diện:
|
||||
|
||||
1. **Mở Training Interface**: http://localhost:8000/training
|
||||
|
||||
2. **Chọn Training Data**:
|
||||
- Chọn shapefile từ dropdown "📊 Training Data"
|
||||
- Xem thông tin: số điểm, labels, bbox
|
||||
- (Optional) Click "📍 Áp dụng Bbox từ Shapefile"
|
||||
|
||||
3. **Chọn Khu vực**:
|
||||
- Option 1: Chọn tỉnh thành
|
||||
- Option 2: Vẽ rectangle trên map
|
||||
- Option 3: Áp dụng bbox từ shapefile
|
||||
- Option 4: Chọn preset
|
||||
|
||||
4. **Cấu hình thời gian và parameters**:
|
||||
- Thời gian mặc định: 2023-03-01 → 2023-12-31
|
||||
- Bbox mặc định: [105.5, 9.2, 106.4, 10.0]
|
||||
|
||||
5. **Start Training**:
|
||||
- Form tự động gửi `training_shapefile` parameter
|
||||
- Backend sẽ dùng đúng shapefile đã chọn
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Kết quả
|
||||
|
||||
### ✅ Backend:
|
||||
- 3 API endpoints mới hoạt động
|
||||
- Default values khớp với notebook
|
||||
- Label mapping được share
|
||||
|
||||
### ✅ Frontend:
|
||||
- UI mới để chọn shapefile
|
||||
- Hiển thị chi tiết labels
|
||||
- Auto-load default shapefile
|
||||
- Bbox từ shapefile có thể áp dụng
|
||||
|
||||
### ✅ Map:
|
||||
- Initial bbox khớp với backend
|
||||
- Update bbox từ nhiều nguồn
|
||||
- Visual feedback rõ ràng
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Debug & Verify
|
||||
|
||||
### Check API:
|
||||
```bash
|
||||
# List training files
|
||||
curl http://localhost:8000/api/training/files
|
||||
|
||||
# Get labels
|
||||
curl http://localhost:8000/api/training/labels
|
||||
|
||||
# Get shapefile labels
|
||||
curl "http://localhost:8000/api/training/shapefile/ST_training data_updated_1130points_new.shp/labels"
|
||||
```
|
||||
|
||||
### Check Browser Console:
|
||||
- F12 → Console
|
||||
- Xem logs khi chọn shapefile
|
||||
- Check network requests
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
1. **Training shapefile path format**:
|
||||
- Frontend select value: `ST_training data_updated_1130points_new.shp`
|
||||
- Backend receives: `train/ST_training data_updated_1130points_new.shp`
|
||||
- Auto-prepend `train/` prefix in form submission
|
||||
|
||||
2. **Label mapping**:
|
||||
- ✅ icon: Label có trong DEFAULT_LABEL_MAPPING
|
||||
- ⚠️ icon: Label chưa có trong mapping
|
||||
|
||||
3. **Bbox sources**:
|
||||
- Default từ backend
|
||||
- Từ tỉnh thành
|
||||
- Từ shapefile
|
||||
- Từ preset
|
||||
- Vẽ thủ công
|
||||
|
||||
Tất cả đều hoạt động đồng bộ!
|
||||
@@ -1,10 +1,3 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a62b68a633d79c53a6fd8893e8ea42dcf2b9a8a3e907b1b9861661f04f21517
|
||||
size 72
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:520c3480efaafdf844c3bba3dd165a8e44f3318ab21209539c78201f18fd6312
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d3bff20682fc510a9c41b5aa2b42e3ebef8098ed81e27b4436f9ae86f47b9a6
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:322f48d2d54ee921facfc3c0c44b320a86a90694e1779b683824264f9affa3fc
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2948384b4c43483675d366b75b7bcdceb63fe5a8ad7d9b7a84ee4a1018cfc826
|
||||
size 55333632
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d5a53bba7a6db30b7f80bc4250d09edacfd244e97d7993382927a9bde85c6f7
|
||||
size 55738194
|
||||
@@ -1,254 +0,0 @@
|
||||
# 🎉 Hệ thống đã được cập nhật hoàn chỉnh!
|
||||
|
||||
## 📁 Cấu trúc hệ thống mới
|
||||
|
||||
```
|
||||
remote-sensing/
|
||||
├── index.html # 🆕 Trang chính với tab navigation
|
||||
├── training_interface.html # ✅ Interface training (độc lập)
|
||||
├── prediction_interface.html # 🆕 Interface prediction (tách riêng)
|
||||
├── dashboard.html # ✅ Dashboard visualization
|
||||
├── api_server.py # ✅ API server (đã cập nhật đầy đủ)
|
||||
├── train_module.py # Training logic
|
||||
├── report_generator.py # Auto report generator
|
||||
├── batch_regions_example.csv # 🆕 CSV mẫu cho batch processing
|
||||
├── NEW_FEATURES.md # Documentation
|
||||
└── test_new_features.py # Test script
|
||||
```
|
||||
|
||||
## 🚀 Các URL hiện tại
|
||||
|
||||
### Main Pages
|
||||
- **Trang chủ với tabs**: http://localhost:8000/
|
||||
- **Training standalone**: http://localhost:8000/training
|
||||
- **Prediction standalone**: http://localhost:8000/prediction
|
||||
- **Dashboard standalone**: http://localhost:8000/dashboard
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
|
||||
### Tab Navigation trong Index
|
||||
1. 🏠 **Trang Chủ** - Tổng quan & quick start
|
||||
2. 🎓 **Training** - Training interface (iframe)
|
||||
3. 🗺️ **Prediction** - Prediction interface (iframe)
|
||||
4. 📊 **Dashboard** - Visualization & charts
|
||||
5. 🤖 **Models** - Quản lý models
|
||||
6. 📄 **Reports** - Xem & download reports
|
||||
7. 🔄 **Batch Processing** - Batch prediction queue
|
||||
|
||||
## ✨ Chức năng đã cập nhật
|
||||
|
||||
### 1. Tab Navigation System
|
||||
- ✅ Giao diện thống nhất với 7 tabs
|
||||
- ✅ Smooth transition animations
|
||||
- ✅ Responsive design
|
||||
- ✅ Real-time data loading
|
||||
|
||||
### 2. Training Interface (Tách riêng)
|
||||
- ✅ Có thể truy cập độc lập tại `/training`
|
||||
- ✅ Hoặc embed trong tab của index.html
|
||||
- ✅ Đầy đủ chức năng như cũ
|
||||
|
||||
### 3. Prediction Interface (Mới tách riêng)
|
||||
- ✅ Giao diện riêng biệt tại `/prediction`
|
||||
- ✅ Map selector với Leaflet
|
||||
- ✅ Model dropdown với info preview
|
||||
- ✅ Time & data configuration
|
||||
- ✅ Real-time status tracking
|
||||
- ✅ Download results & view reports
|
||||
- ✅ History của tất cả predictions
|
||||
|
||||
### 4. Dashboard & Visualization
|
||||
- ✅ Accuracy trends charts
|
||||
- ✅ F1-Score comparison
|
||||
- ✅ Class distribution
|
||||
- ✅ Export PNG/PDF
|
||||
- ✅ Real-time statistics
|
||||
|
||||
### 5. Batch Processing
|
||||
- ✅ Upload CSV file
|
||||
- ✅ Auto-retry mechanism
|
||||
- ✅ Queue management
|
||||
- ✅ Progress tracking
|
||||
- ✅ Real-time status updates
|
||||
|
||||
## 🔧 API Endpoints mới
|
||||
|
||||
### Dashboard APIs
|
||||
```
|
||||
GET /api/dashboard/accuracy-trends # Accuracy trends over time
|
||||
GET /api/dashboard/statistics # Tổng quan thống kê
|
||||
GET /api/dashboard/class-distribution/{model} # Phân bố classes
|
||||
```
|
||||
|
||||
### Batch Processing APIs
|
||||
```
|
||||
POST /api/batch/start # Bắt đầu batch prediction
|
||||
GET /api/batch/status # Kiểm tra queue status
|
||||
GET /api/batch/results/{batch_id} # Lấy kết quả batch
|
||||
POST /api/batch/cancel/{batch_id} # Hủy batch
|
||||
```
|
||||
|
||||
### Existing APIs (đã có)
|
||||
```
|
||||
# Training
|
||||
POST /api/training/start
|
||||
GET /api/training/status
|
||||
POST /api/training/stop
|
||||
|
||||
# Prediction
|
||||
POST /api/prediction/start
|
||||
GET /api/prediction/status
|
||||
|
||||
# Models
|
||||
GET /api/models/list
|
||||
|
||||
# Reports
|
||||
GET /api/reports/list
|
||||
GET /api/reports/view/{filename}
|
||||
GET /api/reports/download/{filename}
|
||||
|
||||
# Predictions
|
||||
GET /api/predictions/list
|
||||
GET /api/predictions/download/{filename}
|
||||
|
||||
# Cache
|
||||
GET /api/cache/info
|
||||
POST /api/cache/clear
|
||||
```
|
||||
|
||||
## 🎯 Cách sử dụng
|
||||
|
||||
### 1. Khởi động server
|
||||
```bash
|
||||
conda activate env_01
|
||||
python api_server.py
|
||||
```
|
||||
|
||||
### 2. Truy cập hệ thống
|
||||
Mở browser: http://localhost:8000/
|
||||
|
||||
### 3. Workflow cơ bản
|
||||
|
||||
#### A. Training
|
||||
1. Click tab "🎓 Training"
|
||||
2. Vẽ bbox hoặc chọn preset
|
||||
3. Cấu hình model type, parameters
|
||||
4. Click "Start Training"
|
||||
5. Theo dõi progress
|
||||
6. Download model & view report
|
||||
|
||||
#### B. Prediction
|
||||
1. Click tab "🗺️ Prediction"
|
||||
2. Chọn model đã train
|
||||
3. Vẽ bbox khu vực cần predict
|
||||
4. Cấu hình time range & data
|
||||
5. Click "Start Prediction"
|
||||
6. Download GeoTIFF khi hoàn thành
|
||||
|
||||
#### C. Dashboard
|
||||
1. Click tab "📊 Dashboard"
|
||||
2. Xem accuracy trends
|
||||
3. So sánh models
|
||||
4. Export charts PNG/PDF
|
||||
|
||||
#### D. Batch Processing
|
||||
1. Click tab "🔄 Batch Processing"
|
||||
2. Upload CSV file (xem batch_regions_example.csv)
|
||||
3. Chọn model
|
||||
4. Click "Start Batch Prediction"
|
||||
5. Theo dõi progress từng job
|
||||
|
||||
## 📊 Format CSV cho Batch Processing
|
||||
|
||||
```csv
|
||||
name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
|
||||
Region_1,105.6,9.3,105.8,9.5,2023-03-01,2023-05-31,12,30,20
|
||||
Region_2,105.8,9.3,106.0,9.5,2023-03-01,2023-05-31,12,30,20
|
||||
```
|
||||
|
||||
## 🔍 Test các chức năng
|
||||
|
||||
```bash
|
||||
# Test tất cả APIs
|
||||
python test_new_features.py
|
||||
|
||||
# Hoặc test thủ công
|
||||
curl http://localhost:8000/api/dashboard/statistics
|
||||
curl http://localhost:8000/api/models/list
|
||||
curl http://localhost:8000/api/batch/status
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
### Import Warnings
|
||||
Các warning về import (xarray, numpy, etc.) là bình thường vì:
|
||||
- Các thư viện này được import động trong runtime
|
||||
- Chỉ khi thực sự cần thiết (prediction/training)
|
||||
- Không ảnh hưởng đến hoạt động của server
|
||||
|
||||
### Browser Compatibility
|
||||
- Khuyến nghị: Chrome, Firefox, Edge (latest)
|
||||
- Mobile responsive: Đã optimize
|
||||
- Chart.js & Leaflet: CDN loaded automatically
|
||||
|
||||
### Performance
|
||||
- Training: Tùy vào config (5-30 phút)
|
||||
- Prediction: 2-10 phút tùy khu vực
|
||||
- Batch: Sequential processing (1 job/time)
|
||||
- Dashboard: Real-time updates mỗi 3s
|
||||
|
||||
## 🎨 Tính năng UI/UX
|
||||
|
||||
### Design
|
||||
- ✅ Modern gradient backgrounds
|
||||
- ✅ Card-based layouts
|
||||
- ✅ Smooth animations
|
||||
- ✅ Consistent color scheme
|
||||
- ✅ Responsive grid system
|
||||
|
||||
### Interactions
|
||||
- ✅ Real-time progress bars
|
||||
- ✅ Status badges
|
||||
- ✅ Loading spinners
|
||||
- ✅ Error alerts
|
||||
- ✅ Success notifications
|
||||
|
||||
### Charts
|
||||
- ✅ Interactive tooltips
|
||||
- ✅ Zoom & pan
|
||||
- ✅ Export functionality
|
||||
- ✅ Responsive sizing
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Server không start
|
||||
```bash
|
||||
# Check port 8000
|
||||
lsof -i :8000
|
||||
# Kill if needed
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### Tab không load
|
||||
- Clear browser cache
|
||||
- Check console (F12)
|
||||
- Verify file paths
|
||||
|
||||
### Batch không chạy
|
||||
- Check CSV format
|
||||
- Verify model exists
|
||||
- Check API logs
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Nếu gặp vấn đề:
|
||||
1. Check terminal logs
|
||||
2. Check browser console (F12)
|
||||
3. Verify all HTML files exist
|
||||
4. Test API endpoints với curl/Postman
|
||||
|
||||
---
|
||||
|
||||
**🎉 Hệ thống đã sẵn sàng sử dụng!**
|
||||
|
||||
Start server: `python api_server.py`
|
||||
Access: http://localhost:8000/
|
||||
@@ -1,263 +0,0 @@
|
||||
# CẬP NHẬT HỆ THỐNG HOÀN TẤT
|
||||
|
||||
## ✅ ĐÃ HOÀN THÀNH
|
||||
|
||||
### 1. Tạo module Feature Extractor chuẩn
|
||||
**File**: `feature_extractor.py`
|
||||
|
||||
Module này chuẩn hóa việc trích xuất features với 3 modes:
|
||||
|
||||
#### Mode 'simple' (3 features - Nhanh nhất)
|
||||
```python
|
||||
features = [
|
||||
'NDVI_mean',
|
||||
'VH_db_mean',
|
||||
'VV_db_mean'
|
||||
]
|
||||
```
|
||||
|
||||
#### Mode 'temporal' (39 features - Cho model_odc.joblib)
|
||||
```python
|
||||
features = [
|
||||
'NDVI_t1', 'NDVI_t2', ..., 'NDVI_t12', # 12 timesteps
|
||||
'NDWI_t1', 'NDWI_t2', ..., 'NDWI_t12', # 12 timesteps
|
||||
'NDBI_t1', 'NDBI_t2', ..., 'NDBI_t12', # 12 timesteps
|
||||
'VH_db_mean', 'VV_db_mean', 'VH_VV_ratio' # 3 radar
|
||||
]
|
||||
# Total: 12 + 12 + 12 + 3 = 39 features
|
||||
```
|
||||
|
||||
#### Mode 'extended' (15 features - Cân bằng)
|
||||
```python
|
||||
features = [
|
||||
'NDVI_mean', 'NDVI_std', 'NDVI_min', 'NDVI_max',
|
||||
'NDWI_mean', 'NDWI_std', 'NDWI_min', 'NDWI_max',
|
||||
'NDBI_mean', 'NDBI_std', 'NDBI_min', 'NDBI_max',
|
||||
'VH_db_mean', 'VV_db_mean', 'VH_VV_ratio'
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Cập nhật Training Module
|
||||
**File**: `train_module.py`
|
||||
|
||||
**Thay đổi chính**:
|
||||
- ✅ Thêm parameter `feature_mode` vào hàm `train_model()`
|
||||
- ✅ Import và sử dụng `FeatureExtractor`
|
||||
- ✅ Load đúng Sentinel-2 bands theo feature mode:
|
||||
- simple: B04, B08, SCL
|
||||
- temporal/extended: B02, B03, B04, B08, B11, SCL
|
||||
- ✅ Lưu `feature_mode` vào model metadata
|
||||
- ✅ Lưu danh sách feature names chính xác vào metadata
|
||||
|
||||
**Cách sử dụng**:
|
||||
```python
|
||||
from train_module import train_model
|
||||
|
||||
# Training với simple mode (mặc định)
|
||||
result = train_model(
|
||||
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||
time_range='2023-03-01/2023-05-31',
|
||||
feature_mode='simple', # Thêm parameter này
|
||||
model_type='xgboost'
|
||||
)
|
||||
|
||||
# Training với temporal mode (cho model 39 features)
|
||||
result = train_model(
|
||||
bbox=[105.6, 9.3, 106.2, 9.8],
|
||||
time_range='2023-03-01/2023-05-31',
|
||||
feature_mode='temporal', # Temporal mode
|
||||
model_type='random_forest'
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Tạo metadata cho model_odc.joblib
|
||||
**File**: `model_train/model_odc_info.json` (đã tạo)
|
||||
|
||||
Metadata này chứa:
|
||||
- `feature_mode`: "temporal"
|
||||
- `n_features`: 39
|
||||
- `features`: danh sách 39 feature names đầy đủ
|
||||
- 8 class names: Lua tom, Lua, CHN, CLN, TS, Song, Dat xay dung, Rung
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
cat model_train/model_odc_info.json | grep feature_mode
|
||||
# Output: "feature_mode": "temporal"
|
||||
```
|
||||
|
||||
### 4. Hướng dẫn sử dụng
|
||||
**File**: `SYSTEM_UPDATE_GUIDE.md`
|
||||
|
||||
Document đầy đủ về:
|
||||
- Cách sử dụng các feature modes
|
||||
- So sánh performance giữa các modes
|
||||
- Troubleshooting
|
||||
- API changes
|
||||
|
||||
### 5. Updated prediction code
|
||||
**File**: `run_prediction_new.py`
|
||||
|
||||
Chứa code mới cho hàm `run_prediction()` sử dụng `FeatureExtractor`.
|
||||
|
||||
## 🔧 CẦN LÀM TIẾP
|
||||
|
||||
### 1. Cập nhật api_server.py (Thủ công)
|
||||
**Cần thay thế hàm `run_prediction` (line 834+)**
|
||||
|
||||
**Lý do không tự động**: Hàm quá dài, file api_server.py quá lớn (3000+ lines)
|
||||
|
||||
**Cách làm**:
|
||||
1. Mở `api_server.py`
|
||||
2. Tìm hàm `async def run_prediction(config: PredictionConfig):`
|
||||
3. Copy toàn bộ code từ `run_prediction_new.py`
|
||||
4. Paste thay thế hàm cũ
|
||||
|
||||
**Hoặc sử dụng editor**:
|
||||
```python
|
||||
# Tìm line bắt đầu:
|
||||
async def run_prediction(config: PredictionConfig):
|
||||
"""Chạy prediction process - Áp dụng phương pháp từ 02.predict_ODC.ipynb"""
|
||||
|
||||
# Thay thế toàn bộ hàm (đến hết try-except) bằng code từ run_prediction_new.py
|
||||
```
|
||||
|
||||
### 2. Test toàn bộ hệ thống
|
||||
|
||||
#### Test 1: Training với simple mode
|
||||
```bash
|
||||
# Via web interface hoặc
|
||||
curl -X POST http://localhost:8000/api/training/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"feature_mode": "simple",
|
||||
"model_type": "xgboost",
|
||||
"bbox": [105.6, 9.3, 106.2, 9.8],
|
||||
...
|
||||
}'
|
||||
```
|
||||
|
||||
#### Test 2: Prediction với model vừa train
|
||||
```bash
|
||||
# Model sẽ tự động detect feature_mode từ metadata
|
||||
curl -X POST http://localhost:8000/api/prediction/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model_filename": "model_xgboost_20251223_120000.joblib"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Test 3: Prediction với model_odc.joblib
|
||||
```bash
|
||||
# Model có metadata với feature_mode='temporal'
|
||||
# Prediction sẽ tự động extract 39 temporal features
|
||||
curl -X POST http://localhost:8000/api/prediction/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model_filename": "model_odc.joblib"
|
||||
}'
|
||||
```
|
||||
|
||||
## 📊 KẾT QUẢ MONG ĐỢI
|
||||
|
||||
### Trước khi cập nhật:
|
||||
- ❌ Training tạo 3 features: NDVI_mean, VH, VV
|
||||
- ❌ Prediction cố extract 39 features
|
||||
- ❌ Mismatch: Model expects 39 but got 3
|
||||
- ❌ Lỗi: "StandardScaler expects 39 features"
|
||||
|
||||
### Sau khi cập nhật:
|
||||
- ✅ Training với `feature_mode='simple'`: 3 features
|
||||
- ✅ Training với `feature_mode='temporal'`: 39 features
|
||||
- ✅ Prediction tự động detect mode từ metadata
|
||||
- ✅ Prediction extract đúng số features như training
|
||||
- ✅ Không còn feature mismatch errors
|
||||
|
||||
## 📁 FILES CHANGED
|
||||
|
||||
| File | Status | Changes |
|
||||
|------|--------|---------|
|
||||
| feature_extractor.py | ✅ NEW | Core feature extraction module |
|
||||
| train_module.py | ✅ UPDATED | Added feature_mode parameter, uses FeatureExtractor |
|
||||
| create_odc_metadata.py | ✅ UPDATED | Added feature_mode and 39 feature names |
|
||||
| model_train/model_odc_info.json | ✅ CREATED | Metadata for model_odc.joblib |
|
||||
| api_server.py | ⏳ MANUAL | Need to replace run_prediction function |
|
||||
| run_prediction_new.py | ✅ NEW | New run_prediction code using FeatureExtractor |
|
||||
| SYSTEM_UPDATE_GUIDE.md | ✅ NEW | Comprehensive guide |
|
||||
| UPDATE_SUMMARY.md | ✅ NEW | This file |
|
||||
|
||||
## 🚀 QUICK START
|
||||
|
||||
### Bước 1: Backup (Optional)
|
||||
```bash
|
||||
cp api_server.py api_server.py.backup
|
||||
```
|
||||
|
||||
### Bước 2: Cập nhật api_server.py
|
||||
**Mở `api_server.py` và thay thế hàm `run_prediction`**
|
||||
|
||||
Tìm line:
|
||||
```python
|
||||
async def run_prediction(config: PredictionConfig):
|
||||
"""Chạy prediction process - Áp dụng phương pháp từ 02.predict_ODC.ipynb"""
|
||||
```
|
||||
|
||||
Thay thế toàn bộ hàm bằng code từ `run_prediction_new.py`
|
||||
|
||||
### Bước 3: Restart API server
|
||||
```bash
|
||||
# Stop current server (Ctrl+C)
|
||||
# Start new server
|
||||
./start.sh
|
||||
# hoặc
|
||||
python api_server.py
|
||||
```
|
||||
|
||||
### Bước 4: Xóa cache cũ (Optional nhưng recommended)
|
||||
```bash
|
||||
rm -rf dataset_cache/*
|
||||
```
|
||||
|
||||
### Bước 5: Test via web interface
|
||||
1. Mở http://localhost:8000
|
||||
2. Vào Training tab
|
||||
3. Chọn feature_mode (sẽ thêm vào UI sau)
|
||||
4. Train model
|
||||
5. Vào Prediction tab
|
||||
6. Chọn model vừa train
|
||||
7. Run prediction
|
||||
|
||||
## 🎯 TỔNG KẾT
|
||||
|
||||
### Vấn đề ban đầu:
|
||||
- Hệ thống training và prediction không đồng bộ features
|
||||
- model_odc.joblib cần 39 features nhưng prediction chỉ tạo 3 features
|
||||
|
||||
### Giải pháp:
|
||||
- Tạo `FeatureExtractor` module chuẩn với 3 modes
|
||||
- Cập nhật training để chọn feature mode và lưu vào metadata
|
||||
- Cập nhật prediction để đọc feature mode từ metadata và extract features tương ứng
|
||||
- Tạo metadata cho model_odc.joblib với feature_mode='temporal'
|
||||
|
||||
### Kết quả:
|
||||
- ✅ Training và prediction hoàn toàn đồng bộ
|
||||
- ✅ Hỗ trợ 3 feature modes: simple (3), extended (15), temporal (39+)
|
||||
- ✅ Model tự động biết cần extract bao nhiêu features
|
||||
- ✅ Không còn feature mismatch errors
|
||||
- ✅ model_odc.joblib có thể sử dụng được với prediction
|
||||
|
||||
### Lợi ích:
|
||||
1. **Linh hoạt**: Chọn feature mode phù hợp với use case
|
||||
2. **Nhất quán**: Training và prediction luôn sync
|
||||
3. **Mở rộng**: Dễ dàng thêm feature mode mới
|
||||
4. **Rõ ràng**: Metadata chứa đầy đủ thông tin về features
|
||||
5. **Tương thích**: Hỗ trợ cả model cũ và mới
|
||||
|
||||
## 📞 SUPPORT
|
||||
|
||||
Nếu gặp lỗi, kiểm tra:
|
||||
1. ✅ `feature_extractor.py` có trong folder chưa
|
||||
2. ✅ `api_server.py` đã cập nhật `run_prediction` chưa
|
||||
3. ✅ Model metadata có field `feature_mode` chưa
|
||||
4. ✅ Cache cũ đã xóa chưa
|
||||
|
||||
Xem thêm: `SYSTEM_UPDATE_GUIDE.md` để biết chi tiết.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+270
-5441
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:37686f6bbdd4590ce2dc5474eef8268f680498c2b78881a486625e264d2a1015
|
||||
size 841901
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78d28d677d39cc67e607bb418948b9e3e921351762d63b4737036fc02d1d361a
|
||||
size 3375913
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:892cabc8da5cfcc0f9295ddb3ad32fda077027efa53e7b48e2f81965ad6ca202
|
||||
size 3361173
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fbab8f823ca6965dcbe43d9ce914f8912e3976833de1b205032eec48bfa83970
|
||||
size 1670930
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c33115ec7d6896758abf1eb8d07eba94ae15444c63f318e8adbd5d032df9a4f
|
||||
size 1671094
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e71c696efbcb9e766eb1a666cc85b4f604992a65efc7258a46a46c7f81c596db
|
||||
size 3536032
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:520c3480efaafdf844c3bba3dd165a8e44f3318ab21209539c78201f18fd6312
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d3bff20682fc510a9c41b5aa2b42e3ebef8098ed81e27b4436f9ae86f47b9a6
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:322f48d2d54ee921facfc3c0c44b320a86a90694e1779b683824264f9affa3fc
|
||||
size 3066028
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a446d333bf7f6d0cb7df014f12b0da3f7298f85bdfb4de06893173e90fbd5ccb
|
||||
size 14112695
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3eec576dffe1cc1393fdb584fb99d28db62abdd0977d41030aee5c4fa5377180
|
||||
size 11474743
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc125de4c172c98d9e50123bfcc2f8dd856e4ccd6bb04edd639badf090af2eef
|
||||
size 5077735
|
||||
@@ -1,789 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Batch Processing - Land Classification</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.batch-item {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.batch-item.completed {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.batch-item.failed {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.batch-item.running {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 25px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
transition: width 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
border-left: 4px solid #0c5460;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
border-left: 4px solid #155724;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.jobs-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#batchMap {
|
||||
height: 400px;
|
||||
border-radius: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 Batch Processing</h1>
|
||||
<p>Xử lý nhiều khu vực cùng lúc với model đã train</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
|
||||
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 Trang Chủ</a>
|
||||
<a href="/training" style="padding: 10px 20px; background: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🎓 Training</a>
|
||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
|
||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing (Active)</a>
|
||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Configuration Section -->
|
||||
<div class="section">
|
||||
<h2>⚙️ Cấu hình Batch</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="batchModelSelect">Model:</label>
|
||||
<select id="batchModelSelect">
|
||||
<option value="">Đang tải...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="batchName">Tên khu vực:</label>
|
||||
<input type="text" id="batchName" placeholder="Ví dụ: Khu vực A">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Bbox (từ bản đồ hoặc nhập thủ công):</label>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<input type="number" id="batchMinLon" placeholder="Min Lon" step="0.0001">
|
||||
<input type="number" id="batchMinLat" placeholder="Min Lat" step="0.0001">
|
||||
<input type="number" id="batchMaxLon" placeholder="Max Lon" step="0.0001">
|
||||
<input type="number" id="batchMaxLat" placeholder="Max Lat" step="0.0001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Thời gian:</label>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<input type="date" id="batchStartDate" value="2023-03-01">
|
||||
<input type="date" id="batchEndDate" value="2023-05-31">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="addBatchItem()">
|
||||
➕ Thêm vào Batch
|
||||
</button>
|
||||
|
||||
<!-- Map for selecting bbox -->
|
||||
<div id="batchMap"></div>
|
||||
</div>
|
||||
|
||||
<!-- Batch Queue Section -->
|
||||
<div class="section">
|
||||
<h2>📋 Batch Queue (<span id="queueCount">0</span> items)</h2>
|
||||
|
||||
<div id="batchQueue" class="jobs-list">
|
||||
<p style="text-align: center; color: #666;">Chưa có item nào. Thêm khu vực từ bên trái.</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<button class="btn btn-success" onclick="startBatch()" id="startBatchBtn" disabled>
|
||||
🚀 Start Batch Processing
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="clearBatchQueue()">
|
||||
🗑️ Clear Queue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Section -->
|
||||
<div class="section" style="grid-column: 1 / -1;">
|
||||
<h2>📊 Batch Status</h2>
|
||||
|
||||
<div id="batchStatus" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<p><strong>Batch ID:</strong> <span id="currentBatchId"></span></p>
|
||||
<p><strong>Status:</strong> Queued: <span id="statusQueued">0</span> | Running: <span id="statusRunning">0</span> | Completed: <span id="statusCompleted">0</span> | Failed: <span id="statusFailed">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="runningJobs" class="jobs-list">
|
||||
<!-- Running jobs will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div class="section" style="grid-column: 1 / -1;">
|
||||
<h2>✅ Completed Results</h2>
|
||||
|
||||
<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;">
|
||||
<button class="btn btn-primary" onclick="loadAllBatchResults()" style="padding: 8px 20px;">
|
||||
🔄 Refresh Results
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="downloadAllResults()" style="padding: 8px 20px;">
|
||||
📦 Download All (Bulk)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="completedResults" class="jobs-list">
|
||||
<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for large preview -->
|
||||
<div id="previewModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; padding: 20px;">
|
||||
<div style="position: relative; height: 100%; display: flex; align-items: center; justify-content: center;">
|
||||
<button onclick="closePreviewModal()" style="position: absolute; top: 20px; right: 20px; background: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 18px; font-weight: bold;">
|
||||
✕ Close
|
||||
</button>
|
||||
<img id="previewImage" style="max-width: 90%; max-height: 90%; border-radius: 10px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
|
||||
<script>
|
||||
let map, drawnItems, drawControl;
|
||||
let batchQueue = [];
|
||||
let currentBatchId = null;
|
||||
let statusCheckInterval = null;
|
||||
|
||||
// Initialize map
|
||||
function initMap() {
|
||||
map = L.map('batchMap').setView([9.5, 105.9], 9);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
rectangle: true,
|
||||
polygon: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
polyline: false,
|
||||
circlemarker: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function(event) {
|
||||
drawnItems.clearLayers();
|
||||
const layer = event.layer;
|
||||
drawnItems.addLayer(layer);
|
||||
|
||||
const bounds = layer.getBounds();
|
||||
document.getElementById('batchMinLon').value = bounds.getWest().toFixed(4);
|
||||
document.getElementById('batchMinLat').value = bounds.getSouth().toFixed(4);
|
||||
document.getElementById('batchMaxLon').value = bounds.getEast().toFixed(4);
|
||||
document.getElementById('batchMaxLat').value = bounds.getNorth().toFixed(4);
|
||||
});
|
||||
}
|
||||
|
||||
// Load models
|
||||
async function loadModels() {
|
||||
try {
|
||||
const response = await fetch('/api/models/list');
|
||||
const data = await response.json();
|
||||
|
||||
const select = document.getElementById('batchModelSelect');
|
||||
select.innerHTML = '<option value="">Chọn model...</option>';
|
||||
|
||||
data.models.filter(m => m.filename.endsWith('.joblib')).forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.filename;
|
||||
option.textContent = `${model.filename} - ${model.created}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if (data.models.length > 0) {
|
||||
select.value = data.models[0].filename;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add item to batch queue
|
||||
function addBatchItem() {
|
||||
const name = document.getElementById('batchName').value;
|
||||
const minLon = parseFloat(document.getElementById('batchMinLon').value);
|
||||
const minLat = parseFloat(document.getElementById('batchMinLat').value);
|
||||
const maxLon = parseFloat(document.getElementById('batchMaxLon').value);
|
||||
const maxLat = parseFloat(document.getElementById('batchMaxLat').value);
|
||||
const startDate = document.getElementById('batchStartDate').value;
|
||||
const endDate = document.getElementById('batchEndDate').value;
|
||||
|
||||
if (!name || isNaN(minLon) || isNaN(minLat) || isNaN(maxLon) || isNaN(maxLat)) {
|
||||
alert('❌ Vui lòng điền đầy đủ thông tin!');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = {
|
||||
name,
|
||||
min_lon: minLon,
|
||||
min_lat: minLat,
|
||||
max_lon: maxLon,
|
||||
max_lat: maxLat,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
max_scenes: 12,
|
||||
cloud_cover: 30,
|
||||
resolution: 20
|
||||
};
|
||||
|
||||
batchQueue.push(item);
|
||||
updateBatchQueueDisplay();
|
||||
|
||||
// Clear form
|
||||
document.getElementById('batchName').value = '';
|
||||
drawnItems.clearLayers();
|
||||
}
|
||||
|
||||
// Update batch queue display
|
||||
function updateBatchQueueDisplay() {
|
||||
const queueDiv = document.getElementById('batchQueue');
|
||||
const countSpan = document.getElementById('queueCount');
|
||||
|
||||
countSpan.textContent = batchQueue.length;
|
||||
|
||||
if (batchQueue.length === 0) {
|
||||
queueDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có item nào. Thêm khu vực từ bên trái.</p>';
|
||||
document.getElementById('startBatchBtn').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('startBatchBtn').disabled = false;
|
||||
|
||||
queueDiv.innerHTML = batchQueue.map((item, idx) => `
|
||||
<div class="batch-item">
|
||||
<div>
|
||||
<strong>${item.name}</strong><br>
|
||||
<small>Bbox: (${item.min_lon.toFixed(2)}, ${item.min_lat.toFixed(2)}) → (${item.max_lon.toFixed(2)}, ${item.max_lat.toFixed(2)})</small><br>
|
||||
<small>Time: ${item.start_date} → ${item.end_date}</small>
|
||||
</div>
|
||||
<button class="btn btn-danger" style="padding: 5px 15px;" onclick="removeBatchItem(${idx})">
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Remove item from queue
|
||||
function removeBatchItem(index) {
|
||||
batchQueue.splice(index, 1);
|
||||
updateBatchQueueDisplay();
|
||||
}
|
||||
|
||||
// Clear batch queue
|
||||
function clearBatchQueue() {
|
||||
if (!confirm('Xóa tất cả items trong queue?')) return;
|
||||
batchQueue = [];
|
||||
updateBatchQueueDisplay();
|
||||
}
|
||||
|
||||
// Start batch processing
|
||||
async function startBatch() {
|
||||
const modelFilename = document.getElementById('batchModelSelect').value;
|
||||
if (!modelFilename) {
|
||||
alert('❌ Vui lòng chọn model!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (batchQueue.length === 0) {
|
||||
alert('❌ Batch queue trống!');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
model_filename: modelFilename,
|
||||
items: batchQueue,
|
||||
auto_retry: true,
|
||||
max_retries: 3
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/batch/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentBatchId = result.batch_id;
|
||||
document.getElementById('currentBatchId').textContent = currentBatchId;
|
||||
document.getElementById('batchStatus').style.display = 'block';
|
||||
|
||||
// Clear local queue
|
||||
batchQueue = [];
|
||||
updateBatchQueueDisplay();
|
||||
|
||||
// Start monitoring
|
||||
startStatusCheck();
|
||||
|
||||
alert(`✅ Đã bắt đầu batch processing với ${result.total_jobs} jobs!`);
|
||||
} else {
|
||||
throw new Error(result.detail || 'Lỗi khi bắt đầu batch');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
alert('❌ Lỗi: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Check batch status
|
||||
async function checkBatchStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/batch/status');
|
||||
const status = await response.json();
|
||||
|
||||
// Update status counts
|
||||
document.getElementById('statusQueued').textContent = status.queue.queued;
|
||||
document.getElementById('statusRunning').textContent = status.queue.running;
|
||||
document.getElementById('statusCompleted').textContent = status.queue.completed;
|
||||
document.getElementById('statusFailed').textContent = status.queue.failed;
|
||||
|
||||
// Update running jobs
|
||||
const runningDiv = document.getElementById('runningJobs');
|
||||
if (status.jobs.running.length > 0) {
|
||||
runningDiv.innerHTML = status.jobs.running.map(job => {
|
||||
const outputFile = job.result?.output_file || '';
|
||||
const pngFile = job.result?.png_file || '';
|
||||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||||
return `
|
||||
<div class="batch-item running">
|
||||
<div style="flex: 1;">
|
||||
<strong>${job.name}</strong> - <span style="color: #ffc107;">Running</span><br>
|
||||
<small>Job ID: ${job.job_id}</small>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: ${job.progress}%">${job.progress}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadResult('${outputFilename}')"
|
||||
${outputFilename ? '' : 'disabled'}>
|
||||
💾 Download GeoTIFF
|
||||
</button>
|
||||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadPNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🖼️ Download PNG
|
||||
</button>
|
||||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="viewLargePNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🔍 View Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
runningDiv.innerHTML = '<p style="text-align: center; color: #666;">Không có job nào đang chạy</p>';
|
||||
}
|
||||
|
||||
// Update completed results
|
||||
const completedDiv = document.getElementById('completedResults');
|
||||
if (status.jobs.recent_completed.length > 0) {
|
||||
completedDiv.innerHTML = status.jobs.recent_completed.map(job => {
|
||||
const outputFile = job.result?.output_file || '';
|
||||
const pngFile = job.result?.png_file || '';
|
||||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||||
return `
|
||||
<div class="batch-item completed">
|
||||
<div style="flex: 1;">
|
||||
<strong>${job.name}</strong> - <span style="color: #28a745;">✓ Completed</span><br>
|
||||
<small>Job ID: ${job.job_id}</small><br>
|
||||
<small>Completed: ${new Date(job.completed_at).toLocaleString()}</small><br>
|
||||
<small><strong>Shape:</strong> ${job.result?.shape ? job.result.shape.join(' x ') : 'N/A'}</small><br>
|
||||
<small><strong>Classes:</strong> ${job.result?.unique_classes ? job.result.unique_classes.join(', ') : 'N/A'}</small><br>
|
||||
<small><strong>Features:</strong> ${job.result?.n_features || 'N/A'}</small><br>
|
||||
<small><strong>Output:</strong> ${outputFilename || 'N/A'}</small><br>
|
||||
<div style="margin-top: 10px;">
|
||||
<img src="/api/predictions/preview/${pngFilename}"
|
||||
style="max-width: 100%; max-height: 300px; border-radius: 5px; cursor: pointer; ${pngFilename ? '' : 'display:none;'}"
|
||||
onclick="viewLargePNG('${pngFilename}')"
|
||||
title="Click để xem lớn hơn"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadResult('${outputFilename}')"
|
||||
${outputFilename ? '' : 'disabled'}>
|
||||
💾 Download GeoTIFF
|
||||
</button>
|
||||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadPNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🖼️ Download PNG
|
||||
</button>
|
||||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="viewLargePNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🔍 View Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
completedDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>';
|
||||
}
|
||||
|
||||
// Show failed jobs if any
|
||||
if (status.jobs.recent_failed.length > 0) {
|
||||
const failedHTML = status.jobs.recent_failed.map(job => {
|
||||
const outputFile = job.result?.output_file || '';
|
||||
const pngFile = job.result?.png_file || '';
|
||||
const outputFilename = outputFile ? outputFile.split('/').pop() : '';
|
||||
const pngFilename = pngFile ? pngFile.split('/').pop() : '';
|
||||
return `
|
||||
<div class="batch-item failed">
|
||||
<div style="flex: 1;">
|
||||
<strong>${job.name}</strong> - <span style="color: #dc3545;">✗ Failed</span><br>
|
||||
<small>Job ID: ${job.job_id}</small><br>
|
||||
<small style="color: #dc3545;">${job.error || 'Unknown error'}</small>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 200px; flex-shrink: 0;">
|
||||
<button class="btn btn-success" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadResult('${outputFilename}')"
|
||||
${outputFilename ? '' : 'disabled'}>
|
||||
💾 Download GeoTIFF
|
||||
</button>
|
||||
<button class="btn btn-primary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="downloadPNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🖼️ Download PNG
|
||||
</button>
|
||||
<button class="btn btn-secondary" style="padding: 10px 20px; margin: 0; white-space: nowrap;"
|
||||
onclick="viewLargePNG('${pngFilename}')"
|
||||
${pngFilename ? '' : 'disabled'}>
|
||||
🔍 View Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
completedDiv.innerHTML += '<h3 style="margin-top: 20px; color: #dc3545;">❌ Failed Jobs</h3>' + failedHTML;
|
||||
}
|
||||
|
||||
// Stop checking if all done
|
||||
if (status.queue.running === 0 && status.queue.queued === 0 && currentBatchId) {
|
||||
stopStatusCheck();
|
||||
alert('✅ Batch processing hoàn thành!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking batch status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Download result
|
||||
function downloadResult(filename) {
|
||||
window.location.href = `/api/predictions/download/${filename}`;
|
||||
}
|
||||
|
||||
// Download PNG
|
||||
function downloadPNG(filename) {
|
||||
window.location.href = `/api/predictions/preview/${filename}`;
|
||||
}
|
||||
|
||||
// View large PNG in new window
|
||||
function viewLargePNG(filename) {
|
||||
const modal = document.getElementById('previewModal');
|
||||
const img = document.getElementById('previewImage');
|
||||
img.src = `/api/predictions/preview/${filename}`;
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
// Close preview modal
|
||||
function closePreviewModal() {
|
||||
document.getElementById('previewModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Load all batch results
|
||||
async function loadAllBatchResults() {
|
||||
try {
|
||||
const response = await fetch('/api/batch/status');
|
||||
const status = await response.json();
|
||||
|
||||
const completedDiv = document.getElementById('completedResults');
|
||||
|
||||
// Combine recent_completed from status
|
||||
const allCompleted = status.jobs.recent_completed || [];
|
||||
|
||||
if (allCompleted.length === 0) {
|
||||
completedDiv.innerHTML = '<p style="text-align: center; color: #666;">Chưa có kết quả nào</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
completedDiv.innerHTML = allCompleted.map(job => `
|
||||
<div class="batch-item completed">
|
||||
<div style="flex: 1;">
|
||||
<strong>${job.name}</strong> - <span style="color: #28a745;">✓ Completed</span><br>
|
||||
<small>Job ID: ${job.job_id}</small><br>
|
||||
<small>Completed: ${new Date(job.completed_at).toLocaleString()}</small><br>
|
||||
${job.result ? `
|
||||
<small><strong>Shape:</strong> ${job.result.shape.join(' x ')}</small><br>
|
||||
<small><strong>Classes:</strong> ${job.result.unique_classes.join(', ')}</small><br>
|
||||
<small><strong>Features:</strong> ${job.result.n_features}</small><br>
|
||||
<small><strong>Model:</strong> ${job.result.model_used}</small><br>
|
||||
${job.result.png_file ? `
|
||||
<div style="margin-top: 10px;">
|
||||
<img src="/api/predictions/preview/${job.result.png_file.split('/').pop()}"
|
||||
style="max-width: 100%; border-radius: 5px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);"
|
||||
onclick="viewLargePNG('${job.result.png_file.split('/').pop()}')"
|
||||
title="Click để xem lớn hơn">
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px; min-width: 200px;">
|
||||
${job.result && job.result.output_file ? `
|
||||
<button class="btn btn-success" style="padding: 8px 20px;" onclick="downloadResult('${job.result.output_file.split('/').pop()}')">
|
||||
💾 Download GeoTIFF
|
||||
</button>
|
||||
${job.result.png_file ? `
|
||||
<button class="btn btn-primary" style="padding: 8px 20px;" onclick="downloadPNG('${job.result.png_file.split('/').pop()}')">
|
||||
🖼️ Download PNG
|
||||
</button>
|
||||
<button class="btn btn-secondary" style="padding: 8px 20px;" onclick="viewLargePNG('${job.result.png_file.split('/').pop()}')">
|
||||
🔍 View Preview
|
||||
</button>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading batch results:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Download all results as ZIP (placeholder)
|
||||
function downloadAllResults() {
|
||||
alert('💡 Tính năng download tất cả batch results sẽ được thêm trong phiên bản tiếp theo.\\nHiện tại vui lòng download từng file riêng lẻ.');
|
||||
}
|
||||
|
||||
// Start/stop status monitoring
|
||||
function startStatusCheck() {
|
||||
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
||||
statusCheckInterval = setInterval(checkBatchStatus, 3000);
|
||||
}
|
||||
|
||||
function stopStatusCheck() {
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
statusCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.onload = function() {
|
||||
initMap();
|
||||
loadModels();
|
||||
};
|
||||
|
||||
// Cleanup on unload
|
||||
window.onbeforeunload = function() {
|
||||
stopStatusCheck();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,383 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Change Detection - Compare Current vs Future Land Use</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; }
|
||||
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
|
||||
.header h1 { font-size: 32px; margin-bottom: 10px; }
|
||||
.header p { font-size: 16px; opacity: 0.9; }
|
||||
.content { padding: 30px; display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
|
||||
.left-panel, .right-panel { display: flex; flex-direction: column; gap: 20px; }
|
||||
#map { width: 100%; height: 400px; border-radius: 8px; border: 2px solid #e0e0e0; }
|
||||
.section { background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #667eea; }
|
||||
.section h2 { color: #333; font-size: 18px; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 6px; color: #555; font-weight: 500; font-size: 14px; }
|
||||
.form-group input[type="text"], .form-group input[type="date"], .form-group input[type="number"], .form-group select { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; transition: all 0.3s ease; }
|
||||
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||||
.bbox-display { background: white; padding: 12px; border-radius: 6px; font-size: 13px; color: #666; font-family: monospace; border: 1px dashed #667eea; word-break: break-all; }
|
||||
.btn { padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; }
|
||||
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
||||
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||
.result { background: white; border: 2px solid #e0e0e0; border-radius: 8px; padding: 20px; display: none; animation: slideIn 0.3s ease; max-height: 600px; overflow-y: auto; }
|
||||
.result.success { border-color: #4caf50; background: #f1f8f5; }
|
||||
.result.error { border-color: #f44336; background: #fdf5f4; }
|
||||
.result.processing { border-color: #2196f3; background: #f3f8fd; }
|
||||
.result h3 { margin-bottom: 15px; color: #333; }
|
||||
.result table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
||||
.result table th, .result table td { padding: 10px; text-align: left; border-bottom: 1px solid #e0e0e0; }
|
||||
.result table th { background: #f0f0f0; font-weight: 600; color: #333; }
|
||||
.result pre { background: #f5f5f5; padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 12px; color: #333; max-height: 300px; overflow-y: auto; border-left: 4px solid #667eea; }
|
||||
.error-text { color: #f44336; font-weight: 500; }
|
||||
.success-text { color: #4caf50; font-weight: 500; }
|
||||
.processing-text { color: #2196f3; font-weight: 500; }
|
||||
.progress { width: 100%; height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; margin: 10px 0; }
|
||||
.progress-bar { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: 0%; transition: width 0.3s ease; }
|
||||
.stat-box { background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #667eea; margin: 10px 0; }
|
||||
.stat-label { font-size: 12px; color: #999; text-transform: uppercase; margin-bottom: 5px; }
|
||||
.stat-value { font-size: 20px; font-weight: 600; color: #333; }
|
||||
.info-box { background: #e3f2fd; padding: 12px; border-radius: 6px; border-left: 4px solid #2196f3; font-size: 13px; color: #1565c0; }
|
||||
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@media (max-width: 1024px) { .content { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔍 Change Detection - Land Use Analysis</h1>
|
||||
<p>Compare current land use with predicted future changes</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Left Panel -->
|
||||
<div class="left-panel">
|
||||
<div class="section">
|
||||
<h2><span>🗺️</span>Select Area on Map</h2>
|
||||
<p style="color: #999; font-size: 13px; margin-bottom: 10px;">Click on map to select bounding box</p>
|
||||
<div id="map"></div>
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<label>BBox (min_lon, min_lat, max_lon, max_lat)</label>
|
||||
<div class="bbox-display" id="bboxDisplay">Click on map to select area</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><span>📅</span>Current Period (Baseline)</h2>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Start Date</label>
|
||||
<input type="date" id="currentStartDate" value="2022-01-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>End Date</label>
|
||||
<input type="date" id="currentEndDate" value="2022-03-31">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><span>🔮</span>Prediction Period (Future)</h2>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Start Date</label>
|
||||
<input type="date" id="predictionStartDate" value="2023-01-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>End Date</label>
|
||||
<input type="date" id="predictionEndDate" value="2023-03-31">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><span>⚙️</span>Parameters</h2>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Max Scenes</label>
|
||||
<input type="number" id="maxScenes" value="12" min="1" max="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cloud Cover %</label>
|
||||
<input type="number" id="cloudCover" value="30" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Resolution (m)</label>
|
||||
<input type="number" id="resolution" value="20" min="10" max="100" step="10">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel -->
|
||||
<div class="right-panel">
|
||||
<div class="section">
|
||||
<h2><span>🤖</span>Select Trained Model</h2>
|
||||
<div class="form-group">
|
||||
<label>Trained Model</label>
|
||||
<select id="modelSelect">
|
||||
<option value="">Loading models...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="modelInfo" style="font-size: 12px; color: #999; margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><span>�</span>Workflow</h2>
|
||||
<div class="info-box">
|
||||
1️⃣ Classify current period satellite data<br>
|
||||
2️⃣ Classify future period satellite data<br>
|
||||
3️⃣ Compare to detect land use changes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<button class="btn btn-primary" id="runBtn" onclick="runChangeDetection()" disabled>
|
||||
<span>▶️</span>Compare Periods
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="resultDiv" class="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
let map, rectangle;
|
||||
let bbox = null;
|
||||
|
||||
function initMap() {
|
||||
map = L.map('map').setView([9.8, 105.85], 10);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
const defaultBbox = [105.6, 9.3, 106.2, 9.8];
|
||||
drawBboxRectangle(defaultBbox);
|
||||
|
||||
map.on('click', function(e) {
|
||||
const size = 0.3;
|
||||
const bounds = L.latLngBounds([
|
||||
[e.latlng.lat - size, e.latlng.lng - size],
|
||||
[e.latlng.lat + size, e.latlng.lng + size]
|
||||
]);
|
||||
drawBboxRectangle([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]);
|
||||
});
|
||||
}
|
||||
|
||||
function drawBboxRectangle(bboxArray) {
|
||||
const [minLon, minLat, maxLon, maxLat] = bboxArray;
|
||||
if (rectangle) map.removeLayer(rectangle);
|
||||
|
||||
rectangle = L.rectangle([[minLat, minLon], [maxLat, maxLon]], {
|
||||
color: '#667eea', weight: 2, fillColor: '#667eea', fillOpacity: 0.1
|
||||
}).addTo(map);
|
||||
|
||||
map.fitBounds(rectangle.getBounds());
|
||||
bbox = bboxArray;
|
||||
document.getElementById('bboxDisplay').textContent =
|
||||
`[${minLon.toFixed(4)}, ${minLat.toFixed(4)}, ${maxLon.toFixed(4)}, ${maxLat.toFixed(4)}]`;
|
||||
updateRunButtonState();
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/models/list`);
|
||||
const data = await response.json();
|
||||
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
modelSelect.innerHTML = '<option value="">-- Select a model --</option>';
|
||||
|
||||
if (data.models && data.models.length > 0) {
|
||||
data.models.forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.filename;
|
||||
option.textContent = `${model.filename} (${model.size_mb}MB)`;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
} else {
|
||||
modelSelect.innerHTML = '<option value="">No trained models found</option>';
|
||||
}
|
||||
|
||||
modelSelect.addEventListener('change', () => {
|
||||
updateModelInfo();
|
||||
updateRunButtonState();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading models:', error);
|
||||
document.getElementById('modelSelect').innerHTML = '<option value="">Error loading models</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateModelInfo() {
|
||||
const modelName = document.getElementById('modelSelect').value;
|
||||
document.getElementById('modelInfo').textContent = modelName ? `Selected: ${modelName}` : '';
|
||||
}
|
||||
|
||||
function updateRunButtonState() {
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
runBtn.disabled = !bbox || !document.getElementById('modelSelect').value;
|
||||
}
|
||||
|
||||
async function runChangeDetection() {
|
||||
const resultDiv = document.getElementById('resultDiv');
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
|
||||
if (!bbox) {
|
||||
showResult('error', 'Error', 'Please select an area on the map');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFilename = document.getElementById('modelSelect').value;
|
||||
if (!modelFilename) {
|
||||
showResult('error', 'Error', 'Please select a trained model');
|
||||
return;
|
||||
}
|
||||
|
||||
runBtn.disabled = true;
|
||||
showResult('processing', 'Processing', 'Analyzing land use changes...');
|
||||
|
||||
try {
|
||||
const [minLon, minLat, maxLon, maxLat] = bbox;
|
||||
const currentStartDate = document.getElementById('currentStartDate').value;
|
||||
const currentEndDate = document.getElementById('currentEndDate').value;
|
||||
const predictionStartDate = document.getElementById('predictionStartDate').value;
|
||||
const predictionEndDate = document.getElementById('predictionEndDate').value;
|
||||
const maxScenes = parseInt(document.getElementById('maxScenes').value);
|
||||
const cloudCover = parseInt(document.getElementById('cloudCover').value);
|
||||
const resolution = parseInt(document.getElementById('resolution').value);
|
||||
|
||||
showResult('processing', 'Step 1/3', 'Classifying current period (baseline)...');
|
||||
|
||||
const payload = {
|
||||
model_filename: modelFilename,
|
||||
min_lon: minLon, min_lat: minLat, max_lon: maxLon, max_lat: maxLat,
|
||||
current_period: {
|
||||
start_date: currentStartDate,
|
||||
end_date: currentEndDate
|
||||
},
|
||||
prediction_period: {
|
||||
start_date: predictionStartDate,
|
||||
end_date: predictionEndDate
|
||||
},
|
||||
max_scenes: maxScenes,
|
||||
cloud_cover: cloudCover,
|
||||
resolution: resolution,
|
||||
export_ndvi: true,
|
||||
export_classification: true
|
||||
};
|
||||
|
||||
const changeResponse = await fetch(`${API_BASE}/change-detection/compare-periods`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!changeResponse.ok) {
|
||||
const errorData = await changeResponse.json();
|
||||
throw new Error(errorData.detail || 'Analysis failed');
|
||||
}
|
||||
|
||||
const changeResult = await changeResponse.json();
|
||||
displayResults(changeResult);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showResult('error', 'Error', error.message);
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(result) {
|
||||
const resultDiv = document.getElementById('resultDiv');
|
||||
let html = '<h3 class="success-text">✓ Change Detection Completed</h3>';
|
||||
|
||||
// Current period classification
|
||||
if (result.current_classification) {
|
||||
const curr = result.current_classification;
|
||||
html += '<div class="stat-box"><div class="stat-label">📊 Current Period Classification</div>';
|
||||
html += `<div style="color: #666; font-size: 12px; margin-bottom: 10px;">Scenes: ${curr.n_scenes} | Resolution: ${curr.resolution}m</div>`;
|
||||
|
||||
if (curr.class_distribution) {
|
||||
html += '<table>';
|
||||
Object.entries(curr.class_distribution).forEach(([cls, count]) => {
|
||||
const percentage = ((count / Object.values(curr.class_distribution).reduce((a,b) => a+b, 0)) * 100).toFixed(1);
|
||||
html += `<tr><td>Class ${cls}:</td><td><strong>${count}</strong> (${percentage}%)</td></tr>`;
|
||||
});
|
||||
html += '</table>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Prediction period classification
|
||||
if (result.prediction_classification) {
|
||||
const pred = result.prediction_classification;
|
||||
html += '<div class="stat-box"><div class="stat-label">🔮 Prediction Period Classification</div>';
|
||||
html += `<div style="color: #666; font-size: 12px; margin-bottom: 10px;">Scenes: ${pred.n_scenes} | Resolution: ${pred.resolution}m</div>`;
|
||||
|
||||
if (pred.class_distribution) {
|
||||
html += '<table>';
|
||||
Object.entries(pred.class_distribution).forEach(([cls, count]) => {
|
||||
const percentage = ((count / Object.values(pred.class_distribution).reduce((a,b) => a+b, 0)) * 100).toFixed(1);
|
||||
html += `<tr><td>Class ${cls}:</td><td><strong>${count}</strong> (${percentage}%)</td></tr>`;
|
||||
});
|
||||
html += '</table>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Change detection
|
||||
if (result.change_detection) {
|
||||
const cd = result.change_detection;
|
||||
html += '<div class="stat-box"><div class="stat-label">🔄 Change Detection Summary</div>';
|
||||
html += `<div class="stat-value" style="color: #e74c3c;">${(cd.change_rate * 100).toFixed(2)}% Changed</div>`;
|
||||
html += '<table>';
|
||||
html += '<tr><td>Changed Pixels:</td><td><strong>' + cd.n_changed_pixels.toLocaleString() + '</strong></td></tr>';
|
||||
html += '<tr><td>Total Pixels:</td><td><strong>' + cd.n_total_pixels.toLocaleString() + '</strong></td></tr>';
|
||||
html += '</table>';
|
||||
|
||||
if (Object.keys(cd.change_matrix).length > 0) {
|
||||
html += '<div style="margin-top: 10px;"><strong>Transitions (Current → Prediction):</strong></div>';
|
||||
html += '<pre>' + JSON.stringify(cd.change_matrix, null, 2) + '</pre>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showResult(type, title, message) {
|
||||
const resultDiv = document.getElementById('resultDiv');
|
||||
const typeClass = type === 'error' ? 'error' : (type === 'processing' ? 'processing' : 'success');
|
||||
const textClass = type === 'error' ? 'error-text' : (type === 'processing' ? 'processing-text' : 'success-text');
|
||||
|
||||
resultDiv.innerHTML = `<h3 class="${textClass}">${title}</h3><p>${message}</p>` +
|
||||
(type === 'processing' ? '<div class="progress"><div class="progress-bar" style="animation: progress 2s infinite;"></div></div>' : '');
|
||||
resultDiv.className = `result ${typeClass}`;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMap();
|
||||
loadModels();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
import xarray as xr
|
||||
import rasterio
|
||||
|
||||
print(f"xarray version: {xr.__version__}")
|
||||
print(f"rasterio version: {rasterio.__version__}")
|
||||
+3
-6236
File diff suppressed because one or more lines are too long
+3
-5851
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,628 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,796 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cloud Removal Training - Deep Learning</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 80px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
animation: headerGlow 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes headerGlow {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(-20%, -20%); }
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.8em;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-shadow: 0 2px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.15em;
|
||||
opacity: 0.95;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: rgba(255,255,255,0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 18px 30px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
padding: 12px 24px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.nav a:nth-child(1) { background: linear-gradient(135deg, #667eea, #764ba2); }
|
||||
.nav a:nth-child(2) { background: linear-gradient(135deg, #f093fb, #f5576c); }
|
||||
.nav a:nth-child(3) { background: linear-gradient(135deg, #4facfe, #00f2fe); }
|
||||
.nav a:nth-child(4) { background: linear-gradient(135deg, #43e97b, #38f9d7); }
|
||||
|
||||
.nav a:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 28px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-size: 1.6em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #374151;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
font-size: 1em;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="text"]:hover,
|
||||
input[type="number"]:hover,
|
||||
select:hover {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 32px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(107, 114, 128, 0.5);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(220, 53, 69, 0.5);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.model-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.model-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.model-card h3 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
font-size: 0.9em;
|
||||
color: #6b7280;
|
||||
margin: 6px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
color: #155724;
|
||||
box-shadow: 0 2px 8px rgba(21, 87, 36, 0.2);
|
||||
}
|
||||
|
||||
.status-training {
|
||||
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||
color: #856404;
|
||||
box-shadow: 0 2px 8px rgba(133, 100, 4, 0.2);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
|
||||
color: #721c24;
|
||||
box-shadow: 0 2px 8px rgba(114, 28, 36, 0.2);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: linear-gradient(to right, #e5e7eb, #f3f4f6);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
box-shadow: inset 0 2px 8px rgba(0,0,0,0.08);
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
transition: width 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
|
||||
border-left: 5px solid #2196F3;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-box:hover {
|
||||
box-shadow: 0 6px 25px rgba(33, 150, 243, 0.15);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||
border-left: 5px solid #ffc107;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.warning-box:hover {
|
||||
box-shadow: 0 6px 25px rgba(255, 193, 7, 0.15);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.model-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.logs {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
box-shadow: inset 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.logs .log-entry {
|
||||
margin: 5px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.logs .log-info {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.logs .log-warning {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.logs .log-error {
|
||||
color: #f48771;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌥️ Cloud Removal Training</h1>
|
||||
<p>Train Deep Learning Models để khử mây từ ảnh Sentinel-2</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/">← Trang chủ</a>
|
||||
<a href="/training">Land Classification</a>
|
||||
<a href="/prediction">Prediction</a>
|
||||
<a href="#models">Models đã train</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Info Section -->
|
||||
<div class="section">
|
||||
<div class="info-box">
|
||||
<strong>📚 Dataset:</strong> SEN12MS-CR (Sentinel-12 Multi-Seasonal Cloud Removal)<br>
|
||||
<strong>🏗️ Architecture:</strong> U-Net với skip connections<br>
|
||||
<strong>📊 Input:</strong> S2 cloudy (4 bands) + S1 radar (2 bands) = 6 channels<br>
|
||||
<strong>🎯 Output:</strong> S2 clean (4 bands)<br>
|
||||
<strong>⏱️ Training time:</strong> ~2-3 hours (GPU) / ~20-30 hours (CPU)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Configuration -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">⚙️ Cấu hình Training</h2>
|
||||
|
||||
<div class="card">
|
||||
<form id="trainingForm">
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>🏗️ Model Architecture</label>
|
||||
<select id="modelArchitecture" required>
|
||||
<option value="unet">U-Net (Classic CNN)</option>
|
||||
<option value="crgan">CR-GAN (Cloud Removal GAN)</option>
|
||||
<option value="spagan">SpA-GAN (Spatial Attention GAN)</option>
|
||||
<option value="glfcr">GLF-CR (Global-Local Fusion)</option>
|
||||
<option value="sen12mscr">SEN12MS-CR (Multi-modal)</option>
|
||||
<option value="rsdehazenet">RSDehazeNet (Remote Sensing)</option>
|
||||
<option value="cloudnet">Cloud-Net (Encoder-Decoder)</option>
|
||||
<option value="dsen2cr">DSen2-CR (Deep Sentinel-2)</option>
|
||||
</select>
|
||||
<small style="color: #6c757d;">Chọn kiến trúc deep learning cho cloud removal</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>🏷️ Model Name</label>
|
||||
<input type="text" id="modelName" value="cloud_removal_unet" required>
|
||||
<small style="color: #6c757d;">Tên model để lưu</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>📂 Data Directory</label>
|
||||
<input type="text" id="dataDir" value="winter_dataset" required>
|
||||
<small style="color: #6c757d;">Thư mục chứa dữ liệu SEN12MS-CR</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>📦 Batch Size</label>
|
||||
<input type="number" id="batchSize" value="8" min="1" max="32" required>
|
||||
<small style="color: #6c757d;">Giảm xuống 4 hoặc 2 nếu GPU hết RAM</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>🔄 Number of Epochs</label>
|
||||
<input type="number" id="numEpochs" value="50" min="1" max="200" required>
|
||||
<small style="color: #6c757d;">Số lượng epochs training</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>📈 Learning Rate</label>
|
||||
<input type="number" id="learningRate" value="0.0001" step="0.00001" min="0.00001" max="0.01" required>
|
||||
<small style="color: #6c757d;">Learning rate (default: 1e-4)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="useS1" checked>
|
||||
<label for="useS1">📡 Use Sentinel-1 (Radar Data)</label>
|
||||
</div>
|
||||
<small style="color: #6c757d;">Sử dụng dữ liệu radar (VV, VH) để cải thiện kết quả</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="useGPU" checked>
|
||||
<label for="useGPU">🚀 Use GPU</label>
|
||||
</div>
|
||||
<small style="color: #6c757d;">Sử dụng GPU để training nhanh hơn</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-primary">🚀 Start Training</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="refreshModels()">🔄 Refresh Models</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Status -->
|
||||
<div class="section" id="trainingStatus" style="display: none;">
|
||||
<h2 class="section-title">📊 Training Status</h2>
|
||||
<div class="card">
|
||||
<div id="statusMessage"></div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressBar" style="width: 0%;">0%</div>
|
||||
</div>
|
||||
<div class="logs" id="trainingLogs">
|
||||
<div class="log-entry log-info">Training logs will appear here...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Models List -->
|
||||
<div class="section" id="models">
|
||||
<h2 class="section-title">🤖 Cloud Removal Models</h2>
|
||||
<div class="model-list" id="modelsList">
|
||||
<div class="model-card">
|
||||
<p style="text-align: center; color: #6c757d;">Loading models...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Methods Info -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">📖 Cloud Removal Deep Learning Architectures</h2>
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h3>🔹 U-Net</h3>
|
||||
<p>Classic encoder-decoder with skip connections. Fast training, good baseline performance.</p>
|
||||
<div class="status-badge status-success">Recommended for beginners</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 CR-GAN</h3>
|
||||
<p>Cloud Removal GAN - adversarial training cho kết quả chân thực hơn.</p>
|
||||
<div class="status-badge status-training">Advanced</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 SpA-GAN</h3>
|
||||
<p>Spatial Attention GAN - attention mechanism tập trung vào vùng có mây.</p>
|
||||
<div class="status-badge status-success">Best quality</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 GLF-CR</h3>
|
||||
<p>Global-Local Fusion - kết hợp features global và local cho chi tiết tốt hơn.</p>
|
||||
<div class="status-badge status-training">High accuracy</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 SEN12MS-CR</h3>
|
||||
<p>Multi-modal fusion - kết hợp Sentinel-1 radar và Sentinel-2 optical.</p>
|
||||
<div class="status-badge status-success">Multi-sensor</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 RSDehazeNet</h3>
|
||||
<p>Remote Sensing Dehaze Network - chuyên cho ảnh viễn thám.</p>
|
||||
<div class="status-badge status-training">RS specialized</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 Cloud-Net</h3>
|
||||
<p>Encoder-Decoder architecture với residual connections.</p>
|
||||
<div class="status-badge status-success">Balanced</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔹 DSen2-CR</h3>
|
||||
<p>Deep Sentinel-2 Cloud Removal - tận dụng temporal information.</p>
|
||||
<div class="status-badge status-training">Temporal fusion</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load models on page load
|
||||
window.addEventListener('load', () => {
|
||||
refreshModels();
|
||||
loadCloudRemovalMethods();
|
||||
});
|
||||
|
||||
// Handle training form submission
|
||||
document.getElementById('trainingForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const config = {
|
||||
data_dir: document.getElementById('dataDir').value,
|
||||
model_name: document.getElementById('modelName').value,
|
||||
architecture: document.getElementById('modelArchitecture').value,
|
||||
use_s1: document.getElementById('useS1').checked,
|
||||
batch_size: parseInt(document.getElementById('batchSize').value),
|
||||
num_epochs: parseInt(document.getElementById('numEpochs').value),
|
||||
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
||||
use_gpu: document.getElementById('useGPU').checked
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/cloud-removal/train', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Show training status section
|
||||
document.getElementById('trainingStatus').style.display = 'block';
|
||||
document.getElementById('statusMessage').innerHTML = `
|
||||
<div class="status-badge status-training">Training Started: ${result.training_id}</div>
|
||||
<p style="margin-top: 10px;">Model training has started in background. This may take several hours.</p>
|
||||
`;
|
||||
|
||||
addLog('info', `Training started: ${result.training_id}`);
|
||||
addLog('info', `Config: ${JSON.stringify(config, null, 2)}`);
|
||||
|
||||
// Simulate progress (actual progress would come from websocket)
|
||||
simulateProgress();
|
||||
} else {
|
||||
alert('Error starting training: ' + (result.detail || result.error));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh models list
|
||||
async function refreshModels() {
|
||||
try {
|
||||
const response = await fetch('/api/cloud-removal/models');
|
||||
const data = await response.json();
|
||||
|
||||
const modelsList = document.getElementById('modelsList');
|
||||
|
||||
if (data.models && data.models.length > 0) {
|
||||
modelsList.innerHTML = data.models.map(model => `
|
||||
<div class="model-card">
|
||||
<h3>📦 ${model.filename}</h3>
|
||||
<div class="model-info">🏗️ Architecture: ${model.architecture || 'U-Net'}</div>
|
||||
<div class="model-info">📊 Epoch: ${model.epoch}</div>
|
||||
<div class="model-info">📉 Train Loss: ${model.train_loss.toFixed(6)}</div>
|
||||
<div class="model-info">📉 Val Loss: ${model.val_loss.toFixed(6)}</div>
|
||||
<div class="model-info">📡 Use S1: ${model.use_s1 ? 'Yes' : 'No'}</div>
|
||||
<div class="model-info">💾 Size: ${model.size_mb.toFixed(2)} MB</div>
|
||||
<div class="model-info">📅 Created: ${new Date(model.created * 1000).toLocaleString()}</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button class="btn btn-danger" onclick="deleteModel('${model.filename}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
modelsList.innerHTML = `
|
||||
<div class="model-card">
|
||||
<p style="text-align: center; color: #6c757d;">
|
||||
No cloud removal models found.<br>
|
||||
Train your first model above!
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete model
|
||||
async function deleteModel(filename) {
|
||||
if (!confirm(`Delete model ${filename}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/cloud-removal/models/${filename}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Model deleted successfully');
|
||||
refreshModels();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert('Error deleting model: ' + error.detail);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Load cloud removal methods
|
||||
async function loadCloudRemovalMethods() {
|
||||
try {
|
||||
const response = await fetch('/api/cloud-removal/methods');
|
||||
const data = await response.json();
|
||||
console.log('Available cloud removal methods:', data.methods);
|
||||
} catch (error) {
|
||||
console.error('Error loading methods:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add log entry
|
||||
function addLog(type, message) {
|
||||
const logs = document.getElementById('trainingLogs');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logClass = type === 'error' ? 'log-error' : (type === 'warning' ? 'log-warning' : 'log-info');
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${logClass}`;
|
||||
entry.textContent = `[${timestamp}] ${message}`;
|
||||
|
||||
logs.appendChild(entry);
|
||||
logs.scrollTop = logs.scrollHeight;
|
||||
}
|
||||
|
||||
// Simulate progress (replace with real progress tracking)
|
||||
function simulateProgress() {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 5;
|
||||
if (progress >= 100) {
|
||||
progress = 100;
|
||||
clearInterval(interval);
|
||||
addLog('info', 'Training completed! Check models list below.');
|
||||
setTimeout(refreshModels, 2000);
|
||||
}
|
||||
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
progressBar.style.width = progress + '%';
|
||||
progressBar.textContent = Math.floor(progress) + '%';
|
||||
|
||||
if (progress % 10 < 5) {
|
||||
addLog('info', `Training progress: ${Math.floor(progress)}%`);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,81 +0,0 @@
|
||||
"""
|
||||
Tạo metadata cho model_odc.joblib (legacy model)
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Metadata cho model_odc.joblib
|
||||
# Model này là GridSearchCV Pipeline với 39 features (temporal mode)
|
||||
# Features: NDVI time series + NDWI time series + NDBI time series + radar features
|
||||
|
||||
# Calculate feature names for temporal mode with 12 timesteps
|
||||
# (12 NDVI + 12 NDWI + 12 NDBI + 3 radar = 39 features)
|
||||
n_timesteps = 12
|
||||
feature_names = []
|
||||
|
||||
# NDVI time series
|
||||
for t in range(n_timesteps):
|
||||
feature_names.append(f"NDVI_t{t+1}")
|
||||
|
||||
# NDWI time series
|
||||
for t in range(n_timesteps):
|
||||
feature_names.append(f"NDWI_t{t+1}")
|
||||
|
||||
# NDBI time series
|
||||
for t in range(n_timesteps):
|
||||
feature_names.append(f"NDBI_t{t+1}")
|
||||
|
||||
# Radar features
|
||||
feature_names.extend(["VH_db_mean", "VV_db_mean", "VH_VV_ratio"])
|
||||
|
||||
metadata = {
|
||||
"timestamp": "2025-12-20T10:00:00",
|
||||
"data_source": "Unknown (Legacy model)",
|
||||
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
|
||||
"features": feature_names,
|
||||
"feature_mode": "temporal", # IMPORTANT: temporal mode with 39 features
|
||||
"training_samples": None,
|
||||
"testing_samples": None,
|
||||
"test_size": 0.2,
|
||||
"train_accuracy": None,
|
||||
"test_accuracy": None,
|
||||
"model_type": "random_forest", # GridSearchCV with RandomForest
|
||||
"device": "cpu",
|
||||
"n_estimators": 100,
|
||||
"max_depth": None,
|
||||
"learning_rate": None,
|
||||
"cnn_epochs": None,
|
||||
"n_features": 39, # GridSearchCV expects 39 features!
|
||||
"n_classes": 8,
|
||||
"class_names": [
|
||||
"Lua tom", # 0
|
||||
"Lua", # 1
|
||||
"CHN", # 2
|
||||
"CLN", # 3
|
||||
"TS", # 4
|
||||
"Song", # 5
|
||||
"Dat xay dung", # 6
|
||||
"Rung" # 7
|
||||
],
|
||||
"classification_report": None,
|
||||
"confusion_matrix": None,
|
||||
"bbox": None,
|
||||
"time_range": None,
|
||||
"resolution": 10,
|
||||
"notes": "Legacy GridSearchCV Pipeline model with 39 temporal features (12 timesteps each for NDVI/NDWI/NDBI + 3 radar features). Requires temporal mode feature extraction."
|
||||
}
|
||||
|
||||
# Save metadata
|
||||
model_train_dir = Path("model_train")
|
||||
metadata_file = model_train_dir / "model_odc_info.json"
|
||||
|
||||
print("Creating metadata for model_odc.joblib...")
|
||||
print(f"Saving to: {metadata_file}")
|
||||
|
||||
with open(metadata_file, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
print("✅ Metadata created successfully!")
|
||||
print("\nMetadata content:")
|
||||
print(json.dumps(metadata, indent=2))
|
||||
-935
@@ -1,935 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Land Classification System</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #667eea;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
padding: 15px 25px;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.stat-card .icon {
|
||||
font-size: 3em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
color: #666;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.batch-queue {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.batch-item {
|
||||
padding: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.batch-item:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.batch-item.running {
|
||||
border-color: #4caf50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.batch-item.completed {
|
||||
border-color: #2196f3;
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.batch-item.failed {
|
||||
border-color: #f44336;
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
.batch-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.batch-name {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.batch-status {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.batch-status.queued {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.batch-status.running {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.batch-status.completed {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.batch-status.failed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-upload input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload label {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-upload label:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '...';
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60%, 100% { content: '...'; }
|
||||
}
|
||||
|
||||
.model-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.model-selector select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-selector select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📊 Dashboard - Land Classification System</h1>
|
||||
<p>Tổng quan hệ thống phân loại đất từ xa</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-tabs">
|
||||
<button class="nav-tab active" onclick="switchTab('overview')">📈 Tổng Quan</button>
|
||||
<button class="nav-tab" onclick="switchTab('trends')">📊 Accuracy Trends</button>
|
||||
<button class="nav-tab" onclick="switchTab('batch')">🔄 Batch Processing</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Tổng Quan -->
|
||||
<div id="overview" class="tab-content active">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="icon">🤖</div>
|
||||
<div class="value" id="totalModels">-</div>
|
||||
<div class="label">Models Trained</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon">🗺️</div>
|
||||
<div class="value" id="totalPredictions">-</div>
|
||||
<div class="label">Predictions Generated</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon">📄</div>
|
||||
<div class="value" id="totalReports">-</div>
|
||||
<div class="label">Reports Created</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon">✅</div>
|
||||
<div class="value" id="latestAccuracy">-</div>
|
||||
<div class="label">Latest Model Accuracy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>📊 Phân bố các lớp đất (Model mới nhất)</h3>
|
||||
<div class="model-selector">
|
||||
<select id="modelSelect" onchange="loadClassDistribution()">
|
||||
<option value="">Chọn model...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="classDistChart"></canvas>
|
||||
</div>
|
||||
<div class="export-buttons">
|
||||
<button class="btn btn-primary" onclick="exportChart('classDistChart', 'class-distribution.png')">
|
||||
💾 Export PNG
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="exportChartPDF('classDistChart', 'class-distribution.pdf')">
|
||||
📄 Export PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Accuracy Trends -->
|
||||
<div id="trends" class="tab-content">
|
||||
<div class="chart-container">
|
||||
<h3>📈 Accuracy Trends Over Time</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="accuracyTrendChart"></canvas>
|
||||
</div>
|
||||
<div class="export-buttons">
|
||||
<button class="btn btn-primary" onclick="exportChart('accuracyTrendChart', 'accuracy-trends.png')">
|
||||
💾 Export PNG
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="exportChartPDF('accuracyTrendChart', 'accuracy-trends.pdf')">
|
||||
📄 Export PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>📊 F1-Score Comparison</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="f1ScoreChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Batch Processing -->
|
||||
<div id="batch" class="tab-content">
|
||||
<div class="batch-queue">
|
||||
<h3>🔄 Batch Prediction Queue</h3>
|
||||
|
||||
<div class="file-upload">
|
||||
<label for="csvFile">📁 Upload CSV File</label>
|
||||
<input type="file" id="csvFile" accept=".csv" onchange="handleCSVUpload(event)">
|
||||
<p style="margin-top: 10px; color: #666;">
|
||||
Format CSV: name,min_lon,min_lat,max_lon,max_lat
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="model-selector">
|
||||
<select id="batchModelSelect">
|
||||
<option value="">Chọn model để predict...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="startBatchPrediction()" style="margin-bottom: 30px;">
|
||||
🚀 Start Batch Prediction
|
||||
</button>
|
||||
|
||||
<h4 style="margin: 20px 0;">Queue Status</h4>
|
||||
<div class="stats-grid" style="margin-bottom: 30px;">
|
||||
<div class="stat-card">
|
||||
<div class="value" id="queuedJobs">0</div>
|
||||
<div class="label">⏳ Queued</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" id="runningJobs">0</div>
|
||||
<div class="label">▶️ Running</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" id="completedJobs">0</div>
|
||||
<div class="label">✅ Completed</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" id="failedJobs">0</div>
|
||||
<div class="label">❌ Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 20px 0;">Active Jobs</h4>
|
||||
<div id="batchJobs">
|
||||
<p class="loading">Đang tải dữ liệu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
let charts = {};
|
||||
let batchItems = [];
|
||||
let refreshInterval = null;
|
||||
|
||||
// Tab switching
|
||||
function switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
|
||||
// Load data for the active tab
|
||||
if (tabName === 'overview') {
|
||||
loadDashboardStats();
|
||||
} else if (tabName === 'trends') {
|
||||
loadAccuracyTrends();
|
||||
} else if (tabName === 'batch') {
|
||||
loadBatchStatus();
|
||||
startBatchRefresh();
|
||||
} else {
|
||||
stopBatchRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Load dashboard statistics
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/statistics');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('totalModels').textContent = data.models.total;
|
||||
document.getElementById('totalPredictions').textContent = data.predictions.total;
|
||||
document.getElementById('totalReports').textContent = data.reports.total;
|
||||
|
||||
if (data.models.latest && data.models.latest.metrics) {
|
||||
const accuracy = (data.models.latest.metrics.accuracy * 100).toFixed(2);
|
||||
document.getElementById('latestAccuracy').textContent = accuracy + '%';
|
||||
}
|
||||
|
||||
// Load models for selector
|
||||
await loadModelsList();
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load models list
|
||||
async function loadModelsList() {
|
||||
try {
|
||||
const response = await fetch('/api/models/list');
|
||||
const data = await response.json();
|
||||
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const batchModelSelect = document.getElementById('batchModelSelect');
|
||||
|
||||
modelSelect.innerHTML = '<option value="">Chọn model...</option>';
|
||||
batchModelSelect.innerHTML = '<option value="">Chọn model...</option>';
|
||||
|
||||
data.models.forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.filename;
|
||||
option.textContent = `${model.filename} (${model.created})`;
|
||||
modelSelect.appendChild(option.cloneNode(true));
|
||||
batchModelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Auto-select latest model
|
||||
if (data.models.length > 0) {
|
||||
modelSelect.value = data.models[0].filename;
|
||||
await loadClassDistribution();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load class distribution
|
||||
async function loadClassDistribution() {
|
||||
const modelFilename = document.getElementById('modelSelect').value;
|
||||
if (!modelFilename) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/dashboard/class-distribution/${modelFilename}`);
|
||||
const data = await response.json();
|
||||
|
||||
const labels = Object.keys(data.class_distribution);
|
||||
const values = Object.values(data.class_distribution);
|
||||
|
||||
if (charts.classDistChart) {
|
||||
charts.classDistChart.destroy();
|
||||
}
|
||||
|
||||
const ctx = document.getElementById('classDistChart').getContext('2d');
|
||||
charts.classDistChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Số lượng mẫu',
|
||||
data: values,
|
||||
backgroundColor: [
|
||||
'rgba(102, 126, 234, 0.7)',
|
||||
'rgba(118, 75, 162, 0.7)',
|
||||
'rgba(76, 175, 80, 0.7)',
|
||||
'rgba(244, 67, 54, 0.7)',
|
||||
'rgba(33, 150, 243, 0.7)',
|
||||
'rgba(255, 193, 7, 0.7)',
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(102, 126, 234, 1)',
|
||||
'rgba(118, 75, 162, 1)',
|
||||
'rgba(76, 175, 80, 1)',
|
||||
'rgba(244, 67, 54, 1)',
|
||||
'rgba(33, 150, 243, 1)',
|
||||
'rgba(255, 193, 7, 1)',
|
||||
],
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: `Tổng: ${data.total_samples} mẫu`
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading class distribution:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load accuracy trends
|
||||
async function loadAccuracyTrends() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/accuracy-trends');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.trends.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare data
|
||||
const labels = data.trends.map(d => new Date(d.date).toLocaleDateString('vi-VN'));
|
||||
const accuracies = data.trends.map(d => d.accuracy * 100);
|
||||
const f1Scores = data.trends.map(d => d.f1_score * 100);
|
||||
const precisions = data.trends.map(d => d.precision * 100);
|
||||
const recalls = data.trends.map(d => d.recall * 100);
|
||||
|
||||
// Accuracy Trend Chart
|
||||
if (charts.accuracyTrendChart) {
|
||||
charts.accuracyTrendChart.destroy();
|
||||
}
|
||||
|
||||
const ctx1 = document.getElementById('accuracyTrendChart').getContext('2d');
|
||||
charts.accuracyTrendChart = new Chart(ctx1, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Accuracy (%)',
|
||||
data: accuracies,
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Precision (%)',
|
||||
data: precisions,
|
||||
borderColor: 'rgba(76, 175, 80, 1)',
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||
fill: false,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Recall (%)',
|
||||
data: recalls,
|
||||
borderColor: 'rgba(244, 67, 54, 1)',
|
||||
backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
||||
fill: false,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// F1-Score Chart
|
||||
if (charts.f1ScoreChart) {
|
||||
charts.f1ScoreChart.destroy();
|
||||
}
|
||||
|
||||
const ctx2 = document.getElementById('f1ScoreChart').getContext('2d');
|
||||
charts.f1ScoreChart = new Chart(ctx2, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'F1-Score (%)',
|
||||
data: f1Scores,
|
||||
backgroundColor: 'rgba(118, 75, 162, 0.7)',
|
||||
borderColor: 'rgba(118, 75, 162, 1)',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading accuracy trends:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Export chart as PNG
|
||||
function exportChart(chartId, filename) {
|
||||
const canvas = document.getElementById(chartId);
|
||||
const url = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.download = filename;
|
||||
link.href = url;
|
||||
link.click();
|
||||
}
|
||||
|
||||
// Export chart as PDF
|
||||
function exportChartPDF(chartId, filename) {
|
||||
const canvas = document.getElementById(chartId);
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'px',
|
||||
format: [canvas.width, canvas.height]
|
||||
});
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
|
||||
pdf.save(filename);
|
||||
}
|
||||
|
||||
// Handle CSV upload
|
||||
function handleCSVUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const text = e.target.result;
|
||||
parseCSV(text);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Parse CSV
|
||||
function parseCSV(text) {
|
||||
const lines = text.trim().split('\n');
|
||||
batchItems = [];
|
||||
|
||||
// Skip header
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const parts = lines[i].split(',');
|
||||
if (parts.length >= 5) {
|
||||
batchItems.push({
|
||||
name: parts[0].trim(),
|
||||
min_lon: parseFloat(parts[1]),
|
||||
min_lat: parseFloat(parts[2]),
|
||||
max_lon: parseFloat(parts[3]),
|
||||
max_lat: parseFloat(parts[4]),
|
||||
start_date: parts[5]?.trim() || "2023-03-01",
|
||||
end_date: parts[6]?.trim() || "2023-05-31",
|
||||
max_scenes: parseInt(parts[7]) || 12,
|
||||
cloud_cover: parseInt(parts[8]) || 30,
|
||||
resolution: parseInt(parts[9]) || 20
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert(`✅ Đã tải ${batchItems.length} khu vực từ CSV`);
|
||||
}
|
||||
|
||||
// Start batch prediction
|
||||
async function startBatchPrediction() {
|
||||
const modelFilename = document.getElementById('batchModelSelect').value;
|
||||
|
||||
if (!modelFilename) {
|
||||
alert('❌ Vui lòng chọn model');
|
||||
return;
|
||||
}
|
||||
|
||||
if (batchItems.length === 0) {
|
||||
alert('❌ Vui lòng upload file CSV trước');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/batch/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model_filename: modelFilename,
|
||||
items: batchItems,
|
||||
auto_retry: true,
|
||||
max_retries: 3
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
alert(`✅ ${result.message}`);
|
||||
|
||||
// Refresh batch status
|
||||
loadBatchStatus();
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
alert('❌ Lỗi khi bắt đầu batch prediction');
|
||||
}
|
||||
}
|
||||
|
||||
// Load batch status
|
||||
async function loadBatchStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/batch/status');
|
||||
const data = await response.json();
|
||||
|
||||
// Update counters
|
||||
document.getElementById('queuedJobs').textContent = data.queue.queued;
|
||||
document.getElementById('runningJobs').textContent = data.queue.running;
|
||||
document.getElementById('completedJobs').textContent = data.queue.completed;
|
||||
document.getElementById('failedJobs').textContent = data.queue.failed;
|
||||
|
||||
// Display jobs
|
||||
const jobsContainer = document.getElementById('batchJobs');
|
||||
jobsContainer.innerHTML = '';
|
||||
|
||||
// Combine all jobs
|
||||
const allJobs = [
|
||||
...data.jobs.running,
|
||||
...data.jobs.queued,
|
||||
...data.jobs.recent_completed,
|
||||
...data.jobs.recent_failed
|
||||
];
|
||||
|
||||
if (allJobs.length === 0) {
|
||||
jobsContainer.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
allJobs.forEach(job => {
|
||||
const jobElement = document.createElement('div');
|
||||
jobElement.className = `batch-item ${job.status}`;
|
||||
|
||||
const progress = job.progress || 0;
|
||||
const errorMsg = job.error ? `<p style="color: #f44336; margin-top: 10px;">⚠️ ${job.error}</p>` : '';
|
||||
|
||||
jobElement.innerHTML = `
|
||||
<div class="batch-header">
|
||||
<div class="batch-name">${job.name}</div>
|
||||
<div class="batch-status ${job.status}">${job.status.toUpperCase()}</div>
|
||||
</div>
|
||||
<p style="color: #666; margin: 5px 0;">Job ID: ${job.job_id}</p>
|
||||
<p style="color: #666; margin: 5px 0;">
|
||||
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
|
||||
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
|
||||
</p>
|
||||
${job.retries > 0 ? `<p style="color: #ff9800; margin: 5px 0;">🔄 Retries: ${job.retries}/${job.max_retries}</p>` : ''}
|
||||
${errorMsg}
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
jobsContainer.appendChild(jobElement);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading batch status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh batch status
|
||||
function startBatchRefresh() {
|
||||
if (refreshInterval) return;
|
||||
refreshInterval = setInterval(loadBatchStatus, 3000);
|
||||
}
|
||||
|
||||
function stopBatchRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
window.onload = function() {
|
||||
loadDashboardStats();
|
||||
};
|
||||
|
||||
// Cleanup on page unload
|
||||
window.onbeforeunload = function() {
|
||||
stopBatchRefresh();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7318566231680cb0c97883b7a5e4177aa1bfeec462a0f52ed34aa103ddec210b
|
||||
size 20903
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9cabf5e0241dcc3a73133ac8ae11171f34042c491f895b268c016407619bdfe1
|
||||
size 20903
|
||||
@@ -1,454 +0,0 @@
|
||||
"""
|
||||
Feature Extraction Module for Land Classification
|
||||
Chuẩn hóa việc trích xuất features từ satellite data cho cả training và prediction
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
|
||||
class FeatureExtractor:
|
||||
"""
|
||||
Extract features từ Sentinel-2 và Sentinel-1 data
|
||||
Hỗ trợ 2 modes:
|
||||
- 'simple': 3 features cơ bản (NDVI_mean, VH_mean, VV_mean)
|
||||
- 'temporal': 39 features time-series (NDVI + NDWI + NDBI theo thời gian)
|
||||
"""
|
||||
|
||||
FEATURE_MODES = {
|
||||
'simple': {
|
||||
'n_features': 3,
|
||||
'features': ['NDVI_mean', 'VH_db_mean', 'VV_db_mean'],
|
||||
'description': 'Simple aggregate features (mean only)'
|
||||
},
|
||||
'temporal': {
|
||||
'n_features': 39,
|
||||
'features': None, # Generated dynamically based on time steps
|
||||
'description': 'Temporal features with NDVI, NDWI, NDBI time series'
|
||||
},
|
||||
'extended': {
|
||||
'n_features': 15,
|
||||
'features': [
|
||||
'NDVI_mean', 'NDVI_std', 'NDVI_min', 'NDVI_max',
|
||||
'NDWI_mean', 'NDWI_std', 'NDWI_min', 'NDWI_max',
|
||||
'NDBI_mean', 'NDBI_std', 'NDBI_min', 'NDBI_max',
|
||||
'VH_db_mean', 'VV_db_mean', 'VH_VV_ratio'
|
||||
],
|
||||
'description': 'Extended aggregate features with statistics'
|
||||
},
|
||||
'odc': {
|
||||
'n_features': 8,
|
||||
'features': [
|
||||
'ndvi_mean', 'ndvi_min', 'ndvi_max', 'ndvi_std', 'ndvi_range',
|
||||
'ndwi_mean', 'ndbi_mean', 'evi_mean'
|
||||
],
|
||||
'description': 'ODC mode: 8 aggregate features (NDVI stats + NDWI/NDBI/EVI mean) - matches 01.train_ODC.ipynb'
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, mode: str = 'simple'):
|
||||
"""
|
||||
Initialize FeatureExtractor
|
||||
|
||||
Args:
|
||||
mode: 'simple', 'temporal', hoặc 'extended'
|
||||
"""
|
||||
if mode not in self.FEATURE_MODES:
|
||||
raise ValueError(f"Invalid mode: {mode}. Choose from {list(self.FEATURE_MODES.keys())}")
|
||||
|
||||
self.mode = mode
|
||||
self.config = self.FEATURE_MODES[mode]
|
||||
|
||||
def get_feature_names(self, n_timesteps: Optional[int] = None) -> List[str]:
|
||||
"""
|
||||
Lấy danh sách tên features
|
||||
|
||||
Args:
|
||||
n_timesteps: Số timesteps (chỉ cần cho mode='temporal')
|
||||
|
||||
Returns:
|
||||
List tên features
|
||||
"""
|
||||
if self.mode == 'temporal':
|
||||
if n_timesteps is None:
|
||||
raise ValueError("n_timesteps required for temporal mode")
|
||||
|
||||
features = []
|
||||
# NDVI time series
|
||||
for t in range(n_timesteps):
|
||||
features.append(f'NDVI_t{t+1}')
|
||||
# NDWI time series
|
||||
for t in range(n_timesteps):
|
||||
features.append(f'NDWI_t{t+1}')
|
||||
# NDBI time series
|
||||
for t in range(n_timesteps):
|
||||
features.append(f'NDBI_t{t+1}')
|
||||
|
||||
# VH/VV radar (mean across time)
|
||||
features.append('VH_db_mean')
|
||||
features.append('VV_db_mean')
|
||||
features.append('VH_VV_ratio')
|
||||
|
||||
return features
|
||||
else:
|
||||
return self.config['features']
|
||||
|
||||
def extract_simple_features(
|
||||
self,
|
||||
ndvi_data: xr.DataArray,
|
||||
vh_data: Optional[xr.DataArray] = None,
|
||||
vv_data: Optional[xr.DataArray] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Extract simple features (3 features: NDVI_mean, VH_db_mean, VV_db_mean)
|
||||
|
||||
Args:
|
||||
ndvi_data: NDVI DataArray (có thể có time dimension)
|
||||
vh_data: VH radar DataArray
|
||||
vv_data: VV radar DataArray
|
||||
|
||||
Returns:
|
||||
Feature array shape (n_pixels, 3)
|
||||
"""
|
||||
# Calculate NDVI mean
|
||||
if 'time' in ndvi_data.dims:
|
||||
ndvi_mean = ndvi_data.mean(dim='time')
|
||||
else:
|
||||
ndvi_mean = ndvi_data
|
||||
|
||||
# Flatten to pixels
|
||||
ndvi_flat = ndvi_mean.values.flatten()
|
||||
|
||||
# Calculate radar features if available
|
||||
if vh_data is not None and vv_data is not None:
|
||||
if 'time' in vh_data.dims:
|
||||
vh_mean = vh_data.mean(dim='time')
|
||||
vv_mean = vv_data.mean(dim='time')
|
||||
else:
|
||||
vh_mean = vh_data
|
||||
vv_mean = vv_data
|
||||
|
||||
vh_flat = vh_mean.values.flatten()
|
||||
vv_flat = vv_mean.values.flatten()
|
||||
else:
|
||||
# If no radar data, use zeros
|
||||
vh_flat = np.zeros_like(ndvi_flat)
|
||||
vv_flat = np.zeros_like(ndvi_flat)
|
||||
|
||||
# Stack features
|
||||
features = np.column_stack([ndvi_flat, vh_flat, vv_flat])
|
||||
|
||||
return features
|
||||
|
||||
def extract_temporal_features(
|
||||
self,
|
||||
s2_data: xr.Dataset,
|
||||
vh_data: Optional[xr.DataArray] = None,
|
||||
vv_data: Optional[xr.DataArray] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Extract temporal features (39 features: time series của NDVI, NDWI, NDBI + radar)
|
||||
|
||||
Args:
|
||||
s2_data: Sentinel-2 Dataset với bands B02, B03, B04, B08, B11
|
||||
vh_data: VH radar DataArray
|
||||
vv_data: VV radar DataArray
|
||||
|
||||
Returns:
|
||||
Feature array shape (n_pixels, 39)
|
||||
"""
|
||||
# Calculate spectral indices
|
||||
nir = s2_data["B08"].astype('float32')
|
||||
red = s2_data["B04"].astype('float32')
|
||||
green = s2_data["B03"].astype('float32')
|
||||
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"] # Fallback to B02
|
||||
|
||||
# NDVI = (NIR - Red) / (NIR + Red)
|
||||
ndvi = (nir - red) / (nir + red + 1e-8)
|
||||
|
||||
# NDWI = (Green - NIR) / (Green + NIR)
|
||||
ndwi = (green - nir) / (green + nir + 1e-8)
|
||||
|
||||
# NDBI = (SWIR - NIR) / (SWIR + NIR)
|
||||
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
||||
|
||||
# Resample to monthly if time dimension exists
|
||||
if 'time' in ndvi.dims:
|
||||
ndvi_monthly = ndvi.resample(time="1ME").mean()
|
||||
ndwi_monthly = ndwi.resample(time="1ME").mean()
|
||||
ndbi_monthly = ndbi.resample(time="1ME").mean()
|
||||
else:
|
||||
ndvi_monthly = ndvi
|
||||
ndwi_monthly = ndwi
|
||||
ndbi_monthly = ndbi
|
||||
|
||||
# Get dimensions
|
||||
n_times = len(ndvi_monthly.time) if 'time' in ndvi_monthly.dims else 1
|
||||
y_size = len(ndvi_monthly.y)
|
||||
x_size = len(ndvi_monthly.x)
|
||||
n_pixels = y_size * x_size
|
||||
|
||||
# Extract temporal features
|
||||
features_list = []
|
||||
|
||||
# NDVI time series
|
||||
for t in range(n_times):
|
||||
if 'time' in ndvi_monthly.dims:
|
||||
ndvi_t = ndvi_monthly.isel(time=t).values.flatten()
|
||||
else:
|
||||
ndvi_t = ndvi_monthly.values.flatten()
|
||||
features_list.append(ndvi_t)
|
||||
|
||||
# NDWI time series
|
||||
for t in range(n_times):
|
||||
if 'time' in ndwi_monthly.dims:
|
||||
ndwi_t = ndwi_monthly.isel(time=t).values.flatten()
|
||||
else:
|
||||
ndwi_t = ndwi_monthly.values.flatten()
|
||||
features_list.append(ndwi_t)
|
||||
|
||||
# NDBI time series
|
||||
for t in range(n_times):
|
||||
if 'time' in ndbi_monthly.dims:
|
||||
ndbi_t = ndbi_monthly.isel(time=t).values.flatten()
|
||||
else:
|
||||
ndbi_t = ndbi_monthly.values.flatten()
|
||||
features_list.append(ndbi_t)
|
||||
|
||||
# Stack all spectral features
|
||||
features = np.column_stack(features_list)
|
||||
|
||||
# Add radar features if available
|
||||
if vh_data is not None and vv_data is not None:
|
||||
if 'time' in vh_data.dims:
|
||||
vh_mean = vh_data.mean(dim='time')
|
||||
vv_mean = vv_data.mean(dim='time')
|
||||
else:
|
||||
vh_mean = vh_data
|
||||
vv_mean = vv_data
|
||||
|
||||
vh_flat = vh_mean.values.flatten()
|
||||
vv_flat = vv_mean.values.flatten()
|
||||
vh_vv_ratio = vh_flat / (vv_flat + 1e-8)
|
||||
|
||||
# Add radar features
|
||||
features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])
|
||||
|
||||
return features
|
||||
|
||||
def extract_odc_features(
|
||||
self,
|
||||
s2_data: xr.Dataset,
|
||||
vh_data: Optional[xr.DataArray] = None,
|
||||
vv_data: Optional[xr.DataArray] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Extract ODC aggregate features (8 features matching 01.train_ODC.ipynb):
|
||||
ndvi_mean, ndvi_min, ndvi_max, ndvi_std, ndvi_range, ndwi_mean, ndbi_mean, evi_mean
|
||||
|
||||
Args:
|
||||
s2_data: Sentinel-2 Dataset with B02, B03, B04, B08, B11
|
||||
vh_data: Not used in ODC mode
|
||||
vv_data: Not used in ODC mode
|
||||
|
||||
Returns:
|
||||
Feature array shape (n_pixels, 8)
|
||||
"""
|
||||
# Calculate spectral indices
|
||||
nir = s2_data["B08"].astype('float32')
|
||||
red = s2_data["B04"].astype('float32')
|
||||
green = s2_data["B03"].astype('float32')
|
||||
blue = s2_data["B02"].astype('float32')
|
||||
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"]
|
||||
|
||||
# NDVI = (NIR - Red) / (NIR + Red)
|
||||
ndvi = (nir - red) / (nir + red + 1e-8)
|
||||
|
||||
# NDWI = (Green - NIR) / (Green + NIR)
|
||||
ndwi = (green - nir) / (green + nir + 1e-8)
|
||||
|
||||
# NDBI = (SWIR - NIR) / (SWIR + NIR)
|
||||
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
||||
|
||||
# EVI = 2.5 * (NIR - Red) / (NIR + 6*Red - 7.5*Blue + 1)
|
||||
evi = 2.5 * (nir - red) / (nir + 6*red - 7.5*blue + 1)
|
||||
|
||||
features_list = []
|
||||
|
||||
# NDVI statistics (5 features)
|
||||
if 'time' in ndvi.dims:
|
||||
features_list.append(ndvi.mean(dim='time').values.flatten()) # ndvi_mean
|
||||
features_list.append(ndvi.min(dim='time').values.flatten()) # ndvi_min
|
||||
features_list.append(ndvi.max(dim='time').values.flatten()) # ndvi_max
|
||||
features_list.append(ndvi.std(dim='time').values.flatten()) # ndvi_std
|
||||
ndvi_range = (ndvi.max(dim='time') - ndvi.min(dim='time')).values.flatten()
|
||||
features_list.append(ndvi_range) # ndvi_range
|
||||
else:
|
||||
ndvi_flat = ndvi.values.flatten()
|
||||
features_list.extend([ndvi_flat, ndvi_flat, ndvi_flat, np.zeros_like(ndvi_flat), np.zeros_like(ndvi_flat)])
|
||||
|
||||
# NDWI mean (1 feature)
|
||||
if 'time' in ndwi.dims:
|
||||
features_list.append(ndwi.mean(dim='time').values.flatten()) # ndwi_mean
|
||||
else:
|
||||
features_list.append(ndwi.values.flatten())
|
||||
|
||||
# NDBI mean (1 feature)
|
||||
if 'time' in ndbi.dims:
|
||||
features_list.append(ndbi.mean(dim='time').values.flatten()) # ndbi_mean
|
||||
else:
|
||||
features_list.append(ndbi.values.flatten())
|
||||
|
||||
# EVI mean (1 feature)
|
||||
if 'time' in evi.dims:
|
||||
features_list.append(evi.mean(dim='time').values.flatten()) # evi_mean
|
||||
else:
|
||||
features_list.append(evi.values.flatten())
|
||||
|
||||
# Stack all features (total: 8 features)
|
||||
features = np.column_stack(features_list)
|
||||
|
||||
return features
|
||||
|
||||
def extract_extended_features(
|
||||
self,
|
||||
s2_data: xr.Dataset,
|
||||
vh_data: Optional[xr.DataArray] = None,
|
||||
vv_data: Optional[xr.DataArray] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Extract extended aggregate features (15 features: stats của NDVI, NDWI, NDBI + radar)
|
||||
|
||||
Args:
|
||||
s2_data: Sentinel-2 Dataset
|
||||
vh_data: VH radar DataArray
|
||||
vv_data: VV radar DataArray
|
||||
|
||||
Returns:
|
||||
Feature array shape (n_pixels, 15)
|
||||
"""
|
||||
# Calculate spectral indices
|
||||
nir = s2_data["B08"].astype('float32')
|
||||
red = s2_data["B04"].astype('float32')
|
||||
green = s2_data["B03"].astype('float32')
|
||||
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"]
|
||||
|
||||
ndvi = (nir - red) / (nir + red + 1e-8)
|
||||
ndwi = (green - nir) / (green + nir + 1e-8)
|
||||
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
||||
|
||||
features_list = []
|
||||
|
||||
# NDVI statistics
|
||||
if 'time' in ndvi.dims:
|
||||
features_list.append(ndvi.mean(dim='time').values.flatten())
|
||||
features_list.append(ndvi.std(dim='time').values.flatten())
|
||||
features_list.append(ndvi.min(dim='time').values.flatten())
|
||||
features_list.append(ndvi.max(dim='time').values.flatten())
|
||||
else:
|
||||
ndvi_flat = ndvi.values.flatten()
|
||||
features_list.extend([ndvi_flat, np.zeros_like(ndvi_flat), ndvi_flat, ndvi_flat])
|
||||
|
||||
# NDWI statistics
|
||||
if 'time' in ndwi.dims:
|
||||
features_list.append(ndwi.mean(dim='time').values.flatten())
|
||||
features_list.append(ndwi.std(dim='time').values.flatten())
|
||||
features_list.append(ndwi.min(dim='time').values.flatten())
|
||||
features_list.append(ndwi.max(dim='time').values.flatten())
|
||||
else:
|
||||
ndwi_flat = ndwi.values.flatten()
|
||||
features_list.extend([ndwi_flat, np.zeros_like(ndwi_flat), ndwi_flat, ndwi_flat])
|
||||
|
||||
# NDBI statistics
|
||||
if 'time' in ndbi.dims:
|
||||
features_list.append(ndbi.mean(dim='time').values.flatten())
|
||||
features_list.append(ndbi.std(dim='time').values.flatten())
|
||||
features_list.append(ndbi.min(dim='time').values.flatten())
|
||||
features_list.append(ndbi.max(dim='time').values.flatten())
|
||||
else:
|
||||
ndbi_flat = ndbi.values.flatten()
|
||||
features_list.extend([ndbi_flat, np.zeros_like(ndbi_flat), ndbi_flat, ndbi_flat])
|
||||
|
||||
# Stack spectral features
|
||||
features = np.column_stack(features_list)
|
||||
|
||||
# Add radar features
|
||||
if vh_data is not None and vv_data is not None:
|
||||
if 'time' in vh_data.dims:
|
||||
vh_mean = vh_data.mean(dim='time')
|
||||
vv_mean = vv_data.mean(dim='time')
|
||||
else:
|
||||
vh_mean = vh_data
|
||||
vv_mean = vv_data
|
||||
|
||||
vh_flat = vh_mean.values.flatten()
|
||||
vv_flat = vv_mean.values.flatten()
|
||||
vh_vv_ratio = vh_flat / (vv_flat + 1e-8)
|
||||
|
||||
features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])
|
||||
|
||||
return features
|
||||
|
||||
def extract(
|
||||
self,
|
||||
s2_data: Optional[xr.Dataset] = None,
|
||||
ndvi_data: Optional[xr.DataArray] = None,
|
||||
vh_data: Optional[xr.DataArray] = None,
|
||||
vv_data: Optional[xr.DataArray] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Extract features theo mode đã chọn
|
||||
|
||||
Args:
|
||||
s2_data: Sentinel-2 Dataset (cần cho temporal, extended, và odc modes)
|
||||
ndvi_data: NDVI DataArray (cần cho simple mode)
|
||||
vh_data: VH radar DataArray
|
||||
vv_data: VV radar DataArray
|
||||
|
||||
Returns:
|
||||
Feature array
|
||||
"""
|
||||
if self.mode == 'simple':
|
||||
if ndvi_data is None:
|
||||
raise ValueError("ndvi_data required for simple mode")
|
||||
return self.extract_simple_features(ndvi_data, vh_data, vv_data)
|
||||
|
||||
elif self.mode == 'temporal':
|
||||
if s2_data is None:
|
||||
raise ValueError("s2_data required for temporal mode")
|
||||
return self.extract_temporal_features(s2_data, vh_data, vv_data)
|
||||
|
||||
elif self.mode == 'extended':
|
||||
if s2_data is None:
|
||||
raise ValueError("s2_data required for extended mode")
|
||||
return self.extract_extended_features(s2_data, vh_data, vv_data)
|
||||
|
||||
elif self.mode == 'odc':
|
||||
if s2_data is None:
|
||||
raise ValueError("s2_data required for odc mode")
|
||||
return self.extract_odc_features(s2_data, vh_data, vv_data)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {self.mode}")
|
||||
|
||||
def get_info(self) -> Dict:
|
||||
"""Lấy thông tin về feature extraction mode"""
|
||||
return {
|
||||
'mode': self.mode,
|
||||
'n_features': self.config['n_features'],
|
||||
'description': self.config['description']
|
||||
}
|
||||
|
||||
|
||||
def get_feature_extractor(mode: str = 'simple') -> FeatureExtractor:
|
||||
"""
|
||||
Factory function để tạo FeatureExtractor
|
||||
|
||||
Args:
|
||||
mode: 'simple', 'temporal', 'extended', hoặc 'odc'
|
||||
|
||||
Returns:
|
||||
FeatureExtractor instance
|
||||
"""
|
||||
return FeatureExtractor(mode=mode)
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate PNG previews for existing GeoTIFF prediction files
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import rasterio
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
def generate_png_preview(tif_file, output_png=None):
|
||||
"""Generate PNG preview from GeoTIFF file"""
|
||||
tif_path = Path(tif_file)
|
||||
|
||||
if not tif_path.exists():
|
||||
print(f"❌ File not found: {tif_file}")
|
||||
return False
|
||||
|
||||
# Determine output PNG path
|
||||
if output_png is None:
|
||||
output_png = tif_path.with_suffix('.png')
|
||||
else:
|
||||
output_png = Path(output_png)
|
||||
|
||||
try:
|
||||
# Read GeoTIFF
|
||||
with rasterio.open(tif_path) as src:
|
||||
data = src.read(1)
|
||||
|
||||
print(f"📊 Data shape: {data.shape}, range: [{np.nanmin(data):.3f}, {np.nanmax(data):.3f}]")
|
||||
|
||||
# Determine if it's classification or NDVI based on filename
|
||||
is_classification = 'classification' in tif_path.name.lower() or 'prediction' in tif_path.name.lower()
|
||||
is_ndvi = 'ndvi' in tif_path.name.lower()
|
||||
|
||||
# Create figure
|
||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
||||
|
||||
if is_ndvi:
|
||||
# NDVI: use RdYlGn colormap, range -1 to 1
|
||||
im = ax.imshow(data, cmap='RdYlGn', vmin=-1, vmax=1, interpolation='nearest')
|
||||
ax.set_title(f'NDVI - {tif_path.stem}', fontsize=14, fontweight='bold')
|
||||
cbar_label = 'NDVI'
|
||||
elif is_classification:
|
||||
# Classification: use tab20 colormap
|
||||
im = ax.imshow(data, cmap='tab20', interpolation='nearest')
|
||||
ax.set_title(f'Land Classification - {tif_path.stem}', fontsize=14, fontweight='bold')
|
||||
cbar_label = 'Class'
|
||||
else:
|
||||
# Generic: use viridis
|
||||
im = ax.imshow(data, cmap='viridis', interpolation='nearest')
|
||||
ax.set_title(f'{tif_path.stem}', fontsize=14, fontweight='bold')
|
||||
cbar_label = 'Value'
|
||||
|
||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
||||
|
||||
# Add colorbar
|
||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
||||
cbar.set_label(cbar_label, rotation=270, labelpad=15)
|
||||
|
||||
# For classification, try to set integer ticks
|
||||
if is_classification:
|
||||
try:
|
||||
unique_vals = np.unique(data[~np.isnan(data)])
|
||||
if len(unique_vals) < 20: # Only if not too many classes
|
||||
cbar.set_ticks(unique_vals)
|
||||
cbar.set_ticklabels([str(int(v)) for v in unique_vals])
|
||||
except:
|
||||
pass
|
||||
|
||||
# Add grid
|
||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||
|
||||
# Save PNG
|
||||
plt.tight_layout()
|
||||
plt.savefig(str(output_png), dpi=150, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f"✅ Created PNG: {output_png}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating PNG: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def generate_all_previews(predictions_dir="predictions"):
|
||||
"""Generate PNG previews for all GeoTIFF files without PNGs"""
|
||||
pred_path = Path(predictions_dir)
|
||||
|
||||
if not pred_path.exists():
|
||||
print(f"❌ Directory not found: {predictions_dir}")
|
||||
return
|
||||
|
||||
tif_files = list(pred_path.glob("*.tif"))
|
||||
print(f"🔍 Found {len(tif_files)} GeoTIFF files")
|
||||
|
||||
generated = 0
|
||||
skipped = 0
|
||||
|
||||
for tif_file in tif_files:
|
||||
png_file = tif_file.with_suffix('.png')
|
||||
|
||||
if png_file.exists():
|
||||
print(f"⏭️ Skipping {tif_file.name} (PNG already exists)")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
print(f"\n🎨 Processing {tif_file.name}...")
|
||||
if generate_png_preview(tif_file):
|
||||
generated += 1
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"✅ Generated {generated} new PNG previews")
|
||||
print(f"⏭️ Skipped {skipped} files (already have PNGs)")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
# Process specific file
|
||||
tif_file = sys.argv[1]
|
||||
generate_png_preview(tif_file)
|
||||
else:
|
||||
# Process all files in predictions directory
|
||||
generate_all_previews()
|
||||
-1002
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
Inspect model_odc.joblib to see what it actually contains
|
||||
"""
|
||||
|
||||
import joblib
|
||||
from pathlib import Path
|
||||
|
||||
model_path = Path("model_train/model_odc.joblib")
|
||||
|
||||
if model_path.exists():
|
||||
print("Loading model_odc.joblib...")
|
||||
model_data = joblib.load(model_path)
|
||||
|
||||
print(f"\nModel type: {type(model_data)}")
|
||||
print(f"Model class: {model_data.__class__.__name__}")
|
||||
|
||||
# Check if it's a dict
|
||||
if isinstance(model_data, dict):
|
||||
print(f"\nModel is a dict with keys: {model_data.keys()}")
|
||||
model = model_data.get('model')
|
||||
else:
|
||||
model = model_data
|
||||
|
||||
print(f"\nActual model type: {type(model)}")
|
||||
print(f"Actual model class: {model.__class__.__name__}")
|
||||
|
||||
# Try to get feature info
|
||||
if hasattr(model, 'n_features_in_'):
|
||||
print(f"\nn_features_in_: {model.n_features_in_}")
|
||||
|
||||
if hasattr(model, 'feature_names_in_'):
|
||||
print(f"feature_names_in_: {model.feature_names_in_}")
|
||||
|
||||
# If it's a GridSearchCV
|
||||
if hasattr(model, 'best_estimator_'):
|
||||
print(f"\nThis is a GridSearchCV!")
|
||||
print(f"Best estimator: {model.best_estimator_}")
|
||||
|
||||
best_est = model.best_estimator_
|
||||
if hasattr(best_est, 'steps'):
|
||||
print(f"\nPipeline steps:")
|
||||
for step_name, step in best_est.steps:
|
||||
print(f" - {step_name}: {step.__class__.__name__}")
|
||||
if hasattr(step, 'n_features_in_'):
|
||||
print(f" n_features_in_: {step.n_features_in_}")
|
||||
|
||||
# If it's a Pipeline
|
||||
if hasattr(model, 'steps'):
|
||||
print(f"\nThis is a Pipeline!")
|
||||
print(f"Pipeline steps:")
|
||||
for step_name, step in model.steps:
|
||||
print(f" - {step_name}: {step.__class__.__name__}")
|
||||
if hasattr(step, 'n_features_in_'):
|
||||
print(f" n_features_in_: {step.n_features_in_}")
|
||||
|
||||
# Try to get booster for XGBoost
|
||||
try:
|
||||
if hasattr(model, 'get_booster'):
|
||||
print(f"\nXGBoost num_features: {model.get_booster().num_features()}")
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
print(f"Model file not found: {model_path}")
|
||||
@@ -1,361 +0,0 @@
|
||||
"""
|
||||
Model Manager - Hệ thống quản lý và vận hành tất cả các loại models
|
||||
Hỗ trợ: XGBoost, Random Forest, Decision Tree, SVM, CNN, và các model khác
|
||||
"""
|
||||
|
||||
import joblib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Any, Tuple
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
import warnings
|
||||
|
||||
# PyTorch for CNN models
|
||||
try:
|
||||
import torch
|
||||
PYTORCH_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYTORCH_AVAILABLE = False
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""Quản lý tất cả các models: load, save, list, validate"""
|
||||
|
||||
def __init__(self, models_dir: str = "model_train"):
|
||||
self.models_dir = Path(models_dir)
|
||||
self.models_dir.mkdir(exist_ok=True)
|
||||
self.current_model = None
|
||||
self.current_metadata = None
|
||||
|
||||
def list_models(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Liệt kê tất cả models có sẵn với metadata
|
||||
|
||||
Returns:
|
||||
List of dicts containing model info
|
||||
"""
|
||||
models = []
|
||||
|
||||
# Tìm tất cả file .joblib
|
||||
for model_file in self.models_dir.glob("*.joblib"):
|
||||
# Skip Zone.Identifier files
|
||||
if "Zone.Identifier" in model_file.name:
|
||||
continue
|
||||
|
||||
model_info = {
|
||||
"filename": model_file.name,
|
||||
"path": str(model_file),
|
||||
"size_mb": model_file.stat().st_size / (1024 * 1024),
|
||||
"modified": datetime.fromtimestamp(model_file.stat().st_mtime).isoformat(),
|
||||
}
|
||||
|
||||
# Tìm metadata file tương ứng
|
||||
metadata_file = model_file.with_suffix('.json')
|
||||
if not metadata_file.exists():
|
||||
# Try with _info.json suffix
|
||||
metadata_file = model_file.parent / (model_file.stem + "_info.json")
|
||||
|
||||
if metadata_file.exists():
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
model_info["metadata"] = metadata
|
||||
model_info["has_metadata"] = True
|
||||
|
||||
# Extract key info
|
||||
model_info["model_type"] = metadata.get("model_type", "unknown")
|
||||
model_info["features"] = metadata.get("features", [])
|
||||
model_info["n_features"] = metadata.get("n_features", 0)
|
||||
model_info["n_classes"] = metadata.get("n_classes", 0)
|
||||
model_info["test_accuracy"] = metadata.get("test_accuracy", None)
|
||||
model_info["timestamp"] = metadata.get("timestamp", None)
|
||||
model_info["data_source"] = metadata.get("data_source", "unknown")
|
||||
|
||||
except Exception as e:
|
||||
model_info["has_metadata"] = False
|
||||
model_info["metadata_error"] = str(e)
|
||||
else:
|
||||
model_info["has_metadata"] = False
|
||||
|
||||
models.append(model_info)
|
||||
|
||||
# Sort by modified time (newest first)
|
||||
models.sort(key=lambda x: x["modified"], reverse=True)
|
||||
|
||||
return models
|
||||
|
||||
def load_model(self, model_filename: str) -> Tuple[Any, Optional[Any], Dict[str, Any]]:
|
||||
"""
|
||||
Load model từ file
|
||||
|
||||
Args:
|
||||
model_filename: Tên file model (ví dụ: "model_odc.joblib")
|
||||
|
||||
Returns:
|
||||
Tuple of (model, label_encoder, metadata)
|
||||
"""
|
||||
model_path = self.models_dir / model_filename
|
||||
|
||||
if not model_path.exists():
|
||||
raise FileNotFoundError(f"Model không tồn tại: {model_filename}")
|
||||
|
||||
# Load model
|
||||
print(f"[MODEL MANAGER] Loading model: {model_filename}")
|
||||
model_data = joblib.load(model_path)
|
||||
|
||||
# Extract model and encoder
|
||||
if isinstance(model_data, dict):
|
||||
model = model_data.get('model')
|
||||
label_encoder = model_data.get('label_encoder')
|
||||
else:
|
||||
# Old format: model only
|
||||
model = model_data
|
||||
label_encoder = None
|
||||
|
||||
# Load metadata
|
||||
metadata = self._load_metadata(model_filename)
|
||||
|
||||
# Store current model
|
||||
self.current_model = model
|
||||
self.current_metadata = metadata
|
||||
|
||||
# Check if CNN model and set to eval mode
|
||||
if PYTORCH_AVAILABLE and hasattr(model, '__class__') and 'CNN' in model.__class__.__name__:
|
||||
model.eval()
|
||||
print(f"[MODEL MANAGER] PyTorch CNN model detected and set to eval mode")
|
||||
|
||||
print(f"[MODEL MANAGER] Model loaded successfully")
|
||||
print(f" - Type: {metadata.get('model_type', 'unknown')}")
|
||||
print(f" - Features: {metadata.get('n_features', 'N/A')}")
|
||||
print(f" - Classes: {metadata.get('n_classes', 'N/A')}")
|
||||
print(f" - Accuracy: {metadata.get('test_accuracy', 'N/A')}")
|
||||
|
||||
return model, label_encoder, metadata
|
||||
|
||||
def _load_metadata(self, model_filename: str) -> Dict[str, Any]:
|
||||
"""Load metadata cho model"""
|
||||
model_path = self.models_dir / model_filename
|
||||
|
||||
# Try multiple metadata file patterns
|
||||
metadata_files = [
|
||||
model_path.with_suffix('.json'),
|
||||
model_path.parent / (model_path.stem + "_info.json"),
|
||||
]
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
if metadata_file.exists():
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[MODEL MANAGER] Warning: Could not load metadata from {metadata_file}: {e}")
|
||||
|
||||
# Return default metadata if not found
|
||||
print(f"[MODEL MANAGER] Warning: No metadata found for {model_filename}")
|
||||
return {
|
||||
"model_type": "unknown",
|
||||
"features": [],
|
||||
"n_features": 0,
|
||||
"n_classes": 0,
|
||||
"timestamp": None
|
||||
}
|
||||
|
||||
def save_model(self, model: Any, metadata: Dict[str, Any],
|
||||
model_filename: Optional[str] = None,
|
||||
label_encoder: Optional[Any] = None) -> str:
|
||||
"""
|
||||
Save model với metadata
|
||||
|
||||
Args:
|
||||
model: Model object
|
||||
metadata: Dict chứa thông tin về model
|
||||
model_filename: Tên file (optional, sẽ auto-generate nếu không có)
|
||||
label_encoder: Label encoder (optional)
|
||||
|
||||
Returns:
|
||||
Path to saved model file
|
||||
"""
|
||||
# Generate filename if not provided
|
||||
if model_filename is None:
|
||||
model_type = metadata.get("model_type", "model")
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
model_filename = f"model_{model_type}_{timestamp}.joblib"
|
||||
|
||||
model_path = self.models_dir / model_filename
|
||||
metadata_path = model_path.parent / (model_path.stem + "_info.json")
|
||||
|
||||
# Prepare model data
|
||||
if label_encoder is not None:
|
||||
model_data = {
|
||||
'model': model,
|
||||
'label_encoder': label_encoder
|
||||
}
|
||||
else:
|
||||
model_data = {
|
||||
'model': model
|
||||
}
|
||||
|
||||
# Save model
|
||||
print(f"[MODEL MANAGER] Saving model to: {model_path}")
|
||||
joblib.dump(model_data, model_path)
|
||||
|
||||
# Save metadata
|
||||
print(f"[MODEL MANAGER] Saving metadata to: {metadata_path}")
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
print(f"[MODEL MANAGER] Model saved successfully!")
|
||||
|
||||
return str(model_path)
|
||||
|
||||
def validate_model(self, model_filename: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate model file và kiểm tra integrity
|
||||
|
||||
Returns:
|
||||
Dict with validation results
|
||||
"""
|
||||
result = {
|
||||
"valid": False,
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
}
|
||||
|
||||
model_path = self.models_dir / model_filename
|
||||
|
||||
# Check file exists
|
||||
if not model_path.exists():
|
||||
result["errors"].append(f"File không tồn tại: {model_filename}")
|
||||
return result
|
||||
|
||||
# Try to load model
|
||||
try:
|
||||
model, encoder, metadata = self.load_model(model_filename)
|
||||
result["valid"] = True
|
||||
|
||||
# Check metadata
|
||||
if not metadata or metadata.get("model_type") == "unknown":
|
||||
result["warnings"].append("Không có metadata hoặc metadata không đầy đủ")
|
||||
|
||||
# Check required features
|
||||
if not metadata.get("features"):
|
||||
result["warnings"].append("Danh sách features không có trong metadata")
|
||||
|
||||
# Check model object
|
||||
if model is None:
|
||||
result["errors"].append("Model object is None")
|
||||
result["valid"] = False
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Lỗi khi load model: {str(e)}")
|
||||
result["valid"] = False
|
||||
|
||||
return result
|
||||
|
||||
def get_required_features(self, model_filename: str) -> List[str]:
|
||||
"""
|
||||
Lấy danh sách features cần thiết cho model
|
||||
|
||||
Returns:
|
||||
List of feature names
|
||||
"""
|
||||
metadata = self._load_metadata(model_filename)
|
||||
return metadata.get("features", [])
|
||||
|
||||
def predict(self, model_filename: str, X: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Predict using specified model
|
||||
|
||||
Args:
|
||||
model_filename: Model file name
|
||||
X: Features array (n_samples, n_features)
|
||||
|
||||
Returns:
|
||||
Predictions array
|
||||
"""
|
||||
if self.current_model is None or model_filename != getattr(self, '_current_model_filename', None):
|
||||
model, encoder, metadata = self.load_model(model_filename)
|
||||
self._current_model_filename = model_filename
|
||||
else:
|
||||
model = self.current_model
|
||||
metadata = self.current_metadata
|
||||
|
||||
# Validate input features
|
||||
expected_features = metadata.get("n_features", 0)
|
||||
if X.shape[1] != expected_features:
|
||||
raise ValueError(f"Expected {expected_features} features, got {X.shape[1]}")
|
||||
|
||||
# Predict
|
||||
predictions = model.predict(X)
|
||||
|
||||
return predictions
|
||||
|
||||
def get_model_info(self, model_filename: str) -> Dict[str, Any]:
|
||||
"""Get detailed info about a model"""
|
||||
models = self.list_models()
|
||||
for model in models:
|
||||
if model["filename"] == model_filename:
|
||||
return model
|
||||
return None
|
||||
|
||||
def delete_model(self, model_filename: str) -> bool:
|
||||
"""
|
||||
Xóa model và metadata
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
model_path = self.models_dir / model_filename
|
||||
|
||||
if not model_path.exists():
|
||||
return False
|
||||
|
||||
# Delete model file
|
||||
model_path.unlink()
|
||||
|
||||
# Delete metadata file if exists
|
||||
metadata_file = model_path.with_suffix('.json')
|
||||
if metadata_file.exists():
|
||||
metadata_file.unlink()
|
||||
|
||||
# Try alternative metadata file name
|
||||
metadata_file_alt = model_path.parent / (model_path.stem + "_info.json")
|
||||
if metadata_file_alt.exists():
|
||||
metadata_file_alt.unlink()
|
||||
|
||||
return True
|
||||
|
||||
def get_latest_model(self, model_type: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Lấy model mới nhất (theo thời gian modified)
|
||||
|
||||
Args:
|
||||
model_type: Filter by model type (xgboost, cnn, etc.), None for any
|
||||
|
||||
Returns:
|
||||
Model filename or None
|
||||
"""
|
||||
models = self.list_models()
|
||||
|
||||
if model_type:
|
||||
models = [m for m in models if m.get("model_type") == model_type]
|
||||
|
||||
if not models:
|
||||
return None
|
||||
|
||||
# Already sorted by modified time
|
||||
return models[0]["filename"]
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_model_manager = None
|
||||
|
||||
def get_model_manager() -> ModelManager:
|
||||
"""Get singleton ModelManager instance"""
|
||||
global _model_manager
|
||||
if _model_manager is None:
|
||||
_model_manager = ModelManager()
|
||||
return _model_manager
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a446d333bf7f6d0cb7df014f12b0da3f7298f85bdfb4de06893173e90fbd5ccb
|
||||
size 14112695
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae2f82f6c837396729cc63efa41ee3048d9a7de3197e28318dc846be830239b9
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:04:27.406540",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.515686274509804,
|
||||
"test_accuracy": 0.5,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abc03d62c7f620b88150a6481143026d516fd3a34bd5ab67c734adac4a8900f9
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:17:48.399298",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.4803921568627451,
|
||||
"test_accuracy": 0.484375,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-25",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:21f906aa2a61d2e793a3463df95fe3e364134934af326771efae90d80caca419
|
||||
size 41536
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T18:23:10.713912",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 0.46862745098039216,
|
||||
"test_accuracy": 0.4609375,
|
||||
"model_type": "cnn",
|
||||
"device": "cpu",
|
||||
"n_estimators": 50,
|
||||
"max_depth": null,
|
||||
"learning_rate": null,
|
||||
"cnn_epochs": 25,
|
||||
"n_features": 3,
|
||||
"n_classes": 7,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3eec576dffe1cc1393fdb584fb99d28db62abdd0977d41030aee5c4fa5377180
|
||||
size 11474743
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fe99f96ad3d3ba7aaacc0e742572a8f5b22947a328c74b245e0aa5f2913c757
|
||||
size 1347520
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"timestamp": "2025-12-12T12:57:54.509336",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 1.0,
|
||||
"test_accuracy": 0.578125,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"gpu_device": "RTX 4060",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||
size 1347520
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-12T22:15:25.794614",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 1.0,
|
||||
"test_accuracy": 0.578125,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b9c6cabb59d9cdbba22a438ae935911f1711d7434dcf3728db4e518ec1b90190
|
||||
size 556184
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-12T22:33:42.950629",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 904,
|
||||
"testing_samples": 226,
|
||||
"train_accuracy": 0.19911504424778761,
|
||||
"test_accuracy": 0.19911504424778761,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
104.89032,
|
||||
10.944563,
|
||||
104.972717,
|
||||
11.016689
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||
size 1347520
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T13:40:03.325930",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 510,
|
||||
"testing_samples": 128,
|
||||
"train_accuracy": 1.0,
|
||||
"test_accuracy": 0.578125,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-05-31",
|
||||
"resolution": 20
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a44efc173619782c8add224e9024f33ef0150ab192b5435d009cb119c379f03c
|
||||
size 2088632
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"timestamp": "2025-12-14T16:52:19.862770",
|
||||
"data_source": "Microsoft Planetary Computer STAC",
|
||||
"collections": [
|
||||
"sentinel-2-l2a",
|
||||
"sentinel-1-rtc"
|
||||
],
|
||||
"features": [
|
||||
"NDVI_mean",
|
||||
"VH_dB_mean",
|
||||
"VV_dB_mean"
|
||||
],
|
||||
"training_samples": 859,
|
||||
"testing_samples": 215,
|
||||
"train_accuracy": 0.9976717112922002,
|
||||
"test_accuracy": 0.6837209302325581,
|
||||
"model_type": "XGBClassifier",
|
||||
"device": "cuda:0",
|
||||
"tree_method": "hist",
|
||||
"n_estimators": 100,
|
||||
"max_depth": 20,
|
||||
"learning_rate": 0.1,
|
||||
"bbox": [
|
||||
105.6,
|
||||
9.3,
|
||||
106.2,
|
||||
9.8
|
||||
],
|
||||
"time_range": "2023-03-01/2023-12-31",
|
||||
"resolution": 20
|
||||
}
|
||||
-1091
File diff suppressed because it is too large
Load Diff
+3
-37
@@ -224,46 +224,12 @@ def train_with_rf(X_train, X_val, y_train, y_val):
|
||||
return grid_search
|
||||
|
||||
|
||||
def save_model(name_file, model, metadata=None, label_encoder=None):
|
||||
"""
|
||||
Save model với metadata để tương thích với ModelManager
|
||||
|
||||
Args:
|
||||
name_file: Tên file model
|
||||
model: Model object
|
||||
metadata: Dict chứa thông tin về model (optional)
|
||||
label_encoder: Label encoder (optional)
|
||||
"""
|
||||
from model_manager import get_model_manager
|
||||
|
||||
def save_model(name_file, grid_search):
|
||||
dir_save_model = "model_train"
|
||||
if not os.path.exists(dir_save_model):
|
||||
os.mkdir(dir_save_model)
|
||||
|
||||
# Nếu có metadata, sử dụng ModelManager
|
||||
if metadata is not None:
|
||||
model_manager = get_model_manager()
|
||||
model_manager.save_model(
|
||||
model=model,
|
||||
metadata=metadata,
|
||||
model_filename=name_file,
|
||||
label_encoder=label_encoder
|
||||
)
|
||||
else:
|
||||
# Legacy mode: save trực tiếp (backward compatibility)
|
||||
model_data = {
|
||||
'model': model,
|
||||
'label_encoder': label_encoder
|
||||
} if label_encoder is not None else model
|
||||
|
||||
joblib.dump(model_data, os.path.join(dir_save_model, name_file))
|
||||
|
||||
print(f"✅ Model saved: {name_file}")
|
||||
if metadata:
|
||||
print(f" - Type: {metadata.get('model_type', 'N/A')}")
|
||||
print(f" - Features: {metadata.get('n_features', 'N/A')}")
|
||||
print(f" - Accuracy: {metadata.get('test_accuracy', 'N/A')}")
|
||||
|
||||
joblib.dump(grid_search, os.path.join(dir_save_model, name_file))
|
||||
print("Done!")
|
||||
|
||||
|
||||
def predict(model, data_crs, ndvi, vh, vv):
|
||||
|
||||
+3
-3728
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||
size 73272920
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||
size 73272920
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||
size 73272920
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user