Coverage for tests/test_maskStreaks.py: 13%

120 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-11 03:52 -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/>. 

21 

22import unittest 

23import numpy as np 

24import warnings 

25import scipy 

26 

27import lsst.utils.tests 

28import lsst.afw.image 

29import lsst.afw.math 

30from lsst.meas.algorithms.maskStreaks import MaskStreaksTask, LineProfile, Line 

31 

32 

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. 

36 

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. 

43 

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 

64 

65 detectionMask = ((mask.array & mask.getPlaneBitMask(detectedPlane))) 

66 badPixelMask = mask.getPlaneBitMask(badMaskPlanes) 

67 badMask = (mask.array & badPixelMask) > 0 

68 fitMask = detectionMask.astype(bool) & ~badMask 

69 

70 fitData = np.copy(data) 

71 fitData[~fitMask] = 0 

72 fitWeights = weights 

73 fitWeights[~fitMask] = 0 

74 

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 

111 

112 # Clear existing Detected Plane: 

113 maskedImage.mask.array &= ~maskedImage.mask.getPlaneBitMask(detectedPlane) 

114 

115 # Set Detected Plane with the binary detection mask: 

116 maskedImage.mask.array[outputMask] |= maskedImage.mask.getPlaneBitMask(detectedPlane) 

117 

118 

119class TestMaskStreaks(lsst.utils.tests.TestCase): 

120 

121 def setUp(self): 

122 self.config = MaskStreaksTask.ConfigClass() 

123 self.config.dChi2Tolerance = 1e-6 

124 self.fst = MaskStreaksTask(config=self.config) 

125 

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" 

134 

135 def test_binning(self): 

136 """Test the two binning methods and the no-binning method""" 

137 

138 binSize = 4 

139 self.assertEqual(self.testx % binSize, 0) 

140 self.assertEqual(self.testy % binSize, 0) 

141 

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()) 

152 

153 def test_canny(self): 

154 """Test that Canny filter returns binary of equal shape""" 

155 

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()) 

159 

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()) 

166 

167 def test_runkht(self): 

168 """Test the whole thing""" 

169 

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()) 

179 

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() 

191 

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) 

196 

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) 

202 

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) 

208 

209 

210def setup_module(module): 

211 lsst.utils.tests.init() 

212 

213 

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()