示例#1
0
文件: qc.py 项目: LCOGT/banzai
def save_qc_results(runtime_context, qc_results, image, **kwargs):
    """
    Save the Quality Control results to ElasticSearch

    Parameters
    ----------
    runtime_context: object
                      Context instance with runtime values
    qc_results : dict
                 Dictionary of key value pairs to be saved to ElasticSearch
    image : banzai.images.Image
            Image that should be linked

    Notes
    -----
    File name, site, camera, dayobs and timestamp are always saved in the database.
    """

    es_output = {}
    if getattr(runtime_context, 'post_to_elasticsearch', False):
        filename, results_to_save = format_qc_results(qc_results, image)
        es = elasticsearch.Elasticsearch(runtime_context.elasticsearch_url)
        try:
            es_output = es.update(index=runtime_context.elasticsearch_qc_index,
                                  doc_type=runtime_context.elasticsearch_doc_type,
                                  id=filename, body={'doc': results_to_save, 'doc_as_upsert': True},
                                  retry_on_conflict=5, timestamp=results_to_save['@timestamp'], **kwargs)
        except Exception:
            error_message = 'Cannot update elasticsearch index to URL \"{url}\": {exception}'
            logger.error(error_message.format(url=runtime_context.elasticsearch_url,
                                              exception=logs.format_exception()))
    return es_output
示例#2
0
def process_master_maker(instrument, frame_type, min_date, max_date,
                         runtime_context):
    extra_tags = {
        'type': instrument.type,
        'site': instrument.site,
        'enclosure': instrument.enclosure,
        'telescope': instrument.telescope,
        'camera': instrument.camera,
        'obstype': frame_type,
        'min_date': min_date,
        'max_date': max_date
    }
    logger.info("Making master frames", extra_tags=extra_tags)
    image_path_list = dbs.get_individual_calibration_images(
        instrument,
        frame_type,
        min_date,
        max_date,
        db_address=runtime_context.db_address)
    if len(image_path_list) == 0:
        logger.info("No calibration frames found to stack",
                    extra_tags=extra_tags)

    try:
        run_master_maker(image_path_list, runtime_context, frame_type)
    except Exception:
        logger.error(logs.format_exception())
    logger.info("Finished")
示例#3
0
def run_realtime_pipeline():
    extra_console_arguments = [{'args': ['--n-processes'],
                                'kwargs': {'dest': 'n_processes', 'default': 12,
                                           'help': 'Number of listener processes to spawn.', 'type': int}},
                               {'args': ['--queue-name'],
                                'kwargs': {'dest': 'queue_name', 'default': 'banzai_pipeline',
                                           'help': 'Name of the queue to listen to from the fits exchange.'}}]

    runtime_context = parse_args(extra_console_arguments=extra_console_arguments)

    # Need to keep the amqp logger level at least as high as INFO,
    # or else it send heartbeat check messages every second
    logging.getLogger('amqp').setLevel(max(logger.level, getattr(logging, 'INFO')))

    try:
        dbs.populate_instrument_tables(db_address=runtime_context.db_address)
    except Exception:
        logger.error('Could not connect to the configdb: {error}'.format(error=logs.format_exception()))

    logger.info('Starting pipeline listener')

    fits_exchange = Exchange('fits_files', type='fanout')
    listener = RealtimeModeListener(runtime_context)

    with Connection(runtime_context.broker_url) as connection:
        listener.connection = connection.clone()
        listener.queue = Queue(runtime_context.queue_name, fits_exchange)
        try:
            listener.run()
        except listener.connection.connection_errors:
            listener.connection = connection.clone()
            listener.ensure_connection(max_retries=10)
        except KeyboardInterrupt:
            logger.info('Shutting down pipeline listener.')
示例#4
0
def get_primary_header(filename):
    try:
        hdulist = open_fits_file(filename)
        return hdulist[0].header
    except Exception:
        logger.error("Unable to open fits file: {}".format(logs.format_exception()), extra_tags={'filename': filename})
        return None
示例#5
0
文件: stages.py 项目: LCOGT/banzai
 def run(self, image):
     if image is None:
         return image
     logger.info('Running {0}'.format(self.stage_name), image=image)
     try:
         image = self.do_stage(image)
     except Exception:
         logger.error(logs.format_exception())
     return image
