Example #1
0
    async def __call__(self, image: Image) -> Image:
        """Processes an image and sets x/y pixel offset to reference in offset attribute.

        Args:
            image: Image to process.

        Returns:
            Original image.

        Raises:
            ValueError: If offset could not be found.
        """

        # copy image and get WCS
        # we make our life a little easier by only using the new WCS from astrometry
        img = image.copy()
        wcs = WCS(img.header)

        # get x/y coordinates from CRVAL1/2, i.e. from center with good WCS
        center = SkyCoord(img.header["CRVAL1"] * u.deg,
                          img.header["CRVAL2"] * u.deg,
                          frame="icrs")
        x_center, y_center = wcs.world_to_pixel(center)

        # get x/y coordinates from TEL-RA/-DEC, i.e. from where the telescope thought it's pointing
        tel = SkyCoord(img.header["TEL-RA"] * u.deg,
                       img.header["TEL-DEC"] * u.deg,
                       frame="icrs")
        x_tel, y_tel = wcs.world_to_pixel(tel)

        # calculate offsets as difference between both
        img.set_meta(PixelOffsets(x_tel - x_center, y_tel - y_center))
        img.set_meta(OnSkyDistance(center.separation(tel)))
        return img
Example #2
0
    async def __call__(self, image: Image) -> Image:
        """Remove background from image.

        Args:
            image: Image to remove background from.

        Returns:
            Image without background.
        """
        from photutils.background import Background2D, MedianBackground

        # init objects
        sigma_clip = SigmaClip(sigma=self.sigma)
        bkg_estimator = MedianBackground()

        # calculate background
        bkg = Background2D(image.data,
                           self.box_size,
                           filter_size=self.filter_size,
                           sigma_clip=sigma_clip,
                           bkg_estimator=bkg_estimator)

        # copy image and remove background
        img = image.copy()
        img.data = img.data - bkg.background
        return img
Example #3
0
    async def __call__(self, image: Image) -> Image:
        """Find stars in given image and append catalog.

        Args:
            image: Image to find stars in.

        Returns:
            Image with attached catalog.
        """
        from astropy.stats import SigmaClip, sigma_clipped_stats
        from photutils import Background2D, MedianBackground, DAOStarFinder

        # get data
        if image.data is None:
            log.warning("No data found in image.")
            return image
        data = image.data.astype(float).copy()

        # estimate background
        sigma_clip = SigmaClip(sigma=self.bkg_sigma)
        bkg_estimator = MedianBackground()
        bkg = Background2D(
            data,
            self.bkg_box_size,
            filter_size=self.bkg_filter_size,
            sigma_clip=sigma_clip,
            bkg_estimator=bkg_estimator,
            mask=image.mask,
        )
        data -= bkg.background

        # do statistics
        mean, median, std = sigma_clipped_stats(data, sigma=3.0)

        # find stars
        daofind = DAOStarFinder(fwhm=self.fwhm, threshold=self.threshold * std)
        loop = asyncio.get_running_loop()
        sources = await loop.run_in_executor(None, daofind, data - median)

        # rename columns
        sources.rename_column("xcentroid", "x")
        sources.rename_column("ycentroid", "y")

        # match fits conventions
        sources["x"] += 1
        sources["y"] += 1

        # pick columns for catalog
        cat = sources["x", "y", "flux", "peak"]

        # copy image, set catalog and return it
        img = image.copy()
        img.catalog = cat
        return img
Example #4
0
    async def __call__(self, image: Image) -> Image:
        """Add filename to image.

        Args:
            image: Image to add filename to.

        Returns:
            Image with filename in FNAME.
        """

        # copy image and set filename
        img = image.copy()
        img.format_filename(self._formatter)
        return img
Example #5
0
    async def calibrate(self, image: Image) -> Image:
        """Calibrate a single science frame.

        Args:
            image: Image to calibrate.

        Returns:
            Calibrated image.
        """

        # copy image
        calibrated = image.copy()

        # run pipeline
        return await self.run_pipeline(calibrated)
