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# LSST Data Management System 

3# Copyright 2015-2016 LSST/AURA 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import numpy as np 

24from deprecated.sphinx import deprecated 

25 

26import lsst.afw.image as afwImage 

27import lsst.afw.math as afwMath 

28from .rgb import replaceSaturatedPixels, getZScale 

29 

30 

31def computeIntensity(imageR, imageG=None, imageB=None): 

32 """Return a naive total intensity from the red, blue, and green intensities 

33 

34 Parameters 

35 ---------- 

36 imageR : `lsst.afw.image.MaskedImage`, `lsst.afw.image.Image`, or `numpy.ndarray`, (Nx, Ny) 

37 intensity of image that'll be mapped to red; or intensity if imageG and imageB are None 

38 imageG : `lsst.afw.image.MaskedImage`, `lsst.afw.image.Image`, or `numpy.ndarray`, (Nx, Ny) 

39 intensity of image that'll be mapped to green; or None 

40 imageB : `lsst.afw.image.MaskedImage`, `lsst.afw.image.Image`, or `numpy.ndarray`, (Nx, Ny) 

41 intensity of image that'll be mapped to blue; or None 

42 

43 Returns 

44 ------- 

45 image : type of ``imageR``, ``imageG``, and `imageB`` 

46 """ 

47 if imageG is None or imageB is None: 

48 assert imageG is None and imageB is None, \ 

49 "Please specify either a single image or red, green, and blue images" 

50 return imageR 

51 

52 imageRGB = [imageR, imageG, imageB] 

53 

54 for i, c in enumerate(imageRGB): 

55 if hasattr(c, "getImage"): 

56 c = imageRGB[i] = c.getImage() 

57 if hasattr(c, "getArray"): 

58 imageRGB[i] = c.getArray() 

59 

60 intensity = (imageRGB[0] + imageRGB[1] + imageRGB[2])/float(3) 

61 # 

62 # Repack into whatever type was passed to us 

63 # 

64 Image = afwImage.ImageU if intensity.dtype == 'uint16' else afwImage.ImageF 

65 

66 if hasattr(imageR, "getImage"): # a maskedImage 

67 intensity = afwImage.makeMaskedImage(Image(intensity)) 

68 elif hasattr(imageR, "getArray"): 

69 intensity = Image(intensity) 

70 

71 return intensity 

72 

73 

74class Mapping: 

75 """Base class to map red, blue, green intensities into uint8 values 

76 

77 Parameters 

78 ---------- 

79 minimum : `float` or sequence of `float` 

80 Intensity that should be mapped to black. If an array, has three 

81 elements for R, G, B. 

82 image 

83 The image to be used to calculate the mapping. 

84 If provided, also the default for makeRgbImage() 

85 """ 

86 

87 def __init__(self, minimum=None, image=None): 

88 self._uint8Max = float(np.iinfo(np.uint8).max) 

89 

90 try: 

91 len(minimum) 

92 except TypeError: 

93 minimum = 3*[minimum] 

94 assert len(minimum) == 3, "Please provide 1 or 3 values for minimum" 

95 

96 self.minimum = minimum 

97 self._image = image 

98 

99 def makeRgbImage(self, imageR=None, imageG=None, imageB=None, 

100 xSize=None, ySize=None, rescaleFactor=None): 

101 """Convert 3 arrays, imageR, imageG, and imageB into a numpy RGB image 

102 

103 imageR : `lsst.afw.image.Image` or `numpy.ndarray`, (Nx, Ny) 

104 Image to map to red (if `None`, use the image passed to the ctor) 

105 imageG : `lsst.afw.image.Image` or `numpy.ndarray`, (Nx, Ny), optional 

106 Image to map to green (if `None`, use imageR) 

107 imageB : `lsst.afw.image.Image` or `numpy.ndarray`, (Nx, Ny), optional 

108 Image to map to blue (if `None`, use imageR) 

109 xSize : `int`, optional 

110 Desired width of RGB image. If ``ySize`` is `None`, preserve aspect ratio 

111 ySize : `int`, optional 

112 Desired height of RGB image 

113 rescaleFactor : `float`, optional 

114 Make size of output image ``rescaleFactor*size`` of the input image 

115 """ 

116 if imageR is None: 

117 if self._image is None: 

118 raise RuntimeError( 

119 "You must provide an image (or pass one to the constructor)") 

120 imageR = self._image 

121 

122 if imageG is None: 

123 imageG = imageR 

124 if imageB is None: 

125 imageB = imageR 

126 

127 imageRGB = [imageR, imageG, imageB] 

128 for i, c in enumerate(imageRGB): 

129 if hasattr(c, "getImage"): 

