Skip to content

Commit

Permalink
Handle empty variance array gracefully.
Browse files Browse the repository at this point in the history
SingleFrameVariancePlugin attempts to compute the median of the
variance array for pixels in the heavy footprint of a source. Some
pixels are ignored because mask flags are set indicating that the pixel
is not to be trusted. Sometimes, this results in all pixels getting
rejected at which point numpy raises a RuntimeWarning stating that it
is trying to compute the median of an empty array. We check for the
empty array and raise a MeasurementError if all the pixels are flagged.
The testVariance unittest has been modified to test the new behavior.
  • Loading branch information
AstroVPK committed May 17, 2016
1 parent 932fedb commit daa3b4d
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 5 deletions.
18 changes: 16 additions & 2 deletions python/lsst/meas/base/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class SingleFrameVariancePlugin(SingleFramePlugin):
'''
ConfigClass = SingleFrameVarianceConfig
FAILURE_BAD_CENTROID = 1
FAILURE_EMPTY_FOOTPRINT = 2

@classmethod
def getExecutionOrder(cls):
Expand All @@ -174,6 +175,8 @@ def __init__(self, config, name, schema, metadata):
SingleFramePlugin.__init__(self, config, name, schema, metadata)
self.varValue = schema.addField(name + '_value', type="D", doc="Variance at object position")
self.varFlag = schema.addField(name + '_flag', type="Flag", doc="Set to True for any fatal failure")
self.emptyFootprintFlag = schema.addField(name + '_flag_emptyFootprint', type="Flag",
doc="Set to True when the footprint has no usable pixels")

# Alias the badCentroid flag to that which is defined for the target of the centroid slot.
# We do not simply rely on the alias because that could be changed post-measurement.
Expand All @@ -198,10 +201,21 @@ def measure(self, measRecord, exposure):
# Compute the median variance value for each pixel not excluded by the mask and write the record.
# Numpy median is used here instead of afw.math makeStatistics because of an issue with data types
# being passed into the C++ layer (DM-2379).
medVar = numpy.median(pixels.getVarianceArray()[logicalMask])
measRecord.set(self.varValue, medVar)
if numpy.any(logicalMask):
medVar = numpy.median(pixels.getVarianceArray()[logicalMask])
measRecord.set(self.varValue, medVar)
else:
raise bl.MeasurementError("Footprint empty, or all pixels are masked, can't compute median",
self.FAILURE_EMPTY_FOOTPRINT)

def fail(self, measRecord, error=None):
# Check that we have a error object and that it is of type MeasurementError
if isinstance(error, bl.MeasurementError):
assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_EMPTY_FOOTPRINT)
# FAILURE_BAD_CENTROID handled by alias to centroid record.
if error.getFlagBit() == self.FAILURE_EMPTY_FOOTPRINT:
measRecord.set(self.emptyFootprintFlag, True)
measRecord.set(self.varValue, numpy.nan)
measRecord.set(self.varFlag, True)

class SingleFrameInputCountConfig(SingleFramePluginConfig):
Expand Down
41 changes: 38 additions & 3 deletions tests/testVariance.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@


class VarianceTest(unittest.TestCase):
def testVariance(self):

def setUp(self):
size = 128 # size of image (pixels)
center = afwGeom.Point2D(size//2, size//2) # object center
width = 2.0 # PSF width
Expand Down Expand Up @@ -123,9 +124,42 @@ def testVariance(self):

source = catalog.addNew()
source.setFootprint(foot)
task.run(catalog, exp)

self.assertTrue(np.abs(source.get("base_Variance_value") - variance) < varianceStd)
self.variance = variance
self.varianceStd = varianceStd
self.mask = mask
self.catalog = catalog
self.exp = exp
self.task = task
self.source = source

def tearDown(self):
del self.mask
del self.catalog
del self.exp
del self.task
del self.source

def testVariance(self):
self.task.run(self.catalog, self.exp)

self.assertTrue(np.abs(self.source.get("base_Variance_value") - self.variance) < self.varianceStd)

# flag_emptyFootprint should not have been set since the footprint has non-masked pixels at this
# point.
self.assertFalse(self.source.get("base_Variance_flag_emptyFootprint"))

def testEmptyFootprint(self):
# Set the pixel mask for all pixels to 'BAD' and remeasure.
self.mask.getArray()[:,:] = self.mask.getPlaneBitMask("BAD")
self.task.run(self.catalog, self.exp)

# The computed variance should be nan and flag_emptyFootprint should have been set since the footprint
#has all masked pixels at this point.
self.assertTrue(np.isnan(self.source.get("base_Variance_value")))
self.assertTrue(self.source.get("base_Variance_flag_emptyFootprint"))

class BadCentroidTest(unittest.TestCase):

def testBadCentroid(self):
"""
Expand Down Expand Up @@ -168,6 +202,7 @@ def suite():

suites = []
suites += unittest.makeSuite(VarianceTest)
suites += unittest.makeSuite(BadCentroidTest)
suites += unittest.makeSuite(utilsTests.MemoryTestCase)
return unittest.TestSuite(suites)

Expand Down

0 comments on commit daa3b4d

Please sign in to comment.