Coverage for tests/test_imageDifference.py: 11%
286 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:04 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:04 +0000
1# This file is part of ip_diffim.
2#
3# LSST Data Management System
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6# See COPYRIGHT file at the top of the source tree.
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 <https://www.lsstcorp.org/LegalNotices/>.
21#
23import numpy as np
25import lsst.afw.geom as afwGeom
26import lsst.afw.math as afwMath
27import lsst.daf.base as dafBase
28import lsst.geom as geom
29from lsst.meas.algorithms.testUtils import plantSources
30import lsst.utils.tests
32from lsst.ip.diffim.imageDecorrelation import DecorrelateALKernelTask, DecorrelateALKernelConfig
33from lsst.ip.diffim.imagePsfMatch import ImagePsfMatchTask, ImagePsfMatchConfig
34from lsst.ip.diffim.zogy import ZogyTask, ZogyConfig
37class ImageDifferenceTestBase(lsst.utils.tests.TestCase):
38 """A test case for comparing image differencing algorithms.
40 Attributes
41 ----------
42 bbox : `lsst.afw.geom.Box2I`
43 Bounding box of the test model.
44 bufferSize : `int`
45 Distance from the inner edge of the bounding box
46 to avoid placing test sources in the model images.
47 nRandIter : `int`
48 Number of iterations to repeat each test with random numbers.
49 statsCtrl : `lsst.afw.math.StatisticsControl`
50 Statistics control object.
51 """
53 def setUp(self):
54 """Define the filter, DCR parameters, and the bounding box for the tests.
55 """
56 self.nRandIter = 5 # Number of iterations to repeat each test with random numbers.
57 self.bufferSize = 5
58 xSize = 250
59 ySize = 260
60 x0 = 12345
61 y0 = 67890
62 self.bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize))
63 self.statsCtrl = afwMath.StatisticsControl()
64 self.statsCtrl.setNumSigmaClip(3.)
65 self.statsCtrl.setNumIter(3)
67 def makeTestImages(self, seed=5, nSrc=5, psfSize=2., noiseLevel=5.,
68 fluxLevel=500., fluxRange=2.):
69 """Make reproduceable PSF-convolved masked images for testing.
71 Parameters
72 ----------
73 seed : `int`, optional
74 Seed value to initialize the random number generator.
75 nSrc : `int`, optional
76 Number of sources to simulate.
77 psfSize : `float`, optional
78 Width of the PSF of the simulated sources, in pixels.
79 noiseLevel : `float`, optional
80 Standard deviation of the noise to add to each pixel.
81 fluxLevel : `float`, optional
82 Reference flux of the simulated sources.
83 fluxRange : `float`, optional
84 Range in flux amplitude of the simulated sources.
86 Returns
87 -------
88 modelImages : `lsst.afw.image.ExposureF`
89 The model image, with the mask and variance planes.
90 sourceCat : `lsst.afw.table.SourceCatalog`
91 Catalog of sources detected on the model image.
92 """
93 rng = np.random.RandomState(seed)
94 x0, y0 = self.bbox.getBegin()
95 xSize, ySize = self.bbox.getDimensions()
96 xLoc = rng.rand(nSrc)*(xSize - 2*self.bufferSize) + self.bufferSize + x0
97 yLoc = rng.rand(nSrc)*(ySize - 2*self.bufferSize) + self.bufferSize + y0
99 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel
100 sigmas = [psfSize for src in range(nSrc)]
101 coordList = list(zip(xLoc, yLoc, flux, sigmas))
102 kernelSize = int(xSize/2) # Need a careful explanation of this kernel size choice
103 skyLevel = 0
104 # Don't use the built in poisson noise: it modifies the global state of numpy random
105 model = plantSources(self.bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False)
106 noise = rng.rand(ySize, xSize)*noiseLevel
107 model.image.array += noise
108 model.variance.array = (np.sqrt(np.abs(model.image.array)) + noiseLevel
109 - np.mean(np.sqrt(np.abs(noise))))
111 # Run source detection to set up the mask plane
112 psfMatchTask = ImagePsfMatchTask(config=ImagePsfMatchConfig())
113 sourceCat = psfMatchTask.getSelectSources(model)
115 model.setWcs(self._makeWcs())
116 return model, sourceCat
118 @staticmethod
119 def _makeWcs(offset=0):
120 """Make a fake Wcs.
122 Parameters
123 ----------
124 offset : `float`
125 offset the Wcs by this many pixels.
126 """
127 # taken from $AFW_DIR/tests/testMakeWcs.py
128 metadata = dafBase.PropertySet()
129 metadata.set("SIMPLE", "T")
130 metadata.set("BITPIX", -32)
131 metadata.set("NAXIS", 2)
132 metadata.set("NAXIS1", 1024)
133 metadata.set("NAXIS2", 1153)
134 metadata.set("RADESYS", 'FK5')
135 metadata.set("EQUINOX", 2000.)
136 metadata.setDouble("CRVAL1", 215.604025685476)
137 metadata.setDouble("CRVAL2", 53.1595451514076)
138 metadata.setDouble("CRPIX1", 1109.99981456774 + offset)
139 metadata.setDouble("CRPIX2", 560.018167811613 + offset)
140 metadata.set("CTYPE1", 'RA---SIN')
141 metadata.set("CTYPE2", 'DEC--SIN')
142 metadata.setDouble("CD1_1", 5.10808596133527E-05)
143 metadata.setDouble("CD1_2", 1.85579539217196E-07)
144 metadata.setDouble("CD2_2", -5.10281493481982E-05)
145 metadata.setDouble("CD2_1", -8.27440751733828E-07)
146 return afwGeom.makeSkyWcs(metadata)
148 def diffimMetricBasic(self, residual, sourceCat, radius=2, sigma=0.):
149 """Compute a basic metric based on the total number of positive and
150 negative pixels in a residual image.
152 Parameters
153 ----------
154 residual : `lsst.afw.image.ExposureF`
155 A residual image resulting from image differencing.
156 sourceCat : `lsst.afw.table.SourceCatalog`
157 Source catalog containing the locations to calculate the metric.
158 radius : `int`, optional
159 Radius in pixels to use around each source location for the metric.
160 sigma : `float`, optional
161 Threshold to include pixel values in the metric.
163 Returns
164 -------
165 `float`
166 Metric assessing the image differencing residual.
167 """
168 nNeg = 0
169 nPos = 0
170 threshold = sigma*self.computeExposureStddev(residual)
171 for src in sourceCat:
172 srcX = int(src.getX()) - residual.getBBox().getBeginX()
173 srcY = int(src.getY()) - residual.getBBox().getBeginY()
174 srcRes = residual.image.array[srcY - radius: srcY + radius + 1, srcX - radius: srcX + radius + 1]
175 nPos += np.sum(srcRes > threshold)
176 nNeg += np.sum(srcRes < -threshold)
178 if (nPos + nNeg) == 0:
179 metric = 0.
180 else:
181 metric = (nPos - nNeg)/(nPos + nNeg)
182 return metric
184 def computeExposureStddev(self, exposure):
185 """Compute the standard deviation of an exposure, using the mask plane.
187 Parameters
188 ----------
189 exposure : `lsst.afw.image.ExposureF`
190 The input exposure.
192 Returns
193 -------
194 `float`
195 The standard deviation of the unmasked pixels of the input image.
196 """
197 statObj = afwMath.makeStatistics(exposure.maskedImage.image,
198 exposure.maskedImage.mask,
199 afwMath.STDEVCLIP, self.statsCtrl)
200 var = statObj.getValue(afwMath.STDEVCLIP)
201 return var
203 @staticmethod
204 def wrapZogyDiffim(config, templateExposure, scienceExposure):
205 """Prepare and run ZOGY-style image differencing.
207 Parameters
208 ----------
209 config : `lsst.pex.config.Config`
210 The image differencing Task configuration settings.
211 templateExposure : `lsst.afw.image.ExposureF`
212 The reference image to subtract from the science image.
213 scienceExposure : `lsst.afw.image.ExposureF`
214 The science image.
216 Returns
217 -------
218 `lsst.afw.image.ExposureF`
219 The image difference.
220 """
221 config.scaleByCalibration = False
222 zogyTask = ZogyTask(config=config)
224 result = zogyTask.run(scienceExposure, templateExposure)
225 return result.diffExp
227 @staticmethod
228 def wrapAlDiffim(config, templateExposure, scienceExposure, convolveTemplate=True, returnKernel=False,
229 precomputeKernelCandidates=False):
230 """Prepare and run Alard&Lupton-style image differencing.
232 Parameters
233 ----------
234 config : `lsst.pex.config.Config`
235 The image differencing Task configuration settings.
236 templateExposure : `lsst.afw.image.ExposureF`
237 The reference image to subtract from the science image.
238 scienceExposure : `lsst.afw.image.ExposureF`
239 The science image.
240 convolveTemplate : `bool`, optional
241 Option to convolve the template or the science image.
242 returnKernel : `bool`, optional
243 Option to return the residual image or the matching kernel.
245 Returns
246 -------
247 `lsst.afw.image.ExposureF` or `lsst.afw.math.LinearCombinationKernel`
248 The image difference, or the PSF matching kernel.
249 """
250 alTask = ImagePsfMatchTask(config=config)
251 candidateList = None
252 if precomputeKernelCandidates:
253 if convolveTemplate:
254 candidateList = alTask.getSelectSources(scienceExposure.clone())
255 else:
256 candidateList = alTask.getSelectSources(templateExposure.clone())
257 templateFwhmPix = templateExposure.getPsf().getSigma()
258 scienceFwhmPix = scienceExposure.getPsf().getSigma()
259 result = alTask.subtractExposures(templateExposure, scienceExposure,
260 templateFwhmPix=templateFwhmPix,
261 scienceFwhmPix=scienceFwhmPix,
262 doWarping=False,
263 convolveTemplate=convolveTemplate,
264 candidateList=candidateList,
265 )
266 if returnKernel:
267 return result.psfMatchingKernel
268 else:
269 return result.subtractedExposure
272class ImageDifferenceTestVerification(ImageDifferenceTestBase):
274 def testModelImages(self):
275 """Check that the simulated images are useable.
276 """
277 sciPsf = 2.4
278 refPsf = 2.
279 sciNoise = 5.
280 refNoise = 1.5
281 fluxRatio = refPsf**2/sciPsf**2
282 sciIm, src = self.makeTestImages(psfSize=sciPsf, noiseLevel=sciNoise)
283 sciIm2, _ = self.makeTestImages(psfSize=sciPsf, noiseLevel=sciNoise)
284 refIm, _ = self.makeTestImages(psfSize=refPsf, noiseLevel=refNoise)
286 # Making the test images should be repeatable
287 self.assertFloatsAlmostEqual(sciIm.image.array, sciIm2.image.array)
289 diffIm = sciIm.clone()
290 diffIm.image.array -= refIm.image.array
292 # The "reference" image has a smaller PSF but the same source fluxes, so the peak should be greater.
293 self.assertGreater(np.max(refIm.image.array), np.max(sciIm.image.array))
294 # The difference image won't be zero since the two images have different PSFs,
295 # but the peak should be much lower.
296 sciPeak = np.max(sciIm.image.array)
297 residualPeak = np.sqrt(1 - fluxRatio)*sciPeak
298 self.assertGreater(residualPeak, np.max(abs(diffIm.image.array)))
300 # It should be possible to compute the diffim metric from the science and reference images
301 refMetric = self.diffimMetricBasic(refIm, src, sigma=3)
302 sciMetric = self.diffimMetricBasic(sciIm, src, sigma=3)
303 self.assertGreaterEqual(refMetric, -1)
304 self.assertGreaterEqual(sciMetric, -1)
306 def testSimDiffim(self):
307 "Basic smoke test to verify that the test code itself can run."
308 refPsf = 2.4
309 sciPsfBase = 2.
310 sciNoise = 5.
311 refNoise = 1.5
312 seed = 8
313 fluxLevel = 500
314 decorrelate = DecorrelateALKernelTask()
315 zogyConfig = ZogyConfig()
316 alConfig = ImagePsfMatchConfig()
318 for s in range(self.nRandIter):
319 sciPsf = sciPsfBase + s*0.2
320 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf,
321 noiseLevel=refNoise, fluxLevel=fluxLevel)
322 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf,
323 noiseLevel=sciNoise, fluxLevel=fluxLevel)
324 # The diffim tasks might modify the images,
325 # so make a deep copy to make sure they are independent
326 sci2 = sci.clone()
327 ref2 = ref.clone()
329 resAl = self.wrapAlDiffim(alConfig, ref, sci)
330 resZogy = self.wrapZogyDiffim(zogyConfig, ref2, sci2)
331 metricZogy = self.diffimMetricBasic(resZogy, src, sigma=3)
332 metricAl = self.diffimMetricBasic(resAl, src, sigma=3)
333 mKernel = self.wrapAlDiffim(alConfig, ref, sci, returnKernel=True)
334 resDecorr = decorrelate.run(sci, ref, resAl, mKernel).correctedExposure
335 metricDecorr = self.diffimMetricBasic(resDecorr, src, sigma=3)
336 self.assertGreaterEqual(metricZogy, -1)
337 self.assertGreaterEqual(metricAl, -1)
338 self.assertGreaterEqual(metricDecorr, -1)
341class ImageDifferenceTestAlardLupton(ImageDifferenceTestBase):
343 def testSimAlRefNotModified(self):
344 "Image differencing should not modify the original template image."
345 refPsf = 2.
346 sciPsfBase = 2.
347 sciNoise = 5.
348 refNoise = 1.5
349 seed = 37
350 fluxLevel = 500
351 rng = np.random.RandomState(seed)
352 alConfig = ImagePsfMatchConfig()
354 sciPsf = sciPsfBase + rng.random()*2.
355 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf,
356 noiseLevel=refNoise, fluxLevel=fluxLevel)
357 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf,
358 noiseLevel=sciNoise, fluxLevel=fluxLevel)
359 # Make a deep copy of the images first
360 sciTest1 = sciOriginal.clone()
361 refTest1 = refOriginal.clone()
363 # Basic AL, but we don't care about the result.
364 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=False)
365 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage)
367 # Basic AL, but we don't care about the result.
368 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=True)
369 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage)
371 def testSimAlSciNotModified(self):
372 "Image differencing should not modify the original science image."
373 refPsf = 2.
374 sciPsfBase = 2.
375 sciNoise = 5.
376 refNoise = 1.5
377 seed = 37
378 fluxLevel = 500
379 rng = np.random.RandomState(seed)
380 alConfig = ImagePsfMatchConfig()
382 sciPsf = sciPsfBase + rng.random()*2.
383 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf,
384 noiseLevel=refNoise, fluxLevel=fluxLevel)
385 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf,
386 noiseLevel=sciNoise, fluxLevel=fluxLevel)
387 # Make a deep copy of the images first
388 sciTest1 = sciOriginal.clone()
389 refTest1 = refOriginal.clone()
391 # Basic AL, but we don't care about the result.
392 # Note that selecting KernelCandidates *does* change the science image slightly
393 # because a background is subtracted before detection, then added back in.
394 # For this test, we separate out that known modification by precomputing the
395 # kernel candidates in wrapAlDiffim and using a deep copy of the science image.
396 # This test is therefore checking that there are no other, unknown, modifications
397 # of the science image.
398 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=True,
399 precomputeKernelCandidates=True)
401 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage)
403 # Basic AL, but we don't care about the result.
404 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=False,
405 precomputeKernelCandidates=True)
407 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage)
409 def testSimReverseAlNoDecorrEqualNoise(self):
410 refPsf = 2.
411 sciPsfBase = 2.
412 sciNoise = 5.
413 refNoise = 5
414 seed = 37
415 metricSigma = 0
416 fluxLevel = 500
417 rng = np.random.RandomState(seed)
418 alConfig = ImagePsfMatchConfig()
420 for s in range(self.nRandIter):
421 sciPsf = sciPsfBase + rng.random()*2.
422 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf,
423 noiseLevel=refNoise, fluxLevel=fluxLevel)
424 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf,
425 noiseLevel=sciNoise, fluxLevel=fluxLevel)
427 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True)
428 resR = self.wrapAlDiffim(alConfig, sci, ref, convolveTemplate=False)
430 metric = self.diffimMetricBasic(res, src, sigma=metricSigma)
431 metricR = self.diffimMetricBasic(resR, src, sigma=metricSigma)
432 # Alard&Lupton is not fully reversable, but the answers should be close.
433 # Partly this needs the decorrelation afterburner
434 # It might also be a difference in background subtraction
435 self.assertFloatsAlmostEqual(metric, -metricR, atol=.1, rtol=.1)
437 def testSimReverseAlNoDecorrUnequalNoise(self):
438 refPsf = 2.
439 sciPsfBase = 2.
440 sciNoise = 5.
441 refNoise = 1.5
442 seed = 37
443 metricSigma = 0
444 fluxLevel = 500
445 rng = np.random.RandomState(seed)
446 alConfig = ImagePsfMatchConfig()
448 for s in range(self.nRandIter):
449 sciPsf = sciPsfBase + rng.random()*2.
450 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf,
451 noiseLevel=refNoise, fluxLevel=fluxLevel)
452 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf,
453 noiseLevel=sciNoise, fluxLevel=fluxLevel)
455 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True)
456 resR = self.wrapAlDiffim(alConfig, sci, ref, convolveTemplate=False)
458 metric = self.diffimMetricBasic(res, src, sigma=metricSigma)
459 metricR = self.diffimMetricBasic(resR, src, sigma=metricSigma)
460 # Alard&Lupton is not fully reversable, but the answers should be close.
461 # Partly this needs the decorrelation afterburner
462 # It might also be a difference in background subtraction
463 self.assertFloatsAlmostEqual(metric, -metricR, atol=.1, rtol=.1)
466class ImageDifferenceTestZogy(ImageDifferenceTestBase):
468 def testSimZogySciRefNotModified(self):
469 "Image differencing should not modify the original images."
470 refPsf = 2.
471 sciPsfBase = 2.
472 sciNoise = 5.
473 refNoise = 1.5
474 seed = 37
475 fluxLevel = 500
476 rng = np.random.RandomState(seed)
477 zogyConfig = ZogyConfig()
479 sciPsf = sciPsfBase + rng.random()*2.
480 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf,
481 noiseLevel=refNoise, fluxLevel=fluxLevel)
482 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf,
483 noiseLevel=sciNoise, fluxLevel=fluxLevel)
484 # Make a deep copy of the images first
485 sciTest1 = sciOriginal.clone()
486 refTest1 = refOriginal.clone()
488 # Basic ZOGY, but we don't care about the result.
489 self.wrapZogyDiffim(zogyConfig, refTest1, sciTest1)
490 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage)
491 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage)
493 def testSimReverseZogy(self):
494 refPsf = 2.
495 sciPsfBase = 2.
496 sciNoise = 5.
497 refNoise = 1.5
498 seed = 18
499 fluxLevel = 500
500 rng = np.random.RandomState(seed)
501 zogyConfig = ZogyConfig()
503 for s in range(self.nRandIter):
504 sciPsf = sciPsfBase + rng.random()*2.
505 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf,
506 noiseLevel=refNoise, fluxLevel=fluxLevel)
507 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf,
508 noiseLevel=sciNoise, fluxLevel=fluxLevel)
510 res = self.wrapZogyDiffim(zogyConfig, ref, sci)
511 resR = self.wrapZogyDiffim(zogyConfig, sci, ref)
512 metric = self.diffimMetricBasic(res, src, sigma=3)
513 metricR = self.diffimMetricBasic(resR, src, sigma=3)
514 self.assertFloatsAlmostEqual(metric, -metricR)
517class ImageDifferenceTestDecorrelation(ImageDifferenceTestBase):
519 def testSimAlDecorr(self):
520 refPsf = 2.
521 sciPsfBase = 2.
522 sciNoise = 5.
523 refNoise = 1.5
524 seed = 37
525 metricSigma = 0
526 fluxLevel = 500
527 rng = np.random.RandomState(seed)
528 decorrelateConfig = DecorrelateALKernelConfig()
529 decorrelate = DecorrelateALKernelTask(config=decorrelateConfig)
530 alConfig = ImagePsfMatchConfig()
532 for s in range(self.nRandIter):
533 sciPsf = sciPsfBase + rng.random()*2.
534 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf,
535 noiseLevel=refNoise, fluxLevel=fluxLevel)
536 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf,
537 noiseLevel=sciNoise, fluxLevel=fluxLevel)
538 # The diffim tasks can modify the images, so make a deep copy to make sure they are independent
539 sci2 = sci.clone()
540 ref2 = ref.clone()
542 # Basic AL
543 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True)
545 # Decorrelated AL
546 mKernel = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True, returnKernel=True)
547 resD = decorrelate.run(sci, ref, res, mKernel).correctedExposure
548 metricD = self.diffimMetricBasic(resD, src, sigma=metricSigma)
550 # Swap the "science" and "reference" images, and alse swap which image is convolved.
551 # The result is that the same image should be convolved as above
552 resR = self.wrapAlDiffim(alConfig, sci2, ref2, convolveTemplate=False)
554 # Swap the images as above, and also decorrelate.
555 mKernelR = self.wrapAlDiffim(alConfig, sci2, ref2, convolveTemplate=False, returnKernel=True)
556 resDR = decorrelate.run(ref2, sci2, resR, mKernelR).correctedExposure
557 metricDR = self.diffimMetricBasic(resDR, src, sigma=metricSigma)
559 self.assertFloatsAlmostEqual(metricD, -metricDR, atol=.1, rtol=0.1)