refactor: reorganize project structure by moving core modules and update import paths in API server
This commit is contained in:
@@ -0,0 +1,645 @@
|
||||
"""
|
||||
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
|
||||
from pathlib import Path
|
||||
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)
|
||||
|
||||
if hasattr(self.model, 'encoder'):
|
||||
# Custom UNet from train_cloud_removal.py
|
||||
expected_channels = self.model.encoder[0].double_conv[0].in_channels
|
||||
elif hasattr(self.model, 'inc') and hasattr(self.model.inc.double_conv[0], 'in_channels'):
|
||||
expected_channels = self.model.inc.double_conv[0].in_channels
|
||||
elif hasattr(self.model, 'conv1') and hasattr(self.model.conv1, 'in_channels'):
|
||||
expected_channels = self.model.conv1.in_channels
|
||||
else:
|
||||
expected_channels = 6
|
||||
|
||||
|
||||
if expected_channels > input_tensor.shape[1]:
|
||||
pad_channels = expected_channels - input_tensor.shape[1]
|
||||
padding = torch.zeros(1, pad_channels, *input_tensor.shape[2:]).to(self.device)
|
||||
input_tensor = torch.cat([input_tensor, padding], dim=1)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
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,93 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
from torch.utils.data import Dataset
|
||||
import glob
|
||||
|
||||
class NDVITimeSeriesDataset(Dataset):
|
||||
def __init__(self, sequence_length=3, spatial=False):
|
||||
"""
|
||||
Đọc dữ liệu S2 từ cache, tính NDVI và tạo Time-Series.
|
||||
spatial=False -> Output 1D cho LSTM/ARIMA
|
||||
spatial=True -> Output 2D cho ConvLSTM
|
||||
"""
|
||||
self.sequence_length = sequence_length
|
||||
self.spatial = spatial
|
||||
self.data_seqs = []
|
||||
self.targets = []
|
||||
|
||||
# Load from cache
|
||||
cache_files = glob.glob("dataset_cache/*.nc")
|
||||
s2_files = [f for f in cache_files if len(os.path.basename(f)) == 35] # S2 cache filenames usually have length 32 + 3 (.nc)
|
||||
|
||||
if not s2_files:
|
||||
print("[WARNING] Không tìm thấy dữ liệu S2 trong cache! Dùng dummy data.")
|
||||
self._create_dummy()
|
||||
return
|
||||
|
||||
try:
|
||||
print(f"[DATA] Loading real data from {s2_files[0]}")
|
||||
ds = xr.open_dataset(s2_files[0], engine='netcdf4')
|
||||
if 'time' not in ds.dims or len(ds.time) < sequence_length + 1:
|
||||
self._create_dummy()
|
||||
return
|
||||
|
||||
# Tính NDVI: (B08 - B04) / (B08 + B04)
|
||||
b8 = ds['B08'].astype(np.float32)
|
||||
b4 = ds['B04'].astype(np.float32)
|
||||
ndvi = (b8 - b4) / (b8 + b4 + 1e-8)
|
||||
ndvi = ndvi.fillna(0).values # shape: (time, y, x)
|
||||
|
||||
# Lấy 1 pixel trung tâm hoặc toàn bộ ảnh
|
||||
if not self.spatial:
|
||||
# Average pooling over space for 1D time series
|
||||
ndvi = ndvi.mean(axis=(1, 2)) # shape: (time,)
|
||||
for i in range(len(ndvi) - sequence_length):
|
||||
self.data_seqs.append(ndvi[i:i+sequence_length])
|
||||
self.targets.append(ndvi[i+sequence_length])
|
||||
else:
|
||||
# Spatial data for ConvLSTM
|
||||
# Downsample to 64x64 to avoid OOM
|
||||
from skimage.transform import resize
|
||||
T = len(ndvi)
|
||||
ndvi_resized = np.zeros((T, 64, 64))
|
||||
for t in range(T):
|
||||
ndvi_resized[t] = resize(ndvi[t], (64, 64))
|
||||
|
||||
for i in range(T - sequence_length):
|
||||
self.data_seqs.append(ndvi_resized[i:i+sequence_length]) # (seq, 64, 64)
|
||||
self.targets.append(ndvi_resized[i+sequence_length]) # (64, 64)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {e}. Dùng dummy data.")
|
||||
self._create_dummy()
|
||||
|
||||
def _create_dummy(self):
|
||||
T = 20
|
||||
if not self.spatial:
|
||||
ndvi = np.random.rand(T).astype(np.float32)
|
||||
for i in range(T - self.sequence_length):
|
||||
self.data_seqs.append(ndvi[i:i+self.sequence_length])
|
||||
self.targets.append(ndvi[i+self.sequence_length])
|
||||
else:
|
||||
ndvi = np.random.rand(T, 64, 64).astype(np.float32)
|
||||
for i in range(T - self.sequence_length):
|
||||
self.data_seqs.append(ndvi[i:i+self.sequence_length])
|
||||
self.targets.append(ndvi[i+self.sequence_length])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data_seqs)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
x = torch.tensor(self.data_seqs[idx], dtype=torch.float32)
|
||||
y = torch.tensor(self.targets[idx], dtype=torch.float32)
|
||||
|
||||
if not self.spatial:
|
||||
x = x.unsqueeze(1) # (seq_len, features=1)
|
||||
y = y.unsqueeze(0) # (1,)
|
||||
else:
|
||||
x = x.unsqueeze(1) # (seq_len, channels=1, H, W)
|
||||
y = y.unsqueeze(0) # (1, H, W)
|
||||
|
||||
return x, y
|
||||
@@ -0,0 +1,6 @@
|
||||
import geopandas as gpd
|
||||
|
||||
|
||||
def load_data_geo(path: str):
|
||||
gdf = gpd.read_file(path)
|
||||
return gdf
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user