Пример #1
0
    def start_video(self, seconds, filename_root, max_frames, image_type=None):
        if not isinstance(seconds, u.Quantity):
            seconds = seconds * u.second
        self._control_setter('EXPOSURE', seconds)
        if image_type:
            self.image_type = image_type

        roi_format = Camera._driver.get_roi_format(self._handle)
        width = int(get_quantity_value(roi_format['width'], unit=u.pixel))
        height = int(get_quantity_value(roi_format['height'], unit=u.pixel))
        image_type = roi_format['image_type']

        timeout = 2 * seconds + self._timeout * u.second

        video_args = (width, height, image_type, timeout, filename_root,
                      self.file_extension, int(max_frames),
                      self._create_fits_header(seconds, dark=False))
        video_thread = threading.Thread(target=self._video_readout,
                                        args=video_args,
                                        daemon=True)

        Camera._driver.start_video_capture(self._handle)
        self._video_event.clear()
        video_thread.start()
        self.logger.debug("Video capture started on {}".format(self))
Пример #2
0
 def set_start_position(self, camera_ID, start_x, start_y):
     """ Set position of the upper left corner of the ROI for camera with given integer ID """
     start_x = int(get_quantity_value(start_x, unit=u.pixel))
     start_y = int(get_quantity_value(start_y, unit=u.pixel))
     self._call_function('ASISetStartPos', camera_ID, ctypes.c_int(start_x),
                         ctypes.c_int(start_y))
     self.logger.debug(
         "Set ROI start position of camera {} to ({}, {})".format(
             camera_ID, start_x, start_y))
Пример #3
0
 def _create_fits_header(self, seconds, dark):
     header = super()._create_fits_header(seconds, dark)
     header.set('CAM-GAIN', self.gain, 'Internal units')
     header.set('XPIXSZ',
                get_quantity_value(self.properties['pixel_size'], u.um),
                'Microns')
     header.set('YPIXSZ',
                get_quantity_value(self.properties['pixel_size'], u.um),
                'Microns')
     return header
Пример #4
0
 def set_roi_format(self, camera_ID, width, height, binning, image_type):
     """ Set the ROI size and image format settings for the camera with given integer ID """
     width = int(get_quantity_value(width, unit=u.pixel))
     height = int(get_quantity_value(height, unit=u.pixel))
     binning = int(binning)
     self._call_function('ASISetROIFormat', camera_ID, ctypes.c_int(width),
                         ctypes.c_int(height), ctypes.c_int(binning),
                         ctypes.c_int(ImgType[image_type]))
     self.logger.debug(
         "Set ROI, format on camera {} to {}x{}/{}, {}".format(
             camera_ID, width, height, binning, image_type))
Пример #5
0
    def _image_array(self, width, height, image_type):
        """ Creates a suitable numpy array for storing image data """
        width = int(get_quantity_value(width, unit=u.pixel))
        height = int(get_quantity_value(height, unit=u.pixel))

        if image_type in ('RAW8', 'Y8'):
            image_array = np.zeros((height, width), dtype=np.uint8, order='C')
        elif image_type == 'RAW16':
            image_array = np.zeros((height, width), dtype=np.uint16, order='C')
        elif image_type == 'RGB24':
            image_array = np.zeros((3, height, width),
                                   dtype=np.uint8,
                                   order='C')

        return image_array