Example #6
0
    async def __call__(self, image: Image) -> Image:
        """Bin an image.

        Args:
            image: Image to bin.

        Returns:
            Binned image.
        """

        # copy image
        img = image.copy()
        if img.data is None:
            log.warning("No data found in image.")
            return image

        # calculate new shape, in which all binned pixels are in a higher dimension
        shape = (img.data.shape[0] // self.binning, self.binning,
                 img.data.shape[1] // self.binning, self.binning)

        # reshape and average
        img.data = img.data.reshape(shape).mean(-1).mean(1)
        if img.data is None:
            log.warning("No data found in image after reshaping.")
            return image

        # set NAXIS1/2
        img.header["NAXIS2"], img.header["NAXIS1"] = img.data.shape

        # divide some header entries by binning
        for key in ["CRPIX1", "CRPIX2"]:
            if key in img.header:
                img.header[key] /= self.binning

        # multiply some header entries with binning
        for key in [
                "DET-BIN1", "DET-BIN2", "XBINNING", "YBINNING", "CDELT1",
                "CDELT2"
        ]:
            if key in img.header:
                img.header[key] *= self.binning

        # return result
        return img
Example #7
0
    async def __call__(self, image: Image) -> Image:
        """Add mask to image.

        Args:
            image: Image to add mask to.

        Returns:
            Image with mask
        """

        # copy image
        img = image.copy()

        # add mask
        instrument = image.header["INSTRUME"]
        binning = "%dx%s" % (image.header["XBINNING"],
                             image.header["YBINNING"])
        if binning in self._masks:
            img.mask = self._masks[instrument][binning].copy()
        else:
            log.warning("No mask found for binning of frame.")

        # finished
        return img
Example #8
0
    async def __call__(self, image: Image) -> Image:
        """Do aperture photometry on given image.

        Args:
            image: Image to do aperture photometry on.

        Returns:
            Image with attached catalog.
        """
        loop = asyncio.get_running_loop()

        # no pixel scale given?
        if image.pixel_scale is None:
            log.warning("No pixel scale provided by image.")
            return image

        # fetch catalog
        if image.catalog is None:
            log.warning("No catalog in image.")
            return image
        sources = image.catalog.copy()

        # get positions
        positions = [(x - 1, y - 1) for x, y in sources.iterrows("x", "y")]

        # perform aperture photometry for diameters of 1" to 8"
        for diameter in [1, 2, 3, 4, 5, 6, 7, 8]:
            # extraction radius in pixels
            radius = diameter / 2.0 / image.pixel_scale
            if radius < 1:
                continue

            # defines apertures
            aperture = CircularAperture(positions, r=radius)
            annulus_aperture = CircularAnnulus(positions,
                                               r_in=2 * radius,
                                               r_out=3 * radius)
            annulus_masks = annulus_aperture.to_mask(method="center")

            # loop annuli
            bkg_median = []
            for m in annulus_masks:
                annulus_data = m.multiply(image.data)
                annulus_data_1d = annulus_data[m.data > 0]
                _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d)
                bkg_median.append(median_sigclip)

            # do photometry
            phot = await loop.run_in_executor(
                None,
                partial(aperture_photometry,
                        image.data,
                        aperture,
                        mask=image.mask,
                        error=image.uncertainty))

            # calc flux
            bkg_median_np = np.array(bkg_median)
            aper_bkg = bkg_median_np * aperture.area
            sources["fluxaper%d" % diameter] = phot["aperture_sum"] - aper_bkg
            if "aperture_sum_err" in phot.columns:
                sources["fluxerr%d" % diameter] = phot["aperture_sum_err"]
            sources["bkgaper%d" % diameter] = bkg_median_np

        # copy image, set catalog and return it
        img = image.copy()
        img.catalog = sources
        return img
