Note
This document captures the Winter 2014 design work on the camera geometry (“CamGeom”) system. For current documentation, see the representation of a camera and the CamGeom documentation.
1 Third party packages¶
None.
3 Requirements¶
- The implementation will be in Python where possible. There will be a (hopefully non-polymorphic) class in C++ to hold the coordinate transforms and other information associated with the detectors and needed in C++. This will eliminate any downcasting in Python.
- The design will define a set of coordinate systems and a way to transform between them.
- The standard set of coordinates systems will be extensible so that other coordinate systems (and accompanying transforms) can be defined.
- The design must, at a minimum, support LSST, HSC, Suprime-Cam, MegaCam, SDSS, and DECam.
- The design will be hierarchical with a top level camera object containing detector objects.
- The hierarchy will be extensible so that cameras with intermediate levels of sensor grouping (LSST rafts) can iterate over any level.
- The position and orientation of every sensor may be specified in all 6 axes.
- Detectors will be iterable from the camera level. Every component will be retrievable via index or slot name. Component identifiers will contain vendor and serial information.
- Wavelength-dependent coordinate transforms will be supported, but only minimally, via band-dependent coordianate transforms. Monochromatic coordinate transforms (where wavelength is a numeric input) are out of scope because we don’t yet understand the use cases well enough to support them.
- Camera objects will be persistable.
- The design will provide simple tools for visualizing the camera geometry.
- The design will make it easy to handle raw data and assembled data and hard to confuse the two. We wish to minimize or eliminate the need for flags that indicate if a bbox is in raw or assembled coordinates and whether data has been trimmed.
4 Coordinate Systems¶
The following coordinate systems are expected to be supported by every project:
pupil
: x,y radians relative to the optical axisfocalPlane
: x,y mm in the focal plane.pixels
: nominal x,y unbinned pixels on the entry surface of the detector.
Projects may wish to support additional coordinate systems, such as:
distortedPixels
: x,y unbinned pixel, taking into account such effects as tree rings.- pupil-like systems at various optical elements (filters, lenses).
Note
Detectors will be positioned with 6 degrees of freedom in the focal plane. The actual position of each detector may be reflected in the transformation between pixels and focalPlane.
5 Classes and API¶
Note: these are written in pseudocode; details such as pointers (unless essential), references and const are intentionally omitted. All classes are in lsst.afw.geom
or lsst.afw.cameraGeom
unless otherwise noted, and all new classes are in the latter, except TransformMap
.
5.1 TransformMap
: C++ (in lsst.afw.geom
)¶
The transform map is a map of CoordSys:XYTransform
, where CoordSys
is a template parameter.
Each map will have a native (aka “reference”) coordinate system, and each transform transforms Point2D
objects from the native system to the named system in the forward direction, and supports the reverse transform.
CoordSys
will be used as a key in some kind of map (likely std::map
until SWIG can wrap unordered_map
), so it will have to follow the appropriate rules for that kind of mapping.
A transform method will transform between any two supported coordinate systems (by transforming the input to the native coordinate system and then transforming that to the output coordinate system).
A variant that can efficiently transform a list of points will also be provided.
To take advantage of caching possibilities, that method will require all input points be in the same coordinate system, so the input is a list of Point2D
instead of CameraPoint
.
The output is also a list of a list of Point2D
, so a list of points can be transformed multiple times.
If chromatic transforms are desired (e.g. pupil↔︎focalPlane may have a slow color dependence) then the color information must be encoded in the coordinate system.
5.1.1 API¶
TransformMap(CoordSys nativeCoordSys, {CoordSys: XYTransform} transformMap)
- construct a
TransformMap
. Adds a unity native→native transform if a native transform is not provided. transform(Point2D point, CoordSys fromCoordSys, CoordSys toCoordSys) → Point2D
- transform a point from one coordinate system to another
transform(afwGeom.Point2D[] pointList, CoordSys fromCoordSys, CoordSys toCoordSys) → Point2D[]
- transform a list of points from one coordinate system to another
getNativeCoordSys() → CoordSys
- get native coordinate system
[CoordSys coordSys] → XYTransform
- get an
XYTransform
by coordinate system; the returned transform transforms from native coordinates to the specifiedCoordSys
in the forward direction contains(coordSys) → bool
- is there an
XYTransform
for the specified coordinate system? (Renamed__contains__
in Python socoordSys in transformMap
works) getCoordSysList() → CoordSys[]
- get list of supported coordinate systems
size() → int
- get number of transforms (including native->native transform) (renamed
__len__
in Python solen(transformMap)
works)
Iteration over the transform mapping will be supported using begin
/end
in C++ and something suitable in Python.
5.2 AmpInfoCatalog
, AmpInfoTable
and AmpInfoRecord
: C++¶
An afw table of amplifier information, including bounding box, gain, read noise, linearity and sufficient information about raw amplifiers to allow assembling images from raw images. In cases where we use post-ISR data as input (such as our present use of SDSS data) the raw amplifier information may be omitted.
Using an afw table allows users to extend the schema to add additional amplifier-specific information that is needed for a particular camera.
There will be setters and getters for each of the following fields:
5.2.1 Fields¶
name
- name of amplifier (each amp in a detector must have a unique name, to allow lookup by name)
bbox
- bounding box of amp image on assembled image
gain
- amplifier gain in e-/ADU
readNoise
- amplifier read noise, in e-
saturation
- saturation value, in ADU
readoutCorner
- readout corner (an enum; one of
LL
,LR
,UR
,UL
) linearityCoeffs
- linearity coefficients
linearityType
- name of linearity algorithm
hasRawInfo
- has raw amp information been provided?
rawBBox
- bounding box of all pixels on raw image
rawDataBBox
- bounding box of data on raw image
rawFlipX
- flip X axis when assembling an image?
rawFlipY
- flip Y axis when assembling an image?
rawXYOffset
offset for assembling a raw CCD image: desired
xy0
- rawxy0
.This offset is NOT used by ISR; it is primarily for display utilities; it supports construction of a raw CCD image in the case that raw data is provided as individual amplifier images.
Use
0,0
for cameras that supply raw data as a raw CCD image (most cameras).Use nonzero for LSST and other cameras that supply raw data as separate amp images with
xy0=0,0
.rawHorizontalOverscanBBox
bounding box of valid pixels of horizontal overscan on raw image
By “valid pixels” we mean to excluded pixels likely to contain electronic artifacts and thus make the data unusable for image processing.
rawVerticalOverscanBBox
- bounding box of valid pixels of vertical overscan on raw image
rawPrescanBBox
- bounding box of valid pixels of (horizontal) prescan on raw image
Here is a pictorial example for flipX
, flipY
:
CCD with 4 amps Desired assembled output Use these parameters
--x x-- y
| amp1 amp2 | | flipX flipY
y y | amp1 False True
| CCD image amp2 True True
y y | amp3 False False
| amp3 amp4 | | amp4 True False
--x x-- ----------- x
This assumes assembled X is always +/- raw X, which is true for CCDs. If some exotic future detector wants to swap X/Y axes then we can add a doTranspose
flag.
5.3 CameraSysPrefix
: C++¶
An incomplete camera coordinate system that only has a coordinate system name (no detector name).
This is used by Detector.getCameraSys
to expand a detector coordinate prefix into a full CameraSys
(at Jim Bosch’s excellent suggestion).
A constant will be provided for each detector-specific coordinate system, including PIXELS
and ACTUAL_PIXELS
(non-detector-specific coordinate systems are handled by CameraSys
).
5.3.1 API¶
CameraSysPrefix(sysName)
- construct a
CameraSysPrefix
getSysName() → string
- coordinate system name, e.g.
"pixels"
operator==(CameraSysPrefix rhs) → bool
- equality operator
operator!=(CameraSysPrefix rhs) →bool
- inequality operator
5.4 CameraSys
: C++¶
Class for camera-based coordinate systems; used as the key for TransformMap
in Detector
and Camera
.
A constant will be provided for each non-detector-specific coordinate system, including FOCAL_PLANE
and PUPIL
(CameraSysPrefix
is used for detector-specific coordinate system prefixes).
5.4.1 API¶
CameraSys(sysName, detectorName="")
- construct a
CameraSys
getSysName() → string
- coordinate system name, e.g. “pixels”
getDetectorName() → string
- detector name, e.g. “R11 S02”
hasDetectorName() → bool
- is detector name non-empty?
operator==(CameraSys rhs) → bool
- equality operator
operator!=(CameraSys rhs) → bool
- inequality operator
operator<(CameraSys rhs) → bool
- to support
std::map
(until we switch tostd::unordered_map
)
5.5 CameraPoint
: C++¶
A struct-ish class that holds:
point
- an
afwGeom.Point2D
cameraSys
- a
CameraSys
5.5.1 API¶
CameraPoint(afwGeom.Point2D point, CameraSys cameraSys)
getPoint() → afwGeom.Point2D point
getCameraSys() → CameraSys cameraSys
5.6 Orientation
: C++¶
Position, orientation in focal plane.
5.6.1 API¶
Orientation(afwGeom.Point3D fpPosition, afwGeom.Point2D refPoint, afwGeom.Angle yaw, afwGeom.Angle pitch, afwGeom.Angle roll)
- constructor
getFpPosition() → afwGeom.Point3D
- position of detector refPoint in focal plane (mm)
getRefPoint() → afwGeom.Point2D
- reference point on detector; offset is measured to this points and all rotations are about this point
getYaw() → afwGeom.Angle
- yaw: rotation about \(Z\) (\(X\) to \(Y\)), 1st rotation
getPitch() → afwGeom.Angle
- pitch: rotation about \(Y'\) (\(Z'\) = \(Z\) to \(X'\)), 2nd rotation
getRoll() → afwGeom.Angle
- roll: rotation about \(X''\) (\(Y''\) = \(Y'\) to \(Z''\)), 3rd rotation
getNQuarter() → int
- number of quarter rotations about focalPlane Z required to display pixels in a focalPlane mosaic
makeFpPixelTransform(float pixelSize) → XYTransform
- make a focalPlane->pixels transform
makePixelFpTransform(float pixelSize) → XYTransform
- make a pixels->focalPlane transform
5.7 DetectorType
: C++¶
An enum that identifies the detector type. Possible values are SCIENCE
, GUIDE
, FOCUS
and WAVEFRONT
.
5.8 Detector
: C++¶
Detector
holds amplifier information, coordinate transformations important for a detector, and some metadata about the detector including the detector name (which idenitifies the detector slot in the focal plane), detector type, and a serial ID (which idenitifies a particular CCD).
5.8.1 API¶
Detector(string name, int id, DetectorType detectorType, string serial, Box2I bbox, AmpInfoCatalog ampInfo, Orientation orientation, float pixelSize, TransformMap transformMap)
- constructor
getId() → int
- return the detector ID (for use as a key in database tables and such)
getName() → string
- return the detector name
getType() → DetectorType
- return the detector type
getSerial() → string
- serial “number” that identifies the physical detector
getBBox() → Box2I
- bounding box of amplifier image (pixels)
getCorners(CameraSys coordSys) → CameraPoint[4]
- that describe the extreme corners of the detector in the specified coordinate system
getCorners(CameraSysPrefix coordSysPrefix) → CameraPoint[4]
- same for
CameraSysPrefix
transform(CameraPoint cameraPoint, CameraSys toSys) → CameraPoint
- transform a point to a new coordinate system
transform(CameraPoint cameraPoint, CameraSysPrefix toSysPrefix) → CameraPoint
- same for
CameraSysPrefix
getCameraSys(CameraSys cameraSys) → cameraSys
- return the input unchanged, but check that the detector name matches
getCameraSys(CameraSysPrefix cameraSysPrefix) → CameraSys
- return a CameraSys with the detector’s name set
getCenter(CameraSys cameraSys) → CameraPoint
- get center of detector in specified coordinates
getCenter(CameraSysPrefix cameraSysPrefix) → CameraPoint
- same for
CameraSysPrefix
size() → amplifierList.size()`: renamed `__len__
- in python
[int index] → AmpInfoRecord
- get amp info by index
[string name] → AmpInfoRecord
- get amp info by amp name
getAmpInfoCatalog() → AmpInfoCatalog
- get amp info catalog
getOrientation() → Orientation
- get orientation
getPixelSize() → afwGeom:Point2D
- x, y pixel size (mm)
getTransformMap() → TransformMap
- return the transform map
makeCameraPoint(afwGeom:Point2D point, CameraSys cameraSys) → CameraPoint
- make a
CameraPoint
in the givenCameraSys
makeCameraPoint(afwGeom:Point2D point, CameraSysPrefix cameraSysPrefix) → CameraPoint
- make a
CameraPoint
in the givenCameraSysPrefix
5.9 DetectorCollection
: Python¶
A class to hold a collection of Detector
s.
It allows for iteration over all detectors in the collection as well as access by name or index.
Each Detector
must support the same coordinate systems (the constructor will enforce this).
5.9.1 API¶
DetectorCollection(list detectorList)
- constructor
__iter__() → iter(detectorList)
- implement iterator protocol
__len__() → len(detectorList)
- implement build in
len()
[int id] → Detector
- get detector by ID
[string name] → Detector
- get detector by name
getIdIter() → iter(int)
- iterator over detector IDs
getNameIter() → iter(str)
- iterator over detector names
getCollection(DetectorType detectorType) → DetectorCollection
- return a collection of detectors of the specified type; this makes it easy to iterate over all science detectors
getFpBBox() → afwGeom.Box2D
- get a bounding box that includes all detectors, in focal plane coordinates
5.10 Camera
: Python¶
A subclass of DetectorCollection
that also holds a camera TransformMap
.
Camera
has the ability to transform CameraPoints
to any coordinate system defined in either the camera TransformMap
or its detectors’ ConversionRegistries
.
Camera
will also provide a convenience function to quickly find the detectors that contain a specific CameraPoint
.
Camera
will also provide class methods to help in building Detector
s.
5.10.1 API¶
Camera(str name, list detectorList, TransformMap transformRegistry)
- constructor
getName() → str
- return the camera name
findDetectors(CameraPoint cameraPoint) → list of Detectors
- return a list of
Detector
s that contain the given point; typically returns 1 or 0 detectors but may return more if a camera has overlapping detectors getTransformMap() → TransformMap
- return the
TransformMap
of the camera transform(CameraPoint cameraPoint, CameraSys toCameraSys) → CameraPoint
return
cameraPoint
transformed to the new coordinate systemNote that
toCameraSys
must have the detector name filled out (if appropriate);transform()
will not search for a detector. CallfindDetectors()
to search.makeCameraPoint(afwGeom:Point2D point, string coordSys) → CameraPoint
- return a new
CameraPoint
, raising ifcoordSys
is not supported by the camera’s transform map (if you want a camera point for a particular detector, useDetector
)
6 Possible helper methods for constructing camera/detector objects¶
6.1 ConstructCameraTask
: Python¶
This will be a camera specific task that will create a full camera given a time from persisted data.
Following is a Python pseudocode implementation of a CameraFactoryTask
for an arbitrary camera.
There are many ways to read persisted camera data.
This implementation uses a sqlite database containing the detector information.
It implies the following schema:
Camera
Date
(DateTime
)CameraId
(int
)pincushion
(float
)plateScale
(float
)
SlotMap
CameraId
int
(foreign key onCamera
)DetectorId
int
(foreign key onDetector
)SlotName
varchar
SlotIndex
int
Detector
DetectorId
int
DetectorSerial
varchar
x
float
y
float
z
float
alpha
float
beta
float
gamma
float
pixelSize
float
AmpInfo
- see
AmpInfoCatalog
above
Following is code to make a camera given a database like the one above.
Note that the makeDetector
method has been reimplemented in this task.
It could also be defined in the Camera
class as I suggest in the API above.
import re
import sqlite3
import lsst.afw.geom as afwGeom
from lsst.awf.table import AmpInfoTable, AmpInfoCatalog
from lsst.afw.cameraGeom import Camera, Detector, DetectorType, Orientation, TransformMap
import lsst.pex.config as pexConfig
from datetime import datetime
class CameraFactoryConfig(pexConfig.Config):
repoFile = pexConfig.Field(doc="Name of sqlite file to read", dtype=str, default='ccdParams.sqlite')
class CameraFactoryTask(object):
configClass = CameraFactoryConfig
def __init__(self, config):
self.config = config
conn = sqlite3.connect(self.config.repoFile)
self.cur = conn.cursor()
cameraProps = self.queryCameraProperties(date)
self.cameraId = cameraProps['cameraId']
self.pincushion = cameraProps['pincushion']
self.plateScale = cameraProps['plateScale'] #arcsec/mm
def run(self, date=datetime.now()):
'''
Construct a camera from a database of detector descriptions given a date
@param date: A datetime object for the time of the camera
'''
detectorList = self.queryDetectors()
pupilTransform = Camera.makePupilFpTransform(self.plateScale, self.pincushion)
transformRegistry = TransformMap('focalplane', [('pupil', pupilTransform),])
return Camera(detectorList, transformRegistry)
def makeDetector(self, ampDictList, detectorType, x, y, z, alpha, beta, gamma,
pixelSize, slotName, slotIndex, detectorSerial, *args, **kwargs):
"""
Make a detector object:
@param ampDictList: A list of dictionaries, one per amp, that describe the amp properties.
@detectorType: Science, guiding, ...
@param x: X Position of detector (mm)
@param y: Y Position of detector (mm)
@param z: Z Position of detector (mm)
@param alpha: First Euler angle of the solid body rotation of the detector
@param beta: Second Euler angle of the solid body rotation of the detector
@param gamma: Third Euler angle of the solid body rotation of the detector
@param pixelSize: The size of pixels for this device (microns)
@param slotName: The name of the slot
@param slotIndex: Integer index of the slot
@param detectorSerial: The serial number of the specific detector.
"""
ampInfoSchema = AmpInfoTable.makeMinimalSchema()
ampInfoCatalog = AmpInfoCatalog(ampInfoSchema)
ccdBox = afwGeom.Box2I()
for ampDict in ampDictList:
record = ampInfoCatalog.addNew()
ccdBox.include(record.getBbox())
dt = DetectorType(detectorType)
position = afwGeom.Point3D(x, y, z)
orientation = Orientation(position, alpha, beta, gamma)
pixscale = self.plateScale*pixelSize/1000. #convet micron to mm
fpPixelTransform = Camera.makeFpPixelTransform(orientation, ccdBox, pixscale)
transformRegistry = TransformMap('pixel', [('focalplane', fpPixelTransform),])
return Detector(slotName, dt, detectorSerial, ampList, transformRegistry)
def setAmplifier(self, record, ampBbox, gain, readNoise,...):
"""
Make an amplifier object.
@param[in,out] record: record of AmpInfoCatalog to set
@param ampBbox: A string containing the bounding box of the full amp pixel grid
@param gain: gain
...
"""
record.setBBox(self.parseBBox(ampBbox, int))
record.set...
@staticmethod
def parseBBox(bboxStr, boxType=int):
"""Parse a bbox string assuming a format like the one used in FITS headers:
[xLL:yLL,xUR:yUR]
param boxType: int or float
"""
typeMap = {int:'I', float:'D'}
(x0, y0, x1, y1) = re.compile('[\[:,\]]').split(bboxStr[1:-1])
x0 = boxType(x0)
y0 = boxType(y0)
extx = boxType(x1) - x0
exty = boxType(y1) - y0
point = getattr(afwGeom, 'Point2%s'%(typeMap[boxType]))
extent = getattr(afwGeom, 'Extent2%s'%(typeMap[boxType]))
box = getattr(afwGeom, 'Box2%s'%(typeMap[boxType]))
return box(point(x0, y0), extent(extx, exty))
def queryCameraProperties(self, date):
'''
Query the database for the camera properties valid for the date.
@param data: datetime object for the date in question
@return A dictionary of camera properties
'''
dateTempl = "%04i-%02i-%02i %02i:%02i:%02.3f"
dateStr = dateTempl%(date.year, date.month, date.day, date.hour,
date.minute, date.second+date.microsecond/1000000.)
rows = self.cur.execute('select cameraId, pincushion, plateScale from Camera '+
'where (? - date) > 0 order by (? - date) limit 1',
(dateStr, dateStr))
row = rows.fetchone()
return {'cameraId':row[0], 'pincushion':row[1], 'plateScale':row[2]}
def queryDetectors(self):
"""
Query a database for the properties of all detectors active at a particular time
@return detectorList: A list of detector objects
"""
qstr = 'select s.slotName, s.slotIndex, d.x, d.y, d.z, d.alpha, d.beta, d.gamma, d.serial '+
'from Camera c '+
'join slotMap s on (c.cameraId = s.cameraId) '+
'join Detector d on (s.detectorId = d.detectorId) '+
'where c.cameraId = ?'
rows = self.cur.execute(qstr, (self.cameraId,))
detectorList = []
for row in rows:
detectorProps = dict([(colname[0], row[i]) for i, colname in enumerate(rows.description)])
amprows = self.cur.execute('select ampName, assembledBbox, gain, readnoise, ampBbox, rawDataBbox, '+
'oscanH, oscanV, flipx, flipy, xOffset, yOffset '+
'from amps where serial = ?', (detectorDict[name]['serial'],))
ampList = []
for amprow in amprows:
ampList.append(dict([(colname[0], amprow[i]) for i, colname in enumerate(amprows.description)]))
7 ISR¶
Here is now ISR assembles an image. Two versions are shown: the first is the default implementation that handles most cameras, and the second handles LSST.
def assembleImageFromCcdImage(rawImage, ampList, bbox=None):
"""Assemble a CCD image for a camera in which raw data is a single CCD image
This is how many cameras manage raw data, but not LSSTSim.
@param[in] rawImage: raw image (Image or MaskedImage). All the amplifier images are included
somewhere on the raw image. Underscan and overscan make the raw image larger than the assembled image.
@param[in] ampList: a list of Amplifiers
@param[in] bbox: bounding box of output amplifier; if None then compute from ampList.
@return assembled image or maskedImage
"""
if bbox is None:
bbox = Box2I()
for amp in ampList:
bbox.include(amp.getBBox())
outImage = Image(bbox)
for amp in ampList:
amp.assembleImage(outImage, rawImage)
return outImage
def assembleImageFromAmpImages(rawImageList, ampList, bbox=None):
"""Assemble a CCD image for a camera in which raw data is a set of separate amplifier images, e.g. LSSTSim
@param[in] rawImageList: list of raw images (Image or MaskedImage), one per amplifier
@param[in] ampList: a list of Amplifiers
@param[in] bbox: bounding box of output amplifier; if None then compute from ampList.
@return assembled image or maskedImage
"""
if len(rawImageList) != len(ampList):
raise RuntimeError("You must provide one raw image per amplifier")
if bbox is None:
bbox = Box2I()
for amp in ampList:
bbox.include(amp.getBBox())
outImage = Image(bbox)
for rawImage, amp in izip(rawImageList, ampList):
amp.assembleImage(outImage, rawImage)
return outImage
8 Notes¶
- We assume the coordinate transformations supported by these classes are two dimensional. This is not fully general, but we feel it covers the primary use cases and is a very useful simplification.
Camera.transform
must allow one to specify a particular detector, for example to use in pupil→pixels transforms with a known detector (if not specified then transform callsfindDetector
.CameraPoint
has a detector name field that works well for pixels→pupil, but would require setting to go the other way. That may suffice, or we can add a detector argument.- Our plan is that
Detector
can only transform between detector-level coordinate systems, such aspixels
, andfocalPlane
coordinates; to transform to other camera coordinate systems such aspupil
requires aCamera
. Supporting additional transforms requires putting a copy of the camera’s transform map in eachDetector
and slightly more complicated code inDetector
‘stransform
method to handle the two transform maps. - Mutability: we plan to make all the above classes immutable after construction.
This will require one to make a new
Detector
orCamera
if either’sTransformMap
is to be extended at run time, but we suspect this use case is fairly rare. - RHL wants enough image assembly and bias subtraction to be in CameraGeom that display code and engineers working on CCDs will not have to use the ISR package. Right now there is duplication between ip_isr and CameraGeom and we should take this opportunity to eliminate that.
- RHL wants a minimal “trim and subtract bias” function for image display (cruder than ISR would use). That will be written in python.
Note
This document was originally published as an LSST TRAC page at https://dev.lsstcorp.org/trac/wiki/Winter2014/Design/CameraGeomDesign.