Пример #6
0
    def is_temperature_stable(self):
        """ True if image sensor temperature is stable, False if not.

        See also: See `temperature_tolerance` for more information about the temperature stability.
        An uncooled camera, or cooled camera with cooling disabled, will always return False.
        """
        if self.is_cooled_camera and self.cooling_enabled:

            # Temperature must be within tolerance
            temp_difference = abs(self.temperature - self.target_temperature)
            at_target_temp = temp_difference <= self.temperature_tolerance

            # Camera cooling power must not be 100%
            cooling_at_maximum = get_quantity_value(self.cooling_power,
                                                    u.percent) == 100

            temp_is_stable = at_target_temp and not cooling_at_maximum

            if not temp_is_stable:
                self.logger.warning(f'Unstable CCD temperature in {self}.')
            self.logger.debug(f'Cooling power={self.cooling_power:.02f} '
                              f'Temperature={self.temperature:.02f} '
                              f'Target temp={self.target_temperature:.02f} '
                              f'Temp tol={self.temperature_tolerance:.02f} '
                              f"Temp diff={temp_difference:.02f} "
                              f"At target={at_target_temp} "
                              f"At max cooling={cooling_at_maximum} "
                              f"Temperature is stable={temp_is_stable}")
            return temp_is_stable
        else:
            return False
Пример #7
0
    def _readout(self, filename, width, height, header):
        exposure_status = Camera._driver.get_exposure_status(self._handle)
        if exposure_status == 'SUCCESS':
            try:
                image_data = Camera._driver.get_exposure_data(
                    self._handle, width, height, self.image_type)
            except RuntimeError as err:
                raise error.PanError(
                    'Error getting image data from {}: {}'.format(self, err))
            else:
                # Fix 'raw' data scaling by changing from zero padding of LSBs
                # to zero padding of MSBs.
                if self.image_type == 'RAW16':
                    pad_bits = 16 - int(
                        get_quantity_value(self.bit_depth, u.bit))
                    image_data = np.right_shift(image_data, pad_bits)

                fits_utils.write_fits(data=image_data,
                                      header=header,
                                      filename=filename)
        elif exposure_status == 'FAILED':
            raise error.PanError("Exposure failed on {}".format(self))
        elif exposure_status == 'IDLE':
            raise error.PanError("Exposure missing on {}".format(self))
        else:
            raise error.PanError(
                "Unexpected exposure status on {}: '{}'".format(
                    self, exposure_status))
Пример #8
0
    def add_observation(self, field_config):
        """Adds an `Observation` to the scheduler

        Args:
            field_config (dict): Configuration items for `Observation`
        """
        with suppress(KeyError):
            field_config['exptime'] = float(
                get_quantity_value(field_config['exptime'],
                                   unit=u.second)) * u.second

        self.logger.debug(f"Adding field_config={field_config!r} to scheduler")
        field = Field(field_config['name'], field_config['position'])
        self.logger.debug(f"Created field.name={field.name!r}")

        try:
            self.logger.debug(f"Creating observation for {field_config!r}")
            obs = Observation(field, **field_config)
            self.logger.debug(
                f"Observation created for field.name={field.name!r}")
        except Exception as e:
            raise error.InvalidObservation(
                f"Skipping invalid field: {field_config!r} {e!r}")
        else:
            if field.name in self._observations:
                self.logger.debug(
                    f"Overriding existing entry for field.name={field.name!r}")
            self._observations[field.name] = obs
            self.logger.debug(f"obs={obs!r} added")
Пример #9
0
 def _start_exposure(self, seconds=None, filename=None, dark=False, header=None, *args,
                     **kwargs):
     self._is_exposing_event.set()
     exposure_thread = Timer(interval=get_quantity_value(seconds, unit=u.second),
                             function=self._end_exposure)
     exposure_thread.start()
     readout_args = (filename, header)
     return readout_args
Пример #10
0
    def _parse_input_value(self, value, control_type):
        """ Helper function to convert input values to appropriate ctypes.c_long """

        if control_type in units_and_scale:
            value = get_quantity_value(value,
                                       unit=units_and_scale[control_type])
        elif control_type == 'FLIP':
            value = FlipStatus[value]

        return ctypes.c_long(int(value))
