Beispiel #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.
        """

        # no reference image?
        if self._ref_image is None:
            log.info("Initialising auto-guiding with new image...")
            self._ref_image = self._process(image)
            self._init_pid()
            self.offset = (0, 0)
            return image

        # process it
        log.info("Perform auto-guiding on new image...")
        sum_x, sum_y = self._process(image)

        # find peaks
        dx = self._correlate(sum_x, self._ref_image[0])
        dy = self._correlate(sum_y, self._ref_image[1])
        if dx is None or dy is None:
            log.error("Could not correlate peaks.")
            return image

        # set it
        image.set_meta(PixelOffsets(dx, dy))
        return image
Beispiel #2
0
    def _set_exp_time(self, image: Image, exp_time: float) -> None:
        """Internal setter for exposure time."""

        # min exp time
        if self._min_exp_time is not None and exp_time < self._min_exp_time:
            exp_time = self._min_exp_time

        # max exp time
        if self._max_exp_time is not None and exp_time > self._max_exp_time:
            exp_time = self._max_exp_time

        # set it
        image.set_meta(ExpTime(exp_time))
Beispiel #3
0
    async def write_image(self, filename: str, image: Image, *args: Any,
                          **kwargs: Any) -> None:
        """Convenience function for writing an Image to a FITS file.

        Args:
            filename: Name of file to write.
            image: Image to write.
        """

        # open file
        async with self.open_file(filename, "wb") as f:
            with io.BytesIO() as bio:
                image.writeto(bio, *args, **kwargs)
                await f.write(bio.getvalue())
Beispiel #4
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
Beispiel #5
0
    def _get_radec_center_target(
            self, image: Image,
            location: EarthLocation) -> Tuple[SkyCoord, SkyCoord]:
        """Return RA/Dec of central pixel and of central pixel plus offsets.

        Args:
            image: Image to take header and offsets from.
            location: Observer location.

        Returns:
            Tuple of RA/Dec coordinates of center and of centre+offsets.
        """

        # get offsets
        offsets = image.get_meta(PixelOffsets)
        log.info("Found pixel shift of dx=%.2f, dy=%.2f.", offsets.dx,
                 offsets.dy)

        # get reference pixel and date obs
        cx, cy = image.header["DET-CPX1"], image.header["DET-CPX2"]

        # get WCS and RA/DEC for pixel and pixel + dx/dy
        w = WCS(image.header)
        center = w.pixel_to_world(cx, cy)
        target = w.pixel_to_world(cx + offsets.dx, cy + offsets.dy)
        return center, target
Beispiel #6
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
Beispiel #7
0
    def get_image(self, exp_time: float, open_shutter: bool) -> Image:
        """Simulate an image.

        Args:
            exp_time: Exposure time in seconds.
            open_shutter: Whether the shutter is opened.

        Returns:
            numpy array with image.
        """

        # get now
        now = Time.now()

        # simulate or what?
        if self.images:
            # take image from list
            filename = self.images.pop(0)
            data = fits.getdata(filename)
            self.images.append(filename)

        else:
            # simulate
            data = self._simulate_image(exp_time, open_shutter)

        # create header
        hdr = self._create_header(exp_time, open_shutter, now, data)

        # return it
        return Image(data, header=hdr)
Beispiel #8
0
    async def download_frames(self, infos: List[FrameInfo]) -> List[Image]:
        # loop infos
        images = []
        for info in infos:
            # make sure it's the correct FrameInfo
            if not isinstance(info, PyobsArchiveFrameInfo):
                log.warning("Incorrect type for frame info.")
                continue

            # download
            url = urllib.parse.urljoin(self._url, info.url)
            async with aiohttp.ClientSession() as session:
                async with session.get(url, headers=self._headers,
                                       timeout=60) as response:
                    if response.status != 200:
                        log.exception("Error downloading file %s.",
                                      info.filename)

                    # create image
                    try:
                        image = Image.from_bytes(await response.read())
                        images.append(image)
                    except OSError:
                        log.exception("Error downloading file %s.",
                                      info.filename)

        # return all
        return images
Beispiel #9
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
Beispiel #10
0
    async def read_image(self, filename: str) -> Image:
        """Convenience function that wraps around open_file() to read an Image.

        Args:
            filename: Name of file to download.

        Returns:
            An image object
        """
        async with self.open_file(filename, "rb") as f:
            data = await f.read()
            return Image.from_bytes(data)
Beispiel #11
0
    def _get_image(self, exp_time: float, open_shutter: bool) -> Image:
        """Actually get (i.e. simulate) the image."""

        # random image or pre-defined?
        if self._sim_images:
            filename = self._sim_images.pop(0)
            self._sim_images.append(filename)
            return Image.from_file(filename)

        else:
            image = self._camera.get_image(exp_time, open_shutter)
            return image
Beispiel #12
0
    def _combine_calib_images(
        images: List[Image], bias: Optional[Image] = None, normalize: bool = False, method: str = "average"
    ) -> Image:
        """Combine a list of given images.

        Args:
            images: List of images to combine.
            bias: If given, subtract from images before combining them.
            normalize: If True, images are normalized to median of 1 before and after combining them.
            method: Method for combining images.
        """
        import ccdproc

        # get CCDData objects
        data = [image.to_ccddata() for image in images]

        # subtract bias?
        if bias is not None:
            bias_data = bias.to_ccddata()
            data = [ccdproc.subtract_bias(d, bias_data) for d in data]

        # normalize?
        if normalize:
            data = [d.divide(np.median(d.data), handle_meta="first_found") for d in data]

        # combine image
        combined = ccdproc.combine(
            data,
            method=method,
            sigma_clip=True,
            sigma_clip_low_thresh=5,
            sigma_clip_high_thresh=5,
            mem_limit=350e6,
            unit="adu",
            combine_uncertainty_function=np.ma.std,
        )

        # normalize?
        if normalize:
            combined = combined.divide(np.median(combined.data), handle_meta="first_found")

        # to Image and copy header
        image = Image.from_ccddata(combined)

        # add history
        for i, src in enumerate(images, 1):
            basename = src.header["FNAME"].replace(".fits.fz", "").replace(".fits", "")
            image.header["L1AVG%03d" % i] = (basename, "Image used for average")
        image.header["RLEVEL"] = (1, "Reduction level")

        # finished
        return image
Beispiel #13
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
Beispiel #14
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)
Beispiel #15
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
Beispiel #16
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.
        """

        # get values from header
        target = [image.header[x] for x in self.target]
        center = [image.header[x] for x in self.center]

        # calculate offset
        image.meta["offsets"] = np.subtract(target, center)
        return image
