361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542 | class ROISparse3D(Sparse3D):
"""Special version of a Sparse3D matrix which only populates and works with data within Regions of Interest."""
def __init__(
self,
data: np.ndarray,
row: np.ndarray,
col: np.ndarray,
imshape: Tuple[int, int],
nROIs: int,
ROI_size: Tuple[int, int],
ROI_corners: List[Tuple[int, int]],
imcorner: Tuple[int, int] = (0, 0),
) -> None:
"""
Initialize a Sparse3D instance with 3D dense data, and specify regions of interest that are required by the user.
This class is designed enable work with small, dense sub images within a larger, sparse image.
In some applications, we care about "regions of interest" within a larger image. Some examples;
1. Target Pixel Files from NASA Kepler are small regions of interest from a large image.
Positions of the TPFs in the larger image dictate the background expected, or the PSF shape expected.
Inside each Target Pixel File there might be many stars
2. Similarly, NASA Pandora will downlink regions of interest from a larger full frame image. The
regions of interet may contain several targets.
This gives us an updated image below:
+-------------------------------------------------+
| |
| +-----------+ +-----------+ |
| | +---+ | | +---+ | |
| | | A | | | | B | | |
| | +---+ | | +---+ | |
| | ROI 1 | | ROI 2 +---+ |
| +-----------+ +---------| E | |
| +---+ |
| |
| +-----------+ |
| | +---+ |
| | | C | |
| +-----------+ | +---+ |
| | +---+ | | ROI 3 | |
| | | D | | +-----------+ |
| | +---+ | |
| | ROI 4 | +---+ |
| +-----------+ | F | |
| +---+ |
+-------------------------------------------------+
In this case, we want to understand the images and their relative position within a larger image,
but we do not want to calculate any values outside of those regions of interest, and we want
the data to be returned to us with the shape of the region of interest. We do not expect the user
to provide data outside of the regions of interest. For example, in the above diagram the "F"
sub image is not close to a region of interest, and so would be superfluous.
Parameters
----------
data : np.ndarray
A dense 3D array containing data elements of shape `(nrows, ncols, n sub images)`.
The shape of data defines the size and number of dense sub images.
row : np.ndarray
A 3D array indicating the row indices of non-zero elements, with shape `(nrows, ncols, n sub images)`.
col : np.ndarray
A 3D array indicating the column indices of non-zero elements, with shape `(nrows, ncols, n sub images)`.
imshape : tuple of int
A tuple `(row, column)` defining the shape of the larger, sparse image.
nROIs: int
The number of regions of interest in the larger image
ROI_size: Tuple
The size the regions of interest in (row, column) pixels. All ROIs must be the same size.
ROI_corners: List[Tuple[int, int]]
The origin (lower left) corner positon for each of the ROIs. Must have length nROIs.
imcorner : tuple of int
A tuple `(row, column)` defining the corner of the larger, sparse image. Defaults to (0, 0)
Raises
------
ValueError
If `data`, `row`, or `col` are not 3D arrays, or if their third dimensions do not match.
ValueError
If corners are not passed for all ROIs
ValueError
If corners are not passed as tuples.
"""
self.nROIs = nROIs
self.ROI_size = ROI_size
self.ROI_corners = ROI_corners
self.imcorner = imcorner
self.get_ROI_mask = self._parse_ROIS(nROIs, ROI_size, ROI_corners)
super().__init__(data=data, row=row, col=col, imshape=imshape)
def _parse_ROIS(self, nROIs: int, ROI_size: tuple, ROI_corners: list):
"""Method checks the ROI inputs are allowable. Returns a function to obtain the boolean mask describing the ROIs"""
if not len(ROI_corners) == nROIs:
raise ValueError("Must pass corners for all ROIs.")
if not np.all([isinstance(corner, tuple) for corner in ROI_corners]):
raise ValueError("Pass corners as tuples.")
def get_ROI_masks_func(row, column):
mask = []
for roi in range(nROIs):
rmin, cmin = ROI_corners[roi]
rmax, cmax = rmin + ROI_size[0], cmin + ROI_size[1]
mask.append(
(row >= rmin)
& (row < rmax)
& (column >= cmin)
& (column < cmax)
)
return np.asarray(mask)
return get_ROI_masks_func
def __repr__(self):
return f"<{(*self.imshape, self.nsubimages)} ROISparse3D array of type {self.dtype}, {self.nROIs} Regions of Interest>"
# def _get_submask(self, offset=(0, 0)):
# # find where the data is within the array bounds
# kr = ((self.subrow + offset[0]) < self.imshape[0]) & (
# (self.subrow + offset[0]) >= 0
# )
# kc = ((self.subcol + offset[1]) < self.imshape[1]) & (
# (self.subcol + offset[1]) >= 0
# )
# # kroi = self.get_ROI_mask(self.subrow + offset[0], self.subcol + offset[0]).any(
# # axis=0
# # )
# return kr & kc & self._kz # & kroi
def dot(self, other):
"""
Compute the dot product with another array.
This method calculates the dot product of this ROISparse3D instance with a 1D or 2D `numpy.ndarray` or sparse array
If `other` is a 1D array, it will be treated as a column vector for the dot product. The resulting product is reshaped
to match the original image shape with an added dimension for multi-dimensional results if `other` is 2D.
Parameters
----------
other : np.ndarray, sparse.csr_matrix
A 1D or 2D array to perform the dot product with. The first dimension must be the number of sub images
in the Sparse3D instance (i.e should match `self.nsubimages`). If the vector is 1D, it will be recast to have shape
(n sub images, 1).
Returns
-------
np.ndarray
The resulting array from the dot product, reshaped to match the image dimensions `(self.nROIs, n, *self.ROI_size)`
where n is the length of the second dimension of `other`.
This will always be a 4D dataset.
"""
if isinstance(other, np.ndarray):
other = sparse.csr_matrix(other).T
if not sparse.issparse(other):
raise ValueError("Must pass a `sparse` array to dot.")
if not other.shape[0] == self.nsubimages:
if other.shape[1] == self.nsubimages:
other = other.T
else:
raise ValueError(
f"Must pass {(self.nsubimages, 1)} shape object."
)
sparse_array = super().tocsr().dot(other)
R, C = np.meshgrid(
np.arange(0, self.ROI_size[0]),
np.arange(0, self.ROI_size[1]),
indexing="ij",
)
array = np.zeros((self.nROIs, other.shape[1], *self.ROI_size))
for rdx, c in enumerate(self.ROI_corners):
idx = (R.ravel() + c[0] - self.imcorner[0]) * self.imshape[1] + (
C.ravel() + c[1] - self.imcorner[1]
)
k = (idx >= 0) & (idx < self.shape[0])
array[rdx, :, k.reshape(self.ROI_size)] = sparse_array[
idx[k]
].toarray() # ).reshape(self.ROI_size))
return array
|