@@ -24,7 +24,7 @@ def dilate_mask(mask: np.ndarray, dilation_size: int) -> np.ndarray:
24
24
def get_hillshade_mask (eop : EOPatch , t_idx : int , dilation_size : int ) -> np .ndarray :
25
25
"""Calculates the hillshade mask for the given EOPatch and time index. The hillshade mask can be dilated."""
26
26
dem_feature = (FeatureType .DATA_TIMELESS , "DEM" )
27
- height , width , _ = eop .get_spatial_dimension (* dem_feature )
27
+ height , width = eop .get_spatial_dimension (* dem_feature )
28
28
resx , resy = bbox_to_resolution (eop .bbox , height = height , width = width )
29
29
30
30
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
86
86
raw_mask = lum_mask & filter_mask
87
87
raw_mask = connected_components_filter (raw_mask , min_size ) # filter out small connected components
88
88
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