Source code for ginga.util.iqcalc

"""Module to handle image quality calculations."""
#
# iqcalc.py -- image quality calculations on FITS data
#
# This is open-source software licensed under a BSD license.
# Please see the file LICENSE.txt for details.
#

import math
import logging
import threading

import numpy as np

try:
    import scipy.optimize as optimize
    import scipy.ndimage as ndimage
    from scipy.ndimage import maximum_filter
    from scipy.interpolate import interp1d
    have_scipy = True
except ImportError:
    have_scipy = False

from ginga.misc import Bunch

__all__ = ['get_mean', 'get_median', 'IQCalcError', 'IQCalc']


[docs] def get_mean(data_np): """Calculate mean for valid values. Parameters ---------- data_np : ndarray Input array. Can contain masked values. Returns ------- result : float Mean of array values that are finite. If array contains no finite values, returns NaN. """ i = np.isfinite(data_np) if not np.any(i): return np.nan # NOTE: we use "ma" version of mean because this can be used with # masked arrays created by cutting out non-rectangular shapes return np.ma.mean(data_np[i])
[docs] def get_median(data_np): """Like :func:`get_mean` but for median.""" i = np.isfinite(data_np) if not np.any(i): return np.nan # NOTE: we use "ma" version of median because this can be used with # masked arrays created by cutting out non-rectangular shapes return np.ma.median(data_np[i])
[docs] class IQCalcError(Exception): """Base exception for raising errors in this module.""" pass
[docs] class IQCalc(object): """Class to handle model fitting and FWHM calculations. Parameters ---------- logger : obj or `None` Python logger. If not given, one will be created. Attributes ---------- lock : :py:class:`threading.RLock` For mutex around `scipy.optimize`, which seems to be non-threadsafe. skylevel_magnification, skylevel_offset : float For adjustments to sky background level. """ def __init__(self, logger=None): if not logger: logger = logging.getLogger('IQCalc') self.logger = logger # for mutex around scipy.optimize, which seems to be non-threadsafe self.lock = threading.RLock() # for adjustments to background level self.skylevel_magnification = 1.05 self.skylevel_offset = 40.0 # FWHM CALCULATION
[docs] def gaussian(self, x, p): """Evaluate Gaussian function in 1D. See :meth:`calc_fwhm`. Parameters ---------- x : array-like X values. p : tuple of float Parameters for Gaussian, i.e., ``(mean, stddev, amplitude)``. Returns ------- y : array-like Y values. """ y = (1.0 / (p[1] * np.sqrt(2 * np.pi)) * np.exp(-(x - p[0]) ** 2 / (2 * p[1] ** 2))) * p[2] return y
[docs] def calc_fwhm_gaussian(self, arr1d, medv=None, gauss_fn=None): """FWHM calculation on a 1D array by using least square fitting of a Gaussian function on the data. Parameters ---------- arr1d : array-like 1D array cut in either X or Y direction on the object. medv : float or `None` Median of the data. If not given, it is calculated from ``arr1d``. gauss_fn : func or `None` Gaussian function for fitting. If not given, :meth:`gaussian` is used. Returns ------- res : `~ginga.misc.Bunch.Bunch` Fitting results. Raises ------ IQCalcError Fitting failed. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") if gauss_fn is None: gauss_fn = self.gaussian N = len(arr1d) X = np.array(list(range(N))) Y = arr1d # Fitting works more reliably if we do the following # a. subtract sky background if medv is None: medv = get_median(Y) Y = Y - medv maxv = Y.max() # b. clamp to 0..max (of the sky subtracted field) Y = Y.clip(0, maxv) # Fit a gaussian p0 = [0, N - 1, maxv] # Inital guess # Distance to the target function errfunc = lambda p, x, y: gauss_fn(x, p) - y # noqa # Least square fit to the gaussian with self.lock: # NOTE: without this mutex, optimize.leastsq causes a fatal error # sometimes--it appears not to be thread safe. # The error is: # "SystemError: null argument to internal routine" # "Fatal Python error: GC object already tracked" p1, success = optimize.leastsq(errfunc, p0[:], args=(X, Y)) if not success: raise IQCalcError("FWHM Gaussian fitting failed") mu, sdev, maxv = p1 self.logger.debug("mu=%f sdev=%f maxv=%f" % (mu, sdev, maxv)) # Now that we have the sdev from fitting, we can calculate FWHM fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) * sdev # some routines choke on numpy values and need "pure" Python floats # e.g. when marshalling through a remote procedure interface fwhm = float(fwhm) mu = float(mu) sdev = float(sdev) maxv = float(maxv) res = Bunch.Bunch(fwhm=fwhm, mu=mu, sdev=sdev, maxv=maxv, fit_fn=gauss_fn, fit_args=[mu, sdev, maxv]) return res
[docs] def moffat(self, x, p): """Evaluate Moffat function in 1D. See :meth:`calc_fwhm`. Parameters ---------- x : array-like X values. p : tuple of float Parameters for Moffat, i.e., ``(x_0, gamma, alpha, amplitude)``, where ``x_0`` a.k.a. mean and ``gamma`` core width. Returns ------- y : array-like Y values. """ y = (1.0 + (x - p[0]) ** 2 / p[1] ** 2) ** (-1.0 * p[2]) * p[3] return y
[docs] def calc_fwhm_moffat(self, arr1d, medv=None, moffat_fn=None): """FWHM calculation on a 1D array by using least square fitting of a Moffat function on the data. Parameters ---------- arr1d : array-like 1D array cut in either X or Y direction on the object. medv : float or `None` Median of the data. If not given, it is calculated from ``arr1d``. moffat_fn : func or `None` Moffat function for fitting. If not given, :meth:`moffat` is used. Returns ------- res : `~ginga.misc.Bunch.Bunch` Fitting results. Raises ------ IQCalcError Fitting failed. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") if moffat_fn is None: moffat_fn = self.moffat N = len(arr1d) X = np.array(list(range(N))) Y = arr1d # Fitting works more reliably if we do the following # a. subtract sky background if medv is None: medv = get_median(Y) Y = Y - medv maxv = Y.max() # b. clamp to 0..max (of the sky subtracted field) Y = Y.clip(0, maxv) # Fit a moffat p0 = [0, N - 1, 2, maxv] # Inital guess # Distance to the target function errfunc = lambda p, x, y: moffat_fn(x, p) - y # noqa # Least square fit to the gaussian with self.lock: # NOTE: without this mutex, optimize.leastsq causes a fatal error # sometimes--it appears not to be thread safe. # The error is: # "SystemError: null argument to internal routine" # "Fatal Python error: GC object already tracked" p1, success = optimize.leastsq(errfunc, p0[:], args=(X, Y)) if not success: raise IQCalcError("FWHM Moffat fitting failed") mu, width, power, maxv = p1 width = np.abs(width) self.logger.debug("mu=%f width=%f power=%f maxv=%f" % ( mu, width, power, maxv)) fwhm = 2.0 * width * np.sqrt(2.0 ** (1.0 / power) - 1.0) # some routines choke on numpy values and need "pure" Python floats # e.g. when marshalling through a remote procedure interface fwhm = float(fwhm) mu = float(mu) width = float(width) power = float(power) maxv = float(maxv) res = Bunch.Bunch(fwhm=fwhm, mu=mu, width=width, power=power, maxv=maxv, fit_fn=moffat_fn, fit_args=[mu, width, power, maxv]) return res
[docs] def calc_fwhm(self, arr1d, medv=None, method_name='gaussian'): """Calculate FWHM for the given input array. Parameters ---------- arr1d : array-like 1D array cut in either X or Y direction on the object. medv : float or `None` Median of the data. If not given, it is calculated from ``arr1d``. method_name : {'gaussian', 'moffat'} Function to use for fitting. Returns ------- res : `~ginga.misc.Bunch.Bunch` Fitting results. """ # Calculate FWHM in each direction fwhm_fn = self.calc_fwhm_gaussian if method_name == 'moffat': fwhm_fn = self.calc_fwhm_moffat return fwhm_fn(arr1d, medv=medv)
[docs] def get_fwhm(self, x, y, radius, data, medv=None, method_name='gaussian'): """Get the FWHM values of the object at the given coordinates and radius. Parameters ---------- x, y : int Indices of the object location in data array. radius : float Radius of the region encompassing the object. data : array-like Data array. medv, method_name See :meth:`calc_fwhm`. Returns ------- fwhm_x, fwhm_y : float FWHM in X and Y, respectively. ctr_x, ctr_y : float Center in X and Y, respectively. x_res, y_res : dict Fit results from :meth:`calc_fwhm` in X and Y, respectively. """ if medv is None: medv = get_median(data) # Get two cuts of the data, one in X and one in Y x0, y0, xarr, yarr = self.cut_cross(x, y, radius, data) # Calculate FWHM in each direction x_res = self.calc_fwhm(xarr, medv=medv, method_name=method_name) fwhm_x, cx = x_res.fwhm, x_res.mu y_res = self.calc_fwhm(yarr, medv=medv, method_name=method_name) fwhm_y, cy = y_res.fwhm, y_res.mu ctr_x = x0 + cx ctr_y = y0 + cy self.logger.debug("fwhm_x,fwhm_y=%f,%f center=%f,%f" % ( fwhm_x, fwhm_y, ctr_x, ctr_y)) return (fwhm_x, fwhm_y, ctr_x, ctr_y, x_res, y_res)
[docs] def starsize(self, fwhm_x, deg_pix_x, fwhm_y, deg_pix_y): """Calculate average FWHM in arcseconds. Parameters ---------- fwhm_x : float FWHM in X (pixels). deg_pix_x : float Plate scale from CDELT1 in degrees per pixel. fwhm_y : float FWHM in Y (pixels). deg_pix_y : float Plate scale from CDELT2 in degrees per pixel. Returns ------- fwhm : float Average FWHM in arcseconds. """ cdelta1 = math.fabs(deg_pix_x) cdelta2 = math.fabs(deg_pix_y) fwhm = (fwhm_x * cdelta1 + fwhm_y * cdelta2) / 2.0 fwhm = fwhm * 3600.0 return fwhm
[docs] def centroid(self, data, xc, yc, radius): """Calculate centroid from center of mass. Parameters ---------- data : array-like Data array. xc, yc : int X and Y indices of the approximate center. radius : float Half-width of the region to consider around the given center. Returns ------- x, y : float Centroid indices. Raises ------ IQCalcError Missing dependency. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") xc, yc = int(xc), int(yc) x0, y0, arr = self.cut_region(xc, yc, int(radius), data) # See https://stackoverflow.com/questions/25369982/center-of-mass-for-roi-in-python cp_arr = np.asarray(arr) cy, cx = ndimage.center_of_mass(cp_arr) return (x0 + cx, y0 + cy)
# FINDING BRIGHT PEAKS
[docs] def get_threshold(self, data, sigma=5.0): """Calculate threshold for :meth:`find_bright_peaks`. Parameters ---------- data : array-like Data array. sigma : float Sigma for the threshold. Returns ------- threshold : float Threshold based on good data, its median, and the given sigma. """ # remove masked elements fdata = data[np.logical_not(np.ma.getmaskarray(data))] # remove Inf or NaN fdata = fdata[np.isfinite(fdata)] # find the median median = get_median(fdata) # NOTE: for this method a good default sigma is 5.0 dist = np.fabs(fdata - median).mean() threshold = median + sigma * dist # NOTE: for this method a good default sigma is 2.0 ## std = np.std(fdata - median) ## threshold = median + sigma * std self.logger.debug("calc threshold=%f" % (threshold)) return threshold
[docs] def find_bright_peaks(self, data, threshold=None, sigma=5, radius=5): """Find bright peak candidates in in the given data. Parameters ---------- data : array-like Input data to find peaks from. threshold : float or `None` Detection threshold. Below this value, an object is not considered a candidate. If not given, a default is calculated using :meth:`get_threshold` with the given ``sigma``. sigma : float Sigma for the threshold. radius : float Pixel radius for determining local maxima. If the desired objects are larger in size, specify a larger radius. Returns ------- peaks : list of tuple A list of candidate object coordinate tuples ``(x, y)`` in data. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") if threshold is None: # set threshold to default if none provided threshold = self.get_threshold(data, sigma=sigma) self.logger.debug("threshold defaults to %f (sigma=%f)" % ( threshold, sigma)) #self.logger.debug("filtering") data_max = maximum_filter(data, radius) maxima = (data == data_max) diff = data_max > threshold maxima[diff == 0] = 0 #self.logger.debug("finding") labeled, num_objects = ndimage.label(maxima) slices = ndimage.find_objects(labeled) peaks = [] for dy, dx in slices: xc = (dx.start + dx.stop - 1) / 2.0 yc = (dy.start + dy.stop - 1) / 2.0 # This is only an approximate center; use FWHM or centroid # calculation to refine further peaks.append((xc, yc)) self.logger.debug("peaks=%s" % (str(peaks))) return peaks
[docs] def cut_region(self, x, y, radius, data): """Return a cut region. Parameters ---------- x, y : int Indices of central pixel. radius : int Half-width in both X and Y directions. data : array-like Data array to cut from. Returns ------- x0, y0 : int Origin of the region. arr : array-like Cut region (a view, not copy). """ n = radius ht, wd = data.shape x0, x1 = max(0, x - n), min(wd - 1, x + n) y0, y1 = max(0, y - n), min(ht - 1, y + n) arr = data[y0:y1 + 1, x0:x1 + 1] return (x0, y0, arr)
[docs] def cut_cross(self, x, y, radius, data): """Cut data vertically and horizontally at the given position with the given radius. Parameters ---------- x, y : int Indices where vertical and horizontal cuts meet. radius : float Radius of both cuts. data : array-like Data array to cut from. Returns ------- x0 : array-like Starting pixel of horizontal cut (in X). y0 : array-like Starting pixel of vertical cut (in Y). xarr : array-like Horizontal cut (in X). yarr : array-like Vertical cut (in Y). """ n = int(round(radius)) ht, wd = data.shape x, y = int(round(x)), int(round(y)) x0, x1 = int(max(0, x - n)), int(min(wd - 1, x + n)) y0, y1 = int(max(0, y - n)), int(min(ht - 1, y + n)) xarr = data[y, x0:x1 + 1] yarr = data[y0:y1 + 1, x] return (x0, y0, xarr, yarr)
[docs] def brightness(self, x, y, radius, medv, data): """Return the brightness value found in a region defined by input location and radius. Region is cut using :meth:`cut_region`. Parameters ---------- x, y : int Indices of central pixel. radius : int Half-width in both X and Y directions. medv : float Background to subtract off. data : array-like Data array. Returns ------- res : float Brightness. """ x0, y0, arr = self.cut_region(x, y, radius, data) arr2 = np.sort(arr.flat) idx = int(len(arr2) * 0.8) res = arr2[idx] - medv return float(res)
[docs] def fwhm_data(self, x, y, data, radius=15, method_name='gaussian'): """Equivalent to :meth:`get_fwhm`.""" return self.get_fwhm(x, y, radius, data, method_name=method_name)
# Encircled and ensquared energies (EE)
[docs] def ensquared_energy(self, data): """Return a function of ensquared energy across pixel indices. Ideally, data is already a masked array and is assumed to be centered. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") tot = data.sum() ny, nx = data.shape cen_x = int(nx // 2) cen_y = int(ny // 2) ee = [] if ny > nx: n_max = ny cen = cen_y else: n_max = nx cen = cen_x if n_max % 2 == 0: # Even delta_i1 = -1 else: # Odd delta_i1 = 0 xr = range(n_max - cen) for i in xr: ix1 = cen_x - i + delta_i1 if ix1 < 0: ix1 = 0 ix2 = cen_x + i + 1 if ix2 > nx: ix2 = nx iy1 = cen_y - i + delta_i1 if iy1 < 0: iy1 = 0 iy2 = cen_y + i + 1 if iy2 > ny: iy2 = ny ee.append(data[iy1:iy2, ix1:ix2].sum() / tot) return interp1d(xr, ee, kind='cubic', bounds_error=False, assume_sorted=True)
# This is adapted from poppy package. See licenses/POPPY_LICENSE.md .
[docs] def encircled_energy(self, data): """Return a function of encircled energy across pixel indices. Ideally, data is already a masked array and is assumed to be centered. """ if not have_scipy: raise IQCalcError("Please install the 'scipy' module " "to use this function") y, x = np.indices(data.shape, dtype=float) cen = tuple((i - 1) * 0.5 for i in data.shape[::-1]) x -= cen[0] y -= cen[1] r = np.sqrt(x * x + y * y) ind = np.argsort(r.flat) sorted_r = r.flat[ind] sorted_data = data.flat[ind] # data is already masked csim = sorted_data.cumsum(dtype=float) sorted_r_int = sorted_r.astype(int) deltar = sorted_r_int[1:] - sorted_r_int[:-1] # assume all radii represented rind = np.where(deltar)[0] ee = csim[rind] / sorted_data.sum() # Normalize if isinstance(ee, np.ma.MaskedArray): ee.set_fill_value(0) ee = ee.filled() return interp1d(range(ee.size), ee, kind='cubic', bounds_error=False, assume_sorted=True)
# EVALUATION ON A FIELD
[docs] def evaluate_peaks(self, peaks, data, fwhm_radius=15, fwhm_method='gaussian', ee_total_radius=10, cb_fn=None, ev_intr=None): """Evaluate photometry for given peaks in data array. Parameters ---------- peaks : list of tuple List of ``(x, y)`` tuples containing indices of peaks. data : array-like Data array that goes with the given peaks. fwhm_radius, fwhm_method See :meth:`get_fwhm`. ee_total_radius : float Radius, in pixels, where encircled and ensquared energy fractions are defined as 1. cb_fn : func or `None` If applicable, provide a callback function that takes a `ginga.misc.Bunch.Bunch` containing the result for each peak. It should not return anything. ev_intr : :py:class:`threading.Event` or `None` For threading, if applicable. .. note:: unused parameter `bright_radius` was removed in release 4.0 Returns ------- objlist : list of `ginga.misc.Bunch.Bunch` A list of successful results for the given peaks. Each result contains the following keys: * ``objx``, ``objy``: Fitted centroid from :meth:`get_fwhm`. * ``pos``: A measure of distance from the center of the image. * ``oid_x``, ``oid_y``: Center-of-mass centroid from :meth:`centroid`. * ``fwhm_x``, ``fwhm_y``: Fitted FWHM from :meth:`get_fwhm`. * ``fwhm``: Overall measure of fwhm as a single value. * ``fwhm_radius``: Input FWHM radius. * ``brightness``: Average peak value based on :meth:`get_fwhm` fits. * ``elipse``: A measure of ellipticity. * ``x``, ``y``: Input indices of the peak. * ``skylevel``: Sky level estimated from median of data array and ``skylevel_magnification`` and ``skylevel_offset`` attributes. * ``background``: Median of the input array. * ``ensquared_energy_fn``: Function of ensquared energy for different pixel radii. * ``encircled_energy_fn``: Function of encircled energy for different pixel radii. """ height, width = data.shape hh = float(height) / 2.0 ht = float(height) h4 = float(height) * 4.0 wh = float(width) / 2.0 wd = float(width) w4 = float(width) * 4.0 # Find the median (sky/background) level median = float(get_median(data)) #skylevel = median # Old SOSS qualsize() applied this calculation to skylevel skylevel = median * self.skylevel_magnification + self.skylevel_offset # Form a list of objects and their characteristics objlist = [] for x, y in peaks: if ev_intr and ev_intr.is_set(): raise IQCalcError("Evaluation interrupted!") # centroid calculation on local peak oid_x, oid_y = None, None try: oid_x, oid_y = self.centroid(data, x, y, fwhm_radius) except Exception as e: # Error doing centroid self.logger.debug("Error doing centroid on object at %.2f,%.2f: %s" % ( x, y, str(e))) # Find the fwhm in x and y, using local peak try: res = self.fwhm_data(x, y, data, radius=fwhm_radius, method_name=fwhm_method) fwhm_x, fwhm_y, ctr_x, ctr_y, x_res, y_res = res bx = x_res.fit_fn(round(ctr_x), (ctr_x,) + tuple(x_res.fit_args[1:])) by = y_res.fit_fn(round(ctr_y), (ctr_y,) + tuple(y_res.fit_args[1:])) bright = float((bx + by) / 2.0) except Exception as e: # Error doing FWHM, skip this object self.logger.debug("Error doing FWHM on object at %.2f,%.2f: %s" % ( x, y, str(e))) continue self.logger.debug("orig=%f,%f ctr=%f,%f fwhm=%f,%f bright=%f" % ( x, y, ctr_x, ctr_y, fwhm_x, fwhm_y, bright)) # overall measure of fwhm as a single value fwhm = (math.sqrt(fwhm_x * fwhm_x + fwhm_y * fwhm_y) * (1.0 / math.sqrt(2.0))) # calculate a measure of ellipticity elipse = math.fabs(min(fwhm_x, fwhm_y) / max(fwhm_x, fwhm_y)) # calculate a measure of distance from center of image dx = wh - ctr_x dy = hh - ctr_y dx2 = dx * dx / wd / w4 dy2 = dy * dy / ht / h4 if dx2 > dy2: pos = 1.0 - dx2 else: pos = 1.0 - dy2 # EE on background subtracted image ee_sq_fn = None ee_circ_fn = None iy1 = int(ctr_y - ee_total_radius) iy2 = int(ctr_y + ee_total_radius) + 1 ix1 = int(ctr_x - ee_total_radius) ix2 = int(ctr_x + ee_total_radius) + 1 if iy1 < 0 or iy2 > height or ix1 < 0 or ix2 > width: self.logger.debug("Error calculating EE on object at %.2f,%.2f: Box out of range with radius=%.2f" % (x, y, ee_total_radius)) else: ee_data = data[iy1:iy2, ix1:ix2] - median try: ee_sq_fn = self.ensquared_energy(ee_data) except Exception as e: self.logger.debug("Error calculating ensquared energy on object at %.2f,%.2f: %s" % (x, y, str(e))) try: ee_circ_fn = self.encircled_energy(ee_data) except Exception as e: self.logger.debug("Error calculating encircled energy on object at %.2f,%.2f: %s" % (x, y, str(e))) obj = Bunch.Bunch(objx=ctr_x, objy=ctr_y, pos=pos, oid_x=oid_x, oid_y=oid_y, fwhm_x=fwhm_x, fwhm_y=fwhm_y, fwhm=fwhm, fwhm_radius=fwhm_radius, brightness=bright, elipse=elipse, x=int(x), y=int(y), skylevel=skylevel, background=median, ensquared_energy_fn=ee_sq_fn, encircled_energy_fn=ee_circ_fn) objlist.append(obj) if cb_fn is not None: cb_fn(obj) return objlist
def _sortkey(self, obj): """For sorting of result in :meth:`objlist_select`.""" val = obj.brightness * obj.pos / math.sqrt(obj.fwhm) return val
[docs] def objlist_select(self, objlist, width, height, minfwhm=2.0, maxfwhm=150.0, minelipse=0.5, edgew=0.01): """Filter output from :meth:`evaluate_peaks`. Parameters ---------- objlist : list of `ginga.misc.Bunch.Bunch` Output from :meth:`evaluate_peaks`. width, height : int Dimension of data array from which ``objlist`` was derived. minfwhm, maxfwhm : float Limits for desired FWHM, where ``(minfwhm, maxfwhm)``. minelipse : float Minimum value of desired ellipticity (not inclusive). edgew : float Factor between 0 and 1 that determines if a location is too close to the edge or not. Returns ------- results : list of `ginga.misc.Bunch.Bunch` Elements of ``objlist`` that contain desired FWHM, ellipticity, and not too close to the edge. """ results = [] count = 0 for obj in objlist: count += 1 self.logger.debug("%d obj x,y=%.2f,%.2f fwhm=%.2f bright=%.2f" % ( count, obj.objx, obj.objy, obj.fwhm, obj.brightness)) # If peak has a minfwhm < fwhm < maxfwhm and the object # is inside the frame by edgew pct if ((minfwhm < obj.fwhm) and (obj.fwhm < maxfwhm) and (minelipse < obj.elipse) and (width * edgew < obj.x) and (height * edgew < obj.y) and (width * (1.0 - edgew) > obj.x) and (height * (1.0 - edgew) > obj.y)): results.append(obj) #results.sort(cmp=self._compare) results.sort(key=self._sortkey, reverse=True) return results
[docs] def pick_field(self, data, peak_radius=5, fwhm_radius=15, threshold=None, minfwhm=2.0, maxfwhm=50.0, minelipse=0.5, edgew=0.01, ee_total_radius=10): """Pick the first good object within the given field. Parameters ---------- data : array-like Data array of the field. peak_radius, threshold See :meth:`find_bright_peaks`. fwhm_radius, ee_total_radius See :meth:`evaluate_peaks`. minfwhm, maxfwhm, minelipse, edgew See :meth:`objlist_select`. .. note:: unused parameter `bright_radius` was removed in release 4.0 Returns ------- result : `ginga.misc.Bunch.Bunch` This is a single element of ``objlist`` as described in :meth:`evaluate_peaks`. Raises ------ IQCalcError No object matches selection criteria. """ height, width = data.shape # Find the bright peaks in the image peaks = self.find_bright_peaks(data, radius=peak_radius, threshold=threshold) self.logger.debug("peaks=%s" % str(peaks)) if len(peaks) == 0: raise IQCalcError("Cannot find bright peaks") # Evaluate those peaks objlist = self.evaluate_peaks(peaks, data, fwhm_radius=fwhm_radius, ee_total_radius=ee_total_radius) if len(objlist) == 0: raise IQCalcError("Error evaluating bright peaks") results = self.objlist_select(objlist, width, height, minfwhm=minfwhm, maxfwhm=maxfwhm, minelipse=minelipse, edgew=edgew) if len(results) == 0: raise IQCalcError("No object matches selection criteria") return results[0]
[docs] def qualsize(self, image, x1=None, y1=None, x2=None, y2=None, radius=5, fwhm_radius=15, threshold=None, minfwhm=2.0, maxfwhm=50.0, minelipse=0.5, edgew=0.01, ee_total_radius=10): """Run :meth:`pick_field` on the given image. Parameters ---------- image : `ginga.AstroImage.AstroImage` Image to process. x1, y1, x2, y2 : int See :meth:`ginga.BaseImage.BaseImage.cutout_data`. radius, threshold See :meth:`find_bright_peaks`. fwhm_radius, ee_total_radius See :meth:`evaluate_peaks`. minfwhm, maxfwhm, minelipse, edgew See :meth:`objlist_select`. .. note:: unused parameter `bright_radius` was removed in release 4.0 Returns ------- qs : `ginga.misc.Bunch.Bunch` This is a single element of ``objlist`` as described in :meth:`evaluate_peaks`. """ if x1 is None: x1 = 0 if y1 is None: y1 = 0 if x2 is None: x2 = image.width if y2 is None: y2 = image.height x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) data = image.cutout_data(x1, y1, x2, y2, astype=float) qs = self.pick_field(data, peak_radius=radius, fwhm_radius=fwhm_radius, threshold=threshold, minfwhm=minfwhm, maxfwhm=maxfwhm, minelipse=minelipse, edgew=edgew, ee_total_radius=ee_total_radius) # Add back in offsets into image to get correct values with respect # to the entire image qs.x += x1 qs.y += y1 qs.objx += x1 qs.objy += y1 self.logger.debug("obj=%f,%f fwhm=%f sky=%f bright=%f" % ( qs.objx, qs.objy, qs.fwhm, qs.skylevel, qs.brightness)) return qs
# END