Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-46287: Make image binning a subtask for IsrTask #349

Merged
merged 4 commits into from
Sep 26, 2024
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: 1 addition & 0 deletions python/lsst/ip/isr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@
from .transmissionCurve import *
from .ampOffset import *
from .overscanAmpConfig import *
from .binExposureTask import *
201 changes: 201 additions & 0 deletions python/lsst/ip/isr/binExposureTask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# This file is part of ip_isr.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

__all__ = ["BinExposureTask",
"BinExposureConfig",
"binExposure"]

import lsst.pipe.base as pipeBase
import lsst.pipe.base.connectionTypes as cT
import lsst.pex.config as pexConfig
import lsst.afw.math as afwMath
from lsst.utils.timer import timeMethod
import lsst.afw.image as afwImage


class BinExposureConnections(
pipeBase.PipelineTaskConnections,
dimensions=("instrument", "exposure", "detector"),
defaultTemplates={"inputName": "postISRCCD", "outputName": "postISRCCDBin"}
):

inputExposure = cT.Input(
name="{inputName}",
doc="Input exposure to bin.",
storageClass="Exposure",
dimensions=["instrument", "exposure", "detector"],
)
binnedExposure = cT.Output(
name="{outputName}",
doc="Binned exsposure.",
storageClass="Exposure",
dimensions=["instrument", "exposure", "detector"],
)

def __init__(self, *, config=None):
"""Customize the connections and storageClass for a specific
instance. This enables both to be dynamically set at runtime,
allowing BinExposureTask to work with different types of
Exposures such as postISRCCD, calexp, deepCoadd_calexp, etc.

Parameters
----------
config : `BinExposureConfig`
A config for `BinExposureTask`.
"""
super().__init__(config=config)
if config and config.exposureDimensions != self.inputExposure.dimensions:
self.dimensions.clear()
self.dimensions.update(config.exposureDimensions)
self.inputExposure = cT.Input(
name=self.inputExposure.name,
doc=self.inputExposure.doc,
storageClass=self.inputExposure.storageClass,
dimensions=frozenset(config.exposureDimensions),
)
self.binnedExposure = cT.Output(
name=self.binnedExposure.name,
doc=self.binnedExposure.doc,
storageClass=self.binnedExposure.storageClass,
dimensions=frozenset(config.exposureDimensions),
)
if config and config.exposureStorageClass != self.inputExposure.storageClass:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any expected cases where the storageClass would need to change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this also would cause problems with the check on L193, as that will raise if the storageClass isn't an Exposure. This does allow ExposureD, so maybe a comment here mentioning that although the storageClass can be changed, if it isn't some kind of Exposure, it's going to cause problems.

Copy link
Contributor Author