130 c = imageRGB[i] = c.getImage() 

131 if hasattr(c, "getArray"): 

132 imageRGB[i] = c.getArray() 

133 

134 if xSize is not None or ySize is not None: 

135 assert rescaleFactor is None, "You may not specify a size and rescaleFactor" 

136 h, w = imageRGB[0].shape 

137 if ySize is None: 

138 ySize = int(xSize*h/float(w) + 0.5) 

139 elif xSize is None: 

140 xSize = int(ySize*w/float(h) + 0.5) 

141 

142 size = (ySize, xSize) # n.b. y, x order for scipy 

143 elif rescaleFactor is not None: 

144 size = float(rescaleFactor) # an int is intepreted as a percentage 

145 else: 

146 size = None 

147 

148 if size is not None: 

149 try: 

150 import scipy.misc 

151 except ImportError as e: 

152 raise RuntimeError( 

153 f"Unable to rescale as scipy.misc is unavailable: {e}") 

154 

155 for i, im in enumerate(imageRGB): 

156 imageRGB[i] = scipy.misc.imresize( 

157 im, size, interp='bilinear', mode='F') 

158 

159 return np.dstack(self._convertImagesToUint8(*imageRGB)).astype(np.uint8) 

160 

161 def intensity(self, imageR, imageG, imageB): 

162 """Return the total intensity from the red, blue, and green intensities 

163 

164 Notes 

165 ----- 

166 This is a naive computation, and may be overridden by subclasses 

167 """ 

168 return computeIntensity(imageR, imageG, imageB) 

169 

170 def mapIntensityToUint8(self, intensity): 

171 """Map an intensity into the range of a uint8, [0, 255] (but not converted to uint8) 

172 """ 

173 with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit 

174 return np.where(intensity <= 0, 0, 

175 np.where(intensity < self._uint8Max, intensity, self._uint8Max)) 

176 

177 def _convertImagesToUint8(self, imageR, imageG, imageB): 

178 """Use the mapping to convert images imageR, imageG, and imageB to a triplet of uint8 images 

179 """ 

180 imageR = imageR - self.minimum[0] # n.b. makes copy 

181 imageG = imageG - self.minimum[1] 

182 imageB = imageB - self.minimum[2] 

183 

184 fac = self.mapIntensityToUint8(self.intensity(imageR, imageG, imageB)) 

185 

186 imageRGB = [imageR, imageG, imageB] 

187 with np.errstate(invalid="ignore"): # suppress NAN warnings 

188 for c in imageRGB: 

189 c *= fac 

190 # individual bands can still be < 0, even if fac isn't 

191 c[c < 0] = 0 

192 

193 pixmax = self._uint8Max 

194 # copies -- could work row by row to minimise memory usage 

195 r0, g0, b0 = imageRGB 

196 

197 # n.b. np.where can't and doesn't short-circuit 

198 with np.errstate(invalid='ignore', divide='ignore'): 

199 for i, c in enumerate(imageRGB): 

200 c = np.where(r0 > g0, 

201 np.where(r0 > b0, 

202 np.where(r0 >= pixmax, c*pixmax/r0, c), 

203 np.where(b0 >= pixmax, c*pixmax/b0, c)), 

204 np.where(g0 > b0, 

205 np.where(g0 >= pixmax, c*pixmax/g0, c), 

206 np.where(b0 >= pixmax, c*pixmax/b0, c))).astype(np.uint8) 

207 c[c > pixmax] = pixmax 

208 

209 imageRGB[i] = c 

210 

211 return imageRGB 

212 

213 

214class LinearMapping(Mapping): 

215 """A linear map of red, blue, green intensities into uint8 values 

216 

217 Parameters 

218 ---------- 

219 minimum : `float` or sequence of `float` 

220 Intensity that should be mapped to black. If an array, has three 

221 elements for R, G, B. 

222 maximum : `float` 

223 Intensity that should be mapped to white 

224 image 

225 Image to estimate minimum/maximum if not explicitly set 

226 """ 

227 

228 def __init__(self, minimum=None, maximum=None, image=None): 

229 if minimum is None or maximum is None: 

230 assert image is not None, "You must provide an image if you don't set both minimum and maximum" 

231 

232 stats = afwMath.makeStatistics(image, afwMath.MIN | afwMath.MAX) 

233 if minimum is None: 

234 minimum = stats.getValue(afwMath.MIN) 

235 if maximum is None: 

236 maximum = stats.getValue(afwMath.MAX) 

237 

238 Mapping.__init__(self, minimum, image) 

239 self.maximum = maximum 

240 

241 if maximum is None: 

242 self._range = None 

243 else: 

244 assert maximum - minimum != 0, "minimum and maximum values must not be equal" 

