refactor: reorganize project structure by moving core modules and update import paths in API server
This commit is contained in:
@@ -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!")
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user