Ejemplo n.º 1
0
def test_mean(testdata, testvar, testmask):
    out_data, out_mask, out_var = NDStacker.mean(testdata)
    assert_allclose(out_data, 2)
    assert_allclose(out_var, 0.5)
    assert out_mask is None

    out_data, out_mask, out_var = NDStacker.mean(testdata, variance=testvar)
    assert_allclose(out_data, 2)
    assert_allclose(out_var, 0.3)

    out_data, out_mask, out_var = NDStacker.mean(testdata, mask=testmask)
    assert_allclose(out_data, [2., 3.])
    assert_array_almost_equal(out_var, [0.5, 0.33], decimal=2)
    assert_allclose(out_mask, 0)

    out_data, out_mask, out_var = NDStacker.mean(testdata,
                                                 mask=testmask,
                                                 variance=testvar)
    assert_allclose(out_data, [2., 3.])
    assert_array_almost_equal(out_var, [0.3, 0.66], decimal=2)
    assert_allclose(out_mask, 0)
Ejemplo n.º 2
0
    def addOIWFSToDQ(self, adinputs=None, **params):
        """
        Flags pixels affected by the On-Instrument Wavefront Sensor (OIWFS) on a
        GMOS image.

        It uses the header information to determine the location of the
        guide star, and basically "flood-fills" low-value pixels around it to
        give a first estimate. This map is then grown pixel-by-pixel until the
        values of the new pixels it covers stop increasing (indicating it's got to the
        sky level).

        Extensions to the right of the one with the guide star are handled by
        taking a starting point near the left-hand edge of the extension, level
        with the location at which the probe met the right-hand edge of the
        previous extension.

        This code assumes that data_section extends over all rows. It is, of
        course, very GMOS-specific.

        Parameters
        ----------
        adinputs : list of :class:`~gemini_instruments.gmos.AstroDataGmos`
            Science data that contains the shadow of the OIWFS.

        contrast : float (range 0-1)
            Initial fractional decrease from sky level to minimum brightness
            where the OIWFS "edge" is defined.

        convergence : float
            Amount within which successive sky level measurements have to
            agree during dilation phase for this phase to finish.

        Returns
        -------
        list of :class:`~gemini_instruments.gmos.AstroDataGmos`
            Data with updated `.DQ` plane considering the shadow of the OIWFS.
        """
        log = self.log
        log.debug(gt.log_message("primitive", self.myself(), "starting"))
        border = 5  # Pixels in from edge where sky level is reliable
        boxsize = 5
        contrast = params["contrast"]
        convergence = params["convergence"]

        for ad in adinputs:
            wfs = ad.wavefront_sensor()
            if wfs is None or 'OIWFS' not in wfs:
                log.fullinfo('OIWFS not used for image {}.'.format(
                    ad.filename))
                continue

            oira = ad.phu.get('OIARA')
            oidec = ad.phu.get('OIADEC')
            if oira is None or oidec is None:
                log.warning('Cannot determine location of OI probe for {}.'
                            'Continuing.'.format(ad.filename))
                continue

            # DQ planes must exist so the unilluminated region is flagged
            if np.any([ext.mask is None for ext in ad]):
                log.warning('No DQ plane for {}. Continuing.'.format(
                    ad.filename))
                continue

            # OIWFS comes in from the right, so we need to have the extensions
            # sorted in order from left to right
            ampsorder = list(
                np.argsort([detsec.x1 for detsec in ad.detector_section()]))
            datasec_list = ad.data_section()
            gs_index = -1
            for index in ampsorder:
                ext = ad[index]
                wcs = WCS(ext.hdr)
                x, y = wcs.all_world2pix([[oira, oidec]], 0)[0]
                if x < datasec_list[index].x2 + 0.5:
                    gs_index = index
                    log.fullinfo('Guide star location found at ({:.2f},{:.2f})'
                                 ' on EXTVER {}'.format(
                                     x, y, ext.hdr['EXTVER']))
                    break
            if gs_index == -1:
                log.warning(
                    'Could not find OI probe location on any extensions.')
                continue

            # The OIWFS extends to the left of the actual star location, which
            # might have it vignetting a part of an earlier extension. Also, it
            # may be in a chip gap, which has the same effect
            amp_index = ampsorder.index(gs_index)
            if x < 50:
                amp_index -= 1
                x = (datasec_list[ampsorder[amp_index]].x2 -
                     datasec_list[ampsorder[amp_index]].x1 - border)
            else:
                x -= datasec_list[ampsorder[amp_index]].x1

            dilator = ndimage.morphology.generate_binary_structure(2, 1)
            for index in ampsorder[amp_index:]:
                datasec = datasec_list[index]
                sky, skysig, _ = gt.measure_bg_from_image(ad[index])

                # To avoid hassle with whether the overscan region is present
                # or not and how adjacent extensions relate to each other,
                # just deal with the data sections
                data_region = ad[index].data[:, datasec.x1:datasec.x2]
                mask_region = ad[index].mask[:, datasec.x1:datasec.x2]
                x1 = max(int(x - boxsize), border)
                x2 = max(min(int(x + boxsize), datasec.x2 - datasec.x1),
                         x1 + border)

                # Try to find the minimum closest to our estimate of the
                # probe location, by downhill method on a spline fit (to
                # smooth out the noise)
                data, mask, var = NDStacker.mean(ad[index].data[:, x1:x2].T,
                                                 mask=ad[index].mask[:,
                                                                     x1:x2].T)

                good_rows = np.logical_and(mask == DQ.good, var > 0)

                if np.sum(good_rows) == 0:
                    log.warning("No good rows in {} extension {}".format(
                        ad.filename, index))
                    continue

                rows = np.arange(datasec.y2 - datasec.y1)
                spline = UnivariateSpline(rows[good_rows],
                                          data[good_rows],
                                          w=1. / np.sqrt(var[good_rows]))
                newy = int(
                    optimize.minimize(spline, y, method='CG').x[0] + 0.5)
                y1 = max(int(newy - boxsize), 0)
                y2 = max(min(int(newy + boxsize), len(rows)), y1 + border)
                wfs_sky = np.median(data_region[y1:y2, x1:x2])
                if wfs_sky > sky - convergence:
                    log.warning('Cannot distinguish probe region from sky for '
                                '{}'.format(ad.filename))
                    break

                # Flood-fill region around guide-star with all pixels fainter
                # than this boundary value
                boundary = sky - contrast * (sky - wfs_sky)
                regions, nregions = ndimage.measurements.label(
                    np.logical_and(data_region < boundary, mask_region == 0))
                wfs_region = regions[newy, int(x + 0.5)]
                blocked = ndimage.morphology.binary_fill_holes(
                    np.where(regions == wfs_region, True, False))
                this_mean_sky = wfs_sky
                condition_met = False
                while not condition_met:
                    last_mean_sky = this_mean_sky
                    new_blocked = ndimage.morphology.binary_dilation(
                        blocked, structure=dilator)
                    this_mean_sky = np.median(data_region[new_blocked
                                                          ^ blocked])
                    blocked = new_blocked
                    if index <= gs_index or ad[index].array_section().x1 == 0:
                        # Stop when convergence is reached on either the first
                        # extension looked at, or the leftmost CCD3 extension
                        condition_met = (this_mean_sky - last_mean_sky <
                                         convergence)
                    else:
                        # Dilate until WFS width at left of image equals width at
                        # right of previous extension image
                        width = np.sum(blocked[:, 0])
                        # Note: this will not be called before y_width is defined
                        condition_met = (y_width - width <
                                         2) or index > 9  # noqa

                # Flag DQ pixels as unilluminated only if not flagged
                # (to avoid problems with the edge extensions and/or saturation)
                datasec_mask = ad[index].mask[:, datasec.x1:datasec.x2]
                datasec_mask |= np.where(
                    blocked,
                    np.where(datasec_mask > 0, DQ.good, DQ.unilluminated),
                    DQ.good)

                # Set up for next extension. If flood-fill hasn't reached
                # right-hand edge of detector, stop.
                column = blocked[:, -1]
                y_width = np.sum(column)
                if y_width == 0:
                    break
                y = np.mean(np.arange(datasec.y1, datasec.y2)[column])
                x = border

            ad.update_filename(suffix=params["suffix"], strip=True)

        return adinputs
