Coverage for tests/test_linearizeLookupTable.py: 17%
147 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-14 10:14 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-14 10:14 +0000
1#
2# LSST Data Management System
3# Copyright 2017 LSST Corporation.
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 <http://www.lsstcorp.org/LegalNotices/>.
21#
22import unittest
23import pickle
24import logging
25import numpy as np
27import lsst.utils.tests
28import lsst.utils
29import lsst.afw.image as afwImage
30import lsst.afw.cameraGeom as cameraGeom
31from lsst.afw.geom.testUtils import BoxGrid
32from lsst.afw.image.testUtils import makeRampImage
33from lsst.ip.isr import applyLookupTable, Linearizer
36def refLinearize(image, detector, table):
37 """!Basic implementation of lookup table based non-linearity correction
39 @param[in,out] image image to correct in place (an lsst.afw.image.Image
40 of some type)
41 @param[in] detector detector info (an lsst.afw.cameraGeom.Detector)
42 @param[in] table lookup table: a 2D array of values of the same type as
43 image;
44 - one row for each row index (value of coef[0] in the amp info
45 catalog)
46 - one column for each image value
48 @return the number of pixels whose values were out of range of the lookup
49 table
50 """
51 ampInfoCat = detector.getAmplifiers()
52 numOutOfRange = 0
53 for ampInfo in ampInfoCat:
54 bbox = ampInfo.getBBox()
55 rowInd, colIndOffset = ampInfo.getLinearityCoeffs()[0:2]
56 rowInd = int(rowInd)
57 tableRow = table[rowInd, :]
58 imView = image.Factory(image, bbox)
59 numOutOfRange += applyLookupTable(imView, tableRow, colIndOffset)
60 return numOutOfRange
63class LinearizeLookupTableTestCase(lsst.utils.tests.TestCase):
64 """!Unit tests for LinearizeLookupTable"""
66 def setUp(self):
67 # the following values are all arbitrary, but sane and varied
68 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-31, 22), lsst.geom.Extent2I(100, 85))
69 self.numAmps = (2, 3)
70 self.colIndOffsets = np.array([[0, -50, 2.5], [37, 1, -3]], dtype=float)
71 self.rowInds = np.array([[0, 1, 4], [3, 5, 2]])
72 numCols = self.numAmps[0]*self.numAmps[1]
73 self.assertLess(np.max(self.rowInds), numCols, "error in test conditions; invalid row index")
74 self.detector = self.makeDetector()
76 def tearDown(self):
77 # destroy LSST objects so memory test passes
78 self.bbox = None
79 self.detector = None
81 def testBasics(self):
82 """!Test basic functionality of LinearizeLookupTable
83 """
84 for imageClass in (afwImage.ImageF, afwImage.ImageD):
85 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=250, imageClass=imageClass)
86 table = self.makeTable(inImage)
88 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable")
90 measImage = inImage.Factory(inImage, True)
91 llt = Linearizer(table=table, detector=self.detector)
92 linRes = llt.applyLinearity(measImage, detector=self.detector, log=log)
94 refImage = inImage.Factory(inImage, True)
95 refNumOutOfRange = refLinearize(image=refImage, detector=self.detector, table=table)
97 self.assertEqual(linRes.numAmps, len(self.detector.getAmplifiers()))
98 self.assertEqual(linRes.numAmps, linRes.numLinearized)
99 self.assertEqual(linRes.numOutOfRange, refNumOutOfRange)
100 self.assertImagesAlmostEqual(refImage, measImage)
102 # make sure logging is accepted
103 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable")
104 linRes = llt.applyLinearity(image=measImage, detector=self.detector, log=log)
106 def testErrorHandling(self):
107 """!Test error handling in LinearizeLookupTable
108 """
109 image = makeRampImage(bbox=self.bbox, start=-5, stop=250)
110 table = self.makeTable(image)
111 llt = Linearizer(table=table, detector=self.detector)
113 # bad name
114 detBadName = self.makeDetector(detName="bad_detector_name")
115 with self.assertRaises(RuntimeError):
116 llt.applyLinearity(image, detBadName)
118 # bad serial
119 detBadSerial = self.makeDetector(detSerial="bad_detector_serial")
120 with self.assertRaises(RuntimeError):
121 llt.applyLinearity(image, detBadSerial)
123 # bad number of amplifiers
124 badNumAmps = (self.numAmps[0]-1, self.numAmps[1])
125 detBadNumMaps = self.makeDetector(numAmps=badNumAmps)
126 with self.assertRaises(RuntimeError):
127 llt.applyLinearity(image, detBadNumMaps)
129 # bad linearity type
130 detBadLinType = self.makeDetector(linearityType="bad_linearity_type")
131 with self.assertRaises(RuntimeError):
132 llt.applyLinearity(image, detBadLinType)
134 # wrong dimension
135 badTable = table[..., np.newaxis]
136 with self.assertRaises(RuntimeError):
137 Linearizer(table=badTable, detector=self.detector)
139 # wrong size
140 badTable = np.transpose(table)
141 with self.assertRaises(RuntimeError):
142 Linearizer(table=badTable, detector=self.detector)
144 def testKnown(self):
145 """!Test a few known values
146 """
147 numAmps = (2, 2)
148 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(4, 4))
149 # make a 4x4 image with 4 identical 2x2 subregions that flatten
150 # to -1, 0, 1, 2
151 im = afwImage.ImageF(bbox)
152 imArr = im.getArray()
153 imArr[:, :] = np.array(((-1, 0, -1, 0),
154 (1, 2, 1, 2),
155 (-1, 0, -1, 0),
156 (1, 2, 1, 2)), dtype=imArr.dtype)
158 def castAndReshape(arr):
159 arr = np.array(arr, dtype=float)
160 arr.shape = numAmps
161 return arr
163 rowInds = castAndReshape((3, 2, 1, 0)) # avoid the trivial mapping to exercise more of the code
164 colIndOffsets = castAndReshape((0, 0, 1, 1))
165 detector = self.makeDetector(bbox=bbox, numAmps=numAmps, rowInds=rowInds, colIndOffsets=colIndOffsets)
166 ampInfoCat = detector.getAmplifiers()
168 # note: table rows are reversed relative to amplifier order because
169 # rowInds is a descending ramp
170 table = np.array(((7, 6, 5, 4), (1, 1, 1, 1), (5, 4, 3, 2), (0, 0, 0, 0)), dtype=imArr.dtype)
172 llt = Linearizer(table=table, detector=detector)
174 lltRes = llt.applyLinearity(image=im, detector=detector)
175 self.assertEqual(lltRes.numOutOfRange, 2)
177 # amp 0 is a constant correction of 0; one image value is out of range,
178 # but it doesn't matter
179 imArr0 = im.Factory(im, ampInfoCat[0].getBBox()).getArray()
180 self.assertFloatsAlmostEqual(imArr0.flatten(), (-1, 0, 1, 2))
182 # amp 1 is a correction of (5, 4, 3, 2), but the first image value is
183 # under range
184 imArr1 = im.Factory(im, ampInfoCat[1].getBBox()).getArray()
185 self.assertFloatsAlmostEqual(imArr1.flatten(), (4, 5, 5, 5))
187 # amp 2 is a constant correction of +1; all image values are in range,
188 # but it doesn't matter
189 imArr2 = im.Factory(im, ampInfoCat[2].getBBox()).getArray()
190 self.assertFloatsAlmostEqual(imArr2.flatten(), (0, 1, 2, 3))
192 # amp 3 is a correction of (7, 6, 5, 4); all image values in range
193 imArr1 = im.Factory(im, ampInfoCat[3].getBBox()).getArray()
194 self.assertFloatsAlmostEqual(imArr1.flatten(), (6, 6, 6, 6))
196 def testPickle(self):
197 """!Test that a LinearizeLookupTable can be pickled and unpickled
198 """
199 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=2500)
200 table = self.makeTable(inImage)
201 llt = Linearizer(table=table, detector=self.detector)
203 refImage = inImage.Factory(inImage, True)
204 refNumOutOfRange = llt.applyLinearity(refImage, self.detector)
206 pickledStr = pickle.dumps(llt)
207 restoredLlt = pickle.loads(pickledStr)
209 measImage = inImage.Factory(inImage, True)
210 measNumOutOfRange = restoredLlt.applyLinearity(measImage, self.detector)
212 self.assertEqual(refNumOutOfRange, measNumOutOfRange)
213 self.assertImagesAlmostEqual(refImage, measImage)
215 def makeDetector(self, bbox=None, numAmps=None, rowInds=None, colIndOffsets=None,
216 detName="det_a", detSerial="123", linearityType="LookupTable"):
217 """!Make a detector
219 @param[in] bbox bounding box for image
220 @param[n] numAmps x,y number of amplifiers (pair of int)
221 @param[in] rowInds index of lookup table for each amplifier (array of
222 shape numAmps)
223 @param[in] colIndOffsets column index offset for each amplifier
224 (array of shape numAmps)
225 @param[in] detName detector name (a str)
226 @param[in] detSerial detector serial numbe (a str)
227 @param[in] linearityType name of linearity type (a str)
229 @return a detector (an lsst.afw.cameraGeom.Detector)
230 """
231 bbox = bbox if bbox is not None else self.bbox
232 numAmps = numAmps if numAmps is not None else self.numAmps
233 rowInds = rowInds if rowInds is not None else self.rowInds
234 colIndOffsets = colIndOffsets if colIndOffsets is not None else self.colIndOffsets
236 detId = 1
237 orientation = cameraGeom.Orientation()
238 pixelSize = lsst.geom.Extent2D(1, 1)
240 camBuilder = cameraGeom.Camera.Builder("fakeCam")
241 detBuilder = camBuilder.add(detName, detId)
242 detBuilder.setSerial(detSerial)
243 detBuilder.setBBox(bbox)
244 detBuilder.setOrientation(orientation)
245 detBuilder.setPixelSize(pixelSize)
247 boxArr = BoxGrid(box=bbox, numColRow=numAmps)
248 for i in range(numAmps[0]):
249 for j in range(numAmps[1]):
250 ampInfo = cameraGeom.Amplifier.Builder()
251 ampInfo.setName("amp %d_%d" % (i + 1, j + 1))
252 ampInfo.setBBox(boxArr[i, j])
253 ampInfo.setLinearityType(linearityType)
254 # setLinearityCoeffs is picky about getting a mixed int/float
255 # list.
256 ampInfo.setLinearityCoeffs(np.array([rowInds[i, j], colIndOffsets[i, j], 0, 0], dtype=float))
257 detBuilder.append(ampInfo)
259 return detBuilder
261 def makeTable(self, image, numCols=None, numRows=2500, sigma=55):
262 """!Make a 2D lookup table
264 @param[in] image image whose type is used for the table
265 @param[in] numCols number of columns for table; defaults to
266 self.numCols
267 @param[in] numRows number of rows for the table
268 @param[in] sigma standard deviation of normal distribution
269 """
270 numCols = numCols or self.numAmps[0]*self.numAmps[1]
271 dtype = image.getArray().dtype
272 table = np.random.normal(scale=sigma, size=(numCols, numRows))
273 return np.array(table, dtype=dtype)
276class MemoryTester(lsst.utils.tests.MemoryTestCase):
277 pass
280def setup_module(module):
281 lsst.utils.tests.init()
284if __name__ == "__main__": 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true
285 lsst.utils.tests.init()
286 unittest.main()