bổ sung chức năng load 64 tỉnh thành và 32 tỉnh thành/ bổ sung mô hình Swin-Unet

This commit is contained in:
Victor Phan
2026-01-03 22:35:33 +07:00
parent e86709df85
commit 03048d9503
10 changed files with 2722 additions and 62 deletions
+278
View File
@@ -0,0 +1,278 @@
# 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
```
### 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!
+173 -21
View File
@@ -29,6 +29,13 @@ from report_generator import generate_training_report, generate_prediction_repor
# Import Model Manager # Import Model Manager
from model_manager import ModelManager, get_model_manager from model_manager import ModelManager, get_model_manager
# Import Vietnam provinces data
from vietnam_provinces import get_all_provinces, get_provinces_by_region, get_province_bbox, search_province
from vietnam_provinces_merged import (
get_all_provinces_32, get_provinces_by_region_32, get_province_bbox_32,
search_province_32, get_merged_info, get_provinces_statistics
)
# Import planetary computer libraries (conditional) # Import planetary computer libraries (conditional)
try: try:
from pystac_client import Client from pystac_client import Client
@@ -95,7 +102,7 @@ class TrainingConfig(BaseModel):
resolution: int = 20 # 10m hoặc 20m resolution: int = 20 # 10m hoặc 20m
# Model parameters # Model parameters
model_type: str = "xgboost" # xgboost, random_forest, decision_tree, svm, cnn model_type: str = "xgboost" # xgboost, random_forest, decision_tree, svm, cnn, swin-unet
n_estimators: int = 100 n_estimators: int = 100
max_depth: int = 20 max_depth: int = 20
learning_rate: float = 0.1 learning_rate: float = 0.1
@@ -131,6 +138,9 @@ class PredictionConfig(BaseModel):
cloud_cover: int = 30 cloud_cover: int = 30
resolution: int = 20 resolution: int = 20
# GPU support for deep learning models
use_gpu: bool = True
class TrainingStatus(BaseModel): class TrainingStatus(BaseModel):
"""Trạng thái training""" """Trạng thái training"""
@@ -185,6 +195,7 @@ class PredictionWithNDVIConfig(BaseModel):
max_scenes: int = 12 max_scenes: int = 12
cloud_cover: int = 30 cloud_cover: int = 30
resolution: int = 20 resolution: int = 20
use_gpu: bool = False # Use GPU for deep learning models
export_ndvi: bool = True # Export NDVI raster export_ndvi: bool = True # Export NDVI raster
export_classification: bool = True # Export classification raster export_classification: bool = True # Export classification raster
@@ -394,6 +405,104 @@ async def get_presets():
} }
@app.get("/api/provinces/list")
async def list_provinces():
"""Lấy danh sách tất cả các tỉnh thành Việt Nam"""
return {
"provinces": get_all_provinces(),
"count": len(get_all_provinces())
}
@app.get("/api/provinces/by-region")
async def list_provinces_by_region():
"""Lấy danh sách tỉnh thành theo vùng miền"""
return get_provinces_by_region()
@app.get("/api/provinces/{province_name}/bbox")
async def get_province_bbox_api(province_name: str):
"""Lấy bbox của một tỉnh thành"""
bbox = get_province_bbox(province_name)
if bbox is None:
raise HTTPException(status_code=404, detail=f"Không tìm thấy tỉnh: {province_name}")
return {
"province": province_name,
"bbox": bbox,
"min_lon": bbox[0],
"min_lat": bbox[1],
"max_lon": bbox[2],
"max_lat": bbox[3]
}
@app.get("/api/provinces/search/{query}")
async def search_provinces(query: str):
"""Tìm kiếm tỉnh thành theo tên"""
results = search_province(query)
return {
"query": query,
"results": results,
"count": len(results)
}
@app.get("/api/provinces-32/list")
async def list_provinces_32():
"""Lấy danh sách 32 tỉnh thành sau sáp nhập"""
return {
"provinces": get_all_provinces_32(),
"count": len(get_all_provinces_32()),
"note": "32 tỉnh thành sau sáp nhập theo Nghị quyết 1211/2023"
}
@app.get("/api/provinces-32/by-region")
async def list_provinces_by_region_32():
"""Lấy danh sách 32 tỉnh thành theo vùng miền"""
return get_provinces_by_region_32()
@app.get("/api/provinces-32/{province_name}/bbox")
async def get_province_bbox_api_32(province_name: str):
"""Lấy bbox của một tỉnh thành (32 tỉnh)"""
bbox = get_province_bbox_32(province_name)
if bbox is None:
raise HTTPException(status_code=404, detail=f"Không tìm thấy tỉnh: {province_name}")
# Get merged info
info = get_merged_info(province_name)
return {
"province": province_name,
"bbox": bbox,
"min_lon": bbox[0],
"min_lat": bbox[1],
"max_lon": bbox[2],
"max_lat": bbox[3],
"merged_from": info.get("merged_from"),
"area_km2": info.get("area_km2"),
"region": info.get("region")
}
@app.get("/api/provinces-32/search/{query}")
async def search_provinces_32(query: str):
"""Tìm kiếm tỉnh thành theo tên (32 tỉnh)"""
results = search_province_32(query)
return {
"query": query,
"results": results,
"count": len(results)
}
@app.get("/api/provinces-32/statistics")
async def get_provinces_stats():
"""Thống kê các tỉnh đã sáp nhập"""
return get_provinces_statistics()
@app.get("/api/training/status", response_model=TrainingStatus) @app.get("/api/training/status", response_model=TrainingStatus)
async def get_training_status(): async def get_training_status():
"""Kiểm tra trạng thái training""" """Kiểm tra trạng thái training"""
@@ -867,14 +976,17 @@ async def run_prediction(config: PredictionConfig):
# Initialize FeatureExtractor với đúng mode như lúc training # Initialize FeatureExtractor với đúng mode như lúc training
extractor = get_feature_extractor(mode=feature_mode) extractor = get_feature_extractor(mode=feature_mode)
# Check if it's a CNN model (PyTorch) # Check if it's a PyTorch model (CNN, Swin-UNet, etc.)
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__ is_pytorch_model = hasattr(model, '__class__') and any(
if is_cnn_model: name in model.__class__.__name__ for name in ['CNN', 'SwinUNet']
prediction_status["progress"] = "Phát hiện PyTorch CNN model..." )
if is_pytorch_model:
model_class_name = model.__class__.__name__
prediction_status["progress"] = f"Phát hiện PyTorch {model_class_name} model..."
try: try:
import torch import torch
except ImportError: except ImportError:
raise ImportError("PyTorch required for CNN models. Install: pip install torch") raise ImportError(f"PyTorch required for {model_class_name} models. Install: pip install torch")
# Initialize common variables # Initialize common variables
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat] bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
@@ -1005,11 +1117,8 @@ async def run_prediction(config: PredictionConfig):
# ============ PREDICT ============ # ============ PREDICT ============
prediction_status["progress"] = "Đang dự đoán..." prediction_status["progress"] = "Đang dự đoán..."
# Make prediction # Make prediction (all PyTorch models have the same predict interface)
if is_cnn_model: predictions = model.predict(features)
predictions = model.predict(features)
else:
predictions = model.predict(features)
# Decode labels if label_encoder exists # Decode labels if label_encoder exists
if label_encoder is not None: if label_encoder is not None:
@@ -1654,8 +1763,10 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
model_manager = get_model_manager() model_manager = get_model_manager()
model, label_encoder, model_metadata = model_manager.load_model(config.model_filename) model, label_encoder, model_metadata = model_manager.load_model(config.model_filename)
# Check if it's a CNN model # Check if it's a PyTorch model (CNN, Swin-UNet, etc.)
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__ is_pytorch_model = hasattr(model, '__class__') and any(
name in model.__class__.__name__ for name in ['CNN', 'SwinUNet']
)
job["progress"] = 20 job["progress"] = 20
@@ -1736,7 +1847,7 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
# Adjust features to match model expectations # Adjust features to match model expectations
try: try:
if is_cnn_model: if is_pytorch_model:
expected_features = model.n_features expected_features = model.n_features
elif hasattr(model, 'n_features_in_'): elif hasattr(model, 'n_features_in_'):
expected_features = model.n_features_in_ expected_features = model.n_features_in_
@@ -1755,11 +1866,8 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
except: except:
pass pass
# Predict # Predict (all models have same predict interface)
if is_cnn_model: predictions = model.predict(features)
predictions = model.predict(features)
else:
predictions = model.predict(features)
# Decode labels # Decode labels
if label_encoder is not None: if label_encoder is not None:
@@ -2748,8 +2856,52 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
print(f"[PREDICT+NDVI] Predicting {features_clean.shape[0]} valid pixels...") print(f"[PREDICT+NDVI] Predicting {features_clean.shape[0]} valid pixels...")
# Predict # Check if model is PyTorch/deep learning model and use GPU if available
predictions = model.predict(features_clean) is_pytorch_model = hasattr(model, '__class__') and ('CNN' in model.__class__.__name__ or 'Swin' in model.__class__.__name__ or 'UNet' in model.__class__.__name__)
if is_pytorch_model and config.use_gpu:
try:
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if torch.cuda.is_available():
print(f"[PREDICT+NDVI] Using GPU: {torch.cuda.get_device_name(0)}")
# Move model to GPU
model = model.to(device)
# Predict in batches to avoid GPU memory overflow
batch_size = 8192 # Adjust based on GPU memory
predictions_list = []
for i in range(0, len(features_clean), batch_size):
batch = features_clean[i:i+batch_size]
batch_tensor = torch.from_numpy(batch).float().to(device)
with torch.no_grad():
batch_pred = model.predict(batch_tensor)
# Move back to CPU if needed
if isinstance(batch_pred, torch.Tensor):
batch_pred = batch_pred.cpu().numpy()
predictions_list.append(batch_pred)
if (i // batch_size) % 10 == 0:
print(f"[PREDICT+NDVI] Processed {i + len(batch)}/{len(features_clean)} pixels on GPU")
predictions = np.concatenate(predictions_list)
print(f"[PREDICT+NDVI] GPU prediction completed!")
else:
print(f"[PREDICT+NDVI] GPU requested but not available, using CPU")
predictions = model.predict(features_clean)
except Exception as gpu_error:
print(f"[PREDICT+NDVI] GPU prediction failed: {gpu_error}, falling back to CPU")
predictions = model.predict(features_clean)
else:
# Use CPU for traditional ML models
if is_pytorch_model and not config.use_gpu:
print(f"[PREDICT+NDVI] GPU disabled by user, using CPU")
predictions = model.predict(features_clean)
# Reshape back to raster # Reshape back to raster
prediction_raster = np.full(n_pixels, -1, dtype=np.int16) prediction_raster = np.full(n_pixels, -1, dtype=np.int16)
+264 -13
View File
@@ -313,12 +313,43 @@
<!-- Prediction Content --> <!-- Prediction Content -->
<div id="predictionContent" class="content"> <div id="predictionContent" class="content">
<!-- Province Selection Section -->
<div class="section" style="grid-column: 1 / -1;">
<h2>🗺️ Chọn Khu Vực Prediction</h2>
<div class="form-group" style="margin-bottom: 20px;">
<label>
<strong>🗺️ Chọn theo Tỉnh Thành:</strong>
<span style="color: #999; font-size: 13px; font-weight: normal;">(Hoặc vẽ bbox thủ công bên dưới)</span>
</label>
<!-- Toggle between 63 and 32 provinces -->
<div style="margin-bottom: 10px; display: flex; gap: 10px; align-items: center;">
<button type="button" id="btnPred63Provinces" onclick="switchPredProvinceList('63')" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">63 Tỉnh (Cũ)</button>
<button type="button" id="btnPred32Provinces" onclick="switchPredProvinceList('32')" style="padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">32 Tỉnh (Sau sáp nhập)</button>
<span id="predProvinceListMode" style="color: #667eea; font-weight: bold;">Danh sách: 63 tỉnh</span>
</div>
<select id="predProvinceSelect" style="padding: 12px; width: 100%; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; cursor: pointer;">
<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>
</select>
</div>
<!-- Region Filter -->
<div class="form-group" style="margin-bottom: 20px;">
<label><strong>🌍 Lọc theo Vùng:</strong></label>
<div id="predRegionFilterContainer" style="display: flex; gap: 10px; flex-wrap: wrap;">
<button type="button" class="pred-region-filter-btn" data-region="all" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">Tất cả</button>
<!-- Dynamic region buttons will be added here -->
</div>
</div>
</div>
<!-- Map Section --> <!-- Map Section -->
<div class="map-container"> <div class="map-container">
<div class="map-instructions"> <div class="map-instructions">
<h3>📍 Chọn khu vực để predict</h3> <h3>💡 Hướng dẫn:</h3>
<p>✏️ Click vào nút hình vuông bên phải để vẽ bbox</p> <p>📍 Chọn tỉnh thành ở trên để tự động điền bbox, hoặc sử dụng công cụ vẽ hình chữ nhật trên bản đồ</p>
<p>🖱️ Kéo và thả để tạo vùng muốn phân loại</p>
<p>🔄 Có thể chỉnh sửa sau khi vẽ</p> <p>🔄 Có thể chỉnh sửa sau khi vẽ</p>
</div> </div>
<div id="predictMap"></div> <div id="predictMap"></div>
@@ -382,6 +413,16 @@
</select> </select>
</div> </div>
<div class="form-group" style="margin-top: 15px; padding: 15px; background: #fff3e0; border-radius: 8px; border-left: 4px solid #ff9800;">
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
<input type="checkbox" id="useGpuPred" checked style="width: 18px; height: 18px; margin-right: 10px;">
<span style="font-weight: 600; color: #e65100;">🚀 Sử dụng GPU (Deep Learning Models)</span>
</label>
<div style="font-size: 12px; color: #e65100; margin-top: 8px; margin-left: 28px;">
⚡ Tăng tốc prediction cho CNN/Swin-UNet models (yêu cầu GPU khả dụng)
</div>
</div>
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #2196F3;"> <div class="form-group" style="margin-top: 20px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #2196F3;">
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;"> <label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
<input type="checkbox" id="exportNDVI" checked style="width: 18px; height: 18px; margin-right: 10px;"> <input type="checkbox" id="exportNDVI" checked style="width: 18px; height: 18px; margin-right: 10px;">
@@ -453,7 +494,7 @@
<!-- NDVI Configuration --> <!-- NDVI Configuration -->
<div class="section"> <div class="section">
<h2>⚙️ Cấu hình NDVI</h2> <h2>⚙️ Cấu hình NDVI để Dự đoán</h2>
<!-- NDVI Map for selecting bbox --> <!-- NDVI Map for selecting bbox -->
<div class="form-group"> <div class="form-group">
<label>Chọn bbox trên bản đồ hoặc nhập tọa độ:</label> <label>Chọn bbox trên bản đồ hoặc nhập tọa độ:</label>
@@ -559,6 +600,12 @@
let ndviData = null; let ndviData = null;
let ndviChart = null; let ndviChart = null;
// Province data
let allPredProvinces = {};
let allPredProvincesMerged = {};
let currentPredProvinceMode = '63'; // '63' or '32'
let currentPredProvinceName = '';
// Initialize map // Initialize map
function initMap() { function initMap() {
map = L.map('predictMap').setView([9.5, 105.9], 9); map = L.map('predictMap').setView([9.5, 105.9], 9);
@@ -724,17 +771,22 @@
const select = document.getElementById('modelSelect'); const select = document.getElementById('modelSelect');
const option = select.options[select.selectedIndex]; const option = select.options[select.selectedIndex];
if (option.dataset.info) { if (option && option.dataset && option.dataset.info) {
const info = JSON.parse(option.dataset.info); try {
const infoDiv = document.getElementById('modelInfo'); const info = JSON.parse(option.dataset.info);
const infoDiv = document.getElementById('modelInfo');
document.getElementById('modelType').textContent = info.model_type || 'N/A'; document.getElementById('modelType').textContent = info.model_type || 'N/A';
document.getElementById('modelAccuracy').textContent = info.metrics?.accuracy document.getElementById('modelAccuracy').textContent = info.metrics?.accuracy
? (info.metrics.accuracy * 100).toFixed(2) + '%' ? (info.metrics.accuracy * 100).toFixed(2) + '%'
: 'N/A'; : 'N/A';
document.getElementById('modelDate').textContent = info.training_date || 'N/A'; document.getElementById('modelDate').textContent = info.training_date || 'N/A';
infoDiv.style.display = 'block'; infoDiv.style.display = 'block';
} catch (e) {
console.warn('Error parsing model info:', e);
document.getElementById('modelInfo').style.display = 'none';
}
} else { } else {
document.getElementById('modelInfo').style.display = 'none'; document.getElementById('modelInfo').style.display = 'none';
} }
@@ -765,6 +817,7 @@
// Check if export NDVI is enabled // Check if export NDVI is enabled
const exportNDVI = document.getElementById('exportNDVI').checked; const exportNDVI = document.getElementById('exportNDVI').checked;
const useGpu = document.getElementById('useGpuPred').checked;
const config = { const config = {
model_filename: modelFilename, model_filename: modelFilename,
@@ -777,6 +830,7 @@
max_scenes: parseInt(document.getElementById('predMaxScenes').value), max_scenes: parseInt(document.getElementById('predMaxScenes').value),
cloud_cover: parseInt(document.getElementById('predCloudCover').value), cloud_cover: parseInt(document.getElementById('predCloudCover').value),
resolution: parseInt(document.getElementById('predResolution').value), resolution: parseInt(document.getElementById('predResolution').value),
use_gpu: useGpu,
export_ndvi: exportNDVI, export_ndvi: exportNDVI,
export_classification: true export_classification: true
}; };
@@ -1100,16 +1154,213 @@
alert(`✅ Đã áp dụng cache preset!\n\nBBox: [${meta.bbox?.join(', ') || 'N/A'}]\nTime: ${meta.start_date || '?'}${meta.end_date || '?'}\n\nBạn có thể predict ngay mà không cần vẽ bbox!`); alert(`✅ Đã áp dụng cache preset!\n\nBBox: [${meta.bbox?.join(', ') || 'N/A'}]\nTime: ${meta.start_date || '?'}${meta.end_date || '?'}\n\nBạn có thể predict ngay mà không cần vẽ bbox!`);
} }
// === PROVINCE SELECTION FUNCTIONS FOR PREDICTION ===
// Load provinces list (both 63 and 32)
async function loadPredProvinces() {
try {
// Load 63 provinces
const response63 = await fetch('/api/provinces/by-region');
allPredProvinces = await response63.json();
// Load 32 merged provinces
const response32 = await fetch('/api/provinces-32/by-region');
allPredProvincesMerged = await response32.json();
// Default to 63 provinces
populatePredRegionButtons();
populatePredProvinceSelect();
} catch (error) {
console.error('Error loading provinces:', error);
document.getElementById('predProvinceSelect').innerHTML = '<option value="">Lỗi tải danh sách tỉnh</option>';
}
}
// Switch between 63 and 32 province lists
function switchPredProvinceList(mode) {
currentPredProvinceMode = mode;
// Update button styles
const btn63 = document.getElementById('btnPred63Provinces');
const btn32 = document.getElementById('btnPred32Provinces');
const modeLabel = document.getElementById('predProvinceListMode');
if (mode === '63') {
btn63.style.background = '#667eea';
btn63.style.color = 'white';
btn32.style.background = '#f0f0f0';
btn32.style.color = '#333';
modeLabel.textContent = 'Danh sách: 63 tỉnh';
} else {
btn63.style.background = '#f0f0f0';
btn63.style.color = '#333';
btn32.style.background = '#667eea';
btn32.style.color = 'white';
modeLabel.textContent = 'Danh sách: 32 tỉnh (sau sáp nhập)';
}
// Update region filter buttons
populatePredRegionButtons();
// Reload province list
populatePredProvinceSelect();
}
// Populate region filter buttons
function populatePredRegionButtons() {
const container = document.getElementById('predRegionFilterContainer');
// Keep the "Tất cả" button
const allButton = container.querySelector('[data-region="all"]');
container.innerHTML = '';
container.appendChild(allButton);
// Get regions from current data
const provinceData = currentPredProvinceMode === '63' ? allPredProvinces : allPredProvincesMerged;
const regions = Object.keys(provinceData);
// Add button for each region
regions.forEach(region => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'pred-region-filter-btn';
button.dataset.region = region;
button.textContent = region;
button.style.cssText = 'padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer;';
container.appendChild(button);
});
// Re-setup event listeners
setupPredRegionFilters();
}
// Populate province select dropdown
function populatePredProvinceSelect(filterRegion = 'all') {
const select = document.getElementById('predProvinceSelect');
select.innerHTML = '<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>';
// Choose which province list to use
const provinceData = currentPredProvinceMode === '63' ? allPredProvinces : allPredProvincesMerged;
// Get all regions dynamically from data
const regions = Object.keys(provinceData);
regions.forEach(region => {
if (filterRegion !== 'all' && filterRegion !== region) {
return;
}
const provinces = provinceData[region];
if (!provinces || provinces.length === 0) return;
const optgroup = document.createElement('optgroup');
optgroup.label = `${region} (${provinces.length} tỉnh)`;
provinces.forEach(province => {
const option = document.createElement('option');
option.value = province.name;
// For merged provinces, show additional info
if (currentPredProvinceMode === '32' && province.merged_from) {
option.textContent = `${province.name} (${province.merged_from.join(', ')})`;
} else {
option.textContent = `${province.name} - ${province.name_en || ''}`;
}
option.dataset.bbox = JSON.stringify(province.bbox);
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
}
// Handle province selection for prediction
function onPredProvinceSelect(event) {
const select = event.target;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption.value) {
currentPredProvinceName = '';
return;
}
const provinceName = selectedOption.value;
const bbox = JSON.parse(selectedOption.dataset.bbox);
currentPredProvinceName = provinceName;
// Update selectedBbox
selectedBbox = {
min_lon: bbox[0],
min_lat: bbox[1],
max_lon: bbox[2],
max_lat: bbox[3]
};
// Save to localStorage
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
// Draw rectangle on map
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
// Remove previous rectangle
drawnItems.clearLayers();
// Add new rectangle
const rectangle = L.rectangle(bounds, {
color: '#667eea',
weight: 3,
fillOpacity: 0.2
});
drawnItems.addLayer(rectangle);
// Fit map to bounds
map.fitBounds(bounds, { padding: [50, 50] });
// Show notification
console.log(`✅ Đã chọn tỉnh: ${provinceName}`);
alert(`✅ Đã chọn tỉnh: ${provinceName}\n\nBbox: [${bbox.join(', ')}]`);
}
// Handle region filter for prediction
function setupPredRegionFilters() {
const filterButtons = document.querySelectorAll('.pred-region-filter-btn');
filterButtons.forEach(btn => {
btn.addEventListener('click', function() {
// Update active button style
filterButtons.forEach(b => {
b.style.background = '#f0f0f0';
b.style.color = '#333';
b.style.fontWeight = 'normal';
});
this.style.background = '#667eea';
this.style.color = 'white';
this.style.fontWeight = '600';
// Filter provinces
const region = this.dataset.region;
populatePredProvinceSelect(region);
});
});
}
// Initialize on page load // Initialize on page load
window.onload = function() { window.onload = function() {
initMap(); initMap();
loadModels(); loadModels();
loadPredictionsList(); loadPredictionsList();
loadCacheList(); loadCacheList();
loadPredProvinces(); // Load provinces list
// Add event listener for model selection // Add event listener for model selection
document.getElementById('modelSelect').addEventListener('change', updateModelInfo); document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
// Add event listener for cache selection // Add event listener for cache selection
document.getElementById('cacheSelect').addEventListener('change', applyCachePreset); document.getElementById('cacheSelect').addEventListener('change', applyCachePreset);
// Add event listener for province selection
document.getElementById('predProvinceSelect').addEventListener('change', onPredProvinceSelect);
setupPredRegionFilters();
}; };
// Cleanup on page unload // Cleanup on page unload
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20260103_211345</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 03/01/2026 21:13:45</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">391,334</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">503x778</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">134.5</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_swin-unet_20260103_211215.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.25259399204516, 9.298120013966226, 105.39404296665454, 9.388909770865236]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20260103_211344.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 03/01/2026 21:13:45</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20260103_211430</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 03/01/2026 21:14:30</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">391,334</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">503x778</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">134.5</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_swin-unet_20260103_211215.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.25259399204516, 9.298120013966226, 105.39404296665454, 9.388909770865236]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20260103_211429.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 03/01/2026 21:14:30</p>
</div>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
+218 -16
View File
@@ -22,17 +22,18 @@ import hashlib
from pathlib import Path from pathlib import Path
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# PyTorch for CNN # PyTorch for CNN and advanced models
try: try:
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
import torch.optim as optim import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader from torch.utils.data import TensorDataset, DataLoader
import torchvision.models as models
PYTORCH_AVAILABLE = True PYTORCH_AVAILABLE = True
except ImportError: except ImportError:
PYTORCH_AVAILABLE = False PYTORCH_AVAILABLE = False
print("Warning: PyTorch not available. CNN model will not work.") print("Warning: PyTorch not available. CNN and advanced models will not work.")
# Define CNN model class for PyTorch # Define CNN model class for PyTorch
class CNNClassifier(nn.Module): class CNNClassifier(nn.Module):
@@ -113,6 +114,146 @@ class CNNClassifier(nn.Module):
y = y.cpu().numpy() y = y.cpu().numpy()
return np.mean(predictions == y) return np.mean(predictions == y)
# Swin-UNet Classifier for feature vectors
class SwinUNetClassifier(nn.Module):
"""
Swin Transformer U-Net style architecture adapted for feature vector classification.
Combines hierarchical Swin Transformer blocks with skip connections.
"""
def __init__(self, n_features, n_classes, embed_dim=128, depths=(2, 2, 6, 2), num_heads=(4, 8, 16, 32)):
super(SwinUNetClassifier, self).__init__()
self.n_features = n_features
self.n_classes = n_classes
self.embed_dim = embed_dim
# Feature adapter - convert input features to embedding
self.adapter = nn.Sequential(
nn.Linear(n_features, embed_dim * 2),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(embed_dim * 2, embed_dim)
)
# Encoder path with hierarchical structure
# Stage 1 - 1/4 resolution
self.encoder1 = nn.Sequential(
nn.Linear(embed_dim, embed_dim),
nn.LayerNorm(embed_dim),
nn.GELU(),
nn.Dropout(0.1)
)
self.down1 = nn.Linear(embed_dim, embed_dim * 2)
# Stage 2 - 1/8 resolution
self.encoder2 = nn.Sequential(
nn.Linear(embed_dim * 2, embed_dim * 2),
nn.LayerNorm(embed_dim * 2),
nn.GELU(),
nn.Dropout(0.1)
)
self.down2 = nn.Linear(embed_dim * 2, embed_dim * 4)
# Stage 3 - 1/16 resolution (bottleneck)
self.encoder3 = nn.Sequential(
nn.Linear(embed_dim * 4, embed_dim * 4),
nn.LayerNorm(embed_dim * 4),
nn.GELU(),
nn.Dropout(0.1)
)
# Decoder path with skip connections
self.up2 = nn.Linear(embed_dim * 4, embed_dim * 2)
self.decoder2 = nn.Sequential(
nn.Linear(embed_dim * 4, embed_dim * 2), # Concatenated with skip
nn.LayerNorm(embed_dim * 2),
nn.GELU(),
nn.Dropout(0.1)
)
self.up1 = nn.Linear(embed_dim * 2, embed_dim)
self.decoder1 = nn.Sequential(
nn.Linear(embed_dim * 2, embed_dim), # Concatenated with skip
nn.LayerNorm(embed_dim),
nn.GELU(),
nn.Dropout(0.1)
)
# Classification head
self.classifier = nn.Sequential(
nn.Linear(embed_dim, embed_dim // 2),
nn.GELU(),
nn.Dropout(0.3),
nn.Linear(embed_dim // 2, n_classes)
)
# Attention mechanism for better feature aggregation
self.attention = nn.MultiheadAttention(embed_dim, num_heads=4, batch_first=True)
def forward(self, x):
# x shape: (batch, n_features)
if len(x.shape) == 3:
x = x.squeeze(1)
batch_size = x.shape[0]
# Feature adaptation
x = self.adapter(x) # (batch, embed_dim)
# Add sequence dimension for attention (treat as sequence of length 1)
x_seq = x.unsqueeze(1) # (batch, 1, embed_dim)
# Encoder path
# Stage 1
x1 = self.encoder1(x_seq) # (batch, 1, embed_dim)
x_down1 = self.down1(x1.squeeze(1)) # (batch, embed_dim*2)
# Stage 2
x2 = self.encoder2(x_down1.unsqueeze(1)) # (batch, 1, embed_dim*2)
x_down2 = self.down2(x2.squeeze(1)) # (batch, embed_dim*4)
# Stage 3 (bottleneck)
x3 = self.encoder3(x_down2.unsqueeze(1)) # (batch, 1, embed_dim*4)
# Decoder path with skip connections
# Up2
x_up2 = self.up2(x3.squeeze(1)) # (batch, embed_dim*2)
x_cat2 = torch.cat([x_up2, x_down1], dim=1) # (batch, embed_dim*4) - concatenate skip
# Create proper 3D tensor for decoder
x_cat2_seq = x_cat2.unsqueeze(1) # (batch, 1, embed_dim*4)
x_dec2 = self.decoder2(x_cat2) # (batch, embed_dim*2)
# Up1
x_up1 = self.up1(x_dec2) # (batch, embed_dim)
x_cat1 = torch.cat([x_up1, x.squeeze(1)], dim=1) # (batch, embed_dim*2) - concatenate skip
x_dec1 = self.decoder1(x_cat1) # (batch, embed_dim)
# Apply attention mechanism for better aggregation
x_dec1_seq = x_dec1.unsqueeze(1) # (batch, 1, embed_dim)
attn_out, _ = self.attention(x_dec1_seq, x_dec1_seq, x_dec1_seq)
# Classification
output = self.classifier(attn_out.squeeze(1))
return output
def predict(self, X):
"""Scikit-learn style predict"""
self.eval()
with torch.no_grad():
if isinstance(X, np.ndarray):
X = torch.FloatTensor(X)
outputs = self(X)
_, predicted = torch.max(outputs, 1)
return predicted.cpu().numpy()
def score(self, X, y):
"""Scikit-learn style score"""
predictions = self.predict(X)
if isinstance(y, torch.Tensor):
y = y.cpu().numpy()
return np.mean(predictions == y)
# Microsoft Planetary Computer imports # Microsoft Planetary Computer imports
import planetary_computer import planetary_computer
from pystac_client import Client from pystac_client import Client
@@ -199,6 +340,10 @@ def train_model(
features = None features = None
labels = None labels = None
# Initialize FeatureExtractor early (will be used for temporal/extended modes)
update_status(f"Initializing FeatureExtractor (mode={feature_mode})...", 5)
extractor = get_feature_extractor(mode=feature_mode)
# Try to load from cache # Try to load from cache
if use_cache and cache_file.exists(): if use_cache and cache_file.exists():
update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5) update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5)
@@ -300,10 +445,6 @@ def train_model(
check_cancellation() check_cancellation()
# ============ FEATURE EXTRACTION ============
update_status(f"Initializing FeatureExtractor (mode={feature_mode})...", 50)
extractor = get_feature_extractor(mode=feature_mode)
# Load training data # Load training data
update_status("Loading training data...", 55) update_status("Loading training data...", 55)
train_gdf = gpd.read_file(training_shapefile) train_gdf = gpd.read_file(training_shapefile)
@@ -541,17 +682,75 @@ def train_model(
# Move model to CPU for saving (compatible with non-GPU systems) # Move model to CPU for saving (compatible with non-GPU systems)
model = model.cpu() model = model.cpu()
model.device_used = str(device) model.device_used = str(device)
else:
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn")
# Fit non-CNN models elif model_type == 'swin-unet':
if model_type != 'cnn': if not PYTORCH_AVAILABLE:
raise ImportError("PyTorch is required for Swin-UNet. Install: pip install torch torchvision")
n_features = X_train.shape[1]
n_classes = len(np.unique(y_train))
device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')
update_status(f"Building Swin-UNet model on {device}...", 75)
model = SwinUNetClassifier(n_features, n_classes, embed_dim=128).to(device)
# Convert to PyTorch tensors (no unsqueeze needed for Swin-UNet)
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.LongTensor(y_test)
# Create data loaders
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# Loss and optimizer with weight decay
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=0.01)
# LR scheduler for better convergence
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
# Train Swin-UNet
update_status("Training Swin-UNet model with PyTorch...", 80)
epochs = min(60, n_estimators // 2) # Swin-UNet benefits from more epochs
model.train()
for epoch in range(epochs):
epoch_loss = 0.0
for batch_X, batch_y in train_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
scheduler.step()
if (epoch + 1) % 10 == 0:
avg_loss = epoch_loss / len(train_loader)
lr = optimizer.param_groups[0]['lr']
update_status(f"Swin-UNet Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}, LR: {lr:.6f}", 80 + (epoch / epochs) * 10)
model = model.cpu()
model.device_used = str(device)
else:
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn, swin-unet")
# Fit non-neural-network models
if model_type not in ['cnn', 'swin-unet']:
model.fit(X_train, y_train) model.fit(X_train, y_train)
# Evaluate # Evaluate
update_status("Evaluating model...", 90) update_status("Evaluating model...", 90)
if model_type == 'cnn': if model_type in ['cnn', 'swin-unet']:
# PyTorch CNN evaluation # PyTorch models evaluation
train_score = model.score(X_train, y_train) train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test) test_score = model.score(X_test, y_test)
y_pred = model.predict(X_test) y_pred = model.predict(X_test)
@@ -597,10 +796,10 @@ def train_model(
"test_accuracy": float(test_score), "test_accuracy": float(test_score),
"model_type": model_type, "model_type": model_type,
"device": device if model_type == 'xgboost' else 'cpu', "device": device if model_type == 'xgboost' else 'cpu',
"n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn'] else None, "n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn', 'swin-unet'] else None,
"max_depth": max_depth if model_type != 'cnn' else None, "max_depth": max_depth if model_type not in ['cnn', 'swin-unet'] else None,
"learning_rate": learning_rate if model_type == 'xgboost' else None, "learning_rate": learning_rate if model_type in ['xgboost', 'swin-unet'] else None,
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None, "epochs": min(50, n_estimators // 2) if model_type == 'cnn' else (min(60, n_estimators // 2) if model_type == 'swin-unet' else None),
"n_features": X_train.shape[1], "n_features": X_train.shape[1],
"n_classes": len(np.unique(y_train)), "n_classes": len(np.unique(y_train)),
"class_names": class_names, "class_names": class_names,
@@ -622,6 +821,9 @@ def train_model(
label_encoder=label_encoder label_encoder=label_encoder
) )
# Construct info path (model manager saves it in model_train/)
info_path = os.path.join('model_train', model_filename.replace('.joblib', '_info.json'))
update_status("Training complete!", 100) update_status("Training complete!", 100)
return { return {
+251 -10
View File
@@ -370,16 +370,45 @@
<form id="trainingForm"> <form id="trainingForm">
<h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực Training</h3> <h3 style="margin-bottom: 15px; color: #667eea;">📍 Khu Vực Training</h3>
<!-- Province Selection Section -->
<div class="form-group" style="margin-bottom: 20px;">
<label>
<strong>🗺️ Chọn theo Tỉnh Thành:</strong>
<span style="color: #999; font-size: 13px; font-weight: normal;">(Hoặc vẽ bbox thủ công bên dưới)</span>
</label>
<!-- Toggle between 63 and 32 provinces -->
<div style="margin-bottom: 10px; display: flex; gap: 10px; align-items: center;">
<button type="button" id="btn63Provinces" onclick="switchProvinceList('63')" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">63 Tỉnh (Cũ)</button>
<button type="button" id="btn32Provinces" onclick="switchProvinceList('32')" style="padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">32 Tỉnh (Sau sáp nhập)</button>
<span id="provinceListMode" style="color: #667eea; font-weight: bold;">Danh sách: 63 tỉnh</span>
</div>
<select id="provinceSelect" style="padding: 12px; width: 100%; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; cursor: pointer;">
<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>
</select>
</div>
<!-- Region Filter -->
<div class="form-group" style="margin-bottom: 20px;">
<label><strong>🌍 Lọc theo Vùng:</strong></label>
<div id="regionFilterContainer" style="display: flex; gap: 10px; flex-wrap: wrap;">
<button type="button" class="region-filter-btn" data-region="all" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;">Tất cả</button>
<!-- Dynamic region buttons will be added here -->
</div>
</div>
<div class="map-container"> <div class="map-container">
<div class="map-instructions"> <div class="map-instructions">
<strong>💡 Hướng dẫn:</strong> Sử dụng công cụ vẽ hình chữ nhật <strong>💡 Hướng dẫn:</strong> Chọn tỉnh thành ở trên để tự động điền bbox, hoặc sử dụng công cụ vẽ hình chữ nhật
<span style="display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #333; vertical-align: middle; margin: 0 5px;"></span> <span style="display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #333; vertical-align: middle; margin: 0 5px;"></span>
ở góc trên bên trái của bản đồ để chọn khu vực training ở góc trên bên trái của bản đồ để vẽ khu vực tùy chỉnh
</div> </div>
<div id="map"></div> <div id="map"></div>
<div style="margin-top: 10px; font-size: 13px; color: #666;"> <div style="margin-top: 10px; font-size: 13px; color: #666;">
<strong>Khu vực đã chọn:</strong> <strong>Khu vực đã chọn:</strong>
<span id="bboxDisplay">Chưa chọn khu vực</span> <span id="bboxDisplay">Chưa chọn khu vực</span>
<span id="provinceDisplay" style="margin-left: 10px; color: #667eea; font-weight: 600;"></span>
</div> </div>
</div> </div>
@@ -443,6 +472,7 @@
<option value="decision_tree">🌳 Decision Tree (Đơn giản, Nhanh nhất)</option> <option value="decision_tree">🌳 Decision Tree (Đơn giản, Nhanh nhất)</option>
<option value="svm">🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</option> <option value="svm">🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</option>
<option value="cnn">🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</option> <option value="cnn">🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</option>
<option value="swin-unet">🌟 Swin-UNet (Transformer + U-Net, Độ chính xác cao, Hỗ trợ GPU)</option>
</select> </select>
<div style="margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;"> <div style="margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;">
<span id="modelTypeDesc" style="color: #1976d2;"> <span id="modelTypeDesc" style="color: #1976d2;">
@@ -463,7 +493,7 @@
</div> </div>
<div class="form-group" id="learningRateGroup"> <div class="form-group" id="learningRateGroup">
<label>Learning Rate:</label> <label>Learning Rate:</label>
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required> <input type="number" step="0.0001" id="learningRate" value="0.1" min="0.0001" max="1" required>
</div> </div>
<div class="form-group" id="testSizeGroup"> <div class="form-group" id="testSizeGroup">
<label>Tỷ lệ dữ liệu test (0-1):</label> <label>Tỷ lệ dữ liệu test (0-1):</label>
@@ -764,8 +794,8 @@
<h3>📦 ${model.filename}</h3> <h3>📦 ${model.filename}</h3>
<p><strong>Tạo lúc:</strong> ${new Date(model.created).toLocaleString('vi-VN')}</p> <p><strong>Tạo lúc:</strong> ${new Date(model.created).toLocaleString('vi-VN')}</p>
<p><strong>Kích thước:</strong> ${model.size_mb} MB</p> <p><strong>Kích thước:</strong> ${model.size_mb} MB</p>
${model.info.train_accuracy ? `<p><strong>Train Accuracy:</strong> ${(model.info.train_accuracy * 100).toFixed(2)}%</p>` : ''} ${model.info && model.info.train_accuracy ? `<p><strong>Train Accuracy:</strong> ${(model.info.train_accuracy * 100).toFixed(2)}%</p>` : ''}
${model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''} ${model.info && model.info.test_accuracy ? `<p><strong>Test Accuracy:</strong> ${(model.info.test_accuracy * 100).toFixed(2)}%</p>` : ''}
`; `;
container.appendChild(item); container.appendChild(item);
@@ -1101,7 +1131,8 @@
'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu', 'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu',
'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh', 'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh',
'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn', 'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn',
'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch' 'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch',
'swin-unet': '✓ Swin-UNet: Kết hợp Transformer + U-Net, độ chính xác cao nhất, phù hợp dataset lớn, tốc độ training trung bình'
}; };
desc.textContent = descriptions[modelType]; desc.textContent = descriptions[modelType];
@@ -1130,10 +1161,19 @@
document.getElementById('nEstimators').value = 50; document.getElementById('nEstimators').value = 50;
learningRateGroup.style.display = 'none'; learningRateGroup.style.display = 'none';
useGpuGroup.style.display = ''; // Show GPU option for CNN useGpuGroup.style.display = ''; // Show GPU option for CNN
} else if (modelType === 'swin-unet') {
// Swin-UNet uses n_estimators as epochs and supports GPU
nEstimatorsGroup.style.display = '';
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
document.getElementById('nEstimators').value = 100;
learningRateGroup.style.display = ''; // Show learning rate for Swin-UNet
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.0005):';
document.getElementById('learningRate').value = 0.0005;
useGpuGroup.style.display = ''; // Show GPU option for Swin-UNet
} }
// Reset n_estimators label for non-CNN // Reset n_estimators label for non-CNN/non-Swin-UNet
if (modelType !== 'cnn' && modelType !== 'decision_tree' && modelType !== 'svm') { if (modelType !== 'cnn' && modelType !== 'swin-unet' && modelType !== 'decision_tree' && modelType !== 'svm') {
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:'; document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
} }
} }
@@ -1278,12 +1318,213 @@
} }
} }
// === PROVINCE SELECTION FUNCTIONS ===
let allProvinces = {};
let allProvincesMerged = {};
let currentProvinceName = '';
let currentProvinceMode = '63'; // '63' or '32'
// Load provinces list (both 63 and 32)
async function loadProvinces() {
try {
// Load 63 provinces
const response63 = await fetch(`${API_BASE}/provinces/by-region`);
allProvinces = await response63.json();
// Load 32 merged provinces
const response32 = await fetch(`${API_BASE}/provinces-32/by-region`);
allProvincesMerged = await response32.json();
// Default to 63 provinces
populateRegionButtons();
populateProvinceSelect();
} catch (error) {
console.error('Error loading provinces:', error);
document.getElementById('provinceSelect').innerHTML = '<option value="">Lỗi tải danh sách tỉnh</option>';
}
}
// Switch between 63 and 32 province lists
function switchProvinceList(mode) {
currentProvinceMode = mode;
// Update button styles
const btn63 = document.getElementById('btn63Provinces');
const btn32 = document.getElementById('btn32Provinces');
const modeLabel = document.getElementById('provinceListMode');
if (mode === '63') {
btn63.style.background = '#667eea';
btn63.style.color = 'white';
btn32.style.background = '#f0f0f0';
btn32.style.color = '#333';
modeLabel.textContent = 'Danh sách: 63 tỉnh';
} else {
btn63.style.background = '#f0f0f0';
btn63.style.color = '#333';
btn32.style.background = '#667eea';
btn32.style.color = 'white';
modeLabel.textContent = 'Danh sách: 32 tỉnh (sau sáp nhập)';
}
// Update region filter buttons
populateRegionButtons();
// Reload province list
populateProvinceSelect();
}
// Populate province select dropdown
function populateProvinceSelect(filterRegion = 'all') {
const select = document.getElementById('provinceSelect');
select.innerHTML = '<option value="">-- Chọn tỉnh thành để tải bbox tự động --</option>';
// Choose which province list to use
const provinceData = currentProvinceMode === '63' ? allProvinces : allProvincesMerged;
// Get all regions dynamically from data
const regions = Object.keys(provinceData);
regions.forEach(region => {
if (filterRegion !== 'all' && filterRegion !== region) {
return;
}
const provinces = provinceData[region];
if (!provinces || provinces.length === 0) return;
const optgroup = document.createElement('optgroup');
optgroup.label = `${region} (${provinces.length} tỉnh)`;
provinces.forEach(province => {
const option = document.createElement('option');
option.value = province.name;
// For merged provinces, show additional info
if (currentProvinceMode === '32' && province.merged_from) {
option.textContent = `${province.name} (${province.merged_from.join(', ')})`;
} else {
option.textContent = `${province.name} - ${province.name_en || ''}`;
}
option.dataset.bbox = JSON.stringify(province.bbox);
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
}
// Handle province selection
function onProvinceSelect(event) {
const select = event.target;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption.value) {
currentProvinceName = '';
document.getElementById('provinceDisplay').textContent = '';
return;
}
const provinceName = selectedOption.value;
const bbox = JSON.parse(selectedOption.dataset.bbox);
currentProvinceName = provinceName;
// Update bbox inputs
document.getElementById('minLon').value = bbox[0];
document.getElementById('minLat').value = bbox[1];
document.getElementById('maxLon').value = bbox[2];
document.getElementById('maxLat').value = bbox[3];
// Update bbox display
document.getElementById('bboxDisplay').textContent =
`Lon: ${bbox[0]}${bbox[2]}, Lat: ${bbox[1]}${bbox[3]}`;
document.getElementById('provinceDisplay').textContent = `📍 ${provinceName}`;
// Draw rectangle on map
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
// Remove previous rectangle
if (currentRectangle) {
drawnItems.removeLayer(currentRectangle);
}
// Add new rectangle
currentRectangle = L.rectangle(bounds, {
color: '#667eea',
weight: 3,
fillOpacity: 0.2
});
drawnItems.addLayer(currentRectangle);
// Fit map to bounds
map.fitBounds(bounds, { padding: [50, 50] });
// Show success notification
showNotification('success', `Đã chọn tỉnh: ${provinceName}`);
}
// Populate region filter buttons
function populateRegionButtons() {
const container = document.getElementById('regionFilterContainer');
// Keep the "Tất cả" button
const allButton = container.querySelector('[data-region="all"]');
container.innerHTML = '';
container.appendChild(allButton);
// Get regions from current data
const provinceData = currentProvinceMode === '63' ? allProvinces : allProvincesMerged;
const regions = Object.keys(provinceData);
// Add button for each region
regions.forEach(region => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'region-filter-btn';
button.dataset.region = region;
button.textContent = region;
button.style.cssText = 'padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 6px; cursor: pointer;';
container.appendChild(button);
});
// Re-setup event listeners
setupRegionFilters();
}
// Handle region filter
function setupRegionFilters() {
const filterButtons = document.querySelectorAll('.region-filter-btn');
filterButtons.forEach(btn => {
btn.addEventListener('click', function() {
// Update active button style
filterButtons.forEach(b => {
b.style.background = '#f0f0f0';
b.style.color = '#333';
b.style.fontWeight = 'normal';
});
this.style.background = '#667eea';
this.style.color = 'white';
this.style.fontWeight = '600';
// Filter provinces
const region = this.dataset.region;
populateProvinceSelect(region);
});
});
}
// Initialize map when page loads // Initialize map when page loads
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initMap(); initMap();
initPredictionMap();
loadPredictionsList(); // Load predictions list on page load
loadCacheInfo(); // Load cache info loadCacheInfo(); // Load cache info
loadProvinces(); // Load provinces list
// Add event listeners
document.getElementById('provinceSelect').addEventListener('change', onProvinceSelect);
setupRegionFilters();
// Add model type change listener // Add model type change listener
document.getElementById('modelType').addEventListener('change', updateModelTypeUI); document.getElementById('modelType').addEventListener('change', updateModelTypeUI);
+376
View File
@@ -0,0 +1,376 @@
"""
Vietnam Provinces Boundaries
Ranh giới các tỉnh thành Việt Nam với bbox coordinates
"""
VIETNAM_PROVINCES = {
"Toàn quốc": {
"bbox": [102.14, 8.18, 109.46, 23.39],
"name_en": "Vietnam (Full)",
"region": "Toàn quốc"
},
# Miền Bắc - Northern Region
"Hà Nội": {
"bbox": [105.35, 20.53, 105.92, 21.33],
"name_en": "Hanoi",
"region": "Miền Bắc"
},
"Hải Phòng": {
"bbox": [106.48, 20.70, 107.07, 21.09],
"name_en": "Hai Phong",
"region": "Miền Bắc"
},
"Quảng Ninh": {
"bbox": [106.48, 20.70, 108.26, 21.62],
"name_en": "Quang Ninh",
"region": "Miền Bắc"
},
"Lào Cai": {
"bbox": [103.22, 21.82, 104.45, 22.77],
"name_en": "Lao Cai",
"region": "Miền Bắc"
},
"Điện Biên": {
"bbox": [102.72, 21.09, 103.45, 22.21],
"name_en": "Dien Bien",
"region": "Miền Bắc"
},
"Lai Châu": {
"bbox": [102.72, 21.82, 103.72, 22.77],
"name_en": "Lai Chau",
"region": "Miền Bắc"
},
"Hà Giang": {
"bbox": [104.42, 22.33, 105.59, 23.39],
"name_en": "Ha Giang",
"region": "Miền Bắc"
},
"Cao Bằng": {
"bbox": [105.52, 22.24, 106.70, 23.04],
"name_en": "Cao Bang",
"region": "Miền Bắc"
},
"Bắc Kạn": {
"bbox": [105.48, 21.95, 106.15, 22.52],
"name_en": "Bac Kan",
"region": "Miền Bắc"
},
"Tuyên Quang": {
"bbox": [104.97, 21.65, 105.65, 22.42],
"name_en": "Tuyen Quang",
"region": "Miền Bắc"
},
"Thái Nguyên": {
"bbox": [105.48, 21.27, 106.15, 22.07],
"name_en": "Thai Nguyen",
"region": "Miền Bắc"
},
"Lạng Sơn": {
"bbox": [106.22, 21.40, 107.18, 22.41],
"name_en": "Lang Son",
"region": "Miền Bắc"
},
"Bắc Giang": {
"bbox": [105.97, 21.05, 106.70, 21.68],
"name_en": "Bac Giang",
"region": "Miền Bắc"
},
"Phú Thọ": {
"bbox": [104.83, 21.01, 105.48, 21.82],
"name_en": "Phu Tho",
"region": "Miền Bắc"
},
"Vĩnh Phúc": {
"bbox": [105.31, 21.14, 105.81, 21.61],
"name_en": "Vinh Phuc",
"region": "Miền Bắc"
},
"Bắc Ninh": {
"bbox": [105.83, 20.93, 106.26, 21.32],
"name_en": "Bac Ninh",
"region": "Miền Bắc"
},
"Hải Dương": {
"bbox": [106.14, 20.68, 106.70, 21.07],
"name_en": "Hai Duong",
"region": "Miền Bắc"
},
"Hưng Yên": {
"bbox": [105.83, 20.58, 106.26, 21.03],
"name_en": "Hung Yen",
"region": "Miền Bắc"
},
"Hà Nam": {
"bbox": [105.79, 20.33, 106.14, 20.73],
"name_en": "Ha Nam",
"region": "Miền Bắc"
},
"Nam Định": {
"bbox": [105.98, 20.04, 106.47, 20.64],
"name_en": "Nam Dinh",
"region": "Miền Bắc"
},
"Thái Bình": {
"bbox": [106.23, 20.27, 106.70, 20.76],
"name_en": "Thai Binh",
"region": "Miền Bắc"
},
"Ninh Bình": {
"bbox": [105.70, 20.05, 106.14, 20.50],
"name_en": "Ninh Binh",
"region": "Miền Bắc"
},
"Hòa Bình": {
"bbox": [104.83, 20.35, 105.74, 21.06],
"name_en": "Hoa Binh",
"region": "Miền Bắc"
},
"Sơn La": {
"bbox": [103.22, 20.66, 104.83, 21.82],
"name_en": "Son La",
"region": "Miền Bắc"
},
"Yên Bái": {
"bbox": [103.97, 21.35, 105.20, 22.21],
"name_en": "Yen Bai",
"region": "Miền Bắc"
},
# Miền Trung - Central Region
"Thanh Hóa": {
"bbox": [104.83, 19.33, 106.14, 20.66],
"name_en": "Thanh Hoa",
"region": "Miền Trung"
},
"Nghệ An": {
"bbox": [103.97, 18.34, 105.74, 19.89],
"name_en": "Nghe An",
"region": "Miền Trung"
},
"Hà Tĩnh": {
"bbox": [105.20, 17.98, 106.23, 18.78],
"name_en": "Ha Tinh",
"region": "Miền Trung"
},
"Quảng Bình": {
"bbox": [105.74, 16.97, 107.04, 18.06],
"name_en": "Quang Binh",
"region": "Miền Trung"
},
"Quảng Trị": {
"bbox": [106.48, 16.38, 107.54, 17.20],
"name_en": "Quang Tri",
"region": "Miền Trung"
},
"Thừa Thiên Huế": {
"bbox": [107.04, 16.01, 108.01, 16.95],
"name_en": "Thua Thien Hue",
"region": "Miền Trung"
},
"Đà Nẵng": {
"bbox": [107.77, 15.87, 108.33, 16.28],
"name_en": "Da Nang",
"region": "Miền Trung"
},
"Quảng Nam": {
"bbox": [107.04, 14.93, 108.70, 16.16],
"name_en": "Quang Nam",
"region": "Miền Trung"
},
"Quảng Ngãi": {
"bbox": [108.01, 14.66, 109.18, 15.53],
"name_en": "Quang Ngai",
"region": "Miền Trung"
},
"Bình Định": {
"bbox": [108.33, 13.76, 109.26, 14.72],
"name_en": "Binh Dinh",
"region": "Miền Trung"
},
"Phú Yên": {
"bbox": [108.70, 12.75, 109.46, 13.96],
"name_en": "Phu Yen",
"region": "Miền Trung"
},
"Khánh Hòa": {
"bbox": [108.70, 11.75, 109.46, 12.95],
"name_en": "Khanh Hoa",
"region": "Miền Trung"
},
"Ninh Thuận": {
"bbox": [108.33, 11.27, 109.18, 12.04],
"name_en": "Ninh Thuan",
"region": "Miền Trung"
},
"Bình Thuận": {
"bbox": [107.54, 10.49, 108.70, 11.75],
"name_en": "Binh Thuan",
"region": "Miền Trung"
},
"Kon Tum": {
"bbox": [107.54, 13.95, 108.70, 15.17],
"name_en": "Kon Tum",
"region": "Tây Nguyên"
},
"Gia Lai": {
"bbox": [107.54, 13.17, 108.70, 14.72],
"name_en": "Gia Lai",
"region": "Tây Nguyên"
},
"Đắk Lắk": {
"bbox": [107.54, 12.24, 108.70, 13.40],
"name_en": "Dak Lak",
"region": "Tây Nguyên"
},
"Đắk Nông": {
"bbox": [107.04, 11.75, 108.33, 12.75],
"name_en": "Dak Nong",
"region": "Tây Nguyên"
},
"Lâm Đồng": {
"bbox": [107.04, 10.99, 108.70, 12.52],
"name_en": "Lam Dong",
"region": "Tây Nguyên"
},
# Miền Nam - Southern Region
"Hồ Chí Minh": {
"bbox": [106.36, 10.35, 107.04, 11.16],
"name_en": "Ho Chi Minh City",
"region": "Miền Nam"
},
"Đồng Nai": {
"bbox": [106.70, 10.49, 107.54, 11.51],
"name_en": "Dong Nai",
"region": "Miền Nam"
},
"Bình Dương": {
"bbox": [106.36, 10.87, 106.96, 11.51],
"name_en": "Binh Duong",
"region": "Miền Nam"
},
"Bà Rịa - Vũng Tàu": {
"bbox": [107.04, 10.16, 107.77, 10.87],
"name_en": "Ba Ria - Vung Tau",
"region": "Miền Nam"
},
"Bình Phước": {
"bbox": [106.36, 11.16, 107.54, 12.24],
"name_en": "Binh Phuoc",
"region": "Miền Nam"
},
"Tây Ninh": {
"bbox": [105.74, 10.87, 106.70, 11.75],
"name_en": "Tay Ninh",
"region": "Miền Nam"
},
"Long An": {
"bbox": [105.74, 10.16, 106.70, 11.16],
"name_en": "Long An",
"region": "Miền Nam"
},
"Tiền Giang": {
"bbox": [105.74, 9.99, 106.70, 10.70],
"name_en": "Tien Giang",
"region": "Miền Nam"
},
"Bến Tre": {
"bbox": [105.98, 9.77, 106.70, 10.35],
"name_en": "Ben Tre",
"region": "Miền Nam"
},
"Đồng Tháp": {
"bbox": [105.20, 10.16, 105.98, 11.16],
"name_en": "Dong Thap",
"region": "Miền Nam"
},
"Vĩnh Long": {
"bbox": [105.74, 9.77, 106.36, 10.35],
"name_en": "Vinh Long",
"region": "Miền Nam"
},
"Trà Vinh": {
"bbox": [105.98, 9.33, 106.70, 10.04],
"name_en": "Tra Vinh",
"region": "Miền Nam"
},
"An Giang": {
"bbox": [104.83, 9.99, 105.74, 10.99],
"name_en": "An Giang",
"region": "Miền Nam"
},
"Kiên Giang": {
"bbox": [103.22, 8.68, 105.48, 10.52],
"name_en": "Kien Giang",
"region": "Miền Nam"
},
"Cần Thơ": {
"bbox": [105.48, 9.77, 106.14, 10.35],
"name_en": "Can Tho",
"region": "Miền Nam"
},
"Hậu Giang": {
"bbox": [105.31, 9.33, 105.98, 9.99],
"name_en": "Hau Giang",
"region": "Miền Nam"
},
"Sóc Trăng": {
"bbox": [105.48, 9.16, 106.23, 9.99],
"name_en": "Soc Trang",
"region": "Miền Nam"
},
"Bạc Liêu": {
"bbox": [105.31, 8.93, 105.98, 9.60],
"name_en": "Bac Lieu",
"region": "Miền Nam"
},
"Cà Mau": {
"bbox": [104.58, 8.18, 105.48, 9.60],
"name_en": "Ca Mau",
"region": "Miền Nam"
},
}
def get_all_provinces():
"""Lấy danh sách tất cả các tỉnh thành"""
return list(VIETNAM_PROVINCES.keys())
def get_provinces_by_region():
"""Lấy danh sách tỉnh thành theo vùng miền"""
regions = {}
for province, data in VIETNAM_PROVINCES.items():
region = data["region"]
if region not in regions:
regions[region] = []
regions[region].append({
"name": province,
"name_en": data["name_en"],
"bbox": data["bbox"]
})
return regions
def get_province_bbox(province_name):
"""Lấy bbox của một tỉnh thành"""
if province_name in VIETNAM_PROVINCES:
return VIETNAM_PROVINCES[province_name]["bbox"]
return None
def search_province(query):
"""Tìm kiếm tỉnh thành theo tên"""
query = query.lower()
results = []
for province, data in VIETNAM_PROVINCES.items():
if (query in province.lower() or
query in data["name_en"].lower()):
results.append({
"name": province,
"name_en": data["name_en"],
"bbox": data["bbox"],
"region": data["region"]
})
return results
+461
View File
@@ -0,0 +1,461 @@
"""
Vietnam Provinces After Administrative Merger (32 provinces)
32 tỉnh thành Việt Nam sau sáp nhập theo Nghị quyết 1211/2023
Bbox đã được mở rộng để bao phủ các tỉnh đã hợp nhất
"""
VIETNAM_PROVINCES_32 = {
# Thành phố trực thuộc TW (5)
"Hà Nội": {
"bbox": [105.35, 20.53, 105.92, 21.33],
"name_en": "Hanoi",
"region": "Đồng bằng Bắc Bộ",
"merged_from": None,
"area_km2": 3359
},
"Hải Phòng": {
"bbox": [106.14, 20.68, 107.07, 21.09], # Bao gồm cả Hải Dương
"name_en": "Hai Phong",
"region": "Đồng bằng Bắc Bộ",
"merged_from": ["Hải Phòng", "Hải Dương"],
"area_km2": 2914
},
"Đà Nẵng": {
"bbox": [107.04, 14.93, 108.70, 16.28], # Bao gồm cả Quảng Nam
"name_en": "Da Nang - Quang Nam",
"region": "Duyên hải Nam Trung Bộ",
"merged_from": ["Đà Nẵng", "Quảng Nam"],
"area_km2": 11065
},
"Hồ Chí Minh": {
"bbox": [106.36, 10.35, 107.04, 11.16],
"name_en": "Ho Chi Minh City",
"region": "Đông Nam Bộ",
"merged_from": None,
"area_km2": 2061
},
"Cần Thơ": {
"bbox": [105.48, 9.77, 106.14, 10.35],
"name_en": "Can Tho",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 1402
},
# Các tỉnh sau sáp nhập (27)
# Vùng núi phía Bắc
"Lào Cai": {
"bbox": [103.22, 21.82, 104.45, 22.77],
"name_en": "Lao Cai",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 6384
},
"Điện Biên": {
"bbox": [102.72, 21.09, 103.72, 22.21], # Bao gồm cả Lai Châu
"name_en": "Dien Bien - Lai Chau",
"region": "Vùng núi phía Bắc",
"merged_from": ["Điện Biên", "Lai Châu"],
"area_km2": 15274
},
"Hà Giang": {
"bbox": [104.42, 22.33, 105.59, 23.39],
"name_en": "Ha Giang",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 7946
},
"Cao Bằng": {
"bbox": [105.48, 21.95, 106.70, 23.04], # Bao gồm cả Bắc Kạn
"name_en": "Cao Bang - Bac Kan",
"region": "Vùng núi phía Bắc",
"merged_from": ["Cao Bằng", "Bắc Kạn"],
"area_km2": 11335
},
"Lạng Sơn": {
"bbox": [106.22, 21.40, 107.18, 22.41],
"name_en": "Lang Son",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 8327
},
"Tuyên Quang": {
"bbox": [104.97, 21.65, 105.65, 22.42],
"name_en": "Tuyen Quang",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 5868
},
"Yên Bái": {
"bbox": [103.97, 21.35, 105.20, 22.21],
"name_en": "Yen Bai",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 6899
},
"Thái Nguyên": {
"bbox": [105.48, 21.27, 106.15, 22.07],
"name_en": "Thai Nguyen",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 3534
},
"Phú Thọ": {
"bbox": [104.83, 21.01, 105.48, 21.82],
"name_en": "Phu Tho",
"region": "Vùng núi phía Bắc",
"merged_from": None,
"area_km2": 3533
},
"Hòa Bình": {
"bbox": [103.22, 20.35, 105.74, 21.82], # Bao gồm cả Sơn La
"name_en": "Hoa Binh - Son La",
"region": "Vùng núi phía Bắc",
"merged_from": ["Hòa Bình", "Sơn La"],
"area_km2": 19210
},
# Đồng bằng Bắc Bộ
"Quảng Ninh": {
"bbox": [106.48, 20.70, 108.26, 21.62],
"name_en": "Quang Ninh",
"region": "Đồng bằng Bắc Bộ",
"merged_from": None,
"area_km2": 6102
},
"Bắc Ninh": {
"bbox": [105.83, 20.93, 106.70, 21.68], # Bao gồm cả Bắc Giang
"name_en": "Bac Ninh - Bac Giang",
"region": "Đồng bằng Bắc Bộ",
"merged_from": ["Bắc Ninh", "Bắc Giang"],
"area_km2": 4631
},
"Vĩnh Phúc": {
"bbox": [105.31, 21.14, 105.81, 21.61],
"name_en": "Vinh Phuc",
"region": "Đồng bằng Bắc Bộ",
"merged_from": None,
"area_km2": 1236
},
"Hưng Yên": {
"bbox": [105.83, 20.58, 106.26, 21.03],
"name_en": "Hung Yen",
"region": "Đồng bằng Bắc Bộ",
"merged_from": None,
"area_km2": 926
},
"Nam Định": {
"bbox": [105.79, 20.04, 106.47, 20.73], # Bao gồm cả Hà Nam
"name_en": "Nam Dinh - Ha Nam",
"region": "Đồng bằng Bắc Bộ",
"merged_from": ["Nam Định", "Hà Nam"],
"area_km2": 2442
},
"Thái Bình": {
"bbox": [106.23, 20.27, 106.70, 20.76],
"name_en": "Thai Binh",
"region": "Đồng bằng Bắc Bộ",
"merged_from": None,
"area_km2": 1570
},
# Bắc Trung Bộ
"Thanh Hóa": {
"bbox": [104.83, 19.33, 106.14, 20.66], # Bao gồm cả Ninh Bình
"name_en": "Thanh Hoa - Ninh Binh",
"region": "Bắc Trung Bộ",
"merged_from": ["Thanh Hóa", "Ninh Bình"],
"area_km2": 12490
},
"Nghệ An": {
"bbox": [103.97, 17.98, 106.23, 19.89], # Bao gồm cả Hà Tĩnh
"name_en": "Nghe An - Ha Tinh",
"region": "Bắc Trung Bộ",
"merged_from": ["Nghệ An", "Hà Tĩnh"],
"area_km2": 22793
},
"Quảng Bình": {
"bbox": [105.74, 16.97, 107.04, 18.06],
"name_en": "Quang Binh",
"region": "Bắc Trung Bộ",
"merged_from": None,
"area_km2": 8065
},
"Quảng Trị": {
"bbox": [106.48, 16.38, 107.54, 17.20],
"name_en": "Quang Tri",
"region": "Bắc Trung Bộ",
"merged_from": None,
"area_km2": 4746
},
"Thừa Thiên Huế": {
"bbox": [107.04, 16.01, 108.01, 16.95],
"name_en": "Thua Thien Hue",
"region": "Bắc Trung Bộ",
"merged_from": None,
"area_km2": 5033
},
# Duyên hải Nam Trung Bộ
"Quảng Ngãi": {
"bbox": [108.01, 14.66, 109.18, 15.53],
"name_en": "Quang Ngai",
"region": "Duyên hải Nam Trung Bộ",
"merged_from": None,
"area_km2": 5153
},
"Bình Định": {
"bbox": [108.33, 12.75, 109.46, 14.72], # Bao gồm cả Phú Yên
"name_en": "Binh Dinh - Phu Yen",
"region": "Duyên hải Nam Trung Bộ",
"merged_from": ["Bình Định", "Phú Yên"],
"area_km2": 11092
},
"Khánh Hòa": {
"bbox": [108.70, 11.75, 109.46, 12.95],
"name_en": "Khanh Hoa",
"region": "Duyên hải Nam Trung Bộ",
"merged_from": None,
"area_km2": 5218
},
"Bình Thuận": {
"bbox": [107.54, 10.49, 109.18, 12.04], # Bao gồm cả Ninh Thuận
"name_en": "Binh Thuan - Ninh Thuan",
"region": "Duyên hải Nam Trung Bộ",
"merged_from": ["Bình Thuận", "Ninh Thuận"],
"area_km2": 11234
},
# Tây Nguyên
"Gia Lai": {
"bbox": [107.54, 13.17, 108.70, 15.17], # Bao gồm cả Kon Tum
"name_en": "Gia Lai - Kon Tum",
"region": "Tây Nguyên",
"merged_from": ["Gia Lai", "Kon Tum"],
"area_km2": 25536
},
"Đắk Lắk": {
"bbox": [107.04, 11.75, 108.70, 13.40], # Bao gồm cả Đắk Nông
"name_en": "Dak Lak - Dak Nong",
"region": "Tây Nguyên",
"merged_from": ["Đắk Lắk", "Đắk Nông"],
"area_km2": 19850
},
"Lâm Đồng": {
"bbox": [107.04, 10.99, 108.70, 12.52],
"name_en": "Lam Dong",
"region": "Tây Nguyên",
"merged_from": None,
"area_km2": 9776
},
# Đông Nam Bộ
"Đồng Nai": {
"bbox": [106.36, 10.49, 107.54, 12.24], # Bao gồm cả Bình Phước
"name_en": "Dong Nai - Binh Phuoc",
"region": "Đông Nam Bộ",
"merged_from": ["Đồng Nai", "Bình Phước"],
"area_km2": 13317
},
"Bình Dương": {
"bbox": [106.36, 10.87, 106.96, 11.51],
"name_en": "Binh Duong",
"region": "Đông Nam Bộ",
"merged_from": None,
"area_km2": 2695
},
"Bà Rịa - Vũng Tàu": {
"bbox": [107.04, 10.16, 107.77, 10.87],
"name_en": "Ba Ria - Vung Tau",
"region": "Đông Nam Bộ",
"merged_from": None,
"area_km2": 1990
},
"Tây Ninh": {
"bbox": [105.74, 10.87, 106.70, 11.75],
"name_en": "Tay Ninh",
"region": "Đông Nam Bộ",
"merged_from": None,
"area_km2": 4040
},
# Đồng bằng sông Cửu Long
"Tiền Giang": {
"bbox": [105.74, 9.99, 106.70, 11.16], # Bao gồm cả Long An
"name_en": "Tien Giang - Long An",
"region": "Đồng bằng sông Cửu Long",
"merged_from": ["Tiền Giang", "Long An"],
"area_km2": 6935
},
"Bến Tre": {
"bbox": [105.98, 9.77, 106.70, 10.35],
"name_en": "Ben Tre",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 2360
},
"Vĩnh Long": {
"bbox": [105.74, 9.33, 106.70, 10.35], # Bao gồm cả Trà Vinh
"name_en": "Vinh Long - Tra Vinh",
"region": "Đồng bằng sông Cửu Long",
"merged_from": ["Vĩnh Long", "Trà Vinh"],
"area_km2": 4567
},
"Đồng Tháp": {
"bbox": [105.20, 10.16, 105.98, 11.16],
"name_en": "Dong Thap",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 3377
},
"An Giang": {
"bbox": [104.83, 9.99, 105.74, 10.99],
"name_en": "An Giang",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 3537
},
"Kiên Giang": {
"bbox": [103.22, 8.68, 105.48, 10.52],
"name_en": "Kien Giang",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 6348
},
"Sóc Trăng": {
"bbox": [105.31, 9.16, 106.23, 9.99], # Bao gồm cả Hậu Giang
"name_en": "Soc Trang - Hau Giang",
"region": "Đồng bằng sông Cửu Long",
"merged_from": ["Sóc Trăng", "Hậu Giang"],
"area_km2": 4750
},
"Bạc Liêu": {
"bbox": [105.31, 8.93, 105.98, 9.60],
"name_en": "Bac Lieu",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 2585
},
"Cà Mau": {
"bbox": [104.58, 8.18, 105.48, 9.60],
"name_en": "Ca Mau",
"region": "Đồng bằng sông Cửu Long",
"merged_from": None,
"area_km2": 5332
},
}
def get_all_provinces_32():
"""Lấy danh sách tất cả 32 tỉnh thành sau sáp nhập"""
return list(VIETNAM_PROVINCES_32.keys())
def get_provinces_by_region_32():
"""Lấy danh sách 32 tỉnh thành theo vùng miền"""
regions = {}
for province, data in VIETNAM_PROVINCES_32.items():
region = data["region"]
if region not in regions:
regions[region] = []
regions[region].append({
"name": province,
"name_en": data["name_en"],
"bbox": data["bbox"],
"merged_from": data.get("merged_from"),
"area_km2": data.get("area_km2")
})
return regions
def get_province_bbox_32(province_name):
"""Lấy bbox của một tỉnh thành (32 tỉnh)"""
if province_name in VIETNAM_PROVINCES_32:
return VIETNAM_PROVINCES_32[province_name]["bbox"]
return None
def get_merged_info(province_name):
"""Lấy thông tin sáp nhập của tỉnh"""
if province_name in VIETNAM_PROVINCES_32:
data = VIETNAM_PROVINCES_32[province_name]
return {
"name": province_name,
"bbox": data["bbox"],
"merged_from": data.get("merged_from"),
"region": data["region"],
"area_km2": data.get("area_km2")
}
return None
def search_province_32(query):
"""Tìm kiếm tỉnh thành theo tên (32 tỉnh)"""
query = query.lower()
results = []
for province, data in VIETNAM_PROVINCES_32.items():
if (query in province.lower() or
query in data["name_en"].lower()):
results.append({
"name": province,
"name_en": data["name_en"],
"bbox": data["bbox"],
"region": data["region"],
"merged_from": data.get("merged_from"),
"area_km2": data.get("area_km2")
})
return results
def get_provinces_statistics():
"""Thống kê các tỉnh đã sáp nhập"""
total = len(VIETNAM_PROVINCES_32)
merged = len([p for p in VIETNAM_PROVINCES_32.values() if p.get("merged_from")])
original = total - merged
return {
"total_provinces": total,
"merged_provinces": merged,
"original_provinces": original,
"regions": list(set(p["region"] for p in VIETNAM_PROVINCES_32.values())),
"total_area_km2": sum(p.get("area_km2", 0) for p in VIETNAM_PROVINCES_32.values())
}