làm mịn các điểm ảnh

This commit is contained in:
Victor Phan
2026-01-06 12:25:42 +07:00
parent 2b308ddb78
commit 612fe1bb88
4 changed files with 975 additions and 202 deletions
+581 -192
View File
@@ -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
+171
View File
@@ -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
View File
@@ -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
View File
@@ -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:';
} }
} }