làm mịn các điểm ảnh
This commit is contained in:
+581
-192
@@ -96,14 +96,14 @@ DEFAULT_LABEL_MAPPING = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_LABEL_NAMES = {
|
DEFAULT_LABEL_NAMES = {
|
||||||
"0": "Lua tom",
|
0: "Lua tom",
|
||||||
"1": "Lua",
|
1: "Lua",
|
||||||
"2": "CHN",
|
2: "CHN",
|
||||||
"3": "CLN",
|
3: "CLN",
|
||||||
"4": "TS",
|
4: "TS",
|
||||||
"5": "Song",
|
5: "Song",
|
||||||
"6": "Dat xay dung",
|
6: "Dat xay dung",
|
||||||
"7": "Rung",
|
7: "Rung",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class TrainingConfig(BaseModel):
|
|||||||
resolution: int # 10m hoặc 20m
|
resolution: int # 10m hoặc 20m
|
||||||
|
|
||||||
# Model parameters
|
# Model parameters
|
||||||
model_type: str # xgboost, random_forest, decision_tree, svm, cnn, swin-unet
|
model_type: str # xgboost, random_forest, decision_tree, svm, cnn, swin-unet, mobilenet-lraspp
|
||||||
n_estimators: int
|
n_estimators: int
|
||||||
max_depth: int
|
max_depth: int
|
||||||
learning_rate: float
|
learning_rate: float
|
||||||
@@ -1151,9 +1151,9 @@ 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 PyTorch model (CNN, Swin-UNet, etc.)
|
# Check if it's a PyTorch model (CNN, Swin-UNet, MobileNet, etc.)
|
||||||
is_pytorch_model = hasattr(model, '__class__') and any(
|
is_pytorch_model = hasattr(model, '__class__') and any(
|
||||||
name in model.__class__.__name__ for name in ['CNN', 'SwinUNet']
|
name in model.__class__.__name__ for name in ['CNN', 'SwinUNet', 'MobileNet']
|
||||||
)
|
)
|
||||||
if is_pytorch_model:
|
if is_pytorch_model:
|
||||||
model_class_name = model.__class__.__name__
|
model_class_name = model.__class__.__name__
|
||||||
@@ -1167,43 +1167,78 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
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]
|
||||||
time_range = f"{config.start_date}/{config.end_date}"
|
time_range = f"{config.start_date}/{config.end_date}"
|
||||||
|
|
||||||
|
# ============ CHECK PREDICTION CACHE FIRST ============
|
||||||
|
cached_s2_data = None
|
||||||
|
cache_key = f"{config.min_lon:.2f}_{config.min_lat:.2f}_{config.max_lon:.2f}_{config.max_lat:.2f}"
|
||||||
|
|
||||||
|
# Try to find matching cache
|
||||||
|
cache_dir = Path("prediction_cache")
|
||||||
|
if cache_dir.exists():
|
||||||
|
for cache_file in cache_dir.glob(f"pred_{cache_key}_*.json"):
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
cache_data = json.load(f)
|
||||||
|
|
||||||
|
# Check if cache matches current config
|
||||||
|
if (cache_data.get('start_date') == config.start_date and
|
||||||
|
cache_data.get('end_date') == config.end_date and
|
||||||
|
cache_data.get('resolution') == config.resolution and
|
||||||
|
cache_data.get('data_file')):
|
||||||
|
|
||||||
|
data_file = cache_dir / cache_data['data_file']
|
||||||
|
if data_file.exists():
|
||||||
|
prediction_status["progress"] = "Đang load dữ liệu từ cache..."
|
||||||
|
import joblib
|
||||||
|
cached_s2_data = joblib.load(data_file)
|
||||||
|
print(f"[CACHE HIT] Using cached Sentinel-2 data from {cache_file.name}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CACHE] Error loading cache {cache_file}: {e}")
|
||||||
|
|
||||||
# ============ LOAD SENTINEL-2 DATA ============
|
# ============ LOAD SENTINEL-2 DATA ============
|
||||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
if cached_s2_data is not None:
|
||||||
import pystac_client
|
s2_data = cached_s2_data
|
||||||
import planetary_computer
|
s2_items = [] # Empty list when using cache
|
||||||
from odc.stac import load
|
prediction_status["progress"] = "Đã load dữ liệu từ cache, đang xử lý..."
|
||||||
|
else:
|
||||||
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
|
import pystac_client
|
||||||
|
import planetary_computer
|
||||||
|
from odc.stac import load
|
||||||
|
|
||||||
|
catalog = pystac_client.Client.open(
|
||||||
|
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
||||||
|
modifier=planetary_computer.sign_inplace,
|
||||||
|
)
|
||||||
|
|
||||||
|
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
||||||
|
s2_search = catalog.search(
|
||||||
|
collections=["sentinel-2-l2a"],
|
||||||
|
bbox=bbox,
|
||||||
|
datetime=time_range,
|
||||||
|
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
||||||
|
)
|
||||||
|
s2_items = list(s2_search.items())
|
||||||
|
|
||||||
|
if not s2_items:
|
||||||
|
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
||||||
|
|
||||||
|
s2_items = s2_items[:config.max_scenes]
|
||||||
|
|
||||||
catalog = pystac_client.Client.open(
|
prediction_status["progress"] = f"Đang xử lý dữ liệu Sentinel-2..."
|
||||||
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
|
||||||
modifier=planetary_computer.sign_inplace,
|
|
||||||
)
|
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2..."
|
# Load different bands based on feature mode (skip if using cache)
|
||||||
s2_search = catalog.search(
|
if cached_s2_data is None:
|
||||||
collections=["sentinel-2-l2a"],
|
if feature_mode == 'simple':
|
||||||
bbox=bbox,
|
bands_to_load = ["B04", "B08", "SCL"]
|
||||||
datetime=time_range,
|
else: # temporal or extended
|
||||||
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
|
bands_to_load = ["B02", "B03", "B04", "B08", "B11", "SCL"]
|
||||||
)
|
|
||||||
s2_items = list(s2_search.items())
|
s2_data = load(
|
||||||
|
s2_items,
|
||||||
if not s2_items:
|
bbox=bbox,
|
||||||
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
|
bands=bands_to_load,
|
||||||
|
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||||
s2_items = s2_items[:config.max_scenes]
|
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
|
|
||||||
|
|
||||||
# Load different bands based on feature mode
|
|
||||||
if feature_mode == 'simple':
|
|
||||||
bands_to_load = ["B04", "B08", "SCL"]
|
|
||||||
else: # temporal or extended
|
|
||||||
bands_to_load = ["B02", "B03", "B04", "B08", "B11", "SCL"]
|
|
||||||
|
|
||||||
s2_data = load(
|
|
||||||
s2_items,
|
|
||||||
bbox=bbox,
|
|
||||||
bands=bands_to_load,
|
|
||||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
|
||||||
groupby="solar_day",
|
groupby="solar_day",
|
||||||
resolution=config.resolution
|
resolution=config.resolution
|
||||||
).compute()
|
).compute()
|
||||||
@@ -1245,15 +1280,90 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
prediction_status["progress"] = f"Lỗi load Sentinel-1: {str(e)}, bỏ qua radar features"
|
prediction_status["progress"] = f"Lỗi load Sentinel-1: {str(e)}, bỏ qua radar features"
|
||||||
|
|
||||||
# ============ APPLY CLOUD MASK ============
|
# ============ ADVANCED CLOUD MASKING & REMOVAL ============
|
||||||
prediction_status["progress"] = "Đang xử lý mây..."
|
prediction_status["progress"] = "Đang xử lý mây nâng cao..."
|
||||||
|
|
||||||
|
cloud_coverage_percent = 0
|
||||||
if "SCL" in s2_data:
|
if "SCL" in s2_data:
|
||||||
scl = s2_data["SCL"]
|
scl = s2_data["SCL"]
|
||||||
# SCL values: 3=cloud shadow, 8=cloud medium, 9=cloud high, 10=cirrus
|
|
||||||
cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10)
|
# SCL classification values (Sentinel-2 Scene Classification):
|
||||||
|
# 0: No data, 1: Saturated/Defective, 2: Dark Area Pixels
|
||||||
|
# 3: Cloud shadows, 4: Vegetation, 5: Not vegetated, 6: Water
|
||||||
|
# 7: Unclassified, 8: Cloud medium probability, 9: Cloud high probability
|
||||||
|
# 10: Thin cirrus, 11: Snow/Ice
|
||||||
|
|
||||||
|
# Comprehensive cloud mask (clouds, shadows, cirrus, snow)
|
||||||
|
cloud_mask = (scl == 3) | (scl == 8) | (scl == 9) | (scl == 10) | (scl == 11)
|
||||||
|
|
||||||
|
# Also mask no-data and saturated pixels
|
||||||
|
invalid_mask = (scl == 0) | (scl == 1)
|
||||||
|
full_mask = cloud_mask | invalid_mask
|
||||||
|
|
||||||
|
# Calculate cloud coverage percentage
|
||||||
|
total_pixels = full_mask.size
|
||||||
|
masked_pixels = int(full_mask.sum().values)
|
||||||
|
cloud_coverage_percent = (masked_pixels / total_pixels * 100) if total_pixels > 0 else 0
|
||||||
|
|
||||||
|
print(f"[CLOUD MASK] Cloud coverage: {cloud_coverage_percent:.1f}%")
|
||||||
|
print(f"[CLOUD MASK] Masked pixels: {masked_pixels}/{total_pixels}")
|
||||||
|
|
||||||
|
# Apply mask to all bands
|
||||||
for band in s2_data.data_vars:
|
for band in s2_data.data_vars:
|
||||||
if band != "SCL":
|
if band != "SCL":
|
||||||
s2_data[band] = s2_data[band].where(~cloud_mask)
|
s2_data[band] = s2_data[band].where(~full_mask)
|
||||||
|
|
||||||
|
# ============ CLOUD REMOVAL STRATEGIES ============
|
||||||
|
|
||||||
|
# Strategy 1: Temporal Interpolation (fill gaps between time steps)
|
||||||
|
prediction_status["progress"] = "Đang khử mây bằng temporal interpolation..."
|
||||||
|
for band in s2_data.data_vars:
|
||||||
|
if band != "SCL":
|
||||||
|
# Forward fill then backward fill along time dimension
|
||||||
|
s2_data[band] = s2_data[band].ffill(dim='time').bfill(dim='time')
|
||||||
|
|
||||||
|
print(f"[CLOUD REMOVAL] Applied temporal interpolation")
|
||||||
|
|
||||||
|
# Strategy 2: Median Compositing (if multiple time steps available)
|
||||||
|
if len(s2_data.time) >= 3:
|
||||||
|
prediction_status["progress"] = "Đang tạo median composite để giảm nhiễu mây..."
|
||||||
|
|
||||||
|
# Create median composite for each band
|
||||||
|
for band in s2_data.data_vars:
|
||||||
|
if band != "SCL":
|
||||||
|
# Median reduces cloud noise better than mean
|
||||||
|
median_composite = s2_data[band].median(dim='time', skipna=True)
|
||||||
|
|
||||||
|
# Fill remaining NaN with median
|
||||||
|
s2_data[band] = s2_data[band].fillna(median_composite)
|
||||||
|
|
||||||
|
print(f"[CLOUD REMOVAL] Applied median compositing from {len(s2_data.time)} scenes")
|
||||||
|
|
||||||
|
# Strategy 3: Spatial Interpolation (fill small gaps)
|
||||||
|
prediction_status["progress"] = "Đang khử mây bằng spatial interpolation..."
|
||||||
|
for band in s2_data.data_vars:
|
||||||
|
if band != "SCL":
|
||||||
|
# Use nearest neighbor interpolation for remaining small gaps
|
||||||
|
s2_data[band] = s2_data[band].interpolate_na(dim='x', method='nearest', fill_value='extrapolate')
|
||||||
|
s2_data[band] = s2_data[band].interpolate_na(dim='y', method='nearest', fill_value='extrapolate')
|
||||||
|
|
||||||
|
print(f"[CLOUD REMOVAL] Applied spatial interpolation")
|
||||||
|
|
||||||
|
# Final check: replace any remaining NaN with 0
|
||||||
|
for band in s2_data.data_vars:
|
||||||
|
if band != "SCL":
|
||||||
|
s2_data[band] = s2_data[band].fillna(0)
|
||||||
|
|
||||||
|
print(f"[CLOUD REMOVAL] Completed - all NaN values handled")
|
||||||
|
|
||||||
|
# Quality warning if cloud coverage too high
|
||||||
|
if cloud_coverage_percent > 30:
|
||||||
|
print(f"[WARNING] High cloud coverage ({cloud_coverage_percent:.1f}%) - prediction quality may be affected")
|
||||||
|
prediction_status["progress"] = f"⚠️ Cảnh báo: Độ phủ mây cao ({cloud_coverage_percent:.1f}%)"
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("[WARNING] No SCL band available - skipping cloud masking")
|
||||||
|
prediction_status["progress"] = "⚠️ Không có SCL band - bỏ qua khử mây"
|
||||||
|
|
||||||
# ============ EXTRACT FEATURES ============
|
# ============ EXTRACT FEATURES ============
|
||||||
prediction_status["progress"] = f"Đang trích xuất features (mode={feature_mode})..."
|
prediction_status["progress"] = f"Đang trích xuất features (mode={feature_mode})..."
|
||||||
@@ -1331,6 +1441,17 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
|
|
||||||
pred_shape = (y_size, x_size)
|
pred_shape = (y_size, x_size)
|
||||||
predictions_2d = predictions.reshape(pred_shape)
|
predictions_2d = predictions.reshape(pred_shape)
|
||||||
|
|
||||||
|
# Smooth classification map to reduce salt-and-pepper noise
|
||||||
|
try:
|
||||||
|
from scipy import ndimage
|
||||||
|
smoothed = ndimage.median_filter(predictions_2d, size=3)
|
||||||
|
# Keep invalid/nodata pixels (-1) untouched
|
||||||
|
smoothed[predictions_2d < 0] = -1
|
||||||
|
predictions_2d = smoothed
|
||||||
|
print("[SMOOTH] Applied 3x3 median filter to classification map")
|
||||||
|
except Exception as smooth_err:
|
||||||
|
print(f"[SMOOTH WARNING] Failed to smooth classification map: {smooth_err}")
|
||||||
|
|
||||||
# ============ CREATE OUTPUT ============
|
# ============ CREATE OUTPUT ============
|
||||||
prediction_status["progress"] = "Đang tạo bản đồ phân loại..."
|
prediction_status["progress"] = "Đang tạo bản đồ phân loại..."
|
||||||
@@ -1370,82 +1491,61 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('Agg') # Non-interactive backend
|
matplotlib.use('Agg') # Non-interactive backend
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.patches import Patch
|
||||||
|
|
||||||
# Create a figure with prediction result
|
# Create a figure with prediction result and legend
|
||||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
|
||||||
|
|
||||||
# Plot prediction with colormap
|
# Plot prediction with colormap
|
||||||
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
||||||
ax.set_title(f'Prediction Result - {timestamp}', fontsize=14, fontweight='bold')
|
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
||||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
ax.set_xlabel('X (pixels)', fontsize=11)
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
ax.set_ylabel('Y (pixels)', fontsize=11)
|
||||||
|
|
||||||
# Build mapping from numeric class value -> display label
|
# Build mapping from numeric class value -> display label
|
||||||
class_map = None
|
# ALWAYS use DEFAULT_LABEL_NAMES - it has the correct Vietnamese names
|
||||||
try:
|
class_map = DEFAULT_LABEL_NAMES.copy()
|
||||||
# 1) Try label_encoder (preferred)
|
|
||||||
if label_encoder is not None:
|
print(f"[LEGEND DEBUG] DEFAULT_LABEL_NAMES: {DEFAULT_LABEL_NAMES}")
|
||||||
try:
|
print(f"[LEGEND DEBUG] Model metadata: {model_metadata.get('label_mapping') if isinstance(model_metadata, dict) else 'No metadata'}")
|
||||||
# label_encoder.classes_ may be strings or numbers
|
print(f"[LEGEND DEBUG] Final class_map: {class_map}")
|
||||||
le_classes = list(label_encoder.classes_)
|
|
||||||
# If classes are strings like names, we'll map indices -> names
|
|
||||||
if all(isinstance(x, str) for x in le_classes):
|
|
||||||
class_map = {i: name for i, name in enumerate(le_classes)}
|
|
||||||
else:
|
|
||||||
# If classes are numeric labels matching values, map value->str(value)
|
|
||||||
class_map = {int(v): str(v) for v in le_classes}
|
|
||||||
except Exception:
|
|
||||||
class_map = None
|
|
||||||
except Exception:
|
|
||||||
class_map = None
|
|
||||||
|
|
||||||
# 2) Try model metadata 'class_names' (list ordered by class code)
|
# Get unique classes in prediction to show only relevant legend items
|
||||||
if class_map is None and isinstance(model_metadata, dict):
|
unique_pred_classes = np.unique(predictions_2d)
|
||||||
|
unique_pred_classes = unique_pred_classes[~np.isnan(unique_pred_classes)]
|
||||||
|
|
||||||
|
# Create custom legend with color patches
|
||||||
|
legend_elements = []
|
||||||
|
cmap = plt.cm.get_cmap('tab20')
|
||||||
|
|
||||||
|
for cls_val in sorted(unique_pred_classes):
|
||||||
try:
|
try:
|
||||||
cn = model_metadata.get('class_names')
|
cls_int = int(cls_val)
|
||||||
if isinstance(cn, list):
|
# Get color from colormap (normalize to 0-1 range)
|
||||||
class_map = {i: str(name) for i, name in enumerate(cn)}
|
color = cmap(cls_int / 20.0) # tab20 has 20 colors
|
||||||
except Exception:
|
# Get label name
|
||||||
|
label_name = class_map.get(cls_int, f"Class {cls_int}")
|
||||||
|
# Create patch for legend
|
||||||
|
legend_elements.append(
|
||||||
|
Patch(facecolor=color, edgecolor='black', linewidth=0.5,
|
||||||
|
label=f"{cls_int}: {label_name}")
|
||||||
|
)
|
||||||
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 3) Try invert label_mapping in metadata if exists (name->code)
|
# Add legend outside plot area
|
||||||
if class_map is None and isinstance(model_metadata, dict):
|
if legend_elements:
|
||||||
try:
|
legend = ax.legend(
|
||||||
lm = model_metadata.get('label_mapping') or model_metadata.get('labels')
|
handles=legend_elements,
|
||||||
if isinstance(lm, dict):
|
loc='center left',
|
||||||
# invert mapping: code -> name
|
bbox_to_anchor=(1.02, 0.5),
|
||||||
inv = {}
|
fontsize=10,
|
||||||
for k, v in lm.items():
|
title='Land Classes',
|
||||||
try:
|
title_fontsize=11,
|
||||||
key_int = int(v)
|
framealpha=0.9,
|
||||||
except Exception:
|
edgecolor='black'
|
||||||
continue
|
)
|
||||||
inv[key_int] = str(k)
|
legend.get_title().set_fontweight('bold')
|
||||||
if inv:
|
|
||||||
class_map = inv
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create colorbar
|
|
||||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
||||||
cbar.set_label('Class', rotation=270, labelpad=15)
|
|
||||||
|
|
||||||
# If we have a class_map, set ticks and labels
|
|
||||||
try:
|
|
||||||
if class_map:
|
|
||||||
vals = np.array(sorted(class_map.keys()))
|
|
||||||
cbar.set_ticks(vals)
|
|
||||||
cbar.set_ticklabels([class_map[int(v)] for v in vals])
|
|
||||||
else:
|
|
||||||
# fallback: label numeric ticks from min..max
|
|
||||||
if np.issubdtype(predictions_2d.dtype, np.number):
|
|
||||||
minv = int(np.nanmin(predictions_2d))
|
|
||||||
maxv = int(np.nanmax(predictions_2d))
|
|
||||||
ticks = np.arange(minv, maxv + 1)
|
|
||||||
cbar.set_ticks(ticks)
|
|
||||||
cbar.set_ticklabels([str(t) for t in ticks])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Add grid
|
# Add grid
|
||||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||||
@@ -1491,6 +1591,42 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
print(f"[PREDICTION REPORT ERROR] Failed to generate report: {e}")
|
print(f"[PREDICTION REPORT ERROR] Failed to generate report: {e}")
|
||||||
prediction_status["progress"] = "Hoàn thành! (Không thể tạo báo cáo)"
|
prediction_status["progress"] = "Hoàn thành! (Không thể tạo báo cáo)"
|
||||||
|
|
||||||
|
# Auto-save prediction cache (including Sentinel-2 data)
|
||||||
|
print(f"[DEBUG] Starting cache save process...")
|
||||||
|
print(f"[DEBUG] cached_s2_data is None: {cached_s2_data is None}")
|
||||||
|
print(f"[DEBUG] s2_data type: {type(s2_data)}")
|
||||||
|
print(f"[DEBUG] s2_data is None: {s2_data is None}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_config = {
|
||||||
|
"min_lon": config.min_lon,
|
||||||
|
"min_lat": config.min_lat,
|
||||||
|
"max_lon": config.max_lon,
|
||||||
|
"max_lat": config.max_lat,
|
||||||
|
"start_date": config.start_date,
|
||||||
|
"end_date": config.end_date,
|
||||||
|
"max_scenes": config.max_scenes,
|
||||||
|
"cloud_cover": config.cloud_cover,
|
||||||
|
"resolution": config.resolution,
|
||||||
|
"model_filename": config.model_filename
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[DEBUG] cache_config created: {cache_config}")
|
||||||
|
|
||||||
|
# Save cache with Sentinel-2 data (only if not from cache)
|
||||||
|
data_to_save = None if cached_s2_data is not None else s2_data
|
||||||
|
print(f"[DEBUG] data_to_save is None: {data_to_save is None}")
|
||||||
|
print(f"[DEBUG] Calling save_prediction_cache_sync...")
|
||||||
|
|
||||||
|
result = save_prediction_cache_sync(cache_config, data_to_save)
|
||||||
|
|
||||||
|
print(f"[CACHE] Prediction cache saved: {result.get('message')} (with data: {data_to_save is not None})")
|
||||||
|
print(f"[CACHE] Result: {result}")
|
||||||
|
except Exception as cache_error:
|
||||||
|
print(f"[CACHE ERROR] Failed to auto-save cache: {cache_error}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
prediction_status["end_time"] = dt.now().isoformat()
|
prediction_status["end_time"] = dt.now().isoformat()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1608,6 +1744,216 @@ async def preview_prediction_png(filename: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ PREDICTION CACHE API (Auto-save) ============
|
||||||
|
|
||||||
|
def save_prediction_cache_sync(config: dict, s2_data=None):
|
||||||
|
"""Tự động lưu prediction cache sau khi predict thành công (bao gồm cả dữ liệu Sentinel-2)"""
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Called with s2_data is None: {s2_data is None}")
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Config: {config}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_dir = Path("prediction_cache")
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Cache dir: {cache_dir.absolute()}")
|
||||||
|
cache_dir.mkdir(exist_ok=True)
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Cache dir created/exists")
|
||||||
|
|
||||||
|
# Create cache filename from bbox and timestamp
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
bbox_str = f"{config['min_lon']:.2f}_{config['min_lat']:.2f}_{config['max_lon']:.2f}_{config['max_lat']:.2f}"
|
||||||
|
base_filename = f"pred_{bbox_str}_{timestamp}"
|
||||||
|
config_file = cache_dir / f"{base_filename}.json"
|
||||||
|
data_file = cache_dir / f"{base_filename}_data.joblib"
|
||||||
|
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Config file: {config_file}")
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Data file: {data_file}")
|
||||||
|
|
||||||
|
# Prepare cache data with full metadata
|
||||||
|
cache_data = {
|
||||||
|
"bbox": [config['min_lon'], config['min_lat'], config['max_lon'], config['max_lat']],
|
||||||
|
"min_lon": config['min_lon'],
|
||||||
|
"min_lat": config['min_lat'],
|
||||||
|
"max_lon": config['max_lon'],
|
||||||
|
"max_lat": config['max_lat'],
|
||||||
|
"start_date": config['start_date'],
|
||||||
|
"end_date": config['end_date'],
|
||||||
|
"max_scenes": config['max_scenes'],
|
||||||
|
"cloud_cover": config['cloud_cover'],
|
||||||
|
"resolution": config['resolution'],
|
||||||
|
"model_filename": config.get('model_filename'),
|
||||||
|
"created": timestamp,
|
||||||
|
"type": "prediction_cache",
|
||||||
|
"auto_saved": True,
|
||||||
|
"has_data": s2_data is not None,
|
||||||
|
"data_file": f"{base_filename}_data.joblib" if s2_data is not None else None
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Saving config JSON...")
|
||||||
|
# Save config to JSON file
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cache_data, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Config JSON saved to {config_file}")
|
||||||
|
|
||||||
|
# Save Sentinel-2 data if provided
|
||||||
|
if s2_data is not None:
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] Saving Sentinel-2 data with joblib...")
|
||||||
|
import joblib
|
||||||
|
joblib.dump(s2_data, data_file)
|
||||||
|
data_size_mb = data_file.stat().st_size / 1024 / 1024
|
||||||
|
cache_data['data_size_mb'] = round(data_size_mb, 2)
|
||||||
|
print(f"[CACHE] Saved Sentinel-2 data: {data_size_mb:.2f} MB to {data_file}")
|
||||||
|
else:
|
||||||
|
print(f"[DEBUG save_prediction_cache_sync] No s2_data to save")
|
||||||
|
|
||||||
|
print(f"[CACHE SUCCESS] Cache saved successfully: {base_filename}.json")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Đã lưu cache tự động" + (" (bao gồm dữ liệu Sentinel-2)" if s2_data is not None else ""),
|
||||||
|
"filename": f"{base_filename}.json",
|
||||||
|
"cache": cache_data
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CACHE ERROR save_prediction_cache_sync] Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
@app.get("/api/prediction/cache/list")
|
||||||
|
async def list_prediction_cache():
|
||||||
|
"""Liệt kê các prediction cache đã lưu (bao gồm thông tin về dữ liệu Sentinel-2)"""
|
||||||
|
try:
|
||||||
|
cache_dir = Path("prediction_cache")
|
||||||
|
if not cache_dir.exists():
|
||||||
|
return {"caches": [], "count": 0}
|
||||||
|
|
||||||
|
caches = []
|
||||||
|
for cache_file in cache_dir.glob("pred_*.json"):
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
cache_data = json.load(f)
|
||||||
|
|
||||||
|
# Check if data file exists
|
||||||
|
data_filename = cache_data.get("data_file")
|
||||||
|
has_data = False
|
||||||
|
data_size_mb = 0
|
||||||
|
|
||||||
|
if data_filename:
|
||||||
|
data_file_path = cache_dir / data_filename
|
||||||
|
if data_file_path.exists():
|
||||||
|
has_data = True
|
||||||
|
data_size_mb = round(data_file_path.stat().st_size / 1024 / 1024, 2)
|
||||||
|
|
||||||
|
# Create display name from metadata
|
||||||
|
bbox = cache_data.get("bbox", [])
|
||||||
|
time_range = f"{cache_data.get('start_date', 'N/A')} → {cache_data.get('end_date', 'N/A')}"
|
||||||
|
data_badge = f" [💾 {data_size_mb}MB]" if has_data else " [⚙️ Config only]"
|
||||||
|
display_name = f"[{bbox[0]:.2f},{bbox[1]:.2f}→{bbox[2]:.2f},{bbox[3]:.2f}] {time_range}{data_badge}"
|
||||||
|
|
||||||
|
caches.append({
|
||||||
|
"filename": cache_file.name,
|
||||||
|
"display_name": display_name,
|
||||||
|
"bbox": bbox,
|
||||||
|
"created": cache_data.get("created"),
|
||||||
|
"has_data": has_data,
|
||||||
|
"data_size_mb": data_size_mb,
|
||||||
|
"data_file": data_filename,
|
||||||
|
"config": cache_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading cache {cache_file}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by created time (newest first)
|
||||||
|
caches.sort(key=lambda x: x.get("created", ""), reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"caches": caches,
|
||||||
|
"count": len(caches)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Lỗi khi load cache: {str(e)}")
|
||||||
|
|
||||||
|
@app.get("/api/prediction/cache/load-data/{filename}")
|
||||||
|
async def load_cached_data(filename: str):
|
||||||
|
"""Load Sentinel-2 data từ cache"""
|
||||||
|
try:
|
||||||
|
cache_dir = Path("prediction_cache")
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
# Load config to get data filename
|
||||||
|
config_file = cache_dir / filename
|
||||||
|
if not config_file.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Cache config not found")
|
||||||
|
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
cache_data = json.load(f)
|
||||||
|
|
||||||
|
data_filename = cache_data.get("data_file")
|
||||||
|
if not data_filename:
|
||||||
|
raise HTTPException(status_code=404, detail="No data file in cache")
|
||||||
|
|
||||||
|
data_file = cache_dir / data_filename
|
||||||
|
if not data_file.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Data file not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"has_data": True,
|
||||||
|
"data_file": data_filename,
|
||||||
|
"data_size_mb": round(data_file.stat().st_size / 1024 / 1024, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Lỗi: {str(e)}")
|
||||||
|
|
||||||
|
@app.delete("/api/prediction/cache/delete/{filename}")
|
||||||
|
async def delete_prediction_cache(filename: str):
|
||||||
|
"""Xóa một prediction cache (bao gồm cả file data nếu có)"""
|
||||||
|
try:
|
||||||
|
cache_dir = Path("prediction_cache")
|
||||||
|
cache_file = cache_dir / filename
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
if not cache_file.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Cache not found")
|
||||||
|
|
||||||
|
# Load config to check for data file
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
cache_data = json.load(f)
|
||||||
|
data_filename = cache_data.get("data_file")
|
||||||
|
if data_filename:
|
||||||
|
data_file = cache_dir / data_filename
|
||||||
|
if data_file.exists():
|
||||||
|
data_file.unlink()
|
||||||
|
print(f"[CACHE] Deleted data file: {data_filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CACHE] Error deleting data file: {e}")
|
||||||
|
|
||||||
|
# Delete config file
|
||||||
|
cache_file.unlink()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Đã xóa cache: {filename}"
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Lỗi khi xóa cache: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# ============ DASHBOARD & VISUALIZATION API ============
|
# ============ DASHBOARD & VISUALIZATION API ============
|
||||||
|
|
||||||
@app.get("/api/dashboard/accuracy-trends")
|
@app.get("/api/dashboard/accuracy-trends")
|
||||||
@@ -1956,9 +2302,9 @@ 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 PyTorch model (CNN, Swin-UNet, etc.)
|
# Check if it's a PyTorch model (CNN, Swin-UNet, MobileNet, etc.)
|
||||||
is_pytorch_model = hasattr(model, '__class__') and any(
|
is_pytorch_model = hasattr(model, '__class__') and any(
|
||||||
name in model.__class__.__name__ for name in ['CNN', 'SwinUNet']
|
name in model.__class__.__name__ for name in ['CNN', 'SwinUNet', 'MobileNet']
|
||||||
)
|
)
|
||||||
|
|
||||||
job["progress"] = 20
|
job["progress"] = 20
|
||||||
@@ -2101,59 +2447,54 @@ def run_batch_prediction(job: dict, config: PredictionConfig):
|
|||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.patches import Patch
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
|
||||||
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
|
||||||
ax.set_title(f"{job['name']} - Batch {job['job_id']}", fontsize=14, fontweight='bold')
|
ax.set_title(f"{job['name']} - Batch {job['job_id']}", fontsize=16, fontweight='bold', pad=20)
|
||||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
ax.set_xlabel('X (pixels)', fontsize=11)
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
ax.set_ylabel('Y (pixels)', fontsize=11)
|
||||||
|
|
||||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
# Build mapping - ALWAYS use DEFAULT_LABEL_NAMES
|
||||||
cbar.set_label('Class', rotation=270, labelpad=15)
|
class_map = DEFAULT_LABEL_NAMES.copy()
|
||||||
try:
|
|
||||||
# Build mapping from numeric class value -> label (reuse logic from above)
|
|
||||||
class_map = None
|
|
||||||
if label_encoder is not None:
|
|
||||||
try:
|
|
||||||
le_classes = list(label_encoder.classes_)
|
|
||||||
if all(isinstance(x, str) for x in le_classes):
|
|
||||||
class_map = {i: name for i, name in enumerate(le_classes)}
|
|
||||||
else:
|
|
||||||
class_map = {int(v): str(v) for v in le_classes}
|
|
||||||
except Exception:
|
|
||||||
class_map = None
|
|
||||||
|
|
||||||
if class_map is None and isinstance(model_metadata, dict):
|
# Get unique classes in prediction to show only relevant legend items
|
||||||
cn = model_metadata.get('class_names')
|
unique_pred_classes = np.unique(predictions_2d)
|
||||||
if isinstance(cn, list):
|
unique_pred_classes = unique_pred_classes[~np.isnan(unique_pred_classes)]
|
||||||
class_map = {i: str(name) for i, name in enumerate(cn)}
|
|
||||||
|
# Create custom legend with color patches
|
||||||
if class_map is None and isinstance(model_metadata, dict):
|
legend_elements = []
|
||||||
lm = model_metadata.get('label_mapping') or model_metadata.get('labels')
|
cmap = plt.cm.get_cmap('tab20')
|
||||||
if isinstance(lm, dict):
|
|
||||||
inv = {}
|
for cls_val in sorted(unique_pred_classes):
|
||||||
for k, v in lm.items():
|
try:
|
||||||
try:
|
cls_int = int(cls_val)
|
||||||
key_int = int(v)
|
# Get color from colormap (normalize to 0-1 range)
|
||||||
except Exception:
|
color = cmap(cls_int / 20.0) # tab20 has 20 colors
|
||||||
continue
|
# Get label name
|
||||||
inv[key_int] = str(k)
|
label_name = class_map.get(cls_int, f"Class {cls_int}")
|
||||||
if inv:
|
# Create patch for legend
|
||||||
class_map = inv
|
legend_elements.append(
|
||||||
|
Patch(facecolor=color, edgecolor='black', linewidth=0.5,
|
||||||
if class_map:
|
label=f"{cls_int}: {label_name}")
|
||||||
vals = np.array(sorted(class_map.keys()))
|
)
|
||||||
cbar.set_ticks(vals)
|
except:
|
||||||
cbar.set_ticklabels([class_map[int(v)] for v in vals])
|
pass
|
||||||
else:
|
|
||||||
if np.issubdtype(predictions_2d.dtype, np.number):
|
# Add legend outside plot area
|
||||||
minv = int(np.nanmin(predictions_2d))
|
if legend_elements:
|
||||||
maxv = int(np.nanmax(predictions_2d))
|
legend = ax.legend(
|
||||||
ticks = np.arange(minv, maxv + 1)
|
handles=legend_elements,
|
||||||
cbar.set_ticks(ticks)
|
loc='center left',
|
||||||
cbar.set_ticklabels([str(t) for t in ticks])
|
bbox_to_anchor=(1.02, 0.5),
|
||||||
except Exception:
|
fontsize=10,
|
||||||
pass
|
title='Land Classes',
|
||||||
|
title_fontsize=11,
|
||||||
|
framealpha=0.9,
|
||||||
|
edgecolor='black'
|
||||||
|
)
|
||||||
|
legend.get_title().set_fontweight('bold')
|
||||||
|
|
||||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||||
|
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
@@ -3095,7 +3436,7 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
|||||||
print(f"[PREDICT+NDVI] Features range: [{features_clean.min():.3f}, {features_clean.max():.3f}]")
|
print(f"[PREDICT+NDVI] Features range: [{features_clean.min():.3f}, {features_clean.max():.3f}]")
|
||||||
|
|
||||||
# Check if model is PyTorch/deep learning model and use GPU if available
|
# Check if model is PyTorch/deep learning model and use GPU if available
|
||||||
is_pytorch_model = hasattr(model, '__class__') and ('CNN' in model.__class__.__name__ or 'Swin' in model.__class__.__name__ or 'UNet' in model.__class__.__name__)
|
is_pytorch_model = hasattr(model, '__class__') and ('CNN' in model.__class__.__name__ or 'Swin' in model.__class__.__name__ or 'UNet' in model.__class__.__name__ or 'MobileNet' in model.__class__.__name__)
|
||||||
|
|
||||||
if is_pytorch_model and config.use_gpu:
|
if is_pytorch_model and config.use_gpu:
|
||||||
try:
|
try:
|
||||||
@@ -3152,6 +3493,16 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
|||||||
prediction_raster[valid_mask] = predictions
|
prediction_raster[valid_mask] = predictions
|
||||||
prediction_raster = prediction_raster.reshape(height, width)
|
prediction_raster = prediction_raster.reshape(height, width)
|
||||||
|
|
||||||
|
# Smooth classification map to make output cleaner (reduce speckle)
|
||||||
|
try:
|
||||||
|
from scipy import ndimage
|
||||||
|
smoothed = ndimage.median_filter(prediction_raster, size=3)
|
||||||
|
smoothed[prediction_raster < 0] = -1 # keep nodata
|
||||||
|
prediction_raster = smoothed
|
||||||
|
print("[PREDICT+NDVI][SMOOTH] Applied 3x3 median filter to classification")
|
||||||
|
except Exception as smooth_err:
|
||||||
|
print(f"[PREDICT+NDVI][SMOOTH WARNING] {smooth_err}")
|
||||||
|
|
||||||
# --- CHANGE DETECTION ---
|
# --- CHANGE DETECTION ---
|
||||||
change_summary = None
|
change_summary = None
|
||||||
change_map = None
|
change_map = None
|
||||||
@@ -3282,35 +3633,51 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
|||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.patches import Patch
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 10), dpi=150)
|
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
|
||||||
im = ax.imshow(prediction_raster, cmap='tab20', interpolation='nearest')
|
im = ax.imshow(prediction_raster, cmap='tab20', interpolation='nearest')
|
||||||
ax.set_title(f'Land Classification - {timestamp}', fontsize=14, fontweight='bold')
|
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
|
||||||
ax.set_xlabel('X (pixels)', fontsize=10)
|
ax.set_xlabel('X (pixels)', fontsize=11)
|
||||||
ax.set_ylabel('Y (pixels)', fontsize=10)
|
ax.set_ylabel('Y (pixels)', fontsize=11)
|
||||||
|
|
||||||
# Try to get class labels
|
# Try to get class labels - ALWAYS use DEFAULT_LABEL_NAMES
|
||||||
class_map = None
|
class_map = DEFAULT_LABEL_NAMES.copy()
|
||||||
if label_encoder is not None:
|
|
||||||
|
# Get unique classes in prediction
|
||||||
|
unique_pred_classes = np.unique(prediction_raster)
|
||||||
|
unique_pred_classes = unique_pred_classes[~np.isnan(unique_pred_classes)]
|
||||||
|
unique_pred_classes = unique_pred_classes[unique_pred_classes >= 0] # Exclude -1
|
||||||
|
|
||||||
|
# Create custom legend with color patches
|
||||||
|
legend_elements = []
|
||||||
|
cmap = plt.cm.get_cmap('tab20')
|
||||||
|
|
||||||
|
for cls_val in sorted(unique_pred_classes):
|
||||||
try:
|
try:
|
||||||
le_classes = list(label_encoder.classes_)
|
cls_int = int(cls_val)
|
||||||
if all(isinstance(x, str) for x in le_classes):
|
color = cmap(cls_int / 20.0)
|
||||||
class_map = {i: name for i, name in enumerate(le_classes)}
|
label_name = class_map.get(cls_int, f"Class {cls_int}")
|
||||||
else:
|
legend_elements.append(
|
||||||
class_map = {int(v): str(v) for v in le_classes}
|
Patch(facecolor=color, edgecolor='black', linewidth=0.5,
|
||||||
|
label=f"{cls_int}: {label_name}")
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
# Add legend outside plot area
|
||||||
cbar.set_label('Class', rotation=270, labelpad=15)
|
if legend_elements:
|
||||||
|
legend = ax.legend(
|
||||||
if class_map:
|
handles=legend_elements,
|
||||||
try:
|
loc='center left',
|
||||||
vals = np.array(sorted(class_map.keys()))
|
bbox_to_anchor=(1.02, 0.5),
|
||||||
cbar.set_ticks(vals)
|
fontsize=10,
|
||||||
cbar.set_ticklabels([class_map[int(v)] for v in vals])
|
title='Land Classes',
|
||||||
except:
|
title_fontsize=11,
|
||||||
pass
|
framealpha=0.9,
|
||||||
|
edgecolor='black'
|
||||||
|
)
|
||||||
|
legend.get_title().set_fontweight('bold')
|
||||||
|
|
||||||
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
|
||||||
|
|
||||||
@@ -3336,6 +3703,28 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
|
|||||||
class_distribution = {
|
class_distribution = {
|
||||||
int(cls): int(count) for cls, count in zip(unique_classes, counts)
|
int(cls): int(count) for cls, count in zip(unique_classes, counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-save prediction cache (Sentinel-2 data + metadata) for this NDVI workflow
|
||||||
|
try:
|
||||||
|
cache_config = {
|
||||||
|
"min_lon": config.min_lon,
|
||||||
|
"min_lat": config.min_lat,
|
||||||
|
"max_lon": config.max_lon,
|
||||||
|
"max_lat": config.max_lat,
|
||||||
|
"start_date": config.start_date,
|
||||||
|
"end_date": config.end_date,
|
||||||
|
"max_scenes": config.max_scenes,
|
||||||
|
"cloud_cover": config.cloud_cover,
|
||||||
|
"resolution": config.resolution,
|
||||||
|
"model_filename": config.model_filename
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always save data for predict_with_ndvi (so lần sau không phải tải lại)
|
||||||
|
data_to_save = data
|
||||||
|
cache_result = save_prediction_cache_sync(cache_config, data_to_save)
|
||||||
|
print(f"[PREDICT+NDVI][CACHE] Saved cache: {cache_result.get('filename')} (with data: {data_to_save is not None})")
|
||||||
|
except Exception as cache_exc:
|
||||||
|
print(f"[PREDICT+NDVI][CACHE ERROR] {cache_exc}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -3526,7 +3915,7 @@ async def ndvi_predict_timeseries(config: NDVIPredictionConfig):
|
|||||||
print(f" Expected features: {n_features_expected}")
|
print(f" Expected features: {n_features_expected}")
|
||||||
|
|
||||||
# Check if PyTorch model for GPU
|
# Check if PyTorch model for GPU
|
||||||
is_pytorch_model = hasattr(model, 'forward') or str(type(model).__name__) in ['SwinUnet', 'CNN']
|
is_pytorch_model = hasattr(model, 'forward') or str(type(model).__name__) in ['SwinUnet', 'CNN', 'MobileNetLRASPPClassifier']
|
||||||
|
|
||||||
if is_pytorch_model and config.use_gpu:
|
if is_pytorch_model and config.use_gpu:
|
||||||
import torch
|
import torch
|
||||||
|
|||||||
@@ -353,6 +353,20 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<!-- Prediction Cache Section -->
|
||||||
|
<div class="section" style="margin-top: 20px;">
|
||||||
|
<h3>💾 Cache Predictions (Tự động lưu)</h3>
|
||||||
|
<p style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Cache được tự động tạo sau mỗi lần predict thành công</p>
|
||||||
|
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||||
|
<select id="predCacheSelect" style="flex: 1; padding: 10px; border: 2px solid #e0e0e0; border-radius: 5px;">
|
||||||
|
<option value="">-- Chọn cache để phục hồi cấu hình --</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="loadPredictionCache()" class="btn btn-success">📂 Phục hồi</button>
|
||||||
|
<button onclick="refreshPredCache()" class="btn btn-secondary" title="Làm mới danh sách">🔄</button>
|
||||||
|
</div>
|
||||||
|
<div id="predCacheList" style="max-height: 200px; overflow-y: auto; background: white; border-radius: 5px; padding: 10px;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Selection -->
|
<!-- Model Selection -->
|
||||||
@@ -1174,6 +1188,162 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ PREDICTION CACHE FUNCTIONS (Auto-save) ============
|
||||||
|
|
||||||
|
// Load prediction cache list
|
||||||
|
async function loadPredCacheList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/prediction/cache/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById('predCacheSelect');
|
||||||
|
const listDiv = document.getElementById('predCacheList');
|
||||||
|
|
||||||
|
select.innerHTML = '<option value="">-- Chọn cache để phục hồi cấu hình --</option>';
|
||||||
|
listDiv.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.caches && data.caches.length > 0) {
|
||||||
|
data.caches.forEach(cache => {
|
||||||
|
// Add to select dropdown
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = cache.filename;
|
||||||
|
option.textContent = cache.display_name;
|
||||||
|
option.dataset.config = JSON.stringify(cache.config);
|
||||||
|
select.appendChild(option);
|
||||||
|
|
||||||
|
// Add to list
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.style.cssText = 'background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; border-left: 3px solid #4facfe;';
|
||||||
|
const cfg = cache.config;
|
||||||
|
item.innerHTML = `
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<strong style="color: #667eea;">📍 [${cfg.bbox[0].toFixed(2)}, ${cfg.bbox[1].toFixed(2)} → ${cfg.bbox[2].toFixed(2)}, ${cfg.bbox[3].toFixed(2)}]</strong><br>
|
||||||
|
<small style="color: #666;">📅 ${cfg.start_date} → ${cfg.end_date}</small><br>
|
||||||
|
<small style="color: #999;">🔧 Resolution: ${cfg.resolution}m | Scenes: ${cfg.max_scenes} | Cloud: ${cfg.cloud_cover}%</small><br>
|
||||||
|
<small style="color: #999;">⏰ ${cache.created || 'N/A'}</small>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 5px;">
|
||||||
|
<button onclick="applyPredCache('${cache.filename}')" class="btn btn-success" style="padding: 5px 10px; font-size: 0.9em;">📂</button>
|
||||||
|
<button onclick="deletePredCache('${cache.filename}')" class="btn btn-secondary" style="padding: 5px 10px; font-size: 0.9em;">🗑️</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
listDiv.appendChild(item);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
listDiv.innerHTML = '<p style="color: #999; text-align: center; padding: 20px;">Chưa có cache nào. Cache sẽ tự động được tạo sau khi predict thành công.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading prediction cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh prediction cache list
|
||||||
|
function refreshPredCache() {
|
||||||
|
loadPredCacheList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply prediction cache from filename
|
||||||
|
function applyPredCache(filename) {
|
||||||
|
const select = document.getElementById('predCacheSelect');
|
||||||
|
|
||||||
|
// Find option by filename
|
||||||
|
for (let i = 0; i < select.options.length; i++) {
|
||||||
|
if (select.options[i].value === filename) {
|
||||||
|
select.selectedIndex = i;
|
||||||
|
loadPredictionCache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load prediction cache and restore all parameters
|
||||||
|
function loadPredictionCache() {
|
||||||
|
const select = document.getElementById('predCacheSelect');
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
|
||||||
|
if (!selectedOption || !selectedOption.value || !selectedOption.dataset.config) {
|
||||||
|
alert('⚠️ Vui lòng chọn cache từ danh sách!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(selectedOption.dataset.config);
|
||||||
|
|
||||||
|
// Update selectedBbox
|
||||||
|
selectedBbox = {
|
||||||
|
min_lon: config.min_lon,
|
||||||
|
min_lat: config.min_lat,
|
||||||
|
max_lon: config.max_lon,
|
||||||
|
max_lat: config.max_lat
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
|
||||||
|
|
||||||
|
// Update ALL form fields
|
||||||
|
if (config.start_date) document.getElementById('predStartDate').value = config.start_date;
|
||||||
|
if (config.end_date) document.getElementById('predEndDate').value = config.end_date;
|
||||||
|
if (config.max_scenes) document.getElementById('predMaxScenes').value = config.max_scenes;
|
||||||
|
if (config.cloud_cover) document.getElementById('predCloudCover').value = config.cloud_cover;
|
||||||
|
if (config.resolution) document.getElementById('predResolution').value = config.resolution;
|
||||||
|
|
||||||
|
// Select model if available
|
||||||
|
if (config.model_filename) {
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
for (let i = 0; i < modelSelect.options.length; i++) {
|
||||||
|
if (modelSelect.options[i].value === config.model_filename) {
|
||||||
|
modelSelect.selectedIndex = i;
|
||||||
|
updateModelInfo();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw rectangle on map
|
||||||
|
const bbox = config.bbox;
|
||||||
|
const bounds = [[bbox[1], bbox[0]], [bbox[3], bbox[2]]];
|
||||||
|
|
||||||
|
// Remove previous rectangle
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
|
||||||
|
// Add new rectangle
|
||||||
|
const rectangle = L.rectangle(bounds, {
|
||||||
|
color: '#4facfe',
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
|
||||||
|
// Fit map to bounds
|
||||||
|
map.fitBounds(bounds, { padding: [50, 50] });
|
||||||
|
|
||||||
|
alert(`✅ Đã phục hồi cấu hình từ cache!\n\n📍 Bbox: [${bbox.map(n => n.toFixed(4)).join(', ')}]\n📅 Time: ${config.start_date} → ${config.end_date}\n🔧 Resolution: ${config.resolution}m, Scenes: ${config.max_scenes}, Cloud: ${config.cloud_cover}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete prediction cache
|
||||||
|
async function deletePredCache(filename) {
|
||||||
|
if (!confirm(`Bạn có chắc muốn xóa cache này?\n\nFile: ${filename}`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/prediction/cache/delete/${filename}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.message}`);
|
||||||
|
loadPredCacheList(); // Reload list
|
||||||
|
} else {
|
||||||
|
alert(`❌ Lỗi: ${result.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting cache:', error);
|
||||||
|
alert(`❌ Lỗi khi xóa cache: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply cache preset - auto fill bbox and other params
|
// Apply cache preset - auto fill bbox and other params
|
||||||
function applyCachePreset() {
|
function applyCachePreset() {
|
||||||
const selectValue = document.getElementById('cacheSelect').value;
|
const selectValue = document.getElementById('cacheSelect').value;
|
||||||
@@ -1698,6 +1868,7 @@
|
|||||||
loadModels();
|
loadModels();
|
||||||
loadPredictionsList();
|
loadPredictionsList();
|
||||||
loadCacheList();
|
loadCacheList();
|
||||||
|
loadPredCacheList(); // Load prediction cache list
|
||||||
loadPredProvinces(); // Load provinces list
|
loadPredProvinces(); // Load provinces list
|
||||||
loadNDVIProvinces(); // Load NDVI provinces list
|
loadNDVIProvinces(); // Load NDVI provinces list
|
||||||
loadNDVIModels(); // Load models for NDVI
|
loadNDVIModels(); // Load models for NDVI
|
||||||
|
|||||||
+209
-7
@@ -254,6 +254,96 @@ class SwinUNetClassifier(nn.Module):
|
|||||||
return np.mean(predictions == y)
|
return np.mean(predictions == y)
|
||||||
|
|
||||||
|
|
||||||
|
# MobileNetV3 + LR-ASPP Classifier
|
||||||
|
class MobileNetLRASPPClassifier(nn.Module):
|
||||||
|
"""
|
||||||
|
MobileNetV3 backbone with LR-ASPP (Lite Reduced Atrous Spatial Pyramid Pooling) for semantic segmentation
|
||||||
|
Lightweight architecture optimized for efficiency and speed
|
||||||
|
"""
|
||||||
|
def __init__(self, n_features, n_classes):
|
||||||
|
super(MobileNetLRASPPClassifier, self).__init__()
|
||||||
|
self.n_features = n_features
|
||||||
|
self.n_classes = n_classes
|
||||||
|
|
||||||
|
# Feature extraction layers (MobileNetV3-inspired)
|
||||||
|
self.feature_extractor = nn.Sequential(
|
||||||
|
nn.Linear(n_features, 128),
|
||||||
|
nn.BatchNorm1d(128),
|
||||||
|
nn.ReLU(inplace=True),
|
||||||
|
nn.Dropout(0.2),
|
||||||
|
|
||||||
|
nn.Linear(128, 256),
|
||||||
|
nn.BatchNorm1d(256),
|
||||||
|
nn.ReLU(inplace=True),
|
||||||
|
nn.Dropout(0.3),
|
||||||
|
|
||||||
|
nn.Linear(256, 512),
|
||||||
|
nn.BatchNorm1d(512),
|
||||||
|
nn.ReLU(inplace=True),
|
||||||
|
nn.Dropout(0.3),
|
||||||
|
)
|
||||||
|
|
||||||
|
# LR-ASPP head (simplified for feature vectors)
|
||||||
|
# Branch 1: Global average pooling
|
||||||
|
self.global_pool = nn.AdaptiveAvgPool1d(1)
|
||||||
|
self.global_conv = nn.Sequential(
|
||||||
|
nn.Linear(512, 128),
|
||||||
|
nn.ReLU(inplace=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Branch 2: 1x1 convolution equivalent
|
||||||
|
self.branch_conv = nn.Sequential(
|
||||||
|
nn.Linear(512, 128),
|
||||||
|
nn.BatchNorm1d(128),
|
||||||
|
nn.ReLU(inplace=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fusion and classification
|
||||||
|
self.classifier = nn.Sequential(
|
||||||
|
nn.Linear(256, 128), # 128 from global + 128 from branch
|
||||||
|
nn.BatchNorm1d(128),
|
||||||
|
nn.ReLU(inplace=True),
|
||||||
|
nn.Dropout(0.4),
|
||||||
|
nn.Linear(128, n_classes)
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
# x shape: (batch, n_features)
|
||||||
|
features = self.feature_extractor(x)
|
||||||
|
|
||||||
|
# LR-ASPP head
|
||||||
|
# Branch 1: Global pooling
|
||||||
|
global_feat = self.global_pool(features.unsqueeze(-1)).squeeze(-1)
|
||||||
|
global_feat = self.global_conv(global_feat)
|
||||||
|
|
||||||
|
# Branch 2: Direct features
|
||||||
|
branch_feat = self.branch_conv(features)
|
||||||
|
|
||||||
|
# Concatenate branches
|
||||||
|
fused = torch.cat([global_feat, branch_feat], dim=1)
|
||||||
|
|
||||||
|
# Classification
|
||||||
|
output = self.classifier(fused)
|
||||||
|
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
|
||||||
@@ -1055,16 +1145,128 @@ def train_model(
|
|||||||
model = model.cpu()
|
model = model.cpu()
|
||||||
model.device_used = str(device)
|
model.device_used = str(device)
|
||||||
|
|
||||||
|
elif model_type == 'mobilenet-lraspp':
|
||||||
|
if not PYTORCH_AVAILABLE:
|
||||||
|
raise ImportError("PyTorch is required for MobileNetV3 + LR-ASPP. 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 MobileNetV3 + LR-ASPP model on {device}...", 75)
|
||||||
|
|
||||||
|
model = MobileNetLRASPPClassifier(n_features, n_classes).to(device)
|
||||||
|
|
||||||
|
# Convert to PyTorch tensors
|
||||||
|
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=64, shuffle=True) # Larger batch for efficiency
|
||||||
|
|
||||||
|
# Calculate class weights for imbalanced data
|
||||||
|
class_counts = np.bincount(y_train)
|
||||||
|
class_weights = 1.0 / (class_counts + 1e-6)
|
||||||
|
class_weights = class_weights / class_weights.sum() * len(class_counts)
|
||||||
|
class_weights_tensor = torch.FloatTensor(class_weights).to(device)
|
||||||
|
|
||||||
|
print(f"[MOBILENET] Class distribution: {class_counts}")
|
||||||
|
print(f"[MOBILENET] Class weights: {class_weights}")
|
||||||
|
|
||||||
|
# Loss with class weights
|
||||||
|
criterion = nn.CrossEntropyLoss(weight=class_weights_tensor)
|
||||||
|
optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=0.0001)
|
||||||
|
|
||||||
|
# LR scheduler
|
||||||
|
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)
|
||||||
|
|
||||||
|
# Early stopping
|
||||||
|
best_val_loss = float('inf')
|
||||||
|
patience = 10
|
||||||
|
patience_counter = 0
|
||||||
|
|
||||||
|
# Train MobileNetV3 + LR-ASPP
|
||||||
|
update_status("Training MobileNetV3 + LR-ASPP model with PyTorch...", 80)
|
||||||
|
epochs = min(60, n_estimators // 2)
|
||||||
|
|
||||||
|
# Validation dataset
|
||||||
|
val_dataset = TensorDataset(X_test_tensor, y_test_tensor)
|
||||||
|
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
|
||||||
|
|
||||||
|
model.train()
|
||||||
|
for epoch in range(epochs):
|
||||||
|
# Training phase
|
||||||
|
model.train()
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Gradient clipping
|
||||||
|
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
|
||||||
|
|
||||||
|
optimizer.step()
|
||||||
|
epoch_loss += loss.item()
|
||||||
|
|
||||||
|
# Validation phase
|
||||||
|
model.eval()
|
||||||
|
val_loss = 0.0
|
||||||
|
correct = 0
|
||||||
|
total = 0
|
||||||
|
with torch.no_grad():
|
||||||
|
for batch_X, batch_y in val_loader:
|
||||||
|
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||||
|
outputs = model(batch_X)
|
||||||
|
loss = criterion(outputs, batch_y)
|
||||||
|
val_loss += loss.item()
|
||||||
|
|
||||||
|
_, predicted = torch.max(outputs, 1)
|
||||||
|
total += batch_y.size(0)
|
||||||
|
correct += (predicted == batch_y).sum().item()
|
||||||
|
|
||||||
|
avg_train_loss = epoch_loss / len(train_loader)
|
||||||
|
avg_val_loss = val_loss / len(val_loader)
|
||||||
|
val_acc = 100 * correct / total
|
||||||
|
|
||||||
|
# Update learning rate
|
||||||
|
scheduler.step(avg_val_loss)
|
||||||
|
lr = optimizer.param_groups[0]['lr']
|
||||||
|
|
||||||
|
if (epoch + 1) % 5 == 0:
|
||||||
|
update_status(f"MobileNet Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%, LR: {lr:.6f}", 80 + (epoch / epochs) * 10)
|
||||||
|
print(f"[MOBILENET] Epoch {epoch+1}/{epochs} - Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%")
|
||||||
|
|
||||||
|
# Early stopping
|
||||||
|
if avg_val_loss < best_val_loss:
|
||||||
|
best_val_loss = avg_val_loss
|
||||||
|
patience_counter = 0
|
||||||
|
else:
|
||||||
|
patience_counter += 1
|
||||||
|
if patience_counter >= patience:
|
||||||
|
print(f"[MOBILENET] Early stopping at epoch {epoch+1} (best val loss: {best_val_loss:.4f})")
|
||||||
|
update_status(f"MobileNet early stopped at epoch {epoch+1}", 90)
|
||||||
|
break
|
||||||
|
|
||||||
|
model = model.cpu()
|
||||||
|
model.device_used = str(device)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn, swin-unet")
|
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn, swin-unet, mobilenet-lraspp")
|
||||||
|
|
||||||
# Fit non-neural-network models
|
# Fit non-neural-network models
|
||||||
if model_type not in ['cnn', 'swin-unet']:
|
if model_type not in ['cnn', 'swin-unet', 'mobilenet-lraspp']:
|
||||||
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 in ['cnn', 'swin-unet']:
|
if model_type in ['cnn', 'swin-unet', 'mobilenet-lraspp']:
|
||||||
# PyTorch models 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)
|
||||||
@@ -1111,10 +1313,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', 'swin-unet'] else None,
|
"n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn', 'swin-unet', 'mobilenet-lraspp'] else None,
|
||||||
"max_depth": max_depth if model_type not in ['cnn', 'swin-unet'] else None,
|
"max_depth": max_depth if model_type not in ['cnn', 'swin-unet', 'mobilenet-lraspp'] else None,
|
||||||
"learning_rate": learning_rate if model_type in ['xgboost', 'swin-unet'] else None,
|
"learning_rate": learning_rate if model_type in ['xgboost', 'swin-unet', 'mobilenet-lraspp'] 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),
|
"epochs": min(50, n_estimators // 2) if model_type == 'cnn' else (min(60, n_estimators // 2) if model_type in ['swin-unet', 'mobilenet-lraspp'] 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,
|
||||||
|
|||||||
+14
-3
@@ -507,6 +507,7 @@
|
|||||||
<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>
|
<option value="swin-unet">🌟 Swin-UNet (Transformer + U-Net, Độ chính xác cao, Hỗ trợ GPU)</option>
|
||||||
|
<option value="mobilenet-lraspp">📱 MobileNetV3 + LR-ASPP (Nhẹ, Nhanh, Semantic Segmentation, 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;">
|
||||||
@@ -1399,7 +1400,8 @@
|
|||||||
'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'
|
'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',
|
||||||
|
'mobilenet-lraspp': '✓ MobileNetV3 + LR-ASPP: Kiến trúc nhẹ cho semantic segmentation, nhanh, hiệu quả, phù hợp edge devices, hỗ trợ GPU'
|
||||||
};
|
};
|
||||||
|
|
||||||
desc.textContent = descriptions[modelType];
|
desc.textContent = descriptions[modelType];
|
||||||
@@ -1437,10 +1439,19 @@
|
|||||||
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.0005):';
|
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.0005):';
|
||||||
document.getElementById('learningRate').value = 0.0005;
|
document.getElementById('learningRate').value = 0.0005;
|
||||||
useGpuGroup.style.display = ''; // Show GPU option for Swin-UNet
|
useGpuGroup.style.display = ''; // Show GPU option for Swin-UNet
|
||||||
|
} else if (modelType === 'mobilenet-lraspp') {
|
||||||
|
// MobileNetV3 + LR-ASPP 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 = 60;
|
||||||
|
learningRateGroup.style.display = ''; // Show learning rate for MobileNet
|
||||||
|
document.querySelector('#learningRateGroup label').textContent = 'Learning Rate (mặc định: 0.001):';
|
||||||
|
document.getElementById('learningRate').value = 0.001;
|
||||||
|
useGpuGroup.style.display = ''; // Show GPU option for MobileNet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset n_estimators label for non-CNN/non-Swin-UNet
|
// Reset n_estimators label for non-CNN/non-Swin-UNet/non-MobileNet
|
||||||
if (modelType !== 'cnn' && modelType !== 'swin-unet' && modelType !== 'decision_tree' && modelType !== 'svm') {
|
if (modelType !== 'cnn' && modelType !== 'swin-unet' && modelType !== 'mobilenet-lraspp' && modelType !== 'decision_tree' && modelType !== 'svm') {
|
||||||
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
|
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user