feat: implement comprehensive land cover classification pipeline with model benchmarking and experiment logging

This commit is contained in:
2026-07-17 18:54:25 +07:00
parent a258db54cd
commit abab846884
69 changed files with 155558 additions and 105 deletions
+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 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()