Ejemplo n.º 3
0
def trace_lines(ext, axis, start=None, initial=None, width=5, nsum=10,
                step=1, initial_tolerance=1.0, max_shift=0.05, max_missed=10,
                func=NDStacker.mean, viewer=None):
    """
    This function traces features along one axis of a two-dimensional image.
    Initial peak locations are provided and then these are matched to peaks
    found a small distance away along the direction of tracing. In terms of
    its use to map the distortion from a 2D spectral image of an arc lamp,
    these lists of coordinates can then be used to determine a distortion map
    that will remove any curvature of lines of constant wavelength.

    For a horizontally-dispersed spectrum like GMOS, the reference y-coords
    will match the input y-coords, while the reference x-coords will all be
    equal to the initial x-coords of the peaks.

    Parameters
    ----------
    ext : single-sliced AD object
        The extension within which to trace features.

    axis : int (0 or 1)
        Axis along which to trace (0=y-direction, 1=x-direction).

    start : int/None
        Row/column to start trace (None => middle).

    initial : sequence
        Coordinates of peaks.

    width : int
        Width of centroid box in pixels.

    nsum : int
        Number of rows/columns to combine at each step.

    step : int
        Step size along axis in pixels.

    initial_tolerance : float
        Maximum perpendicular shift (in pixels) between provided location and
        first calculation of peak.

    max_shift: float
        Maximum perpendicular shift (in pixels) from pixel to pixel.

    max_missed: int
        Maximum number of interactions without finding line before line is
        considered lost forever.

    func: callable
        function to use when collapsing to 1D. This takes the data, mask, and
        variance as arguments.

    viewer: imexam viewer or None
        Viewer to draw lines on.

    Returns
    -------
    refcoords, incoords: 2xN arrays (x-first) of coordinates
    """
    log = logutils.get_logger(__name__)

    # We really don't care about non-linear/saturated pixels
    bad_bits = 65535 ^ (DQ.non_linear | DQ.saturated)

    halfwidth = int(0.5 * width)

    # Make life easier for the poor coder by transposing data if needed,
    # so that we're always tracing along columns
    if axis == 0:
        ext_data = ext.data
        ext_mask = None if ext.mask is None else ext.mask & bad_bits
        direction = "row"
    else:
        ext_data = ext.data.T
        ext_mask = None if ext.mask is None else ext.mask.T & bad_bits
        direction = "column"

    if start is None:
        start = int(0.5 * ext_data.shape[0])
        log.stdinfo("Starting trace at {} {}".format(direction, start))

    if initial is None:
        y1 = int(start - 0.5 * nsum + 0.5)
        data, mask, var = NDStacker.mean(ext_data[y1:y1 + nsum],
                                         mask=None if ext_mask is None else ext_mask[y1:y1 + nsum],
                                         variance=None)
        fwidth = estimate_peak_width(data.copy(), 10)
        widths = 0.42466 * fwidth * np.arange(0.8, 1.21, 0.05)  # TODO!
        initial, _ = find_peaks(data, widths, mask=mask,
                                variance=var, min_snr=5)
        print("Feature width", fwidth, "nlines", len(initial))

    coord_lists = [[] for peak in initial]
    for direction in (-1, 1):
        ypos = start
        last_coords = [[ypos, peak] for peak in initial]

        while True:
            y1 = int(ypos - 0.5 * nsum + 0.5)
            data, mask, var = func(ext_data[y1:y1 + nsum],
                                   mask=None if ext_mask is None else ext_mask[y1:y1 + nsum],
                                   variance=None)
            # Variance could plausibly be zero
            var = np.where(var <= 0, np.inf, var)
            clipped_data = np.where(data / np.sqrt(var) > 0.5, data, 0)
            last_peaks = [c[1] for c in last_coords if not np.isnan(c[1])]
            peaks = pinpoint_peaks(clipped_data, mask, last_peaks)
            # if ypos == start:
            #    print("Found {} peaks".format(len(peaks)))
            #    print(peaks)

            for i, (last_row, old_peak) in enumerate(last_coords):
                if np.isnan(old_peak):
                    continue
                # If we found no peaks at all, then continue through
                # the loop but nothing will match
                if peaks:
                    j = np.argmin(abs(np.array(peaks) - old_peak))
                    new_peak = peaks[j]
                else:
                    new_peak = np.inf

                # Is this close enough to the existing peak?
                tolerance = (initial_tolerance if ypos == start
                             else max_shift * abs(ypos - last_row))
                if (abs(new_peak - old_peak) > tolerance):
                    # If it's gone for good, set the coord to NaN to avoid it
                    # picking up a different line if there's significant tilt
                    if abs(ypos - last_row) > max_missed * step:
                        last_coords[i][1] = np.nan
                    continue

                # Too close to the edge?
                if (new_peak < halfwidth or
                        new_peak > ext_data.shape[1] - 0.5 * halfwidth):
                    last_coords[i][1] = np.nan
                    continue

                new_coord = [ypos, new_peak]
                if viewer:
                    kwargs = dict(zip(('y1', 'x1'), last_coords[i] if axis == 0
                    else reversed(last_coords[i])))
                    kwargs.update(dict(zip(('y2', 'x2'), new_coord if axis == 0
                    else reversed(new_coord))))
                    viewer.line(origin=0, **kwargs)

                if not (ypos == start and direction > 1):
                    coord_lists[i].append(new_coord)
                last_coords[i] = new_coord.copy()

            ypos += direction * step
            # Reached the bottom or top?
            if ypos < 0.5 * nsum or ypos > ext_data.shape[0] - 0.5 * nsum:
                break

            # Lost all lines!
            if all(np.isnan(c[1]) for c in last_coords):
                break

    # List of traced peak positions
    in_coords = np.array([c for coo in coord_lists for c in coo]).T
    # List of "reference" positions (i.e., the coordinate perpendicular to
    # the line remains constant at its initial value
    ref_coords = np.array([(ypos, ref) for coo, ref in zip(coord_lists, initial) for (ypos, xpos) in coo]).T

    # Return the coordinate lists, in the form (x-coords, y-coords),
    # regardless of the dispersion axis
    return (ref_coords, in_coords) if axis == 1 else (ref_coords[::-1], in_coords[::-1])