Coverage for python/lsst/ip/isr/vignette.py: 25%

65 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 12:50 +0000

1# This file is part of ip_isr. 

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 

22__all__ = ('VignetteConfig', 'VignetteTask', 'setValidPolygonCcdIntersect', 

23 'maskVignettedRegion') 

24 

25import logging 

26import numpy as np 

27 

28import lsst.geom as geom 

29import lsst.afw.cameraGeom as cameraGeom 

30import lsst.afw.geom as afwGeom 

31 

32from lsst.pex.config import Config, Field 

33from lsst.pipe.base import Task 

34 

35 

36class VignetteConfig(Config): 

37 """Settings to define vignetting pattern. 

38 """ 

39 xCenter = Field( 

40 dtype=float, 

41 doc="Center of vignetting pattern, in focal plane x coordinates.", 

42 default=0.0, 

43 ) 

44 yCenter = Field( 

45 dtype=float, 

46 doc="Center of vignetting pattern, in focal plane y coordinates.", 

47 default=0.0, 

48 ) 

49 radius = Field( 49 ↛ exitline 49 didn't jump to the function exit

50 dtype=float, 

51 doc="Radius of vignetting pattern, in focal plane coordinates.", 

52 default=100.0, 

53 check=lambda x: x >= 0 

54 ) 

55 numPolygonPoints = Field( 

56 dtype=int, 

57 doc="Number of points used to define the vignette polygon.", 

58 default=100, 

59 ) 

60 doWriteVignettePolygon = Field( 

61 dtype=bool, 

62 doc="Persist polygon used to define vignetted region?", 

63 default=False, 

64 deprecated=("Vignetted polygon is added to the exposure by default." 

65 " This option is no longer used, and will be removed after v24.") 

66 ) 

67 

68 

69class VignetteTask(Task): 

70 """Define a simple circular vignette pattern and optionally update mask 

71 plane. 

72 """ 

73 ConfigClass = VignetteConfig 

74 _DefaultName = "isrVignette" 

75 

76 def run(self, exposure=None, doUpdateMask=True, maskPlane="NO_DATA", vignetteValue=None, log=None): 

77 """Generate circular vignette pattern. 

78 

79 Parameters 

80 ---------- 

81 exposure : `lsst.afw.image.Exposure`, optional 

82 Exposure to construct, apply, and optionally mask vignette for. 

83 doUpdateMask : `bool`, optional 

84 If true, the mask will be updated to mask the vignetted region. 

85 maskPlane : `str`, optional 

86 Mask plane to assign vignetted pixels to. 

87 vignetteValue : `float` or `None`, optional 

88 Value to assign to the image array pixels within the ``polygon`` 

89 region. If `None`, image pixel values are not replaced. 

90 log : `logging.Logger`, optional 

91 Log object to write to. 

92 

93 Returns 

94 ------- 

95 polygon : `lsst.afw.geom.Polygon` 

96 Polygon defining the boundary of the vignetted region. 

97 """ 

98 theta = np.linspace(0, 2*np.pi, num=self.config.numPolygonPoints, endpoint=False) 

99 x = self.config.radius*np.cos(theta) + self.config.xCenter 

100 y = self.config.radius*np.sin(theta) + self.config.yCenter 

101 points = np.array([x, y]).transpose() 

102 fpPolygon = afwGeom.Polygon([geom.Point2D(x1, y1) for x1, y1 in reversed(points)]) 

103 if exposure is None: 

104 return fpPolygon 

105 

106 # Exposure was provided, so attach the validPolygon associated with the 

107 # vignetted region. 

108 setValidPolygonCcdIntersect(exposure, fpPolygon, log=log) 

109 

110 if doUpdateMask: 

111 polygon = exposure.getInfo().getValidPolygon() 

112 maskVignettedRegion(exposure, polygon, maskPlane="NO_DATA", vignetteValue=vignetteValue, log=log) 

113 return fpPolygon 

114 

115 

116def setValidPolygonCcdIntersect(ccdExposure, fpPolygon, log=None): 

