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 ¶ms) 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 ¶ms) 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 }