Kstars

fitsstretchui.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Hy Murveit <hy@murveit.com>
3 SPDX-License-Identifier: GPL-2.0-or-later
4*/
5
6#include "fitsstretchui.h"
7#include "fitsview.h"
8#include "fitsdata.h"
9#include "Options.h"
10
11#include <KMessageBox>
12
13namespace
14{
15
16const char kAutoToolTip[] = "Automatically find stretch parameters";
17const char kStretchOffToolTip[] = "Stretch the image";
18const char kStretchOnToolTip[] = "Disable stretching of the image.";
19
20// The midtones slider works logarithmically (otherwise the useful range of the slider would
21// be only way on the left. So these functions translate from a linear slider value
22// logarithmically in the 0-1 range, assuming the slider varies from 1 to 10000.
23constexpr double HISTO_SLIDER_MAX = 10000.0;
24constexpr double HISTO_SLIDER_FACTOR = 5.0;
25double midValueFcn(int x)
26{
27 return pow(10, -(HISTO_SLIDER_FACTOR - (x / (HISTO_SLIDER_MAX / HISTO_SLIDER_FACTOR))));
28}
29int invertMidValueFcn(double x)
30{
31 return (int) 0.5 + (HISTO_SLIDER_MAX / HISTO_SLIDER_FACTOR) * (HISTO_SLIDER_FACTOR + log10(x));
32}
33
34// These are defaults for the histogram plot's QCustomPlot axes.
35void setupAxisDefaults(QCPAxis *axis)
36{
37 axis->setBasePen(QPen(Qt::white, 1));
38 axis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
39 axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
41 axis->setBasePen(QPen(Qt::white, 1));
42 axis->setTickPen(QPen(Qt::white, 1));
43 axis->setSubTickPen(QPen(Qt::white, 1));
46 axis->grid()->setVisible(true);
47}
48}
49
50FITSStretchUI::FITSStretchUI(const QSharedPointer<FITSView> &view, QWidget * parent) : QWidget(parent)
51{
52 setupUi(this);
53 m_View = view;
54 setupButtons();
55 setupHistoPlot();
56 setupHistoSlider();
57 setupConnections();
58}
59
60void FITSStretchUI::setupButtons()
61{
62 stretchButton->setIcon(QIcon::fromTheme("transform-move"));
63 toggleHistoButton->setIcon(QIcon::fromTheme("histogram-symbolic"));
64 autoButton->setIcon(QIcon::fromTheme("tools-wizard"));
65}
66
67void FITSStretchUI::setupHistoPlot()
68{
69 histoPlot->setBackground(QBrush(QColor(25, 25, 25)));
70 setupAxisDefaults(histoPlot->yAxis);
71 setupAxisDefaults(histoPlot->xAxis);
72 histoPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen);
73 histoPlot->setMaximumHeight(75);
74 histoPlot->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
75 histoPlot->setVisible(false);
76
77 connect(histoPlot, &QCustomPlot::mouseDoubleClick, this, &FITSStretchUI::onHistoDoubleClick);
78 connect(histoPlot, &QCustomPlot::mouseMove, this, &FITSStretchUI::onHistoMouseMove);
79}
80
81void FITSStretchUI::onHistoDoubleClick(QMouseEvent *event)
82{
83 Q_UNUSED(event);
84 if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
85 const double histogramSize = m_View->imageData()->getHistogramBinCount();
86 histoPlot->xAxis->setRange(0, histogramSize + 1);
87 histoPlot->replot();
88}
89
90// This creates 1-channel or RGB tooltips on this histogram.
91void FITSStretchUI::onHistoMouseMove(QMouseEvent *event)
92{
93 const auto image = m_View->imageData();
94 if (!image->isHistogramConstructed())
95 return;
96
97 const bool rgbHistogram = (image->channels() > 1);
98 const int numPixels = image->width() * image->height();
99 const int histogramSize = image->getHistogramBinCount();
100 const int histoBin = std::max(0, std::min(histogramSize - 1,
101 static_cast<int>(histoPlot->xAxis->pixelToCoord(event->x()))));
102
103 QString tip = "";
104 if (histoBin >= 0 && histoBin < histogramSize)
105 {
106 for (int c = 0; c < image->channels(); ++c)
107 {
108 const QVector<double> &intervals = image->getHistogramIntensity(c);
109 const double lowRange = intervals[histoBin];
110 const double highRange = lowRange + image->getHistogramBinWidth(c);
111
112 if (rgbHistogram)
113 tip.append(QString("<font color=\"%1\">").arg(c == 0 ? "red" : (c == 1) ? "lightgreen" : "lightblue"));
114
115 if (image->getMax(c) > 1.1)
116 tip.append(QString("%1 %2 %3: ").arg(lowRange, 0, 'f', 0).arg(QChar(0x2192)).arg(highRange, 0, 'f', 0));
117 else
118 tip.append(QString("%1 %2 %3: ").arg(lowRange, 0, 'f', 4).arg(QChar(0x2192)).arg(highRange, 0, 'f', 4));
119
120 const int count = image->getHistogramFrequency(c)[histoBin];
121 const double percentage = count * 100.0 / (double) numPixels;
122 tip.append(QString("%1 %2%").arg(count).arg(percentage, 0, 'f', 2));
123 if (rgbHistogram)
124 tip.append("</font><br/>");
125 }
126 }
127 if (tip.size() > 0)
128 QToolTip::showText(event->globalPos(), tip, nullptr, QRect(), 10000);
129}
130
131void FITSStretchUI::setupHistoSlider()
132{
133 histoSlider->setOrientation(Qt::Horizontal);
134 histoSlider->setMinimum(0);
135 histoSlider->setMaximum(HISTO_SLIDER_MAX);
136 histoSlider->setMinimumPosition(0);
137 histoSlider->setMaximumPosition(HISTO_SLIDER_MAX);
138 histoSlider->setMidPosition(HISTO_SLIDER_MAX / 2);
139 histoSlider->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
140
141 connect(histoSlider, &ctk3Slider::minimumPositionChanged, this, [ = ](int value)
142 {
143 StretchParams params = m_View->getStretchParams();
144 const double shadowValue = value / HISTO_SLIDER_MAX;
145 if (shadowValue != params.grey_red.shadows)
146 {
147 params.grey_red.shadows = shadowValue;
148
149 // In this and below callbacks, we allow for "stretchPreviewSampling". That is,
150 // it downsamples the image when the slider is moving, to allow slower computers
151 // to render faster, so that the slider can adjust the image in real time.
152 // When the mouse is released, a final render at full resolution will be done.
153 m_View->setPreviewSampling(Options::stretchPreviewSampling());
154 m_View->setStretchParams(params);
155 m_View->setPreviewSampling(0);
156
157 // The min and max sliders draw cursors, corresponding to their positions, on
158 // this histogram plot.
159 setCursors(params);
160 histoPlot->replot();
161 }
162 });
163 connect(histoSlider, &ctk3Slider::maximumPositionChanged, this, [ = ](int value)
164 {
165 StretchParams params = m_View->getStretchParams();
166 const double highValue = value / HISTO_SLIDER_MAX;
167 if (highValue != params.grey_red.highlights)
168 {
169 params.grey_red.highlights = highValue;
170 m_View->setPreviewSampling(Options::stretchPreviewSampling());
171 m_View->setStretchParams(params);
172 m_View->setPreviewSampling(0);
173 setCursors(params);
174 histoPlot->replot();
175 }
176 });
177 connect(histoSlider, &ctk3Slider::midPositionChanged, this, [ = ](int value)
178 {
179 StretchParams params = m_View->getStretchParams();
180 const double midValue = midValueFcn(value);
181 if (midValue != params.grey_red.midtones)
182 {
183 params.grey_red.midtones = midValue;
184 m_View->setPreviewSampling(Options::stretchPreviewSampling());
185 m_View->setStretchParams(params);
186 m_View->setPreviewSampling(0);
187 }
188 });
189
190 // We need this released callback since if Options::stretchPreviewSampling() is > 1,
191 // then when the sliders are dragged, the stretched image is rendered in lower resolution.
192 // However when the dragging is done (and the mouse is released) we want to end by rendering
193 // in full resolution.
194 connect(histoSlider, &ctk3Slider::released, this, [ = ](int minValue, int midValue, int maxValue)
195 {
196 StretchParams params = m_View->getStretchParams();
197 const double shadowValue = minValue / HISTO_SLIDER_MAX;
198 const double middleValue = midValueFcn(midValue);
199 const double highValue = maxValue / HISTO_SLIDER_MAX;
200
201 if (middleValue != params.grey_red.midtones ||
202 highValue != params.grey_red.highlights ||
203 shadowValue != params.grey_red.shadows)
204 {
205 params.grey_red.shadows = shadowValue;
206 params.grey_red.midtones = middleValue;
207 params.grey_red.highlights = highValue;
208 m_View->setPreviewSampling(0);
209 m_View->setStretchParams(params);
210 }
211 });
212}
213
214// Updates all the widgets in the stretch area to display the view's stretch parameters.
215void FITSStretchUI::setStretchUIValues(const StretchParams1Channel &params)
216{
217 shadowsVal->setValue(params.shadows);
218 midtonesVal->setValue(params.midtones);
219 highlightsVal->setValue(params.highlights);
220
221 bool stretchActive = m_View->isImageStretched();
222 if (stretchActive)
223 {
224 stretchButton->setChecked(true);
225 stretchButton->setToolTip(kStretchOnToolTip);
226 }
227 else
228 {
229 stretchButton->setChecked(false);
230 stretchButton->setToolTip(kStretchOffToolTip);
231 }
232
233 // Only activate the auto button if stretching is on and auto-stretching is not set.
234 if (stretchActive && !m_View->getAutoStretch())
235 {
236 autoButton->setEnabled(true);
237 autoButton->setIcon(QIcon::fromTheme("tools-wizard"));
238 autoButton->setIconSize(QSize(22, 22));
239 autoButton->setToolTip(kAutoToolTip);
240 }
241 else
242 {
243 autoButton->setEnabled(false);
244 autoButton->setIcon(QIcon());
245 autoButton->setIconSize(QSize(22, 22));
246 autoButton->setToolTip("");
247 }
248 autoButton->setChecked(m_View->getAutoStretch());
249
250 // Disable most of the UI if stretching is not active.
251 shadowsVal->setEnabled(stretchActive);
252 shadowsLabel->setEnabled(stretchActive);
253 midtonesVal->setEnabled(stretchActive);
254 midtonesLabel->setEnabled(stretchActive);
255 highlightsVal->setEnabled(stretchActive);
256 highlightsLabel->setEnabled(stretchActive);
257 histoSlider->setEnabled(stretchActive);
258}
259
260void FITSStretchUI::setupConnections()
261{
262 connect(m_View.get(), &FITSView::mouseOverPixel, this, [ this ](int x, int y)
263 {
264 if (pixelCursors.size() != m_View->imageData()->channels())
265 pixelCursors.fill(nullptr, m_View->imageData()->channels());
266
267 if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
268 auto image = m_View->imageData();
269 const int nChannels = m_View->imageData()->channels();
270 for (int c = 0; c < nChannels; ++c)
271 {
272 if (pixelCursors[c] != nullptr)
273 {
274 histoPlot->removeItem(pixelCursors[c]);
275 pixelCursors[c] = nullptr;
276 }
277 if (x < 0 || y < 0 || x >= m_View->imageData()->width() ||
278 y >= m_View->imageData()->height())
279 continue;
280 int32_t bin = image->histogramBin(x, y, c);
281 QColor color = Qt::darkGray;
282 if (nChannels > 1)
283 color = c == 0 ? QColor(255, 10, 65) : ((c == 1) ? QColor(144, 238, 144, 225) : QColor(173, 216, 230, 175));
284
285 pixelCursors[c] = setCursor(bin, QPen(color, 2, Qt::SolidLine));
286 }
287 histoPlot->replot();
288 });
289
290 connect(highlightsVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
291 {
292 StretchParams params = m_View->getStretchParams();
293 params.grey_red.highlights = highlightsVal->value();
294 setCursors(params);
295 m_View->setStretchParams(params);
296 histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
297 histoPlot->replot();
298 });
299
300 connect(midtonesVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
301 {
302 StretchParams params = m_View->getStretchParams();
303 params.grey_red.midtones = midtonesVal->value();
304 setCursors(params);
305 m_View->setStretchParams(params);
306 histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
307 histoPlot->replot();
308 });
309
310 connect(shadowsVal, &QDoubleSpinBox::editingFinished, this, [ this ]()
311 {
312 StretchParams params = m_View->getStretchParams();
313 params.grey_red.shadows = shadowsVal->value();
314 setCursors(params);
315 m_View->setStretchParams(params);
316 histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
317 histoPlot->replot();
318 });
319
320 connect(stretchButton, &QPushButton::clicked, this, [ = ]()
321 {
322 // This will toggle whether we're currently stretching.
323 m_View->setStretch(!m_View->isImageStretched());
324 });
325
326 connect(autoButton, &QPushButton::clicked, this, [ = ]()
327 {
328 // If we're not currently using automatic stretch parameters, turn that on.
329 // If we're already using automatic parameters, don't do anything.
330 // User can just move the sliders to take manual control.
331 if (!m_View->getAutoStretch())
332 m_View->setAutoStretchParams();
333 else
334 KMessageBox::information(this, "You are already using automatic stretching. To manually stretch, drag a slider.");
335 setStretchUIValues(m_View->getStretchParams().grey_red);
336 });
337
338 connect(toggleHistoButton, &QPushButton::clicked, this, [ = ]()
339 {
340 histoPlot->setVisible(!histoPlot->isVisible());
341 });
342
343 // This is mostly useful right at the start, when the image is displayed without any user interaction.
344 // Check for slider-in-use, as we don't wont to rescale while the user is active.
345 connect(m_View.get(), &FITSView::newStatus, this, [ = ](const QString & unused)
346 {
347 Q_UNUSED(unused);
348 setStretchUIValues(m_View->getStretchParams().grey_red);
349 });
350
351 connect(m_View.get(), &FITSView::newStretch, this, [ = ](const StretchParams & params)
352 {
353 histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
354 histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
355 histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
356 });
357}
358
359
360namespace
361{
362// Converts from the position of the min or max slider position (on a 0 to 1.0 scale) to an
363// x-axis position on the histogram plot, which varies from 0 to the number of bins in the histogram.
364double toHistogramPosition(double position, const QSharedPointer<FITSData> &data)
365{
366 if (!data->isHistogramConstructed())
367 return 0;
368 const double size = data->getHistogramBinCount();
369 return position * size;
370}
371}
372
373// Adds a vertical line on the histogram plot (the cursor for the min or max slider).
374QCPItemLine * FITSStretchUI::setCursor(int position, const QPen &pen)
375{
376 QCPItemLine *line = new QCPItemLine(histoPlot);
377 line->setPen(pen);
378 const double top = histoPlot->yAxis->range().upper;
379 const double bottom = histoPlot->yAxis->range().lower;
380 line->start->setCoords(position + .5, bottom);
381 line->end->setCoords(position + .5, top);
382 return line;
383}
384
385void FITSStretchUI::setCursors(const StretchParams &params)
386{
387 const QPen pen(Qt::white, 1, Qt::DotLine);
388 removeCursors();
389 auto data = m_View->imageData();
390 minCursor = setCursor(toHistogramPosition(params.grey_red.shadows, data), pen);
391 maxCursor = setCursor(toHistogramPosition(params.grey_red.highlights, data), pen);
392}
393
394void FITSStretchUI::removeCursors()
395{
396 if (minCursor != nullptr)
397 histoPlot->removeItem(minCursor);
398 minCursor = nullptr;
399
400 if (maxCursor != nullptr)
401 histoPlot->removeItem(maxCursor);
402 maxCursor = nullptr;
403}
404
405void FITSStretchUI::generateHistogram()
406{
407 if (!m_View->imageData()->isHistogramConstructed())
408 m_View->imageData()->constructHistogram();
409 if (m_View->imageData()->isHistogramConstructed())
410 {
411 histoPlot->clearGraphs();
412 const int nChannels = m_View->imageData()->channels();
413 histoPlot->clearGraphs();
414 histoPlot->clearItems();
415 for (int i = 0; i < nChannels; ++i)
416 {
417 histoPlot->addGraph(histoPlot->xAxis, histoPlot->yAxis);
418 auto graph = histoPlot->graph(i);
419 graph->setLineStyle(QCPGraph::lsStepLeft);
420 graph->setVisible(true);
421 QColor color = Qt::lightGray;
422 if (nChannels > 1)
423 color = i == 0 ? QColor(255, 0, 0) : ((i == 1) ? QColor(0, 255, 0, 225) : QColor(0, 0, 255, 175));
424 graph->setBrush(QBrush(color));
425 graph->setPen(QPen(color));
426 const QVector<double> &h = m_View->imageData()->getHistogramFrequency(i);
427 const int size = m_View->imageData()->getHistogramBinCount();
428 for (int j = 0; j < size; ++j)
429 graph->addData(j, log1p(h[j]));
430 }
431 histoPlot->rescaleAxes();
432 histoPlot->xAxis->setRange(0, m_View->imageData()->getHistogramBinCount() + 1);
433 }
434
435 histoPlot->setInteractions(QCP::iRangeZoom | QCP::iRangeDrag);
436 histoPlot->axisRect()->setRangeZoomAxes(histoPlot->xAxis, 0);
437 histoPlot->axisRect()->setRangeDragAxes(histoPlot->xAxis, 0);
438 histoPlot->xAxis->setTickLabels(false);
439 histoPlot->yAxis->setTickLabels(false);
440
441 // This controls the x-axis zoom in/out on the histogram plot.
442 // It doesn't allow the x-axis to go less than 0, or more than the number of histogram bins.
443 connect(histoPlot->xAxis, QOverload<const QCPRange &>::of(&QCPAxis::rangeChanged), this,
444 [ = ](const QCPRange & newRange)
445 {
446 if (!m_View || !m_View->imageData() || !m_View->imageData()->isHistogramConstructed()) return;
447 const double histogramSize = m_View->imageData()->getHistogramBinCount();
448 double tLower = newRange.lower;
449 double tUpper = newRange.upper;
450 if (tLower < 0) tLower = 0;
451 if (tUpper > histogramSize + 1) tUpper = histogramSize + 1;
452 if (tLower != newRange.lower || tUpper != newRange.upper)
453 histoPlot->xAxis->setRange(tLower, tUpper);
454 });
455}
456
457void FITSStretchUI::setStretchValues(double shadows, double midtones, double highlights)
458{
459 StretchParams params = m_View->getStretchParams();
460 params.grey_red.shadows = shadows;
461 params.grey_red.midtones = midtones;
462 params.grey_red.highlights = highlights;
463 setCursors(params);
464 m_View->setPreviewSampling(0);
465 m_View->setStretchParams(params);
466 histoSlider->setMinimumValue(params.grey_red.shadows * HISTO_SLIDER_MAX);
467 histoSlider->setMidValue(invertMidValueFcn(params.grey_red.midtones));
468 histoSlider->setMaximumValue(params.grey_red.highlights * HISTO_SLIDER_MAX);
469 histoPlot->replot();
470}
Manages a single axis inside a QCustomPlot.
void rangeChanged(const QCPRange &newRange)
void setTickLabelColor(const QColor &color)
QCPGrid * grid() const
void setLabelColor(const QColor &color)
void setBasePen(const QPen &pen)
void setTickPen(const QPen &pen)
void setSubTickPen(const QPen &pen)
@ lsStepLeft
line is drawn as steps where the step height is the value of the left data point
void setZeroLinePen(const QPen &pen)
void setSubGridPen(const QPen &pen)
void setPen(const QPen &pen)
A line from one point to another.
void setPen(const QPen &pen)
void setCoords(double key, double value)
void setVisible(bool on)
Represents the range an axis is encompassing.
void mouseMove(QMouseEvent *event)
void mouseDoubleClick(QMouseEvent *event)
void maximumPositionChanged(int max)
This signal is emitted when sliderDown is true and the slider moves.
void minimumPositionChanged(int min)
This signal is emitted when sliderDown is true and the slider moves.
void midPositionChanged(int max)
This signal is emitted when sliderDown is true and the slider moves.
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
@ iRangeDrag
0x001 Axis ranges are draggable (see QCPAxisRect::setRangeDrag, QCPAxisRect::setRangeDragAxes)
@ iRangeZoom
0x002 Axis ranges are zoomable with the mouse wheel (see QCPAxisRect::setRangeZoom,...
void clicked(bool checked)
void editingFinished()
QIcon fromTheme(const QString &name)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T * get() const const
QString & append(QChar ch)
qsizetype size() const const
Horizontal
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
virtual bool event(QEvent *event) override
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:38:43 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.