Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

# Copyright (C) 2017 HSC Software Team 

# Copyright (C) 2017 Sogo Mineo 

# 

# This program is free software: you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# This program is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the GNU General Public License 

# along with this program. If not, see <http://www.gnu.org/licenses/>. 

 

__all__ = ["SubaruStrayLightTask"] 

 

import datetime 

 

import numpy 

from astropy.io import fits 

import scipy.interpolate 

 

from lsst.ip.isr.straylight import StrayLightConfig, StrayLightTask 

 

from . import waveletCompression 

from .rotatorAngle import inrStartEnd 

 

 

BAD_THRESHOLD = 500 # Threshold for identifying bad pixels in the reconstructed dark image 

 

 

# TODO DM-16805: This doesn't match the rest of the obs_subaru/ISR code. 

class SubaruStrayLightTask(StrayLightTask): 

"""Remove stray light in the y-band 

 

LEDs in an encoder in HSC are producing stray light on the detectors, 

producing the "Eye of Y-band" feature. It can be removed by 

subtracting open-shutter darks. However, because the pattern of stray 

light varies with rotator angle, many dark exposures are required. 

To reduce the data volume for the darks, the images have been 

compressed using wavelets. The code used to construct these is at: 

 

https://hsc-gitlab.mtk.nao.ac.jp/sogo.mineo/ybackground/ 

 

This Task retrieves the appropriate dark, uncompresses it and 

uses it to remove the stray light from an exposure. 

""" 

ConfigClass = StrayLightConfig 

 

def readIsrData(self, dataRef, rawExposure): 

# Docstring inherited from StrayLightTask.runIsrTask. 

# Note that this is run only in Gen2; in Gen3 we will rely on having 

# a proper butler-recognized dataset type with the right validity 

# ranges (though this has not yet been implemented). 

detId = rawExposure.getDetector().getId() 

filterName = rawExposure.getFilter().getName() 

if filterName != 'y': 

# No correction to be made 

return None 

if detId in range(104, 112): 

# No correction data: assume it's zero 

return None 

if rawExposure.getInfo().getVisitInfo().getDate().toPython() >= datetime.datetime(2018, 1, 1): 

# LEDs causing the stray light have been covered up. 

# We believe there is no remaining stray light. 

return None 

return SubaruStrayLightData(dataRef.get("yBackground_filename")[0]) 

 

def run(self, exposure, strayLightData): 

"""Subtract the y-band stray light 

 

This relies on knowing the instrument rotator angle during the 

exposure. The FITS headers provide only the instrument rotator 

angle at the start of the exposure (INR_STR), but better 

stray light removal is obtained when we calculate the start and 

end instrument rotator angles ourselves (config parameter 

``doRotatorAngleCorrection=True``). 

 

Parameters 

---------- 

exposure : `lsst.afw.image.Exposure` 

Exposure to correct. 

strayLightData : `SubaruStrayLightData` 

An opaque object that contains any calibration data used to 

correct for stray light. 

""" 

exposureMetadata = exposure.getMetadata() 

detId = exposure.getDetector().getId() 

 

if self.config.doRotatorAngleCorrection: 

angleStart, angleEnd = inrStartEnd(exposure.getInfo().getVisitInfo()) 

self.log.debug( 

"(INR-STR, INR-END) = ({:g}, {:g}) (FITS header says ({:g}, {:g})).".format( 

angleStart, angleEnd, 

exposureMetadata.getDouble('INR-STR'), exposureMetadata.getDouble('INR-END')) 

) 

else: 

angleStart = exposureMetadata.getDouble('INR-STR') 

angleEnd = None 

 

self.log.info("Correcting y-band background") 

 

model = strayLightData.evaluate(angleStart, None if angleStart == angleEnd else angleEnd) 

 

# Some regions don't have useful model values because the amplifier is dead when the darks were taken 

# 

badAmps = {9: [0, 1, 2, 3], 33: [0, 1], 43: [0]} # Known bad amplifiers in the data: {ccdId: [ampId]} 

if detId in badAmps: 

isBad = numpy.zeros_like(model, dtype=bool) 

for ii in badAmps[detId]: 

amp = exposure.getDetector()[ii] 

box = amp.getBBox() 

isBad[box.getBeginY():box.getEndY(), box.getBeginX():box.getEndX()] = True 

mask = exposure.getMaskedImage().getMask() 

if numpy.all(isBad): 