245 self._range = float(maximum - minimum) 

246 

247 def mapIntensityToUint8(self, intensity): 

248 """Return an array which, when multiplied by an image, returns that 

249 image mapped to the range of a uint8, [0, 255] (but not converted to uint8) 

250 

251 The intensity is assumed to have had ``minimum`` subtracted (as that 

252 can be done per-band) 

253 """ 

254 with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit 

255 return np.where(intensity <= 0, 0, 

256 np.where(intensity >= self._range, 

257 self._uint8Max/intensity, self._uint8Max/self._range)) 

258 

259 

260class ZScaleMapping(LinearMapping): 

261 """A mapping for a linear stretch chosen by the zscale algorithm 

262 (preserving colours independent of brightness) 

263 

264 x = (I - minimum)/range 

265 

266 Parameters 

267 ---------- 

268 image 

269 Image whose parameters are desired 

270 nSamples : `int` 

271 The number of samples to use to estimate the zscale parameters 

272 contrast : `float` 

273 """ 

274 

275 def __init__(self, image, nSamples=1000, contrast=0.25): 

276 if not hasattr(image, "getArray"): 

277 image = afwImage.ImageF(image) 

278 z1, z2 = getZScale(image, nSamples, contrast) 

279 

280 LinearMapping.__init__(self, z1, z2, image) 

281 

282 

283class AsinhMapping(Mapping): 

284 """A mapping for an asinh stretch (preserving colours independent of brightness) 

285 

286 x = asinh(Q (I - minimum)/range)/Q 

287 

288 Notes 

289 ----- 

290 This reduces to a linear stretch if Q == 0 

291 

292 See http://adsabs.harvard.edu/abs/2004PASP..116..133L 

293 """ 

294 

295 def __init__(self, minimum, dataRange, Q=8): 

296 Mapping.__init__(self, minimum) 

297 

298 # 32bit floating point machine epsilon; sys.float_info.epsilon is 64bit 

299 epsilon = 1.0/2**23 

300 if abs(Q) < epsilon: 

301 Q = 0.1 

302 else: 

303 Qmax = 1e10 

304 if Q > Qmax: 

305 Q = Qmax 

306 

307 if False: 

308 self._slope = self._uint8Max/Q # gradient at origin is self._slope 

309 else: 

310 frac = 0.1 # gradient estimated using frac*range is _slope 

311 self._slope = frac*self._uint8Max/np.arcsinh(frac*Q) 

312 

313 self._soften = Q/float(dataRange) 

314 

315 def mapIntensityToUint8(self, intensity): 

316 """Return an array which, when multiplied by an image, returns that image mapped to the range of a 

317 uint8, [0, 255] (but not converted to uint8) 

318 

319 The intensity is assumed to have had minimum subtracted (as that can be done per-band) 

320 """ 

321 with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit 

322 return np.where(intensity <= 0, 0, np.arcsinh(intensity*self._soften)*self._slope/intensity) 

323 

324 

325class AsinhZScaleMapping(AsinhMapping): 

326 """A mapping for an asinh stretch, estimating the linear stretch by zscale 

327 

328 x = asinh(Q (I - z1)/(z2 - z1))/Q 

329 

330 Parameters 

331 ---------- 

332 image 

333 The image to analyse, or a list of 3 images to be converted to an intensity image 

334 Q : `int` 

335 The asinh softening parameter 

336 pedestal : `float` or sequence of `float`, optional 

337 The value, or array of 3 values, to subtract from the images 

338 

339 N.b. pedestal, if not None, is removed from the images when calculating the zscale 

340 stretch, and added back into Mapping.minimum[] 

341 

342 See also 

343 -------- 

344 AsinhMapping 

345 """ 

346 

347 def __init__(self, image, Q=8, pedestal=None): 

348 try: 

349 assert len(image) in (1, 3,), "Please provide 1 or 3 images" 

350 except TypeError: 

351 image = [image] 

352 

353 if pedestal is not None: 

354 try: 

355 assert len(pedestal) in ( 

356 1, 3,), "Please provide 1 or 3 pedestals" 

357 except TypeError: 

358 pedestal = 3*[pedestal] 

359 

360 image = list(image) # needs to be mutable 

361 for i, im in enumerate(image): 

362 if pedestal[i] != 0.0: 

363 if hasattr(im, "getImage"): 

364 im = im.getImage() 

365 if hasattr(im, "getArray"): 

366 im = im.getArray() 

367 

368 image[i] = im - pedestal[i] # n.b. a copy 

369 else: 

370 pedestal = len(image)*[0.0] 

371 

372 image = computeIntensity(*image) 

373 

374 zscale = ZScaleMapping(image) 

