1 /*  Stretch
2 
3     This application is free software; you can redistribute it and/or
4     modify it under the terms of the GNU General Public
5     License as published by the Free Software Foundation; either
6     version 2 of the License, or (at your option) any later version.
7 */
8 
9 #include "stretch.h"
10 
11 #include <fitsio.h>
12 #include <math.h>
13 #include <QtConcurrent>
14 #include "sep/sep.h"
15 
16 namespace {
17 
18 // Returns the median value of the vector.
19 // The vector is modified in an undefined way.
20 template <typename T>
median(std::vector<T> & values)21 T median(std::vector<T>& values)
22 {
23   const int middle = values.size() / 2;
24   std::nth_element(values.begin(), values.begin() + middle, values.end());
25   return values[middle];
26 }
27 
28 // Returns the rough max of the buffer.
29 template <typename T>
sampledMax(T * values,int size,int sampleBy)30 T sampledMax(T *values, int size, int sampleBy)
31 {
32     T maxVal = 0;
33     for (int i = 0; i < size; i+= sampleBy)
34         if (maxVal < values[i])
35             maxVal = values[i];
36     return  maxVal;
37 }
38 
39 // Returns the median of the sample values.
40 // The values are not modified.
41 template <typename T>
median(T * values,int size,int sampleBy)42 T median(T *values, int size, int sampleBy)
43 {
44   const int downsampled_size = size / sampleBy;
45   std::vector<T> samples(downsampled_size);
46   for (int index = 0, i = 0; i < downsampled_size; ++i, index += sampleBy)
47     samples[i] = values[index];
48   return median(samples);
49 }
50 
51 // This stretches one channel given the input parameters.
52 // Based on the spec in section 8.5.6
53 // https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html
54 // Uses multiple threads, blocks until done.
55 // The extension parameters are not used.
56 // Sampling is applied to the output (that is, with sampling=2, we compute every other output
57 // sample both in width and height, so the output would have about 4X fewer pixels.
58 template <typename T>
stretchOneChannel(T * input_buffer,QImage * output_image,const StretchParams & stretch_params,int input_range,int image_height,int image_width,int sampling)59 void stretchOneChannel(T *input_buffer, QImage *output_image,
60                        const StretchParams& stretch_params,
61                        int input_range, int image_height, int image_width, int sampling)
62 {
63   QVector<QFuture<void>> futures;
64 
65   // We're outputting uint8, so the max output is 255.
66   constexpr int maxOutput = 255;
67 
68   // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int).
69   const float maxInput = input_range > 1 ? input_range - 1 : input_range;
70 
71   const float midtones   = stretch_params.grey_red.midtones;
72   const float highlights = stretch_params.grey_red.highlights;
73   const float shadows    = stretch_params.grey_red.shadows;
74 
75   // Precomputed expressions moved out of the loop.
76   // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale.
77   const float hsRangeFactor = highlights == shadows ? 1.0f : 1.0f / (highlights - shadows);
78   // Shadow and highlight values translated to the ADU scale.
79   const T nativeShadows = shadows * maxInput;
80   const T nativeHighlights = highlights * maxInput;
81   // Constants based on above needed for the stretch calculations.
82   const float k1 = (midtones - 1) * hsRangeFactor * maxOutput / maxInput;
83   const float k2 = ((2 * midtones) - 1) * hsRangeFactor / maxInput;
84 
85   // Increment the input index by the sampling, the output index increments by 1.
86   for (int j = 0, jout = 0; j < image_height; j+=sampling, jout++)
87   {
88     futures.append(QtConcurrent::run([ = ]()
89     {
90         T * inputLine  = input_buffer + j * image_width;
91         auto * scanLine = output_image->scanLine(jout);
92 
93     for (int i = 0, iout = 0; i < image_width; i+=sampling, iout++)
94         {
95           const T input = inputLine[i];
96           if (input < nativeShadows) scanLine[iout] = 0;
97           else if (input >= nativeHighlights) scanLine[iout] = maxOutput;
98           else
99           {
100             const T inputFloored = (input - nativeShadows);
101             scanLine[iout] = (inputFloored * k1) / (inputFloored * k2 - midtones);
102 	  }
103         }
104     }));
105   }
106   for(QFuture<void> future : futures)
107     future.waitForFinished();
108 }
109 
110 // This is like the above 1-channel stretch, but extended for 3 channels.
111 // This could have been more modular, but the three channels are combined
112 // into a single qRgb value at the end, so it seems the simplest thing is to
113 // replicate the code. It is assume the colors are not interleaved--the red image
114 // is stored fully, then the green, then the blue.
115 // Sampling is applied to the output (that is, with sampling=2, we compute every other output
116 // sample both in width and height, so the output would have about 4X fewer pixels.
117 template <typename T>
stretchThreeChannels(T * inputBuffer,QImage * outputImage,const StretchParams & stretchParams,int inputRange,int imageHeight,int imageWidth,int sampling)118 void stretchThreeChannels(T *inputBuffer, QImage *outputImage,
119                           const StretchParams& stretchParams,
120                           int inputRange, int imageHeight, int imageWidth, int sampling)
121 {
122   QVector<QFuture<void>> futures;
123 
124   // We're outputting uint8, so the max output is 255.
125   constexpr int maxOutput = 255;
126 
127   // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int).
128   const float maxInput = inputRange > 1 ? inputRange - 1 : inputRange;
129 
130   const float midtonesR   = stretchParams.grey_red.midtones;
131   const float highlightsR = stretchParams.grey_red.highlights;
132   const float shadowsR    = stretchParams.grey_red.shadows;
133   const float midtonesG   = stretchParams.green.midtones;
134   const float highlightsG = stretchParams.green.highlights;
135   const float shadowsG    = stretchParams.green.shadows;
136   const float midtonesB   = stretchParams.blue.midtones;
137   const float highlightsB = stretchParams.blue.highlights;
138   const float shadowsB    = stretchParams.blue.shadows;
139 
140   // Precomputed expressions moved out of the loop.
141   // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale.
142   const float hsRangeFactorR = highlightsR == shadowsR ? 1.0f : 1.0f / (highlightsR - shadowsR);
143   const float hsRangeFactorG = highlightsG == shadowsG ? 1.0f : 1.0f / (highlightsG - shadowsG);
144   const float hsRangeFactorB = highlightsB == shadowsB ? 1.0f : 1.0f / (highlightsB - shadowsB);
145   // Shadow and highlight values translated to the ADU scale.
146   const T nativeShadowsR = shadowsR * maxInput;
147   const T nativeShadowsG = shadowsG * maxInput;
148   const T nativeShadowsB = shadowsB * maxInput;
149   const T nativeHighlightsR = highlightsR * maxInput;
150   const T nativeHighlightsG = highlightsG * maxInput;
151   const T nativeHighlightsB = highlightsB * maxInput;
152   // Constants based on above needed for the stretch calculations.
153   const float k1R = (midtonesR - 1) * hsRangeFactorR * maxOutput / maxInput;
154   const float k1G = (midtonesG - 1) * hsRangeFactorG * maxOutput / maxInput;
155   const float k1B = (midtonesB - 1) * hsRangeFactorB * maxOutput / maxInput;
156   const float k2R = ((2 * midtonesR) - 1) * hsRangeFactorR / maxInput;
157   const float k2G = ((2 * midtonesG) - 1) * hsRangeFactorG / maxInput;
158   const float k2B = ((2 * midtonesB) - 1) * hsRangeFactorB / maxInput;
159 
160   const int size = imageWidth * imageHeight;
161 
162   for (int j = 0, jout = 0; j < imageHeight; j+=sampling, jout++)
163   {
164     futures.append(QtConcurrent::run([ = ]()
165     {
166         // R, G, B input images are stored one after another.
167         T * inputLineR  = inputBuffer + j * imageWidth;
168         T * inputLineG  = inputLineR + size;
169         T * inputLineB  = inputLineG + size;
170 
171         auto * scanLine = reinterpret_cast<QRgb*>(outputImage->scanLine(jout));
172 
173     for (int i = 0, iout = 0; i < imageWidth; i+=sampling, iout++)
174         {
175           const T inputR = inputLineR[i];
176           const T inputG = inputLineG[i];
177           const T inputB = inputLineB[i];
178 
179           uint8_t red, green, blue;
180 
181           if (inputR < nativeShadowsR) red = 0;
182           else if (inputR >= nativeHighlightsR) red = maxOutput;
183           else
184           {
185             const T inputFloored = (inputR - nativeShadowsR);
186             red = (inputFloored * k1R) / (inputFloored * k2R - midtonesR);
187 	  }
188 
189           if (inputG < nativeShadowsG) green = 0;
190           else if (inputG >= nativeHighlightsG) green = maxOutput;
191           else
192           {
193             const T inputFloored = (inputG - nativeShadowsG);
194             green = (inputFloored * k1G) / (inputFloored * k2G - midtonesG);
195 	  }
196 
197           if (inputB < nativeShadowsB) blue = 0;
198           else if (inputB >= nativeHighlightsB) blue = maxOutput;
199           else
200           {
201             const T inputFloored = (inputB - nativeShadowsB);
202             blue = (inputFloored * k1B) / (inputFloored * k2B - midtonesB);
203 	  }
204           scanLine[iout] = qRgb(red, green, blue);
205         }
206     }));
207   }
208   for(QFuture<void> future : futures)
209     future.waitForFinished();
210 }
211 
212 template <typename T>
stretchChannels(T * input_buffer,QImage * output_image,const StretchParams & stretch_params,int input_range,int image_height,int image_width,int num_channels,int sampling)213 void stretchChannels(T *input_buffer, QImage *output_image,
214                        const StretchParams& stretch_params,
215                      int input_range, int image_height, int image_width, int num_channels, int sampling)
216 {
217     if (num_channels == 1)
218       stretchOneChannel(input_buffer, output_image, stretch_params, input_range,
219                         image_height, image_width, sampling);
220     else if (num_channels == 3)
221       stretchThreeChannels(input_buffer, output_image, stretch_params, input_range,
222                            image_height, image_width, sampling);
223 }
224 
225 // See section 8.5.7 in above link  https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html
226 template <typename T>
computeParamsOneChannel(T * buffer,StretchParams1Channel * params,int inputRange,int height,int width)227 void computeParamsOneChannel(T *buffer, StretchParams1Channel *params,
228                              int inputRange, int height, int width)
229 {
230   // Find the median sample.
231   constexpr int maxSamples = 500000;
232   const int sampleBy = width * height < maxSamples ? 1 : width * height / maxSamples;
233 
234   T medianSample = median(buffer, width * height, sampleBy);
235   // Find the Median deviation: 1.4826 * median of abs(sample[i] - median).
236   const int numSamples = width * height / sampleBy;
237   std::vector<T> deviations(numSamples);
238   for (int index = 0, i = 0; i < numSamples; ++i, index += sampleBy)
239   {
240     if (medianSample > buffer[index])
241       deviations[i] = medianSample - buffer[index];
242     else
243       deviations[i] = buffer[index] - medianSample;
244   }
245 
246   // Shift everything to 0 -> 1.0.
247   const float medDev = median(deviations);
248   const float normalizedMedian = medianSample / static_cast<float>(inputRange);
249   const float MADN = 1.4826 * medDev / static_cast<float>(inputRange);
250 
251   const bool upperHalf = normalizedMedian > 0.5;
252 
253   const float shadows = (upperHalf || MADN == 0) ? 0.0 :
254     fmin(1.0, fmax(0.0, (normalizedMedian + -2.8 * MADN)));
255 
256   const float highlights = (!upperHalf || MADN == 0) ? 1.0 :
257     fmin(1.0, fmax(0.0, (normalizedMedian - -2.8 * MADN)));
258 
259   float X, M;
260   constexpr float B = 0.25;
261   if (!upperHalf) {
262     X = normalizedMedian - shadows;
263     M = B;
264   } else {
265     X = B;
266     M = highlights - normalizedMedian;
267   }
268   float midtones;
269   if (X == 0) midtones = 0.0f;
270   else if (X == M) midtones = 0.5f;
271   else if (X == 1) midtones = 1.0f;
272   else midtones = ((M - 1) * X) / ((2 * M - 1) * X - M);
273 
274   // Store the params.
275   params->shadows = shadows;
276   params->highlights = highlights;
277   params->midtones = midtones;
278   params->shadows_expansion = 0.0;
279   params->highlights_expansion = 1.0;
280 }
281 
282 // Need to know the possible range of input values.
283 // Using the type of the sample and guessing.
284 // Perhaps we should examine the contents for the file
285 // (e.g. look at maximum value and extrapolate from that).
getRange(int data_type)286 int getRange(int data_type)
287 {
288     switch (data_type)
289     {
290         case SEP_TBYTE:
291             return 256;
292         case TSHORT:
293             return 64*1024;
294         case TUSHORT:
295             return 64*1024;
296         case TLONG:
297             return 64*1024;
298         case TFLOAT:
299             return 64*1024;
300         case TLONGLONG:
301             return 64*1024;
302         case TDOUBLE:
303             return 64*1024;
304         default:
305             return 64*1024;
306     }
307 }
308 
309 }  // namespace
310 
Stretch(int width,int height,int channels,int data_type)311 Stretch::Stretch(int width, int height, int channels, int data_type)
312 {
313   image_width = width;
314   image_height = height;
315   image_channels = channels;
316   dataType = data_type;
317   input_range = getRange(dataType);
318 }
319 
run(uint8_t * input,QImage * outputImage,int sampling)320 void Stretch::run(uint8_t *input, QImage *outputImage, int sampling)
321 {
322     Q_ASSERT(outputImage->width() == (image_width + sampling - 1) / sampling);
323     Q_ASSERT(outputImage->height() == (image_height + sampling - 1) / sampling);
324     recalculateInputRange(input);
325 
326     switch (dataType)
327     {
328         case SEP_TBYTE:
329             stretchChannels(reinterpret_cast<uint8_t*>(input), outputImage, params,
330                             input_range, image_height, image_width, image_channels, sampling);
331             break;
332         case TSHORT:
333             stretchChannels(reinterpret_cast<short*>(input), outputImage, params,
334                             input_range, image_height, image_width, image_channels, sampling);
335             break;
336         case TUSHORT:
337             stretchChannels(reinterpret_cast<unsigned short*>(input), outputImage, params,
338                             input_range, image_height, image_width, image_channels, sampling);
339             break;
340         case TLONG:
341             stretchChannels(reinterpret_cast<long*>(input), outputImage, params,
342                             input_range, image_height, image_width, image_channels, sampling);
343             break;
344         case TFLOAT:
345             stretchChannels(reinterpret_cast<float*>(input), outputImage, params,
346                             input_range, image_height, image_width, image_channels, sampling);
347             break;
348         case TLONGLONG:
349             stretchChannels(reinterpret_cast<long long*>(input), outputImage, params,
350                             input_range, image_height, image_width, image_channels, sampling);
351             break;
352         case TDOUBLE:
353             stretchChannels(reinterpret_cast<double*>(input), outputImage, params,
354                             input_range, image_height, image_width, image_channels, sampling);
355             break;
356         default:
357         break;
358     }
359 }
360 
361 // The input range for float/double is ambiguous, and we can't tell without the buffer,
362 // so we set it to 64K and possibly reduce it when we see the data.
recalculateInputRange(uint8_t * input)363 void Stretch::recalculateInputRange(uint8_t *input)
364 {
365     if (input_range <= 1) return;
366     if (dataType != TFLOAT && dataType != TDOUBLE) return;
367 
368     float mx = 0;
369     if (dataType == TFLOAT)
370         mx = sampledMax(reinterpret_cast<float*>(input), image_height * image_width, 1000);
371     else if (dataType == TDOUBLE)
372         mx = sampledMax(reinterpret_cast<double*>(input), image_height * image_width, 1000);
373     if (mx <= 1.01f) input_range = 1;
374 }
375 
computeParams(uint8_t * input)376 StretchParams Stretch::computeParams(uint8_t *input)
377 {
378   recalculateInputRange(input);
379   StretchParams result;
380   for (int channel = 0; channel < image_channels; ++channel)
381   {
382     int offset = channel * image_width * image_height;
383     StretchParams1Channel *params = channel == 0 ? &result.grey_red :
384       (channel == 1 ? &result.green : &result.blue);
385     switch (dataType)
386     {
387         case SEP_TBYTE:
388         {
389             auto buffer = reinterpret_cast<uint8_t*>(input);
390             computeParamsOneChannel(buffer + offset, params, input_range,
391                                     image_height, image_width);
392             break;
393         }
394         case TSHORT:
395         {
396             auto buffer = reinterpret_cast<short*>(input);
397             computeParamsOneChannel(buffer + offset, params, input_range,
398                                     image_height, image_width);
399             break;
400         }
401         case TUSHORT:
402         {
403             auto buffer = reinterpret_cast<unsigned short*>(input);
404             computeParamsOneChannel(buffer + offset, params, input_range,
405                                     image_height, image_width);
406             break;
407         }
408         case TLONG:
409         {
410             auto buffer = reinterpret_cast<long*>(input);
411             computeParamsOneChannel(buffer + offset, params, input_range,
412                                     image_height, image_width);
413             break;
414         }
415         case TFLOAT:
416         {
417             auto buffer = reinterpret_cast<float*>(input);
418             computeParamsOneChannel(buffer + offset, params, input_range,
419                                     image_height, image_width);
420             break;
421         }
422         case TLONGLONG:
423         {
424             auto buffer = reinterpret_cast<long long*>(input);
425             computeParamsOneChannel(buffer + offset, params, input_range,
426                                     image_height, image_width);
427             break;
428         }
429         case TDOUBLE:
430         {
431             auto buffer = reinterpret_cast<double*>(input);
432             computeParamsOneChannel(buffer + offset, params, input_range,
433                                     image_height, image_width);
434             break;
435         }
436         default:
437         break;
438     }
439   }
440   return result;
441 }
442