Beispiel #17
0
    async def __call__(self, image: Image) -> Image:
        """Broadcast image.

        Args:
            image: Image to broadcast.

        Returns:
            Original image.
        """

        # format filename
        filename = image.format_filename(self._formatter)

        # upload
        await self.vfs.write_image(filename, image)

        # broadcast
        await self.comm.send_event(NewImageEvent(filename, image_type=ImageType(image.header["IMAGETYP"])))

        # finished
        return image
Beispiel #18
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
Beispiel #19
0
    async def __call__(self, image: Image) -> Image:
        """Calibrate an image.

        Args:
            image: Image to calibrate.

        Returns:
            Calibrated image.
        """
        import ccdproc

        # get calibration masters
        try:
            bias = await self._find_master(image, ImageType.BIAS)
            dark = await self._find_master(image, ImageType.DARK)
            flat = await self._find_master(image, ImageType.SKYFLAT)
        except ValueError as e:
            log.error("Could not find calibration frames: " + str(e))
            return image

        # calibrate image
        c = await asyncio.get_running_loop().run_in_executor(
            None,
            partial(
                ccdproc.ccd_process,
                image.to_ccddata(),
                oscan=image.header["BIASSEC"]
                if "BIASSEC" in image.header else None,
                trim=image.header["TRIMSEC"]
                if "TRIMSEC" in image.header else None,
                error=True,
                master_bias=bias.to_ccddata() if bias is not None else None,
                dark_frame=dark.to_ccddata() if dark is not None else None,
                master_flat=flat.to_ccddata() if flat is not None else None,
                bad_pixel_mask=None,
                gain=image.header["DET-GAIN"] * u.electron / u.adu,
                readnoise=image.header["DET-RON"] * u.electron,
                dark_exposure=dark.header["EXPTIME"] *
                u.second if dark is not None else None,
                data_exposure=image.header["EXPTIME"] * u.second,
                dark_scale=True,
                gain_corrected=False,
            ),
        )

        # to image
        calibrated = Image.from_ccddata(c)
        calibrated.header["BUNIT"] = ("electron", "Unit of pixel values")

        # set raw filename
        if "ORIGNAME" in image.header:
            calibrated.header["L1RAW"] = image.header["ORIGNAME"].replace(
                ".fits", "")

        # add calibration frames
        if bias is not None:
            calibrated.header["L1BIAS"] = (
                bias.header["FNAME"].replace(".fits.fz",
                                             "").replace(".fits", ""),
                "Name of BIAS frame",
            )
        if dark is not None:
            calibrated.header["L1DARK"] = (
                dark.header["FNAME"].replace(".fits.fz",
                                             "").replace(".fits", ""),
                "Name of DARK frame",
            )
        if flat is not None:
            calibrated.header["L1FLAT"] = (
                flat.header["FNAME"].replace(".fits.fz",
                                             "").replace(".fits", ""),
                "Name of FLAT frame",
            )

        # set RLEVEL
        calibrated.header["RLEVEL"] = (1, "Reduction level")

        # finished
        return calibrated
