diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt --- a/kstars/CMakeLists.txt +++ b/kstars/CMakeLists.txt @@ -240,6 +240,7 @@ set (fits_SRCS fitsviewer/fitslabel.cpp fitsviewer/fitsviewer.cpp + fitsviewer/stretch.cpp fitsviewer/fitstab.cpp fitsviewer/fitsdebayer.cpp fitsviewer/opsfits.cpp diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -126,12 +126,14 @@ void drawObjectNames(QPainter *painter); void drawPixelGrid(QPainter *painter); + bool isImageStretched(); bool isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); + // Setup the graphics. void updateFrame(); bool isTelescopeActive(); @@ -222,6 +224,8 @@ void centerTelescope(); + void toggleStretch(); + virtual void processPointSelection(int x, int y); virtual void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); @@ -300,6 +304,8 @@ bool showPixelGrid { false }; bool showStarsHFR { false }; + bool stretchImage { false }; + struct { bool used() const @@ -339,6 +345,8 @@ QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; + QAction *toggleStretchAction { nullptr }; + //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -19,6 +19,7 @@ #include "Options.h" #include "skymap.h" #include "fits_debug.h" +#include "stretch.h" #ifdef HAVE_INDI #include "basedevice.h" @@ -41,8 +42,28 @@ #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 +namespace +{ + +void doStretch(FITSData *data, QImage *outputImage, bool stretchOn) +{ + if (outputImage->isNull()) + return; + Stretch stretch(static_cast(data->width()), + static_cast(data->height()), + data->channels(), data->property("dataType").toInt()); + if (stretchOn) + stretch.setParams(stretch.computeParams(data->getImageBuffer())); + stretch.run(data->getImageBuffer(), outputImage); +} + +} // namespace + + FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { + stretchImage = Options::autoStretch(); + grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); @@ -384,119 +405,24 @@ { if (rawImage.isNull()) return false; - - uint8_t * imageBuffer = imageData->getImageBuffer(); - uint8_t * displayBuffer = nullptr; - uint32_t size = imageData->width() * imageData->height(); - - QVector min(3), max(3); - - if (Options::autoStretch()) - { - displayBuffer = new uint8_t[size * imageData->channels() * imageData->getBytesPerPixel()]; - memcpy(displayBuffer, imageBuffer, size * imageData->channels() * imageData->getBytesPerPixel()); - imageData->applyFilter(FITS_AUTO_STRETCH, displayBuffer, &min, &max); - } - else + if (true || image_height != imageData->height() || image_width != imageData->width()) { - displayBuffer = imageBuffer; - for (int i = 0; i < 3; i++) - { - min[i] = imageData->getMin(i); - max[i] = imageData->getMax(i); - } - } - - scaledImage = QImage(); + image_width = imageData->width(); + image_height = imageData->height(); - auto * buffer = reinterpret_cast(displayBuffer); + initDisplayImage(); - if (min[0] == max[0]) - { - rawImage.fill(Qt::white); - emit newStatus(i18n("Image is saturated."), FITS_MESSAGE); + if (isVisible()) + emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); } - else - { - if (image_height != imageData->height() || image_width != imageData->width()) - { - image_width = imageData->width(); - image_height = imageData->height(); - initDisplayImage(); + image_frame->setScaledContents(true); + currentWidth = rawImage.width(); + currentHeight = rawImage.height(); - if (isVisible()) - emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); - } - - image_frame->setScaledContents(true); - currentWidth = rawImage.width(); - currentHeight = rawImage.height(); - - if (imageData->channels() == 1) - { - double range = max[0] - min[0]; - double bscale = 255. / range; - double bzero = (-min[0]) * (255. / range); - - QVector> futures; - - /* Fill in pixel values using indexed map, linear scale */ - for (uint32_t j = 0; j < image_height; j++) - { - futures.append(QtConcurrent::run([ = ]() - { - T * runningBuffer = buffer + j * image_width; - uint8_t * scanLine = rawImage.scanLine(j); - for (uint32_t i = 0; i < image_width; i++) - { - //scanLine[i] = qBound(0, static_cast(runningBuffer[i] * bscale + bzero), 255); - scanLine[i] = qBound(0.0, runningBuffer[i] * bscale + bzero, 255.0); - } - })); - } - - for(QFuture future : futures) - future.waitForFinished(); - } - else - { - QVector> futures; - double bscale[3], bzero[3]; - for (int i = 0; i < 3; i++) - { - bscale[i] = 255. / (max[i] - min[i]); - bzero[i] = (-min[i]) * (255. / (max[i] - min[i])); - } - - /* Fill in pixel values using indexed map, linear scale */ - for (uint32_t j = 0; j < image_height; j++) - { - futures.append(QtConcurrent::run([ = ]() - { - auto * scanLine = reinterpret_cast((rawImage.scanLine(j))); - T * runningBufferR = buffer + j * image_width; - T * runningBufferG = buffer + j * image_width + size; - T * runningBufferB = buffer + j * image_width + size * 2; - - for (uint32_t i = 0; i < image_width; i++) - { - scanLine[i] = qRgb(runningBufferR[i] * bscale[0] + bzero[0], - runningBufferG[i] * bscale[1] + bzero[1], - runningBufferB[i] * bscale[2] + bzero[2]); - } - })); - } - - for(QFuture future : futures) - future.waitForFinished(); - } - - } - - // Clear memory if it was allocated. - if (displayBuffer != imageBuffer) - delete [] displayBuffer; + doStretch(imageData, &rawImage, stretchImage); + + scaledImage = QImage(); switch (type) { @@ -627,6 +553,9 @@ { bool ok = false; + if (toggleStretchAction) + toggleStretchAction->setChecked(stretchImage); + if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary @@ -1215,6 +1144,11 @@ setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } +bool FITSView::isImageStretched() +{ + return stretchImage; +} + bool FITSView::isCrosshairShown() { return showCrosshair; @@ -1278,6 +1212,13 @@ updateFrame(); } +void FITSView::toggleStretch() +{ + stretchImage = !stretchImage; + if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) + updateFrame(); +} + void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION @@ -1635,6 +1576,12 @@ floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); + toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"), + i18n("Toggle Stretch"), + this, SLOT(toggleStretch())); + toggleStretchAction->setCheckable(true); + + floatingToolBar->addSeparator(); action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), diff --git a/kstars/fitsviewer/fitsviewer.h b/kstars/fitsviewer/fitsviewer.h --- a/kstars/fitsviewer/fitsviewer.h +++ b/kstars/fitsviewer/fitsviewer.h @@ -108,7 +108,6 @@ void headerFITS(); void debayerFITS(); void histoFITS(); - void stretchFITS(); void tabFocusUpdated(int currentIndex); void updateStatusBar(const QString &msg, FITSBar id); void ZoomIn(); @@ -128,6 +127,7 @@ void centerTelescope(); void updateWCSFunctions(); void applyFilter(int ftype); + void toggleStretch(); void rotateCW(); void rotateCCW(); void flipHorizontal(); diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -150,10 +150,11 @@ connect(action, SIGNAL(triggered(bool)), SLOT(debayerFITS())); action = actionCollection()->addAction("image_stretch"); - action->setText(i18n("Auto stretch")); - connect(action, SIGNAL(triggered(bool)), SLOT(stretchFITS())); + action->setText(i18n("Toggle Auto stretch")); + action->setCheckable(true); + connect(action, SIGNAL(triggered(bool)), SLOT(toggleStretch())); actionCollection()->setDefaultShortcut(action, QKeySequence::SelectAll); - action->setIcon(QIcon::fromTheme("transform-move")); + action->setIcon(QIcon::fromTheme("transform-move")); action = KStandardAction::close(this, SLOT(close()), actionCollection()); action->setIcon(QIcon::fromTheme("window-close")); @@ -397,6 +398,8 @@ tab->getView()->setCursorMode(FITSView::dragCursor); + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), tab->getView()->isImageStretched()); + updateWCSFunctions(); return true; @@ -592,6 +595,9 @@ updateButtonStatus("view_eq_grid", i18n("Equatorial Gridines"), getCurrentView()->isEQGridShown()); updateButtonStatus("view_objects", i18n("Objects in Image"), getCurrentView()->areObjectsShown()); updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), getCurrentView()->isPixelGridShown()); + + fprintf(stderr, "Updating button status to %s\n", getCurrentView()->isImageStretched() ? "true" : "false"); + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), getCurrentView()->isImageStretched()); updateScopeButton(); updateWCSFunctions(); } @@ -665,11 +671,6 @@ fitsTabs[fitsTabWidget->currentIndex()]->statFITS(); } -void FITSViewer::stretchFITS() -{ - applyFilter(FITS_AUTO_STRETCH); -} - void FITSViewer::rotateCW() { applyFilter(FITS_ROTATE_CW); @@ -991,6 +992,23 @@ updateStatusBar(i18n("Ready."), FITS_MESSAGE); } +void FITSViewer::toggleStretch() +{ + if (fitsTabs.empty()) + return; + + QApplication::setOverrideCursor(Qt::WaitCursor); + updateStatusBar(i18n("Processing toggle stretch"), FITS_MESSAGE); + qApp->processEvents(); + fitsTabs[fitsTabWidget->currentIndex()]->getView()->toggleStretch(); + + updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), + getCurrentView()->isImageStretched()); + + QApplication::restoreOverrideCursor(); + updateStatusBar(i18n("Ready."), FITS_MESSAGE); +} + FITSView *FITSViewer::getView(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); diff --git a/kstars/fitsviewer/stretch.h b/kstars/fitsviewer/stretch.h new file mode 100644 --- /dev/null +++ b/kstars/fitsviewer/stretch.h @@ -0,0 +1,90 @@ +/* Stretch + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. +*/ + +#pragma once + +#include +#include + +struct StretchParams1Channel +{ + // Stretch algorithm parameters + float shadows;; + float highlights; + float midtones; + // The extension parameters are not yet used. + float shadows_expansion; + float highlights_expansion; + + // The default parameters result in no stretch at all. + StretchParams1Channel() + { + shadows = 0.0; + highlights = 1.0; + midtones = 0.5; + shadows_expansion = 0.0; + highlights_expansion = 1.0; + } +}; + +struct StretchParams +{ + StretchParams1Channel grey_red, green, blue; +}; + +class Stretch +{ + public: + /** + * @brief Stretch Constructor for Stretch class + * @param image_buffer pointer to the image memory + * @param width the image width + * @param height the image height + * @param channels should be 1 or 3 + * @note The image should either be 1-channel or 3-channel + * The image buffer is not copied, so it should not be deleted while the object is in use + */ + explicit Stretch(int width, int height, int channels, int data_type); + ~Stretch() {} + + /** + * @brief setParams Sets the stretch parameters. + * @param param The desired parameter values. + * @note This set method used for both 1-channel and 3-channel images. + * In 1-channel images, the _g and _b parameters are ignored. + * The parameter scale is 0-1 for all data types. + */ + void setParams(StretchParams input_params) { params = input_params; } + + /** + * @brief getParams Returns the stretch parameters (computed by computeParameters()). + */ + StretchParams getParams() { return params; } + + /** + * @brief computeParams Automatically generates and sets stretch parameters from the image. + */ + StretchParams computeParams(uint8_t *input); + + /** + * @brief run run the stretch algorithm according to the params given + * placing the output in output_image. + */ + void run(uint8_t *input, QImage *output_image); + + private: + // Inputs. + int image_width; + int image_height; + int image_channels; + int input_range; + int dataType; + + // Parameters. + StretchParams params; +}; diff --git a/kstars/fitsviewer/stretch.cpp b/kstars/fitsviewer/stretch.cpp new file mode 100644 --- /dev/null +++ b/kstars/fitsviewer/stretch.cpp @@ -0,0 +1,415 @@ +#include "stretch.h" + +/* Stretch + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. +*/ + +#include "stretch.h" + +#include +#include +#include + +namespace { + +// Returns the median v of the vector. +// The vector is modified in an undefined way. +template +T median(std::vector& values) +{ + const int middle = values.size() / 2; + std::nth_element(values.begin(), values.begin() + middle, values.end()); + return values[middle]; +} + +// Returns the median of the sample values. +// The values are not modified. +template +T median(T *values, int size, int sampleBy) +{ + const int downsampled_size = size / sampleBy; + std::vector samples(downsampled_size); + for (int index = 0, i = 0; index < size; ++i, index += sampleBy) + samples[i] = values[index]; + return median(samples); +} + +// This stretches one channel given the input parameters. +// Based on the spec in section 8.5.6 +// http://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html +// Uses multiple threads, blocks until done. +// The extension parameters are not used. +template +void stretchOneChannel(T *input_buffer, QImage *output_image, + const StretchParams& stretch_params, + int input_range, int image_height, int image_width) +{ + QVector> futures; + + // We're outputting uint8, so the max output is 255. + constexpr int maxOutput = 255; + + // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). + const float maxInput = input_range > 1 ? input_range - 1 : input_range; + + const float midtones = stretch_params.grey_red.midtones; + const float highlights = stretch_params.grey_red.highlights; + const float shadows = stretch_params.grey_red.shadows; + + // Precomputed expressions moved out of the loop. + // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. + const float hsRangeFactor = highlights == shadows ? 1.0 : 1.0 / (highlights - shadows); + // Shadow and highlight values translated to the ADU scale. + const T nativeShadows = shadows * maxInput; + const T nativeHighlights = highlights * maxInput; + // Constants based on above needed for the stretch calculations. + const float k1 = (midtones - 1) * hsRangeFactor * maxOutput / maxInput; + const float k2 = ((2 * midtones) - 1) * hsRangeFactor / maxInput; + + for (int j = 0; j < image_height; j++) + { + futures.append(QtConcurrent::run([ = ]() + { + T * inputLine = input_buffer + j * image_width; + auto * scanLine = output_image->scanLine(j); + + for (int i = 0; i < image_width; i++) + { + const T input = inputLine[i]; + if (input < nativeShadows) scanLine[i] = 0; + else if (input >= nativeHighlights) scanLine[i] = maxOutput; + else + { + const T inputFloored = (input - nativeShadows); + scanLine[i] = (inputFloored * k1) / (inputFloored * k2 - midtones); + } + } + })); + } + for(QFuture future : futures) + future.waitForFinished(); +} + +// This is like the above 1-channel stretch, but extended for 3 channels. +// This could have been more modular, but the three channels are combined +// into a single qRgb value at the end, so it seems the simplest thing is to +// replicate the code. It is assume the colors are not interleaved--the red image +// is stored fully, then the green, then the blue. +template +void stretchThreeChannels(T *inputBuffer, QImage *outputImage, + const StretchParams& stretchParams, + int inputRange, int imageHeight, int imageWidth) +{ + QVector> futures; + + // We're outputting uint8, so the max output is 255. + constexpr int maxOutput = 255; + + // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). + const float maxInput = inputRange > 1 ? inputRange - 1 : inputRange; + + const float midtonesR = stretchParams.grey_red.midtones; + const float highlightsR = stretchParams.grey_red.highlights; + const float shadowsR = stretchParams.grey_red.shadows; + const float midtonesG = stretchParams.green.midtones; + const float highlightsG = stretchParams.green.highlights; + const float shadowsG = stretchParams.green.shadows; + const float midtonesB = stretchParams.blue.midtones; + const float highlightsB = stretchParams.blue.highlights; + const float shadowsB = stretchParams.blue.shadows; + + // Precomputed expressions moved out of the loop. + // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. + const float hsRangeFactorR = highlightsR == shadowsR ? 1.0 : 1.0 / (highlightsR - shadowsR); + const float hsRangeFactorG = highlightsG == shadowsG ? 1.0 : 1.0 / (highlightsG - shadowsG); + const float hsRangeFactorB = highlightsB == shadowsB ? 1.0 : 1.0 / (highlightsB - shadowsB); + // Shadow and highlight values translated to the ADU scale. + const T nativeShadowsR = shadowsR * maxInput; + const T nativeShadowsG = shadowsG * maxInput; + const T nativeShadowsB = shadowsB * maxInput; + const T nativeHighlightsR = highlightsR * maxInput; + const T nativeHighlightsG = highlightsG * maxInput; + const T nativeHighlightsB = highlightsB * maxInput; + // Constants based on above needed for the stretch calculations. + const float k1R = (midtonesR - 1) * hsRangeFactorR * maxOutput / maxInput; + const float k1G = (midtonesG - 1) * hsRangeFactorG * maxOutput / maxInput; + const float k1B = (midtonesB - 1) * hsRangeFactorB * maxOutput / maxInput; + const float k2R = ((2 * midtonesR) - 1) * hsRangeFactorR / maxInput; + const float k2G = ((2 * midtonesG) - 1) * hsRangeFactorG / maxInput; + const float k2B = ((2 * midtonesB) - 1) * hsRangeFactorB / maxInput; + + const int size = imageWidth * imageHeight; + + for (int j = 0; j < imageHeight; j++) + { + futures.append(QtConcurrent::run([ = ]() + { + // R, G, B input images are stored one after another. + T * inputLineR = inputBuffer + j * imageWidth; + T * inputLineG = inputLineR + size; + T * inputLineB = inputLineG + size; + + auto * scanLine = reinterpret_cast(outputImage->scanLine(j)); + + for (int i = 0; i < imageWidth; i++) + { + const T inputR = inputLineR[i]; + const T inputG = inputLineG[i]; + const T inputB = inputLineB[i]; + + uint8_t red, green, blue; + + if (inputR < nativeShadowsR) red = 0; + else if (inputR >= nativeHighlightsR) red = maxOutput; + else + { + const T inputFloored = (inputR - nativeShadowsR); + red = (inputFloored * k1R) / (inputFloored * k2R - midtonesR); + } + + if (inputG < nativeShadowsG) green = 0; + else if (inputG >= nativeHighlightsG) green = maxOutput; + else + { + const T inputFloored = (inputG - nativeShadowsG); + green = (inputFloored * k1G) / (inputFloored * k2G - midtonesG); + } + + if (inputB < nativeShadowsB) blue = 0; + else if (inputB >= nativeHighlightsB) blue = maxOutput; + else + { + const T inputFloored = (inputB - nativeShadowsB); + blue = (inputFloored * k1B) / (inputFloored * k2B - midtonesB); + } + scanLine[i] = qRgb(red, green, blue); + } + })); + } + for(QFuture future : futures) + future.waitForFinished(); +} + +template +void stretchChannels(T *input_buffer, QImage *output_image, + const StretchParams& stretch_params, + int input_range, int image_height, int image_width, int num_channels) +{ + if (num_channels == 1) + stretchOneChannel(input_buffer, output_image, stretch_params, input_range, + image_height, image_width); + else if (num_channels == 3) + stretchThreeChannels(input_buffer, output_image, stretch_params, input_range, + image_height, image_width); +} + +// See section 8.5.7 in above link http://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html +template +void computeParamsOneChannel(T *buffer, StretchParams1Channel *params, + int inputRange, int height, int width) +{ + // Find the median sample. + constexpr int maxSamples = 500000; + const int sampleBy = width * height < maxSamples ? 1 : width * height / maxSamples; + const int size = width * height; + T medianSample = median(buffer, width * height, sampleBy); + + // Find the Median deviation: 1.4826 * median of abs(sample[i] - median). + const int numSamples = width * height / sampleBy; + std::vector deviations(numSamples); + for (int index = 0, i = 0; index < size; ++i, index += sampleBy) + { + if (medianSample > buffer[index]) + deviations[i] = medianSample - buffer[index]; + else + deviations[i] = buffer[index] - medianSample; + } + + // Shift everything to 0 -> 1.0. + const float medDev = median(deviations); + const float normalizedMedian = medianSample / static_cast(inputRange); + const float MADN = 1.4826 * medDev / static_cast(inputRange); + + const bool upperHalf = normalizedMedian > 0.5; + + const float shadows = (upperHalf || MADN == 0) ? 0.0 : + fmin(1.0, fmax(0.0, (normalizedMedian + -2.8 * MADN))); + + const float highlights = (!upperHalf || MADN == 0) ? 1.0 : + fmin(1.0, fmax(0.0, (normalizedMedian - -2.8 * MADN))); + + float X, M; + constexpr float B = 0.25; + if (!upperHalf) { + X = normalizedMedian - shadows; + M = B; + } else { + X = B; + M = highlights - normalizedMedian; + } + float midtones; + if (X == 0) midtones = 0; + else if (X == M) midtones = 0.5; + else if (X == 1) midtones = 1.0; + else midtones = ((M - 1) * X) / ((2 * M - 1) * X - M); + + // Store the params. + params->shadows = shadows; + params->highlights = highlights; + params->midtones = midtones; + params->shadows_expansion = 0.0; + params->highlights_expansion = 1.0; +} + +// Need to know the possible range of input values. +// Using the type of the sample and guessing. +// Perhaps we should examine the contents for the file +// (e.g. look at maximum value and extrapolate from that). +int getRange(int data_type) +{ + switch (data_type) + { + case TBYTE: + return 256; + break; + case TSHORT: + return 64*1024; + break; + case TUSHORT: + return 64*1024; + break; + case TLONG: + return 64*1024; + break; + case TFLOAT: + return 64*1024; + break; + case TLONGLONG: + return 64*1024; + break; + case TDOUBLE: + return 64*1024; + break; + default: + return 64*1024; + break; + } +} + +} // namespace + +Stretch::Stretch(int width, int height, int channels, int data_type) +{ + image_width = width; + image_height = height; + image_channels = channels; + dataType = data_type; + input_range = getRange(dataType); +} + +void Stretch::run(uint8_t *input, QImage *outputImage) +{ + switch (dataType) + { + case TBYTE: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TSHORT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TUSHORT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TLONG: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TFLOAT: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TLONGLONG: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + case TDOUBLE: + stretchChannels(reinterpret_cast(input), outputImage, params, + input_range, image_height, image_width, image_channels); + break; + default: + break; + } +} + +StretchParams Stretch::computeParams(uint8_t *input) +{ + StretchParams result; + for (int channel = 0; channel < image_channels; ++channel) + { + int offset = channel * image_width * image_height; + StretchParams1Channel *params = channel == 0 ? &result.grey_red : + (channel == 1 ? &result.green : &result.blue); + switch (dataType) + { + case TBYTE: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TSHORT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TUSHORT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TLONG: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TFLOAT: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TLONGLONG: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + case TDOUBLE: + { + auto buffer = reinterpret_cast(input); + computeParamsOneChannel(buffer + offset, params, input_range, + image_height, image_width); + break; + } + default: + break; + } + } + return result; +}