@jrmullaney jrmullaney Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to ensure that the output storage class would always match the input storage class. While I could have the input and output storage classes as the generic Exposure, and it would be happy with any of the sub-types as input, any subtype information (I,F,D etc) would be lost on the output (at least that's my understanding).

I can certainly add the comment. [Late edit: Rather than add a comment, I've added an extra line to the config doc to state that it must be of type Exposure or one of its sub-types.]

self.inputExposure = cT.Input(
name=self.inputExposure.name,
doc=self.inputExposure.doc,
storageClass=config.exposureStorageClass,
dimensions=self.inputExposure.dimensions,
)
self.binnedExposure = cT.Output(
name=self.binnedExposure.name,
doc=self.binnedExposure.doc,
storageClass=config.exposureStorageClass,
dimensions=self.binnedExposure.dimensions,
)


class BinExposureConfig(
pipeBase.PipelineTaskConfig,
pipelineConnections=BinExposureConnections
):
"""Config for BinExposureTask"""
exposureDimensions = pexConfig.ListField(
# Sort to ensure default order is consistent between runs
default=sorted(BinExposureConnections.dimensions),
dtype=str,
doc="Override for the dimensions of the input and binned exposures.",
)
exposureStorageClass = pexConfig.Field(
default='ExposureF',
dtype=str,
doc=(
"Override the storageClass of the input and binned exposures. "
"Must be of type lsst.afw.Image.Exposure, or one of its subtypes."
)
)
binFactor = pexConfig.Field(
dtype=int,
doc="Binning factor applied to both spatial dimensions.",
default=8,
check=lambda x: x > 1,
)


class BinExposureTask(pipeBase.PipelineTask):
"""Perform an nxn binning of an Exposure dataset type.

The binning factor is the same in both spatial dimensions (i.e.,
an nxn binning is performed). Each of the input Exposure's image
arrays are binned by the same factor.
"""
# TODO: DM-46501: Add tasks to nxn bin Image and MaskedImage classes
ConfigClass = BinExposureConfig
_DefaultName = "binExposure"

@timeMethod
def run(self, inputExposure, binFactor=None):
"""Perform an nxn binning of an Exposure.

Parameters:
-----------
inputExposure : `lsst.afw.image.Exposure` or one of its
sub-types.
Exposure to spatially bin
binFactor : `int`, optional.
nxn binning factor. If not provided then self.config.binFactor
is used.

Returns:
--------
result : `lsst.pipe.base.Struct`
Results as a struct with attributes:

``binnedExposure``
Binned exposure (`lsst.afw.image.Exposure` or one of its
sub-types. The type matches that of the inputExposure).
"""
if not binFactor:
binFactor = self.config.binFactor
return pipeBase.Struct(
binnedExposure=binExposure(inputExposure, binFactor)
)


def binExposure(inputExposure, binFactor=8):
jrmullaney marked this conversation as resolved.
Show resolved Hide resolved
"""Bin an exposure to reduce its spatial dimensions.

Performs an nxn binning of the input exposure, reducing both spatial
dimensions of each of the input exposure's image data by the provided
factor.

Parameters:
-----------
inputExposure: `lsst.afw.image.Exposure` or one of its sub-types.
Input exposure data to bin.
binFactor: `int`
Binning factor to apply to each input exposure's image data.
Default 8.

Returns:
--------
binnedExposure: `lsst.afw.image.Exposure` or one of its sub-types.
Binned version of input image.

Raises
------
TypeError
Raised if either the binning factor is not of type `int`, or if the
input data to be binned is not of type `lsst.afw.image.Exposure`
or one of its sub-types.
"""

if not isinstance(binFactor, int):
raise TypeError("binFactor must be of type int")
if not isinstance(inputExposure, afwImage.Exposure):
raise TypeError("inputExp must be of type lsst.afw.image.Exposure or one of its sub-tyoes.")

binned = inputExposure.getMaskedImage()
binned = afwMath.binImage(binned, binFactor)
binnedExposure = afwImage.makeExposure(binned)

binnedExposure.setInfo(inputExposure.getInfo())

return binnedExposure
24 changes: 23 additions & 1 deletion python/lsst/ip/isr/isrTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import lsst.pipe.base.connectionTypes as cT

from contextlib import contextmanager
from deprecated.sphinx import deprecated
from lsstDebug import getDebugFrame

from lsst.afw.cameraGeom import NullLinearityType
Expand All @@ -46,6 +47,7 @@
from .defects import Defects

from .assembleCcdTask import AssembleCcdTask
from .binExposureTask import BinExposureTask
jrmullaney marked this conversation as resolved.
Show resolved Hide resolved
from .crosstalk import CrosstalkTask, CrosstalkCalib
from .fringe import FringeTask
from .isr import maskNans
Expand Down Expand Up @@ -948,6 +950,10 @@ class IsrTaskConfig(pipeBase.PipelineTaskConfig,
doc="Should binned exposures be calculated?",
default=False,
)
binning = pexConfig.ConfigurableField(
target=BinExposureTask,
doc="Task to bin the exposure.",
)
binFactor1 = pexConfig.Field(
dtype=int,
doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
Expand Down Expand Up @@ -1027,6 +1033,7 @@ def __init__(self, **kwargs):
self.makeSubtask("ampOffset")
self.makeSubtask("deferredChargeCorrection")
self.makeSubtask("isrStats")
self.makeSubtask("binning")

def runQuantum(self, butlerQC, inputRefs, outputRefs):
inputs = butlerQC.get(inputRefs)
Expand Down Expand Up @@ -1777,7 +1784,15 @@ def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
outputBin1Exposure = None
outputBin2Exposure = None
if self.config.doBinnedExposures:
outputBin1Exposure, outputBin2Exposure = self.makeBinnedImages(ccdExposure)
jrmullaney marked this conversation as resolved.
Show resolved Hide resolved
self.log.info("Creating binned exposures.")
outputBin1Exposure = self.binning.run(
ccdExposure,
binFactor=self.config.binFactor1,
).binnedExposure
outputBin2Exposure = self.binning.run(
ccdExposure,
binFactor=self.config.binFactor2,
).binnedExposure

self.debugView(ccdExposure, "postISRCCD")

Expand Down Expand Up @@ -2773,6 +2788,13 @@ def flatContext(self, exp, flat, dark=None):
if self.config.doDark and dark is not None:
self.darkCorrection(exp, dark, invert=True)

@deprecated(
reason=(
"makeBinnedImages is no longer used. "
"Please subtask lsst.ip.isr.BinExposureTask instead."
),
version="v28", category=FutureWarning
)
def makeBinnedImages(self, exposure):
"""Make visualizeVisit style binned exposures.

Expand Down
24 changes: 23 additions & 1 deletion python/lsst/ip/isr/isrTaskLSST.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .defects import Defects

from contextlib import contextmanager
from deprecated.sphinx import deprecated
import lsst.pex.config as pexConfig
import lsst.afw.math as afwMath
import lsst.pipe.base as pipeBase
Expand All @@ -16,6 +17,7 @@
from lsst.meas.algorithms.detection import SourceDetectionTask

from .ampOffset import AmpOffsetTask
from .binExposureTask import BinExposureTask
from .overscan import SerialOverscanCorrectionTask, ParallelOverscanCorrectionTask
from .overscanAmpConfig import OverscanCameraConfig
from .assembleCcdTask import AssembleCcdTask
Expand Down Expand Up @@ -502,6 +504,10 @@ class IsrTaskLSSTConfig(pipeBase.PipelineTaskConfig,
doc="Should binned exposures be calculated?",
default=False,
)
binning = pexConfig.ConfigurableField(
target=BinExposureTask,
doc="Task to bin the exposure.",
)
binFactor1 = pexConfig.Field(
dtype=int,
doc="Binning factor for first binned exposure. This is intended for a finely binned output.",
Expand Down Expand Up @@ -550,6 +556,7 @@ def __init__(self, **kwargs):
self.makeSubtask("masking")
self.makeSubtask("isrStats")
self.makeSubtask("ampOffset")
self.makeSubtask("binning")

def runQuantum(self, butlerQC, inputRefs, outputRefs):

Expand Down Expand Up @@ -1439,6 +1446,13 @@ def flatCorrection(self, exposure, flatExposure, invert=False):
invert=invert,
)

@deprecated(
reason=(
"makeBinnedImages is no longer used. "
"Please subtask lsst.ip.isr.BinExposureTask instead."
),
version="v28", category=FutureWarning
)
def makeBinnedImages(self, exposure):
"""Make visualizeVisit style binned exposures.

Expand Down Expand Up @@ -1863,7 +1877,15 @@ def run(self, ccdExposure, *, dnlLUT=None, bias=None, deferredChargeCalib=None,
outputBin1Exposure = None
outputBin2Exposure = None
if self.config.doBinnedExposures:
outputBin1Exposure, outputBin2Exposure = self.makeBinnedImages(ccdExposure)
self.log.info("Creating binned exposures.")
outputBin1Exposure = self.binning.run(
ccdExposure,
binFactor=self.config.binFactor1,
).binnedExposure
outputBin2Exposure = self.binning.run(
ccdExposure,
binFactor=self.config.binFactor2,
).binnedExposure

return pipeBase.Struct(
exposure=ccdExposure,
Expand Down
Loading
Loading