Exemple #1
0
 def __init__(self, image, xy_start, xy_end, foreground_radius, gap, background_width,
              source_id=None, obs_id=None):
     """ Main and probably sole constructor for this class.
     :param image: the parent image array [numpy ndarray].
     :param xy_start: x,y pixel position in parent image of the beginning of the MP's motion.
            [XY object, 2-tuple, 2-list, or 2-array of floats]
     :param xy_end:x,y pixel position in parent image of the beginning of the MP's motion.
            [XY object, 2-tuple, 2-list, or 2-array of floats]
     :param foreground_radius: radius of the aperture source end-caps, in pixels.
            Does not include effect of MP motion. [float]
     :param gap: Gap in pixels between foreground mask and background mask. [float]
     :param background_width: Width in pixels of background mask. [float]
     :param source_id: optional string describing the source (e.g., comp star ID, MP number) that this
            aperture is intended to measure. A tag only, not used in calculations. [string]
     :param obs_id: optional observation ID string, will typically be unique among all observations
            (aperture objects) in one image. A tag only, not used in calculations. [string]
     """
     self.xy_start = xy_start if isinstance(xy_start, XY) else XY(xy_start[0], xy_start[1])
     self.xy_end = xy_end if isinstance(xy_end, XY) else XY(xy_end[0], xy_end[1])
     self.foreground_radius = foreground_radius
     self.gap = gap
     self.background_width = background_width
     self.background_inner_radius = self.foreground_radius + self.gap
     self.background_outer_radius = self.foreground_radius + self.gap + self.background_width
     xy_center = self.xy_start + (self.xy_end - self.xy_start) / 2
     corner_x_values = (self.xy_start.x + self.background_outer_radius,
                        self.xy_start.x - self.background_outer_radius,
                        self.xy_end.x + self.background_outer_radius,
                        self.xy_end.x - self.background_outer_radius)
     x_min = min(corner_x_values)
     x_max = max(corner_x_values)
     corner_y_values = (self.xy_start.y + self.background_outer_radius,
                        self.xy_start.y - self.background_outer_radius,
                        self.xy_end.y + self.background_outer_radius,
                        self.xy_end.y - self.background_outer_radius)
     y_min = min(corner_y_values)
     y_max = max(corner_y_values)
     dxy_cutout_size = DXY(int(round(x_max - x_min + 4)), int(round(y_max - y_min + 4)))
     dxy_offset = DXY(int(round(xy_center.x) - dxy_cutout_size.dx / 2.0),
                      int(round(xy_center.y) - dxy_cutout_size.dy / 2.0))
     xy_start_cutout = self.xy_start - dxy_offset
     xy_end_cutout = self.xy_end - dxy_offset
     foreground_mask = make_pill_mask(tuple(dxy_cutout_size),
                                      tuple(xy_start_cutout), tuple(xy_end_cutout),
                                      self.foreground_radius)
     background_inner_mask = make_pill_mask(tuple(dxy_cutout_size),
                                            tuple(xy_start_cutout), tuple(xy_end_cutout),
                                            self.background_inner_radius)
     background_outer_mask = make_pill_mask(tuple(dxy_cutout_size),
                                            tuple(xy_start_cutout), tuple(xy_end_cutout),
                                            self.background_outer_radius)
     background_mask = np.logical_or(background_outer_mask,
                                     np.logical_not(background_inner_mask))
     super().__init__(image, xy_center, dxy_offset, foreground_mask, background_mask, source_id, obs_id)
     self.motion = (self.xy_end - self.xy_start).length
     sigma2_motion = (self.motion ** 2) / 12.0  # sigma2 of uniform segment distribution.
     sigma2 = (1 * (self.stats.semimajor_sigma.value ** 2 - sigma2_motion) +
               2 * (self.stats.semiminor_sigma.value ** 2)) / 3
     self.sigma = sqrt(sigma2)
     self.fwhm = self.sigma * FWHM_PER_SIGMA
