Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ repos:
jwst/saturation/.* |
jwst/scripts/.* |
jwst/skymatch/.* |
jwst/source_catalog/.* |
jwst/spectral_leak/.* |
jwst/srctype/.* |
jwst/stpipe/.* |
Expand Down
4 changes: 2 additions & 2 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ exclude = [
"jwst/saturation/**.py",
"jwst/scripts/**.py",
"jwst/skymatch/**.py",
"jwst/source_catalog/**.py",
# "jwst/source_catalog/**.py",
"jwst/spectral_leak/**.py",
"jwst/srctype/**.py",
"jwst/stpipe/**.py",
Expand Down Expand Up @@ -206,7 +206,7 @@ ignore-fully-untyped = true # Turn of annotation checking for fully untyped cod
"jwst/saturation/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/scripts/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/skymatch/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/source_catalog/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
# "jwst/source_catalog/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/spectral_leak/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/srctype/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
"jwst/stpipe/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
Expand Down
4 changes: 3 additions & 1 deletion jwst/source_catalog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Build a catalog of sources detected in an image with photometry."""

from .source_catalog_step import SourceCatalogStep

__all__ = ['SourceCatalogStep']
__all__ = ["SourceCatalogStep"]
9 changes: 2 additions & 7 deletions jwst/source_catalog/_wcs_helpers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
# (taken from photutils: should probably migrate into astropy.wcs)
"""
This module provides WCS helper tools.
"""
"""Provide WCS helper tools to source_catalog."""

import astropy.units as u
import numpy as np


def pixel_scale_angle_at_skycoord(skycoord, wcs, offset=1 * u.arcsec):
"""
Calculate the pixel coordinate and scale and WCS rotation angle at
the position of a SkyCoord coordinate.
Calculate the pixel coordinate, scale, and WCS rotation angle at a SkyCoord coordinate.

Parameters
----------
Expand Down
200 changes: 108 additions & 92 deletions jwst/source_catalog/detection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Module to detect sources using image segmentation.
"""
"""Module to detect sources using image segmentation."""

import logging
import warnings
Expand All @@ -19,38 +17,30 @@


class JWSTBackground:
"""
Class to estimate a 2D background and background RMS noise in an
image.

Parameters
----------
data : 2D `~numpy.ndarray`
The input 2D array.

box_size : int or array_like (int)
The box size along each axis. If ``box_size`` is a scalar then
a square box of size ``box_size`` will be used. If ``box_size``
has two elements, they should be in ``(ny, nx)`` order.

coverage_mask : array_like (bool), optional
A boolean mask, with the same shape as ``data``, where a `True`
value indicates the corresponding element of ``data`` is masked.
Masked data are excluded from calculations. ``coverage_mask``
should be `True` where there is no coverage (i.e., no data) for
a given pixel (e.g., blank areas in a mosaic image). It should
not be used for bad pixels.

Attributes
----------
background : 2D `~numpy.ndimage`
The estimated 2D background image.

background_rms : 2D `~numpy.ndimage`
The estimated 2D background RMS image.
"""
"""Class to estimate a 2D background and background RMS noise in an image."""

def __init__(self, data, box_size=100, coverage_mask=None):
"""
Initialize the class.

Parameters
----------
data : `~numpy.ndarray`
The input 2D image for which to estimate the background.

box_size : int or array_like (int)
The box size along each axis. If ``box_size`` is a scalar then
a square box of size ``box_size`` will be used. If ``box_size``
has two elements, they should be in ``(ny, nx)`` order.

coverage_mask : array_like (bool), optional
A boolean mask, with the same shape as ``data``, where a `True`
value indicates the corresponding element of ``data`` is masked.
Masked data are excluded from calculations. ``coverage_mask``
should be `True` where there is no coverage (i.e., no data) for
a given pixel (e.g., blank areas in a mosaic image). It should
not be used for bad pixels.
"""
self.data = data
self.box_size = np.asarray(box_size).astype(int) # must be integer
self.coverage_mask = coverage_mask
Expand All @@ -66,53 +56,71 @@
A Background2D object containing the 2D background and
background RMS noise estimates.
"""
sigma_clip = SigmaClip(sigma=3.)
sigma_clip = SigmaClip(sigma=3.0)
bkg_estimator = MedianBackground()
filter_size = (3, 3)

# All data have NaNs. Suppress warnings about them.
with warnings.catch_warnings():
warnings.filterwarnings(action="ignore", category=AstropyUserWarning)
try:
bkg = Background2D(self.data, self.box_size,
filter_size=filter_size,
coverage_mask=self.coverage_mask,
sigma_clip=sigma_clip,
bkg_estimator=bkg_estimator)
bkg = Background2D(
self.data,
self.box_size,
filter_size=filter_size,
coverage_mask=self.coverage_mask,
sigma_clip=sigma_clip,
bkg_estimator=bkg_estimator,
)
except ValueError:
# use the entire unmasked array
bkg = Background2D(self.data, self.data.shape,
filter_size=filter_size,
coverage_mask=self.coverage_mask,
sigma_clip=sigma_clip,
bkg_estimator=bkg_estimator,
exclude_percentile=100.)
log.info('Background could not be estimated in meshes. '
'Using the entire unmasked array for background '
f'estimation: bkg_boxsize={self.data.shape}.')
bkg = Background2D(

Check warning on line 77 in jwst/source_catalog/detection.py

View check run for this annotation

Codecov / codecov/patch

jwst/source_catalog/detection.py#L77

Added line #L77 was not covered by tests
self.data,
self.data.shape,
filter_size=filter_size,
coverage_mask=self.coverage_mask,
sigma_clip=sigma_clip,
bkg_estimator=bkg_estimator,
exclude_percentile=100.0,
)
log.info(

Check warning on line 86 in jwst/source_catalog/detection.py

View check run for this annotation

Codecov / codecov/patch

jwst/source_catalog/detection.py#L86

Added line #L86 was not covered by tests
"Background could not be estimated in meshes. "
"Using the entire unmasked array for background "
f"estimation: bkg_boxsize={self.data.shape}."
)

return bkg

@lazyproperty
def background(self):
"""
The 2D background image.
Compute the 2-D background if it has not been computed yet, then return it.

Returns
-------
background : `~numpy.ndarray`
The 2D background image.
"""
return self._background2d.background

@lazyproperty
def background_rms(self):
"""
The 2D background RMS image.
Compute the 2-D background RMS image if it has not been computed yet, then return it.

Returns
-------
background_rms : `~numpy.ndarray`
The 2D background RMS image.
"""
return self._background2d.background_rms


def make_kernel(kernel_fwhm):
"""
Make a 2D Gaussian smoothing kernel that is used to filter the image
before thresholding.
Make a 2D Gaussian smoothing kernel.

The kernel is used to filter the image before thresholding.
Filtering the image will smooth the noise and maximize detectability
of objects with a shape similar to the kernel.

Expand All @@ -131,7 +139,7 @@
"""
sigma = kernel_fwhm * gaussian_fwhm_to_sigma
kernel = Gaussian2DKernel(sigma)
kernel.normalize(mode='integral')
kernel.normalize(mode="integral")
return kernel


Expand All @@ -143,13 +151,16 @@
----------
data : `~numpy.ndarray`
The 2D array to convolve.

kernel_fwhm : float
The full-width at half-maximum (FWHM) of the 2D Gaussian kernel.

mask : array_like, bool, optional
A boolean mask with the same shape as ``data``, where a `True`
value indicates the corresponding element of ``data`` is masked.

Returns
-------
convolved_data : `~numpy.ndarray`
The convolved 2D array.
"""
kernel = make_kernel(kernel_fwhm)

Expand All @@ -160,36 +171,36 @@


class JWSTSourceFinder:
"""
Class to detect sources, including deblending, using image
segmentation.

Parameters
----------
threshold : float
The data value to be used as the per-pixel detection threshold.

npixels : int
The number of connected pixels, each greater than the threshold,
that an object must have to be detected. ``npixels`` must be a
positive integer.

deblend : bool, optional
Whether to deblend overlapping sources. Source deblending
requires scikit-image.
"""
"""Detect sources, including deblending, using image segmentation."""

def __init__(self, threshold, npixels, deblend=False):
"""
Initialize the class.

Parameters
----------
threshold : float
The data value to be used as the per-pixel detection threshold.
npixels : int
The number of connected pixels, each greater than the threshold,
that an object must have to be detected. ``npixels`` must be a
positive integer.
deblend : bool, optional
Whether to deblend overlapping sources. Source deblending
requires scikit-image.
"""
self.threshold = threshold
self.npixels = npixels
self.deblend = deblend
self.connectivity = 8
self.nlevels = 32
self.contrast = 0.001
self.mode = 'exponential'
self.mode = "exponential"

def __call__(self, convolved_data, mask=None):
"""
Run the source detection and deblending.

Parameters
----------
convolved_data : 2D `numpy.ndarray`
Expand All @@ -211,31 +222,36 @@
"""
if mask is not None:
if mask.all():
log.error('There are no valid pixels in the image to detect '
'sources.')
log.error("There are no valid pixels in the image to detect sources.")

Check warning on line 225 in jwst/source_catalog/detection.py

View check run for this annotation

Codecov / codecov/patch

jwst/source_catalog/detection.py#L225

Added line #L225 was not covered by tests
return None

with warnings.catch_warnings():
# suppress NoDetectionsWarning from photutils
warnings.filterwarnings('ignore', category=NoDetectionsWarning)

segment_img = detect_sources(convolved_data, self.threshold,
self.npixels, mask=mask,
connectivity=self.connectivity)
warnings.filterwarnings("ignore", category=NoDetectionsWarning)

segment_img = detect_sources(
convolved_data,
self.threshold,
self.npixels,
mask=mask,
connectivity=self.connectivity,
)
if segment_img is None:
log.warning('No sources were found. Source catalog will not '
'be created.')
log.warning("No sources were found. Source catalog will not be created.")
return None

# source deblending requires scikit-image
if self.deblend:
segment_img = deblend_sources(convolved_data, segment_img,
npixels=self.npixels,
nlevels=self.nlevels,
contrast=self.contrast,
mode=self.mode,
connectivity=self.connectivity,
relabel=True)

log.info(f'Detected {segment_img.nlabels} sources')
segment_img = deblend_sources(

Check warning on line 245 in jwst/source_catalog/detection.py

View check run for this annotation

Codecov / codecov/patch

jwst/source_catalog/detection.py#L245

Added line #L245 was not covered by tests
convolved_data,
segment_img,
npixels=self.npixels,
nlevels=self.nlevels,
contrast=self.contrast,
mode=self.mode,
connectivity=self.connectivity,
relabel=True,
)

log.info(f"Detected {segment_img.nlabels} sources")
return segment_img
Loading