Пример #11
0
    def _video_readout(self, width, height, image_type, timeout, filename_root,
                       file_extension, max_frames, header):

        start_time = time.monotonic()
        good_frames = 0
        bad_frames = 0

        # Calculate number of bits that have been used to pad the raw data to RAW16 format.
        if self.image_type == 'RAW16':
            pad_bits = 16 - int(get_quantity_value(self.bit_depth, u.bit))
        else:
            pad_bits = 0

        for frame_number in range(max_frames):
            if self._video_event.is_set():
                break
            # This call will block for up to timeout milliseconds waiting for a frame
            video_data = Camera._driver.get_video_data(self._handle, width,
                                                       height, image_type,
                                                       timeout)
            if video_data is not None:
                now = Time.now()
                header.set('DATE-OBS', now.fits, 'End of exposure + readout')
                filename = "{}_{:06d}.{}".format(filename_root, frame_number,
                                                 file_extension)
                # Fix 'raw' data scaling by changing from zero padding of LSBs
                # to zero padding of MSBs.
                video_data = np.right_shift(video_data, pad_bits)
                fits_utils.write_fits(video_data, header, filename)
                good_frames += 1
            else:
                bad_frames += 1

        if frame_number == max_frames - 1:
            # No one callled stop_video() before max_frames so have to call it here
            self.stop_video()

        elapsed_time = (time.monotonic() - start_time) * u.second
        self.logger.debug(
            "Captured {} of {} frames in {:.2f} ({:.2f} fps), {} frames lost".
            format(good_frames, max_frames, elapsed_time,
                   get_quantity_value(good_frames / elapsed_time), bad_frames))
Пример #12
0
    def FLISetExposureTime(self, handle, exposure_time):
        """
        Set the exposure time for a camera.

        Args:
            handle (ctypes.c_long): handle of the camera to set the exposure time of.
            exposure_time (u.Quantity): required exposure time. A simple numeric type
                can be given instead of a Quantity, in which case the units are assumed
                to be seconds.
        """
        exposure_time = get_quantity_value(exposure_time, unit=u.second)
        milliseconds = ctypes.c_long(int(exposure_time * 1000))
        self._call_function('setting exposure time',
                            self._CDLL.FLISetExposureTime, handle,
                            milliseconds)
Пример #13
0
 def get_video_data(self, camera_ID, width, height, image_type, timeout):
     """ Get the image data from the next available video frame """
     video_data = self._image_array(width, height, image_type)
     timeout = int(get_quantity_value(timeout, unit=u.ms))
     try:
         self._call_function(
             'ASIGetVideoData', camera_ID,
             video_data.ctypes.data_as(ctypes.POINTER(ctypes.c_byte)),
             ctypes.c_long(video_data.nbytes), ctypes.c_int(-1))
         # If set timeout to anything but -1 (no timeout) this call times out instantly?
     except RuntimeError:
         # Expect some dropped frames during video capture
         return None
     else:
         return video_data
Пример #14
0
    def FLISetTemperature(self, handle, temperature):
        """
        Set the temperature of a given camera.

        Args:
            handle (ctypes.c_long): handle of the camera device to set the temperature of.
            temperature (astropy.units.Quantity): temperature to set the cold finger of the camera
                to. A simple numeric type can be given instead of a Quantity, in which case the
                units are assumed to be degrees Celsius.
        """
        temperature = get_quantity_value(temperature, unit=u.Celsius)
        temperature = ctypes.c_double(temperature)

        self._call_function('setting temperature',
                            self._CDLL.FLISetTemperature, handle, temperature)
Пример #15
0
    def _start_exposure(self,
                        seconds=None,
                        filename=None,
                        dark=None,
                        header=None,
                        *args,
                        **kwargs):
        """Take an exposure for given number of seconds and saves to provided filename

        Note:
            See `scripts/take-pic.sh`

            Tested With:
                * Canon EOS 100D

        Args:
            seconds (u.second, optional): Length of exposure
            filename (str, optional): Image is saved to this filename
        """
        script_path = os.path.expandvars('$POCS/scripts/take-pic.sh')

        # Make sure we have just the value, no units
        seconds = get_quantity_value(seconds)

        run_cmd = [script_path, self.port, str(seconds), filename]

        # Take Picture
        try:
            self._is_exposing_event.set()
            self._exposure_proc = subprocess.Popen(run_cmd,
                                                   stdout=subprocess.PIPE,
                                                   stderr=subprocess.PIPE,
                                                   universal_newlines=True)
        except error.InvalidCommand as e:
            self.logger.warning(e)
        finally:
            readout_args = (filename, header)
            return readout_args
