455 lines
16 KiB
Python
455 lines
16 KiB
Python
"""
|
|
Feature Extraction Module for Land Classification
|
|
Chuẩn hóa việc trích xuất features từ satellite data cho cả training và prediction
|
|
"""
|
|
|
|
import numpy as np
|
|
import xarray as xr
|
|
from typing import List, Dict, Tuple, Optional
|
|
|
|
|
|
class FeatureExtractor:
|
|
"""
|
|
Extract features từ Sentinel-2 và Sentinel-1 data
|
|
Hỗ trợ 2 modes:
|
|
- 'simple': 3 features cơ bản (NDVI_mean, VH_mean, VV_mean)
|
|
- 'temporal': 39 features time-series (NDVI + NDWI + NDBI theo thời gian)
|
|
"""
|
|
|
|
FEATURE_MODES = {
|
|
'simple': {
|
|
'n_features': 3,
|
|
'features': ['NDVI_mean', 'VH_db_mean', 'VV_db_mean'],
|
|
'description': 'Simple aggregate features (mean only)'
|
|
},
|
|
'temporal': {
|
|
'n_features': 39,
|
|
'features': None, # Generated dynamically based on time steps
|
|
'description': 'Temporal features with NDVI, NDWI, NDBI time series'
|
|
},
|
|
'extended': {
|
|
'n_features': 15,
|
|
'features': [
|
|
'NDVI_mean', 'NDVI_std', 'NDVI_min', 'NDVI_max',
|
|
'NDWI_mean', 'NDWI_std', 'NDWI_min', 'NDWI_max',
|
|
'NDBI_mean', 'NDBI_std', 'NDBI_min', 'NDBI_max',
|
|
'VH_db_mean', 'VV_db_mean', 'VH_VV_ratio'
|
|
],
|
|
'description': 'Extended aggregate features with statistics'
|
|
},
|
|
'odc': {
|
|
'n_features': 8,
|
|
'features': [
|
|
'ndvi_mean', 'ndvi_min', 'ndvi_max', 'ndvi_std', 'ndvi_range',
|
|
'ndwi_mean', 'ndbi_mean', 'evi_mean'
|
|
],
|
|
'description': 'ODC mode: 8 aggregate features (NDVI stats + NDWI/NDBI/EVI mean) - matches 01.train_ODC.ipynb'
|
|
}
|
|
}
|
|
|
|
def __init__(self, mode: str = 'simple'):
|
|
"""
|
|
Initialize FeatureExtractor
|
|
|
|
Args:
|
|
mode: 'simple', 'temporal', hoặc 'extended'
|
|
"""
|
|
if mode not in self.FEATURE_MODES:
|
|
raise ValueError(f"Invalid mode: {mode}. Choose from {list(self.FEATURE_MODES.keys())}")
|
|
|
|
self.mode = mode
|
|
self.config = self.FEATURE_MODES[mode]
|
|
|
|
def get_feature_names(self, n_timesteps: Optional[int] = None) -> List[str]:
|
|
"""
|
|
Lấy danh sách tên features
|
|
|
|
Args:
|
|
n_timesteps: Số timesteps (chỉ cần cho mode='temporal')
|
|
|
|
Returns:
|
|
List tên features
|
|
"""
|
|
if self.mode == 'temporal':
|
|
if n_timesteps is None:
|
|
raise ValueError("n_timesteps required for temporal mode")
|
|
|
|
features = []
|
|
# NDVI time series
|
|
for t in range(n_timesteps):
|
|
features.append(f'NDVI_t{t+1}')
|
|
# NDWI time series
|
|
for t in range(n_timesteps):
|
|
features.append(f'NDWI_t{t+1}')
|
|
# NDBI time series
|
|
for t in range(n_timesteps):
|
|
features.append(f'NDBI_t{t+1}')
|
|
|
|
# VH/VV radar (mean across time)
|
|
features.append('VH_db_mean')
|
|
features.append('VV_db_mean')
|
|
features.append('VH_VV_ratio')
|
|
|
|
return features
|
|
else:
|
|
return self.config['features']
|
|
|
|
def extract_simple_features(
|
|
self,
|
|
ndvi_data: xr.DataArray,
|
|
vh_data: Optional[xr.DataArray] = None,
|
|
vv_data: Optional[xr.DataArray] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Extract simple features (3 features: NDVI_mean, VH_db_mean, VV_db_mean)
|
|
|
|
Args:
|
|
ndvi_data: NDVI DataArray (có thể có time dimension)
|
|
vh_data: VH radar DataArray
|
|
vv_data: VV radar DataArray
|
|
|
|
Returns:
|
|
Feature array shape (n_pixels, 3)
|
|
"""
|
|
# Calculate NDVI mean
|
|
if 'time' in ndvi_data.dims:
|
|
ndvi_mean = ndvi_data.mean(dim='time')
|
|
else:
|
|
ndvi_mean = ndvi_data
|
|
|
|
# Flatten to pixels
|
|
ndvi_flat = ndvi_mean.values.flatten()
|
|
|
|
# Calculate radar features if available
|
|
if vh_data is not None and vv_data is not None:
|
|
if 'time' in vh_data.dims:
|
|
vh_mean = vh_data.mean(dim='time')
|
|
vv_mean = vv_data.mean(dim='time')
|
|
else:
|
|
vh_mean = vh_data
|
|
vv_mean = vv_data
|
|
|
|
vh_flat = vh_mean.values.flatten()
|
|
vv_flat = vv_mean.values.flatten()
|
|
else:
|
|
# If no radar data, use zeros
|
|
vh_flat = np.zeros_like(ndvi_flat)
|
|
vv_flat = np.zeros_like(ndvi_flat)
|
|
|
|
# Stack features
|
|
features = np.column_stack([ndvi_flat, vh_flat, vv_flat])
|
|
|
|
return features
|
|
|
|
def extract_temporal_features(
|
|
self,
|
|
s2_data: xr.Dataset,
|
|
vh_data: Optional[xr.DataArray] = None,
|
|
vv_data: Optional[xr.DataArray] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Extract temporal features (39 features: time series của NDVI, NDWI, NDBI + radar)
|
|
|
|
Args:
|
|
s2_data: Sentinel-2 Dataset với bands B02, B03, B04, B08, B11
|
|
vh_data: VH radar DataArray
|
|
vv_data: VV radar DataArray
|
|
|
|
Returns:
|
|
Feature array shape (n_pixels, 39)
|
|
"""
|
|
# Calculate spectral indices
|
|
nir = s2_data["B08"].astype('float32')
|
|
red = s2_data["B04"].astype('float32')
|
|
green = s2_data["B03"].astype('float32')
|
|
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"] # Fallback to B02
|
|
|
|
# NDVI = (NIR - Red) / (NIR + Red)
|
|
ndvi = (nir - red) / (nir + red + 1e-8)
|
|
|
|
# NDWI = (Green - NIR) / (Green + NIR)
|
|
ndwi = (green - nir) / (green + nir + 1e-8)
|
|
|
|
# NDBI = (SWIR - NIR) / (SWIR + NIR)
|
|
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
|
|
|
# Resample to monthly if time dimension exists
|
|
if 'time' in ndvi.dims:
|
|
ndvi_monthly = ndvi.resample(time="1ME").mean()
|
|
ndwi_monthly = ndwi.resample(time="1ME").mean()
|
|
ndbi_monthly = ndbi.resample(time="1ME").mean()
|
|
else:
|
|
ndvi_monthly = ndvi
|
|
ndwi_monthly = ndwi
|
|
ndbi_monthly = ndbi
|
|
|
|
# Get dimensions
|
|
n_times = len(ndvi_monthly.time) if 'time' in ndvi_monthly.dims else 1
|
|
y_size = len(ndvi_monthly.y)
|
|
x_size = len(ndvi_monthly.x)
|
|
n_pixels = y_size * x_size
|
|
|
|
# Extract temporal features
|
|
features_list = []
|
|
|
|
# NDVI time series
|
|
for t in range(n_times):
|
|
if 'time' in ndvi_monthly.dims:
|
|
ndvi_t = ndvi_monthly.isel(time=t).values.flatten()
|
|
else:
|
|
ndvi_t = ndvi_monthly.values.flatten()
|
|
features_list.append(ndvi_t)
|
|
|
|
# NDWI time series
|
|
for t in range(n_times):
|
|
if 'time' in ndwi_monthly.dims:
|
|
ndwi_t = ndwi_monthly.isel(time=t).values.flatten()
|
|
else:
|
|
ndwi_t = ndwi_monthly.values.flatten()
|
|
features_list.append(ndwi_t)
|
|
|
|
# NDBI time series
|
|
for t in range(n_times):
|
|
if 'time' in ndbi_monthly.dims:
|
|
ndbi_t = ndbi_monthly.isel(time=t).values.flatten()
|
|
else:
|
|
ndbi_t = ndbi_monthly.values.flatten()
|
|
features_list.append(ndbi_t)
|
|
|
|
# Stack all spectral features
|
|
features = np.column_stack(features_list)
|
|
|
|
# Add radar features if available
|
|
if vh_data is not None and vv_data is not None:
|
|
if 'time' in vh_data.dims:
|
|
vh_mean = vh_data.mean(dim='time')
|
|
vv_mean = vv_data.mean(dim='time')
|
|
else:
|
|
vh_mean = vh_data
|
|
vv_mean = vv_data
|
|
|
|
vh_flat = vh_mean.values.flatten()
|
|
vv_flat = vv_mean.values.flatten()
|
|
vh_vv_ratio = vh_flat / (vv_flat + 1e-8)
|
|
|
|
# Add radar features
|
|
features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])
|
|
|
|
return features
|
|
|
|
def extract_odc_features(
|
|
self,
|
|
s2_data: xr.Dataset,
|
|
vh_data: Optional[xr.DataArray] = None,
|
|
vv_data: Optional[xr.DataArray] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Extract ODC aggregate features (8 features matching 01.train_ODC.ipynb):
|
|
ndvi_mean, ndvi_min, ndvi_max, ndvi_std, ndvi_range, ndwi_mean, ndbi_mean, evi_mean
|
|
|
|
Args:
|
|
s2_data: Sentinel-2 Dataset with B02, B03, B04, B08, B11
|
|
vh_data: Not used in ODC mode
|
|
vv_data: Not used in ODC mode
|
|
|
|
Returns:
|
|
Feature array shape (n_pixels, 8)
|
|
"""
|
|
# Calculate spectral indices
|
|
nir = s2_data["B08"].astype('float32')
|
|
red = s2_data["B04"].astype('float32')
|
|
green = s2_data["B03"].astype('float32')
|
|
blue = s2_data["B02"].astype('float32')
|
|
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"]
|
|
|
|
# NDVI = (NIR - Red) / (NIR + Red)
|
|
ndvi = (nir - red) / (nir + red + 1e-8)
|
|
|
|
# NDWI = (Green - NIR) / (Green + NIR)
|
|
ndwi = (green - nir) / (green + nir + 1e-8)
|
|
|
|
# NDBI = (SWIR - NIR) / (SWIR + NIR)
|
|
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
|
|
|
# EVI = 2.5 * (NIR - Red) / (NIR + 6*Red - 7.5*Blue + 1)
|
|
evi = 2.5 * (nir - red) / (nir + 6*red - 7.5*blue + 1)
|
|
|
|
features_list = []
|
|
|
|
# NDVI statistics (5 features)
|
|
if 'time' in ndvi.dims:
|
|
features_list.append(ndvi.mean(dim='time').values.flatten()) # ndvi_mean
|
|
features_list.append(ndvi.min(dim='time').values.flatten()) # ndvi_min
|
|
features_list.append(ndvi.max(dim='time').values.flatten()) # ndvi_max
|
|
features_list.append(ndvi.std(dim='time').values.flatten()) # ndvi_std
|
|
ndvi_range = (ndvi.max(dim='time') - ndvi.min(dim='time')).values.flatten()
|
|
features_list.append(ndvi_range) # ndvi_range
|
|
else:
|
|
ndvi_flat = ndvi.values.flatten()
|
|
features_list.extend([ndvi_flat, ndvi_flat, ndvi_flat, np.zeros_like(ndvi_flat), np.zeros_like(ndvi_flat)])
|
|
|
|
# NDWI mean (1 feature)
|
|
if 'time' in ndwi.dims:
|
|
features_list.append(ndwi.mean(dim='time').values.flatten()) # ndwi_mean
|
|
else:
|
|
features_list.append(ndwi.values.flatten())
|
|
|
|
# NDBI mean (1 feature)
|
|
if 'time' in ndbi.dims:
|
|
features_list.append(ndbi.mean(dim='time').values.flatten()) # ndbi_mean
|
|
else:
|
|
features_list.append(ndbi.values.flatten())
|
|
|
|
# EVI mean (1 feature)
|
|
if 'time' in evi.dims:
|
|
features_list.append(evi.mean(dim='time').values.flatten()) # evi_mean
|
|
else:
|
|
features_list.append(evi.values.flatten())
|
|
|
|
# Stack all features (total: 8 features)
|
|
features = np.column_stack(features_list)
|
|
|
|
return features
|
|
|
|
def extract_extended_features(
|
|
self,
|
|
s2_data: xr.Dataset,
|
|
vh_data: Optional[xr.DataArray] = None,
|
|
vv_data: Optional[xr.DataArray] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Extract extended aggregate features (15 features: stats của NDVI, NDWI, NDBI + radar)
|
|
|
|
Args:
|
|
s2_data: Sentinel-2 Dataset
|
|
vh_data: VH radar DataArray
|
|
vv_data: VV radar DataArray
|
|
|
|
Returns:
|
|
Feature array shape (n_pixels, 15)
|
|
"""
|
|
# Calculate spectral indices
|
|
nir = s2_data["B08"].astype('float32')
|
|
red = s2_data["B04"].astype('float32')
|
|
green = s2_data["B03"].astype('float32')
|
|
swir = s2_data["B11"].astype('float32') if "B11" in s2_data else s2_data["B02"]
|
|
|
|
ndvi = (nir - red) / (nir + red + 1e-8)
|
|
ndwi = (green - nir) / (green + nir + 1e-8)
|
|
ndbi = (swir - nir) / (swir + nir + 1e-8)
|
|
|
|
features_list = []
|
|
|
|
# NDVI statistics
|
|
if 'time' in ndvi.dims:
|
|
features_list.append(ndvi.mean(dim='time').values.flatten())
|
|
features_list.append(ndvi.std(dim='time').values.flatten())
|
|
features_list.append(ndvi.min(dim='time').values.flatten())
|
|
features_list.append(ndvi.max(dim='time').values.flatten())
|
|
else:
|
|
ndvi_flat = ndvi.values.flatten()
|
|
features_list.extend([ndvi_flat, np.zeros_like(ndvi_flat), ndvi_flat, ndvi_flat])
|
|
|
|
# NDWI statistics
|
|
if 'time' in ndwi.dims:
|
|
features_list.append(ndwi.mean(dim='time').values.flatten())
|
|
features_list.append(ndwi.std(dim='time').values.flatten())
|
|
features_list.append(ndwi.min(dim='time').values.flatten())
|
|
features_list.append(ndwi.max(dim='time').values.flatten())
|
|
else:
|
|
ndwi_flat = ndwi.values.flatten()
|
|
features_list.extend([ndwi_flat, np.zeros_like(ndwi_flat), ndwi_flat, ndwi_flat])
|
|
|
|
# NDBI statistics
|
|
if 'time' in ndbi.dims:
|
|
features_list.append(ndbi.mean(dim='time').values.flatten())
|
|
features_list.append(ndbi.std(dim='time').values.flatten())
|
|
features_list.append(ndbi.min(dim='time').values.flatten())
|
|
features_list.append(ndbi.max(dim='time').values.flatten())
|
|
else:
|
|
ndbi_flat = ndbi.values.flatten()
|
|
features_list.extend([ndbi_flat, np.zeros_like(ndbi_flat), ndbi_flat, ndbi_flat])
|
|
|
|
# Stack spectral features
|
|
features = np.column_stack(features_list)
|
|
|
|
# Add radar features
|
|
if vh_data is not None and vv_data is not None:
|
|
if 'time' in vh_data.dims:
|
|
vh_mean = vh_data.mean(dim='time')
|
|
vv_mean = vv_data.mean(dim='time')
|
|
else:
|
|
vh_mean = vh_data
|
|
vv_mean = vv_data
|
|
|
|
vh_flat = vh_mean.values.flatten()
|
|
vv_flat = vv_mean.values.flatten()
|
|
vh_vv_ratio = vh_flat / (vv_flat + 1e-8)
|
|
|
|
features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])
|
|
|
|
return features
|
|
|
|
def extract(
|
|
self,
|
|
s2_data: Optional[xr.Dataset] = None,
|
|
ndvi_data: Optional[xr.DataArray] = None,
|
|
vh_data: Optional[xr.DataArray] = None,
|
|
vv_data: Optional[xr.DataArray] = None
|
|
) -> np.ndarray:
|
|
"""
|
|
Extract features theo mode đã chọn
|
|
|
|
Args:
|
|
s2_data: Sentinel-2 Dataset (cần cho temporal, extended, và odc modes)
|
|
ndvi_data: NDVI DataArray (cần cho simple mode)
|
|
vh_data: VH radar DataArray
|
|
vv_data: VV radar DataArray
|
|
|
|
Returns:
|
|
Feature array
|
|
"""
|
|
if self.mode == 'simple':
|
|
if ndvi_data is None:
|
|
raise ValueError("ndvi_data required for simple mode")
|
|
return self.extract_simple_features(ndvi_data, vh_data, vv_data)
|
|
|
|
elif self.mode == 'temporal':
|
|
if s2_data is None:
|
|
raise ValueError("s2_data required for temporal mode")
|
|
return self.extract_temporal_features(s2_data, vh_data, vv_data)
|
|
|
|
elif self.mode == 'extended':
|
|
if s2_data is None:
|
|
raise ValueError("s2_data required for extended mode")
|
|
return self.extract_extended_features(s2_data, vh_data, vv_data)
|
|
|
|
elif self.mode == 'odc':
|
|
if s2_data is None:
|
|
raise ValueError("s2_data required for odc mode")
|
|
return self.extract_odc_features(s2_data, vh_data, vv_data)
|
|
|
|
else:
|
|
raise ValueError(f"Unknown mode: {self.mode}")
|
|
|
|
def get_info(self) -> Dict:
|
|
"""Lấy thông tin về feature extraction mode"""
|
|
return {
|
|
'mode': self.mode,
|
|
'n_features': self.config['n_features'],
|
|
'description': self.config['description']
|
|
}
|
|
|
|
|
|
def get_feature_extractor(mode: str = 'simple') -> FeatureExtractor:
|
|
"""
|
|
Factory function để tạo FeatureExtractor
|
|
|
|
Args:
|
|
mode: 'simple', 'temporal', 'extended', hoặc 'odc'
|
|
|
|
Returns:
|
|
FeatureExtractor instance
|
|
"""
|
|
return FeatureExtractor(mode=mode)
|