Exemple #2
0
def make_pill_mask(mask_shape_xy, xya, xyb, radius):
    """ Construct a mask array for MP in motion: unmask only those pixels within radius pixels of
        any point in line segment from xya to xyb. Convention: pixel True -> VALID (opposite of numpy).
    :param mask_shape_xy: (x,y) size of array to generate. [2-tuple of ints]
    :param xya: (xa, ya) pixel coordinates of start-motion point. [XY object, or 2-tuple of floats]
    :param xyb: (xb, yb) pixel coordinates of end-motion point. [XY object, or 2-tuple of floats]
    :param radius: radius of ends and half-width of center region. [float]
    :return: mask array, True -> MASKED out/invalid (numpy boolean convention),
             and indexed as (x,y) (indexing convention is x,y-image, not numpy). [np.ndarray of booleans]
    """
    xya = xya if isinstance(xya, XY) else XY(xya[0], xya[1])
    xyb = xyb if isinstance(xyb, XY) else XY(xyb[0], xyb[1])
    if xya == xyb:
        return make_circular_mask(max(mask_shape_xy), xya, radius)

    # Make circle and rectangle objects:
    circle_a = Circle_in_2D(xya, radius)
    circle_b = Circle_in_2D(xyb, radius)
    dxy_ab = xyb - xya
    length_ab = dxy_ab.length
    dxy_a_corner1 = (radius / length_ab) * DXY(dxy_ab.dy, -dxy_ab.dx)  # perpendicular to ab vector.
    dxy_a_corner2 = (radius / length_ab) * DXY(-dxy_ab.dy, dxy_ab.dx)  # "
    xy_corner1 = xya + dxy_a_corner1
    xy_corner2 = xya + dxy_a_corner2
    xy_corner3 = xyb + dxy_a_corner2
    rectangle = Rectangle_in_2D(xy_corner1, xy_corner2, xy_corner3)

    # Make mask, including edges so no gaps can appear at rectangle corners:
    circle_a_contains = circle_a.contains_points_unitgrid(0, mask_shape_xy[0] - 1,
                                                          0, mask_shape_xy[1] - 1, True)
    circle_b_contains = circle_b.contains_points_unitgrid(0, mask_shape_xy[0] - 1,
                                                          0, mask_shape_xy[1] - 1, True)
    rectangle_contains = rectangle.contains_points_unitgrid(0, mask_shape_xy[0] - 1,
                                                            0, mask_shape_xy[1] - 1, True)
    # Render each in numpy mask-boolean and index conventions:
    # circle_a_mask = np.transpose(np.logical_not(circle_a_contains))
    # circle_b_mask = np.transpose(np.logical_not(circle_b_contains))
    # rectangle_mask = np.transpose(np.logical_not(rectangle_contains))
    circle_a_mask = np.logical_not(circle_a_contains)
    circle_b_mask = np.logical_not(circle_b_contains)
    rectangle_mask = np.logical_not(rectangle_contains)
    mask = np.logical_and(np.logical_and(circle_a_mask, circle_b_mask), rectangle_mask)
    # any_contains = np.logical_or(np.logical_or(circle_a_contains, circle_b_contains), rectangle_contains)
    # mask = np.transpose(np.logical_not(any_contains))
    return mask
Exemple #3
0
 def test_constructor_masks_inside_image(self, get_image):
     hdr, im = get_image
     ap = image.MovingSourceAp(im,
                               xy_start=(2354, 1505),
                               xy_end=(2361, 1510),
                               foreground_radius=9,
                               gap=6,
                               background_width=5)
     # Quality criteria:
     assert ap.is_valid is True
     assert ap.all_inside_image is True
     assert ap.all_outside_image is False
     assert ap.any_foreground_outside_image is False
     assert ap.mask_overlap_pixel_count == 0
     # Values as input:
     assert (ap.foreground_radius, ap.gap, ap.background_width) == (9, 6, 5)
     assert np.array_equal(ap.image, im)
     assert ap.xy_start == XY(2354, 1505)
     assert ap.xy_end == XY(2361, 1510)
     # Shapes and pixel counts:
     assert ap.cutout.shape == ap.foreground_mask.shape == ap.background_mask.shape == (
         49, 51)  # (y,x)
     assert ap.foreground_pixel_count == 408
     assert ap.foreground_pixel_count == np.sum(
         ap.input_foreground_mask == False)
     assert ap.background_pixel_count == 633
     assert ap.background_pixel_count == np.sum(
         ap.input_background_mask == False)
     # ADUs and fluxes:
     assert ap.background_level == pytest.approx(254, abs=1)
     assert ap.background_std == pytest.approx(15.1, abs=0.2)
     assert ap.foreground_max == 1817
     assert ap.foreground_min == 224
     assert ap.raw_flux == pytest.approx(210000, abs=100)
     assert ap.net_flux == pytest.approx(106368, abs=100)
     assert ap.flux_stddev(gain=1.57) == pytest.approx(366, abs=1)
     # Source flux position & shape:
     assert ap.xy_centroid[0] == pytest.approx(2357.56, abs=0.05)
     assert ap.xy_centroid[1] == pytest.approx(1507.14, abs=0.05)
     assert ap.sigma == pytest.approx(3.14, abs=0.05)
     assert ap.fwhm == pytest.approx(7.40, abs=0.05)
