LSSTApplications  15.0+21,16.0+1,16.0+3,16.0+4,16.0+8,16.0-1-g2115a9e+2,16.0-1-g4515a79+6,16.0-1-g5c6f5ee+4,16.0-1-g7bb14cc,16.0-1-g80120d7+4,16.0-1-g98efed3+4,16.0-1-gb7f560d+1,16.0-14-gb4f0cd2fa,16.0-2-g1ad129e+1,16.0-2-g2ed7261+1,16.0-2-g311bfd2,16.0-2-g568a347+3,16.0-2-g852da13+6,16.0-2-gd4c87cb+3,16.0-3-g099ede0,16.0-3-g150e024+3,16.0-3-g1f513a6,16.0-3-g958ce35,16.0-4-g08dccf71+4,16.0-4-g128aaef,16.0-4-g84f75fb+5,16.0-4-gcfd1396+4,16.0-4-gde8cee2,16.0-4-gdfb0d14+1,16.0-5-g7bc0afb+3,16.0-5-g86fb31a+3,16.0-6-g2dd73041+4,16.0-7-g95fb7bf,16.0-7-gc37dbc2+4,w.2018.28
LSSTDataManagementBasePackage
rgb.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 
25 import lsst.afw.image as afwImage
26 import lsst.afw.math as afwMath
27 from lsst.afw.display.displayLib import replaceSaturatedPixels, getZScale
28 
29 
30 def computeIntensity(imageR, imageG=None, imageB=None):
31  """!Return a naive total intensity from the red, blue, and green intensities
32  @param imageR intensity of image that'll be mapped to red; or intensity if imageG and imageB are None
33  @param imageG intensity of image that'll be mapped to green; or None
34  @param imageB intensity of image that'll be mapped to blue; or None
35 
36  Inputs may be MaskedImages, Images, or numpy arrays and the return is of the same type
37  """
38  if imageG is None or imageB is None:
39  assert imageG is None and imageB is None, \
40  "Please specify either a single image or red, green, and blue images"
41  return imageR
42 
43  imageRGB = [imageR, imageG, imageB]
44 
45  for i, c in enumerate(imageRGB):
46  if hasattr(c, "getImage"):
47  c = imageRGB[i] = c.getImage()
48  if hasattr(c, "getArray"):
49  imageRGB[i] = c.getArray()
50 
51  intensity = (imageRGB[0] + imageRGB[1] + imageRGB[2])/float(3)
52  #
53  # Repack into whatever type was passed to us
54  #
55  Image = afwImage.ImageU if intensity.dtype == 'uint16' else afwImage.ImageF
56 
57  if hasattr(imageR, "getImage"): # a maskedImage
58  intensity = afwImage.makeMaskedImage(Image(intensity))
59  elif hasattr(imageR, "getArray"):
60  intensity = Image(intensity)
61 
62  return intensity
63 
64 
65 class Mapping:
66  """!Baseclass to map red, blue, green intensities into uint8 values"""
67 
68  def __init__(self, minimum=None, image=None):
69  """!Create a mapping
70  @param minimum Intensity that should be mapped to black (a scalar or array for R, G, B)
71  @param image The image to be used to calculate the mapping.
72 
73  If provided, also the default for makeRgbImage()
74  """
75  self._uint8Max = float(np.iinfo(np.uint8).max)
76 
77  try:
78  len(minimum)
79  except TypeError:
80  minimum = 3*[minimum]
81  assert len(minimum) == 3, "Please provide 1 or 3 values for minimum"
82 
83  self.minimum = minimum
84  self._image = image
85 
86  def makeRgbImage(self, imageR=None, imageG=None, imageB=None,
87  xSize=None, ySize=None, rescaleFactor=None):
88  """!Convert 3 arrays, imageR, imageG, and imageB into a numpy RGB image
89  @param imageR Image to map to red (if None, use the image passed to the ctor)
90  @param imageG Image to map to green (if None, use imageR)
91  @param imageB Image to map to blue (if None, use imageR)
92  @param xSize Desired width of RGB image (or None). If ySize is None, preserve aspect ratio
93  @param ySize Desired height of RGB image (or None)
94  @param rescaleFactor Make size of output image rescaleFactor*size of the input image (or None)
95 
96  N.b. images may be afwImage.Images or numpy arrays
97  """
98  if imageR is None:
99  if self._image is None:
100  raise RuntimeError(
101  "You must provide an image (or pass one to the constructor)")
102  imageR = self._image
103 
104  if imageG is None:
105  imageG = imageR
106  if imageB is None:
107  imageB = imageR
108 
109  imageRGB = [imageR, imageG, imageB]
110  for i, c in enumerate(imageRGB):
111  if hasattr(c, "getImage"):
112  c = imageRGB[i] = c.getImage()
113  if hasattr(c, "getArray"):
114  imageRGB[i] = c.getArray()
115 
116  if xSize is not None or ySize is not None:
117  assert rescaleFactor is None, "You may not specify a size and rescaleFactor"
118  h, w = imageRGB[0].shape
119  if ySize is None:
120  ySize = int(xSize*h/float(w) + 0.5)
121  elif xSize is None:
122  xSize = int(ySize*w/float(h) + 0.5)
123 
124  size = (ySize, xSize) # n.b. y, x order for scipy
125  elif rescaleFactor is not None:
126  size = float(rescaleFactor) # an int is intepreted as a percentage
127  else:
128  size = None
129 
130  if size is not None:
131  try:
132  import scipy.misc
133  except ImportError as e:
134  raise RuntimeError(
135  "Unable to rescale as scipy.misc is unavailable: %s" % e)
136 
137  for i, im in enumerate(imageRGB):
138  imageRGB[i] = scipy.misc.imresize(
139  im, size, interp='bilinear', mode='F')
140 
141  return np.dstack(self._convertImagesToUint8(*imageRGB)).astype(np.uint8)
142 
143  def intensity(self, imageR, imageG, imageB):
144  """!Return the total intensity from the red, blue, and green intensities
145 
146  This is a naive computation, and may be overridden by subclasses
147  """
148  return computeIntensity(imageR, imageG, imageB)
149 
150  def mapIntensityToUint8(self, intensity):
151  """Map an intensity into the range of a uint8, [0, 255] (but not converted to uint8)"""
152  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
153  return np.where(intensity <= 0, 0,
154  np.where(intensity < self._uint8Max, intensity, self._uint8Max))
155 
156  def _convertImagesToUint8(self, imageR, imageG, imageB):
157  """Use the mapping to convert images imageR, imageG, and imageB to a triplet of uint8 images"""
158  imageR = imageR - self.minimum[0] # n.b. makes copy
159  imageG = imageG - self.minimum[1]
160  imageB = imageB - self.minimum[2]
161 
162  fac = self.mapIntensityToUint8(self.intensity(imageR, imageG, imageB))
163 
164  imageRGB = [imageR, imageG, imageB]
165  with np.errstate(invalid="ignore"): # suppress NAN warnings
166  for c in imageRGB:
167  c *= fac
168  # individual bands can still be < 0, even if fac isn't
169  c[c < 0] = 0
170 
171  pixmax = self._uint8Max
172  # copies -- could work row by row to minimise memory usage
173  r0, g0, b0 = imageRGB
174 
175  # n.b. np.where can't and doesn't short-circuit
176  with np.errstate(invalid='ignore', divide='ignore'):
177  for i, c in enumerate(imageRGB):
178  c = np.where(r0 > g0,
179  np.where(r0 > b0,
180  np.where(r0 >= pixmax, c*pixmax/r0, c),
181  np.where(b0 >= pixmax, c*pixmax/b0, c)),
182  np.where(g0 > b0,
183  np.where(g0 >= pixmax, c*pixmax/g0, c),
184  np.where(b0 >= pixmax, c*pixmax/b0, c))).astype(np.uint8)
185  c[c > pixmax] = pixmax
186 
187  imageRGB[i] = c
188 
189  return imageRGB
190 
191 
193  """!A linear map map of red, blue, green intensities into uint8 values"""
194 
195  def __init__(self, minimum=None, maximum=None, image=None):
196  """!A linear stretch from [minimum, maximum]; if one or both are omitted use image minimum/maximum to set them
197 
198  @param minimum Intensity that should be mapped to black (a scalar or array for R, G, B)
199  @param maximum Intensity that should be mapped to white (a scalar)
200  @param image Image to estimate minimum/maximum if not explicitly set
201  """
202 
203  if minimum is None or maximum is None:
204  assert image is not None, "You must provide an image if you don't set both minimum and maximum"
205 
206  stats = afwMath.makeStatistics(image, afwMath.MIN | afwMath.MAX)
207  if minimum is None:
208  minimum = stats.getValue(afwMath.MIN)
209  if maximum is None:
210  maximum = stats.getValue(afwMath.MAX)
211 
212  Mapping.__init__(self, minimum, image)
213  self.maximum = maximum
214 
215  if maximum is None:
216  self._range = None
217  else:
218  assert maximum - minimum != 0, "minimum and maximum values must not be equal"
219  self._range = float(maximum - minimum)
220 
221  def mapIntensityToUint8(self, intensity):
222  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
223  uint8, [0, 255] (but not converted to uint8)
224 
225  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
226  """
227  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
228  return np.where(intensity <= 0, 0,
229  np.where(intensity >= self._range,
230  self._uint8Max/intensity, self._uint8Max/self._range))
231 
232 
234  """!A mapping for a linear stretch chosen by the zscale algorithm
235  (preserving colours independent of brightness)
236 
237  x = (I - minimum)/range
238  """
239 
240  def __init__(self, image, nSamples=1000, contrast=0.25):
241  """!A linear stretch from [z1, z2] chosen by the zscale algorithm
242  @param image Image whose parameters are desired
243  @param nSamples The number of samples to use to estimate the zscale parameters
244  @param contrast The number of samples to use to estimate the zscale parameters
245  """
246 
247  if not hasattr(image, "getArray"):
248  image = afwImage.ImageF(image)
249  z1, z2 = getZScale(image, nSamples, contrast)
250 
251  LinearMapping.__init__(self, z1, z2, image)
252 
253 
255  """!A mapping for an asinh stretch (preserving colours independent of brightness)
256 
257  x = asinh(Q (I - minimum)/range)/Q
258 
259  This reduces to a linear stretch if Q == 0
260 
261  See http://adsabs.harvard.edu/abs/2004PASP..116..133L
262  """
263 
264  def __init__(self, minimum, dataRange, Q=8):
265  Mapping.__init__(self, minimum)
266 
267  # 32bit floating point machine epsilon; sys.float_info.epsilon is 64bit
268  epsilon = 1.0/2**23
269  if abs(Q) < epsilon:
270  Q = 0.1
271  else:
272  Qmax = 1e10
273  if Q > Qmax:
274  Q = Qmax
275 
276  if False:
277  self._slope = self._uint8Max/Q # gradient at origin is self._slope
278  else:
279  frac = 0.1 # gradient estimated using frac*range is _slope
280  self._slope = frac*self._uint8Max/np.arcsinh(frac*Q)
281 
282  self._soften = Q/float(dataRange)
283 
284  def mapIntensityToUint8(self, intensity):
285  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
286  uint8, [0, 255] (but not converted to uint8)
287 
288  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
289  """
290  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
291  return np.where(intensity <= 0, 0, np.arcsinh(intensity*self._soften)*self._slope/intensity)
292 
293 
295  """!A mapping for an asinh stretch, estimating the linear stretch by zscale
296 
297  x = asinh(Q (I - z1)/(z2 - z1))/Q
298 
299  See AsinhMapping
300  """
301 
302  def __init__(self, image, Q=8, pedestal=None):
303  """!
304  Create an asinh mapping from an image, setting the linear part of the stretch using zscale
305 
306  @param image The image to analyse, or a list of 3 images to be converted to an intensity image
307  @param Q The asinh softening parameter
308  @param pedestal The value, or array of 3 values, to subtract from the images; or None
309 
310  N.b. pedestal, if not None, is removed from the images when calculating the zscale
311  stretch, and added back into Mapping.minimum[]
312  """
313  try:
314  assert len(image) in (1, 3,), "Please provide 1 or 3 images"
315  except TypeError:
316  image = [image]
317 
318  if pedestal is not None:
319  try:
320  assert len(pedestal) in (
321  1, 3,), "Please provide 1 or 3 pedestals"
322  except TypeError:
323  pedestal = 3*[pedestal]
324 
325  image = list(image) # needs to be mutable
326  for i, im in enumerate(image):
327  if pedestal[i] != 0.0:
328  if hasattr(im, "getImage"):
329  im = im.getImage()
330  if hasattr(im, "getArray"):
331  im = im.getArray()
332 
333  image[i] = im - pedestal[i] # n.b. a copy
334  else:
335  pedestal = len(image)*[0.0]
336 
337  image = computeIntensity(*image)
338 
339  zscale = ZScaleMapping(image)
340  # zscale.minimum is always a triple
341  dataRange = zscale.maximum - zscale.minimum[0]
342  minimum = zscale.minimum
343 
344  for i, level in enumerate(pedestal):
345  minimum[i] += level
346 
347  AsinhMapping.__init__(self, minimum, dataRange, Q)
348  self._image = image # support self.makeRgbImage()
349 
350 
351 def makeRGB(imageR, imageG=None, imageB=None, minimum=0, dataRange=5, Q=8, fileName=None,
352  saturatedBorderWidth=0, saturatedPixelValue=None,
353  xSize=None, ySize=None, rescaleFactor=None):
354  """Make a set of three images into an RGB image using an asinh stretch and optionally write it to disk
355 
356  If saturatedBorderWidth is non-zero, replace saturated pixels with saturatedPixelValue. Note
357  that replacing saturated pixels requires that the input images be MaskedImages.
358  """
359  if imageG is None:
360  imageG = imageR
361  if imageB is None:
362  imageB = imageR
363 
364  if saturatedBorderWidth:
365  if saturatedPixelValue is None:
366  raise ValueError(
367  "saturatedPixelValue must be set if saturatedBorderWidth is set")
368  replaceSaturatedPixels(imageR, imageG, imageB,
369  saturatedBorderWidth, saturatedPixelValue)
370 
371  asinhMap = AsinhMapping(minimum, dataRange, Q)
372  rgb = asinhMap.makeRgbImage(imageR, imageG, imageB,
373  xSize=xSize, ySize=ySize, rescaleFactor=rescaleFactor)
374 
375  if fileName:
376  writeRGB(fileName, rgb)
377 
378  return rgb
379 
380 
381 def displayRGB(rgb, show=True):
382  """!Display an rgb image using matplotlib
383  @param rgb The RGB image in question
384  @param show If true, call plt.show()
385  """
386  import matplotlib.pyplot as plt
387  plt.imshow(rgb, interpolation='nearest', origin="lower")
388  if show:
389  plt.show()
390  return plt
391 
392 
393 def writeRGB(fileName, rgbImage):
394  """!Write an RGB image to disk
395  @param fileName The output file. The suffix defines the format, and must be supported by matplotlib
396  @param rgbImage The image, as made by e.g. makeRGB
397 
398  Most versions of matplotlib support png and pdf (although the eps/pdf/svg writers may be buggy,
399  possibly due an interaction with useTeX=True in the matplotlib settings).
400 
401  If your matplotlib bundles pil/pillow you should also be able to write jpeg and tiff files.
402  """
403  import matplotlib.image
404  matplotlib.image.imsave(fileName, rgbImage)
405 
406 #
407 # Support the legacy API
408 #
409 
410 
411 class asinhMappingF: # noqa N801
412  """!@deprecated Object used to support legacy API"""
413 
414  def __init__(self, minimum, dataRange, Q):
415  self.minimum = minimum
416  self.dataRange = dataRange
417  self.Q = Q
418 
419 
421  """!@deprecated Object used to support legacy API"""
422 
423  def __init__(self, imageR, imageG, imageB, mapping):
424  """!@deprecated Legacy API"""
425  asinh = AsinhMapping(mapping.minimum, mapping.dataRange, mapping.Q)
426  self.rgb = asinh.makeRgbImage(imageR, imageG, imageB)
427 
428  def write(self, fileName):
429  """!@deprecated Legacy API"""
430  writeRGB(fileName, self.rgb)
431 
432 
433 def RgbImageF(imageR, imageG, imageB, mapping):
434  """!@deprecated Legacy API"""
435  return _RgbImageF(imageR, imageG, imageB, mapping)
Angle abs(Angle const &a)
Definition: Angle.h:106
def __init__(self, minimum=None, image=None)
Create a mapping.
Definition: rgb.py:68
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, minimum, dataRange, Q=8)
Definition: rgb.py:264
def __init__(self, image, Q=8, pedestal=None)
Create an asinh mapping from an image, setting the linear part of the stretch using zscale...
Definition: rgb.py:302
def __init__(self, image, nSamples=1000, contrast=0.25)
A linear stretch from [z1, z2] chosen by the zscale algorithm.
Definition: rgb.py:240
def computeIntensity(imageR, imageG=None, imageB=None)
Return a naive total intensity from the red, blue, and green intensities.
Definition: rgb.py:30
def __init__(self, imageR, imageG, imageB, mapping)
Definition: rgb.py:423
Baseclass to map red, blue, green intensities into uint8 values.
Definition: rgb.py:65
void replaceSaturatedPixels(ImageT &rim, ImageT &gim, ImageT &bim, int borderWidth=2, float saturatedPixelValue=65535)
Definition: saturated.cc:30
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)
Definition: rgb.py:353
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:1282
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 mapIntensityToUint8(self, intensity)
Definition: rgb.py:150
A linear map map of red, blue, green intensities into uint8 values.
Definition: rgb.py:192
def displayRGB(rgb, show=True)
Display an rgb image using matplotlib.
Definition: rgb.py:381
def write(self, fileName)
Definition: rgb.py:428
def makeRgbImage(self, imageR=None, imageG=None, imageB=None, xSize=None, ySize=None, rescaleFactor=None)
Convert 3 arrays, imageR, imageG, and imageB into a numpy RGB image.
Definition: rgb.py:87
def mapIntensityToUint8(self, intensity)
Definition: rgb.py:284
A mapping for an asinh stretch, estimating the linear stretch by zscale.
Definition: rgb.py:294
A mapping for a linear stretch chosen by the zscale algorithm (preserving colours independent of brig...
Definition: rgb.py:233
A mapping for an asinh stretch (preserving colours independent of brightness)
Definition: rgb.py:254
def mapIntensityToUint8(self, intensity)
Definition: rgb.py:221
def __init__(self, minimum, dataRange, Q)
Definition: rgb.py:414
def _convertImagesToUint8(self, imageR, imageG, imageB)
Definition: rgb.py:156
daf::base::PropertyList * list
Definition: fits.cc:819
def RgbImageF(imageR, imageG, imageB, mapping)
Definition: rgb.py:433
def intensity(self, imageR, imageG, imageB)
Return the total intensity from the red, blue, and green intensities.
Definition: rgb.py:143
def writeRGB(fileName, rgbImage)
Write an RGB image to disk.
Definition: rgb.py:393
def __init__(self, minimum=None, maximum=None, image=None)
A linear stretch from [minimum, maximum]; if one or both are omitted use image minimum/maximum to set...
Definition: rgb.py:195