File indexing completed on 2024-04-28 15:10:21

0001 /*
0002     SPDX-FileCopyrightText: 2023 Hy Murveit <hy@murveit.com>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "fitsstretchui.h"
0007 #include "fitsview.h"
0008 #include "fitsdata.h"
0009 #include "Options.h"
0010 
0011 #include <KMessageBox>
0012 
0013 namespace
0014 {
0015 
0016 const char kAutoToolTip[] = "Automatically find stretch parameters";
0017 const char kStretchOffToolTip[] = "Stretch the image";
0018 const char kStretchOnToolTip[] = "Disable stretching of the image.";
0019 
0020 // The midtones slider works logarithmically (otherwise the useful range of the slider would
0021 // be only way on the left. So these functions translate from a linear slider value
0022 // logarithmically in the 0-1 range, assuming the slider  varies from 1 to 10000.
0023 constexpr double HISTO_SLIDER_MAX = 10000.0;
0024 constexpr double HISTO_SLIDER_FACTOR = 5.0;
0025 double midValueFcn(int x)
0026 {
0027     return pow(10, -(HISTO_SLIDER_FACTOR - (x / (HISTO_SLIDER_MAX / HISTO_SLIDER_FACTOR))));
0028 }
0029 int invertMidValueFcn(double x)
0030 {
0031     return (int) 0.5 + (HISTO_SLIDER_MAX / HISTO_SLIDER_FACTOR) * (HISTO_SLIDER_FACTOR + log10(x));
0032 }
0033 
0034 // These are defaults for the histogram plot's QCustomPlot axes.
0035 void setupAxisDefaults(QCPAxis *axis)
0036 {
0037     axis->setBasePen(QPen(Qt::white, 1));
0038     axis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
0039     axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
0040     axis->grid()->setZeroLinePen(Qt::NoPen);
0041     axis->setBasePen(QPen(Qt::white, 1));
0042     axis->setTickPen(QPen(Qt::white, 1));
0043     axis->setSubTickPen(QPen(Qt::white, 1));
0044     axis->setTickLabelColor(Qt::white);
0045     axis->setLabelColor(Qt::white);
0046     axis->grid()->setVisible(true);
0047 }
0048 }
0049 
0050 FITSStretchUI::FITSStretchUI(const QSharedPointer<FITSView> &view, QWidget * parent) : QWidget(parent)
0051 {
0052     setupUi(this);
0053     m_View = view;
0054     setupButtons();
0055     setupHistoPlot();
0056     setupHistoSlider();
0057     setupConnections();
0058 }
0059 
0060 void FITSStretchUI::setupButtons()
0061 {
0062     stretchButton->setIcon(QIcon::fromTheme("transform-move"));
0063     toggleHistoButton->setIcon(QIcon::fromTheme("histogram-symbolic"));
0064     autoButton->setIcon(QIcon::fromTheme("tools-wizard"));
0065 }
0066 
0067 void FITSStretchUI::setupHistoPlot()
0068 {
0069     histoPlot->setBackground(QBrush(QColor(25, 25, 25)));
0070     setupAxisDefaults(histoPlot->yAxis);
0071     setupAxisDefaults(histoPlot->xAxis);
0072     histoPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen);
0073     histoPlot->setMaximumHeight(75);
0074     histoPlot->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
0075     histoPlot->setVisible(false);
0076 
0077     connect(histoPlot, &QCustomPlot::mouseDoubleClick, this, &FITSStretchUI::onHistoDoubleClick);
0078     connect(histoPlot, &QCustomPlot::mouseMove, this, &FITSStretchUI::onHistoMouseMove);
0079 }
0080 
0081 void FITSStretchUI::onHistoDoubleClick(QMouseEvent *event)
0082 {
0083     Q_UNUSED(event);
0084     if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
0085     const double histogramSize = m_View->imageData()->getHistogramBinCount();
0086     histoPlot->xAxis->setRange(0, histogramSize + 1);
0087     histoPlot->replot();
0088 }
0089 
0090 // This creates 1-channel or RGB tooltips on this histogram.
0091 void FITSStretchUI::onHistoMouseMove(QMouseEvent *event)
0092 {
0093     const auto image = m_View->imageData();
0094     if (!image->isHistogramConstructed())
0095         return;
0096 
0097     const bool rgbHistogram = (image->channels() > 1);
0098     const int numPixels = image->width() * image->height();
0099     const int histogramSize = image->getHistogramBinCount();
0100     const int histoBin = std::max(0, std::min(histogramSize - 1,
0101                                   static_cast<int>(histoPlot->xAxis->pixelToCoord(event->x()))));
0102 
0103     QString tip = "";
0104     if (histoBin >= 0 && histoBin < histogramSize)
0105     {
0106         for (int c = 0; c < image->channels(); ++c)
0107         {
0108             const QVector<double> &intervals = image->getHistogramIntensity(c);
0109             const double lowRange = intervals[histoBin];
0110             const double highRange = lowRange + image->getHistogramBinWidth(c);
0111 
0112             if (rgbHistogram)
0113                 tip.append(QString("<font color=\"%1\">").arg(c == 0 ? "red" : (c == 1) ? "lightgreen" : "lightblue"));
0114 
0115             if (image->getMax(c) > 1.1)
0116                 tip.append(QString("%1 %2 %3: ").arg(lowRange, 0, 'f', 0).arg(QChar(0x2192)).arg(highRange, 0, 'f', 0));
0117             else
0118                 tip.append(QString("%1 %2 %3: ").arg(lowRange, 0, 'f', 4).arg(QChar(0x2192)).arg(highRange, 0, 'f', 4));
0119 
0120             const int count = image->getHistogramFrequency(c)[histoBin];
0121             const double percentage = count * 100.0 / (double) numPixels;
0122             tip.append(QString("%1 %2%").arg(count).arg(percentage, 0, 'f', 2));
0123             if (rgbHistogram)
0124                 tip.append("</font><br/>");
0125         }
0126     }
0127     if (tip.size() > 0)
0128         QToolTip::showText(event->globalPos(), tip, nullptr, QRect(), 10000);
0129 }
0130 
0131 void FITSStretchUI::setupHistoSlider()
0132 {
0133     histoSlider->setOrientation(Qt::Horizontal);
0134     histoSlider->setMinimum(0);
0135     histoSlider->setMaximum(HISTO_SLIDER_MAX);
0136     histoSlider->setMinimumPosition(0);
0137     histoSlider->setMaximumPosition(HISTO_SLIDER_MAX);
0138     histoSlider->setMidPosition(HISTO_SLIDER_MAX / 2);
0139     histoSlider->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
0140 
0141     connect(histoSlider, &ctk3Slider::minimumPositionChanged, this, [ = ](int value)
0142     {
0143         StretchParams params = m_View->getStretchParams();
0144         const double shadowValue = value / HISTO_SLIDER_MAX;
0145         if (shadowValue != params.grey_red.shadows)
0146         {
0147             params.grey_red.shadows = shadowValue;
0148 
0149             // In this and below callbacks, we allow for "stretchPreviewSampling". That is,
0150             // it downsamples the image when the slider is moving, to allow slower computers
0151             // to render faster, so that the slider can adjust the image in real time.
0152             // When the mouse is released, a final render at full resolution will be done.
0153             m_View->setPreviewSampling(Options::stretchPreviewSampling());
0154             m_View->setStretchParams(params);
0155             m_View->setPreviewSampling(0);
0156 
0157             // The min and max sliders draw cursors, corresponding to their positions, on
0158             // this histogram plot.
0159             setCursors(params);
0160             histoPlot->replot();
0161         }
0162     });
0163     connect(histoSlider, &ctk3Slider::maximumPositionChanged, this, [ = ](int value)
0164     {
0165         StretchParams params = m_View->getStretchParams();
0166         const double highValue = value / HISTO_SLIDER_MAX;
0167         if (highValue != params.grey_red.highlights)
0168         {
0169             params.grey_red.highlights = highValue;
0170             m_View->setPreviewSampling(Options::stretchPreviewSampling());
0171             m_View->setStretchParams(params);
0172             m_View->setPreviewSampling(0);
0173             setCursors(params);
0174             histoPlot->replot();
0175         }
0176     });
0177     connect(histoSlider, &ctk3Slider::midPositionChanged, this, [ = ](int value)
0178     {
0179         StretchParams params = m_View->getStretchParams();
0180         const double midValue = midValueFcn(value);
0181         if (midValue != params.grey_red.midtones)
0182         {
0183             params.grey_red.midtones = midValue;
0184             m_View->setPreviewSampling(Options::stretchPreviewSampling());
0185             m_View->setStretchParams(params);
0186             m_View->setPreviewSampling(0);
0187         }
0188     });
0189 
0190     // We need this released callback since if Options::stretchPreviewSampling() is > 1,
0191     // then when the sliders are dragged, the stretched image is rendered in lower resolution.
0192     // However when the dragging is done (and the mouse is released) we want to end by rendering
0193     // in full resolution.
0194     connect(histoSlider, &ctk3Slider::released, this, [ = ](int minValue, int midValue, int maxValue)
0195     {
0196         StretchParams params = m_View->getStretchParams();
0197         const double shadowValue = minValue / HISTO_SLIDER_MAX;
0198         const double middleValue = midValueFcn(midValue);
0199         const double highValue = maxValue / HISTO_SLIDER_MAX;
0200 
0201         if (middleValue != params.grey_red.midtones ||
0202                 highValue != params.grey_red.highlights ||
0203                 shadowValue != params.grey_red.shadows)
0204         {
0205             params.grey_red.shadows = shadowValue;
0206             params.grey_red.midtones = middleValue;
0207             params.grey_red.highlights = highValue;
0208             m_View->setPreviewSampling(0);
0209             m_View->setStretchParams(params);
0210         }
0211     });
0212 }
0213 
0214 // Updates all the widgets in the stretch area to display the view's stretch parameters.
0215 void FITSStretchUI::setStretchUIValues(const StretchParams1Channel &params)
0216 {
0217     shadowsVal->setValue(params.shadows);
0218     midtonesVal->setValue(params.midtones);
0219     highlightsVal->setValue(params.highlights);
0220 
0221     bool stretchActive = m_View->isImageStretched();
0222     if (stretchActive)
0223     {
0224         stretchButton->setChecked(true);
0225         stretchButton->setToolTip(kStretchOnToolTip);
0226     }
0227     else
0228     {
0229         stretchButton->setChecked(false);
0230         stretchButton->setToolTip(kStretchOffToolTip);
0231     }
0232 
0233     // Only activate the auto button if stretching is on and auto-stretching is not set.
0234     if (stretchActive && !m_View->getAutoStretch())
0235     {
0236         autoButton->setEnabled(true);
0237         autoButton->setIcon(QIcon::fromTheme("tools-wizard"));
0238         autoButton->setIconSize(QSize(22, 22));
0239         autoButton->setToolTip(kAutoToolTip);
0240     }
0241     else
0242     {
0243         autoButton->setEnabled(false);
0244         autoButton->setIcon(QIcon());
0245         autoButton->setIconSize(QSize(22, 22));
0246         autoButton->setToolTip("");
0247     }
0248     autoButton->setChecked(m_View->getAutoStretch());
0249 
0250     // Disable most of the UI if stretching is not active.
0251     shadowsVal->setEnabled(stretchActive);
0252     shadowsLabel->setEnabled(stretchActive);
0253     midtonesVal->setEnabled(stretchActive);
0254     midtonesLabel->setEnabled(stretchActive);
0255     highlightsVal->setEnabled(stretchActive);
0256     highlightsLabel->setEnabled(stretchActive);
0257     histoSlider->setEnabled(stretchActive);
0258 }
0259 
0260 void FITSStretchUI::setupConnections()
0261 {
0262     connect(m_View.get(), &FITSView::mouseOverPixel, this, [ this ](int x, int y)
0263     {
0264         if (pixelCursors.size() != m_View->imageData()->channels())
0265             pixelCursors.fill(nullptr, m_View->imageData()->channels());
0266 
0267         if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
0268         auto image = m_View->imageData();
0269         const int nChannels = m_View->imageData()->channels();
0270         for (int c = 0; c < nChannels; ++c)
0271         {
0272             if (pixelCursors[c] != nullptr)
0273             {
0274                 histoPlot->removeItem(pixelCursors[c]);
0275                 pixelCursors[c] = nullptr;
0276             }
0277             if (x < 0 || y < 0 || x >= m_View->imageData()->width() ||
0278                     y >= m_View->imageData()->height())
0279                 continue;
0280             int32_t bin = image->histogramBin(x, y, c);
0281             QColor color = Qt::darkGray;
0282             if (nChannels > 1)
0283                 color = c == 0 ? QColor(255, 10, 65) : ((c == 1) ? QColor(144, 238, 144, 225) : QColor(173, 216, 230, 175));
0284 
0285             pixelCursors[c] = setCursor(bin, QPen(color, 2, Qt::SolidLine));
0286         }
0287         histoPlot->replot();
0288     });
0289 
0290     connect(highlightsVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
0291     {
0292         StretchParams params = m_View->getStretchParams();
0293         params.grey_red.highlights = highlightsVal->value();
0294         setCursors(params);
0295         m_View->setStretchParams(params);
0296         histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
0297         histoPlot->replot();
0298     });
0299 
0300     connect(midtonesVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
0301     {
0302         StretchParams params = m_View->getStretchParams();
0303         params.grey_red.midtones = midtonesVal->value();
0304         setCursors(params);
0305         m_View->setStretchParams(params);
0306         histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
0307         histoPlot->replot();
0308     });
0309 
0310     connect(shadowsVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
0311     {
0312         StretchParams params = m_View->getStretchParams();
0313         params.grey_red.shadows = shadowsVal->value();
0314         setCursors(params);
0315         m_View->setStretchParams(params);
0316         histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
0317         histoPlot->replot();
0318     });
0319 
0320     connect(stretchButton, &QPushButton::clicked, this, [ = ]()
0321     {
0322         // This will toggle whether we're currently stretching.
0323         m_View->setStretch(!m_View->isImageStretched());
0324     });
0325 
0326     connect(autoButton, &QPushButton::clicked, this, [ = ]()
0327     {
0328         // If we're not currently using automatic stretch parameters, turn that on.
0329         // If we're already using automatic parameters, don't do anything.
0330         // User can just move the sliders to take manual control.
0331         if (!m_View->getAutoStretch())
0332             m_View->setAutoStretchParams();
0333         else
0334             KMessageBox::information(this, "You are already using automatic stretching. To manually stretch, drag a slider.");
0335         setStretchUIValues(m_View->getStretchParams().grey_red);
0336     });
0337 
0338     connect(toggleHistoButton, &QPushButton::clicked, this, [ = ]()
0339     {
0340         histoPlot->setVisible(!histoPlot->isVisible());
0341     });
0342 
0343     // This is mostly useful right at the start, when the image is displayed without any user interaction.
0344     // Check for slider-in-use, as we don't wont to rescale while the user is active.
0345     connect(m_View.get(), &FITSView::newStatus, this, [ = ](const QString & unused)
0346     {
0347         Q_UNUSED(unused);
0348         setStretchUIValues(m_View->getStretchParams().grey_red);
0349     });
0350 
0351     connect(m_View.get(), &FITSView::newStretch, this, [ = ](const StretchParams & params)
0352     {
0353         histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
0354         histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
0355         histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
0356     });
0357 }
0358 
0359 
0360 namespace
0361 {
0362 // Converts from the position of the min or max slider position (on a 0 to 1.0 scale) to an
0363 // x-axis position on the histogram plot, which varies from 0 to the number of bins in the histogram.
0364 double toHistogramPosition(double position, const QSharedPointer<FITSData> &data)
0365 {
0366     if (!data->isHistogramConstructed())
0367         return 0;
0368     const double size = data->getHistogramBinCount();
0369     return position * size;
0370 }
0371 }
0372 
0373 // Adds a vertical line on the histogram plot (the cursor for the min or max slider).
0374 QCPItemLine * FITSStretchUI::setCursor(int position, const QPen &pen)
0375 {
0376     QCPItemLine *line = new QCPItemLine(histoPlot);
0377     line->setPen(pen);
0378     const double top = histoPlot->yAxis->range().upper;
0379     const double bottom = histoPlot->yAxis->range().lower;
0380     line->start->setCoords(position + .5, bottom);
0381     line->end->setCoords(position + .5, top);
0382     return line;
0383 }
0384 
0385 void FITSStretchUI::setCursors(const StretchParams &params)
0386 {
0387     const QPen pen(Qt::white, 1, Qt::DotLine);
0388     removeCursors();
0389     auto data = m_View->imageData();
0390     minCursor = setCursor(toHistogramPosition(params.grey_red.shadows, data), pen);
0391     maxCursor = setCursor(toHistogramPosition(params.grey_red.highlights, data), pen);
0392 }
0393 
0394 void FITSStretchUI::removeCursors()
0395 {
0396     if (minCursor != nullptr)
0397         histoPlot->removeItem(minCursor);
0398     minCursor = nullptr;
0399 
0400     if (maxCursor != nullptr)
0401         histoPlot->removeItem(maxCursor);
0402     maxCursor = nullptr;
0403 }
0404 
0405 void FITSStretchUI::generateHistogram()
0406 {
0407     if (!m_View->imageData()->isHistogramConstructed())
0408         m_View->imageData()->constructHistogram();
0409     if (m_View->imageData()->isHistogramConstructed())
0410     {
0411         histoPlot->clearGraphs();
0412         const int nChannels = m_View->imageData()->channels();
0413         histoPlot->clearGraphs();
0414         histoPlot->clearItems();
0415         for (int i = 0; i < nChannels; ++i)
0416         {
0417             histoPlot->addGraph(histoPlot->xAxis, histoPlot->yAxis);
0418             auto graph = histoPlot->graph(i);
0419             graph->setLineStyle(QCPGraph::lsStepLeft);
0420             graph->setVisible(true);
0421             QColor color = Qt::lightGray;
0422             if (nChannels > 1)
0423                 color = i == 0 ? QColor(255, 0, 0) : ((i == 1) ? QColor(0, 255, 0, 225) : QColor(0, 0, 255, 175));
0424             graph->setBrush(QBrush(color));
0425             graph->setPen(QPen(color));
0426             const QVector<double> &h = m_View->imageData()->getHistogramFrequency(i);
0427             const int size = m_View->imageData()->getHistogramBinCount();
0428             for (int j = 0; j < size; ++j)
0429                 graph->addData(j, log1p(h[j]));
0430         }
0431         histoPlot->rescaleAxes();
0432         histoPlot->xAxis->setRange(0, m_View->imageData()->getHistogramBinCount() + 1);
0433     }
0434 
0435     histoPlot->setInteractions(QCP::iRangeZoom | QCP::iRangeDrag);
0436     histoPlot->axisRect()->setRangeZoomAxes(histoPlot->xAxis, 0);
0437     histoPlot->axisRect()->setRangeDragAxes(histoPlot->xAxis, 0);
0438     histoPlot->xAxis->setTickLabels(false);
0439     histoPlot->yAxis->setTickLabels(false);
0440 
0441     // This controls the x-axis zoom in/out on the histogram plot.
0442     // It doesn't allow the x-axis to go less than 0, or more than the number of histogram bins.
0443     connect(histoPlot->xAxis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this,
0444             [ = ](const QCPRange & newRange)
0445     {
0446         if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
0447         const double histogramSize = m_View->imageData()->getHistogramBinCount();
0448         double tLower = newRange.lower;
0449         double tUpper = newRange.upper;
0450         if (tLower < 0) tLower = 0;
0451         if (tUpper > histogramSize + 1) tUpper = histogramSize + 1;
0452         if (tLower != newRange.lower || tUpper != newRange.upper)
0453             histoPlot->xAxis->setRange(tLower, tUpper);
0454     });
0455 }
0456 
0457 void FITSStretchUI::setStretchValues(double shadows, double midtones, double highlights)
0458 {
0459     StretchParams params = m_View->getStretchParams();
0460     params.grey_red.shadows = shadows;
0461     params.grey_red.midtones = midtones;
0462     params.grey_red.highlights = highlights;
0463     setCursors(params);
0464     m_View->setPreviewSampling(0);
0465     m_View->setStretchParams(params);
0466     histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
0467     histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
0468     histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
0469     histoPlot->replot();
0470 }