From 2c510c85da8d81461e4301ac97ed0a201eeb43d3 Mon Sep 17 00:00:00 2001 From: Robert Lupton the Good Date: Wed, 7 Aug 2019 15:13:33 -0500 Subject: [PATCH] Added some camera geometry functionality for camera team In particular, mappings between amp/ccd/focal plane coordinates using an LsstCameraTransforms object, but also getAmpImage and channelToAmp free functions --- policy/cameraHeader.yaml | 2 +- python/lsst/obs/lsst/cameraTransforms.py | 35 ++++++-------- tests/test_lsstCam.py | 59 +++++++++++++++++++++++- 3 files changed, 72 insertions(+), 24 deletions(-) diff --git a/policy/cameraHeader.yaml b/policy/cameraHeader.yaml index 992cb38c9..d9eb7492c 100644 --- a/policy/cameraHeader.yaml +++ b/policy/cameraHeader.yaml @@ -170,7 +170,7 @@ CCD_E2V : &CCD_E2V CCD_ITL : &CCD_ITL detectorType : 0 - refpos : [2036.5, 2000.5] + refpos : [2035.5, 1999.5] offset : [.nan, .nan] # This is the orientation we need to put the serial direction along the x-axis bbox : [[0, 0], [4071, 3999]] diff --git a/python/lsst/obs/lsst/cameraTransforms.py b/python/lsst/obs/lsst/cameraTransforms.py index 43616ba8f..065588d25 100644 --- a/python/lsst/obs/lsst/cameraTransforms.py +++ b/python/lsst/obs/lsst/cameraTransforms.py @@ -366,24 +366,18 @@ def ampPixelToCcdPixel(x, y, detector, channel): """ amp = channelToAmp(detector, channel) - rawBBox, rawDataBBox = amp.getRawBBox(), amp.getRawDataBBox() + bbox = amp.getRawDataBBox() + # Allow for flips (due e.g. to physical location of the amplifiers) - w, h = rawBBox.getDimensions() + x, y = geom.PointI(x, y) # definitely ints + w, h = bbox.getDimensions() if amp.getRawFlipX(): - rawBBox.flipLR(w) - rawDataBBox.flipLR(w) - - x = rawBBox.getWidth() - x - 1 + x = w - x - 1 if amp.getRawFlipY(): - rawBBox.flipTB(h) - rawDataBBox.flipTB(h) - - y = rawBBox.getHeight() - y - 1 - - dxy = rawBBox.getBegin() - rawDataBBox.getBegin() # correction for overscan etc. + y = h - y - 1 - return amp.getBBox().getBegin() + dxy + geom.ExtentI(x, y) + return amp.getBBox().getBegin() + geom.ExtentI(x, y) def ccdPixelToAmpPixel(xy, detector): @@ -411,31 +405,28 @@ def ccdPixelToAmpPixel(xy, detector): RuntimeError If the requested pixel doesn't lie on the detector """ + xy = geom.PointI(xy) # use pixel coordinates found = False for amp in detector: - if geom.BoxD(amp.getBBox()).contains(xy): + if amp.getBBox().contains(xy): + x, y = xy - amp.getBBox().getBegin() # coordinates within data segment found = True - xy = geom.PointI(xy) # pixel coordinates as ints break if not found: raise RuntimeError("Point (%g, %g) does not lie on detector %s" % (xy[0], xy[1], detector.getName())) - x, y = xy - amp.getBBox().getBegin() # offset from origin of amp's data segment - # Allow for flips (due e.g. to physical location of the amplifiers) - w, h = amp.getRawDataBBox().getDimensions() + w, h = amp.getBBox().getDimensions() + if amp.getRawFlipX(): x = w - x - 1 if amp.getRawFlipY(): y = h - y - 1 - dxy = amp.getRawBBox().getBegin() - amp.getRawDataBBox().getBegin() # correction for overscan etc. - xy = geom.ExtentI(x, y) - dxy - - return amp, xy + return amp, geom.PointI(x, y) def focalMmToCcdPixel(camera, focalPlaneXY): diff --git a/tests/test_lsstCam.py b/tests/test_lsstCam.py index c450eb4b2..927deebd3 100644 --- a/tests/test_lsstCam.py +++ b/tests/test_lsstCam.py @@ -24,8 +24,9 @@ import unittest import lsst.utils.tests -from lsst.geom import arcseconds, Extent2I +from lsst.geom import arcseconds, Extent2I, PointD, PointI import lsst.afw.image +from lsst.obs.lsst.cameraTransforms import LsstCameraTransforms from lsst.obs.lsst.testHelper import ObsLsstButlerTests, ObsLsstObsBaseOverrides @@ -162,6 +163,62 @@ def testDetectorName(self): with self.assertRaises(RuntimeError): self.mapper._extractDetectorName({'visit': 1}) + def testCameraTransforms(self): + """Test the geometry routines requested by the camera team + + These are found in cameraTransforms.py""" + + camera = self.butler.get('camera', immediate=True) + + raft = 'R22' + sensor = 'S11' + ccdid = f"{raft}_{sensor}" + + lct = LsstCameraTransforms(camera) + + # check that we can map ccd pixels to amp pixels + for cxy, apTrue in [ + ((0, 0), (1, 508, 0)), # noqa: E241 + ((509, 0), (2, 508, 0)), + ]: + ap = lct.ccdPixelToAmpPixel(*cxy, ccdid) + self.assertEqual(ap, apTrue) + + # check inverse mapping + for ap, cpTrue in [ + ((508, 0, 1), (0, 0)), + ((0, 0, 9), (4071, 3999)), + ]: + cp = lct.ampPixelToCcdPixel(*ap, ccdid) + self.assertEqual(cp, PointI(*cpTrue)) + + # check round-tripping + ampX, ampY, channel = 2, 0, 1 + cx, cy = lct.ampPixelToCcdPixel(ampX, ampY, channel, ccdid) + finalChannel, finalAmpX, finalAmpY = lct.ccdPixelToAmpPixel(cx, cy, ccdid) + self.assertEqual((finalAmpX, finalAmpY, finalChannel), (ampX, ampY, channel)) + + # Check that four amp pixels near the camera's centre are + # indeed close in focal plane coords + for ap, fpTrue in [ + ((508, 1999, 5), ( 0.005, -0.005)), # noqa: E201,E241 + ((0, 1999, 4), (-0.005, -0.005)), # noqa: E201,E241 + ((0, 1999, 13), (-0.005, 0.005)), # noqa: E201,E241 + ((508, 1999, 12), ( 0.005, 0.005)), # noqa: E201,E241 + ]: + fp = lct.ampPixelToFocalMm(*ap, ccdid) + self.assertAlmostEqual((fp - PointD(*fpTrue)).computeNorm(), 0.0) + + # and for ccd coordinates: + for cp, fpTrue in [ + ((2035, 1999), (-0.005, -0.005)), # noqa: E201,E241 + ((2036, 1999), ( 0.005, -0.005)), # noqa: E201,E241 + ((2035, 2000), (-0.005, 0.005)), # noqa: E201,E241 + ((2036, 2000), ( 0.005, 0.005)), # noqa: E201,E241 + ]: + fp = lct.ccdPixelToFocalMm(*cp, ccdid) + self.assertAlmostEqual((fp - PointD(*fpTrue)).computeNorm(), 0.0) + class MemoryTester(lsst.utils.tests.MemoryTestCase): pass