Coverage for python/lsst/afw/display/rgb/rgbContinued.py : 12%

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#
23import numpy as np
24from deprecated.sphinx import deprecated
26import lsst.afw.image as afwImage
27import lsst.afw.math as afwMath
28from .rgb import replaceSaturatedPixels, getZScale
31def computeIntensity(imageR, imageG=None, imageB=None):
32 """Return a naive total intensity from the red, blue, and green intensities
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
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
52 imageRGB = [imageR, imageG, imageB]
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()
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
66 if hasattr(imageR, "getImage"): # a maskedImage
67 intensity = afwImage.makeMaskedImage(Image(intensity))
68 elif hasattr(imageR, "getArray"):
69 intensity = Image(intensity)
71 return intensity
74class Mapping:
75 """Base class to map red, blue, green intensities into uint8 values
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 """
87 def __init__(self, minimum=None, image=None):
88 self._uint8Max = float(np.iinfo(np.uint8).max)
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"
96 self.minimum = minimum
97 self._image = image
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
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
122 if imageG is None:
123 imageG = imageR
124 if imageB is None:
125 imageB = imageR
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()
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)
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
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}")
155 for i, im in enumerate(imageRGB):
156 imageRGB[i] = scipy.misc.imresize(
157 im, size, interp='bilinear', mode='F')
159 return np.dstack(self._convertImagesToUint8(*imageRGB)).astype(np.uint8)
161 def intensity(self, imageR, imageG, imageB):
162 """Return the total intensity from the red, blue, and green intensities
164 Notes
165 -----
166 This is a naive computation, and may be overridden by subclasses
167 """
168 return computeIntensity(imageR, imageG, imageB)
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))
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]
184 fac = self.mapIntensityToUint8(self.intensity(imageR, imageG, imageB))
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
193 pixmax = self._uint8Max
194 # copies -- could work row by row to minimise memory usage
195 r0, g0, b0 = imageRGB
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
209 imageRGB[i] = c
211 return imageRGB
214class LinearMapping(Mapping):
215 """A linear map of red, blue, green intensities into uint8 values
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 """
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"
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)
238 Mapping.__init__(self, minimum, image)
239 self.maximum = maximum
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)
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)
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))
260class ZScaleMapping(LinearMapping):
261 """A mapping for a linear stretch chosen by the zscale algorithm
262 (preserving colours independent of brightness)
264 x = (I - minimum)/range
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 """
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)
280 LinearMapping.__init__(self, z1, z2, image)
283class AsinhMapping(Mapping):
284 """A mapping for an asinh stretch (preserving colours independent of brightness)
286 x = asinh(Q (I - minimum)/range)/Q
288 Notes
289 -----
290 This reduces to a linear stretch if Q == 0
292 See http://adsabs.harvard.edu/abs/2004PASP..116..133L
293 """
295 def __init__(self, minimum, dataRange, Q=8):
296 Mapping.__init__(self, minimum)
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
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)
313 self._soften = Q/float(dataRange)
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)
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)
325class AsinhZScaleMapping(AsinhMapping):
326 """A mapping for an asinh stretch, estimating the linear stretch by zscale
328 x = asinh(Q (I - z1)/(z2 - z1))/Q
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
339 N.b. pedestal, if not None, is removed from the images when calculating the zscale
340 stretch, and added back into Mapping.minimum[]
342 See also
343 --------
344 AsinhMapping
345 """
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]
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]
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()
368 image[i] = im - pedestal[i] # n.b. a copy
369 else:
370 pedestal = len(image)*[0.0]
372 image = computeIntensity(*image)
374 zscale = ZScaleMapping(image)
375 # zscale.minimum is always a triple
376 dataRange = zscale.maximum - zscale.minimum[0]
377 minimum = zscale.minimum
379 for i, level in enumerate(pedestal):
380 minimum[i] += level
382 AsinhMapping.__init__(self, minimum, dataRange, Q)
383 self._image = image # support self.makeRgbImage()
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
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
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)
423 asinhMap = AsinhMapping(minimum, dataRange, Q)
424 rgb = asinhMap.makeRgbImage(imageR, imageG, imageB,
425 xSize=xSize, ySize=ySize, rescaleFactor=rescaleFactor)
427 if fileName:
428 writeRGB(fileName, rgb)
430 return rgb
433def displayRGB(rgb, show=True):
434 """Display an rgb image using matplotlib
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
450def writeRGB(fileName, rgbImage):
451 """Write an RGB image to disk
453 Parameters
454 ----------
455 fileName : `str`
456 The output file. The suffix defines the format, and must be supported by matplotlib
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).
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)
468#
469# Support the legacy API
470#
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 """
479 def __init__(self, minimum, dataRange, Q):
480 self.minimum = minimum
481 self.dataRange = dataRange
482 self.Q = Q
485class _RgbImageF:
486 """Deprecated object used to support legacy API
487 """
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)
493 def write(self, fileName):
494 writeRGB(fileName, self.rgb)
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)