示例#6
0
def get_primary_header(filename):
    try:
        hdulist = open_fits_file(filename)
        return hdulist[0].header
    except Exception:
        logger.error("Unable to open fits file: {}".format(
            logs.format_exception()),
                     extra_tags={'filename': filename})
        return None
示例#7
0
 def _post_to_archive(self, filepath, runtime_context):
     logger.info('Posting file to the archive', image=self)
     try:
         file_utils.post_to_archive_queue(filepath,
                                          runtime_context.broker_url)
     except Exception:
         logger.error("Could not post to ingester: {error}".format(
             error=logs.format_exception()),
                      image=self)
示例#8
0
文件: stages.py 项目: baulml/banzai
    def run(self, image):
        if image is None:
            return image
        logger.info('Running {0}'.format(self.stage_name), image=image)
        try:
            image = self.do_stage(image)
            return image

        except Exception:
            logger.error(logs.format_exception())
            return None
示例#9
0
文件: stages.py 项目: LCOGT/banzai
 def run(self, images):
     images.sort(key=self.get_grouping)
     processed_images = []
     for _, image_set in itertools.groupby(images, self.get_grouping):
         try:
             image_set = list(image_set)
             logger.info('Running {0}'.format(self.stage_name), image=image_set[0])
             processed_images += self.do_stage(image_set)
         except Exception:
             logger.error(logs.format_exception())
     return processed_images
示例#10
0
def read_image(filename, runtime_context):
    try:
        image = FRAME_CLASS(runtime_context, filename=filename)
        if image.instrument is None:
            logger.error("Image instrument attribute is None, aborting", image=image)
            raise IOError
        munge(image)
        return image
    except Exception:
        logger.error('Error loading image: {error}'.format(error=logs.format_exception()),
                     extra_tags={'filename': filename})
示例#11
0
文件: stages.py 项目: baulml/banzai
 def run(self, images):
     images.sort(key=self.get_grouping)
     processed_images = []
     for _, image_set in itertools.groupby(images, self.get_grouping):
         try:
             image_set = list(image_set)
             logger.info('Running {0}'.format(self.stage_name),
                         image=image_set[0])
             processed_images += self.do_stage(image_set)
         except Exception:
             logger.error(logs.format_exception())
     return processed_images
示例#12
0
def reduce_single_frame():
    extra_console_arguments = [{'args': ['--filepath'],
                                'kwargs': {'dest': 'path', 'help': 'Full path to the file to process'}}]
    runtime_context = parse_directory_args(extra_console_arguments=extra_console_arguments)
    # Short circuit
    if not image_utils.image_can_be_processed(fits_utils.get_primary_header(runtime_context.path),
                                              runtime_context):
        logger.error('Image cannot be processed. Check to make sure the instrument '
                     'is in the database and that the OBSTYPE is recognized by BANZAI',
                     extra_tags={'filename': runtime_context.path})
        return
    try:
        run(runtime_context.path, runtime_context)
    except Exception:
        logger.error(logs.format_exception(), extra_tags={'filepath': runtime_context.path})
示例#13
0
def read_image(filename, runtime_context):
    try:
        frame_class = import_utils.import_attribute(
            runtime_context.FRAME_CLASS)
        image = frame_class(runtime_context, filename=filename)
        if image.instrument is None:
            logger.error("Image instrument attribute is None, aborting",
                         image=image)
            raise IOError
        munge(image)
        return image
    except Exception:
        logger.error('Error loading image: {error}'.format(
            error=logs.format_exception()),
                     extra_tags={'filename': filename})
示例#14
0
def select_images(image_list, image_type, db_address, ignore_schedulability):
    images = []
    for filename in image_list:
        try:
            header = get_primary_header(filename)
            should_process = image_can_be_processed(header, db_address)
            should_process &= (image_type is None or get_obstype(header) == image_type)
            if not ignore_schedulability:
                instrument = dbs.get_instrument(header, db_address=db_address)
                should_process &= instrument.schedulable
            if should_process:
                images.append(filename)
        except Exception:
            logger.error(logs.format_exception(), extra_tags={'filename': filename})
    return images
