LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
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 
11 
12 extern float* fits_rand_value; // Random numbers, defined in cfitsio
13 int const N_RESERVED_VALUES = 10; // Number of reserved values for float --> bitpix=32 conversions (cfitsio)
14 
15 namespace lsst {
16 namespace afw {
17 namespace fits {
18 
20  if (name == "NONE") return ImageCompressionOptions::NONE;
21  if (name == "GZIP") return ImageCompressionOptions::GZIP;
22  if (name == "GZIP_SHUFFLE") return ImageCompressionOptions::GZIP_SHUFFLE;
23  if (name == "RICE") return ImageCompressionOptions::RICE;
24  if (name == "HCOMPRESS")
25  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "HCOMPRESS is unsupported");
26  if (name == "PLIO") return ImageCompressionOptions::PLIO;
27  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "Unrecognised compression algorithm: " + name);
28 }
29 
31  switch (algorithm) {
33  return "NONE";
35  return "GZIP";
37  return "GZIP_SHUFFLE";
39  return "RICE";
41  return "PLIO";
42  default:
44  os << "Unrecognized compression algorithm: " << algorithm;
46  }
47 }
48 
50  switch (cfitsio) {
51  case 0:
53  case RICE_1:
55  case GZIP_1:
57  case GZIP_2:
59  case PLIO_1:
61  case HCOMPRESS_1:
63  "Unsupported compression algorithm: HCOMPRESS_1");
64  default:
66  os << "Unrecognized cfitsio compression: " << cfitsio;
68  }
69 }
70 
72  switch (algorithm) {
74  return 0;
76  return GZIP_1;
78  return GZIP_2;
80  return RICE_1;
82  return PLIO_1;
83  default:
85  os << "Unrecognized compression algorithm: " << algorithm;
87  }
88 }
89 
91  int rows, float quantizeLevel_)
92  : algorithm(algorithm_), tiles(ndarray::allocate(MAX_COMPRESS_DIM)), quantizeLevel(quantizeLevel_) {
93  tiles[0] = 0;
94  tiles[1] = rows;
95  for (int ii = 2; ii < MAX_COMPRESS_DIM; ++ii) tiles[ii] = 1;
96 }
97 
99  if (name == "NONE") return ImageScalingOptions::NONE;
100  if (name == "RANGE") return ImageScalingOptions::RANGE;
101  if (name == "STDEV_POSITIVE") return ImageScalingOptions::STDEV_POSITIVE;
102  if (name == "STDEV_NEGATIVE") return ImageScalingOptions::STDEV_NEGATIVE;
103  if (name == "STDEV_BOTH") return ImageScalingOptions::STDEV_BOTH;
104  if (name == "MANUAL") return ImageScalingOptions::MANUAL;
105  throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "Unrecognized scaling algorithm: " + name);
106 }
107 
109  switch (algorithm) {
111  return "NONE";
113  return "RANGE";
115  return "STDEV_POSITIVE";
117  return "STDEV_NEGATIVE";
119  return "STDEV_BOTH";
121  return "MANUAL";
122  default:
124  os << "Unrecognized scaling algorithm: " << algorithm;
126  }
127 }
128 
130  std::vector<std::string> const& maskPlanes_, int seed_,
131  float quantizeLevel_, float quantizePad_, bool fuzz_, double bscale_,
132  double bzero_)
133  : algorithm(algorithm_),
134  bitpix(bitpix_),
135  fuzz(fuzz_),
136  seed(std::abs(seed_ - 1) % (N_RANDOM - 1) +
137  1), // zero is bad (cfitsio uses non-deterministic method)
138  maskPlanes(maskPlanes_),
139  quantizeLevel(quantizeLevel_),
140  quantizePad(quantizePad_),
141  bscale(bscale_),
142  bzero(bzero_) {}
143 
144 namespace {
145 
147 template <typename T, int N>
148 std::pair<T, T> calculateMedianStdev(ndarray::Array<T const, N, N> const& image,
149  ndarray::Array<bool, N, N> const& mask) {
150  std::size_t num = 0;
151  auto const& flatMask = ndarray::flatten<1>(mask);
152  for (auto mm = flatMask.begin(); mm != flatMask.end(); ++mm) {
153  if (!*mm) ++num;
154  }
155  ndarray::Array<T, 1, 1> array = ndarray::allocate(num);
156  auto const& flatImage = ndarray::flatten<1>(image);
157  auto mm = ndarray::flatten<1>(mask).begin();
158  auto aa = array.begin();
159  for (auto ii = flatImage.begin(); ii != flatImage.end(); ++ii, ++mm) {
160  if (*mm) continue;
161  *aa = *ii;
162  ++aa;
163  }
164 
165  // Quartiles; from https://stackoverflow.com/a/11965377/834250
166  auto const q1 = num / 4;
167  auto const q2 = num / 2;
168  auto const q3 = q1 + q2;
169  std::nth_element(array.begin(), array.begin() + q1, array.end());
170  std::nth_element(array.begin() + q1 + 1, array.begin() + q2, array.end());
171  std::nth_element(array.begin() + q2 + 1, array.begin() + q3, array.end());
172 
173  T const median = num % 2 ? array[num / 2] : 0.5 * (array[num / 2] + array[num / 2 - 1]);
174  // No, we're not doing any interpolation for the lower and upper quartiles.
175  // We're estimating the noise, so it doesn't need to be super precise.
176  T const lq = array[q1];
177  T const uq = array[q3];
178  return std::make_pair(median, 0.741 * (uq - lq));
179 }
180 
182 template <typename T, int N>
183 std::pair<T, T> calculateMinMax(ndarray::Array<T const, N, N> const& image,
184  ndarray::Array<bool, N, N> const& mask) {
186  auto mm = ndarray::flatten<1>(mask).begin();
187  auto const& flatImage = ndarray::flatten<1>(image);
188  for (auto ii = flatImage.begin(); ii != flatImage.end(); ++ii, ++mm) {
189  if (*mm) continue;
190  if (!std::isfinite(*ii)) continue;
191  if (*ii > max) max = *ii;
192  if (*ii < min) min = *ii;
193  }
194  return std::make_pair(min, max);
195 }
196 
197 // Return range of values for target BITPIX
198 template <typename T>
199 double rangeForBitpix(int bitpix, bool cfitsioPadding) {
200  if (bitpix == 0) {
201  bitpix = detail::Bitpix<T>::value;
202  }
203  double range = std::pow(2.0, bitpix) - 1; // Range of values for target BITPIX
204  if (cfitsioPadding) {
205  range -= N_RESERVED_VALUES;
206  }
207  return range;
208 }
209 
210 } // anonymous namespace
211 
212 template <typename T, int N>
213 ImageScale ImageScalingOptions::determineFromRange(ndarray::Array<T const, N, N> const& image,
214  ndarray::Array<bool, N, N> const& mask, bool isUnsigned,
215  bool cfitsioPadding) const {
216  auto minMax = calculateMinMax(image, mask);
217  T const min = minMax.first;
218  T const max = minMax.second;
219  if (min == max) return ImageScale(bitpix, 1.0, min);
220  double range = rangeForBitpix<T>(bitpix, cfitsioPadding);
221  range -= 2; // To allow for rounding and fuzz at either end
222  double const bscale = static_cast<T>((max - min) / range);
223  double bzero = static_cast<T>(isUnsigned ? min : min + 0.5 * range * bscale);
224  if (cfitsioPadding) {
226  }
227  bzero -= bscale; // Allow for rounding and fuzz on the low end
228  return ImageScale(bitpix, bscale, bzero);
229 }
230 
231 template <typename T, int N>
232 ImageScale ImageScalingOptions::determineFromStdev(ndarray::Array<T const, N, N> const& image,
233  ndarray::Array<bool, N, N> const& mask, bool isUnsigned,
234  bool cfitsioPadding) const {
235  auto stats = calculateMedianStdev(image, mask);
236  auto const median = stats.first, stdev = stats.second;
237  double const bscale = static_cast<T>(stdev / quantizeLevel);
238 
240  auto minMax = calculateMinMax(image, mask);
241  T const min = minMax.first;
242  T const max = minMax.second;
243  double range = rangeForBitpix<T>(bitpix, cfitsioPadding); // Range of values for target BITPIX
244  double const numUnique = (max - min) / bscale; // Number of unique values
245 
246  double imageVal; // Value on image
247  long diskVal; // Corresponding quantized value
248  if (numUnique < range) {
249  imageVal = median;
250  diskVal = cfitsioPadding ? 0.5 * N_RESERVED_VALUES : 0;
251  } else {
252  switch (algorithm) {
254  // Put (median - N sigma) at the lowest possible value: predominantly positive images
255  imageVal = median - quantizePad * stdev;
256  diskVal = (bitpix == 8) ? 0 : -(1L << (bitpix - 1)); // Lowest value: -2^(bitpix-1)
257  if (cfitsioPadding) diskVal -= N_RESERVED_VALUES;
258  break;
260  // Put (median + N sigma) at the highest possible value: predominantly negative images
261  imageVal = median + quantizePad * stdev;
262  diskVal = (bitpix == 8) ? 255 : (1L << (bitpix - 1)) - 1; // Lowest value: 2^(bitpix-1)-1
263  break;
265  // Put median right in the middle: images with an equal abundance of positive and negative
266  // values
267  imageVal = median;
268  diskVal = cfitsioPadding ? 0.5 * N_RESERVED_VALUES : 0;
269  break;
270  default:
271  std::abort(); // Programming error: should never get here
272  }
273  }
274 
275  double bzero = static_cast<T>(imageVal - bscale * diskVal);
276  return ImageScale(bitpix, bscale, bzero);
277 }
278 
280 template <typename T, class Enable = void>
281 struct Bzero {
282  static double constexpr value = 0.0;
283 };
284 
285 // uint64 version
286 // 'double' doesn't have sufficient bits to represent the appropriate BZERO,
287 // so let cfitsio handle it.
288 template <>
289 struct Bzero<std::uint64_t> {
290  static double constexpr value = 0.0;
291 };
292 
293 // Unsigned integer version
294 template <typename T>
295 struct Bzero<T, typename std::enable_if<std::numeric_limits<T>::is_integer &&
296  !std::numeric_limits<T>::is_signed>::type> {
297  static double constexpr value = (std::numeric_limits<T>::max() >> 1) + 1;
298 };
299 
300 #ifndef DOXYGEN // suppress a bogus Doxygen complaint about an documented symbol
301 template <typename T, int N>
302 ImageScale ImageScalingOptions::determine(ndarray::Array<T const, N, N> const& image,
303  ndarray::Array<bool, N, N> const& mask) const {
305  algorithm != NONE) {
307  "Image scaling not supported for integral types");
308  }
309  bool const isUnsigned = bitpix == 8 || (bitpix == 0 && detail::Bitpix<T>::value == 8);
310  bool const cfitsioPadding = !std::numeric_limits<T>::is_integer && bitpix == 32;
311  switch (algorithm) {
312  case NONE:
313  return ImageScale(detail::Bitpix<T>::value, 1.0, Bzero<T>::value);
314  case RANGE:
315  return determineFromRange(image, mask, isUnsigned, cfitsioPadding);
316  case MANUAL:
317  return ImageScale(bitpix, bscale, bzero);
321  return determineFromStdev(image, mask, isUnsigned, cfitsioPadding);
322  default:
323  std::abort(); // should never get here
324  }
325 }
326 #endif
327 
328 namespace {
329 
334 class CfitsioRandom {
335 public:
337  CfitsioRandom(int seed) : _seed(seed) {
338  assert(seed != 0 && seed < N_RANDOM);
339  fits_init_randoms();
340  resetForTile(0);
341  }
342 
344  void resetForTile(int iTile) {
345  _start = (iTile + _seed - 1) % N_RANDOM;
346  reseed();
347  }
348 
350  float getNext() {
351  float const value = fits_rand_value[_index];
352  increment();
353  return value;
354  }
355 
357  void increment() {
358  ++_index;
359  if (_index == N_RANDOM) {
360  ++_start;
361  if (_start == N_RANDOM) {
362  _start = 0;
363  }
364  reseed();
365  }
366  }
367 
369  template <typename T>
370  ndarray::Array<T, 1, 1> forImage(typename ndarray::Array<T const, 2, 2>::Index const& shape,
371  ndarray::Array<long, 1> const& tiles) {
372  std::size_t const xSize = shape[1], ySize = shape[0];
373  ndarray::Array<T, 1, 1> out = ndarray::allocate(xSize * ySize);
374  std::size_t const xTileSize = tiles[0] <= 0 ? xSize : tiles[0];
375  std::size_t const yTileSize = tiles[1] < 0 ? ySize : (tiles[1] == 0 ? 1 : tiles[1]);
376  int const xNumTiles = std::ceil(xSize / static_cast<float>(xTileSize));
377  int const yNumTiles = std::ceil(ySize / static_cast<float>(yTileSize));
378  for (int iTile = 0, yTile = 0; yTile < yNumTiles; ++yTile) {
379  int const yStart = yTile * yTileSize;
380  int const yStop = std::min(yStart + yTileSize, ySize);
381  for (int xTile = 0; xTile < xNumTiles; ++xTile, ++iTile) {
382  int const xStart = xTile * xTileSize;
383  int const xStop = std::min(xStart + xTileSize, xSize);
384  resetForTile(iTile);
385  for (int y = yStart; y < yStop; ++y) {
386  auto iter = out.begin() + y * xSize + xStart;
387  for (int x = xStart; x < xStop; ++x, ++iter) {
388  *iter = static_cast<T>(getNext());
389  }
390  }
391  }
392  }
393  return out;
394  }
395 
396 private:
398  void reseed() { _index = static_cast<int>(fits_rand_value[_start] * 500); }
399 
400  int _seed; // Initial seed
401  int _start; // Starting index for tile; "iseed" in cfitsio
402  int _index; // Index of next value; "nextrand" in cfitsio
403 };
404 
405 } // anonymous namespace
406 
407 template <typename T>
409  bool forceNonfiniteRemoval, bool fuzz,
410  ndarray::Array<long, 1> const& tiles,
411  int seed) const {
415  "Floating-point images may not be converted to different floating-point types");
416  }
417  if (bscale != 1.0 || bzero != 0.0) {
419  "Scaling may not be applied to floating-point images");
420  }
421  }
422 
423  if (bitpix < 0 || (bitpix == 0 && !std::numeric_limits<T>::is_integer) ||
424  (bscale == 1.0 && bzero == 0.0 && !fuzz)) {
425  if (!forceNonfiniteRemoval) {
426  // Type conversion only
427  return detail::makePixelArray(bitpix, ndarray::Array<T const, 1, 1>(ndarray::flatten<1>(image)));
428  }
430  ndarray::Array<T, 1, 1> out = ndarray::allocate(image.getNumElements());
431  auto outIter = out.begin();
432  auto const& flatImage = ndarray::flatten<1>(image);
433  for (auto inIter = flatImage.begin(); inIter != flatImage.end(); ++inIter, ++outIter) {
434  *outIter = std::isfinite(*inIter) ? *inIter : std::numeric_limits<T>::max();
435  }
436  return detail::makePixelArray(bitpix, out);
437  }
438  // Fall through for explicit scaling
439  }
440 
441  // Note: BITPIX=8 treated differently, since it uses unsigned values; the rest use signed */
442  double min = bitpix == 8 ? 0 : -std::pow(2.0, bitpix - 1);
443  double max = bitpix == 8 ? 255 : (std::pow(2.0, bitpix - 1) - 1.0);
444 
446  // cfitsio saves space for N_RESERVED_VALUES=10 values at the low end
448  }
449 
450  double const scale = 1.0 / bscale;
451  std::size_t const num = image.getNumElements();
452  bool const applyFuzz = fuzz && !std::numeric_limits<T>::is_integer && bitpix > 0;
453  ndarray::Array<double, 1, 1> out;
454  if (applyFuzz) {
455  if (tiles.isEmpty()) {
457  "Tile sizes must be provided if fuzzing is desired");
458  }
459  out = CfitsioRandom(seed).forImage<double>(image.getShape(), tiles);
460  } else {
461  out = ndarray::allocate(num);
462  out.deep() = 0;
463  }
464  auto outIter = out.begin();
465  auto const& flatImage = ndarray::flatten<1>(image);
466  for (auto inIter = flatImage.begin(); inIter != flatImage.end(); ++inIter, ++outIter) {
467  double value = (*inIter - bzero) * scale;
468  if (!std::isfinite(value)) {
469  // This choice of "max" for non-finite and overflow pixels is mainly cosmetic --- it has to be
470  // something, and "min" would produce holes in the cores of bright stars.
471  *outIter = blank;
472  continue;
473  }
474  if (applyFuzz) {
475  // Add random factor [0.0,1.0): adds a variance of 1/12,
476  // but preserves the expectation value given the floor()
477  value += *outIter;
478  }
479  *outIter = (value < min ? blank : (value > max ? blank : std::floor(value)));
480  }
481  return detail::makePixelArray(bitpix, out);
482 }
483 
484 template <typename T>
485 ndarray::Array<T, 2, 2> ImageScale::fromFits(ndarray::Array<T, 2, 2> const& image) const {
486  ndarray::Array<T, 2, 2> memory = ndarray::allocate(image.getShape());
487  memory.deep() = bscale * image + bzero;
488  return memory;
489 }
490 
491 // Explicit instantiation
492 #define INSTANTIATE(TYPE) \
493  template ImageScale ImageScalingOptions::determine<TYPE, 2>( \
494  ndarray::Array<TYPE const, 2, 2> const& image, ndarray::Array<bool, 2, 2> const& mask) const; \
495  template std::shared_ptr<detail::PixelArrayBase> ImageScale::toFits<TYPE>( \
496  ndarray::Array<TYPE const, 2, 2> const&, bool, bool, ndarray::Array<long, 1> const&, int) const; \
497  template ndarray::Array<TYPE, 2, 2> ImageScale::fromFits<TYPE>(ndarray::Array<TYPE, 2, 2> const&) const;
498 
506 INSTANTIATE(boost::float32_t);
507 INSTANTIATE(boost::float64_t);
508 
509 } // namespace fits
510 } // namespace afw
511 } // namespace lsst
table::Key< std::string > name
Definition: Amplifier.cc:116
int min
int max
double x
table::Key< int > type
Definition: Detector.cc:163
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition: Exception.h:48
Fits * fits
Definition: FitsWriter.cc:90
afw::table::Key< afw::table::Array< MaskPixelT > > mask
std::ostream * os
Definition: Schema.cc:557
int y
Definition: SpanSet.cc:48
T abort(T... args)
T ceil(T... args)
ScalingAlgorithm algorithm
Scaling algorithm to use.
float quantizeLevel
Divisor of the standard deviation for STDEV_* scaling.
@ STDEV_NEGATIVE
Scale based on the standard deviation, dynamic range negative.
@ STDEV_POSITIVE
Scale based on the standard deviation. dynamic range positive.
@ STDEV_BOTH
Scale based on the standard deviation, dynamic range positive+negative.
@ RANGE
Scale to preserve dynamic range.
float quantizePad
Number of stdev to allow on the low/high side (for STDEV_POSITIVE/NEGATIVE)
int bitpix
Bits per pixel (0, 8,16,32,64,-32,-64)
double bscale
Manually specified BSCALE (for MANUAL scaling)
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.
double bzero
Manually specified BZERO (for MANUAL scaling)
Reports invalid arguments.
Definition: Runtime.h:66
float * fits_rand_value
int const N_RESERVED_VALUES
#define INSTANTIATE(TYPE)
T floor(T... args)
T isfinite(T... args)
T make_pair(T... args)
T max(T... args)
T min(T... args)
def scale(algorithm, min, max=None, frame=None)
Definition: ds9.py:108
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.
std::string compressionAlgorithmToString(ImageCompressionOptions::CompressionAlgorithm algorithm)
Provide string version of compression algorithm.
ImageScalingOptions::ScalingAlgorithm scalingAlgorithmFromString(std::string const &name)
Interpret scaling algorithm expressed in string.
int compressionAlgorithmToCfitsio(ImageCompressionOptions::CompressionAlgorithm algorithm)
Convert ImageCompressionOptions::CompressionAlgorithm to cfitsio.
std::string scalingAlgorithmToString(ImageScalingOptions::ScalingAlgorithm algorithm)
Provide string version of compression algorithm.
ImageCompressionOptions::CompressionAlgorithm compressionAlgorithmFromCfitsio(int cfitsio)
Convert compression algorithm from cfitsio to ImageCompressionOptions::CompressionAlgorithm.
ImageCompressionOptions::CompressionAlgorithm compressionAlgorithmFromString(std::string const &name)
Interpret compression algorithm expressed in string.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
def stdev(vector)
Definition: fringe.py:532
Angle abs(Angle const &a)
Definition: Angle.h:106
A base class for image defects.
STL namespace.
T nth_element(T... args)
T pow(T... args)
Scaling zero-point, set according to pixel type.
static constexpr double value
ImageCompressionOptions(CompressionAlgorithm algorithm_, Tiles tiles_, float quantizeLevel_=0.0)
Custom compression.
Tiles tiles
Tile size; a dimension with 0 means infinite (e.g., to specify one row: 0,1)
@ GZIP_SHUFFLE
GZIP compression with shuffle (most-significant byte first)
Scale to apply to image.
double bscale
Scale to apply when reading from FITS.
long blank
Value for integer images indicating non-finite values.
int bitpix
Bits per pixel; negative means floating-point: 8,16,32,64,-32,-64.
double bzero
Zero-point to apply when reading from FITS.
ndarray::Array< T, 2, 2 > fromFits(ndarray::Array< T, 2, 2 > const &image) const
Convert to an array.
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.
FITS BITPIX header value by C++ type.