LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
LSSTDataManagementBasePackage
dipoleMeasurement.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 
24 import numpy as np
25 
26 import lsst.afw.geom as afwGeom
27 import lsst.afw.image as afwImage
28 import lsst.pex.config as pexConfig
29 from lsst.log import Log
30 import lsst.meas.deblender.baseline as deblendBaseline
31 from lsst.meas.base.pluginRegistry import register
32 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
33  SingleFramePluginConfig, SingleFramePlugin
34 import lsst.afw.display.ds9 as ds9
35 
36 __all__ = ("DipoleMeasurementConfig", "DipoleMeasurementTask", "DipoleAnalysis", "DipoleDeblender",
37  "SourceFlagChecker", "ClassificationDipoleConfig", "ClassificationDipolePlugin")
38 
39 
40 class ClassificationDipoleConfig(SingleFramePluginConfig):
41  """Configuration for classification of detected diaSources as dipole or not"""
42  minSn = pexConfig.Field(
43  doc="Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
44  dtype=float, default=np.sqrt(2) * 5.0,
45  )
46  maxFluxRatio = pexConfig.Field(
47  doc="Maximum flux ratio in either lobe to be considered a dipole",
48  dtype=float, default=0.65
49  )
50 
51 
52 @register("ip_diffim_ClassificationDipole")
53 class ClassificationDipolePlugin(SingleFramePlugin):
54  """A plugin to classify whether a diaSource is a dipole.
55  """
56 
57  ConfigClass = ClassificationDipoleConfig
58 
59  @classmethod
61  """
62  Returns
63  -------
64  result : `callable`
65  """
66  return cls.APCORR_ORDER
67 
68  def __init__(self, config, name, schema, metadata):
69  SingleFramePlugin.__init__(self, config, name, schema, metadata)
71  self.keyProbability = schema.addField(name + "_value", type="D",
72  doc="Set to 1 for dipoles, else 0.")
73  self.keyFlag = schema.addField(name + "_flag", type="Flag", doc="Set to 1 for any fatal failure.")
74 
75  def measure(self, measRecord, exposure):
76  passesSn = self.dipoleAnalysis.getSn(measRecord) > self.config.minSn
77  negFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_neg_instFlux"))
78  negFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_neg_flag")
79  posFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_pos_instFlux"))
80  posFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_pos_flag")
81 
82  if negFluxFlag or posFluxFlag:
83  self.fail(measRecord)
84  # continue on to classify
85 
86  totalFlux = negFlux + posFlux
87 
88  # If negFlux or posFlux are NaN, these evaluate to False
89  passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio
90  passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio
91  if (passesSn and passesFluxPos and passesFluxNeg):
92  val = 1.0
93  else:
94  val = 0.0
95 
96  measRecord.set(self.keyProbability, val)
97 
98  def fail(self, measRecord, error=None):
99  measRecord.set(self.keyFlag, True)
100 
101 
103  """Measurement of detected diaSources as dipoles"""
104 
105  def setDefaults(self):
106  SingleFrameMeasurementConfig.setDefaults(self)
107  self.plugins = ["base_CircularApertureFlux",
108  "base_PixelFlags",
109  "base_SkyCoord",
110  "base_PsfFlux",
111  "ip_diffim_NaiveDipoleCentroid",
112  "ip_diffim_NaiveDipoleFlux",
113  "ip_diffim_PsfDipoleFlux",
114  "ip_diffim_ClassificationDipole",
115  ]
116 
117  self.slots.calibFlux = None
118  self.slots.modelFlux = None
119  self.slots.gaussianFlux = None
120  self.slots.shape = None
121  self.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
122  self.doReplaceWithNoise = False
123 
124 
126  """Measurement of Sources, specifically ones from difference images, for characterization as dipoles
127 
128  Parameters
129  ----------
130  sources : 'lsst.afw.table.SourceCatalog'
131  Sources that will be measured
132  badFlags : `list` of `dict`
133  A list of flags that will be used to determine if there was a measurement problem
134 
135  Notes
136  -----
137  The list of badFlags will be used to make a list of keys to check for measurement flags on. By
138  default the centroid keys are added to this list
139 
140  Description
141 
142  This class provides a default configuration for running Source measurement on image differences.
143 
144  .. code-block:: py
145 
146  class DipoleMeasurementConfig(SingleFrameMeasurementConfig):
147  "Measurement of detected diaSources as dipoles"
148  def setDefaults(self):
149  SingleFrameMeasurementConfig.setDefaults(self)
150  self.plugins = ["base_CircularApertureFlux",
151  "base_PixelFlags",
152  "base_SkyCoord",
153  "base_PsfFlux",
154  "ip_diffim_NaiveDipoleCentroid",
155  "ip_diffim_NaiveDipoleFlux",
156  "ip_diffim_PsfDipoleFlux",
157  "ip_diffim_ClassificationDipole",
158  ]
159  self.slots.calibFlux = None
160  self.slots.modelFlux = None
161  self.slots.instFlux = None
162  self.slots.shape = None
163  self.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
164  self.doReplaceWithNoise = False
165 
166  These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole.
167  This includes a set of measurements derived from intermediate base classes
168  DipoleCentroidAlgorithm and DipoleFluxAlgorithm.
169  Their respective algorithm control classes are defined in
170  DipoleCentroidControl and DipoleFluxControl.
171  Each centroid and flux measurement will have _neg (negative)
172  and _pos (positive lobe) fields.
173 
174  The first set of measurements uses a "naive" alrogithm
175  for centroid and flux measurements, implemented in
176  NaiveDipoleCentroidControl and NaiveDipoleFluxControl.
177  The algorithm uses a naive 3x3 weighted moment around
178  the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields
179  ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux*
180 
181  The second set of measurements undertakes a joint-Psf model on the negative
182  and positive lobe simultaneously. This fit simultaneously solves for the negative and positive
183  lobe centroids and fluxes using non-linear least squares minimization.
184  The fields are stored in table elements ip_diffim_PsfDipoleFlux*.
185 
186  Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually
187  editing the config and running SourceMeasurementTask. For example:
188 
189  .. code-block:: py
190 
191  config = SingleFrameMeasurementConfig()
192  config.plugins.names = ["base_PsfFlux",
193  "ip_diffim_PsfDipoleFlux",
194  "ip_diffim_NaiveDipoleFlux",
195  "ip_diffim_NaiveDipoleCentroid",
196  "ip_diffim_ClassificationDipole",
197  "base_CircularApertureFlux",
198  "base_SkyCoord"]
199 
200  config.slots.calibFlux = None
201  config.slots.modelFlux = None
202  config.slots.instFlux = None
203  config.slots.shape = None
204  config.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
205  config.doReplaceWithNoise = False
206 
207  schema = afwTable.SourceTable.makeMinimalSchema()
208  task = SingleFrameMeasurementTask(schema, config=config)-
209 
210  Debug variables
211 
212  The ``lsst.pipe.base.cmdLineTask.CmdLineTask`` command line task interface supports a
213  flag-d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py
214  for this Task include:
215 
216  .. code-block:: py
217 
218  import sys
219  import lsstDebug
220  def DebugInfo(name):
221  di = lsstDebug.getInfo(name)
222  if name == "lsst.ip.diffim.dipoleMeasurement":
223  di.display = True # enable debug output
224  di.maskTransparency = 90 # ds9 mask transparency
225  di.displayDiaSources = True # show exposure with dipole results
226  return di
227  lsstDebug.Info = DebugInfo
228  lsstDebug.frame = 1
229 
230  config.slots.calibFlux = None
231  config.slots.modelFlux = None
232  config.slots.gaussianFlux = None
233  config.slots.shape = None
234  config.slots.centroid = "ip_diffim_NaiveDipoleCentroid"
235  config.doReplaceWithNoise = False
236 
237  This code is dipoleMeasTask.py in the examples directory, and can be run as e.g.
238 
239  .. code-block:: none
240 
241  examples/dipoleMeasTask.py
242  examples/dipoleMeasTask.py --debug
243  examples/dipoleMeasTask.py --debug --image /path/to/image.fits
244 
245 
246 
247  Start the processing by parsing the command line, where the user has the option of
248  enabling debugging output and/or sending their own image for demonstration
249  (in case they have not downloaded the afwdata package).
250 
251  .. code-block:: py
252 
253  if __name__ == "__main__":
254  import argparse
255  parser = argparse.ArgumentParser(
256  description="Demonstrate the use of SourceDetectionTask and DipoleMeasurementTask")
257  parser.add_argument('--debug', '-d', action="store_true", help="Load debug.py?", default=False)
258  parser.add_argument("--image", "-i", help="User defined image", default=None)
259  args = parser.parse_args()
260  if args.debug:
261  try:
262  import debug
263  debug.lsstDebug.frame = 2
264  except ImportError as e:
265  print(e, file=sys.stderr)
266  run(args)
267 
268  The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying
269  it if requested:
270 
271  .. code-block:: py
272 
273  def run(args):
274  exposure = loadData(args.image)
275  if args.debug:
276  ds9.mtv(exposure, frame=1)
277 
278  Create a default source schema that we will append fields to as we add more algorithms:
279 
280  .. code-block:: py
281 
282  schema = afwTable.SourceTable.makeMinimalSchema()
283 
284  Create the detection and measurement Tasks, with some minor tweaking of their configs:
285 
286  .. code-block:: py
287 
288  # Create the detection task
289  config = SourceDetectionTask.ConfigClass()
290  config.thresholdPolarity = "both"
291  config.background.isNanSafe = True
292  config.thresholdValue = 3
293  detectionTask = SourceDetectionTask(config=config, schema=schema)
294  # And the measurement Task
295  config = DipoleMeasurementTask.ConfigClass()
296  config.plugins.names.remove('base_SkyCoord')
297  algMetadata = dafBase.PropertyList()
298  measureTask = DipoleMeasurementTask(schema, algMetadata, config=config)
299 
300  Having fully initialied the schema, we create a Source table from it:
301 
302  .. code-block:: py
303 
304  # Create the output table
305  tab = afwTable.SourceTable.make(schema)
306 
307  Run detection:
308 
309  .. code-block:: py
310 
311  # Process the data
312  results = detectionTask.run(tab, exposure)
313 
314  Because we are looking for dipoles, we need to merge the positive and negative detections:
315 
316  .. code-block:: py
317 
318  # Merge the positve and negative sources
319  fpSet = results.fpSets.positive
320  growFootprint = 2
321  fpSet.merge(results.fpSets.negative, growFootprint, growFootprint, False)
322  diaSources = afwTable.SourceCatalog(tab)
323  fpSet.makeSources(diaSources)
324  print("Merged %s Sources into %d diaSources (from %d +ve, %d -ve)" % (len(results.sources),
325  len(diaSources),
326  results.fpSets.numPos,
327  results.fpSets.numNeg))
328 
329  Finally, perform measurement (both standard and dipole-specialized) on the merged sources:
330 
331  .. code-block:: py
332 
333  measureTask.run(diaSources, exposure)
334 
335  Optionally display debugging information:
336 
337  .. code-block:: py
338 
339  # Display dipoles if debug enabled
340  if args.debug:
341  dpa = DipoleAnalysis()
342  dpa.displayDipoles(exposure, diaSources)
343 
344  """
345  ConfigClass = DipoleMeasurementConfig
346  _DefaultName = "dipoleMeasurement"
347 
348 
349 
352 
354  """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad."""
355 
356  def __init__(self, sources, badFlags=None):
357  self.badFlags = ['base_PixelFlags_flag_edge', 'base_PixelFlags_flag_interpolatedCenter',
358  'base_PixelFlags_flag_saturatedCenter']
359  if badFlags is not None:
360  for flag in badFlags:
361  self.badFlags.append(flag)
362  self.keys = [sources.getSchema().find(name).key for name in self.badFlags]
363  self.keys.append(sources.table.getCentroidFlagKey())
364 
365  def __call__(self, source):
366  """Call the source flag checker on a single Source
367 
368  Parameters
369  ----------
370  source :
371  Source that will be examined
372  """
373  for k in self.keys:
374  if source.get(k):
375  return False
376  return True
377 
378 
380  """Functor class that provides (S/N, position, orientation) of measured dipoles"""
381 
382  def __init__(self):
383  pass
384 
385  def __call__(self, source):
386  """Parse information returned from dipole measurement
387 
388  Parameters
389  ----------
390  source : `lsst.afw.table.SourceRecord`
391  The source that will be examined"""
392  return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
393 
394  def getSn(self, source):
395  """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe
396 
397  Parameters
398  ----------
399  source : `lsst.afw.table.SourceRecord`
400  The source that will be examined"""
401 
402  posflux = source.get("ip_diffim_PsfDipoleFlux_pos_instFlux")
403  posfluxErr = source.get("ip_diffim_PsfDipoleFlux_pos_instFluxErr")
404  negflux = source.get("ip_diffim_PsfDipoleFlux_neg_instFlux")
405  negfluxErr = source.get("ip_diffim_PsfDipoleFlux_neg_instFluxErr")
406 
407  # Not a dipole!
408  if (posflux < 0) is (negflux < 0):
409  return 0
410 
411  return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
412 
413  def getCentroid(self, source):
414  """Get the centroid of the dipole; average of positive and negative lobe
415 
416  Parameters
417  ----------
418  source : `lsst.afw.table.SourceRecord`
419  The source that will be examined"""
420 
421  negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
422  negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
423  posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
424  posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
425  if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
426  return None
427 
428  center = afwGeom.Point2D(0.5*(negCenX+posCenX),
429  0.5*(negCenY+posCenY))
430  return center
431 
432  def getOrientation(self, source):
433  """Calculate the orientation of dipole; vector from negative to positive lobe
434 
435  Parameters
436  ----------
437  source : `lsst.afw.table.SourceRecord`
438  The source that will be examined"""
439 
440  negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
441  negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
442  posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
443  posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
444  if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
445  return None
446 
447  dx, dy = posCenX-negCenX, posCenY-negCenY
448  angle = afwGeom.Angle(np.arctan2(dx, dy), afwGeom.radians)
449  return angle
450 
451  def displayDipoles(self, exposure, sources):
452  """Display debugging information on the detected dipoles
453 
454  Parameters
455  ----------
456  exposure : `lsst.afw.image.Exposure`
457  Image the dipoles were measured on
458  sources : `lsst.afw.table.SourceCatalog`
459  The set of diaSources that were measured"""
460 
461  import lsstDebug
462  display = lsstDebug.Info(__name__).display
463  displayDiaSources = lsstDebug.Info(__name__).displayDiaSources
464  maskTransparency = lsstDebug.Info(__name__).maskTransparency
465  if not maskTransparency:
466  maskTransparency = 90
467  ds9.setMaskTransparency(maskTransparency)
468  ds9.mtv(exposure, frame=lsstDebug.frame)
469 
470  if display and displayDiaSources:
471  with ds9.Buffering():
472  for source in sources:
473  cenX, cenY = source.get("ipdiffim_DipolePsfFlux_centroid")
474  if np.isinf(cenX) or np.isinf(cenY):
475  cenX, cenY = source.getCentroid()
476 
477  isdipole = source.get("classification.dipole")
478  if isdipole and np.isfinite(isdipole):
479  # Dipole
480  ctype = "green"
481  else:
482  # Not dipole
483  ctype = "red"
484 
485  ds9.dot("o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame)
486 
487  negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x")
488  negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y")
489  posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x")
490  posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y")
491  if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)):
492  continue
493 
494  ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype="yellow", frame=lsstDebug.frame)
495 
496  lsstDebug.frame += 1
497 
498 
500  """Functor to deblend a source as a dipole, and return a new source with deblended footprints.
501 
502  This necessarily overrides some of the functionality from
503  meas_algorithms/python/lsst/meas/algorithms/deblend.py since we
504  need a single source that contains the blended peaks, not
505  multiple children sources. This directly calls the core
506  deblending code deblendBaseline.deblend (optionally _fitPsf for
507  debugging).
508 
509  Not actively being used, but there is a unit test for it in
510  dipoleAlgorithm.py.
511  """
512 
513  def __init__(self):
514  # Set up defaults to send to deblender
515 
516  # Always deblend as Psf
517  self.psfChisqCut1 = self.psfChisqCut2 = self.psfChisqCut2b = np.inf
518  self.log = Log.getLogger('ip.diffim.DipoleDeblender')
519  self.sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
520 
521  def __call__(self, source, exposure):
522  fp = source.getFootprint()
523  peaks = fp.getPeaks()
524  peaksF = [pk.getF() for pk in peaks]
525  fbb = fp.getBBox()
526  fmask = afwImage.Mask(fbb)
527  fmask.setXY0(fbb.getMinX(), fbb.getMinY())
528  fp.spans.setMask(fmask, 1)
529 
530  psf = exposure.getPsf()
531  psfSigPix = psf.computeShape().getDeterminantRadius()
532  psfFwhmPix = psfSigPix * self.sigma2fwhm
533  subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=True)
534  cpsf = deblendBaseline.CachingPsf(psf)
535 
536  # if fewer than 2 peaks, just return a copy of the source
537  if len(peaks) < 2:
538  return source.getTable().copyRecord(source)
539 
540  # make sure you only deblend 2 peaks; take the brighest and faintest
541  speaks = [(p.getPeakValue(), p) for p in peaks]
542  speaks.sort()
543  dpeaks = [speaks[0][1], speaks[-1][1]]
544 
545  # and only set these peaks in the footprint (peaks is mutable)
546  peaks.clear()
547  for peak in dpeaks:
548  peaks.append(peak)
549 
550  if True:
551  # Call top-level deblend task
552  fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
553  log=self.log,
554  psfChisqCut1=self.psfChisqCut1,
555  psfChisqCut2=self.psfChisqCut2,
556  psfChisqCut2b=self.psfChisqCut2b)
557  else:
558  # Call lower-level _fit_psf task
559 
560  # Prepare results structure
561  fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.log)
562 
563  for pki, (pk, pkres, pkF) in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
564  self.log.debug('Peak %i', pki)
565  deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.log,
566  cpsf, psfFwhmPix,
567  subimage.getMaskedImage().getImage(),
568  subimage.getMaskedImage().getVariance(),
569  self.psfChisqCut1, self.psfChisqCut2, self.psfChisqCut2b)
570 
571  deblendedSource = source.getTable().copyRecord(source)
572  deblendedSource.setParent(source.getId())
573  peakList = deblendedSource.getFootprint().getPeaks()
574  peakList.clear()
575 
576  for i, peak in enumerate(fpres.deblendedParents[0].peaks):
577  if peak.psfFitFlux > 0:
578  suffix = "pos"
579  else:
580  suffix = "neg"
581  c = peak.psfFitCenter
582  self.log.info("deblended.centroid.dipole.psf.%s %f %f",
583  suffix, c[0], c[1])
584  self.log.info("deblended.chi2dof.dipole.%s %f",
585  suffix, peak.psfFitChisq / peak.psfFitDof)
586  self.log.info("deblended.flux.dipole.psf.%s %f",
587  suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
588  peakList.append(peak.peak)
589  return deblendedSource
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
A class representing an angle.
Definition: Angle.h:127
Definition: Log.h:691
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:78
def __init__(self, config, name, schema, metadata)