deha_hf / methods /method_rast.py
hkayabilisim's picture
Initial commit
ab4021e
Raw
History Blame Contribute Delete
6.92 kB
"""
Method RAST — Raster-only observability scoring.
=================================================
Uses only the point geometries and the raster pixel values; no attribute
fields are required.
For each pole point:
1. An *inner circle* (radius = ``inner_radius``) is drawn around the pole.
The maximum pixel value inside this circle is ``l_pole``.
2. An *annulus* (ring between ``inner_radius`` and ``outer_radius``) is
drawn around the pole. The maximum pixel value inside the annulus is
``l_bg``.
3. Global background statistics are computed across **all** poles:
mu_bg = median(l_bg)
sigma_bg = MAD(l_bg)
4. Per-pole observability score:
s = (l_pole - mu_bg) / sigma_bg
5. Detection: detected = (s >= threshold)
Produced attributes
-------------------
l_pole, l_bg, mu_bg, sigma_bg, s,
img_radiance_near_mean, img_radiance_near_minus_far,
method, detected
"""
import math
import numpy as np
import geopandas as gpd
import rasterio
from rasterio.mask import mask as rasterio_mask
from scipy.stats import median_abs_deviation
def run(tiff_path, vector_path, band=1, threshold=3.0,
inner_radius=5, outer_radius=15):
"""
Run the RAST analysis.
Parameters
----------
tiff_path : str
Path to the input GeoTIFF (night-time light raster).
vector_path : str
Path to the input vector file (pole points, GeoJSON / GPKG / SHP).
band : int
Raster band to use (1, 2 or 3).
threshold : float
Observability threshold. A point is *detected* when ``s >= threshold``.
inner_radius : int
Inner radius of the annulus in metres (also the pole-circle radius).
outer_radius : int
Outer radius of the annulus in metres.
Returns
-------
geopandas.GeoDataFrame
Original poles enriched with RAST attributes.
"""
# ------------------------------------------------------------------
# Read vector data
# ------------------------------------------------------------------
gdf = gpd.read_file(vector_path)
# Convert any datetime columns to ISO strings (avoids JSON issues)
for col in gdf.columns:
if str(gdf[col].dtype).startswith('datetime'):
gdf[col] = gdf[col].apply(
lambda x: x.isoformat()
if x is not None and not (isinstance(x, float) and math.isnan(x))
else None
)
# ------------------------------------------------------------------
# Open raster and reproject vector
# ------------------------------------------------------------------
with rasterio.open(tiff_path) as src:
raster_crs = src.crs
# Reproject vector to raster CRS
gdf_raster_crs = gdf.to_crs(raster_crs)
# Determine a projected CRS (UTM) for metre-accurate buffering
centroid = gdf_raster_crs.geometry.unary_union.centroid
lon, lat = centroid.x, centroid.y
if gdf_raster_crs.crs.is_geographic:
utm_zone = int((lon + 180) / 6) + 1
hemisphere = 'north' if lat >= 0 else 'south'
utm_crs = f'+proj=utm +zone={utm_zone} +{hemisphere} +datum=WGS84'
else:
utm_crs = raster_crs
gdf_proj = gdf_raster_crs.to_crs(utm_crs)
# ------------------------------------------------------------------
# Per-pole pixel extraction
# ------------------------------------------------------------------
l_pole_list = []
l_bg_list = []
near_mean_list = []
near_minus_far_list = []
for _idx, row in gdf_proj.iterrows():
point = row.geometry
inner_circle = point.buffer(inner_radius)
outer_circle = point.buffer(outer_radius)
annulus = outer_circle.difference(inner_circle)
# Reproject geometries back to raster CRS for masking
inner_gdf = gpd.GeoDataFrame(
geometry=[inner_circle], crs=utm_crs
).to_crs(raster_crs)
annulus_gdf = gpd.GeoDataFrame(
geometry=[annulus], crs=utm_crs
).to_crs(raster_crs)
inner_geom = [inner_gdf.geometry.iloc[0]]
annulus_geom = [annulus_gdf.geometry.iloc[0]]
# Extract pixels inside inner circle
try:
inner_data, _ = rasterio_mask(
src, inner_geom, crop=True, nodata=0, filled=True
)
band_data_inner = inner_data[band - 1]
valid_inner = band_data_inner[band_data_inner != 0]
l_pole = float(np.max(valid_inner)) if len(valid_inner) > 0 else 0.0
near_mean = float(np.mean(valid_inner)) if len(valid_inner) > 0 else 0.0
except Exception:
l_pole = 0.0
near_mean = 0.0
# Extract pixels inside annulus
try:
annulus_data, _ = rasterio_mask(
src, annulus_geom, crop=True, nodata=0, filled=True
)
band_data_annulus = annulus_data[band - 1]
valid_annulus = band_data_annulus[band_data_annulus != 0]
l_bg = float(np.max(valid_annulus)) if len(valid_annulus) > 0 else 0.0
far_mean = float(np.mean(valid_annulus)) if len(valid_annulus) > 0 else 0.0
except Exception:
l_bg = 0.0
far_mean = 0.0
l_pole_list.append(l_pole)
l_bg_list.append(l_bg)
near_mean_list.append(near_mean)
near_minus_far_list.append(near_mean - far_mean)
# ------------------------------------------------------------------
# Global statistics
# ------------------------------------------------------------------
l_bg_array = np.array(l_bg_list)
mu_bg = float(np.median(l_bg_array))
sigma_bg = float(median_abs_deviation(l_bg_array))
# ------------------------------------------------------------------
# Per-point scoring
# ------------------------------------------------------------------
s_list = []
for l_pole in l_pole_list:
if sigma_bg > 0:
s = (l_pole - mu_bg) / sigma_bg
else:
s = 0.0
s_list.append(s)
# ------------------------------------------------------------------
# Build output GeoDataFrame
# ------------------------------------------------------------------
result_gdf = gdf.copy()
result_gdf['l_pole'] = l_pole_list
result_gdf['l_bg'] = l_bg_list
result_gdf['mu_bg'] = mu_bg
result_gdf['sigma_bg'] = sigma_bg
result_gdf['s'] = s_list
result_gdf['img_radiance_near_mean'] = near_mean_list
result_gdf['img_radiance_near_minus_far'] = near_minus_far_list
result_gdf['method'] = 'RAST'
result_gdf['detected'] = [bool(s >= threshold) for s in s_list]
return result_gdf