示例#15
0
    def do_stage(self, image):
        master_calibration_filename = self.get_calibration_filename(image)

        if master_calibration_filename is None:
            self.on_missing_master_calibration(image)
            return image

        master_calibration_image = FRAME_CLASS(self.runtime_context, filename=master_calibration_filename)
        try:
            image_utils.check_image_homogeneity([image, master_calibration_image], self.master_selection_criteria)
        except image_utils.InhomogeneousSetException:
            logger.error(logs.format_exception(), image=image)
            return None

        return self.apply_master_calibration(image, master_calibration_image)
示例#16
0
def update_db():
    parser = argparse.ArgumentParser(description="Query the configdb to ensure that the instruments table"
                                                 "has the most up-to-date information")

    parser.add_argument("--log-level", default='debug', choices=['debug', 'info', 'warning',
                                                                 'critical', 'fatal', 'error'])
    parser.add_argument('--db-address', dest='db_address',
                        default='mysql://*****:*****@localhost/test',
                        help='Database address: Should be in SQLAlchemy form')
    args = parser.parse_args()
    logs.set_log_level(args.log_level)

    try:
        dbs.populate_instrument_tables(db_address=args.db_address)
    except Exception:
        logger.error('Could not populate instruments table: {error}'.format(error=logs.format_exception()))
示例#17
0
文件: celery.py 项目: baulml/banzai
def process_image(path: str, runtime_context: dict):
    runtime_context = Context(runtime_context)
    logger.info('Running process image.')
    try:
        if realtime_utils.need_to_process_image(path, runtime_context):
            logger.info('Reducing frame', extra_tags={'filename': os.path.basename(path)})

            # Increment the number of tries for this file
            realtime_utils.increment_try_number(path, db_address=runtime_context.db_address)

            run(path, runtime_context)
            realtime_utils.set_file_as_processed(path, db_address=runtime_context.db_address)

    except Exception:
        logger.error("Exception processing frame: {error}".format(error=logs.format_exception()),
                     extra_tags={'filename': os.path.basename(path)})
示例#18
0
文件: celery.py 项目: LCOGT/banzai
def process_image(path, runtime_context_dict):
    logger.info('Running process image.')
    runtime_context = Context(runtime_context_dict)
    try:
        if realtime_utils.need_to_process_image(path, runtime_context,
                                                db_address=runtime_context.db_address,
                                                max_tries=runtime_context.max_tries):
            logger.info('Reducing frame', extra_tags={'filename': os.path.basename(path)})

            # Increment the number of tries for this file
            realtime_utils.increment_try_number(path, db_address=runtime_context.db_address)

            run(path, runtime_context)
            realtime_utils.set_file_as_processed(path, db_address=runtime_context.db_address)

    except Exception:
        logger.error("Exception processing frame: {error}".format(error=logs.format_exception()),
                     extra_tags={'filename': os.path.basename(path)})
示例#19
0
def select_images(image_list, image_type, context):
    images = []
    for filename in image_list:
        try:
            header = get_primary_header(filename)
            should_process = image_can_be_processed(header, context)
            should_process &= (image_type is None
                               or get_obstype(header) == image_type)
            if not context.ignore_schedulability:
                instrument = dbs.get_instrument(header,
                                                db_address=context.db_address)
                should_process &= instrument.schedulable
            if should_process:
                images.append(filename)
        except Exception:
            logger.error(logs.format_exception(),
                         extra_tags={'filename': filename})
    return images
示例#20
0
def process_master_maker(runtime_context, instrument, frame_type, min_date, max_date, use_masters=False):
    extra_tags = {'type': instrument.type, 'site': instrument.site,
                  'enclosure': instrument.enclosure, 'telescope': instrument.telescope,
                  'camera': instrument.camera, 'obstype': frame_type,
                  'min_date': min_date,
                  'max_date': max_date}
    logger.info("Making master frames", extra_tags=extra_tags)
    image_path_list = dbs.get_individual_calibration_images(instrument, frame_type, min_date, max_date,
                                                            use_masters=use_masters,
                                                            db_address=runtime_context.db_address)
    if len(image_path_list) == 0:
        logger.info("No calibration frames found to stack", extra_tags=extra_tags)

    try:
        run_master_maker(image_path_list, runtime_context, frame_type)
    except Exception:
        logger.error(logs.format_exception())
    logger.info("Finished")
