lsst.meas.algorithms g4a7591d645+ff8a1bd350
Loading...
Searching...
No Matches
gp_interpolation.py
Go to the documentation of this file.
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 numpy as np
23from lsst.meas.algorithms import CloughTocher2DInterpolatorUtils as ctUtils
24from lsst.geom import Box2I, Point2I
25from lsst.afw.geom import SpanSet
26import copy
27import treecorr
28import treegp
29
30import logging
31
32# We need to explicitly turn off multiprocessing in treecorr which is used
33# by treegp.
34treecorr.set_max_omp_threads(1)
35
36__all__ = [
37 "InterpolateOverDefectGaussianProcess",
38 "GaussianProcessTreegp",
39]
40
41
42def updateMaskFromArray(mask, bad_pixel, interpBit):
43 """
44 Update the mask array with the given bad pixels.
45
46 Parameters
47 ----------
48 mask : `lsst.afw.image.MaskedImage`
49 The mask image to update.
50 bad_pixel : `np.array`
51 An array-like object containing the coordinates of the bad pixels.
52 Each row should contain the x and y coordinates of a bad pixel.
53 interpBit : `int`
54 The bit value to set for the bad pixels in the mask.
55 """
56 x0 = mask.getX0()
57 y0 = mask.getY0()
58 for row in bad_pixel:
59 x = int(row[0] - x0)
60 y = int(row[1] - y0)
61 mask.array[y, x] |= interpBit
62 # TO DO --> might be better: mask.array[int(bad_pixel[:,1]-y0), int(bad_pixel[:,0]-x)] |= interpBit
63
64
65def median_with_mad_clipping(data, mad_multiplier=2.0):
66 """
67 Calculate the median of the input data after applying Median Absolute Deviation (MAD) clipping.
68
69 The MAD clipping method is used to remove outliers from the data. The median of the data is calculated,
70 and then the MAD is calculated as the median absolute deviation from the median. The data is then clipped
71 by removing values that are outside the range of median +/- mad_multiplier * MAD. Finally, the median of
72 the clipped data is returned.
73
74 Parameters:
75 -----------
76 data : `np.array`
77 Input data array.
78 mad_multiplier : `float`, optional
79 Multiplier for the MAD value used for clipping. Default is 2.0.
80
81 Returns:
82 --------
83 median_clipped : `float`
84 Median value of the clipped data.
85
86 Examples:
87 ---------
88 >>> data = [1, 2, 3, 4, 5, 100]
89 >>> median_with_mad_clipping(data)
90 3.5
91 """
92 median = np.median(data)
93 mad = np.median(np.abs(data - median))
94 clipping_range = mad_multiplier * mad
95 clipped_data = np.clip(data, median - clipping_range, median + clipping_range)
96 median_clipped = np.median(clipped_data)
97 return median_clipped
98
99
101 """
102 Gaussian Process Treegp class for Gaussian Process interpolation.
103
104 The basic GP regression, which uses Cholesky decomposition.
105
106 Parameters:
107 -----------
108 std : `float`, optional
109 Standard deviation of the Gaussian Process kernel. Default is 1.0.
110 correlation_length : `float`, optional
111 Correlation length of the Gaussian Process kernel. Default is 1.0.
112 white_noise : `float`, optional
113 White noise level of the Gaussian Process. Default is 0.0.
114 mean : `float`, optional
115 Mean value of the Gaussian Process. Default is 0.0.
116 """
117
118 def __init__(self, std=1.0, correlation_length=1.0, white_noise=0.0, mean=0.0):
119 self.std = std
120 self.correlation_length = correlation_length
121 self.white_noise = white_noise
122 self.mean = mean
123
124 # Looks like weird to do that, but this is justified.
125 # in GP if no noise is provided, even if matrix
126 # can be inverted, it wont invert because of numerical
127 # issue (det(K)~0). Add a little bit of noise allow
128 # to compute a numerical solution in the case of no
129 # external noise is added. Wont happened on real
130 # image but help for unit test.
131 if self.white_noise == 0.0:
132 self.white_noise = 1e-5
133
134 def fit(self, x_train, y_train):
135 """
136 Fit the Gaussian Process to the given training data.
137
138 Parameters:
139 -----------
140 x_train : `np.array`
141 Input features for the training data.
142 y_train : `np.array`
143 Target values for the training data.
144 """
145 kernel = f"{self.std}**2 * RBF({self.correlation_length})"
146 self.gp = treegp.GPInterpolation(
147 kernel=kernel,
148 optimizer="none",
149 normalize=False,
150 white_noise=self.white_noise,
151 )
152 self.gp.initialize(x_train, y_train - self.mean)
153 self.gp.solve()
154
155 def predict(self, x_predict):
156 """
157 Predict the target values for the given input features.
158
159 Parameters:
160 -----------
161 x_predict : `np.array`
162 Input features for the prediction.
163
164 Returns:
165 --------
166 y_pred : `np.array`
167 Predicted target values.
168 """
169 y_pred = self.gp.predict(x_predict)
170 return y_pred + self.mean
171
172
174 """
175 InterpolateOverDefectGaussianProcess class performs Gaussian Process
176 (GP) interpolation over defects in an image.
177
178 Parameters:
179 -----------
180 masked_image : `lsst.afw.image.MaskedImage`
181 The masked image containing the defects to be interpolated.
182 defects : `list`[`str`], optional
183 The types of defects to be interpolated. Default is ["SAT"].
184 fwhm : `float`, optional
185 The full width at half maximum (FWHM) of the PSF. Default is 5.
186 bin_spacing : `int`, optional
187 The spacing between bins for good pixel binning. Default is 10.
188 threshold_dynamic_binning : `int`, optional
189 The threshold for dynamic binning. Default is 1000.
190 threshold_subdivide : `int`, optional
191 The threshold for sub-dividing the bad pixel array to avoid memory error. Default is 20000.
192 correlation_length_cut : `int`, optional
193 The factor by which to dilate the bounding box around defects. Default is 5.
194 log : `lsst.log.Log`, `logging.Logger` or `None`, optional
195 Logger object used to write out messages. If `None` a default
196 logger will be used.
197 """
198
200 self,
201 masked_image,
202 defects=["SAT"],
203 fwhm=5,
204 bin_image=True,
205 bin_spacing=10,
206 threshold_dynamic_binning=1000,
207 threshold_subdivide=20000,
208 correlation_length_cut=5,
209 log=None,
210 ):
211
212 self.log = log or logging.getLogger(__name__)
213
214 self.bin_image = bin_image
215 self.bin_spacing = bin_spacing
216 self.threshold_subdivide = threshold_subdivide
217 self.threshold_dynamic_binning = threshold_dynamic_binning
218
219 self.masked_image = masked_image
220 self.defects = defects
222 self.correlation_length_cut = correlation_length_cut
223
224 self.interpBit = self.masked_image.mask.getPlaneBitMask("INTRP")
225
226 def run(self):
227 """
228 Interpolate over the defects in the image.
229
230 Change self.masked_image .
231 """
232 if self.defects == [] or self.defects is None:
233 self.log.info("No defects found. No interpolation performed.")
234 else:
235 mask = self.masked_image.getMask()
236 bad_pixel_mask = mask.getPlaneBitMask(self.defects)
237 bad_mask_span_set = SpanSet.fromMask(mask, bad_pixel_mask).split()
238
239 bbox = self.masked_image.getBBox()
240 global_xmin, global_xmax = bbox.minX, bbox.maxX
241 global_ymin, global_ymax = bbox.minY, bbox.maxY
242
243 for spanset in bad_mask_span_set:
244 bbox = spanset.getBBox()
245 # Dilate the bbox to make sure we have enough good pixels around the defect
246 # For now, we dilate by 5 times the correlation length
247 # For GP with the isotropic kernel, points at the default value of
248 # correlation_length_cut=5 have negligible effect on the prediction.
249 bbox = bbox.dilatedBy(
251 ) # need integer as input.
252 xmin, xmax = max([global_xmin, bbox.minX]), min(global_xmax, bbox.maxX)
253 ymin, ymax = max([global_ymin, bbox.minY]), min(global_ymax, bbox.maxY)
254 localBox = Box2I(Point2I(xmin, ymin), Point2I(xmax - xmin, ymax - ymin))
255 masked_sub_image = self.masked_image[localBox]
256
257 masked_sub_image = self.interpolate_masked_sub_image(masked_sub_image)
258 self.masked_image[localBox] = masked_sub_image
259
260 def _good_pixel_binning(self, pixels):
261 """
262 Performs pixel binning using treegp.meanify
263
264 Parameters:
265 -----------
266 pixels : `np.array`
267 The array of pixels.
268
269 Returns:
270 --------
271 `np.array`
272 The binned array of pixels.
273 """
274
275 n_pixels = len(pixels[:, 0])
276 dynamic_binning = int(np.sqrt(n_pixels / self.threshold_dynamic_binning))
277 if n_pixels / self.bin_spacing**2 < n_pixels / dynamic_binning**2:
278 bin_spacing = self.bin_spacing
279 else:
280 bin_spacing = dynamic_binning
281 binning = treegp.meanify(bin_spacing=bin_spacing, statistics="mean")
282 binning.add_field(
283 pixels[:, :2],
284 pixels[:, 2:].T,
285 )
286 binning.meanify()
287 return np.array(
288 [binning.coords0[:, 0], binning.coords0[:, 1], binning.params0]
289 ).T
290
291 def interpolate_masked_sub_image(self, masked_sub_image):
292 """
293 Interpolate the masked sub-image.
294
295 Parameters:
296 -----------
297 masked_sub_image : `lsst.afw.image.MaskedImage`
298 The sub-masked image to be interpolated.
299
300 Returns:
301 --------
302 `lsst.afw.image.MaskedImage`
303 The interpolated sub-masked image.
304 """
305
306 cut = int(
308 ) # need integer as input.
309 bad_pixel, good_pixel = ctUtils.findGoodPixelsAroundBadPixels(
310 masked_sub_image, self.defects, buffer=cut
311 )
312 # Do nothing if bad pixel is None.
313 if bad_pixel.size == 0 or good_pixel.size == 0:
314 self.log.info("No bad or good pixels found. No interpolation performed.")
315 return masked_sub_image
316 # Do GP interpolation if bad pixel found.
317 else:
318 # gp interpolation
319 sub_image_array = masked_sub_image.getVariance().array
320 white_noise = np.sqrt(
321 np.mean(sub_image_array[np.isfinite(sub_image_array)])
322 )
323 kernel_amplitude = np.max(good_pixel[:, 2:])
324 if not np.isfinite(kernel_amplitude):
325 filter_finite = np.isfinite(good_pixel[:, 2:]).T[0]
326 good_pixel = good_pixel[filter_finite]
327 if good_pixel.size == 0:
328 self.log.info(
329 "No bad or good pixels found. No interpolation performed."
330 )
331 return masked_sub_image
332 # kernel amplitude might be better described by maximum value of good pixel given
333 # the data and not really a random gaussian field.
334 kernel_amplitude = np.max(good_pixel[:, 2:])
335
336 if self.bin_image:
337 try:
338 good_pixel = self._good_pixel_binning(copy.deepcopy(good_pixel))
339 except Exception:
340 self.log.info(
341 "Binning failed, use original good pixel array in interpolation."
342 )
343
344 # put this after binning as computing median is O(n*log(n))
345 clipped_median = median_with_mad_clipping(good_pixel[:, 2:])
346
348 std=np.sqrt(kernel_amplitude),
349 correlation_length=self.correlation_length,
350 white_noise=white_noise,
351 mean=clipped_median,
352 )
353 gp.fit(good_pixel[:, :2], np.squeeze(good_pixel[:, 2:]))
354 if bad_pixel.size < self.threshold_subdivide:
355 gp_predict = gp.predict(bad_pixel[:, :2])
356 bad_pixel[:, 2:] = gp_predict.reshape(np.shape(bad_pixel[:, 2:]))
357 else:
358 self.log.info("sub-divide bad pixel array to avoid memory error.")
359 for i in range(0, len(bad_pixel), self.threshold_subdivide):
360 end = min(i + self.threshold_subdivide, len(bad_pixel))
361 gp_predict = gp.predict(bad_pixel[i:end, :2])
362 bad_pixel[i:end, 2:] = gp_predict.reshape(
363 np.shape(bad_pixel[i:end, 2:])
364 )
365
366 # Update values
367 ctUtils.updateImageFromArray(masked_sub_image.image, bad_pixel)
368 updateMaskFromArray(masked_sub_image.mask, bad_pixel, self.interpBit)
369 return masked_sub_image
__init__(self, std=1.0, correlation_length=1.0, white_noise=0.0, mean=0.0)
__init__(self, masked_image, defects=["SAT"], fwhm=5, bin_image=True, bin_spacing=10, threshold_dynamic_binning=1000, threshold_subdivide=20000, correlation_length_cut=5, log=None)
median_with_mad_clipping(data, mad_multiplier=2.0)
updateMaskFromArray(mask, bad_pixel, interpBit)