LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
rgb.py
Go to the documentation of this file.
1 from __future__ import division
2 from builtins import object
3 #
4 # LSST Data Management System
5 # Copyright 2015-2016 LSST/AURA
6 #
7 # This product includes software developed by the
8 # LSST Project (http://www.lsst.org/).
9 #
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the LSST License Statement and
21 # the GNU General Public License along with this program. If not,
22 # see <https://www.lsstcorp.org/LegalNotices/>.
23 #
24 
25 import numpy as np
26 
27 import lsst.afw.image as afwImage
28 import lsst.afw.math as afwMath
29 from lsst.afw.display.displayLib import replaceSaturatedPixels, getZScale
30 
31 def computeIntensity(imageR, imageG=None, imageB=None):
32  """!Return a naive total intensity from the red, blue, and green intensities
33  \param imageR intensity of image that'll be mapped to red; or intensity if imageG and imageB are None
34  \param imageG intensity of image that'll be mapped to green; or None
35  \param imageB intensity of image that'll be mapped to blue; or None
36 
37  Inputs may be MaskedImages, Images, or numpy arrays and the return is of the same type
38  """
39  if imageG is None or imageB is None:
40  assert imageG is None and imageB is None, \
41  "Please specify either a single image or red, green, and blue images"
42  return imageR
43 
44  imageRGB = [imageR, imageG, imageB]
45 
46  for i, c in enumerate(imageRGB):
47  if hasattr(c, "getImage"):
48  c = imageRGB[i] = c.getImage()
49  if hasattr(c, "getArray"):
50  imageRGB[i] = c.getArray()
51 
52  intensity = (imageRGB[0] + imageRGB[1] + imageRGB[2])/float(3)
53  #
54  # Repack into whatever type was passed to us
55  #
56  Image = afwImage.ImageU if intensity.dtype == 'uint16' else afwImage.ImageF
57 
58  if hasattr(imageR, "getImage"): # a maskedImage
59  intensity = afwImage.makeMaskedImage(Image(intensity))
60  elif hasattr(imageR, "getArray"):
61  intensity = Image(intensity)
62 
63  return intensity
64 
65 class Mapping(object):
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:
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("You must provide an image (or pass one to the constructor)")
101  imageR = self._image
102 
103  if imageG is None:
104  imageG = imageR
105  if imageB is None:
106  imageB = imageR
107 
108  imageRGB = [imageR, imageG, imageB]
109  for i, c in enumerate(imageRGB):
110  if hasattr(c, "getImage"):
111  c = imageRGB[i] = c.getImage()
112  if hasattr(c, "getArray"):
113  imageRGB[i] = c.getArray()
114 
115  if xSize is not None or ySize is not None:
116  assert rescaleFactor is None, "You may not specify a size and rescaleFactor"
117  h, w = imageRGB[0].shape
118  if ySize is None:
119  ySize = int(xSize*h/float(w) + 0.5)
120  elif xSize is None:
121  xSize = int(ySize*w/float(h) + 0.5)
122 
123  size = (ySize, xSize) # n.b. y, x order for scipy
124  elif rescaleFactor is not None:
125  size = float(rescaleFactor) # an int is intepreted as a percentage
126  else:
127  size = None
128 
129  if size is not None:
130  try:
131  import scipy.misc
132  except ImportError as e:
133  raise RuntimeError("Unable to rescale as scipy.misc is unavailable: %s" % e)
134 
135  for i, im in enumerate(imageRGB):
136  imageRGB[i] = scipy.misc.imresize(im, size, interp='bilinear', mode='F')
137 
138  return np.dstack(self._convertImagesToUint8(*imageRGB)).astype(np.uint8)
139 
140  def intensity(self, imageR, imageG, imageB):
141  """!Return the total intensity from the red, blue, and green intensities
142 
143  This is a naive computation, and may be overridden by subclasses
144  """
145  return computeIntensity(imageR, imageG, imageB)
146 
147  def mapIntensityToUint8(self, I):
148  """Map an intensity into the range of a uint8, [0, 255] (but not converted to uint8)"""
149  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
150  return np.where(I <= 0, 0, np.where(I < self._uint8Max, I, self._uint8Max))
151 
152  def _convertImagesToUint8(self, imageR, imageG, imageB):
153  """Use the mapping to convert images imageR, imageG, and imageB to a triplet of uint8 images"""
154  imageR = imageR - self.minimum[0] # n.b. makes copy
155  imageG = imageG - self.minimum[1]
156  imageB = imageB - self.minimum[2]
157 
158  fac = self.mapIntensityToUint8(self.intensity(imageR, imageG, imageB))
159 
160  imageRGB = [imageR, imageG, imageB]
161  for c in imageRGB:
162  c *= fac
163  c[c < 0] = 0 # individual bands can still be < 0, even if fac isn't
164 
165  pixmax = self._uint8Max
166  r0, g0, b0 = imageRGB # copies -- could work row by row to minimise memory usage
167 
168  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
169  for i, c in enumerate(imageRGB):
170  c = np.where(r0 > g0,
171  np.where(r0 > b0,
172  np.where(r0 >= pixmax, c*pixmax/r0, c),
173  np.where(b0 >= pixmax, c*pixmax/b0, c)),
174  np.where(g0 > b0,
175  np.where(g0 >= pixmax, c*pixmax/g0, c),
176  np.where(b0 >= pixmax, c*pixmax/b0, c))).astype(np.uint8)
177  c[c > pixmax] = pixmax
178 
179  imageRGB[i] = c
180 
181  return imageRGB
182 
184  """!A linear map map of red, blue, green intensities into uint8 values"""
185 
186  def __init__(self, minimum=None, maximum=None, image=None):
187  """!A linear stretch from [minimum, maximum]; if one or both are omitted use image minimum/maximum to set them
188 
189  \param minimum Intensity that should be mapped to black (a scalar or array for R, G, B)
190  \param maximum Intensity that should be mapped to white (a scalar)
191  \param image Image to estimate minimum/maximum if not explicitly set
192  """
193 
194  if minimum is None or maximum is None:
195  assert image is not None, "You must provide an image if you don't set both minimum and maximum"
196 
197  stats = afwMath.makeStatistics(image, afwMath.MIN | afwMath.MAX)
198  if minimum is None:
199  minimum = stats.getValue(afwMath.MIN)
200  if maximum is None:
201  maximum = stats.getValue(afwMath.MAX)
202 
203  Mapping.__init__(self, minimum, image)
204  self.maximum = maximum
205 
206  if maximum is None:
207  self._range = None
208  else:
209  assert maximum - minimum != 0, "minimum and maximum values must not be equal"
210  self._range = float(maximum - minimum)
211 
212  def mapIntensityToUint8(self, I):
213  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
214  uint8, [0, 255] (but not converted to uint8)
215 
216  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
217  """
218  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
219  return np.where(I <= 0, 0,
220  np.where(I >= self._range, self._uint8Max/I, self._uint8Max/self._range))
221 
223  """!A mapping for a linear stretch chosen by the zscale algorithm
224  (preserving colours independent of brightness)
225 
226  x = (I - minimum)/range
227  """
228 
229  def __init__(self, image, nSamples=1000, contrast=0.25):
230  """!A linear stretch from [z1, z2] chosen by the zscale algorithm
231  \param image Image whose parameters are desired
232  \param nSamples The number of samples to use to estimate the zscale parameters
233  \param contrast The number of samples to use to estimate the zscale parameters
234  """
235 
236  if not hasattr(image, "getArray"):
237  image = afwImage.ImageF(image)
238  z1, z2 = getZScale(image, nSamples, contrast)
239 
240  LinearMapping.__init__(self, z1, z2, image)
241 
243  """!A mapping for an asinh stretch (preserving colours independent of brightness)
244 
245  x = asinh(Q (I - minimum)/range)/Q
246 
247  This reduces to a linear stretch if Q == 0
248 
249  See http://adsabs.harvard.edu/abs/2004PASP..116..133L
250  """
251 
252  def __init__(self, minimum, dataRange, Q=8):
253  Mapping.__init__(self, minimum)
254 
255  epsilon = 1.0/2**23 # 32bit floating point machine epsilon; sys.float_info.epsilon is 64bit
256  if abs(Q) < epsilon:
257  Q = 0.1
258  else:
259  Qmax = 1e10
260  if Q > Qmax:
261  Q = Qmax
262 
263  if False:
264  self._slope = self._uint8Max/Q # gradient at origin is self._slope
265  else:
266  frac = 0.1 # gradient estimated using frac*range is _slope
267  self._slope = frac*self._uint8Max/np.arcsinh(frac*Q)
268 
269  self._soften = Q/float(dataRange)
270 
271  def mapIntensityToUint8(self, I):
272  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
273  uint8, [0, 255] (but not converted to uint8)
274 
275  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
276  """
277  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
278  return np.where(I <= 0, 0, np.arcsinh(I*self._soften)*self._slope/I)
279 
281  """!A mapping for an asinh stretch, estimating the linear stretch by zscale
282 
283  x = asinh(Q (I - z1)/(z2 - z1))/Q
284 
285  See AsinhMapping
286  """
287 
288  def __init__(self, image, Q=8, pedestal=None):
289  """!
290  Create an asinh mapping from an image, setting the linear part of the stretch using zscale
291 
292  \param image The image to analyse, or a list of 3 images to be converted to an intensity image
293  \param Q The asinh softening parameter
294  \param pedestal The value, or array of 3 values, to subtract from the images; or None
295 
296  N.b. pedestal, if not None, is removed from the images when calculating the zscale
297  stretch, and added back into Mapping.minimum[]
298  """
299  try:
300  assert len(image) in (1, 3,), "Please provide 1 or 3 images"
301  except TypeError:
302  image = [image]
303 
304  if pedestal is not None:
305  try:
306  assert len(pedestal) in (1, 3,), "Please provide 1 or 3 pedestals"
307  except TypeError:
308  pedestal = 3*[pedestal]
309 
310  image = list(image) # needs to be mutable
311  for i, im in enumerate(image):
312  if pedestal[i] != 0.0:
313  if hasattr(im, "getImage"):
314  im = im.getImage()
315  if hasattr(im, "getArray"):
316  im = im.getArray()
317 
318  image[i] = im - pedestal[i] # n.b. a copy
319  else:
320  pedestal = len(image)*[0.0]
321 
322  image = computeIntensity(*image)
323 
324  zscale = ZScaleMapping(image)
325  dataRange = zscale.maximum - zscale.minimum[0] # zscale.minimum is always a triple
326  minimum = zscale.minimum
327 
328  for i, level in enumerate(pedestal):
329  minimum[i] += level
330 
331  AsinhMapping.__init__(self, minimum, dataRange, Q)
332  self._image = image # support self.makeRgbImage()
333 
334 def makeRGB(imageR, imageG=None, imageB=None, minimum=0, dataRange=5, Q=8, fileName=None,
335  saturatedBorderWidth=0, saturatedPixelValue=None,
336  xSize=None, ySize=None, rescaleFactor=None):
337  """Make a set of three images into an RGB image using an asinh stretch and optionally write it to disk
338 
339  If saturatedBorderWidth is non-zero, replace saturated pixels with saturatedPixelValue. Note
340  that replacing saturated pixels requires that the input images be MaskedImages.
341  """
342  if imageG is None:
343  imageG = imageR
344  if imageB is None:
345  imageB = imageR
346 
347  if saturatedBorderWidth:
348  if saturatedPixelValue is None:
349  raise ValueError("saturatedPixelValue must be set if saturatedBorderWidth is set")
350  replaceSaturatedPixels(imageR, imageG, imageB, saturatedBorderWidth, saturatedPixelValue)
351 
352  asinhMap = AsinhMapping(minimum, dataRange, Q)
353  rgb = asinhMap.makeRgbImage(imageR, imageG, imageB,
354  xSize=xSize, ySize=ySize, rescaleFactor=rescaleFactor)
355 
356  if fileName:
357  writeRGB(fileName, rgb)
358 
359  return rgb
360 
361 def displayRGB(rgb, show=True):
362  """!Display an rgb image using matplotlib
363  \param rgb The RGB image in question
364  \param show If true, call plt.show()
365  """
366  import matplotlib.pyplot as plt
367  plt.imshow(rgb, interpolation='nearest', origin="lower")
368  if show:
369  plt.show()
370  return plt
371 
372 def writeRGB(fileName, rgbImage):
373  """!Write an RGB image to disk
374  \param fileName The output file. The suffix defines the format, and must be supported by matplotlib
375  \param rgbImage The image, as made by e.g. makeRGB
376 
377  Most versions of matplotlib support png and pdf (although the eps/pdf/svg writers may be buggy,
378  possibly due an interaction with useTeX=True in the matplotlib settings).
379 
380  If your matplotlib bundles pil/pillow you should also be able to write jpeg and tiff files.
381  """
382  import matplotlib.image
383  matplotlib.image.imsave(fileName, rgbImage)
384 
385 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
386 #
387 # Support the legacy API
388 #
389 class asinhMappingF(object):
390  """!\deprecated Object used to support legacy API"""
391  def __init__(self, minimum, dataRange, Q):
392  self.minimum = minimum
393  self.dataRange = dataRange
394  self.Q = Q
395 
396 class _RgbImageF(object):
397  """!\deprecated Object used to support legacy API"""
398  def __init__(self, imageR, imageG, imageB, mapping):
399  """!\deprecated Legacy API"""
400  asinh = AsinhMapping(mapping.minimum, mapping.dataRange, mapping.Q)
401  self.rgb = asinh.makeRgbImage(imageR, imageG, imageB)
402 
403  def write(self, fileName):
404  """!\deprecated Legacy API"""
405  writeRGB(fileName, self.rgb)
406 
407 def RgbImageF(imageR, imageG, imageB, mapping):
408  """!\deprecated Legacy API"""
409  return _RgbImageF(imageR, imageG, imageB, mapping)
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:185
def intensity
Return the total intensity from the red, blue, and green intensities.
Definition: rgb.py:140
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename Image< ImagePixelT >::Ptr image, typename Mask< MaskPixelT >::Ptr mask=typename Mask< MaskPixelT >::Ptr(), typename Image< VariancePixelT >::Ptr variance=typename Image< VariancePixelT >::Ptr())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1073
def __init__
A linear stretch from [z1, z2] chosen by the zscale algorithm.
Definition: rgb.py:229
Baseclass to map red, blue, green intensities into uint8 values.
Definition: rgb.py:65
def __init__
Create an asinh mapping from an image, setting the linear part of the stretch using zscale...
Definition: rgb.py:288
A linear map map of red, blue, green intensities into uint8 values.
Definition: rgb.py:183
def makeRgbImage
Convert 3 arrays, imageR, imageG, and imageB into a numpy RGB image.
Definition: rgb.py:87
def displayRGB
Display an rgb image using matplotlib.
Definition: rgb.py:361
def __init__
Create a mapping.
Definition: rgb.py:68
A mapping for an asinh stretch, estimating the linear stretch by zscale.
Definition: rgb.py:280
A mapping for a linear stretch chosen by the zscale algorithm (preserving colours independent of brig...
Definition: rgb.py:222
def computeIntensity
Return a naive total intensity from the red, blue, and green intensities.
Definition: rgb.py:31
A mapping for an asinh stretch (preserving colours independent of brightness)
Definition: rgb.py:242
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1107
def __init__
A linear stretch from [minimum, maximum]; if one or both are omitted use image minimum/maximum to set...
Definition: rgb.py:186
def writeRGB
Write an RGB image to disk.
Definition: rgb.py:372
template void replaceSaturatedPixels(image::MaskedImage< float > &rim, image::MaskedImage< float > &gim, image::MaskedImage< float > &bim, int borderWidth, float saturatedPixelValue)