375 # zscale.minimum is always a triple 

376 dataRange = zscale.maximum - zscale.minimum[0] 

377 minimum = zscale.minimum 

378 

379 for i, level in enumerate(pedestal): 

380 minimum[i] += level 

381 

382 AsinhMapping.__init__(self, minimum, dataRange, Q) 

383 self._image = image # support self.makeRgbImage() 

384 

385 

386def makeRGB(imageR, imageG=None, imageB=None, minimum=0, dataRange=5, Q=8, fileName=None, 

387 saturatedBorderWidth=0, saturatedPixelValue=None, 

388 xSize=None, ySize=None, rescaleFactor=None): 

389 """Make a set of three images into an RGB image using an asinh stretch and 

390 optionally write it to disk 

391 

392 Parameters 

393 ---------- 

394 imageR 

395 imageG 

396 imageB 

397 minimum : `float` or sequence of `float` 

398 dataRange 

399 Q : `int` 

400 fileName : `str` 

401 The output file. The suffix defines the format, and must be supported by matplotlib 

402 saturatedBorderWidth 

403 If saturatedBorderWidth is non-zero, replace saturated pixels with 

404 ``saturatedPixelValue``. Note that replacing saturated pixels requires 

405 that the input images be `lsst.afw.image.MaskedImage`. 

406 saturatedPixelValue 

407 xSize 

408 ySize 

409 rescaleFactor 

410 """ 

411 if imageG is None: 

412 imageG = imageR 

413 if imageB is None: 

414 imageB = imageR 

415 

416 if saturatedBorderWidth: 

417 if saturatedPixelValue is None: 

418 raise ValueError( 

419 "saturatedPixelValue must be set if saturatedBorderWidth is set") 

420 replaceSaturatedPixels(imageR, imageG, imageB, 

421 saturatedBorderWidth, saturatedPixelValue) 

422 

423 asinhMap = AsinhMapping(minimum, dataRange, Q) 

424 rgb = asinhMap.makeRgbImage(imageR, imageG, imageB, 

425 xSize=xSize, ySize=ySize, rescaleFactor=rescaleFactor) 

426 

427 if fileName: 

428 writeRGB(fileName, rgb) 

429 

430 return rgb 

431 

432 

433def displayRGB(rgb, show=True): 

434 """Display an rgb image using matplotlib 

435 

436 Parameters 

437 ---------- 

438 rgb 

439 The RGB image in question 

440 show : `bool` 

441 If `True`, call `matplotlib.pyplot.show()` 

442 """ 

443 import matplotlib.pyplot as plt 

444 plt.imshow(rgb, interpolation='nearest', origin="lower") 

445 if show: 

446 plt.show() 

447 return plt 

448 

449 

450def writeRGB(fileName, rgbImage): 

451 """Write an RGB image to disk 

452 

453 Parameters 

454 ---------- 

455 fileName : `str` 

456 The output file. The suffix defines the format, and must be supported by matplotlib 

457 

458 Most versions of matplotlib support png and pdf (although the eps/pdf/svg writers may be buggy, 

459 possibly due an interaction with useTeX=True in the matplotlib settings). 

460 

461 If your matplotlib bundles pil/pillow you should also be able to write jpeg and tiff files. 

462 rgbImage 

463 The image, as made by e.g. makeRGB 

464 """ 

465 import matplotlib.image 

466 matplotlib.image.imsave(fileName, rgbImage) 

467 

468# 

469# Support the legacy API 

470# 

471 

472 

473@deprecated(reason="Use `AsinhMapping` instead. To be removed after 20.0.0.", 

474 category=FutureWarning) # noqa: N801 

475class asinhMappingF: 

476 """Deprecated object used to support legacy API 

477 """ 

478 

479 def __init__(self, minimum, dataRange, Q): 

480 self.minimum = minimum 

481 self.dataRange = dataRange 

482 self.Q = Q 

483 

484 

485class _RgbImageF: 

486 """Deprecated object used to support legacy API 

487 """ 

488 

489 def __init__(self, imageR, imageG, imageB, mapping): 

490 asinh = AsinhMapping(mapping.minimum, mapping.dataRange, mapping.Q) 

491 self.rgb = asinh.makeRgbImage(imageR, imageG, imageB) 

492 

493 def write(self, fileName): 

494 writeRGB(fileName, self.rgb) 

495 

496 

497@deprecated( 

498 reason="Use `Mapping.makeRgbImage` instead. To be removed after 20.0.0.", 

499 category=FutureWarning) 

500def RgbImageF(imageR, imageG, imageB, mapping): 

501 """Deprecated legacy API 

502 """ 

503 return _RgbImageF(imageR, imageG, imageB, mapping)