示例#21
0
    def do_stage(self, images):
        try:
            min_images = settings.CALIBRATION_MIN_FRAMES[self.calibration_type.upper()]
        except KeyError:
            msg = 'The minimum number of frames required to create a master calibration of type ' \
                  '{calibration_type} has not been specified in the settings.'
            logger.error(msg.format(calibration_type=self.calibration_type.upper()))
            return []
        if len(images) < min_images:
            # Do nothing
            msg = 'Number of images less than minimum requirement of {min_images}, not combining'
            logger.warning(msg.format(min_images=min_images))
            return []
        try:
            image_utils.check_image_homogeneity(images, self.group_by_attributes())
        except image_utils.InhomogeneousSetException:
            logger.error(logs.format_exception())
            return []

        return [self.make_master_calibration_frame(images)]
示例#22
0
文件: qc.py 项目: baulml/banzai
def save_qc_results(runtime_context, qc_results, image, **kwargs):
    """
    Save the Quality Control results to ElasticSearch

    Parameters
    ----------
    runtime_context: object
                      Context instance with runtime values
    qc_results : dict
                 Dictionary of key value pairs to be saved to ElasticSearch
    image : banzai.images.Image
            Image that should be linked

    Notes
    -----
    File name, site, camera, dayobs and timestamp are always saved in the database.
    """

    es_output = {}
    if getattr(runtime_context, 'post_to_elasticsearch', False):
        filename, results_to_save = format_qc_results(qc_results, image)
        es = elasticsearch.Elasticsearch(runtime_context.elasticsearch_url)
        try:
            es_output = es.update(
                index=runtime_context.elasticsearch_qc_index,
                doc_type=runtime_context.elasticsearch_doc_type,
                id=filename,
                body={
                    'doc': results_to_save,
                    'doc_as_upsert': True
                },
                retry_on_conflict=5,
                timestamp=results_to_save['@timestamp'],
                **kwargs)
        except Exception:
            error_message = 'Cannot update elasticsearch index to URL \"{url}\": {exception}'
            logger.error(
                error_message.format(url=runtime_context.elasticsearch_url,
                                     exception=logs.format_exception()))
    return es_output
示例#23
0
    def do_stage(self, images):
        try:
            min_images = settings.CALIBRATION_MIN_FRAMES[
                self.calibration_type.upper()]
        except KeyError:
            msg = 'The minimum number of frames required to create a master calibration of type ' \
                  '{calibration_type} has not been specified in the settings.'
            logger.error(
                msg.format(calibration_type=self.calibration_type.upper()))
            return []
        if len(images) < min_images:
            # Do nothing
            msg = 'Number of images less than minimum requirement of {min_images}, not combining'
            logger.warning(msg.format(min_images=min_images))
            return []
        try:
            image_utils.check_image_homogeneity(images,
                                                self.group_by_attributes())
        except image_utils.InhomogeneousSetException:
            logger.error(logs.format_exception())
            return []

        return [self.make_master_calibration_frame(images)]
