refactor: reorganize project structure by moving core modules and update import paths in API server

This commit is contained in:
2026-07-18 01:24:30 +07:00
parent abab846884
commit a82b2f6fa5
155 changed files with 25 additions and 370 deletions
+91
View File
@@ -0,0 +1,91 @@
import json
import glob
import os
import sys
# Đảm bảo import được new_import_ODC
sys.path.insert(0, os.getcwd())
import new_import_ODC
from new_import_ODC import load_data, load_sen1
NOTEBOOKS_TO_RUN = [
"01.train_ODC.ipynb",
"01.train_ODC_XGBoost.ipynb",
"02.predict_ODC.ipynb",
"new_train.ipynb"
]
def extract_params(nb_file):
params = {}
try:
with open(nb_file, 'r', encoding='utf-8') as f:
nb = json.load(f)
for cell in nb.get('cells', []):
if cell.get('cell_type') == 'code':
source = cell.get('source', [])
if isinstance(source, list):
source_code = "".join(source)
else:
source_code = source
# Phân tích các dòng
for line in source_code.split('\n'):
line = line.strip()
if line.startswith('date_range = '):
# Lấy giá trị của date_range
try:
val = eval(line.split('=', 1)[1].strip())
params['date_range'] = val
except: pass
elif line.startswith('longtitude_range = '):
try:
val = eval(line.split('=', 1)[1].strip())
params['longtitude_range'] = val
except: pass
elif line.startswith('latitude_range = '):
try:
val = eval(line.split('=', 1)[1].strip())
params['latitude_range'] = val
except: pass
elif line.startswith('time_range = '):
try:
val = eval(line.split('=', 1)[1].strip())
params['time_range'] = val
except: pass
except Exception as e:
print(f"Error reading {nb_file}: {e}")
return params
print("Starting to cache data for all notebooks...")
for nb_file in NOTEBOOKS_TO_RUN:
if os.path.exists(nb_file):
params = extract_params(nb_file)
if 'date_range' in params and 'longtitude_range' in params and 'latitude_range' in params:
date_range = params['date_range']
lon_range = params['longtitude_range']
lat_range = params['latitude_range']
print(f"\n--- Caching for {nb_file} ---")
print(f"Date: {date_range}, Lon: {lon_range}, Lat: {lat_range}")
# Caching Sentinel-2
print("Loading Sentinel-2 (load_data)...")
try:
load_data(None, date_range, lon_range, lat_range)
except Exception as e:
print(f"Failed Sentinel-2: {e}")
# Caching Sentinel-1
time_range = f"{date_range[0]}/{date_range[1]}"
bbox = [lon_range[0], lat_range[0], lon_range[1], lat_range[1]]
print("Loading Sentinel-1 (load_sen1)...")
try:
load_sen1(bbox, time_range)
except Exception as e:
print(f"Failed Sentinel-1: {e}")
else:
print(f"\nSkipped {nb_file}: Could not find all parameters.")
print("\nDone caching all data!")
+81
View File
@@ -0,0 +1,81 @@
"""
Tạo metadata cho model_odc.joblib (legacy model)
"""
import json
from pathlib import Path
# Metadata cho model_odc.joblib
# Model này là GridSearchCV Pipeline với 39 features (temporal mode)
# Features: NDVI time series + NDWI time series + NDBI time series + radar features
# Calculate feature names for temporal mode with 12 timesteps
# (12 NDVI + 12 NDWI + 12 NDBI + 3 radar = 39 features)
n_timesteps = 12
feature_names = []
# NDVI time series
for t in range(n_timesteps):
feature_names.append(f"NDVI_t{t+1}")
# NDWI time series
for t in range(n_timesteps):
feature_names.append(f"NDWI_t{t+1}")
# NDBI time series
for t in range(n_timesteps):
feature_names.append(f"NDBI_t{t+1}")
# Radar features
feature_names.extend(["VH_db_mean", "VV_db_mean", "VH_VV_ratio"])
metadata = {
"timestamp": "2025-12-20T10:00:00",
"data_source": "Unknown (Legacy model)",
"collections": ["sentinel-2-l2a", "sentinel-1-rtc"],
"features": feature_names,
"feature_mode": "temporal", # IMPORTANT: temporal mode with 39 features
"training_samples": None,
"testing_samples": None,
"test_size": 0.2,
"train_accuracy": None,
"test_accuracy": None,
"model_type": "random_forest", # GridSearchCV with RandomForest
"device": "cpu",
"n_estimators": 100,
"max_depth": None,
"learning_rate": None,
"cnn_epochs": None,
"n_features": 39, # GridSearchCV expects 39 features!
"n_classes": 8,
"class_names": [
"Lua tom", # 0
"Lua", # 1
"CHN", # 2
"CLN", # 3
"TS", # 4
"Song", # 5
"Dat xay dung", # 6
"Rung" # 7
],
"classification_report": None,
"confusion_matrix": None,
"bbox": None,
"time_range": None,
"resolution": 10,
"notes": "Legacy GridSearchCV Pipeline model with 39 temporal features (12 timesteps each for NDVI/NDWI/NDBI + 3 radar features). Requires temporal mode feature extraction."
}
# Save metadata
model_train_dir = Path("model_train")
metadata_file = model_train_dir / "model_odc_info.json"
print("Creating metadata for model_odc.joblib...")
print(f"Saving to: {metadata_file}")
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
print("✅ Metadata created successfully!")
print("\nMetadata content:")
print(json.dumps(metadata, indent=2))
+228
View File
@@ -0,0 +1,228 @@
import os
import gc
import json
import joblib
import numpy as np
import pandas as pd
import geopandas as gpd
import xarray as xr
from tqdm import tqdm
from joblib import Parallel, delayed
import pystac_client
import planetary_computer
import odc.stac
from shapely.geometry import Point, shape
from pyproj import Transformer
import warnings
warnings.filterwarnings('ignore')
from core.cloud_removal import DeepInpaintingStrategy
def get_s2_items(bbox, time_range):
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
)
items = list(search.items())
print(f"Found {len(items)} Sentinel-2 scenes")
return items
def get_s1_items(bbox, time_range):
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
items = list(search.items())
print(f"Found {len(items)} Sentinel-1 scenes")
return items
def process_point_s1_s2(idx, row, s2_items_dicts, s1_items_dicts, patch_size=16):
try:
import pystac
import odc.stac
import planetary_computer
from shapely.geometry import Point, shape
from pyproj import Transformer
s2_items = [pystac.Item.from_dict(d) for d in s2_items_dicts]
s1_items = [pystac.Item.from_dict(d) for d in s1_items_dicts]
x_coord = row['geometry'].x
y_coord = row['geometry'].y
transformer = Transformer.from_crs("epsg:32648", "epsg:4326", always_xy=True)
lon, lat = transformer.transform(x_coord, y_coord)
point = Point(lon, lat)
# --- SENTINEL-2 ---
filtered_s2 = [item for item in s2_items if shape(item.geometry).contains(point)]
if not filtered_s2: return None
filtered_s2 = [planetary_computer.sign(item) for item in filtered_s2][:10]
patch_s2 = odc.stac.load(
filtered_s2,
bands=["B02", "B03", "B04", "B08", "SCL"],
x=(x_coord - 100, x_coord + 100),
y=(y_coord - 100, y_coord + 100),
crs="EPSG:32648",
resolution=10,
patch_url=planetary_computer.sign,
fail_on_error=False
).compute()
b2_sums = patch_s2["B02"].sum(dim=["x", "y"])
valid_times = b2_sums > 0
patch_s2 = patch_s2.isel(time=valid_times)
if len(patch_s2.time) == 0: return None
patch_s2 = patch_s2.isel(time=slice(0, min(4, len(patch_s2.time))))
if "SCL" not in patch_s2 or "B02" not in patch_s2: return None
if patch_s2.dims['x'] < patch_size or patch_s2.dims['y'] < patch_size: return None
patch_s2 = patch_s2.isel(x=slice(0, patch_size), y=slice(0, patch_size))
# --- SENTINEL-1 ---
filtered_s1 = [item for item in s1_items if shape(item.geometry).contains(point)]
if not filtered_s1: return None
filtered_s1 = [planetary_computer.sign(item) for item in filtered_s1][:10]
patch_s1 = odc.stac.load(
filtered_s1,
bands=["vv", "vh"],
x=(x_coord - 100, x_coord + 100),
y=(y_coord - 100, y_coord + 100),
crs="EPSG:32648",
resolution=10,
patch_url=planetary_computer.sign,
fail_on_error=False
).compute()
vv_sums = patch_s1["vv"].sum(dim=["x", "y"])
valid_s1_times = vv_sums > 0
patch_s1 = patch_s1.isel(time=valid_s1_times)
if len(patch_s1.time) == 0: return None
# take up to 4 timesteps to match S2
patch_s1 = patch_s1.isel(time=slice(0, min(4, len(patch_s1.time))))
if patch_s1.dims['x'] < patch_size or patch_s1.dims['y'] < patch_size: return None
patch_s1 = patch_s1.isel(x=slice(0, patch_size), y=slice(0, patch_size))
return {
'patch_s2': patch_s2,
'patch_s1': patch_s1,
'label': row['HT_code'] - 1
}
except Exception as e:
return None
def extract_fusion_patches(s2_items, s1_items, gdf, patch_size=16):
print(f"Extracting S1+S2 Fusion patches for {len(gdf)} points using 8 parallel jobs...")
s2_items_dicts = [item.to_dict() for item in s2_items]
s1_items_dicts = [item.to_dict() for item in s1_items]
results = Parallel(n_jobs=8, backend="loky")(
delayed(process_point_s1_s2)(idx, row, s2_items_dicts, s1_items_dicts, patch_size)
for idx, row in tqdm(gdf.iterrows(), total=len(gdf), desc="Downloading S1+S2 Patches")
)
X = []
y = []
cloud_remover = DeepInpaintingStrategy(model_path="cloud_removal_model/cloud_removal_unet_best.pth")
if cloud_remover.model is None:
print("Warning: Could not load DeepInpainting model.")
print("Applying Cloud Removal & Merging Sentinel-1...")
valid_results = [r for r in results if r is not None]
print(f"Valid points extracted: {len(valid_results)}/{len(gdf)}")
for res in tqdm(valid_results, desc="Processing Fusion Features"):
try:
patch_s2 = res['patch_s2']
patch_s1 = res['patch_s1']
label = res['label']
# --- PROCESS S2 ---
patch_cloud_mask = patch_s2["SCL"].isin([3, 8, 9, 10])
clean_patch, _ = cloud_remover.remove_clouds(patch_s2, patch_cloud_mask)
b4 = clean_patch["B04"].values
b8 = clean_patch["B08"].values
b3 = clean_patch["B03"].values
b2 = clean_patch["B02"].values
ndvi = (b8 - b4) / (b8 + b4 + 1e-6)
ndwi = (b3 - b8) / (b3 + b8 + 1e-6)
b2 = np.clip(b2 / 10000.0, 0, 1)
b3 = np.clip(b3 / 10000.0, 0, 1)
b4 = np.clip(b4 / 10000.0, 0, 1)
b8 = np.clip(b8 / 10000.0, 0, 1)
features_t_s2 = np.stack([b2, b3, b4, b8, ndvi, ndwi], axis=1) # (time, 6, 16, 16)
t_len = features_t_s2.shape[0]
if t_len < 4:
pad = np.zeros((4 - t_len, 6, 16, 16))
features_t_s2 = np.concatenate([features_t_s2, pad], axis=0)
# --- PROCESS S1 ---
vv = patch_s1["vv"].values
vh = patch_s1["vh"].values
vv = np.clip(vv, 0, 1.0)
vh = np.clip(vh, 0, 1.0)
features_t_s1 = np.stack([vv, vh], axis=1) # (time, 2, 16, 16)
t_len_s1 = features_t_s1.shape[0]
if t_len_s1 < 4:
pad_s1 = np.zeros((4 - t_len_s1, 2, 16, 16))
features_t_s1 = np.concatenate([features_t_s1, pad_s1], axis=0)
# --- MERGE S1 and S2 ---
features_t = np.concatenate([features_t_s2, features_t_s1], axis=1) # (4, 8, 16, 16)
features = features_t.reshape(32, 16, 16)
features = np.nan_to_num(features, nan=0.0)
X.append(features)
y.append(label)
except Exception as e:
pass
return np.array(X), np.array(y)
def main():
print("🚀 BẮT ĐẦU TRÍCH XUẤT FUSION S1 + S2 (32-CHANNELS)")
cache_file = "dataset_cache/training_data_fusion_32ch.joblib"
bbox = [105.5, 9.2, 106.3, 10.0]
time_range = "2023-01-01/2023-04-30"
s2_items = get_s2_items(bbox, time_range)
s1_items = get_s1_items(bbox, time_range)
gdf = gpd.read_file("train/ST_training_data_updated_1130points_new.shp")
gdf = gdf.to_crs("EPSG:32648")
X, y = extract_fusion_patches(s2_items, s1_items, gdf, patch_size=16)
print(f"Final extracted shape: X={X.shape}, y={y.shape}")
os.makedirs('dataset_cache', exist_ok=True)
joblib.dump({'X': X, 'y': y}, cache_file)
print(f"Saved 32-channel Fusion cache to {cache_file}")
if __name__ == "__main__":
main()
+224
View File
@@ -0,0 +1,224 @@
import os
import gc
import json
import joblib
import numpy as np
import pandas as pd
import geopandas as gpd
import xarray as xr
from tqdm import tqdm
from joblib import Parallel, delayed
import pystac_client
import planetary_computer
import odc.stac
from shapely.geometry import Point, shape
from pyproj import Transformer
import warnings
warnings.filterwarnings('ignore')
from core.cloud_removal import DeepInpaintingStrategy
# Add GDAL optimizations for fast HTTP access
os.environ["GDAL_HTTP_MAX_RETRY"] = "5"
os.environ["GDAL_HTTP_RETRY_DELAY"] = "2"
os.environ["GDAL_HTTP_CONNECTION_TIMEOUT"] = "10"
os.environ["GDAL_HTTP_TIMEOUT"] = "30"
os.environ["CPL_VSIL_CURL_ALLOWED_EXTENSIONS"] = ".tif,.tiff"
os.environ["GDAL_DISABLE_READDIR_ON_OPEN"] = "YES"
def get_s2_items(bbox, time_range):
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
)
return list(search.items())
def get_s1_items(bbox, time_range):
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
return list(search.items())
def process_point_fast(idx, x_coord, y_coord, label, s2_items_dicts, s1_items_dicts, patch_size=16):
try:
import pystac
import odc.stac
import planetary_computer
from shapely.geometry import Point, shape
from pyproj import Transformer
# Add GDAL config per worker just in case
import os
os.environ["GDAL_HTTP_MAX_RETRY"] = "5"
os.environ["GDAL_HTTP_CONNECTION_TIMEOUT"] = "5"
os.environ["GDAL_HTTP_TIMEOUT"] = "10"
s2_items = [pystac.Item.from_dict(d) for d in s2_items_dicts]
s1_items = [pystac.Item.from_dict(d) for d in s1_items_dicts]
transformer = Transformer.from_crs("epsg:32648", "epsg:4326", always_xy=True)
lon, lat = transformer.transform(x_coord, y_coord)
point = Point(lon, lat)
# --- SENTINEL-2 ---
filtered_s2 = [item for item in s2_items if shape(item.geometry).contains(point)]
if not filtered_s2: return None
filtered_s2 = [planetary_computer.sign(item) for item in filtered_s2][:8] # Less temporal depth to speed up
patch_s2 = odc.stac.load(
filtered_s2,
bands=["B02", "B03", "B04", "B08", "SCL"],
x=(x_coord - 100, x_coord + 100),
y=(y_coord - 100, y_coord + 100),
crs="EPSG:32648",
resolution=10,
patch_url=planetary_computer.sign,
fail_on_error=False
).compute()
if patch_s2.dims['x'] < patch_size or patch_s2.dims['y'] < patch_size: return None
patch_s2 = patch_s2.isel(x=slice(0, patch_size), y=slice(0, patch_size))
# Get up to 4 valid
b2_sums = patch_s2["B02"].sum(dim=["x", "y"])
valid_times = b2_sums > 0
patch_s2 = patch_s2.isel(time=valid_times)
if len(patch_s2.time) == 0: return None
patch_s2 = patch_s2.isel(time=slice(0, min(4, len(patch_s2.time))))
if "SCL" not in patch_s2: return None
# --- SENTINEL-1 ---
filtered_s1 = [item for item in s1_items if shape(item.geometry).contains(point)]
if not filtered_s1: return None
filtered_s1 = [planetary_computer.sign(item) for item in filtered_s1][:6]
patch_s1 = odc.stac.load(
filtered_s1,
bands=["vv", "vh"],
x=(x_coord - 100, x_coord + 100),
y=(y_coord - 100, y_coord + 100),
crs="EPSG:32648",
resolution=10,
patch_url=planetary_computer.sign,
fail_on_error=False
).compute()
if patch_s1.dims['x'] < patch_size or patch_s1.dims['y'] < patch_size: return None
patch_s1 = patch_s1.isel(x=slice(0, patch_size), y=slice(0, patch_size))
vv_sums = patch_s1["vv"].sum(dim=["x", "y"])
valid_s1_times = vv_sums > 0
patch_s1 = patch_s1.isel(time=valid_s1_times)
if len(patch_s1.time) == 0: return None
patch_s1 = patch_s1.isel(time=slice(0, min(4, len(patch_s1.time))))
return {
'patch_s2': patch_s2,
'patch_s1': patch_s1,
'label': label
}
except Exception as e:
return None
def extract_fusion_fast(s2_items, s1_items, gdf, patch_size=16):
print(f"Extracting S1+S2 Fusion patches using 24 parallel jobs (FAST MODE)...")
s2_items_dicts = [item.to_dict() for item in s2_items]
s1_items_dicts = [item.to_dict() for item in s1_items]
# We pass individual scalar values to avoid pickling the whole row object
jobs = []
for idx, row in gdf.iterrows():
jobs.append((idx, row.geometry.x, row.geometry.y, row['HT_code'] - 1))
results = Parallel(n_jobs=24, backend="loky", pre_dispatch='1.5*n_jobs')(
delayed(process_point_fast)(idx, x, y, lbl, s2_items_dicts, s1_items_dicts, patch_size)
for idx, x, y, lbl in tqdm(jobs, total=len(jobs), desc="Downloading S1+S2 Patches")
)
X = []
y = []
cloud_remover = DeepInpaintingStrategy(model_path="cloud_removal_model/cloud_removal_unet_best.pth")
valid_results = [r for r in results if r is not None]
print(f"Valid points extracted: {len(valid_results)}/{len(gdf)}")
for res in tqdm(valid_results, desc="Processing Fusion Features"):
try:
patch_s2 = res['patch_s2']
patch_s1 = res['patch_s1']
label = res['label']
patch_cloud_mask = patch_s2["SCL"].isin([3, 8, 9, 10])
clean_patch, _ = cloud_remover.remove_clouds(patch_s2, patch_cloud_mask)
b4 = np.clip(clean_patch["B04"].values / 10000.0, 0, 1)
b8 = np.clip(clean_patch["B08"].values / 10000.0, 0, 1)
b3 = np.clip(clean_patch["B03"].values / 10000.0, 0, 1)
b2 = np.clip(clean_patch["B02"].values / 10000.0, 0, 1)
ndvi = (b8 - b4) / (b8 + b4 + 1e-6)
ndwi = (b3 - b8) / (b3 + b8 + 1e-6)
features_t_s2 = np.stack([b2, b3, b4, b8, ndvi, ndwi], axis=1) # (time, 6, 16, 16)
t_len = features_t_s2.shape[0]
if t_len < 4:
pad = np.zeros((4 - t_len, 6, 16, 16))
features_t_s2 = np.concatenate([features_t_s2, pad], axis=0)
vv = np.clip(patch_s1["vv"].values, 0, 1.0)
vh = np.clip(patch_s1["vh"].values, 0, 1.0)
features_t_s1 = np.stack([vv, vh], axis=1) # (time, 2, 16, 16)
t_len_s1 = features_t_s1.shape[0]
if t_len_s1 < 4:
pad_s1 = np.zeros((4 - t_len_s1, 2, 16, 16))
features_t_s1 = np.concatenate([features_t_s1, pad_s1], axis=0)
features_t = np.concatenate([features_t_s2, features_t_s1], axis=1) # (4, 8, 16, 16)
features = features_t.reshape(32, 16, 16)
features = np.nan_to_num(features, nan=0.0)
X.append(features)
y.append(label)
except Exception as e:
pass
return np.array(X), np.array(y)
def main():
print("🚀 BẮT ĐẦU TRÍCH XUẤT FUSION S1 + S2 (FAST MODE)")
cache_file = "dataset_cache/training_data_fusion_32ch.joblib"
bbox = [105.5, 9.2, 106.3, 10.0]
time_range = "2023-01-01/2023-04-30"
s2_items = get_s2_items(bbox, time_range)
s1_items = get_s1_items(bbox, time_range)
gdf = gpd.read_file("train/ST_training_data_updated_1130points_new.shp")
gdf = gdf.to_crs("EPSG:32648")
X, y = extract_fusion_fast(s2_items, s1_items, gdf, patch_size=16)
print(f"Final extracted shape: X={X.shape}, y={y.shape}")
os.makedirs('dataset_cache', exist_ok=True)
joblib.dump({'X': X, 'y': y}, cache_file)
print(f"Saved 32-channel Fusion cache to {cache_file}")
if __name__ == "__main__":
main()
+326
View File
@@ -0,0 +1,326 @@
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import transforms
import torchvision.models as models
import joblib
import pandas as pd
import geopandas as gpd
import planetary_computer
import pystac_client
import odc.stac
import numpy as np
import os
import json
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
from tqdm import tqdm
from joblib import Parallel, delayed
from core.cloud_removal import DeepInpaintingStrategy
def get_s2_items(bbox, time_range):
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": 30}}
)
items = list(search.items())
items = sorted(items, key=lambda x: x.properties["eo:cloud_cover"])
print(f"Found {len(items)} Sentinel-2 items")
return items
class SwinUNetWrapper(nn.Module):
def __init__(self, in_channels=24, num_classes=5):
super().__init__()
self.swin = models.swin_t(weights=models.Swin_T_Weights.IMAGENET1K_V1)
old_conv = self.swin.features[0][0]
new_conv = nn.Conv2d(in_channels, old_conv.out_channels,
kernel_size=old_conv.kernel_size,
stride=old_conv.stride,
padding=old_conv.padding)
with torch.no_grad():
new_conv.weight[:, :3] = old_conv.weight
new_conv.weight[:, 3:] = old_conv.weight.mean(dim=1, keepdim=True).repeat(1, in_channels-3, 1, 1)
new_conv.bias = old_conv.bias
self.swin.features[0][0] = new_conv
self.swin.head = nn.Linear(self.swin.head.in_features, num_classes)
self.upsample = nn.Upsample(size=(224, 224), mode='bilinear', align_corners=False)
def forward(self, x):
x = self.upsample(x)
return self.swin(x)
def process_point(idx, row, items_dicts, patch_size=16):
try:
import pystac
import odc.stac
import planetary_computer
from shapely.geometry import Point, shape
from pyproj import Transformer
items = [pystac.Item.from_dict(d) for d in items_dicts]
x_coord = row['geometry'].x
y_coord = row['geometry'].y
transformer = Transformer.from_crs("epsg:32648", "epsg:4326", always_xy=True)
lon, lat = transformer.transform(x_coord, y_coord)
point = Point(lon, lat)
filtered_items = []
for item in items:
geom = shape(item.geometry)
if geom.contains(point):
filtered_items.append(item)
if not filtered_items:
return None
filtered_items = [planetary_computer.sign(item) for item in filtered_items][:10]
# Increase bounds to 100m radius (20x20 pixels) to avoid boundary issues!
patch_s2 = odc.stac.load(
filtered_items,
bands=["B02", "B03", "B04", "B08", "SCL"],
x=(x_coord - 100, x_coord + 100),
y=(y_coord - 100, y_coord + 100),
crs="EPSG:32648",
resolution=10,
patch_url=planetary_computer.sign,
fail_on_error=False
).compute()
b2_sums = patch_s2["B02"].sum(dim=["x", "y"])
valid_times = b2_sums > 0
patch_s2 = patch_s2.isel(time=valid_times)
if len(patch_s2.time) == 0:
return None
patch_s2 = patch_s2.isel(time=slice(0, min(4, len(patch_s2.time))))
if "SCL" not in patch_s2 or "B02" not in patch_s2:
return None
if patch_s2.dims['x'] < patch_size or patch_s2.dims['y'] < patch_size:
return None
patch_s2 = patch_s2.isel(x=slice(0, patch_size), y=slice(0, patch_size))
return {
'patch_s2': patch_s2,
'label': row['HT_code'] - 1
}
except Exception as e:
return None
def extract_2d_patches(items, gdf, patch_size=16):
print(f"Extracting 2D patches for {len(gdf)} points using 8 parallel jobs...")
items_dicts = [item.to_dict() for item in items]
results = Parallel(n_jobs=8, backend="loky")(
delayed(process_point)(idx, row, items_dicts, patch_size)
for idx, row in tqdm(gdf.iterrows(), total=len(gdf), desc="Downloading Patches")
)
X = []
y = []
cloud_remover = DeepInpaintingStrategy(model_path="cloud_removal_model/cloud_removal_unet_best.pth")
if cloud_remover.model is None:
print("Warning: Could not load DeepInpainting model.")
print("Applying Cloud Removal sequentially...")
valid_results = [r for r in results if r is not None]
print(f"Valid points extracted: {len(valid_results)}/{len(gdf)}")
for res in tqdm(valid_results, desc="Cloud Removal & Features"):
try:
patch_s2 = res['patch_s2']
label = res['label']
patch_cloud_mask = patch_s2["SCL"].isin([3, 8, 9, 10])
# Apply cloud removal (returns 4 time steps)
clean_patch, _ = cloud_remover.remove_clouds(patch_s2, patch_cloud_mask)
b4 = clean_patch["B04"].values
b8 = clean_patch["B08"].values
b3 = clean_patch["B03"].values
b2 = clean_patch["B02"].values
ndvi = (b8 - b4) / (b8 + b4 + 1e-6)
ndwi = (b3 - b8) / (b3 + b8 + 1e-6)
b2 = np.clip(b2 / 10000.0, 0, 1)
b3 = np.clip(b3 / 10000.0, 0, 1)
b4 = np.clip(b4 / 10000.0, 0, 1)
b8 = np.clip(b8 / 10000.0, 0, 1)
# Stack across channels
features_t = np.stack([b2, b3, b4, b8, ndvi, ndwi], axis=1) # Shape: (time, 6, 16, 16)
# Pad time dimension to exactly 4 if needed
t_len = features_t.shape[0]
if t_len < 4:
pad = np.zeros((4 - t_len, 6, 16, 16))
features_t = np.concatenate([features_t, pad], axis=0)
# Flatten time and channels: (4, 6, 16, 16) -> (24, 16, 16)
features = features_t.reshape(24, 16, 16)
features = np.nan_to_num(features, nan=0.0)
X.append(features)
y.append(label)
except Exception as e:
pass
return np.array(X), np.array(y)
def train_2d_model(X, y):
print(f"Training 2D CNN with Data Augmentation... Dataset shape: {X.shape}")
unique_labels = sorted(list(np.unique(y)))
label_map = {lbl: i for i, lbl in enumerate(unique_labels)}
y_mapped = np.array([label_map[l] for l in y])
X_train, X_test, y_train, y_test = train_test_split(X, y_mapped, test_size=0.2, random_state=42)
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
])
class PatchDataset(torch.utils.data.Dataset):
def __init__(self, X, y, augment=False):
self.X = torch.FloatTensor(X)
self.y = torch.LongTensor(y)
self.augment = augment
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
x = self.X[idx]
if self.augment:
x = transform(x)
return x, self.y[idx]
train_dataset = PatchDataset(X_train, y_train, augment=True)
test_dataset = PatchDataset(X_test, y_test, augment=False)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
model = SwinUNetWrapper(in_channels=24, num_classes=len(unique_labels)).to(device)
class_counts = np.bincount(y_train)
weights = 1.0 / (class_counts + 1e-6)
weights = torch.FloatTensor(weights / weights.sum() * len(class_counts)).to(device)
criterion = nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.05)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
epochs = 150
best_acc = 0
best_state = None
for epoch in range(epochs):
model.train()
train_loss = 0
for batch_X, batch_y in train_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
out = model(batch_X)
loss = criterion(out, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
model.eval()
all_preds = []
all_targets = []
with torch.no_grad():
for batch_X, batch_y in test_loader:
out = model(batch_X.to(device))
preds = out.argmax(dim=1).cpu().numpy()
all_preds.extend(preds)
all_targets.extend(batch_y.numpy())
acc = accuracy_score(all_targets, all_preds)
scheduler.step()
if acc > best_acc:
best_acc = acc
best_state = model.state_dict()
print(f"Epoch {epoch+1}/{epochs} - Loss: {train_loss/len(train_loader):.4f} - Test Acc: {acc:.4f} 🌟")
if acc >= 0.95:
print("🎯 Đã đạt mốc >95% Accuracy!")
break
elif (epoch+1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs} - Loss: {train_loss/len(train_loader):.4f} - Test Acc: {acc:.4f}")
if best_state:
model.load_state_dict(best_state)
os.makedirs('land_classification_model', exist_ok=True)
joblib.dump(model.cpu(), 'land_classification_model/model_cnn_2d_95.joblib')
print(f"✅ Đã lưu mô hình đạt {best_acc:.4f} vào land_classification_model/model_cnn_2d_95.joblib")
clf_rep = classification_report(all_targets, all_preds, output_dict=True)
info = {
"model_type": "CNN_2D_Patch_CloudRemoval_Temporal",
"test_accuracy": float(best_acc),
"params": {"epochs": epochs, "architecture": "2D CNN Swin-UNet Temporal"},
"classification_report": clf_rep
}
os.makedirs('model_train', exist_ok=True)
with open('model_train/model_cnn_2d_info.json', 'w') as f:
json.dump(info, f, indent=2)
def main():
print("🚀 BẮT ĐẦU PIPELINE 2D PATCH-BASED & CLOUD REMOVAL (TEMPORAL 24-CHANNELS)")
# Dùng tên file mới để tránh bị trùng với dữ liệu 6 channel cũ
cache_file = "dataset_cache/training_data_2d_temporal.joblib"
if os.path.exists(cache_file):
print(f"Loading 2D patches from {cache_file}...")
data = joblib.load(cache_file)
X, y = data['X'], data['y']
else:
bbox = [105.5, 9.2, 106.3, 10.0]
time_range = "2023-01-01/2023-04-30"
items = get_s2_items(bbox, time_range)
gdf = gpd.read_file("train/ST_training_data_updated_1130points_new.shp")
gdf = gdf.to_crs("EPSG:32648")
X, y = extract_2d_patches(items, gdf, patch_size=16)
os.makedirs('dataset_cache', exist_ok=True)
joblib.dump({'X': X, 'y': y}, cache_file)
print(f"Saved 2D cache to {cache_file}")
train_2d_model(X, y)
print("🎉 Hoàn tất quá trình! Check-point với Accuracy > 95% đã được lưu!")
if __name__ == "__main__":
main()