Skip to content

Commit c394ad1

Browse files
author
Matic Lubej
committed
update utils
1 parent 3bb2261 commit c394ad1

File tree

1 file changed

+79
-1
lines changed

1 file changed

+79
-1
lines changed

cloud-shadows-projection/utils.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def dilate_mask(mask: np.ndarray, dilation_size: int) -> np.ndarray:
2424
def get_hillshade_mask(eop: EOPatch, t_idx: int, dilation_size: int) -> np.ndarray:
2525
"""Calculates the hillshade mask for the given EOPatch and time index. The hillshade mask can be dilated."""
2626
dem_feature = (FeatureType.DATA_TIMELESS, "DEM")
27-
height, width, _ = eop.get_spatial_dimension(*dem_feature)
27+
height, width = eop.get_spatial_dimension(*dem_feature)
2828
resx, resy = bbox_to_resolution(eop.bbox, height=height, width=width)
2929

3030
dem = eop[dem_feature].squeeze(-1)
@@ -86,3 +86,81 @@ def get_shadow_candidates(eop: EOPatch, t_idx: int, lum_perc_thr: float, min_siz
8686
raw_mask = lum_mask & filter_mask
8787
raw_mask = connected_components_filter(raw_mask, min_size) # filter out small connected components
8888
return raw_mask > 0
89+
90+
91+
def project_cloud_mask(eop: EOPatch, t_idx: int, elevation: float) -> np.ndarray:
92+
"""Projects the cloud mask to the ground level assuming some cloud height and using the sun and view angles."""
93+
94+
# calculate the image resolution
95+
dem_feature = (FeatureType.DATA_TIMELESS, "DEM")
96+
h, w = eop.get_spatial_dimension(*dem_feature)
97+
resx, resy = bbox_to_resolution(eop.bbox, height=h, width=w)
98+
99+
# load the necessary data
100+
clm = eop[(FeatureType.MASK, "CLM")][t_idx].squeeze(-1) == 1
101+
hsm = eop[(FeatureType.MASK, "HSM")][t_idx].squeeze(-1)
102+
valid = eop[(FeatureType.MASK, "dataMask")][t_idx].squeeze(-1)
103+
104+
# obtain the solar and view angles
105+
mask = clm & valid & ~hsm
106+
sZ = eop.data["sunZenithAngles"][t_idx].squeeze(-1)[mask] * np.pi / 180
107+
sA = eop.data["sunAzimuthAngles"][t_idx].squeeze(-1)[mask] * np.pi / 180
108+
vZ = eop.data["viewZenithMean"][t_idx].squeeze(-1)[mask] * np.pi / 180
109+
vA = eop.data["viewAzimuthMean"][t_idx].squeeze(-1)[mask] * np.pi / 180
110+
sZ, sA, vZ, vA = list(map(np.nanmean, [sZ, sA, vZ, vA])) # calculate the average
111+
112+
# get pixel indices of the cloud mask and convert them to CRS coordinates
113+
rows_c, cols_c = np.where(mask)
114+
xmin, _ = eop.bbox.lower_left
115+
_, ymax = eop.bbox.upper_right
116+
xc = xmin + cols_c * resx
117+
yc = ymax - rows_c * resy
118+
119+
# project the cloud mask to the ground level in CRS coordinates
120+
xs = xc - elevation * (-np.tan(vZ) * np.sin(vA) + np.tan(sZ) * np.sin(sA))
121+
ys = yc - elevation * (np.tan(vZ) * np.cos(vA) + np.tan(sZ) * np.cos(sA))
122+
123+
# filter out NaN values
124+
nan_mask = np.isnan(xs) | np.isnan(ys)
125+
xc, yc, xs, ys = [array[~nan_mask] for array in [xc, yc, xs, ys]]
126+
127+
# convert the CRS coordinates back to pixel indices
128+
cols_s = (np.round(xs - xmin, decimals=-1) / resx).astype(int)
129+
rows_s = (np.round(ymax - ys, decimals=-1) / resy).astype(int)
130+
131+
# filter out-of-bounds values
132+
out_of_bounds = (cols_s < 0) | (cols_s >= w) | (rows_s < 0) | (rows_s >= h)
133+
cols_s = cols_s[~out_of_bounds]
134+
rows_s = rows_s[~out_of_bounds]
135+
136+
# create the cloud shadow mask
137+
shadow_mask = np.zeros_like(mask)
138+
shadow_mask[(rows_s, cols_s)] = 1
139+
shadow_mask[~valid] = 0
140+
141+
return (
142+
shadow_mask & ~clm & valid, # remove clouds and invalid pixels
143+
(np.round(xs[0] - xc[0], decimals=-1) / resx).astype(int), # projection offset in x
144+
(np.round(ys[0] - yc[0], decimals=-1) / resy).astype(int), # projection offset in y
145+
)
146+
147+
148+
def iou(a: np.ndarray, b: np.ndarray) -> float:
149+
"""Calculates the intersection over union between two binary masks."""
150+
return np.count_nonzero(a & b) / np.count_nonzero(a | b)
151+
152+
153+
def get_edge_mask(cols_off: int, rows_off: int, shape: tuple[int, int]) -> np.ndarray:
154+
"""Function to get the edge area of the mask after projection"""
155+
edge_offset_mask = np.zeros(shape, dtype=bool)
156+
if cols_off > 0:
157+
edge_offset_mask[:, :cols_off] = True
158+
else:
159+
edge_offset_mask[:, cols_off:] = True
160+
161+
if rows_off > 0:
162+
edge_offset_mask[-rows_off:] = True
163+
else:
164+
edge_offset_mask[:-rows_off] = True
165+
166+
return edge_offset_mask

0 commit comments

Comments
 (0)