Example #9
0
    async def __call__(self, image: Image) -> Image:
        """Find astrometric solution on given image.

        Writes WCSERR=1 into FITS header on failure.

        Args:
            image: Image to analyse.
        """

        # copy image
        img = image.copy()

        # get catalog
        if img.catalog is None:
            log.warning("No catalog found in image.")
            return image
        cat = img.catalog[["x", "y", "flux"]].to_pandas().dropna()

        # nothing?
        if cat is None or len(cat) < 3:
            log.warning("Not enough sources for astrometry.")
            img.header["WCSERR"] = 1
            return img

        # sort it and take N brightest sources
        cat = cat.sort_values("flux", ascending=False)
        cat = cat[: self.source_count]

        # no CDELT1?
        if "CDELT1" not in img.header:
            log.warning("No CDELT1 found in header.")
            img.header["WCSERR"] = 1
            return img

        # build request data
        scale = abs(img.header["CDELT1"]) * 3600
        data = {
            "ra": img.header["TEL-RA"],
            "dec": img.header["TEL-DEC"],
            "scale_low": scale * 0.9,
            "scale_high": scale * 1.1,
            "radius": self.radius,
            "nx": img.header["NAXIS1"],
            "ny": img.header["NAXIS2"],
            "x": cat["x"].tolist(),
            "y": cat["y"].tolist(),
            "flux": cat["flux"].tolist(),
        }

        # log it
        ra_dec = SkyCoord(ra=data["ra"] * u.deg, dec=data["dec"] * u.deg, frame="icrs")
        cx, cy = img.header["CRPIX1"], img.header["CRPIX2"]
        log.info(
            "Found original RA=%s (%.4f), Dec=%s (%.4f) at pixel %.2f,%.2f.",
            ra_dec.ra.to_string(sep=":", unit=u.hour, pad=True),
            data["ra"],
            ra_dec.dec.to_string(sep=":", unit=u.deg, pad=True),
            data["dec"],
            cx,
            cy,
        )

        # send it
        async with aiohttp.ClientSession() as session:
            async with session.post(self.url, json=data, timeout=10) as response:
                status_code = response.status
                json = await response.json()

        # success?
        if status_code != 200 or "error" in json:
            # set error
            img.header["WCSERR"] = 1
            if "error" in json:
                # "Could not find WCS file." is just an info, which means that WCS was not successful
                if json["error"] == "Could not find WCS file.":
                    log.info("Could not determine WCS.")
                else:
                    log.warning("Received error from astrometry service: %s", json["error"])
            else:
                log.error("Could not connect to astrometry service.")
            return img

        else:
            # copy keywords
            hdr = json
            header_keywords_to_update = [
                "CTYPE1",
                "CTYPE2",
                "CRPIX1",
                "CRPIX2",
                "CRVAL1",
                "CRVAL2",
                "CD1_1",
                "CD1_2",
                "CD2_1",
                "CD2_2",
            ]
            for keyword in header_keywords_to_update:
                img.header[keyword] = hdr[keyword]

            # astrometry.net gives a CD matrix, so we have to delete the PC matrix and the CDELT* parameters
            for keyword in ["PC1_1", "PC1_2", "PC2_1", "PC2_2", "CDELT1", "CDELT2"]:
                del img.header[keyword]

            # calculate world coordinates for all sources in catalog
            image_wcs = WCS(img.header)
            ras, decs = image_wcs.all_pix2world(img.catalog["x"], img.catalog["y"], 1)

            # set them
            img.catalog["ra"] = ras
            img.catalog["dec"] = decs

            # RA/Dec at center pos
            final_ra, final_dec = image_wcs.all_pix2world(cx, cy, 0)
            ra_dec = SkyCoord(ra=final_ra * u.deg, dec=final_dec * u.deg, frame="icrs")

            # log it
            log.info(
                "Found final RA=%s (%.4f), Dec=%s (%.4f) at pixel %.2f,%.2f.",
                ra_dec.ra.to_string(sep=":", unit=u.hour, pad=True),
                data["ra"],
                ra_dec.dec.to_string(sep=":", unit=u.deg, pad=True),
                data["dec"],
                cx,
                cy,
            )

            # success
            img.header["WCSERR"] = 0

        # finished
        return img