Пример #16
0
    def _create_fits_header(self, seconds, dark=None):
        header = fits.Header()
        header.set('INSTRUME', self.uid, 'Camera serial number')
        now = Time.now()
        header.set('DATE-OBS', now.fits, 'Start of exposure')
        header.set('EXPTIME', get_quantity_value(seconds, u.second), 'Seconds')
        if dark is not None:
            if dark:
                header.set('IMAGETYP', 'Dark Frame')
            else:
                header.set('IMAGETYP', 'Light Frame')
        header.set('FILTER', self.filter_type)
        with suppress(
                NotImplementedError):  # SBIG & ZWO cameras report their gain.
            header.set('EGAIN',
                       get_quantity_value(self.egain, u.electron / u.adu),
                       'Electrons/ADU')
        with suppress(NotImplementedError):
            # ZWO cameras have ADC bit depths with differ from BITPIX
            header.set('BITDEPTH',
                       int(get_quantity_value(self.bit_depth, u.bit)),
                       'ADC bit depth')
        with suppress(NotImplementedError):
            # Some non cooled cameras can still report the image sensor temperature
            header.set('CCD-TEMP',
                       get_quantity_value(self.temperature, u.Celsius),
                       'Degrees C')
        if self.is_cooled_camera:
            header.set('SET-TEMP',
                       get_quantity_value(self.target_temperature, u.Celsius),
                       'Degrees C')
            header.set('COOL-POW',
                       get_quantity_value(self.cooling_power, u.percent),
                       'Percentage')
        header.set('CAM-ID', self.uid, 'Camera serial number')
        header.set('CAM-NAME', self.name, 'Camera name')
        header.set('CAM-MOD', self.model, 'Camera model')

        for sub_name, subcomponent in self.subcomponents.items():
            header = subcomponent._add_fits_keywords(header)

        return header
Пример #17
0
    def process_exposure(self,
                         metadata,
                         observation_event,
                         compress_fits=None,
                         record_observations=None,
                         make_pretty_images=None):
        """ Processes the exposure.

        Performs the following steps:

            1. First checks to make sure that the file exists on the file system.
            2. Calls `_process_fits` with the filename and info, which is specific to each camera.
            3. Makes pretty images if requested.
            4. Records observation metadata if requested.
            5. Compresses FITS files if requested.
            6. Sets the observation_event.

        If the camera is a primary camera, extract the jpeg image and save metadata to database
        `current` collection. Saves metadata to `observations` collection for all images.

        Args:
            metadata (dict): Header metadata saved for the image
            observation_event (threading.Event): An event that is set signifying that the
                camera is done with this exposure
            compress_fits (bool or None): If FITS files should be fpacked into .fits.fz.
                If None (default), checks the `observations.compress_fits` config-server key.
            record_observations (bool or None): If observation metadata should be saved.
                If None (default), checks the `observations.record_observations`
                config-server key.
            make_pretty_images (bool or None): If should make a jpg from raw image.
                If None (default), checks the `observations.make_pretty_images`
                config-server key.

        Raises:
            FileNotFoundError: If the FITS file isn't at the specified location.
        """
        # Wait for exposure to complete. Timeout handled by exposure thread.
        while self.is_exposing:
            time.sleep(1)

        self.logger.debug(
            f'Starting exposure processing for {observation_event}')

        if compress_fits is None:
            compress_fits = self.get_config('observations.compress_fits',
                                            default=False)

        if make_pretty_images is None:
            make_pretty_images = self.get_config(
                'observations.make_pretty_images', default=False)

        image_id = metadata['image_id']
        seq_id = metadata['sequence_id']
        file_path = metadata['file_path']
        exptime = metadata['exptime']
        field_name = metadata['field_name']

        # Make sure image exists.
        if not os.path.exists(file_path):
            observation_event.set()
            raise FileNotFoundError(
                f"Expected image at file_path={file_path!r} does not exist or "
                + "cannot be accessed, cannot process.")

        self.logger.debug(f'Starting FITS processing for {file_path}')
        file_path = self._process_fits(file_path, metadata)
        self.logger.debug(f'Finished FITS processing for {file_path}')

        # TODO make this async and take it out of camera.
        if make_pretty_images:
            try:
                image_title = f'{field_name} [{exptime}s] {seq_id}'

                self.logger.debug(
                    f"Making pretty image for file_path={file_path!r}")
                link_path = None
                if metadata['is_primary']:
                    # This should be in the config somewhere.
                    link_path = os.path.expandvars('$PANDIR/images/latest.jpg')

                img_utils.make_pretty_image(file_path,
                                            title=image_title,
                                            link_path=link_path)
            except Exception as e:  # pragma: no cover
                self.logger.warning(
                    f'Problem with extracting pretty image: {e!r}')

        metadata['exptime'] = get_quantity_value(metadata['exptime'],
                                                 unit='seconds')

        if record_observations:
            self.logger.debug(f"Adding current observation to db: {image_id}")
            self.db.insert_current('observations', metadata)

        if compress_fits:
            self.logger.debug(f'Compressing file_path={file_path!r}')
            compressed_file_path = fits_utils.fpack(file_path)
            self.logger.debug(f'Compressed {compressed_file_path}')

        # Mark the event as done
        observation_event.set()