示例#24
0
    def do_stage(self, image):
        try:
            # Set the number of source pixels to be 5% of the total. This keeps us safe from
            # satellites and airplanes.
            sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

            data = image.data.copy()
            error = (np.abs(data) + image.readnoise**2.0)**0.5
            mask = image.bpm > 0

            # Fits can be backwards byte order, so fix that if need be and subtract
            # the background
            try:
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            except ValueError:
                data = data.byteswap(True).newbyteorder()
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            bkg.subfrom(data)

            # Do an initial source detection
            # TODO: Add back in masking after we are sure SEP works
            sources = sep.extract(data,
                                  self.threshold,
                                  minarea=self.min_area,
                                  err=error,
                                  deblend_cont=0.005)

            # Convert the detections into a table
            sources = Table(sources)

            # We remove anything with a detection flag >= 8
            # This includes memory overflows and objects that are too close the edge
            sources = sources[sources['flag'] < 8]

            sources = array_utils.prune_nans_from_table(sources)

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

            # Fix any value of theta that are invalid due to floating point rounding
            # -pi / 2 < theta < pi / 2
            sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
            sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

            # Calculate the kron radius
            kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'],
                                              sources['a'], sources['b'],
                                              sources['theta'], 6.0)
            sources['flag'] |= krflag
            sources['kronrad'] = kronrad

            # Calcuate the equivilent of flux_auto
            flux, fluxerr, flag = sep.sum_ellipse(data,
                                                  sources['x'],
                                                  sources['y'],
                                                  sources['a'],
                                                  sources['b'],
                                                  np.pi / 2.0,
                                                  2.5 * kronrad,
                                                  subpix=1,
                                                  err=error)
            sources['flux'] = flux
            sources['fluxerr'] = fluxerr
            sources['flag'] |= flag

            # Do circular aperture photometry for diameters of 1" to 6"
            for diameter in [1, 2, 3, 4, 5, 6]:
                flux, fluxerr, flag = sep.sum_circle(data,
                                                     sources['x'],
                                                     sources['y'],
                                                     diameter / 2.0 /
                                                     image.pixel_scale,
                                                     gain=1.0,
                                                     err=error)
                sources['fluxaper{0}'.format(diameter)] = flux
                sources['fluxerr{0}'.format(diameter)] = fluxerr
                sources['flag'] |= flag

            # 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

            # Cut individual bright pixels. Often cosmic rays
            sources = sources[fwhm > 1.0]

            # Measure the flux profile
            flux_radii, flag = sep.flux_radius(data,
                                               sources['x'],
                                               sources['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]

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

            # Calculate the average background at each source
            bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(),
                                                     sources['x'],
                                                     sources['y'],
                                                     sources['a'],
                                                     sources['b'],
                                                     np.pi / 2.0,
                                                     2.5 * sources['kronrad'],
                                                     subpix=1)
            # masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
            #                                         sources['a'], sources['b'], np.pi / 2.0,
            #                                         2.5 * kronrad, subpix=1)

            background_area = (
                2.5 * sources['kronrad']
            )**2.0 * sources['a'] * sources['b'] * np.pi  # - masksum
            sources['background'] = bkgflux
            sources['background'][background_area > 0] /= background_area[
                background_area > 0]
            # Update the catalog to match fits convention instead of python array convention
            sources['x'] += 1.0
            sources['y'] += 1.0

            sources['xpeak'] += 1
            sources['ypeak'] += 1

            sources['xwin'] += 1.0
            sources['ywin'] += 1.0

            sources['theta'] = np.degrees(sources['theta'])

            catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                              'flux', 'fluxerr', 'peak', 'fluxaper1',
                              'fluxerr1', 'fluxaper2', 'fluxerr2', 'fluxaper3',
                              'fluxerr3', 'fluxaper4', 'fluxerr4', 'fluxaper5',
                              'fluxerr5', 'fluxaper6', 'fluxerr6',
                              'background', 'fwhm', 'a', 'b', 'theta',
                              'kronrad', 'ellipticity', 'fluxrad25',
                              'fluxrad50', 'fluxrad75', 'x2', 'y2', 'xy',
                              'flag']

            # Add the units and description to the catalogs
            catalog['x'].unit = 'pixel'
            catalog['x'].description = 'X coordinate of the object'
            catalog['y'].unit = 'pixel'
            catalog['y'].description = 'Y coordinate of the object'
            catalog['xwin'].unit = 'pixel'
            catalog['xwin'].description = 'Windowed X coordinate of the object'
            catalog['ywin'].unit = 'pixel'
            catalog['ywin'].description = 'Windowed Y coordinate of the object'
            catalog['xpeak'].unit = 'pixel'
            catalog['xpeak'].description = 'X coordinate of the peak'
            catalog['ypeak'].unit = 'pixel'
            catalog['ypeak'].description = 'Windowed Y coordinate of the peak'
            catalog['flux'].unit = 'count'
            catalog[
                'flux'].description = 'Flux within a Kron-like elliptical aperture'
            catalog['fluxerr'].unit = 'count'
            catalog[
                'fluxerr'].description = 'Error on the flux within Kron aperture'
            catalog['peak'].unit = 'count'
            catalog['peak'].description = 'Peak flux (flux at xpeak, ypeak)'
            for diameter in [1, 2, 3, 4, 5, 6]:
                catalog['fluxaper{0}'.format(diameter)].unit = 'count'
                catalog['fluxaper{0}'.format(
                    diameter
                )].description = 'Flux from fixed circular aperture: {0}" diameter'.format(
                    diameter)
                catalog['fluxerr{0}'.format(diameter)].unit = 'count'
                catalog['fluxerr{0}'.format(
                    diameter
                )].description = 'Error on Flux from circular aperture: {0}"'.format(
                    diameter)

            catalog['background'].unit = 'count'
            catalog[
                'background'].description = 'Average background value in the aperture'
            catalog['fwhm'].unit = 'pixel'
            catalog['fwhm'].description = 'FWHM of the object'
            catalog['a'].unit = 'pixel'
            catalog['a'].description = 'Semi-major axis of the object'
            catalog['b'].unit = 'pixel'
            catalog['b'].description = 'Semi-minor axis of the object'
            catalog['theta'].unit = 'degree'
            catalog['theta'].description = 'Position angle of the object'
            catalog['kronrad'].unit = 'pixel'
            catalog['kronrad'].description = 'Kron radius used for extraction'
            catalog['ellipticity'].description = 'Ellipticity'
            catalog['fluxrad25'].unit = 'pixel'
            catalog[
                'fluxrad25'].description = 'Radius containing 25% of the flux'
            catalog['fluxrad50'].unit = 'pixel'
            catalog[
                'fluxrad50'].description = 'Radius containing 50% of the flux'
            catalog['fluxrad75'].unit = 'pixel'
            catalog[
                'fluxrad75'].description = 'Radius containing 75% of the flux'
            catalog['x2'].unit = 'pixel^2'
            catalog[
                'x2'].description = 'Variance on X coordinate of the object'
            catalog['y2'].unit = 'pixel^2'
            catalog[
                'y2'].description = 'Variance on Y coordinate of the object'
            catalog['xy'].unit = 'pixel^2'
            catalog['xy'].description = 'XY covariance of the object'
            catalog[
                'flag'].description = 'Bit mask of extraction/photometry flags'

            catalog.sort('flux')
            catalog.reverse()

            # Save some background statistics in the header
            mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
            image.header['L1MEAN'] = (
                mean_background,
                '[counts] Sigma clipped mean of frame background')

            median_background = np.median(bkg.back())
            image.header['L1MEDIAN'] = (median_background,
                                        '[counts] Median of frame background')

            std_background = stats.robust_standard_deviation(bkg.back())
            image.header['L1SIGMA'] = (
                std_background, '[counts] Robust std dev of frame background')

            # Save some image statistics to the header
            good_objects = catalog['flag'] == 0
            for quantity in ['fwhm', 'ellipticity', 'theta']:
                good_objects = np.logical_and(
                    good_objects, np.logical_not(np.isnan(catalog[quantity])))
            if good_objects.sum() == 0:
                image.header['L1FWHM'] = ('NaN',
                                          '[arcsec] Frame FWHM in arcsec')
                image.header['L1ELLIP'] = ('NaN',
                                           'Mean image ellipticity (1-B/A)')
                image.header['L1ELLIPA'] = (
                    'NaN', '[deg] PA of mean image ellipticity')
            else:
                seeing = np.median(
                    catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing,
                                          '[arcsec] Frame FWHM in arcsec')

                mean_ellipticity = stats.sigma_clipped_mean(
                    catalog['ellipticity'][good_objects], 3.0)
                image.header['L1ELLIP'] = (mean_ellipticity,
                                           'Mean image ellipticity (1-B/A)')

                mean_position_angle = stats.sigma_clipped_mean(
                    catalog['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (
                    mean_position_angle, '[deg] PA of mean image ellipticity')

            logging_tags = {
                key: float(image.header[key])
                for key in [
                    'L1MEAN', 'L1MEDIAN', 'L1SIGMA', 'L1FWHM', 'L1ELLIP',
                    'L1ELLIPA'
                ]
            }

            logger.info('Extracted sources',
                        image=image,
                        extra_tags=logging_tags)
            # adding catalog (a data table) to the appropriate images attribute.
            image.data_tables['catalog'] = DataTable(data_table=catalog,
                                                     name='CAT')
        except Exception:
            logger.error(logs.format_exception(), image=image)
        return image