Pending Footprints API change

Footprint API change

This post serves as an announcement that in a few hours a ticket will be merged which will introduce a new design for the Footprint class in afw, as discussed in RFC-37. The change this merge introduces is a breaking change to the Footprints API. If you have scripts which use the old API they will almost certainly need updating. The w_2017_17 tag of the LSST software stack was created prior to the merge, and can be used until such a time as your scripts can be updated to the new API.

What is new

The total number of changes is larger than what is reasonable to describe in a community post, but the core ideas are as follows.

  • The spatial functionality of a Footprint has been moved into a new class
    called SpanSet which lives in afw geom.
  • A Footprint becomes a container for a SpanSet, and a PeakCatalog (which has
    largely not changed).
  • Some convenience methods exist in Footprint which forwards calls to the
    underlying SpanSet, but much of the familiar functionality will be accessed
    from the Footprint`s SpanSet attribute. See the API for details
  • The grow method is renamed dilate and happens in place
  • The shrink method is renamed erode and happens in place
  • In places where locality, and not peaks, is important it is expected that the
    SpanSet class can be used in place of a Footprint
  • More Pythonic API, and behavior.

What this means for me

If you commonly use the stack, but do not work directly with Footprints this change will likely not be noticed. If you encounter and use Footprints often, you will need to learn the new API.

Where can I get more info

A complete API reference can be found within the doxygen documentation for the Footprint and SpanSet classes. Higher level documentation with examples can be found here <https://afw.lsst.io/>_. NOTE This is a temporary url, eventually it will be removed as afw docs will migrate to pipelines.lsst.io as more afw documentation is added. Additional examples covering all class methods (for both Python and C++) can be found in the unit tests, for those who learn best from reading code.

1 Like

You say that there are breaking changes. Can you point us at documentation of all python-visible changes (this isn’t quite the same as an API document, welcome though that is)?

At the behest of the good, here is a quick summary:

Footprint Migration Guide

This is a brief guide to help migrating code from the old footprint API to the new API in a one to one way. This guide does not guarantee this is the best way to code using the new API, or that it an optimal way to map from old to new. This is merely meant to be a demonstration. Because of this, the syntax below occasionally looks clunky. In particular, it is often best to work with SpanSets directly, and only convert to a full Footprint at the end of a manipulation (and when peak information is actually needed). This guide does not outline any of the new functionality outside what is needed to show the API mapping. There is much more functionality that can be found in the guide and API documentation. Utilizing new functionality, and new styles of coding made available by the update will often result in different coding patterns than before.

Where there are default arguments, they are listed as assigned to a function argument.

Before:

Footprint(bbox, region=Box2I())

After:

Footprint(SpanSet(bbox), region=Box2I())

Before:

Footprint(Schema, nspan=0, region=Box2I())

After:

Footprint()
Footprint.setSchema(Schema)

Before:

Footprint(center, radius, region=Box2I())

After:

ss = SpanSet.fromShape(radius, stencil=Stencil.CIRCLE, offset=center)
Footprint(ss, region=Box2I())

Before:

Footprint(ellipse, region=Box2I())

After:

ss = SpanSet.fromShape(ellipse)
Footprint(ss, region=Box2I())

Before:

Footprint(spans, region=Box2I())

After:

Footprint(SpanSet(spans), region=Box2I())

Before:

Footprint()

After:

Footprint()

Before:

foot.intersectMask(mask)

# Or

foot.intersectMask(mask, bitmask)

After:

newSS = foot.spans.interectNot(mask) # Selects any pixel with a bit set
foot.spans = newSS

# Or

newSS = foot.spans.intesectNot(mask, bitmask)
foot.spans = newSS

Before:

foot.isHeavy()

After:

foot.isHeavy()

Before:

foot.getId()

After:

N/A

Before:

foot.getSpans()

After:

foot.spans

Before:

foot.getPeaks()

After:

foot.peaks

Before:

foot.addPeak(peak)

After:

foot.addPeak(floatX, floatY, floatValue)

Before:

foot.sortPeaks()

After:

foot.sortPeaks()

Before:

foot.setPeakSchema(schema)

After:

foot.setPeakScheam(schema)

Before:

foot.getNpix()

After:

foot.getArea()

Before:

foot.getArea()

After:

foot.getArea()

Before:

foot.getCentroid()

After:

foot.getCentroid()

Before:

foot.getShape()

After:

foot.getShape()

Before:

foot.addSpan(y, x0, x1)

After:

spanList = [s for s in foot.spans]
spanList.append(Span(y, x0, x1))
foot.spans = SpanSet(spanList)

Before:

foot.shift(x, y)

After:

foot.shift(x, y)

Before:

foot.getBBox()

After:

foot.getBBox()

Before:

foot.getRegion()

After:

foot.getRegion()

Before:

foot.setRegion(region)

After:

foot.setRegion(region)

Before:

foot.clipTo(bbox)

After:

foot.clipTo(bbox)

Before:

foot.contains(point2I)

After:

foot.contains(point2I)

Before:

foot.normalize()

After:

N/A

Before:

foot.isNormalized()

After:

N/A

Before:

foot.transform(transformation)

After:

foot.transform(transformation)

Before:

out = foot.findEdgePixels()

After:

out = foot.spans.findEdgePixels()
# If a footprint is desired
out = Footprint(out)

Before:

foot.include([foot2])

After:

foot.spans = foot.spans.union(foot2.spans)

Before:

foot.insertIntoImage(image, value, region=Box2I())

After:

foot.spans.setImage(image, value)

Before:

foot.overlapsMask(mask)

After:

newSS = foot.spans.intersect(mask)
foot.spans = newSS

# Or

newSS = foot.spans.intersect(mask, bitmask)
foot.spans = newSS

Before:

foot.clipToNonzero(Image)

After:

from lsst.meas.deblender.plugins import clipFootprintToNonzeroImpl
clipFootprintToNonzeroImpl(foot, image)

Before:

setImageFromFootprint(image, foot, value)

After:

foot.spans.setImage(image, value)

Before:

setImageFromFootprintList(image, footprintList, value)

After:

for foot in footprintList:
    foot.spans.setImage(image, value)

Before:

copyWithinFootprintImage(foot, src, dest)

After:

foot.spans.copyImage(src, dest)

Before:

copyWithinFootprintMaskedImage(foot, src, dest)
foot.spans.copyMaskedImage(src, dest)

Before:

nearestFootprint(footprintList, image, distanceImage)
N/A

Before:

foot3 = mergeFootprints(foot1, foot2)

After:

foot3 = mergeFootprints(foot1, foot2)

Before:

foot2 = shrinkFootprint(foot1, nGrow, isotropic)

After:

newSS = foot1.spans.eroded(nGrow, Stencil.CIRCLE) # isotropic (CIRCLE is the dfault)
newSS = foot1.spans.eroded(nGrow, Stencil.MANHATTAN) # non isotropic
foot2 = foot1.assign() # copy the old footprint
foot2.spans = newSS

Before:

foot2 = growFootprint(foot1, nGrow, isotropic=True)

After:

newSS = foot1.spans.dilated(nShrink, Stencil.CIRCLE) # isotropic (CIRCLE is the default)
newSS = foot1.spans.dilated(nShrink, Stencil.MANHATTAN) # non isotropic
foot2 = foot1.assign() # copy the old footprint
foot2.spans = newSS

Before:

out = footprintToBBoxList(foot)

After:

out = footprintTOBBoxList(foot)

Before:

setMaskFromFootprint(mask, foot, value)

After:

foot.spans.setMask(mask, value)

Before:

clearMaskFromFootprint(mask, foot, value)

After:

foot.spans.clearMask(mask, value)

Before:

setMaskFromFootprintList(mask, footprintList, value)

After:

setMaskFromFootprintList(mask, footprintList, value, doClip=True)

Before:

for span in foot.getSpans()
    y = span.getY()
    for x in range(span.getX0(), span.getX1()):
        array[y, x] += 1

After:


yind, xind = foot.spans.indices()
array[yind, xind] += 1

# Or

for span in foot.spans:
    y = span.getY()
    for x in range(span.getX0(), span.getX1()):
        array[y, x] += 1
1 Like