@@ -166,11 +166,13 @@ def sub_size_radial_bins_from(
166166
167167 return sub_size_list [bin_indices ]
168168
169+ from autoarray .geometry import geometry_util
170+
169171def grid_2d_slim_over_sampled_via_mask_from (
170- mask_2d : np .ndarray ,
171- pixel_scales : ty .PixelScales ,
172- sub_size : np .ndarray ,
173- origin : Tuple [float , float ] = (0.0 , 0.0 ),
172+ mask_2d : np .ndarray ,
173+ pixel_scales : ty .PixelScales ,
174+ sub_size : np .ndarray ,
175+ origin : Tuple [float , float ] = (0.0 , 0.0 ),
174176) -> np .ndarray :
175177 """
176178 For a sub-grid, every unmasked pixel of its 2D mask with shape (total_y_pixels, total_x_pixels) is divided into
@@ -211,55 +213,144 @@ def grid_2d_slim_over_sampled_via_mask_from(
211213 grid_slim = grid_2d_slim_over_sampled_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), sub_size=1, origin=(0.0, 0.0))
212214 """
213215
214- H , W = mask_2d .shape
215- sy , sx = pixel_scales
216- oy , ox = origin
217-
218- # 1) Find unmasked pixel indices in row-major order
219- rows , cols = np .nonzero (~ mask_2d )
220- Npix = rows .size
221-
222- # 2) Broadcast or validate sub_size array
223- sub_arr = np .asarray (sub_size )
224- if sub_arr .ndim == 0 :
225- sub_arr = np .full (Npix , int (sub_arr ), int )
226- elif sub_arr .ndim == 1 and sub_arr .size == Npix :
227- sub_arr = sub_arr .astype (int )
228- else :
229- raise ValueError (f"sub_size must be scalar or length-{ Npix } array, got shape { sub_arr .shape } " )
230-
231- # 3) Compute pixel centers (y ↑ up, x → right)
232- cy = (H - 1 ) / 2.0
233- cx = (W - 1 ) / 2.0
234- y_pix = (cy - rows ) * sy + oy
235- x_pix = (cols - cx ) * sx + ox
236-
237- # 4) For each pixel, generate its sub-pixel coords and collect
238- coords_list = []
239- for i in range (Npix ):
240- s = sub_arr [i ]
241- dy = sy / s
242- dx = sx / s
243-
244- # y offsets: from top (+sy/2 - dy/2) down to bottom (-sy/2 + dy/2)
245- y_off = np .linspace (+ sy / 2 - dy / 2 , - sy / 2 + dy / 2 , s )
246- # x offsets: left to right
247- x_off = np .linspace (- sx / 2 + dx / 2 , + sx / 2 - dx / 2 , s )
248-
249- # build subgrid
250- y_sub , x_sub = np .meshgrid (y_off , x_off , indexing = "ij" )
251- y_sub = y_sub .ravel ()
252- x_sub = x_sub .ravel ()
253-
254- # center + offsets
255- y_center = y_pix [i ]
256- x_center = x_pix [i ]
257- coords = np .stack ([y_center + y_sub , x_center + x_sub ], axis = 1 )
258-
259- coords_list .append (coords )
260-
261- # 5) Concatenate all sub-pixel blocks in row-major pixel order
262- return np .vstack (coords_list )
216+ pixels_in_mask = (np .size (mask_2d ) - np .sum (mask_2d )).astype (int )
217+
218+ if isinstance (sub_size , int ):
219+ sub_size = np .full (
220+ fill_value = sub_size , shape = pixels_in_mask
221+ )
222+
223+ total_sub_pixels = np .sum (sub_size ** 2 )
224+
225+ grid_slim = np .zeros (shape = (total_sub_pixels , 2 ))
226+
227+ centres_scaled = geometry_util .central_scaled_coordinate_2d_from (
228+ shape_native = mask_2d .shape , pixel_scales = pixel_scales , origin = origin
229+ )
230+
231+ index = 0
232+ sub_index = 0
233+
234+ for y in range (mask_2d .shape [0 ]):
235+ for x in range (mask_2d .shape [1 ]):
236+ if not mask_2d [y , x ]:
237+ sub = sub_size [index ]
238+
239+ y_sub_half = pixel_scales [0 ] / 2
240+ y_sub_step = pixel_scales [0 ] / (sub )
241+
242+ x_sub_half = pixel_scales [1 ] / 2
243+ x_sub_step = pixel_scales [1 ] / (sub )
244+
245+ y_scaled = (y - centres_scaled [0 ]) * pixel_scales [0 ]
246+ x_scaled = (x - centres_scaled [1 ]) * pixel_scales [1 ]
247+
248+ for y1 in range (sub ):
249+ for x1 in range (sub ):
250+ grid_slim [sub_index , 0 ] = - (
251+ y_scaled - y_sub_half + y1 * y_sub_step + (y_sub_step / 2.0 )
252+ )
253+ grid_slim [sub_index , 1 ] = (
254+ x_scaled - x_sub_half + x1 * x_sub_step + (x_sub_step / 2.0 )
255+ )
256+ sub_index += 1
257+
258+ index += 1
259+
260+ return grid_slim
261+
262+
263+ #
264+
265+ # def grid_2d_slim_over_sampled_via_mask_from(
266+ # mask_2d: np.ndarray,
267+ # pixel_scales: ty.PixelScales,
268+ # sub_size: np.ndarray,
269+ # origin: Tuple[float, float] = (0.0, 0.0),
270+ # ) -> np.ndarray:
271+ # """
272+ # For a sub-grid, every unmasked pixel of its 2D mask with shape (total_y_pixels, total_x_pixels) is divided into
273+ # a finer uniform grid of shape (total_y_pixels*sub_size, total_x_pixels*sub_size). This routine computes the (y,x)
274+ # scaled coordinates at the centre of every sub-pixel defined by this 2D mask array.
275+ #
276+ # The sub-grid is returned on an array of shape (total_unmasked_pixels*sub_size**2, 2). y coordinates are
277+ # stored in the 0 index of the second dimension, x coordinates in the 1 index. Masked coordinates are therefore
278+ # removed and not included in the slimmed grid.
279+ #
280+ # Grid2D are defined from the top-left corner, where the first unmasked sub-pixel corresponds to index 0.
281+ # Sub-pixels that are part of the same mask array pixel are indexed next to one another, such that the second
282+ # sub-pixel in the first pixel has index 1, its next sub-pixel has index 2, and so forth.
283+ #
284+ # Parameters
285+ # ----------
286+ # mask_2d
287+ # A 2D array of bools, where `False` values are unmasked and therefore included as part of the calculated
288+ # sub-grid.
289+ # pixel_scales
290+ # The (y,x) scaled units to pixel units conversion factor of the 2D mask array.
291+ # sub_size
292+ # The size of the sub-grid that each pixel of the 2D mask array is divided into.
293+ # origin
294+ # The (y,x) origin of the 2D array, which the sub-grid is shifted around.
295+ #
296+ # Returns
297+ # -------
298+ # ndarray
299+ # A slimmed sub grid of (y,x) scaled coordinates at the centre of every pixel unmasked pixel on the 2D mask
300+ # array. The sub grid array has dimensions (total_unmasked_pixels*sub_size**2, 2).
301+ #
302+ # Examples
303+ # --------
304+ # mask = np.array([[True, False, True],
305+ # [False, False, False]
306+ # [True, False, True]])
307+ # grid_slim = grid_2d_slim_over_sampled_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), sub_size=1, origin=(0.0, 0.0))
308+ # """
309+ #
310+ # H, W = mask_2d.shape
311+ # sy, sx = pixel_scales
312+ # oy, ox = origin
313+ #
314+ # # 1) Find unmasked pixel indices in row-major order
315+ # rows, cols = np.nonzero(~mask_2d)
316+ # Npix = rows.size
317+ #
318+ # # 2) Broadcast or validate sub_size array
319+ # sub_arr = np.asarray(sub_size)
320+ # sub_arr = np.full(Npix, sub_arr, dtype=int) if sub_arr.size == 1 else sub_arr
321+ #
322+ # # 3) Compute pixel centers (y ↑ up, x → right)
323+ # cy = (H - 1) / 2.0
324+ # cx = (W - 1) / 2.0
325+ # y_pix = (cy - rows) * sy + oy
326+ # x_pix = (cols - cx) * sx + ox
327+ #
328+ # # 4) For each pixel, generate its sub-pixel coords and collect
329+ # coords_list = []
330+ # for i in range(Npix):
331+ # s = sub_arr[i]
332+ # dy = sy / s
333+ # dx = sx / s
334+ #
335+ # # y offsets: from top (+sy/2 - dy/2) down to bottom (-sy/2 + dy/2)
336+ # y_off = np.linspace(+sy/2 - dy/2, -sy/2 + dy/2, s)
337+ # # x offsets: left to right
338+ # x_off = np.linspace(-sx/2 + dx/2, +sx/2 - dx/2, s)
339+ #
340+ # # build subgrid
341+ # y_sub, x_sub = np.meshgrid(y_off, x_off, indexing="ij")
342+ # y_sub = y_sub.ravel()
343+ # x_sub = x_sub.ravel()
344+ #
345+ # # center + offsets
346+ # y_center = y_pix[i]
347+ # x_center = x_pix[i]
348+ # coords = np.stack([y_center + y_sub, x_center + x_sub], axis=1)
349+ #
350+ # coords_list.append(coords)
351+ #
352+ # # 5) Concatenate all sub-pixel blocks in row-major pixel order
353+ # return np.vstack(coords_list)
263354
264355
265356def over_sample_size_via_radial_bins_from (
0 commit comments