Beispiel #20
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
Beispiel #21
0
    async def _expose(self, exposure_time: float, open_shutter: bool, abort_event: asyncio.Event) -> Image:
        """Actually do the exposure, should be implemented by derived classes.

        Args:
            exposure_time: The requested exposure time in seconds.
            open_shutter: Whether or not to open the shutter.
            abort_event: Event that gets triggered when exposure should be aborted.

        Returns:
            The actual image.

        Raises:
            GrabImageError: If exposure was not successful.
        """
        from .flidriver import FliTemperature

        # check driver
        if self._driver is None:
            raise ValueError("No camera driver.")

        # set binning
        log.info("Set binning to %dx%d.", self._binning[0], self._binning[1])
        self._driver.set_binning(*self._binning)

        # set window, divide width/height by binning, from libfli:
        # "Note that the given lower-right coordinate must take into account the horizontal and
        # vertical bin factor settings, but the upper-left coordinate is absolute."
        width = int(math.floor(self._window[2]) / self._binning[0])
        height = int(math.floor(self._window[3]) / self._binning[1])
        log.info(
            "Set window to %dx%d (binned %dx%d) at %d,%d.",
            self._window[2],
            self._window[3],
            width,
            height,
            self._window[0],
            self._window[1],
        )
        self._driver.set_window(self._window[0], self._window[1], width, height)

        # set some stuff
        self._driver.init_exposure(open_shutter)
        self._driver.set_exposure_time(int(exposure_time * 1000.0))

        # get date obs
        log.info(
            "Starting exposure with %s shutter for %.2f seconds...", "open" if open_shutter else "closed", exposure_time
        )
        date_obs = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")

        # do exposure
        self._driver.start_exposure()

        # wait for exposure to finish
        while True:
            # aborted?
            if abort_event.is_set():
                await self._change_exposure_status(ExposureStatus.IDLE)
                raise InterruptedError("Aborted exposure.")

            # is exposure finished?
            if self._driver.is_exposing():
                break
            else:
                # sleep a little
                time.sleep(0.2)

        # readout
        log.info("Exposure finished, reading out...")
        await self._change_exposure_status(ExposureStatus.READOUT)
        width = int(math.floor(self._window[2] / self._binning[0]))
        height = int(math.floor(self._window[3] / self._binning[1]))
        img = np.zeros((height, width), dtype=np.uint16)
        for row in range(height):
            img[row, :] = self._driver.grab_row(width)

        # create FITS image and set header
        image = Image(img)
        image.header["DATE-OBS"] = (date_obs, "Date and time of start of exposure")
        image.header["EXPTIME"] = (exposure_time, "Exposure time [s]")
        image.header["DET-TEMP"] = (self._driver.get_temp(FliTemperature.CCD), "CCD temperature [C]")
        image.header["DET-COOL"] = (self._driver.get_cooler_power(), "Cooler power [percent]")
        image.header["DET-TSET"] = (self._temp_setpoint, "Cooler setpoint [C]")

        # instrument and detector
        image.header["INSTRUME"] = (self._driver.name, "Name of instrument")

        # binning
        image.header["XBINNING"] = image.header["DET-BIN1"] = (self._binning[0], "Binning factor used on X axis")
        image.header["YBINNING"] = image.header["DET-BIN2"] = (self._binning[1], "Binning factor used on Y axis")

        # window
        image.header["XORGSUBF"] = (self._window[0], "Subframe origin on X axis")
        image.header["YORGSUBF"] = (self._window[1], "Subframe origin on Y axis")

        # statistics
        image.header["DATAMIN"] = (float(np.min(img)), "Minimum data value")
        image.header["DATAMAX"] = (float(np.max(img)), "Maximum data value")
        image.header["DATAMEAN"] = (float(np.mean(img)), "Mean data value")

        # biassec/trimsec
        full = self._driver.get_visible_frame()
        self.set_biassec_trimsec(image.header, *full)

        # return FITS image
        log.info("Readout finished.")
        return image
