LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
rgbContinued.py
Go to the documentation of this file.
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 
23 import numpy as np
24 from deprecated.sphinx import deprecated
25 
26 import lsst.afw.image as afwImage
27 import lsst.afw.math as afwMath
28 from .rgb import replaceSaturatedPixels, getZScale
29 
30 
31 def 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 
74 class 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  "Unable to rescale as scipy.misc is unavailable: %s" % 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 
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 
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 
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 
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 
386 def 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 
433 def 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 
450 def 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
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 
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)
500 def RgbImageF(imageR, imageG, imageB, mapping):
501  """Deprecated legacy API
502  """
503  return _RgbImageF(imageR, imageG, imageB, mapping)
Angle abs(Angle const &a)
Definition: Angle.h:106
std::pair< double, double > getZScale(image::Image< T > const &image, int const nSamples=1000, double const contrast=0.25)
Calculate an IRAF/ds9-style zscaling.
Definition: scaling.cc:167
def __init__(self, imageR, imageG, imageB, mapping)
def __init__(self, minimum, dataRange, Q)
def makeRGB(imageR, imageG=None, imageB=None, minimum=0, dataRange=5, Q=8, fileName=None, saturatedBorderWidth=0, saturatedPixelValue=None, xSize=None, ySize=None, rescaleFactor=None)
def __init__(self, minimum=None, image=None)
Definition: rgbContinued.py:87
def makeRgbImage(self, imageR=None, imageG=None, imageB=None, xSize=None, ySize=None, rescaleFactor=None)
def RgbImageF(imageR, imageG, imageB, mapping)
def _convertImagesToUint8(self, imageR, imageG, imageB)
void replaceSaturatedPixels(ImageT &rim, ImageT &gim, ImageT &bim, int borderWidth=2, float saturatedPixelValue=65535)
Definition: saturated.cc:30
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT >> image, typename std::shared_ptr< Mask< MaskPixelT >> mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT >> variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1279
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def writeRGB(fileName, rgbImage)
def __init__(self, minimum, dataRange, Q=8)
def intensity(self, imageR, imageG, imageB)
def __init__(self, image, Q=8, pedestal=None)
def __init__(self, image, nSamples=1000, contrast=0.25)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def computeIntensity(imageR, imageG=None, imageB=None)
Definition: rgbContinued.py:31
daf::base::PropertyList * list
Definition: fits.cc:903
def __init__(self, minimum=None, maximum=None, image=None)