model[:] = 0.0 

else: 

model[isBad] = numpy.median(model[~isBad]) 

mask.array[isBad] |= mask.getPlaneBitMask("SUSPECT") 

 

model *= exposure.getInfo().getVisitInfo().getExposureTime() 

exposure.image.array -= model 

 

 

class SubaruStrayLightData: 

"""Lazy-load object that reads and integrates the wavelet-compressed 

HSC y-band stray-light model. 

 

Parameters 

---------- 

filename : `str` 

Full path to a FITS files containing the stray-light model. 

""" 

 

def __init__(self, filename): 

self._filename = filename 

 

def evaluate(self, angle_start, angle_end=None): 

"""Get y-band background image array for a range of angles. 

 

It is hypothesized that the instrument rotator rotates at a constant 

angular velocity. This is not strictly true, but should be a 

sufficient approximation for the relatively short exposure times 

typical for HSC. 

 

Parameters 

---------- 

angle_start : `float` 

Instrument rotation angle in degrees at the start of the exposure. 

angle_end : `float`, optional 

Instrument rotation angle in degrees at the end of the exposure. 

If not provided, the returned array will reflect a snapshot at 

`angle_start`. 

 

Returns 

------- 

ccd_img : `numpy.ndarray` 

Background data for this exposure. 

""" 

hdulist = fits.open(self._filename) 

header = hdulist[0].header 

 

# full-size ccd height & channel width 

ccd_h, ch_w = header["F_NAXIS2"], header["F_NAXIS1"] 

# saved data is compressed to 1/2**scale_level of the original size 

image_scale_level = header["WTLEVEL2"], header["WTLEVEL1"] 

angle_scale_level = header["WTLEVEL3"] 

 

ccd_w = ch_w * len(hdulist) 

ccd_img = numpy.empty(shape=(ccd_h, ccd_w), dtype=numpy.float32) 

 

for ch, hdu in enumerate(hdulist): 

volume = _upscale_volume(hdu.data, angle_scale_level) 

 

if angle_end is None: 

img = volume(angle_start) 

else: 

img = volume.integrate(angle_start, angle_end) * (1.0 / (angle_end - angle_start)) 

 

ccd_img[:, ch_w*ch:ch_w*(ch+1)] = _upscale_image(img, (ccd_h, ch_w), image_scale_level) 

 

# Some regions don't have useful values because the amplifier is dead when the darks were taken 

# is_bad = ccd_img > BAD_THRESHOLD 

# ccd_img[is_bad] = numpy.median(ccd_img[~is_bad]) 

 

return ccd_img 

 

 

def _upscale_image(img, target_shape, level): 

""" 

Upscale the given image to `target_shape` . 

 

@param img (numpy.array[][]) 

Compressed image. `img.shape` must agree 

with waveletCompression.scaled_size(target_shape, level) 

@param target_shape ((int, int)) 

The shape of upscaled image, which is to be returned. 

@param level (int or tuple of int) 

Level of multiresolution analysis (or synthesis) 

 

@return (numpy.array[][]) 

""" 

h, w = waveletCompression.scaled_size(target_shape, level) 

 

large_img = numpy.zeros(shape=target_shape, dtype=float) 

large_img[:h, :w] = img 

 

return waveletCompression.icdf_9_7(large_img, level) 

 

 

def _upscale_volume(volume, level): 

""" 

Upscale the given volume (= sequence of images) along the 0-th axis, 

and return an instance of a interpolation object that interpolates 

the 0-th axis. The 0-th axis is the instrument rotation. 

 

@param volume (numpy.array[][][]) 

Sequence of images. 

@param level (int) 

Level of multiresolution analysis along the 0-th axis. 

 

@return (scipy.interpolate.CubicSpline) 

You get a slice of the volume at a specific angle (in degrees) 

by calling the returned value as `ret_value(angle)` . 

""" 

angles = 720 

_, h, w = volume.shape 

 

large_volume = numpy.zeros(shape=(angles+1, h, w), dtype=float) 

 

layers = waveletCompression.scaled_size(angles, level) 

large_volume[:layers] = volume 

 

large_volume[:-1] = waveletCompression.periodic_icdf_9_7_1d(large_volume[:-1], level, axis=0) 

large_volume[-1] = large_volume[0] 

 

x = numpy.arange(angles+1) / 2.0 

return scipy.interpolate.CubicSpline(x, large_volume, axis=0, bc_type="periodic")