Beispiel #22
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
Beispiel #23
0
    async def _expose(self, exposure_time: float, open_shutter: bool, abort_event: asyncio.Event) -> Image:
        """Actually do the exposure, should be implemented by derived classes.

        Args:
            exposure_time: The requested exposure time in ms.
            open_shutter: Whether or not to open the shutter.
            abort_event: Event that gets triggered when exposure should be aborted.

        Returns:
            The actual image.

        Raises:
            pyobs.utils.exceptions.GrabImageError: If exposure was not successful.
        """

        async with self._lock_active:
            #  binning
            binning = self._binning

            # set window, CSBIGCam expects left/top also in binned coordinates, so divide by binning
            left = int(math.floor(self._window[0]) / binning[0])
            top = int(math.floor(self._window[1]) / binning[1])
            width = int(math.floor(self._window[2]) / binning[0])
            height = int(math.floor(self._window[3]) / binning[1])
            log.info(
                "Set window to %dx%d (binned %dx%d) at %d,%d.",
                self._window[2],
                self._window[3],
                width,
                height,
                left,
                top,
            )
            window = (left, top, width, height)

            # get date obs
            log.info("Starting exposure with for %.2f seconds...", exposure_time)
            date_obs = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")

            # init image
            self._img.image_can_close = False

            # set exposure time, window and binning
            self._cam.exposure_time = exposure_time
            self._cam.window = window
            self._cam.binning = binning

            # start exposure
            self._cam.start_exposure(self._img, open_shutter)

            # wait for it
            while not self._cam.has_exposure_finished():
                # was aborted?
                if abort_event.is_set():
                    raise InterruptedError("Exposure aborted.")
                await asyncio.sleep(0.01)

            # finish exposure
            self._cam.end_exposure()

            # wait for readout
            log.info("Exposure finished, reading out...")
            await self._change_exposure_status(ExposureStatus.READOUT)

            # start readout (can raise ValueError)
            loop = asyncio.get_running_loop()
            await loop.run_in_executor(None, self._cam.readout, self._img, open_shutter)

            # finalize image
            self._img.image_can_close = True

            # download data
            data = self._img.data

            # temp & cooling
            _, temp, setpoint, _ = self._cam.get_cooling()

            # create FITS image and set header
            img = Image(data)
            img.header["DATE-OBS"] = (date_obs, "Date and time of start of exposure")
            img.header["EXPTIME"] = (exposure_time, "Exposure time [s]")
            img.header["DET-TEMP"] = (temp, "CCD temperature [C]")
            img.header["DET-TSET"] = (setpoint, "Cooler setpoint [C]")

            # binning
            img.header["XBINNING"] = img.header["DET-BIN1"] = (self._binning[0], "Binning factor used on X axis")
            img.header["YBINNING"] = img.header["DET-BIN2"] = (self._binning[1], "Binning factor used on Y axis")

            # window
            img.header["XORGSUBF"] = (self._window[0], "Subframe origin on X axis")
            img.header["YORGSUBF"] = (self._window[1], "Subframe origin on Y axis")

            # statistics
            img.header["DATAMIN"] = (float(np.min(data)), "Minimum data value")
            img.header["DATAMAX"] = (float(np.max(data)), "Maximum data value")
            img.header["DATAMEAN"] = (float(np.mean(data)), "Mean data value")

            # biassec/trimsec
            self.set_biassec_trimsec(img.header, *self._full_frame)

            # return FITS image
            log.info("Readout finished.")
            return img