Exemple #4
0
 def make_new_object(self, new_xy_center):
     """ Make new object using new xy_center. Overrides parent-class method.
         Masks will be recreated by the constructor, using new xy_center.
     """
     if not isinstance(new_xy_center, XY):
         new_xy_center = XY(new_xy_center[0], new_xy_center[1])
     current_xy_center = self.xy_start + (self.xy_end - self.xy_start) / 2
     dxy_shift = new_xy_center - current_xy_center
     new_xy_start = self.xy_start + dxy_shift
     new_xy_end = self.xy_end + dxy_shift
     return MovingSourceAp(self.image, new_xy_start, new_xy_end,
                           self.foreground_radius, self.gap, self.background_width,
                           self.source_id, self.obs_id)
Exemple #5
0
def make_circular_mask(mask_size, xy_origin, radius):
    """ Construct a traditional mask array for small, stationary object, esp. for a star.
        Unmask only those pixels *within* radius pixels of a given point. Invert the mask separately to
            mask the interior.
        Numpy mask bookean convention: pixel True -> MASKED OUT.
        Numpy indexing convention: (y,x)
    :param mask_size: edge size of new mask array, which will be a square. [int]
    :param xy_origin: (x, y) pixel coordinates of circle origin, rel. to mask origin. [2-tuple of floats]
    :param radius: radius of ends and half-width of center region. [float]
    :return: mask array, True -> MASKED out/invalid (numpy boolean convention),
             and indexed as (y,x) (numpy indexing convention). [np.ndarray of booleans]
    """
    xy_origin = xy_origin if isinstance(xy_origin, XY) else XY(xy_origin[0], xy_origin[1])
    circle = Circle_in_2D(xy_origin=xy_origin, radius=radius)
    is_inside = circle.contains_points_unitgrid(0, mask_size - 1, 0, mask_size - 1, include_edges=True)
    mask = np.transpose(np.logical_not(is_inside))  # render in numpy mask-boolean and index conventions.
    return mask
Exemple #6
0
 def test_constructor_masks_inside_image(self, get_image):
     hdr, im = get_image
     ap = image.PointSourceAp(im,
                              xy_center=(1476.3, 1243.7),
                              foreground_radius=9,
                              gap=6,
                              background_width=5)
     # Quality criteria:
     assert ap.is_valid is True
     assert ap.all_inside_image is True
     assert ap.all_outside_image is False
     assert ap.any_foreground_outside_image is False
     assert ap.mask_overlap_pixel_count == 0
     # Values as input:
     assert (ap.foreground_radius, ap.gap, ap.background_width) == (9, 6, 5)
     assert ap.annulus_inner_radius == ap.foreground_radius + ap.gap
     assert ap.annulus_outer_radius == ap.foreground_radius + ap.gap + ap.background_width
     assert np.array_equal(ap.image, im)
     assert ap.xy_center == XY(1476.3, 1243.7)
     assert ap.input_foreground_mask.shape == (44, 44)
     assert ap.input_background_mask.shape == ap.input_foreground_mask.shape
     # Shapes and pixel counts:
     assert ap.cutout.shape == ap.foreground_mask.shape == ap.background_mask.shape == (
         44, 44)
     assert ap.foreground_pixel_count == np.sum(
         ap.input_foreground_mask == False) == 255
     assert ap.background_pixel_count == np.sum(
         ap.input_background_mask == False) == 549
     # ADUs and fluxes:
     assert ap.background_level == pytest.approx(257, abs=1)
     assert ap.background_std == pytest.approx(13.84, abs=0.1)
     assert ap.foreground_max == 1065
     assert ap.foreground_min == 234
     assert ap.raw_flux == pytest.approx(96364, abs=10)
     assert ap.net_flux == pytest.approx(30829, abs=10)
     assert ap.flux_stddev(gain=1.57) == pytest.approx(247.95, abs=0.1)
     # Source flux position & shape:
     assert ap.xy_centroid[0] == pytest.approx(1476.23, abs=0.01)
     assert ap.xy_centroid[1] == pytest.approx(1243.39, abs=0.01)
     assert ap.sigma == pytest.approx(2.81, abs=0.1)
     assert ap.fwhm == pytest.approx(6.37, abs=0.1)
     assert ap.elongation == pytest.approx(1.086, abs=0.02)