Пример #18
0
    def __init__(self,
                 name='Generic Camera',
                 model='simulator',
                 port=None,
                 primary=False,
                 *args,
                 **kwargs):
        super().__init__(*args, **kwargs)

        self.model = model
        self.port = port
        self.name = name
        self.is_primary = primary

        self._filter_type = kwargs.get('filter_type', 'RGGB')
        self._serial_number = kwargs.get('serial_number', 'XXXXXX')
        self._readout_time = get_quantity_value(kwargs.get(
            'readout_time', 5.0),
                                                unit=u.second)
        self._file_extension = kwargs.get('file_extension', 'fits')
        self._timeout = get_quantity_value(kwargs.get('timeout', 10),
                                           unit=u.second)

        # Default is uncooled camera. Should be set to True if appropriate in camera connect()
        # method, based on info received from camera.
        self._is_cooled_camera = False
        self._cooling_enabled = False
        self._is_temperature_stable = False
        self._temperature_thread = None
        self.temperature_tolerance = kwargs.get('temperature_tolerance',
                                                0.5 * u.Celsius)
        self._cooling_required_table_time = None
        self._cooling_sleep_delay = None
        self._cooling_timeout = None

        self._connected = False
        self._current_observation = None
        self._is_exposing_event = threading.Event()
        self._exposure_error = None

        # By default assume camera isn't capable of internal darks.
        self._internal_darks = kwargs.get('internal_darks', False)

        # Set up any subcomponents.
        self.subcomponents = dict()
        for attr_name, class_path in self._SUBCOMPONENT_LIST.items():
            # Create the subcomponent as an attribute with default None.
            self.logger.debug(
                f'Setting default attr_name={attr_name!r} to None')
            setattr(self, attr_name, None)

            # If given subcomponent class (or dict), try to create instance.
            subcomponent = kwargs.get(attr_name)
            if subcomponent is not None:
                self.logger.debug(
                    f'Found subcomponent={subcomponent!r}, creating instance')

                subcomponent = self._create_subcomponent(
                    class_path, subcomponent)
                self.logger.debug(
                    f'Assigning subcomponent={subcomponent!r} to attr_name={attr_name!r}'
                )
                setattr(self, attr_name, subcomponent)
                # Keep a list of active subcomponents
                self.subcomponents[attr_name] = subcomponent

        self.logger.debug(f'Camera created: {self}')