LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
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.doDeblend and not self.doDetection:
79  raise ValueError("Cannot run source deblending without source detection.")
81  raise ValueError("Cannot write HeavyFootprints (doWriteHeavyFootprintsInSources) without doWriteSources")
82 
83 class ProcessImageTask(pipeBase.CmdLineTask):
84  """An abstract base class for tasks do simple calibration, detection, deblending, and measurement
85  on individual images.
86 
87  Other command-line Process***Tasks (such as ProcessCcdTask and ProcessCoaddTask) rely on
88  ProcessImageTask for their main algorithmic code, and only need to add pre- and post- processing
89  and serialization.
90 
91  Subclasses are responsible for meeting the requirements of CmdLineTask.
92  """
93  ConfigClass = ProcessImageConfig
94  dataPrefix = "" # Name to prepend to all input and output datasets (e.g. 'goodSeeingCoadd_')
95 
96  def __init__(self, **kwargs):
97  pipeBase.CmdLineTask.__init__(self, **kwargs)
98 
99  self.makeSubtask("calibrate")
100 
101  # Setup our schema by starting with fields we want to propagate from icSrc.
102  calibSchema = self.calibrate.schema
104  self.schemaMapper.addMinimalSchema(afwTable.SourceTable.makeMinimalSchema(), False)
105 
106  # Add fields needed to identify stars used in the calibration step
107  self.calibSourceKey = self.schemaMapper.addOutputField(
108  afwTable.Field["Flag"]("calib_detected", "Source was detected as an icSrc")
109  )
110 
111  for key in self.calibrate.getCalibKeys():
112  self.schemaMapper.addMapping(key)
113  self.schema = self.schemaMapper.getOutputSchema()
115  if self.config.doDetection:
116  self.makeSubtask("detection", schema=self.schema)
117  if self.config.doDeblend:
118  self.makeSubtask("deblend", schema=self.schema)
119  if self.config.doMeasurement:
120  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
121 
122  def makeIdFactory(self, sensorRef):
123  raise NotImplementedError()
124 
125  def getExposureId(self, sensorRef):
126  raise NotImplementedError()
127 
128  @pipeBase.timeMethod
129  def process(self, dataRef, inputExposure, enableWriteSources=True):
130  """Process an Image
131 
132  @param dataRef: data reference that corresponds to the input image
133  @param inputExposure: exposure to process
134  @param enableWriteSources: if True then writing sources is allowed.
135  Set False if you need to postprocess sources before writing them.
136 
137  @return pipe_base Struct containing these fields:
138  - postIsrExposure: exposure after ISR performed if calib.doIsr or config.doCalibrate, else None
139  - exposure: calibrated exposure (calexp): as computed if config.doCalibrate,
140  else as upersisted and updated if config.doDetection, else None
141  - calib: object returned by calibration process if config.doCalibrate, else None
142  - sources: detected source if config.doPhotometry, else None
143  """
144  idFactory = self.makeIdFactory(dataRef)
145 
146  # initialize outputs
147  calExposure = inputExposure
148  calib = None
149  sources = None
150  backgrounds = afwMath.BackgroundList()
151  if self.config.doCalibrate:
152  calib = self.calibrate.run(inputExposure, idFactory=idFactory)
153  calExposure = calib.exposure
154  if self.config.doWriteCalibrate:
155  dataRef.put(calib.sources, self.dataPrefix + "icSrc")
156  if calib.matches is not None and self.config.doWriteCalibrateMatches:
157  normalizedMatches = afwTable.packMatches(calib.matches)
158  normalizedMatches.table.setMetadata(calib.matchMeta)
159  dataRef.put(normalizedMatches, self.dataPrefix + "icMatch")
160  try:
161  for bg in calib.backgrounds:
162  backgrounds.append(bg)
163  except TypeError:
164  backgrounds.append(calib.backgrounds)
165  except AttributeError:
166  self.log.warn("The calibration task did not return any backgrounds. " +
167  "Any background subtracted in the calibration process cannot be persisted.")
168  else:
169  calib = None
170 
171  if self.config.doDetection:
172  table = afwTable.SourceTable.make(self.schema, idFactory)
173  table.setMetadata(self.algMetadata)
174  detections = self.detection.run(table, calExposure)
175  sources = detections.sources
176  fpSets = detections.fpSets
177  if fpSets.background:
178  backgrounds.append(fpSets.background)
179 
180  if self.config.doDeblend:
181  self.deblend.run(calExposure, sources, calExposure.getPsf())
182 
183  if self.config.doMeasurement:
184  self.measurement.run(calExposure, sources, exposureId=self.getExposureId(dataRef))
185 
186  if self.config.doWriteCalibrate:
187  # wait until after detection and measurement, since detection sets detected mask bits
188  # and both require a background subtracted exposure;
189  # note that this overwrites an existing calexp if doCalibrate false
190 
191  if calExposure is None:
192  self.log.warn("calibrated exposure is None; cannot save it")
193  else:
194  if self.config.persistBackgroundModel:
195  self.writeBackgrounds(dataRef, backgrounds)
196  else:
197  self.restoreBackgrounds(calExposure, backgrounds)
198  dataRef.put(calExposure, self.dataPrefix + "calexp")
199 
200  if calib is not None:
201  self.propagateCalibFlags(calib.sources, sources)
202 
203  if sources is not None and self.config.doWriteSources:
204  sourceWriteFlags = (0 if self.config.doWriteHeavyFootprintsInSources
205  else afwTable.SOURCE_IO_NO_HEAVY_FOOTPRINTS)
206  if enableWriteSources:
207  dataRef.put(sources, self.dataPrefix + 'src', flags=sourceWriteFlags)
208 
209  if self.config.doMeasurement and self.config.doWriteSourceMatches:
210  self.log.info("Matching src to reference catalogue" % (dataRef.dataId))
211  srcMatches, srcMatchMeta = self.matchSources(calExposure, sources)
212 
213  normalizedSrcMatches = afwTable.packMatches(srcMatches)
214  normalizedSrcMatches.table.setMetadata(srcMatchMeta)
215  dataRef.put(normalizedSrcMatches, self.dataPrefix + "srcMatch")
216  else:
217  srcMatches = None; srcMatchMeta = None
218 
219  return pipeBase.Struct(
220  inputExposure = inputExposure,
221  exposure = calExposure,
222  calib = calib,
223  sources = sources,
224  matches = srcMatches,
225  matchMeta = srcMatchMeta,
226  backgrounds = backgrounds,
227  )
228 
229  def matchSources(self, exposure, sources):
230  """Match the sources to the reference object loaded by the calibrate task"""
231  try:
232  astrometer = self.calibrate.astrometry.astrometer
233  if astrometer is None:
234  raise AttributeError("No astrometer")
235  except AttributeError:
236  self.log.warn("Failed to find an astrometer in calibrate's astronomy task")
237  return None, None
238 
239  astromRet = astrometer.useKnownWcs(sources, exposure=exposure)
240  # N.b. yes, this is what useKnownWcs calls the returned values
241  return astromRet.matches, astromRet.matchMetadata
242 
243  def propagateCalibFlags(self, icSources, sources, matchRadius=1):
244  """Match the icSources and sources, and propagate Interesting Flags (e.g. PSF star) to the sources
245  """
246  self.log.info("Matching icSource and Source catalogs to propagate flags.")
247  if icSources is None or sources is None:
248  return
249 
250  closest = False # return all matched objects
251  matched = afwTable.matchRaDec(icSources, sources, matchRadius*afwGeom.arcseconds, closest)
252  if self.config.doDeblend:
253  matched = [m for m in matched if m[1].get("deblend_nChild") == 0] # if deblended, keep children
254  #
255  # Because we had to allow multiple matches to handle parents, we now need to
256  # prune to the best matches
257  #
258  bestMatches = {}
259  for m0, m1, d in matched:
260  id0 = m0.getId()
261  if bestMatches.has_key(id0):
262  if d > bestMatches[id0][2]:
263  continue
264 
265  bestMatches[id0] = (m0, m1, d)
266 
267  matched = bestMatches.values()
268  #
269  # Check that we got it right
270  #
271  if len(set(m[0].getId() for m in matched)) != len(matched):
272  self.log.warn("At least one icSource is matched to more than one Source")
273  #
274  # Copy over the desired flags
275  #
276  for ics, s, d in matched:
277  s.setFlag(self.calibSourceKey, True)
278  s.assign(ics, self.schemaMapper)
279 
280  return
281 
282  def getSchemaCatalogs(self):
283  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
284  src = afwTable.SourceCatalog(self.schema)
285  src.getTable().setMetadata(self.algMetadata)
286  d = {self.dataPrefix + "src": src}
287  icSrc = None
288  try:
289  icSrc = afwTable.SourceCatalog(self.calibrate.schema)
290  icSrc.getTable().setMetadata(self.calibrate.algMetadata)
291  except AttributeError:
292  pass
293  if icSrc is not None:
294  d[self.dataPrefix + "icSrc"] = icSrc
295  return d
296 
297  def writeBackgrounds(self, dataRef, backgrounds):
298  """Backgrounds are persisted via the butler
299 
300  @param dataRef: Data reference
301  @param backgrounds: List of background models
302  """
303  self.log.warn("Persisting background models")
304 
305  dataRef.put(backgrounds, self.dataPrefix+"calexpBackground")
306 
307  def restoreBackgrounds(self, exp, backgrounds):
308  """Add backgrounds back in to an exposure
309 
310  @param exp: Exposure to which to add backgrounds
311  @param backgrounds: List of background models
312  """
313  mi = exp.getMaskedImage()
314  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