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
fitsCompression.cc
Go to the documentation of this file.
1 // -*- lsst-c++ -*-
2 
3 #include "fitsio.h"
4 extern "C" {
5 #include "fitsio2.h"
6 }
7 
8 #include "lsst/pex/exceptions.h"
9 #include "lsst/afw/math/Random.h"
10 
12 
13 extern float* fits_rand_value; // Random numbers, defined in cfitsio
14 int const N_RESERVED_VALUES = 10; // Number of reserved values for float --> bitpix=32 conversions (cfitsio)
15 
16 namespace lsst {
17 namespace afw {
18 namespace fits {
19 
21  if (name == "NONE") return ImageCompressionOptions::NONE;
22  if (name == "GZIP") return ImageCompressionOptions::GZIP;
23  if (name == "GZIP_SHUFFLE") return ImageCompressionOptions::GZIP_SHUFFLE;
24  if (name == "RICE") return ImageCompressionOptions::RICE;
25  if (name == "HCOMPRESS")
26  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "HCOMPRESS is unsupported");
27  if (name == "PLIO") return ImageCompressionOptions::PLIO;
28  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "Unrecognised compression algorithm: " + name);
29 }
30 
32  switch (algorithm) {
34  return "NONE";
36  return "GZIP";
38  return "GZIP_SHUFFLE";
40  return "RICE";
42  return "PLIO";
43  default:
45  os << "Unrecognized compression algorithm: " << algorithm;
47  }
48 }
49 
51  switch (cfitsio) {
52  case 0:
54  case RICE_1:
56  case GZIP_1:
58  case GZIP_2:
60  case PLIO_1:
62  case HCOMPRESS_1:
64  "Unsupported compression algorithm: HCOMPRESS_1");
65  default:
67  os << "Unrecognized cfitsio compression: " << cfitsio;
69  }
70 }
71 
73  switch (algorithm) {
75  return 0;
77  return GZIP_1;
79  return GZIP_2;
81  return RICE_1;
83  return PLIO_1;
84  default:
86  os << "Unrecognized compression algorithm: " << algorithm;
88  }
89 }
90 
92  int rows, float quantizeLevel_)
93  : algorithm(algorithm_), tiles(ndarray::allocate(MAX_COMPRESS_DIM)), quantizeLevel(quantizeLevel_) {
94  tiles[0] = 0;
95  tiles[1] = rows;
96  for (int ii = 2; ii < MAX_COMPRESS_DIM; ++ii) tiles[ii] = 1;
97 }
98 
100  if (name == "NONE") return ImageScalingOptions::NONE;
101  if (name == "RANGE") return ImageScalingOptions::RANGE;
102  if (name == "STDEV_POSITIVE") return ImageScalingOptions::STDEV_POSITIVE;
103  if (name == "STDEV_NEGATIVE") return ImageScalingOptions::STDEV_NEGATIVE;
104  if (name == "STDEV_BOTH") return ImageScalingOptions::STDEV_BOTH;
105  if (name == "MANUAL") return ImageScalingOptions::MANUAL;
106  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "Unrecognized scaling algorithm: " + name);
107 }
108 
110  switch (algorithm) {
112  return "NONE";
114  return "RANGE";
116  return "STDEV_POSITIVE";
118  return "STDEV_NEGATIVE";
120  return "STDEV_BOTH";
122  return "MANUAL";
123  default:
125  os << "Unrecognized scaling algorithm: " << algorithm;
127  }
128 }
129 
131  std::vector<std::string> const& maskPlanes_, int seed_,
132  float quantizeLevel_, float quantizePad_, bool fuzz_, double bscale_,
133  double bzero_)
134  : algorithm(algorithm_),
135  bitpix(bitpix_),
136  fuzz(fuzz_),
137  seed(std::abs(seed_ - 1) % (N_RANDOM - 1) +
138  1), // zero is bad (cfitsio uses non-deterministic method)
139  maskPlanes(maskPlanes_),
140  quantizeLevel(quantizeLevel_),
141  quantizePad(quantizePad_),
142  bscale(bscale_),
143  bzero(bzero_) {}
144 
145 namespace {
146 
148 template <typename T, int N>
149 std::pair<T, T> calculateMedianStdev(ndarray::Array<T const, N, N> const& image,
150  ndarray::Array<bool, N, N> const& mask) {
151  std::size_t num = 0;
152  auto const& flatMask = ndarray::flatten<1>(mask);
153  for (auto mm = flatMask.begin(); mm != flatMask.end(); ++mm) {
154  if (!*mm) ++num;
155  }
156  ndarray::Array<T, 1, 1> array = ndarray::allocate(num);
157  auto const& flatImage = ndarray::flatten<1>(image);
158  auto mm = ndarray::flatten<1>(mask).begin();
159  auto aa = array.begin();
160  for (auto ii = flatImage.begin(); ii != flatImage.end(); ++ii, ++mm) {
161  if (*mm) continue;
162  *aa = *ii;
163  ++aa;
164  }
165 
166  // Quartiles; from https://stackoverflow.com/a/11965377/834250
167  auto const q1 = num / 4;
168  auto const q2 = num / 2;
169  auto const q3 = q1 + q2;
170  std::nth_element(array.begin(), array.begin() + q1, array.end());
171  std::nth_element(array.begin() + q1 + 1, array.begin() + q2, array.end());
172  std::nth_element(array.begin() + q2 + 1, array.begin() + q3, array.end());
173 
174  T const median = num % 2 ? array[num / 2] : 0.5 * (array[num / 2] + array[num / 2 - 1]);
175  // No, we're not doing any interpolation for the lower and upper quartiles.
176  // We're estimating the noise, so it doesn't need to be super precise.
177  T const lq = array[q1];
178  T const uq = array[q3];
179  return std::make_pair(median, 0.741 * (uq - lq));
180 }
181 
183 template <typename T, int N>
184 std::pair<T, T> calculateMinMax(ndarray::Array<T const, N, N> const& image,
185  ndarray::Array<bool, N, N> const& mask) {
187  auto mm = ndarray::flatten<1>(mask).begin();
188  auto const& flatImage = ndarray::flatten<1>(image);
189  for (auto ii = flatImage.begin(); ii != flatImage.end(); ++ii, ++mm) {
190  if (*mm) continue;
191  if (!std::isfinite(*ii)) continue;
192  if (*ii > max) max = *ii;
193  if (*ii < min) min = *ii;
194  }
195  return std::make_pair(min, max);
196 }
197 
198 // Return range of values for target BITPIX
199 template <typename T>
200 double rangeForBitpix(int bitpix, bool cfitsioPadding) {
201  if (bitpix == 0) {
202  bitpix = detail::Bitpix<T>::value;
203  }
204  double range = std::pow(2.0, bitpix) - 1; // Range of values for target BITPIX
205  if (cfitsioPadding) {
206  range -= N_RESERVED_VALUES;
207  }
208  return range;
209 }
210 
211 } // anonymous namespace
212 
213 template <typename T, int N>
214 ImageScale ImageScalingOptions::determineFromRange(ndarray::Array<T const, N, N> const& image,
215  ndarray::Array<bool, N, N> const& mask, bool isUnsigned,
216  bool cfitsioPadding) const {
217  auto minMax = calculateMinMax(image, mask);
218  T const min = minMax.first;
219  T const max = minMax.second;
220  if (min == max) return ImageScale(bitpix, 1.0, min);
221  double range = rangeForBitpix<T>(bitpix, cfitsioPadding);
222  range -= 2; // To allow for rounding and fuzz at either end
223  double const bscale = static_cast<T>((max - min) / range);
224  double bzero = static_cast<T>(isUnsigned ? min : min + 0.5 * range * bscale);
225  if (cfitsioPadding) {
226  bzero -= bscale * N_RESERVED_VALUES;
227  }
228  bzero -= bscale; // Allow for rounding and fuzz on the low end
229  return ImageScale(bitpix, bscale, bzero);
230 }
231 
232 template <typename T, int N>
233 ImageScale ImageScalingOptions::determineFromStdev(ndarray::Array<T const, N, N> const& image,
234  ndarray::Array<bool, N, N> const& mask, bool isUnsigned,
235  bool cfitsioPadding) const {
236  auto stats = calculateMedianStdev(image, mask);
237  auto const median = stats.first, stdev = stats.second;
238  double const bscale = static_cast<T>(stdev / quantizeLevel);
239 
241  auto minMax = calculateMinMax(image, mask);
242  T const min = minMax.first;
243  T const max = minMax.second;
244  double range = rangeForBitpix<T>(bitpix, cfitsioPadding); // Range of values for target BITPIX
245  double const numUnique = (max - min) / bscale; // Number of unique values
246 
247  double imageVal; // Value on image
248  long diskVal; // Corresponding quantized value
249  if (numUnique < range) {
250  imageVal = median;
251  diskVal = cfitsioPadding ? 0.5 * N_RESERVED_VALUES : 0;
252  } else {
253  switch (algorithm) {
255  // Put (median - N sigma) at the lowest possible value: predominantly positive images
256  imageVal = median - quantizePad * stdev;
257  diskVal = (bitpix == 8) ? 0 : -(1L << (bitpix - 1)); // Lowest value: -2^(bitpix-1)
258  if (cfitsioPadding) diskVal -= N_RESERVED_VALUES;
259  break;
261  // Put (median + N sigma) at the highest possible value: predominantly negative images
262  imageVal = median + quantizePad * stdev;
263  diskVal = (bitpix == 8) ? 255 : (1L << (bitpix - 1)) - 1; // Lowest value: 2^(bitpix-1)-1
264  break;
266  // Put median right in the middle: images with an equal abundance of positive and negative
267  // values
268  imageVal = median;
269  diskVal = cfitsioPadding ? 0.5 * N_RESERVED_VALUES : 0;
270  break;
271  default:
272  std::abort(); // Programming error: should never get here
273  }
274  }
275 
276  double bzero = static_cast<T>(imageVal - bscale * diskVal);
277  return ImageScale(bitpix, bscale, bzero);
278 }
279 
281 template <typename T, class Enable = void>
282 struct Bzero {
283  static double constexpr value = 0.0;
284 };
285 
286 // uint64 version
287 // 'double' doesn't have sufficient bits to represent the appropriate BZERO,
288 // so let cfitsio handle it.
289 template <>
290 struct Bzero<std::uint64_t> {
291  static double constexpr value = 0.0;
292 };
293 
294 // Unsigned integer version
295 template <typename T>
296 struct Bzero<T, typename std::enable_if<std::numeric_limits<T>::is_integer &&
297  !std::numeric_limits<T>::is_signed>::type> {
298  static double constexpr value = (std::numeric_limits<T>::max() >> 1) + 1;
299 };
300 
301 #ifndef DOXYGEN // suppress a bogus Doxygen complaint about an documented symbol
302 template <typename T, int N>
303 ImageScale ImageScalingOptions::determine(ndarray::Array<T const, N, N> const& image,
304  ndarray::Array<bool, N, N> const& mask) const {
306  algorithm != NONE) {
308  "Image scaling not supported for integral types");
309  }
310  bool const isUnsigned = bitpix == 8 || (bitpix == 0 && detail::Bitpix<T>::value == 8);
311  bool const cfitsioPadding = !std::numeric_limits<T>::is_integer && bitpix == 32;
312  switch (algorithm) {
313  case NONE:
315  case RANGE:
316  return determineFromRange(image, mask, isUnsigned, cfitsioPadding);
317  case MANUAL:
318  return ImageScale(bitpix, bscale, bzero);
322  return determineFromStdev(image, mask, isUnsigned, cfitsioPadding);
323  default:
324  std::abort(); // should never get here
325  }
326 }
327 #endif
328 
329 namespace {
330 
335 class CfitsioRandom {
336 public:
338  CfitsioRandom(int seed) : _seed(seed) {
339  assert(seed != 0 && seed < N_RANDOM);
340  fits_init_randoms();
341  resetForTile(0);
342  }
343 
345  void resetForTile(int iTile) {
346  _start = (iTile + _seed - 1) % N_RANDOM;
347  reseed();
348  }
349 
351  float getNext() {
352  float const value = fits_rand_value[_index];
353  increment();
354  return value;
355  }
356 
358  void increment() {
359  ++_index;
360  if (_index == N_RANDOM) {
361  ++_start;
362  if (_start == N_RANDOM) {
363  _start = 0;
364  }
365  reseed();
366  }
367  }
368 
370  template <typename T>
371  ndarray::Array<T, 1, 1> forImage(typename ndarray::Array<T const, 2, 2>::Index const& shape,
372  ndarray::Array<long, 1> const& tiles) {
373  std::size_t const xSize = shape[1], ySize = shape[0];
374  ndarray::Array<T, 1, 1> out = ndarray::allocate(xSize * ySize);
375  std::size_t const xTileSize = tiles[0] <= 0 ? xSize : tiles[0];
376  std::size_t const yTileSize = tiles[1] < 0 ? ySize : (tiles[1] == 0 ? 1 : tiles[1]);
377  int const xNumTiles = std::ceil(xSize / static_cast<float>(xTileSize));
378  int const yNumTiles = std::ceil(ySize / static_cast<float>(yTileSize));
379  for (int iTile = 0, yTile = 0; yTile < yNumTiles; ++yTile) {
380  int const yStart = yTile * yTileSize;
381  int const yStop = std::min(yStart + yTileSize, ySize);
382  for (int xTile = 0; xTile < xNumTiles; ++xTile, ++iTile) {
383  int const xStart = xTile * xTileSize;
384  int const xStop = std::min(xStart + xTileSize, xSize);
385  resetForTile(iTile);
386  for (int y = yStart; y < yStop; ++y) {
387  auto iter = out.begin() + y * xSize + xStart;
388  for (int x = xStart; x < xStop; ++x, ++iter) {
389  *iter = static_cast<T>(getNext());
390  }
391  }
392  }
393  }
394  return out;
395  }
396 
397 private:
399  void reseed() { _index = static_cast<int>(fits_rand_value[_start] * 500); }
400 
401  int _seed; // Initial seed
402  int _start; // Starting index for tile; "iseed" in cfitsio
403  int _index; // Index of next value; "nextrand" in cfitsio
404 };
405 
406 } // anonymous namespace
407 
408 template <typename T>
409 std::shared_ptr<detail::PixelArrayBase> ImageScale::toFits(ndarray::Array<T const, 2, 2> const& image,
410  bool forceNonfiniteRemoval, bool fuzz,
411  ndarray::Array<long, 1> const& tiles,
412  int seed) const {
416  "Floating-point images may not be converted to different floating-point types");
417  }
418  if (bscale != 1.0 || bzero != 0.0) {
420  "Scaling may not be applied to floating-point images");
421  }
422  }
423 
424  if (bitpix < 0 || (bitpix == 0 && !std::numeric_limits<T>::is_integer) ||
425  (bscale == 1.0 && bzero == 0.0 && !fuzz)) {
426  if (!forceNonfiniteRemoval) {
427  // Type conversion only
428  return detail::makePixelArray(bitpix, ndarray::Array<T const, 1, 1>(ndarray::flatten<1>(image)));
429  }
431  ndarray::Array<T, 1, 1> out = ndarray::allocate(image.getNumElements());
432  auto outIter = out.begin();
433  auto const& flatImage = ndarray::flatten<1>(image);
434  for (auto inIter = flatImage.begin(); inIter != flatImage.end(); ++inIter, ++outIter) {
435  *outIter = std::isfinite(*inIter) ? *inIter : std::numeric_limits<T>::max();
436  }
437  return detail::makePixelArray(bitpix, out);
438  }
439  // Fall through for explicit scaling
440  }
441 
442  // Note: BITPIX=8 treated differently, since it uses unsigned values; the rest use signed */
443  double min = bitpix == 8 ? 0 : -std::pow(2.0, bitpix - 1);
444  double max = bitpix == 8 ? 255 : (std::pow(2.0, bitpix - 1) - 1.0);
445 
447  // cfitsio saves space for N_RESERVED_VALUES=10 values at the low end
448  min += N_RESERVED_VALUES;
449  }
450 
451  double const scale = 1.0 / bscale;
452  std::size_t const num = image.getNumElements();
453  bool const applyFuzz = fuzz && !std::numeric_limits<T>::is_integer && bitpix > 0;
454  ndarray::Array<double, 1, 1> out;
455  if (applyFuzz) {
456  if (tiles.isEmpty()) {
458  "Tile sizes must be provided if fuzzing is desired");
459  }
460  out = CfitsioRandom(seed).forImage<double>(image.getShape(), tiles);
461  } else {
462  out = ndarray::allocate(num);
463  out.deep() = 0;
464  }
465  auto outIter = out.begin();
466  auto const& flatImage = ndarray::flatten<1>(image);
467  for (auto inIter = flatImage.begin(); inIter != flatImage.end(); ++inIter, ++outIter) {
468  double value = (*inIter - bzero) * scale;
469  if (!std::isfinite(value)) {
470  // This choice of "max" for non-finite and overflow pixels is mainly cosmetic --- it has to be
471  // something, and "min" would produce holes in the cores of bright stars.
472  *outIter = blank;
473  continue;
474  }
475  if (applyFuzz) {
476  // Add random factor [0.0,1.0): adds a variance of 1/12,
477  // but preserves the expectation value given the floor()
478  value += *outIter;
479  }
480  *outIter = (value < min ? blank : (value > max ? blank : std::floor(value)));
481  }
482  return detail::makePixelArray(bitpix, out);
483 }
484 
485 template <typename T>
486 ndarray::Array<T, 2, 2> ImageScale::fromFits(ndarray::Array<T, 2, 2> const& image) const {
487  ndarray::Array<T, 2, 2> memory = ndarray::allocate(image.getShape());
488  memory.deep() = bscale * image + bzero;
489  return memory;
490 }
491 
492 // Explicit instantiation
493 #define INSTANTIATE(TYPE) \
494  template ImageScale ImageScalingOptions::determine<TYPE, 2>( \
495  ndarray::Array<TYPE const, 2, 2> const& image, ndarray::Array<bool, 2, 2> const& mask) const; \
496  template std::shared_ptr<detail::PixelArrayBase> ImageScale::toFits<TYPE>( \
497  ndarray::Array<TYPE const, 2, 2> const&, bool, bool, ndarray::Array<long, 1> const&, int) const; \
498  template ndarray::Array<TYPE, 2, 2> ImageScale::fromFits<TYPE>(ndarray::Array<TYPE, 2, 2> const&) const;
499 
507 INSTANTIATE(boost::float32_t);
508 INSTANTIATE(boost::float64_t);
509 
510 } // namespace fits
511 } // namespace afw
512 } // namespace lsst
int const N_RESERVED_VALUES
Angle abs(Angle const &a)
Definition: Angle.h:106
int bitpix
Bits per pixel (0, 8,16,32,64,-32,-64)
std::shared_ptr< PixelArrayBase > makePixelArray(int bitpix, ndarray::Array< T, 1, 1 > const &array)
Create a PixelArray suitable for an image with the nominated BITPIX.
afw::table::Key< afw::table::Array< ImagePixelT > > image
afw::table::Key< afw::table::Array< MaskPixelT > > mask
T ceil(T... args)
Scale to preserve dynamic range.
int compressionAlgorithmToCfitsio(ImageCompressionOptions::CompressionAlgorithm algorithm)
Convert ImageCompressionOptions::CompressionAlgorithm to cfitsio.
def scale(algorithm, min, max=None, frame=None)
Definition: ds9.py:109
int y
Definition: SpanSet.cc:49
STL namespace.
#define INSTANTIATE(TYPE)
T floor(T... args)
CompressionAlgorithm
Compression algorithms.
FITS BITPIX header value by C++ type.
int min
int seed
Seed for random number generator when fuzzing.
Fits * fits
Definition: FitsWriter.cc:90
STL class.
T min(T... args)
def stdev(vector)
Definition: fringe.py:522
Tiles tiles
Tile size; a dimension with 0 means infinite (e.g., to specify one row: 0,1)
ImageCompressionOptions(CompressionAlgorithm algorithm_, Tiles tiles_, float quantizeLevel_=0.0)
Custom compression.
A base class for image defects.
float quantizeLevel
Divisor of the standard deviation for STDEV_* scaling.
table::Key< int > type
Definition: Detector.cc:163
ndarray::Array< T, 2, 2 > fromFits(ndarray::Array< T, 2, 2 > const &image) const
Convert to an array.
ImageCompressionOptions::CompressionAlgorithm compressionAlgorithmFromString(std::string const &name)
Interpret compression algorithm expressed in string.
double bscale
Manually specified BSCALE (for MANUAL scaling)
T str(T... args)
T make_pair(T... args)
int max
T isfinite(T... args)
std::string scalingAlgorithmToString(ImageScalingOptions::ScalingAlgorithm algorithm)
Provide string version of compression algorithm.
T max(T... args)
Scaling zero-point, set according to pixel type.
std::string compressionAlgorithmToString(ImageCompressionOptions::CompressionAlgorithm algorithm)
Provide string version of compression algorithm.
double x
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition: Exception.h:48
T pow(T... args)
float quantizeLevel
quantization level: 0.0 = none requires use of GZIP or GZIP_SHUFFLE
Scale based on the standard deviation, dynamic range negative.
double bzero
Manually specified BZERO (for MANUAL scaling)
float * fits_rand_value
Reports invalid arguments.
Definition: Runtime.h:66
CompressionAlgorithm algorithm
Compresion algorithm to use.
ImageCompressionOptions::CompressionAlgorithm compressionAlgorithmFromCfitsio(int cfitsio)
Convert compression algorithm from cfitsio to ImageCompressionOptions::CompressionAlgorithm.
ImageScale determine(image::ImageBase< T > const &image, std::shared_ptr< image::Mask< image::MaskPixel > const > mask=nullptr) const
Determine the scaling for a particular image.
T abort(T... args)
T nth_element(T... args)
Scale based on the standard deviation, dynamic range positive+negative.
float quantizePad
Number of stdev to allow on the low/high side (for STDEV_POSITIVE/NEGATIVE)
bool fuzz
Fuzz the values when quantising floating-point values?
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
std::ostream * os
Definition: Schema.cc:746
Scale based on the standard deviation. dynamic range positive.
ImageScalingOptions::ScalingAlgorithm scalingAlgorithmFromString(std::string const &name)
Interpret scaling algorithm expressed in string.
GZIP compression with shuffle (most-significant byte first)
std::shared_ptr< detail::PixelArrayBase > toFits(ndarray::Array< T const, 2, 2 > const &image, bool forceNonfiniteRemoval, bool fuzz=true, ndarray::Array< long, 1 > const &tiles=ndarray::Array< long, 1, 1 >(), int seed=1) const
Convert to an array of pixel values to write to FITS.
Scale to apply to image.