LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
processImage.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008, 2009, 2010 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 import lsst.pex.config as pexConfig
24 import lsst.pex.exceptions as pexExceptions
25 import lsst.pipe.base as pipeBase
26 import lsst.daf.base as dafBase
27 import lsst.afw.geom as afwGeom
28 import lsst.afw.math as afwMath
29 import lsst.afw.table as afwTable
30 from lsst.meas.algorithms import SourceDetectionTask
31 from lsst.meas.base import SingleFrameMeasurementTask
32 from lsst.meas.deblender import SourceDeblendTask
33 from lsst.ip.isr import IsrTask
34 from lsst.pipe.tasks.calibrate import CalibrateTask
35 
36 class ProcessImageConfig(pexConfig.Config):
37  """Config for ProcessImage"""
38  doCalibrate = pexConfig.Field(dtype=bool, default=True, doc = "Perform calibration?")
39  doDetection = pexConfig.Field(dtype=bool, default=True, doc = "Detect sources?")
40  ## NOTE, default this to False until it is fully vetted; #2138
41  doDeblend = pexConfig.Field(dtype=bool, default=False, doc = "Deblend sources?")
42  doMeasurement = pexConfig.Field(dtype=bool, default=True, doc = "Measure sources?")
43  doWriteCalibrate = pexConfig.Field(dtype=bool, default=True, doc = "Write calibration results?")
44  persistBackgroundModel = pexConfig.Field(dtype=bool, default=True, doc = "If True persist background model with background subtracted calexp. \
45  If False persist calexp with the background included.")
46  doWriteCalibrateMatches = pexConfig.Field(dtype=bool, default=True,
47  doc = "Write icSrc to reference matches?")
48  doWriteSources = pexConfig.Field(dtype=bool, default=True, doc = "Write sources?")
49  doWriteSourceMatches = pexConfig.Field(dtype=bool, default=False,
50  doc = "Compute and write src to reference matches?")
51  doWriteHeavyFootprintsInSources = pexConfig.Field(dtype=bool, default=False,
52  doc = "Include HeavyFootprint data in source table?")
53  calibrate = pexConfig.ConfigurableField(
54  target = CalibrateTask,
55  doc = "Calibration (inc. high-threshold detection and measurement)",
56  )
57  detection = pexConfig.ConfigurableField(
58  target = SourceDetectionTask,
59  doc = "Low-threshold detection for final measurement",
60  )
61  deblend = pexConfig.ConfigurableField(
62  target = SourceDeblendTask,
63  doc = "Split blended sources into their components",
64  )
65  measurement = pexConfig.ConfigurableField(
66  target = SingleFrameMeasurementTask,
67  doc = "Final source measurement on low-threshold detections",
68  )
69 
70  def validate(self):
71  pexConfig.Config.validate(self)
72  if self.doMeasurement:
73  if not self.doDetection:
74  raise ValueError("Cannot run source measurement without source detection.")
75  if ("skycoord" not in self.measurement.algorithms.names
76  and "base_SkyCoord" not in self.measurement.algorithms.names):
77  raise ValueError("If you run source measurement you must let it run the skycoord algorithm.")
78  if self.measurement.target.tableVersion != self.calibrate.measurement.target.tableVersion:
79  raise ValueError("measurement subtask tableVersion must match those in calibrate subtask")
80  if self.doDeblend and not self.doDetection:
81  raise ValueError("Cannot run source deblending without source detection.")
83  raise ValueError("Cannot write HeavyFootprints (doWriteHeavyFootprintsInSources) without doWriteSources")
84 
85 class ProcessImageTask(pipeBase.CmdLineTask):
86  """An abstract base class for tasks do simple calibration, detection, deblending, and measurement
87  on individual images.
88 
89  Other command-line Process***Tasks (such as ProcessCcdTask and ProcessCoaddTask) rely on
90  ProcessImageTask for their main algorithmic code, and only need to add pre- and post- processing
91  and serialization.
92 
93  Subclasses are responsible for meeting the requirements of CmdLineTask.
94  """
95  ConfigClass = ProcessImageConfig
96  dataPrefix = "" # Name to prepend to all input and output datasets (e.g. 'goodSeeingCoadd_')
97 
98  def __init__(self, **kwargs):
99  pipeBase.CmdLineTask.__init__(self, **kwargs)
100 
101  tableVersion = self.config.measurement.target.tableVersion
102  self.makeSubtask("calibrate")
103 
104  # Setup our schema by starting with fields we want to propagate from icSrc.
105  calibSchema = self.calibrate.schema
107  self.schemaMapper.addMinimalSchema(afwTable.SourceTable.makeMinimalSchema(), False)
108 
109  # Add fields needed to identify stars used in the calibration step
110  if tableVersion == 0:
111  self.calibSourceKey = self.schemaMapper.addOutputField(
112  afwTable.Field["Flag"]("calib.detected", "Source was detected as an icSrc")
113  )
114  else:
115  self.calibSourceKey = self.schemaMapper.addOutputField(
116  afwTable.Field["Flag"]("calib_detected", "Source was detected as an icSrc")
117  )
118 
119  for key in self.calibrate.getCalibKeys():
120  self.schemaMapper.addMapping(key)
121  self.schema = self.schemaMapper.getOutputSchema()
122  self.schema.setVersion(tableVersion)
124  if self.config.doDetection:
125  self.makeSubtask("detection", schema=self.schema)
126  if self.config.doDeblend:
127  self.makeSubtask("deblend", schema=self.schema)
128  if self.config.doMeasurement:
129  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
130 
131  def makeIdFactory(self, sensorRef):
132  raise NotImplementedError()
133 
134  @pipeBase.timeMethod
135  def process(self, dataRef, inputExposure, enableWriteSources=True):
136  """Process an Image
137 
138  @param dataRef: data reference that corresponds to the input image
139  @param inputExposure: exposure to process
140  @param enableWriteSources: if True then writing sources is allowed.
141  Set False if you need to postprocess sources before writing them.
142 
143  @return pipe_base Struct containing these fields:
144  - postIsrExposure: exposure after ISR performed if calib.doIsr or config.doCalibrate, else None
145  - exposure: calibrated exposure (calexp): as computed if config.doCalibrate,
146  else as upersisted and updated if config.doDetection, else None
147  - calib: object returned by calibration process if config.doCalibrate, else None
148  - sources: detected source if config.doPhotometry, else None
149  """
150  idFactory = self.makeIdFactory(dataRef)
151 
152  # initialize outputs
153  calExposure = inputExposure
154  calib = None
155  sources = None
156  backgrounds = afwMath.BackgroundList()
157  if self.config.doCalibrate:
158  calib = self.calibrate.run(inputExposure, idFactory=idFactory)
159  calExposure = calib.exposure
160  if self.config.doWriteCalibrate:
161  dataRef.put(calib.sources, self.dataPrefix + "icSrc")
162  if calib.matches is not None and self.config.doWriteCalibrateMatches:
163  normalizedMatches = afwTable.packMatches(calib.matches)
164  normalizedMatches.table.setMetadata(calib.matchMeta)
165  dataRef.put(normalizedMatches, self.dataPrefix + "icMatch")
166  try:
167  for bg in calib.backgrounds:
168  backgrounds.append(bg)
169  except TypeError:
170  backgrounds.append(calib.backgrounds)
171  except AttributeError:
172  self.log.warn("The calibration task did not return any backgrounds. " +
173  "Any background subtracted in the calibration process cannot be persisted.")
174  else:
175  calib = None
176 
177  if self.config.doDetection:
178  table = afwTable.SourceTable.make(self.schema, idFactory)
179  table.setMetadata(self.algMetadata)
180  detections = self.detection.run(table, calExposure)
181  sources = detections.sources
182  fpSets = detections.fpSets
183  if fpSets.background:
184  backgrounds.append(fpSets.background)
185 
186  if self.config.doDeblend:
187  self.deblend.run(calExposure, sources, calExposure.getPsf())
188 
189  if self.config.doMeasurement:
190  self.measurement.run(calExposure, sources)
191 
192  if self.config.doWriteCalibrate:
193  # wait until after detection and measurement, since detection sets detected mask bits
194  # and both require a background subtracted exposure;
195  # note that this overwrites an existing calexp if doCalibrate false
196 
197  if calExposure is None:
198  self.log.warn("calibrated exposure is None; cannot save it")
199  else:
200  if self.config.persistBackgroundModel:
201  self.writeBackgrounds(dataRef, backgrounds)
202  else:
203  self.restoreBackgrounds(calExposure, backgrounds)
204  dataRef.put(calExposure, self.dataPrefix + "calexp")
205 
206  if calib is not None:
207  self.propagateCalibFlags(calib.sources, sources)
208 
209  if sources is not None and self.config.doWriteSources:
210  sourceWriteFlags = (0 if self.config.doWriteHeavyFootprintsInSources
211  else afwTable.SOURCE_IO_NO_HEAVY_FOOTPRINTS)
212  if enableWriteSources:
213  dataRef.put(sources, self.dataPrefix + 'src', flags=sourceWriteFlags)
214 
215  if self.config.doMeasurement and self.config.doWriteSourceMatches:
216  self.log.info("Matching src to reference catalogue" % (dataRef.dataId))
217  srcMatches, srcMatchMeta = self.matchSources(calExposure, sources)
218 
219  normalizedSrcMatches = afwTable.packMatches(srcMatches)
220  normalizedSrcMatches.table.setMetadata(srcMatchMeta)
221  dataRef.put(normalizedSrcMatches, self.dataPrefix + "srcMatch")
222  else:
223  srcMatches = None; srcMatchMeta = None
224 
225  return pipeBase.Struct(
226  inputExposure = inputExposure,
227  exposure = calExposure,
228  calib = calib,
229  sources = sources,
230  matches = srcMatches,
231  matchMeta = srcMatchMeta,
232  backgrounds = backgrounds,
233  )
234 
235  def matchSources(self, exposure, sources):
236  """Match the sources to the reference object loaded by the calibrate task"""
237  try:
238  astrometer = self.calibrate.astrometry.astrometer
239  if astrometer is None:
240  raise AttributeError("No astrometer")
241  except AttributeError:
242  self.log.warn("Failed to find an astrometer in calibrate's astronomy task")
243  return None, None
244 
245  astromRet = astrometer.useKnownWcs(sources, exposure=exposure)
246  # N.b. yes, this is what useKnownWcs calls the returned values
247  return astromRet.matches, astromRet.matchMetadata
248 
249  def propagateCalibFlags(self, icSources, sources, matchRadius=1):
250  """Match the icSources and sources, and propagate Interesting Flags (e.g. PSF star) to the sources
251  """
252  self.log.info("Matching icSource and Source catalogs to propagate flags.")
253  if icSources is None or sources is None:
254  return
255 
256  closest = False # return all matched objects
257  matched = afwTable.matchRaDec(icSources, sources, matchRadius*afwGeom.arcseconds, closest)
258  if self.config.doDeblend:
259  if self.config.measurement.target.tableVersion == 0:
260  matched = [m for m in matched if m[1].get("deblend.nchild") == 0] # if deblended, keep children
261  else:
262  matched = [m for m in matched if m[1].get("deblend_nChild") == 0] # if deblended, keep children
263  #
264  # Because we had to allow multiple matches to handle parents, we now need to
265  # prune to the best matches
266  #
267  bestMatches = {}
268  for m0, m1, d in matched:
269  id0 = m0.getId()
270  if bestMatches.has_key(id0):
271  if d > bestMatches[id0][2]:
272  continue
273 
274  bestMatches[id0] = (m0, m1, d)
275 
276  matched = bestMatches.values()
277  #
278  # Check that we got it right
279  #
280  if len(set(m[0].getId() for m in matched)) != len(matched):
281  self.log.warn("At least one icSource is matched to more than one Source")
282  #
283  # Copy over the desired flags
284  #
285  for ics, s, d in matched:
286  s.setFlag(self.calibSourceKey, True)
287  s.assign(ics, self.schemaMapper)
288 
289  return
290 
291  def getSchemaCatalogs(self):
292  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
293  src = afwTable.SourceCatalog(self.schema)
294  src.getTable().setMetadata(self.algMetadata)
295  d = {self.dataPrefix + "src": src}
296  icSrc = None
297  try:
298  icSrc = afwTable.SourceCatalog(self.calibrate.schema)
299  icSrc.getTable().setMetadata(self.calibrate.algMetadata)
300  except AttributeError:
301  pass
302  if icSrc is not None:
303  d[self.dataPrefix + "icSrc"] = icSrc
304  return d
305 
306  def writeBackgrounds(self, dataRef, backgrounds):
307  """Backgrounds are persisted via the butler
308 
309  @param dataRef: Data reference
310  @param backgrounds: List of background models
311  """
312  self.log.warn("Persisting background models")
313 
314  dataRef.put(backgrounds, self.dataPrefix+"calexpBackground")
315 
316  def restoreBackgrounds(self, exp, backgrounds):
317  """Add backgrounds back in to an exposure
318 
319  @param exp: Exposure to which to add backgrounds
320  @param backgrounds: List of background models
321  """
322  mi = exp.getMaskedImage()
323  mi += backgrounds.getImage()
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:19
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
A description of a field in a table.
Definition: Field.h:22
BaseCatalog packMatches(std::vector< Match< Record1, Record2 > > const &matches)
Return a table representation of a MatchVector that can be used to persist it.
std::vector< Match< typename Cat::Record, typename Cat::Record > > matchRaDec(Cat const &cat, Angle radius, bool symmetric=true)
tuple doDeblend
NOTE, default this to False until it is fully vetted; #2138.
Definition: processImage.py:41