LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
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  """
192 
193  if minimum is None or maximum is None:
194  assert image is not None, "You must provide an image if you don't set both minimum and maximum"
195 
196  stats = afwMath.makeStatistics(image, afwMath.MIN | afwMath.MAX)
197  if minimum is None:
198  minimum = stats.getValue(afwMath.MIN)
199  if maximum is None:
200  maximum = stats.getValue(afwMath.MAX)
201 
202  Mapping.__init__(self, minimum, image)
203  self.maximum = maximum
204 
205  if maximum is None:
206  self._range = None
207  else:
208  assert maximum - minimum != 0, "minimum and maximum values must not be equal"
209  self._range = float(maximum - minimum)
210 
211  def mapIntensityToUint8(self, I):
212  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
213  uint8, [0, 255] (but not converted to uint8)
214 
215  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
216  """
217  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
218  return np.where(I <= 0, 0,
219  np.where(I >= self._range, self._uint8Max/I, self._uint8Max/self._range))
220 
222  """!A mapping for a linear stretch chosen by the zscale algorithm
223  (preserving colours independent of brightness)
224 
225  x = (I - minimum)/range
226  """
227 
228  def __init__(self, image, nSamples=1000, contrast=0.25):
229  """!A linear stretch from [z1, z2] chosen by the zscale algorithm
230  \param nSamples The number of samples to use to estimate the zscale parameters
231  \param contrast The number of samples to use to estimate the zscale parameters
232  """
233 
234  if not hasattr(image, "getArray"):
235  image = afwImage.ImageF(image)
236  z1, z2 = getZScale(image, nSamples, contrast)
237 
238  LinearMapping.__init__(self, z1, z2, image)
239 
241  """!A mapping for an asinh stretch (preserving colours independent of brightness)
242 
243  x = asinh(Q (I - minimum)/range)/Q
244 
245  This reduces to a linear stretch if Q == 0
246 
247  See http://adsabs.harvard.edu/abs/2004PASP..116..133L
248  """
249 
250  def __init__(self, minimum, dataRange, Q=8):
251  Mapping.__init__(self, minimum)
252 
253  epsilon = 1.0/2**23 # 32bit floating point machine epsilon; sys.float_info.epsilon is 64bit
254  if abs(Q) < epsilon:
255  Q = 0.1
256  else:
257  Qmax = 1e10
258  if Q > Qmax:
259  Q = Qmax
260 
261  if False:
262  self._slope = self._uint8Max/Q # gradient at origin is self._slope
263  else:
264  frac = 0.1 # gradient estimated using frac*range is _slope
265  self._slope = frac*self._uint8Max/np.arcsinh(frac*Q)
266 
267  self._soften = Q/float(dataRange)
268 
269  def mapIntensityToUint8(self, I):
270  """Return an array which, when multiplied by an image, returns that image mapped to the range of a
271  uint8, [0, 255] (but not converted to uint8)
272 
273  The intensity is assumed to have had minimum subtracted (as that can be done per-band)
274  """
275  with np.errstate(invalid='ignore', divide='ignore'): # n.b. np.where can't and doesn't short-circuit
276  return np.where(I <= 0, 0, np.arcsinh(I*self._soften)*self._slope/I)
277 
279  """!A mapping for an asinh stretch, estimating the linear stretch by zscale
280 
281  x = asinh(Q (I - z1)/(z2 - z1))/Q
282 
283  See AsinhMapping
284  """
285 
286  def __init__(self, image, Q=8, pedestal=None):
287  """!
288  Create an asinh mapping from an image, setting the linear part of the stretch using zscale
289 
290  \param image The image to analyse, or a list of 3 images to be converted to an intensity image
291  \param Q The asinh softening parameter
292  \param pedestal The value, or array of 3 values, to subtract from the images; or None
293 
294  N.b. pedestal, if not None, is removed from the images when calculating the zscale
295  stretch, and added back into Mapping.minimum[]
296  """
297  try:
298  assert len(image) in (1, 3,), "Please provide 1 or 3 images"
299  except TypeError:
300  image = [image]
301 
302  if pedestal is not None:
303  try:
304  assert len(pedestal) in (1, 3,), "Please provide 1 or 3 pedestals"
305  except TypeError:
306  pedestal = 3*[pedestal]
307 
308  image = list(image) # needs to be mutable
309  for i, im in enumerate(image):
310  if pedestal[i] != 0.0:
311  if hasattr(im, "getImage"):
312  im = im.getImage()
313  if hasattr(im, "getArray"):
314  im = im.getArray()
315 
316  image[i] = im - pedestal[i] # n.b. a copy
317  else:
318  pedestal = len(image)*[0.0]
319 
320  image = computeIntensity(*image)
321 
322  zscale = ZScaleMapping(image)
323  dataRange = zscale.maximum - zscale.minimum[0] # zscale.minimum is always a triple
324  minimum = zscale.minimum
325 
326  for i, level in enumerate(pedestal):
327  minimum[i] += level
328 
329  AsinhMapping.__init__(self, minimum, dataRange, Q)
330  self._image = image # support self.makeRgbImage()
331 
332 def makeRGB(imageR, imageG=None, imageB=None, minimum=0, dataRange=5, Q=8, fileName=None,
333  saturatedBorderWidth=0, saturatedPixelValue=None,
334  xSize=None, ySize=None, rescaleFactor=None):
335  """Make a set of three images into an RGB image using an asinh stretch and optionally write it to disk
336 
337  If saturatedBorderWidth is non-zero, replace saturated pixels with saturatedPixelValue. Note
338  that replacing saturated pixels requires that the input images be MaskedImages.
339  """
340  if imageG is None:
341  imageG = imageR
342  if imageB is None:
343  imageB = imageR
344 
345  if saturatedBorderWidth:
346  if saturatedPixelValue is None:
347  raise ValueError("saturatedPixelValue must be set if saturatedBorderWidth is set")
348  replaceSaturatedPixels(imageR, imageG, imageB, saturatedBorderWidth, saturatedPixelValue)
349 
350  asinhMap = AsinhMapping(minimum, dataRange, Q)
351  rgb = asinhMap.makeRgbImage(imageR, imageG, imageB,
352  xSize=xSize, ySize=ySize, rescaleFactor=rescaleFactor)
353 
354  if fileName:
355  writeRGB(fileName, rgb)
356 
357  return rgb
358 
359 def displayRGB(rgb, show=True):
360  """!Display an rgb image using matplotlib
361  \param rgb The RGB image in question
362  \param show If true, call plt.show()
363  """
364  import matplotlib.pyplot as plt
365  plt.imshow(rgb, interpolation='nearest', origin="lower")
366  if show:
367  plt.show()
368  return plt
369 
370 def writeRGB(fileName, rgbImage):
371  """!Write an RGB image to disk
372  \param fileName The output file. The suffix defines the format, and must be supported by matplotlib
373  \param rgbImage The image, as made by e.g. makeRGB
374 
375  Most versions of matplotlib support png and pdf (although the eps/pdf/svg writers may be buggy,
376  possibly due an interaction with useTeX=True in the matplotlib settings).
377 
378  If your matplotlib bundles pil/pillow you should also be able to write jpeg and tiff files.
379  """
380  import matplotlib.image
381  matplotlib.image.imsave(fileName, rgbImage)
382 
383 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
384 #
385 # Support the legacy API
386 #
387 class asinhMappingF(object):
388  """!\deprecated Object used to support legacy API"""
389  def __init__(self, minimum, dataRange, Q):
390  self.minimum = minimum
391  self.dataRange = dataRange
392  self.Q = Q
393 
394 class _RgbImageF(object):
395  """!\deprecated Object used to support legacy API"""
396  def __init__(self, imageR, imageG, imageB, mapping):
397  """!\deprecated Legacy API"""
398  asinh = AsinhMapping(mapping.minimum, mapping.dataRange, mapping.Q)
399  self.rgb = asinh.makeRgbImage(imageR, imageG, imageB)
400 
401  def write(self, fileName):
402  """!\deprecated Legacy API"""
403  writeRGB(fileName, self.rgb)
404 
405 def RgbImageF(imageR, imageG, imageB, mapping):
406  """!\deprecated Legacy API"""
407  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)
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())
Definition: MaskedImage.h:1073
def __init__
A linear stretch from [z1, z2] chosen by the zscale algorithm.
Definition: rgb.py:228
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:286
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:359
def __init__
Create a mapping.
Definition: rgb.py:68
A mapping for an asinh stretch, estimating the linear stretch by zscale.
Definition: rgb.py:278
A mapping for a linear stretch chosen by the zscale algorithm (preserving colours independent of brig...
Definition: rgb.py:221
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:240
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:370
template void replaceSaturatedPixels(image::MaskedImage< float > &rim, image::MaskedImage< float > &gim, image::MaskedImage< float > &bim, int borderWidth, float saturatedPixelValue)