Coverage for tests/test_maskStreaks.py: 13%
120 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:42 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:42 -0700
1# This file is part of meas_algorithms.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import unittest
23import numpy as np
24import warnings
25import scipy
27import lsst.utils.tests
28import lsst.afw.image
29import lsst.afw.math
30from lsst.meas.algorithms.maskStreaks import MaskStreaksTask, LineProfile, Line
33def setDetectionMask(maskedImage, forceSlowBin=False, binning=None, detectedPlane="DETECTED",
34 badMaskPlanes=("NO_DATA", "INTRP", "BAD", "SAT", "EDGE"), detectionThreshold=5):
35 """Make detection mask and set the mask plane.
37 Create a binary image from a masked image by setting all data with signal-to-
38 noise below some threshold to zero, and all data above the threshold to one.
39 If the binning parameter has been set, this procedure will be preceded by a
40 weighted binning of the data in order to smooth the result, after which the
41 result is scaled back to the original dimensions. Set the detection mask
42 plane with this binary image.
44 Parameters
45 ----------
46 maskedImage : `lsst.afw.image.maskedImage`
47 Image to be (optionally) binned and converted.
48 forceSlowBin : `bool`, optional
49 Force usage of slower binning method to check that the two methods
50 give the same result.
51 binning : `int`, optional
52 Number of pixels by which to bin image.
53 detectedPlane : `str`, optional
54 Name of mask with pixels that were detected above threshold in image.
55 badMaskPlanes : `tuple`, optional
56 Names of masks with pixels that are rejected.
57 detectionThreshold : `float`, optional
58 Boundary in signal-to-noise between non-detections and detections for
59 making a binary image from the original input image.
60 """
61 data = maskedImage.image.array
62 weights = 1 / maskedImage.variance.array
63 mask = maskedImage.mask
65 detectionMask = ((mask.array & mask.getPlaneBitMask(detectedPlane)))
66 badPixelMask = mask.getPlaneBitMask(badMaskPlanes)
67 badMask = (mask.array & badPixelMask) > 0
68 fitMask = detectionMask.astype(bool) & ~badMask
70 fitData = np.copy(data)
71 fitData[~fitMask] = 0
72 fitWeights = weights
73 fitWeights[~fitMask] = 0
75 if binning:
76 # Do weighted binning:
77 ymax, xmax = fitData.shape
78 if (ymax % binning == 0) and (xmax % binning == 0) and (not forceSlowBin):
79 # Faster binning method
80 binNumeratorReshape = (fitData * fitWeights).reshape(ymax // binning, binning,
81 xmax // binning, binning)
82 binDenominatorReshape = fitWeights.reshape(binNumeratorReshape.shape)
83 binnedNumerator = binNumeratorReshape.sum(axis=3).sum(axis=1)
84 binnedDenominator = binDenominatorReshape.sum(axis=3).sum(axis=1)
85 else:
86 # Slower binning method when (image shape mod binsize) != 0
87 warnings.warn('Using slow binning method--consider choosing a binsize that evenly divides '
88 f'into the image size, so that {ymax} mod binning == 0 '
89 f'and {xmax} mod binning == 0', stacklevel=2)
90 xarray = np.arange(xmax)
91 yarray = np.arange(ymax)
92 xmesh, ymesh = np.meshgrid(xarray, yarray)
93 xbins = np.arange(0, xmax + binning, binning)
94 ybins = np.arange(0, ymax + binning, binning)
95 numerator = fitWeights * fitData
96 binnedNumerator, *_ = scipy.stats.binned_statistic_2d(ymesh.ravel(), xmesh.ravel(),
97 numerator.ravel(), statistic='sum',
98 bins=(ybins, xbins))
99 binnedDenominator, *_ = scipy.stats.binned_statistic_2d(ymesh.ravel(), xmesh.ravel(),
100 fitWeights.ravel(), statistic='sum',
101 bins=(ybins, xbins))
102 binnedData = np.zeros(binnedNumerator.shape)
103 ind = binnedDenominator != 0
104 np.divide(binnedNumerator, binnedDenominator, out=binnedData, where=ind)
105 binnedWeight = binnedDenominator
106 binMask = (binnedData * binnedWeight**0.5) > detectionThreshold
107 tmpOutputMask = binMask.repeat(binning, axis=0)[:ymax]
108 outputMask = tmpOutputMask.repeat(binning, axis=1)[:, :xmax]
109 else:
110 outputMask = (fitData * fitWeights**0.5) > detectionThreshold
112 # Clear existing Detected Plane:
113 maskedImage.mask.array &= ~maskedImage.mask.getPlaneBitMask(detectedPlane)
115 # Set Detected Plane with the binary detection mask:
116 maskedImage.mask.array[outputMask] |= maskedImage.mask.getPlaneBitMask(detectedPlane)
119class TestMaskStreaks(lsst.utils.tests.TestCase):
121 def setUp(self):
122 self.config = MaskStreaksTask.ConfigClass()
123 self.config.dChi2Tolerance = 1e-6
124 self.fst = MaskStreaksTask(config=self.config)
126 self.testx = 500
127 self.testy = 600
128 self.exposure = lsst.afw.image.ExposureF(self.testy, self.testx)
129 rand = lsst.afw.math.Random(seed=98765)
130 lsst.afw.math.randomGaussianImage(self.exposure.image, rand)
131 self.exposure.maskedImage.variance.set(1)
132 self.maskName = "STREAK"
133 self.detectedPlane = "DETECTED"
135 def test_binning(self):
136 """Test the two binning methods and the no-binning method"""
138 binSize = 4
139 self.assertEqual(self.testx % binSize, 0)
140 self.assertEqual(self.testy % binSize, 0)
142 testExposure1 = self.exposure.clone()
143 setDetectionMask(testExposure1, binning=binSize, detectedPlane=self.detectedPlane)
144 mask1 = testExposure1.getMask()
145 reshapeBinning = mask1.array & mask1.getPlaneBitMask(self.detectedPlane)
146 testExposure2 = self.exposure.clone()
147 with self.assertWarns(Warning):
148 setDetectionMask(testExposure2, binning=binSize, forceSlowBin=True)
149 mask2 = testExposure2.getMask()
150 scipyBinning = mask2.array & mask2.getPlaneBitMask(self.detectedPlane)
151 self.assertAlmostEqual(reshapeBinning.tolist(), scipyBinning.tolist())
153 def test_canny(self):
154 """Test that Canny filter returns binary of equal shape"""
156 zeroExposure = lsst.afw.image.ExposureF(self.testy, self.testx)
157 cannyZeroExposure = self.fst._cannyFilter(zeroExposure.image.array)
158 self.assertEqual(cannyZeroExposure.tolist(), zeroExposure.image.array.tolist())
160 exposure = self.exposure.clone()
161 setDetectionMask(exposure, detectedPlane=self.detectedPlane)
162 mask = exposure.getMask()
163 processedImage = mask.array & mask.getPlaneBitMask(self.detectedPlane)
164 cannyNonZero = self.fst._cannyFilter(processedImage)
165 self.assertEqual(cannyNonZero.tolist(), cannyNonZero.astype(bool).tolist())
167 def test_runkht(self):
168 """Test the whole thing"""
170 # Empty image:
171 zeroArray = np.zeros((self.testx, self.testy))
172 zeroLines = self.fst._runKHT(zeroArray)
173 self.assertEqual(len(zeroLines), 0)
174 testExposure = self.exposure.clone()
175 result = self.fst.run(testExposure)
176 self.assertEqual(len(result.lines), 0)
177 resultMask = testExposure.mask.array & testExposure.mask.getPlaneBitMask(self.maskName)
178 self.assertEqual(resultMask.tolist(), zeroArray.tolist())
180 # Make image with line and check that input line is recovered:
181 testExposure1 = self.exposure.clone()
182 inputRho = 150
183 inputTheta = 45
184 inputSigma = 3
185 testLine = Line(inputRho, inputTheta, inputSigma)
186 lineProfile = LineProfile(testExposure.image.array, testExposure.variance.array**-1,
187 line=testLine)
188 testData = lineProfile.makeProfile(testLine, fitFlux=False)
189 testExposure1.image.array = testData * 100
190 detectedInd = abs(testData) > 0.1 * (abs(testData)).max()
192 testExposure1.mask.addMaskPlane(self.detectedPlane)
193 testExposure1.mask.array[detectedInd] |= testExposure1.mask.getPlaneBitMask(self.detectedPlane)
194 testExposure2 = testExposure1.clone()
195 setDetectionMask(testExposure2, detectedPlane=self.detectedPlane)
197 result_withSetDetMask = self.fst.run(testExposure2)
198 self.assertEqual(len(result_withSetDetMask.lines), 1)
199 self.assertAlmostEqual(inputRho, result_withSetDetMask.lines[0].rho, places=2)
200 self.assertAlmostEqual(inputTheta, result_withSetDetMask.lines[0].theta, places=2)
201 self.assertAlmostEqual(inputSigma, result_withSetDetMask.lines[0].sigma, places=2)
203 result = self.fst.run(testExposure1)
204 self.assertEqual(len(result.lines), 1)
205 self.assertAlmostEqual(inputRho, result.lines[0].rho, places=2)
206 self.assertAlmostEqual(inputTheta, result.lines[0].theta, places=2)
207 self.assertAlmostEqual(inputSigma, result.lines[0].sigma, places=2)
210def setup_module(module):
211 lsst.utils.tests.init()
214if __name__ == "__main__": 214 ↛ 215line 214 didn't jump to line 215, because the condition on line 214 was never true
215 lsst.utils.tests.init()
216 unittest.main()