Beispiel #24
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
Beispiel #25
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
Beispiel #26
0
    def _expose(self, exposure_time: float, open_shutter: bool,
                abort_event: threading.Event) -> Image:
        """Actually do the exposure, should be implemented by derived classes.

        Args:
            exposure_time: The requested exposure time in s.
            open_shutter: Whether or not to open the shutter.
            abort_event: Event that gets triggered when exposure should be aborted.

        Returns:
            The actual image.
        """

        # no camera?
        if self._camera is None:
            raise ValueError('No camera initialised.')

        # get image format
        image_format = FORMATS[self._image_format]

        # set window, divide width/height by binning
        width = int(math.floor(self._window[2]) / self._binning)
        height = int(math.floor(self._window[3]) / self._binning)
        log.info("Set window to %dx%d (binned %dx%d with %dx%d) at %d,%d.",
                 self._window[2], self._window[3], width, height,
                 self._binning, self._binning, self._window[0],
                 self._window[1])
        self._camera.set_roi(int(self._window[0]), int(self._window[1]), width,
                             height, self._binning, image_format)

        # set status and exposure time in ms
        self._change_exposure_status(ExposureStatus.EXPOSING)
        self._camera.set_control_value(asi.ASI_EXPOSURE,
                                       int(exposure_time * 1e6))

        # get date obs
        log.info('Starting exposure with %s shutter for %.2f seconds...',
                 'open' if open_shutter else 'closed', exposure_time)
        date_obs = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")

        # do exposure
        self._camera.start_exposure()
        self.closing.wait(0.01)

        # wait for image
        while self._camera.get_exposure_status() == asi.ASI_EXP_WORKING:
            # aborted?
            if abort_event.is_set():
                self._change_exposure_status(ExposureStatus.IDLE)
                raise ValueError('Aborted exposure.')

            # sleep a little
            abort_event.wait(0.01)

        # success?
        status = self._camera.get_exposure_status()
        if status != asi.ASI_EXP_SUCCESS:
            raise ValueError('Could not capture image: %s' % status)

        # get data
        log.info('Exposure finished, reading out...')
        self._change_exposure_status(ExposureStatus.READOUT)
        buffer = self._camera.get_data_after_exposure()
        whbi = self._camera.get_roi_format()

        # decide on image format
        shape = [whbi[1], whbi[0]]
        if image_format == asi.ASI_IMG_RAW8:
            data = np.frombuffer(buffer, dtype=np.uint8)
        elif image_format == asi.ASI_IMG_RAW16:
            data = np.frombuffer(buffer, dtype=np.uint16)
        elif image_format == asi.ASI_IMG_RGB24:
            shape.append(3)
            data = np.frombuffer(buffer, dtype=np.uint8)
        else:
            raise ValueError('Unknown image format.')

        # reshape
        data = data.reshape(shape)

        # special treatment for RGB images
        if image_format == asi.ASI_IMG_RGB24:
            # convert BGR to RGB
            data = data[:, :, ::-1]

            # now we need to separate the R, G, and B images
            # this is easiest done by shifting the RGB axis from last to first position
            # i.e. we go from RGBRGBRGBRGBRGB to RRRRRGGGGGBBBBB
            data = np.moveaxis(data, 2, 0)

        # create FITS image and set header
        image = Image(data)
        image.header['DATE-OBS'] = (date_obs,
                                    'Date and time of start of exposure')
        image.header['EXPTIME'] = (exposure_time, 'Exposure time [s]')

        # instrument and detector
        image.header['INSTRUME'] = (self._camera_name, 'Name of instrument')

        # binning
        image.header['XBINNING'] = image.header['DET-BIN1'] = (
            self._binning, 'Binning factor used on X axis')
        image.header['YBINNING'] = image.header['DET-BIN2'] = (
            self._binning, 'Binning factor used on Y axis')

        # window
        image.header['XORGSUBF'] = (self._window[0],
                                    'Subframe origin on X axis')
        image.header['YORGSUBF'] = (self._window[1],
                                    'Subframe origin on Y axis')

        # statistics
        image.header['DATAMIN'] = (float(np.min(data)), 'Minimum data value')
        image.header['DATAMAX'] = (float(np.max(data)), 'Maximum data value')
        image.header['DATAMEAN'] = (float(np.mean(data)), 'Mean data value')

        # pixels
        image.header['DET-PIXL'] = (self._camera_info['PixelSize'] / 1000.,
                                    'Size of detector pixels (square) [mm]')
        image.header['DET-GAIN'] = (self._camera_info['ElecPerADU'],
                                    'Detector gain [e-/ADU]')

        # Bayer pattern?
        if image_format in [asi.ASI_IMG_RAW8, asi.ASI_IMG_RAW16]:
            image.header['BAYERPAT'] = image.header['COLORTYP'] = (
                'GBRG', 'Bayer pattern for colors')

        # biassec/trimsec
        self.set_biassec_trimsec(image.header, *self._window)

        # return FITS image
        log.info('Readout finished.')
        self._change_exposure_status(ExposureStatus.IDLE)
        return image