117 """Set valid polygon on ccdExposure associated with focal plane polygon. 

118 

119 The ccd exposure's valid polygon is the intersection of fpPolygon, 

120 a valid polygon in focal plane coordinates, and the ccd corners, 

121 in ccd pixel coordinates. 

122 

123 Parameters 

124 ---------- 

125 ccdExposure : `lsst.afw.image.Exposure` 

126 Exposure to process. 

127 fpPolygon : `lsst.afw.geom.Polygon` 

128 Polygon in focal plane coordinates. 

129 log : `logging.Logger`, optional 

130 Log object to write to. 

131 """ 

132 # Get ccd corners in focal plane coordinates 

133 ccd = ccdExposure.getDetector() 

134 fpCorners = ccd.getCorners(cameraGeom.FOCAL_PLANE) 

135 ccdPolygon = afwGeom.Polygon(fpCorners) 

136 # Get intersection of ccd corners with fpPolygon 

137 try: 

138 intersect = ccdPolygon.intersectionSingle(fpPolygon) 

139 except afwGeom.SinglePolygonException: 

140 intersect = None 

141 if intersect is not None: 

142 # Transform back to pixel positions and build new polygon 

143 ccdPoints = ccd.transform(intersect, cameraGeom.FOCAL_PLANE, cameraGeom.PIXELS) 

144 validPolygon = afwGeom.Polygon(ccdPoints) 

145 ccdExposure.getInfo().setValidPolygon(validPolygon) 

146 else: 

147 if log is not None: 

148 log.info("Ccd exposure does not overlap with focal plane polygon. Not setting validPolygon.") 

149 

150 

151def maskVignettedRegion(exposure, polygon, maskPlane="NO_DATA", vignetteValue=None, log=None): 

152 """Add mask bit to image pixels according to vignetted polygon region. 

153 

154 NOTE: this function could be used to mask and replace pixels associated 

155 with any polygon in the exposure pixel coordinates. 

156 

157 Parameters 

158 ---------- 

159 exposure : `lsst.afw.image.Exposure` 

160 Image whose mask plane is to be updated. 

161 polygon : `lsst.afw.geom.Polygon` 

162 Polygon region defining the vignetted region in the pixel coordinates 

163 of ``exposure``. 

164 maskPlane : `str`, optional 

165 Mask plane to assign vignetted pixels to. 

166 vignetteValue : `float` or `None`, optional 

167 Value to assign to the image array pixels within the ``polygon`` 

168 region. If `None`, image pixel values are not replaced. 

169 log : `logging.Logger`, optional 

170 Log object to write to. 

171 

172 Raises 

173 ------ 

174 RuntimeError 

175 Raised if no valid polygon exists. 

176 """ 

177 log = log if log else logging.getLogger(__name__) 

178 if not polygon: 

179 log.info("No polygon provided. Masking nothing.") 

180 return 

181 

182 fullyIlluminated = True 

183 if not all(polygon.contains(exposure.getBBox().getCorners())): 

184 fullyIlluminated = False 

185 log.info("Exposure is fully illuminated? %s", fullyIlluminated) 

186 

187 if not fullyIlluminated: 

188 # Scan pixels. 

189 mask = exposure.getMask() 

190 xx, yy = np.meshgrid(np.arange(0, mask.getWidth(), dtype=float), 

191 np.arange(0, mask.getHeight(), dtype=float)) 

192 

193 vignMask = ~(polygon.contains(xx, yy)) 

194 

195 bitMask = mask.getPlaneBitMask(maskPlane) 

196 maskArray = mask.getArray() 

197 maskArray[vignMask] |= bitMask 

198 log.info("Exposure contains {} vignetted pixels which are now masked with mask plane {}.". 

199 format(np.count_nonzero(vignMask), maskPlane)) 

200 

201 if vignetteValue is not None: 

202 imageArray = exposure.getImage().getArray() 

203 imageArray[vignMask] = vignetteValue 

204 log.info("Vignetted pixels in image array have been replaced with {}.".format(vignetteValue))