Exemple #7
0
 def __init__(self, image, xy_center, foreground_radius, gap, background_width,
              source_id=None, obs_id=None):
     """ Main and probably sole constructor for this class.
     :param image: the parent image array [numpy ndarray].
     :param xy_center: center pixel position in parent. This should be the best prior estimate
            of the light source's centroid at mid-exposure, as (x,y) (not as numpy [y, x] array).
            [XY object, 2-tuple, 2-list, or 2-array of floats]
     :param foreground_radius: radial size of foreground around point source, in pixels. [float]
     :param gap: width of gap, difference between radius of foreground and inside radius of
            background annulus, in pixels. [float]
     :param background_width: width of annulus, difference between inside and outside radii
            of background annulus, in pixels.
     :param source_id: string describing the source (e.g., comp star ID, MP number) that this
            aperture is intended to measure. A tag only, not used in calculations. [string]
     :param obs_id: observation ID string, will typically be unique among all observations
            (aperture objects) in one image. A tag only, not used in calculations. [string]
     """
     xy_center = xy_center if isinstance(xy_center, XY) else XY(xy_center[0], xy_center[1])
     self.foreground_radius = foreground_radius
     self.gap = gap
     self.background_width = background_width
     self.annulus_inner_radius = self.foreground_radius + self.gap
     self.annulus_outer_radius = self.annulus_inner_radius + self.background_width
     cutout_size = int(ceil(2 * self.annulus_outer_radius)) + 4  # generous, for safety.
     dxy_origin = DXY(int(round(xy_center.x - cutout_size / 2)),
                      int(round(xy_center.y - cutout_size / 2)))
     xy_center_in_cutout = xy_center - dxy_origin
     foreground_mask = make_circular_mask(mask_size=cutout_size, xy_origin=tuple(xy_center_in_cutout),
                                          radius=self.foreground_radius)
     background_mask_center_disc = np.logical_not(make_circular_mask(cutout_size,
                                                                     tuple(xy_center_in_cutout),
                                                                     self.annulus_inner_radius))
     background_mask_outer_disc = make_circular_mask(cutout_size, tuple(xy_center_in_cutout),
                                                     self.annulus_outer_radius)
     background_mask = np.logical_or(background_mask_center_disc, background_mask_outer_disc)
     super().__init__(image, xy_center, dxy_origin, foreground_mask, background_mask, source_id, obs_id)