Example #10
0
    async def __call__(self, image: Image) -> Image:
        """Find stars in given image and append catalog.

        Args:
            image: Image to find stars in.

        Returns:
            Image with attached catalog.
        """
        import sep

        loop = asyncio.get_running_loop()

        # got data?
        if image.data is None:
            log.warning("No data found in image.")
            return image

        # no mask?
        mask = image.mask if image.mask is not None else np.zeros(
            image.data.shape, dtype=bool)

        # remove background
        data, bkg = SepSourceDetection.remove_background(image.data, mask)

        # extract sources
        sources = await loop.run_in_executor(
            None,
            partial(
                sep.extract,
                data,
                self.threshold,
                err=bkg.globalrms,
                minarea=self.minarea,
                deblend_nthresh=self.deblend_nthresh,
                deblend_cont=self.deblend_cont,
                clean=self.clean,
                clean_param=self.clean_param,
                mask=image.mask,
            ),
        )

        # convert to astropy table
        sources = pd.DataFrame(sources)

        # only keep sources with detection flag < 8
        sources = sources[sources["flag"] < 8]
        x, y = sources["x"], sources["y"]

        # Calculate the ellipticity
        sources["ellipticity"] = 1.0 - (sources["b"] / sources["a"])

        # calculate the FWHMs of the stars
        fwhm = 2.0 * (np.log(2) * (sources["a"]**2.0 + sources["b"]**2.0))**0.5
        sources["fwhm"] = fwhm

        # clip theta to [-pi/2,pi/2]
        sources["theta"] = sources["theta"].clip(lower=np.pi / 2,
                                                 upper=np.pi / 2)

        # Kron radius
        kronrad, krflag = sep.kron_radius(data, x, y, sources["a"],
                                          sources["b"], sources["theta"], 6.0)
        sources["flag"] |= krflag
        sources["kronrad"] = kronrad

        # equivalent of FLUX_AUTO
        gain = image.header["DET-GAIN"] if "DET-GAIN" in image.header else None
        flux, fluxerr, flag = await loop.run_in_executor(
            None,
            partial(
                sep.sum_ellipse,
                data,
                x,
                y,
                sources["a"],
                sources["b"],
                sources["theta"],
                2.5 * kronrad,
                subpix=5,
                mask=image.mask,
                gain=gain,
            ),
        )
        sources["flag"] |= flag
        sources["flux"] = flux

        # radii at 0.25, 0.5, and 0.75 flux
        flux_radii, flag = sep.flux_radius(data,
                                           x,
                                           y,
                                           6.0 * sources["a"],
                                           [0.25, 0.5, 0.75],
                                           normflux=sources["flux"],
                                           subpix=5)
        sources["flag"] |= flag
        sources["fluxrad25"] = flux_radii[:, 0]
        sources["fluxrad50"] = flux_radii[:, 1]
        sources["fluxrad75"] = flux_radii[:, 2]

        # xwin/ywin
        sig = 2.0 / 2.35 * sources["fluxrad50"]
        xwin, ywin, flag = sep.winpos(data, x, y, sig)
        sources["flag"] |= flag
        sources["xwin"] = xwin
        sources["ywin"] = ywin

        # theta in degrees
        sources["theta"] = np.degrees(sources["theta"])

        # only keep sources with detection flag < 8
        sources = sources[sources["flag"] < 8]

        # match fits conventions
        sources["x"] += 1
        sources["y"] += 1

        # pick columns for catalog
        cat = sources[[
            "x",
            "y",
            "peak",
            "flux",
            "fwhm",
            "a",
            "b",
            "theta",
            "ellipticity",
            "tnpix",
            "kronrad",
            "fluxrad25",
            "fluxrad50",
            "fluxrad75",
            "xwin",
            "ywin",
        ]]

        # copy image, set catalog and return it
        img = image.copy()
        img.catalog = Table.from_pandas(cat)
        return img
Example #11
0
    def _photometry(image: Image) -> Image:
        import sep
        from pyobs.images.processors.detection import SepSourceDetection

        # check data
        if image.data is None:
            log.warning("No data found in image.")
            return image
        if image.catalog is None:
            log.warning("No catalog found in image.")
            return image

        # no mask?
        mask = image.mask if image.mask is not None else np.ones(image.data.shape, dtype=bool)

        # remove background
        data, bkg = SepSourceDetection.remove_background(image.data, mask)

        # fetch catalog
        sources = image.catalog.copy()

        # match SEP conventions
        x, y = sources["x"] - 1, sources["y"] - 1

        # get gain
        gain = image.header["DET-GAIN"] if "DET-GAIN" in image.header else None

        # perform aperture photometry for diameters of 1" to 8"
        for diameter in [1, 2, 3, 4, 5, 6, 7, 8]:
            if image.pixel_scale is not None:
                flux, fluxerr, flag = sep.sum_circle(
                    data, x, y, diameter / 2.0 / image.pixel_scale, mask=image.mask, err=image.uncertainty, gain=gain
                )
                sources["fluxaper{0}".format(diameter)] = flux
                sources["fluxerr{0}".format(diameter)] = fluxerr

            else:
                sources["fluxaper{0}".format(diameter)] = 0
                sources["fluxerr{0}".format(diameter)] = 0

        # average background at each source
        # since SEP sums up whole pixels, we need to do the same on an image of ones for the background_area
        bkgflux, fluxerr, flag = sep.sum_ellipse(
            bkg.back(), x, y, sources["a"], sources["b"], np.pi / 2.0, 2.5 * sources["kronrad"], subpix=1
        )
        background_area, _, _ = sep.sum_ellipse(
            np.ones(shape=bkg.back().shape),
            x,
            y,
            sources["a"],
            sources["b"],
            np.pi / 2.0,
            2.5 * sources["kronrad"],
            subpix=1,
        )
        sources["background"] = bkgflux
        sources["background"][background_area > 0] /= background_area[background_area > 0]

        # pick columns for catalog
        new_columns = [
            "fluxaper1",
            "fluxerr1",
            "fluxaper2",
            "fluxerr2",
            "fluxaper3",
            "fluxerr3",
            "fluxaper4",
            "fluxerr4",
            "fluxaper5",
            "fluxerr5",
            "fluxaper6",
            "fluxerr6",
            "fluxaper7",
            "fluxerr7",
            "fluxaper8",
            "fluxerr8",
            "background",
        ]
        cat = sources[image.catalog.colnames + new_columns]

        # copy image, set catalog and return it
        img = image.copy()
        img.catalog = cat
        return img