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