Point Spread Function (PSF) models¶
This page covers PSF model construction and PSF fitting photometry. For object detection and aperture/optimal photometry see Object detection and measurement, and for photometric calibration see Photometric calibration.
STDPipe includes basic support for point spread function (PSF) construction and analysis through the interface to PSFEx code that is able to build supersampled PSF models from the object lists created using SExtractor. Please consider checking its documentation to better understand the concepts of PSFEx operation and its possible configuration options.
The stdpipe.psf.run_psfex() routine transparently calls the SExtractor on the image and then uses its output (it needs a special set of measured object parameters and postage stamps for them) to run PSFEx. The results are then parsed and returned as a structure representing the PSF model file as described here. This structure may later be used to visualize PSF shape at arbitrary position inside the image, as well as to generate artificial stars and inject them into the image.
- stdpipe.psf.run_psfex(image, mask=None, thresh=2.0, aper=None, r0=0.0, gain=1, minarea=5, vignet_size=None, psf_size=None, order=0, sex_extra=None, checkimages=None, extra=None, psffile=None, get_obj=False, _workdir=None, _tmpdir=None, _exe=None, _sex_exe=None, verbose=False)[source]
Wrapper around PSFEx to help extracting PSF models from images.
For the details of PSFEx operation we suggest to consult its documentation at https://psfex.readthedocs.io
- Parameters:
- imagenumpy.ndarray
Input image as a NumPy array.
- masknumpy.ndarray, optional
Image mask as a boolean array (True values will be masked).
- threshfloat, optional
Detection threshold in sigmas above local background, for running initial SExtractor object detection.
- aperfloat, optional
Circular aperture radius in pixels, to be used for PSF normalization. Should contain most of object flux. If not specified, will be estimated as twice the FWHM.
- r0float, optional
Smoothing kernel size (sigma) to be used for improving object detection in initial SExtractor call.
- gainfloat, optional
Image gain.
- minareaint, optional
Minimal number of pixels in the object to be considered a detection (
DETECT_MINAREAparameter of SExtractor).- vignet_sizeint, optional
The size of postage stamps to be used for PSF model creation.
- psf_sizeint, optional
The size of the supersampled PSF model.
- orderint, optional
Spatial order of PSF model variance.
- sex_extradict, optional
Dictionary of additional options to be passed to SExtractor for initial object detection (
extraparameter ofstdpipe.photometry.get_objects_sextractor()).- checkimageslist, optional
List of PSFEx checkimages to return along with PSF model.
- extradict, optional
Dictionary of extra configuration parameters to be passed to PSFEx call, with keys as parameter names. See
psfex -ddfor the full list.- psffilestr, optional
If specified, PSF model file will also be stored under this file name, so that it may e.g. be re-used by SExtractor later.
- get_objbool, optional
If set, also return the table with SExtractor detected objects.
- _workdirstr, optional
If specified, all temporary files will be created in this directory, and will be kept intact after running SExtractor and PSFEx. May be used for debugging exact inputs and outputs of the executable.
- _tmpdirstr, optional
If specified, all temporary files will be created in a dedicated directory (that will be deleted after running the executable) inside this path.
- _exestr, optional
Full path to PSFEx executable. If not provided, the code tries to locate it automatically in your
PATH.- _sex_exestr, optional
Full path to SExtractor executable. If not provided, the code tries to locate it automatically in your
PATH.- verbosebool or callable, optional
Whether to show verbose messages during the run of the function or not.
- Returns:
- dict
PSF structure corresponding to the built PSFEx model.
The structure has at least the following fields:
width,height- dimensions of supersampled PSF stampfwhm- mean full width at half maximum (FWHM) of the images used for building the PSF modelsampling- conversion factor between PSF stamp (supersampled) pixel size, and original image one (less than unity when supersampled resolution is finer than original image one)ncoeffs- number of coefficients pixel polynomials havedegree- polynomial degree of a spatial variance of PSF modeldata- the data containing per-pixel polynomial coefficients for PSF modelheader- original FITS header of PSF model file, ifget_header=Trueparameter was set
This structure corresponds to the contents of original PSFEx generated output file that is documented at https://psfex.readthedocs.io/en/latest/Appendices.html#psf-file-format-description
Attention
While PSFEx may build PSF models depending on a variety of object parameters as described here, STDPipe currently allows reading and interpreting only the ones depending on the position on the image (e.g. representing optical distortions). On the other hand, the original PSFEx model file may be stored to the file using psffile parameter of stdpipe.psf.run_psfex(), and then supplied directly to SExtractor through psf parameter of stdpipe.photometry.get_objects_sextractor() to be used for SExtractor PSF photometry.
Using PSF models¶
The PSF model built by PSFEx may also be loaded from PSFEx model file. This model may then be used to construct PSF stamps both in supersampled PSF model pixels, or original image pixels.
- stdpipe.psf.load_psf(filename, get_header=False, verbose=False)[source]
Load PSF model from PSFEx file
The structure may be useful for inspection of PSF model with
stdpipe.psf.get_supersampled_psf_stamp()andstdpipe.psf.get_psf_stamp(), as well as for injection of PSF instances (fake objects) into the image withstdpipe.psf.place_psf_stamp().- Parameters:
- filenamestr
Name of a file containing PSF model built by PSFEx.
- get_headerbool, optional
Whether to return the original FITS header of PSF model file or not. If set, the header will be stored in the
headerfield of the returned structure.- verbosebool or callable, optional
Whether to show verbose messages during the run of the function or not.
- Returns:
- dict
PSF structure in the same format as returned from
stdpipe.psf.run_psfex().
- stdpipe.psf.get_supersampled_psf_stamp(psf, x=0, y=0, normalize=True)[source]
Returns supersampled PSF model for a given position inside the image.
The returned stamp corresponds to PSF model evaluated at a given position inside the image, with its center always in the center of central stamp pixel. Every supersampled pixel of the stamp corresponds to
psf['sampling']pixels of the original image.- Parameters:
- psfdict
Input PSF structure as returned by
stdpipe.psf.run_psfex()orstdpipe.psf.load_psf().- xfloat, optional
xcoordinate of the position inside the original image to evaluate the PSF model.- yfloat, optional
ycoordinate of the position inside the original image to evaluate the PSF model.- normalizebool, optional
Whether to normalize the stamp to have flux exactly equal to unity or not.
- Returns:
- numpy.ndarray
Stamp of the PSF model evaluated at the given position inside the image.
- stdpipe.psf.get_psf_stamp(psf, x=0, y=0, dx=None, dy=None, normalize=True)[source]
Returns PSF stamp in original image pixel space with sub-pixel shift applied.
The PSF model is evaluated at the requested position inside the original image, and then downscaled from supersampled pixels of the PSF model to original image pixels, and then adjusted to accommodate for requested
(dx, dy)sub-pixel shift.Stamp is odd-sized, with PSF center at:
x0 = floor(width/2) + dx y0 = floor(height/2) + dy
If
dx=Noneordy=None, they are computed directly from the floating point parts of the positionxandyarguments:dx = x - round(x) dy = y - round(y)
The stamp should directly represent stellar shape at a given position (including sub-pixel center shift) inside the image.
- Parameters:
- psfdict
Input PSF structure as returned by
stdpipe.psf.run_psfex()orstdpipe.psf.load_psf().- xfloat, optional
xcoordinate of the position inside the original image to evaluate the PSF model.- yfloat, optional
ycoordinate of the position inside the original image to evaluate the PSF model.- dxfloat, optional
Sub-pixel adjustment of PSF position in image space,
xdirection.- dyfloat, optional
Sub-pixel adjustment of PSF position in image space,
ydirection.- normalizebool, optional
Whether to normalize the stamp to have flux exactly equal to unity or not.
- Returns:
- numpy.ndarray
Stamp of the PSF model evaluated at the given position inside the image, in original image pixels.
PSF model utilities¶
Two small helpers complement the stamp-construction routines above:
enclosed_psf_fraction()– fraction of normalised PSF flux inside a circular aperture at an arbitrary image position. Vectorised overradius(scalar in → scalar out, array in → array out). Useful for on-the-fly aperture corrections.select_psf_seeds()– pick bright, edge-clear sources distributed uniformly across the field for use as PSF seeds. Bins on a regulargrid_x × grid_yspatial grid and keeps themax_per_tilebrightest per cell. Configurable column names so it works directly on SExtractor catalogues (X_IMAGE/FLUX_AUTO).
from stdpipe import psf
# Aperture correction at one source's position, as a function of radius
radii = [3.0, 5.0, 7.0, 10.0]
frac = psf.enclosed_psf_fraction(psf_model, x=512.0, y=512.0, radius=radii)
print('aperture corrections:', dict(zip(radii, frac)))
# Build a uniform PSF-seed sample for create_psf_model() / run_psfex()
seeds = psf.select_psf_seeds(
obj, image_shape=image.shape,
max_per_tile=20, grid=(8, 6), edge=20,
accept_flags=0,
)
- stdpipe.psf.enclosed_psf_fraction(psf, x=0, y=0, radius=None, subpixel=10)[source]
Fraction of normalised PSF flux inside a circular aperture.
Evaluates the (possibly position-dependent) PSF model at
(x, y)viaget_psf_stamp(), normalises it to unit total flux, and sums the pixels lying withinradiusof the stamp centre. Useful for on-the-fly aperture corrections.Pixels are weighted by the fractional coverage of the circular aperture using a sub-pixel grid (
subpixel×subpixelsamples per pixel) — setsubpixel=1to fall back to a hard pixel-centre mask, which is faster but has up to ~10 % radius-dependent jitter when the aperture edge cuts through individual pixels.- Parameters:
- psfdict
PSF model from
run_psfex(),load_psf(), orcreate_psf_model().- x, yfloat, optional
Position in image pixels at which to evaluate a position-dependent PSF model. The sub-pixel parts of
xandyare folded into the stamp centre viaget_psf_stamp().- radiusfloat or array-like
Aperture radius (or radii) in image pixels.
- subpixelint, optional
Linear sub-pixel sampling per image pixel for partial-coverage weighting. Default 10 (100 sub-samples per pixel).
- Returns:
- float or ndarray
Enclosed flux fraction at each radius. Scalar when
radiusis a scalar, otherwise an array of the same shape.
- stdpipe.psf.select_psf_seeds(obj, image_shape, *, max_per_tile=25, grid=6, edge=20, obj_col_x='x', obj_col_y='y', obj_col_flux='flux', obj_col_flags='flags', accept_flags=0)[source]
Select bright, edge-clear sources spread uniformly across the field.
Bins
objon a regulargrid_x × grid_yspatial grid and returns themax_per_tilebrightest sources per cell that pass basic quality filters: finitex/y/flux, positive flux, optionally a flag mask, and inside the image after anedge-pixel margin.The returned table is a strict subset of
obj(same columns, sub-selected rows) and is the natural input forstdpipe.psf.create_psf_model()and similar PSF-modelling routines that benefit from a uniform spatial sampling.- Parameters:
- objastropy.table.Table
Source catalogue.
- image_shape(H, W) tuple
Image shape used for the spatial grid and edge mask.
- max_per_tileint
Maximum number of seeds kept per spatial cell.
- gridint or (nx, ny) tuple
Number of cells in the spatial grid. A scalar means a square
grid × gridlayout.- edgefloat
Edge margin in pixels; sources within
edgeof any image boundary are excluded.- obj_col_x, obj_col_ystr
Column names for source positions in pixel coordinates.
- obj_col_fluxstr
Column name used to rank candidates within each cell (brightest first). Sources with non-positive values are dropped.
- obj_col_flagsstr, optional
Column name for a SExtractor-style integer flag mask. Set to
Noneto skip flag filtering. If the column is missing fromobj, no flag filter is applied either.- accept_flagsint
Bitwise mask of acceptable flags. A source is kept only when
flags & ~accept_flags == 0.
- Returns:
- astropy.table.Table
A subset of
objcontaining the selected seeds.
Placing artificial stars into the image¶
STDPipe also contains a couple of convenience functions to directly place a PSF model at a given image position - that would correspond to artificial point source injection into the image. Higher-level one, stdpipe.pipeline.place_random_stars(), inserts a number of randomly generated sources with realistically (log-uniform in fluxes) distributed brightness at random positions, as well as returns the catalogue of injected stars that may be used e.g. in deriving the object detection efficiency.
# Derive PSF model assuming that it does not change over the image
psf_model = psf.run_psfex(image, mask=mask, order=0, verbose=True)
# ..and now perform the detection efficiency analysis
sims = [] # will hold simulated stars
# We will repeatedly inject the stars (20 at a time), detect objects, and
# match them in order to see whether simulated stars are detectable
for _ in tqdm(range(100)):
# We will operate on a copy of original image
image1 = image.copy()
# We will use this value as a saturation level
saturation = 50000
# Simulate 20 random stars
sim = pipeline.place_random_stars(image1, psf_model, nstars=20, minflux=10, maxflux=1e6,
wcs=wcs, gain=gain, saturation=saturation)
# Initial metadata for injected stars
sim['mag_calib'] = sim['mag'] + zero_fn(sim['x'], sim['y']) # Apply zero point from photometric calibration
sim['detected'] = False
sim['mag_measured'] = np.nan
sim['magerr_measured'] = np.nan
sim['flags_measured'] = np.nan
# Mask corresponding to the saturation level
mask1 = image1 >= saturation
obj1 = photometry.get_objects_sextractor(image1, mask=mask|mask1, r0=1, aper=5.0,
wcs=wcs, gain=gain, minarea=3, sn=5)
# Apply zero point from photometric calibration
obj1['mag_calib'] = obj1['mag'] + zero_fn(obj1['x'], obj1['y'])
# Positional match within FWHM/2 radius
oidx,sidx,dist = astrometry.spherical_match(obj1['ra'], obj1['dec'], sim['ra'], sim['dec'],
pixscale*np.median(obj1['fwhm'])/2)
# Mark matched stars
sim['detected'][sidx] = True
# Also store measured magnitude, its error and flags
sim['mag_measured'][sidx] = obj1['mag_calib'][oidx]
sim['magerr_measured'][sidx] = obj1['magerr'][oidx]
sim['flags_measured'][sidx] = obj1['flags'][oidx]
sims.append(sim)
# Now we may stack the data from all runs into a single table
from astropy.table import vstack
sims = vstack(sims)
# ..and plot it as a histograms of magnitudes
h0,b0,_ = plt.hist(sims['mag_calib'], range=[12,22], bins=50, alpha=0.3, label='All simulated stars');
h1,b1,_ = plt.hist(sims['mag_calib'][sims['detected']], range=[12,22], bins=50, alpha=0.3, label='Detected');
h2,b2,_ = plt.hist(sims['mag_calib'][idx], range=[12,22], bins=50, alpha=0.3, label='Detected and unflagged');
plt.legend()
plt.xlabel('Injected magnitude')
plt.show()
# ..or as a detection efficiency
plt.plot(0.5*(b0[1:]+b0[:-1]), h1/h0, 'o-', label='Detected')
plt.plot(0.5*(b0[1:]+b0[:-1]), h2/h0, 'o-', label='Detected and unflagged')
plt.legend()
plt.xlabel('Injected magnitude')
plt.title('Fraction of detected artificial stars')
The complete example of detection efficiency analysis may be seen in the corresponding notebook.
Attention
Note that the flux that you specify for the artificial star is a total flux - it will not necessarily correspond to the one measured inside an aperture (especially if the aperture size is quite small). The difference (aperture correction) is due to the fraction of the total flux that falls outside of your aperture, and it has to be somehow estimated and corrected if you wish to compare the generated fluxes of injected stars with measured values.
- stdpipe.psf.place_psf_stamp(image, psf, x0, y0, flux=1, gain=None)[source]
Places PSF stamp, scaled to a given flux, at a given position inside the image.
PSF stamp is evaluated at a given position, then adjusted to accommodate for required sub-pixel shift, and finally scaled to requested flux value. Thus, the routine corresponds to injection of an artificial point source into the image.
The stamp values are added on top of current content of the image. If
gainvalue is set, the Poissonian noise is applied to the stamp.The image is modified in-place.
- Parameters:
- imagenumpy.ndarray
The image where the artificial source will be injected (modified in-place).
- psfdict
Input PSF structure as returned by
stdpipe.psf.run_psfex()orstdpipe.psf.load_psf().- x0float
xcoordinate of the position to inject the source.- y0float
ycoordinate of the position to inject the source.- fluxfloat, optional
The source flux in ADU units.
- gainfloat, optional
Image gain value. If set, used to apply Poissonian noise to the source.
- stdpipe.pipeline.place_random_stars(image, psf_model, nstars=100, minflux=1, maxflux=100000, gain=None, saturation=65535, edge=0, wcs=None, verbose=False)[source]
Randomly place artificial stars into an image in-place.
Stars are added on top of the existing image content. Poissonian noise is applied according to
gain, and pixel values are clipped atsaturation. Coordinates are uniformly distributed; fluxes are log-uniform.- Parameters:
- imagendarray
2D image array; modified in-place.
- psf_modeldict
PSF model structure as returned by
stdpipe.psf.run_psfex().- nstarsint, optional
Number of artificial stars to inject.
- minfluxfloat, optional
Minimum star flux in ADU.
- maxfluxfloat, optional
Maximum star flux in ADU.
- gainfloat, optional
Detector gain in e-/ADU; used to add Poissonian noise to each injected star.
- saturationfloat, optional
Saturation level in ADU; injected pixels above this are clipped.
- edgeint, optional
Minimum distance to image edges for star placement.
- wcsastropy.wcs.WCS, optional
If provided, sky coordinates
raanddecare added to the catalogue.- verbosebool or callable, optional
Whether to show verbose messages. May be boolean or a
print-like callable.
- Returns:
- astropy.table.Table
Catalogue of injected stars as returned by
make_random_stars(), with columnsx,y,flux,mag, and optionallyra,dec.
PSF photometry with photutils¶
STDPipe provides PSF fitting photometry using the photutils library as an alternative to SExtractor. This approach is pure Python, more flexible, and works with various PSF models including PSFEx, Gaussian, and empirical ePSF.
The stdpipe.photometry_psf.measure_objects_psf() function performs PSF photometry
at the positions of already detected objects. It supports:
Multiple PSF model types (PSFEx, Gaussian, empirical ePSF)
Position-dependent PSF for wide-field images
Grouped fitting for crowded fields
Comprehensive quality metrics
from stdpipe import photometry, photometry_psf, psf
# Detect objects
obj = photometry.get_objects_sep(image, mask=mask, thresh=3.0)
# Build PSF model
psf_model = psf.run_psfex(image, mask=mask, order=1, verbose=True)
# Perform PSF photometry
obj_psf = photometry_psf.measure_objects_psf(
obj, image,
psf=psf_model,
mask=mask,
verbose=True
)
The output table includes fitted positions (x_psf, y_psf), fluxes (flux, fluxerr),
magnitudes (mag, magerr), and quality metrics:
qfit_psf- Fit quality (0 = good, higher values indicate poor fits)cfit_psf- Central pixel fit qualityflags_psf- Photutils fit flagsnpix_psf- Number of unmasked pixels used in fitreduced_chi2_psf- Reduced chi-squared (photutils >= 2.3.0)
- stdpipe.photometry_psf.measure_objects_psf(obj, image, psf=None, psf_size=None, fwhm=None, mask=None, bg=None, err=None, gain=None, bg_size=64, bkgann=None, bkg_order=1, sn=None, fit_shape='circular', fit_size=None, maxiters=3, recentroid=True, keep_negative=True, get_bg=False, use_position_dependent_psf=False, group_sources=True, grouper_radius=None, verbose=False)[source]
PSF photometry at the positions of already detected objects using photutils.
Performs PSF fitting photometry which is more accurate than aperture photometry, especially for point sources in crowded fields or when accurate flux measurement of PSF wings is important.
This function will estimate and subtract the background unless external background estimation (
bg) is provided, and use user-provided noise map (err) if requested.If a PSF model is not provided, a simple Gaussian PSF will be constructed based on the
fwhmparameter or estimated from the data.- Parameters:
- obj
~astropy.table.Table Table with initial object detections to be measured. Must have ‘x’ and ‘y’ columns.
- image
~numpy.ndarray Input image as a 2D NumPy array.
- psfphotutils PSF model, dict, or None, optional
PSF model to use. Can be a photutils PSF model (e.g., IntegratedGaussianPRF, FittableImageModel), a PSFEx PSF structure from
stdpipe.psf.run_psfex(), or None (will create Gaussian PSF based on fwhm).- psf_sizeint or None, optional
Size of the PSF model in pixels. If None, will be estimated from PSF or set to 5*fwhm.
- fwhmfloat or None, optional
Full width at half maximum in pixels. Used if PSF model is not provided, or to estimate psf_size. If None, will be estimated from obj[‘fwhm’] if available.
- mask
~numpy.ndarrayor None, optional Image mask as a boolean array (True values will be masked).
- bg
~numpy.ndarrayor None, optional If provided, use this background (same shape as input image) instead of automatically computed one.
- err
~numpy.ndarrayor None, optional Image noise map to be used instead of automatically computed one.
- gainfloat or None, optional
Image gain in e-/ADU, used to build image noise model.
- bg_sizeint, optional
Background grid size in pixels.
- bkgannlist of float or None, optional
Background annulus for local background estimation, [inner_radius, outer_radius] in pixels. If None, no local background subtraction is performed (relies only on global Background2D subtraction). If set, uses gradient-aware local background fitting to handle non-uniform backgrounds. Note: radii are NOT scaled by FWHM (unlike measure_objects).
- bkg_orderint, optional
Polynomial order for local background fitting. 0 = constant (mean), 1 = plane (linear gradient, recommended), 2 = quadratic surface. Only used if bkgann is set.
- snfloat or None, optional
Minimal S/N ratio for the object to be considered good. If set, all measurements with magnitude errors exceeding 1/sn will be discarded.
- fit_shapestr, optional
Shape of fitting region. Options: ‘circular’ (default), ‘square’. Determines the aperture used for PSF fitting.
- fit_sizeint or None, optional
Size of fitting region in pixels. If None, defaults to psf_size.
- maxitersint, optional
Maximum number of iterations for PSF fitting.
- recentroidbool, optional
If True, allow PSF position to vary during fitting (recommended).
- keep_negativebool, optional
If False, measurements with negative fluxes will be discarded.
- get_bgbool, optional
If True, the routine will also return estimated background and background noise images.
- use_position_dependent_psfbool, optional
If True and PSF is a PSFEx model, use polynomial evaluation for position-dependent PSF (evaluates PSF at each source position).
- group_sourcesbool, optional
If True, use grouped PSF fitting for overlapping sources. Fits nearby sources simultaneously for better accuracy in crowded fields.
- grouper_radiusfloat or None, optional
Radius in pixels for grouping nearby sources. If None, defaults to 2*psf_size. Only used if group_sources is True.
- verbosebool or callable, optional
Whether to show verbose messages during the run. May be either boolean, or a
print-like function.
- obj
- Returns:
- result
~astropy.table.Tableor tuple Copy of original table with
flux,fluxerr,mag,magerr,x_psf,y_psfcolumns from PSF fitting. Also includes quality of fit columns:qfit_psf(fit quality, 0=good),cfit_psf(central pixel fit quality),flags_psf(photutils fit flags),npix_psf(number of unmasked pixels used in fit), andreduced_chi2_psf(reduced chi-squared, available in photutils >= 2.3.0). Ifget_bgis True, returns a tuple of (table, background, background_error).
- result
Position-dependent PSF¶
For wide-field images where the PSF varies across the field, enable position-dependent evaluation of PSFEx polynomial models:
# Build PSF model with spatial variation (order > 0)
psf_model = psf.run_psfex(image, mask=mask, order=2, verbose=True)
# Use position-dependent PSF
obj_psf = photometry_psf.measure_objects_psf(
obj, image,
psf=psf_model,
use_position_dependent_psf=True, # Evaluate PSF at each position
verbose=True
)
This evaluates the PSFEx polynomial (constant, linear, quadratic terms) at each source position for more accurate photometry in fields with PSF gradients.
Grouped PSF fitting for crowded fields¶
In crowded fields with overlapping sources, grouped fitting simultaneously fits nearby sources to reduce contamination:
obj_psf = photometry_psf.measure_objects_psf(
obj, image,
psf=psf_model,
group_sources=True, # Enable grouped fitting
grouper_radius=10.0, # Group sources within 10 pixels
verbose=True
)
# Check grouping results
print(f"Grouped {len(np.unique(obj_psf['group_id']))} groups")
This is slower but more accurate when sources are close together.
Building empirical PSF models¶
When PSFEx is not available or you want a purely empirical PSF, use
stdpipe.psf.create_psf_model() to build an ePSF from stars in your image:
from stdpipe import psf
# Create empirical PSF from stars in the image
epsf_model = psf.create_psf_model(
image,
mask=mask,
fwhm=3.0, # Approximate FWHM
size=25, # Cutout size
oversampling=2, # ePSF oversampling factor
verbose=True
)
# Use it just like a PSFEx model
obj_psf = photometry_psf.measure_objects_psf(
obj, image,
psf=epsf_model,
verbose=True
)
The ePSF model is built by combining postage stamps of isolated stars and returns a PSFEx-compatible dictionary structure that works with all PSF functions.
- stdpipe.psf.create_psf_model(image, obj=None, fwhm=None, size=None, mask=None, oversampling=None, degree=0, regularization=1e-06, subtract_neighbors=True, subtract_background=False, isolation=2.0, get_raw=False, verbose=False)[source]
Create an empirical PSF (ePSF) model from stars in the image.
For
degree=0(default), builds a position-invariant ePSF using photutilsEPSFBuilder(iterative recentering and stacking).For
degree > 0, builds a position-dependent PSF model by fitting per-pixel polynomial coefficients to resampled star stamps, following the same approach as PSFEx. The polynomial model is:\[PSF(i,j; x,y) = \sum_k c_k(i,j) \cdot dx^{p1_k} \cdot dy^{p2_k}\]where
(dx, dy)are normalized image coordinates and(p1_k, p2_k)are polynomial exponents withp1_k + p2_k <= degree.The returned dictionary structure is compatible with PSFEx output from
stdpipe.psf.run_psfex()and can be used with the same evaluation functions likestdpipe.psf.get_psf_stamp().- Parameters:
- imagenumpy.ndarray
Input image as a NumPy array, must be background subtracted.
- objastropy.table.Table, optional
Table of star positions. If None, stars will be detected automatically. Should have ‘x’, ‘y’ columns and optionally ‘flux’.
- fwhmfloat, optional
Approximate FWHM of stars in pixels. If None, will be estimated.
- sizeint, optional
Size of cutouts to extract around stars (should be odd). If None, automatically determined from FWHM as
max(15, round_up_to_odd(5 * fwhm)).- masknumpy.ndarray, optional
Image mask as a boolean array (True values will be masked).
- oversamplingint, optional
Oversampling factor for the ePSF. If None (default), it is auto-selected from the FWHM:
1whenfwhm >= 2.5image pixels (well-sampled PSF) and2otherwise (under-sampled PSF). Pass an explicit integer to override.- degreeint, optional
Polynomial degree for spatial PSF variation (default: 0 = constant). Degree 1 = linear (3 coefficients), degree 2 = quadratic (6 coefficients), etc.
- regularizationfloat, optional
Tikhonov regularization parameter for polynomial fitting (default: 1e-6). Only used when
degree > 0. Set to 0 for unregularized least-squares.- subtract_neighborsbool, optional
If True (default), subtract estimated flux from neighboring stars before extracting cutouts. Reduces contamination in crowded fields. Only used when
degree > 0.- subtract_backgroundbool or {‘none’, ‘median’, ‘plane’}, optional
Local background handling for each training stamp before normalization.
Falseor'none'leaves the current image values unchanged,Trueor'median'subtracts the median of the stamp border, and'plane'fits and subtracts a tilted background plane from the border pixels. Only used whendegree > 0. When building from the raw image instead of a background-subtracted one,'plane'is usually the most robust choice.- isolationfloat, optional
Minimum nearest-neighbor distance in FWHM units for selecting stars for ePSF building (default: 2.0). Stars with a neighbor closer than
isolation * fwhmare excluded. Combined withsubtract_neighbors=True, the lower default value works in both sparse and dense fields. Set to 0 or None to disable isolation filtering.- get_rawbool, optional
If True and
degree=0, returns raw photutils EPSFModel object. Ignored whendegree > 0.- verbosebool or callable, optional
Whether to show verbose messages.
- Returns:
- dict
Dictionary with PSFEx-compatible structure containing the PSF model.
PSF photometry with DAOPHOT (IRAF)¶
As an alternative to the photutils backend, STDPipe also provides PSF photometry through
the classic IRAF DAOPHOT workflow (phot → psf → allstar) via stdpipe.photometry_iraf.measure_objects_psf().
This requires PyRAF and IRAF to be installed.
DAOPHOT automatically selects bright, isolated stars to build a PSF model, then fits all
sources using the allstar task. It supports multiple analytical PSF functions (Gaussian,
Moffat, Lorentzian, Penny) and spatially variable PSF models.
from stdpipe import photometry_iraf
# Basic DAOPHOT PSF photometry (automatic PSF star selection)
result = photometry_iraf.measure_objects_psf(obj, image, fwhm=fwhm, gain=gain)
# Custom PSF function and fitting parameters
result = photometry_iraf.measure_objects_psf(obj, image, fwhm=fwhm, gain=gain,
psf_function='moffat25', psfrad=12.0, fitrad=4.0, sn=5)
# Provide specific PSF stars
bright = np.argsort(obj['flux'])[-20:]
result = photometry_iraf.measure_objects_psf(obj, image, fwhm=fwhm,
psf_stars=bright, verbose=True)
- stdpipe.photometry_iraf.measure_objects_psf(obj, image, fwhm=None, psf_stars=None, n_psf_stars=20, mask=None, bg=None, err=None, gain=None, bg_size=64, sn=None, psfrad=None, fitrad=None, psf_function='auto', psf_varorder=0, keep_negative=True, get_bg=False, _workdir=None, _tmpdir=None, verbose=False)[source]
PSF photometry using IRAF DAOPHOT (psf + allstar tasks).
This function performs PSF photometry using the full DAOPHOT workflow: 1. Select PSF stars (or use provided psf_stars) 2. Build PSF model using DAOPHOT psf task 3. Fit all sources using DAOPHOT allstar task 4. Return results in stdpipe format with quality metrics
The PSF model is built automatically from bright, isolated stars in the image. If psf_stars is provided, those specific stars will be used instead of automatic selection.
Note: Requires PyRAF and IRAF to be installed and properly configured.
- Parameters:
- objastropy.table.Table
Initial object detections. Must have ‘x’ and ‘y’ columns. Should also have ‘flux’ or ‘mag’ for PSF star selection.
- imagenumpy.ndarray
Input image as a 2D NumPy array.
- fwhmfloat
FWHM in pixels (required for PSF building and fitting).
- psf_starsarray-like or None
Indices of objects to use as PSF stars. If None, stars are selected automatically.
- n_psf_starsint
Number of PSF stars to select automatically.
- masknumpy.ndarray or None
Boolean image mask (True values will be masked).
- bgnumpy.ndarray or None
Background image to subtract instead of automatically computed one.
- errnumpy.ndarray or None
Image noise map.
- gainfloat or None
Image gain in e/ADU.
- bg_sizeint
Background grid size in pixels.
- snfloat or None
Minimal S/N ratio for output filtering.
- psfradfloat or None
PSF radius in pixels (default: 3*fwhm). Sets how far the PSF extends.
- fitradfloat or None
Fitting radius in pixels (default: fwhm). Sets how many pixels to use for fitting each star.
- psf_functionstr
PSF function type: ‘auto’, ‘gauss’, ‘moffat15’, ‘moffat25’, ‘lorentz’, ‘penny1’, or ‘penny2’.
- psf_varorderint
PSF variability order: 0=constant, 1=linear, 2=quadratic.
- keep_negativebool
If False, discard measurements with negative fluxes.
- get_bgbool
If True, also return background and error images.
- _workdirstr or None
Working directory for temporary files.
- _tmpdirstr or None
Parent directory for temporary directory creation.
- verbosebool or callable
Verbose output.
- Returns:
- astropy.table.Table
Table with PSF photometry results including flux, mag, x_psf, y_psf, qfit_psf, cfit_psf, flags_psf columns. If
get_bg=True, returns a tuple of (table, background, background_error).
- Raises:
- ImportError
If PyRAF is not available.
- ValueError
If required parameters are missing.
- RuntimeError
If DAOPHOT tasks fail.
PSF photometry in SExtractor (alternative)¶
As an alternative to the photutils-based approach described above, derived PSF models may also be used for performing PSF photometry in SExtractor.
As currently STDPipe lacks the routine for saving PSF models back to FITS files, in order to use PSF photometry you will need to pass psffile argument to stdpipe.psf.run_psfex() in order to directly store the produced PSF model into the file. Then, this file may be used in SExtractor by passing its path as a psf argument into stdpipe.photometry.get_objects_sextractor(). It will then produce the list of detected objects having a set of additional measured parameters including flux_psf, fluxerr_psf, mag_psf, magerr_psf - they correspond to the flux derived from fitting the objects with the PSF model.
# We probably do not have enough stars to study PSF spatial variance, so we use order=0 here
psf_model = psf.run_psfex(image, mask=mask, order=0, gain=gain, psffile='/tmp/psf.psf', verbose=True)
obj_psf = photometry.get_objects_sextractor(image, mask=mask, aper=3.0, edge=10, gain=gain,
wcs=wcs, psf='/tmp/psf.psf', verbose=False)
# Now we may calibrate the photometry using PSF fluxes
m_psf = pipeline.calibrate_photometry(obj_psf, cat, sr=1/3600,
obj_col_mag='mag_psf', obj_col_mag_err='magerr_psf',
cat_col_mag='rmag', cat_col_mag1='gmag', cat_col_mag2='rmag',
order=0, verbose=True)