Exemple #8
0
    def __init__(self, image, xy_center, xy_offset, foreground_mask, background_mask=None,
                 source_id='', obs_id=''):
        """ General constructor, from explicitly passed-in parent data array and 2 mask arrays.
        :param image: the parent image array [numpy ndarray; to pass in CCDData or numpy masked array,
                   please see separate, specific constructors, below].
        :param xy_center: center pixel position in parent. This should be the best prior estimate
                   of the light source's centroid at mid-exposure, as (x,y) (not as numpy [y, x] array).
                   [XY object, 2-tuple, 2-list, or 2-array of floats]
        :param xy_offset: lowest index of cutout (upper-left corner of image), that is, the offset of
                   cutout's incides from parent image's indices.
                   [XY object, 2-tuple, 2-list, or 2-array of floats]
        :param foreground_mask: mask array for pixels to be counted in flux, centroid, etc.
                   Required boolean array. Numpy mask convention (True -> pixel masked out, unused).
                   Mask shape defines shape of cutout to be used. [numpy ndarray of booleans]
        :param background_mask: mask array for pixels to be counted in background flux, centroid, etc.
                   If None, will be set to inverse of foreground_mask.
                   If zero [int or float], will be set to zero (that is, background is not used).
                   Otherwise (normal case), must be boolean array in same shape as foreground_mask.
                   Numpy mask convention (True -> pixel masked out, unused).
        :param source_id: optional string describing the source (e.g., comp star ID, MP number) that this
                   aperture is intended to measure. [string]
        :param obs_id: optional observation ID string, will typically be unique among all observations
                   (aperture objects) in one image. [string]
        """
        # Save inputs:
        self.image = image
        self.xy_center = xy_center if isinstance(xy_center, XY) else XY(xy_center[0], xy_center[1])
        xy_input_offset = xy_offset if isinstance(xy_offset, XY) else XY(xy_offset[0], xy_offset[1])
        self.input_foreground_mask = foreground_mask
        self.input_background_mask = background_mask
        self.source_id = str(source_id)
        self.obs_id = str(obs_id)

        # Default values:
        self.is_valid = None
        self.all_inside_image = None
        self.all_outside_image = None
        self.any_foreground_outside_image = None
        self.mask_overlap_pixel_count = None

        # Ensure background mask is boolean array of same shape as foreground mask, whatever was input:
        if self.input_background_mask is None:
            self.background_mask = np.logical_not(self.input_foreground_mask)
        elif type(self.input_background_mask) in (int, float) and self.input_background_mask == 0:
            self.background_mask = np.full_like(self.input_foreground_mask, fill_value=True, dtype=np.bool)
        elif isinstance(self.input_background_mask, np.ndarray):
            if self.input_background_mask.shape != self.input_foreground_mask.shape:
                raise MaskError('Foreground and background masks differ in shape.')
            self.background_mask = self.input_background_mask
        else:
            raise MaskError('Background mask type ' + str(type(self.input_background_mask)) +
                            ' is not valid.')

        # Determine whether any foreground mask pixels fall outside of parent image:
        x_low_raw = xy_input_offset.x
        y_low_raw = xy_input_offset.y
        x_high_raw = xy_input_offset.x + self.input_foreground_mask.shape[1] - 1
        y_high_raw = xy_input_offset.y + self.input_foreground_mask.shape[0] - 1
        x_low_final = max(x_low_raw, 0)
        y_low_final = max(y_low_raw, 0)
        x_high_final = min(x_high_raw, image.shape[1] - 1)
        y_high_final = min(y_high_raw, image.shape[0] - 1)
        self.all_inside_image = ((x_low_final, y_low_final, x_high_final, y_high_final) ==
                                 (x_low_raw, y_low_raw, x_high_raw, y_high_raw))

        # Make the cutout array. Crop masks if any pixels fall outside of parent image:
        # (NB: we must crop and not merely mask, to ensure that no indices fall outside parent image.)
        self.cutout = image[y_low_final: y_high_final + 1, x_low_final: x_high_final + 1]
        self.foreground_mask = self.input_foreground_mask[y_low_final-y_low_raw:y_high_final-y_low_raw + 1,
                                                          x_low_final-x_low_raw:x_high_final-x_low_raw + 1]
        self.background_mask = self.background_mask[y_low_final-y_low_raw:y_high_final-y_low_raw + 1,
                                                    x_low_final-x_low_raw:x_high_final-x_low_raw + 1]
        self.xy_offset = x_low_final, y_low_final

        # Invalidate aperture if entirely outside image (not an error, but aperture cannot be used):
        self.all_outside_image = (self.cutout.size <= 0)
        if self.all_outside_image:
            self.any_foreground_outside_image = True
            self.is_valid = False
            return

        # Compute pixels in both masks (should always be zero):
        self.mask_overlap_pixel_count = np.sum(np.logical_and((self.foreground_mask == False),
                                                              (self.background_mask == False)))

        # Invalidate aperture if any foreground pixels were lost in cropping:
        self.foreground_pixel_count = np.sum(self.foreground_mask == False)
        input_foreground_pixel_count = np.sum(self.input_foreground_mask == False)
        self.any_foreground_outside_image = \
            bool(self.foreground_pixel_count != input_foreground_pixel_count)
        if self.any_foreground_outside_image:
            self.is_valid = False
            return

        # Compute background mask statistics:
        self.background_pixel_count = np.sum(self.background_mask == False)
        if self.background_pixel_count >= 1:
            self.background_level, self.background_std = calc_background_value(self.cutout,
                                                                               self.background_mask)
        else:
            self.background_level, self.background_std = 0.0, 0.0

        # Compute aperture statistics:
        foreground_ma = np.ma.array(data=self.cutout, mask=self.foreground_mask)
        self.foreground_max = np.ma.max(foreground_ma)
        self.foreground_min = np.ma.min(foreground_ma)
        self.raw_flux = np.ma.sum(foreground_ma)
        cutout_net_ma = np.ma.array(self.cutout - self.background_level, mask=self.foreground_mask)
        self.net_flux = np.sum(cutout_net_ma)
        # self.stats = data_properties(data=cutout_net_ma.data, mask=self.foreground_mask,
        #                              background=self.background_level)
        # scalar background fails photutils v.1.1.0, must have array matching shape of data & mask:
        self.stats = data_properties(data=cutout_net_ma.data, mask=self.foreground_mask,
                                     background=np.full_like(cutout_net_ma.data,
                                                             fill_value=self.background_level))
        self.xy_centroid = (self.stats.xcentroid + self.xy_offset[0],
                            self.stats.ycentroid + self.xy_offset[1])
        # Sigma and FWHM apply to spreading *other than* motion (i.e., approx. perpendicular to motion).
        self.sigma = self.stats.semimajor_sigma.value
        # self.fwhm = self.sigma * FWHM_PER_SIGMA  # obsolete.
        self.fwhm = self.stats.fwhm.value  # given by photutils v.1.1.0
        self.elongation = self.stats.elongation.value
        self.is_valid = True