diff --git a/kstars/fitsviewer/fitstab.cpp b/kstars/fitsviewer/fitstab.cpp index 78a1bf9b8..3bcd1e8de 100644 --- a/kstars/fitsviewer/fitstab.cpp +++ b/kstars/fitsviewer/fitstab.cpp @@ -1,487 +1,725 @@ /*************************************************************************** FITS Tab ------------------- copyright : (C) 2012 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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 "fitstab.h" #include "fitsdata.h" #include "fitshistogram.h" #include "fitsview.h" #include "fitsviewer.h" #include "ksnotification.h" #include "kstars.h" #include "Options.h" #include "ui_fitsheaderdialog.h" #include "ui_statform.h" #include #include +#include + + +namespace { +const char kAutoToolTip[] = "Automatically find stretch parameters"; +const char kStretchOffToolTip[] = "Stretch the image"; +const char kStretchOnToolTip[] = "Disable stretching of the image."; +} // namespace FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent) { viewer = parent; undoStack = new QUndoStack(this); undoStack->setUndoLimit(10); undoStack->clear(); connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool))); statWidget = new QDialog(this); fitsHeaderDialog = new QDialog(this); histogram = new FITSHistogram(this); } FITSTab::~FITSTab() { // Make sure it's done //histogramFuture.waitForFinished(); //disconnect(); } void FITSTab::saveUnsaved() { if (undoStack->isClean() || view->getMode() != FITS_NORMAL) return; QString caption = i18n("Save Changes to FITS?"); QString message = i18n("The current FITS file has unsaved changes. Would you like to save before closing it?"); int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard()); if (ans == KMessageBox::Yes) saveFile(); if (ans == KMessageBox::No) { undoStack->clear(); modifyFITSState(); } } void FITSTab::closeEvent(QCloseEvent *ev) { saveUnsaved(); if (undoStack->isClean()) ev->accept(); else ev->ignore(); } QString FITSTab::getPreviewText() const { return previewText; } void FITSTab::setPreviewText(const QString &value) { previewText = value; } void FITSTab::selectRecentFITS(int i) { loadFITS(QUrl::fromLocalFile(recentImages->item(i)->text())); } void FITSTab::clearRecentFITS() { disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); recentImages->clear(); connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); } +namespace { + +// Sets the text value in the slider's value display, and if adjustSlider is true, +// moves the slider to the correct position. +void setSlider(QSlider *slider, QLabel *label, float value, float maxValue, bool adjustSlider) +{ + if (adjustSlider) + slider->setValue(static_cast(value * 10000 / maxValue)); + QString valStr = QString("%1").arg(static_cast(value), 5, 'f', 4); + label->setText(valStr); +} + +// Adds the following to a horizontal layout (left to right): a vertical line, +// a label with the slider's name, a slider, and a text field to display the slider's value. +void setupStretchSlider(QSlider *slider, QLabel *label, QLabel *val, int fontSize, + const QString& name, QHBoxLayout *layout) +{ + QFrame* line = new QFrame(); + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + layout->addWidget(line); + QFont font = label->font(); + font.setPointSize(fontSize); + + label->setText(name); + label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + label->setFont(font); + layout->addWidget(label); + slider->setMinimum(0); + slider->setMaximum(10000); + slider->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + layout->addWidget(slider); + val->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + val->setFont(font); + layout->addWidget(val); +} + +// Adds a button with the icon and tooltip to the layout. +void setupStretchButton(QPushButton *button, const QString &iconName, const QString &tip, QHBoxLayout *layout) +{ + button->setIcon(QIcon::fromTheme(iconName)); + button->setIconSize(QSize(22, 22)); + button->setToolTip(tip); + button->setCheckable(true); + button->setChecked(true); + button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + layout->addWidget(button); +} + +} // namespace + +// Updates all the widgets in the stretch area to display the view's stretch parameters. +void FITSTab::setStretchUIValues(bool adjustSliders) +{ + StretchParams1Channel params = view->getStretchParams().grey_red; + setSlider(shadowsSlider.get(), shadowsVal.get(), params.shadows, maxShadows, adjustSliders); + setSlider(midtonesSlider.get(), midtonesVal.get(), params.midtones, maxMidtones, adjustSliders); + setSlider(highlightsSlider.get(), highlightsVal.get(), params.highlights, maxHighlights, adjustSliders); + + + bool stretchActive = view->isImageStretched(); + if (stretchActive) + { + stretchButton->setChecked(true); + stretchButton->setToolTip(kStretchOnToolTip); + } + else + { + stretchButton->setChecked(false); + stretchButton->setToolTip(kStretchOffToolTip); + } + + // Only activeate the auto button if stretching is on and auto-stretching is not set. + if (stretchActive && !view->getAutoStretch()) + { + autoButton->setEnabled(true); + autoButton->setIcon(QIcon::fromTheme("tools-wizard")); + autoButton->setIconSize(QSize(22, 22)); + autoButton->setToolTip(kAutoToolTip); + } + else + { + autoButton->setEnabled(false); + autoButton->setIcon(QIcon()); + autoButton->setIconSize(QSize(22, 22)); + autoButton->setToolTip(""); + } + autoButton->setChecked(view->getAutoStretch()); + + // Disable most of the UI if stretching is not active. + shadowsSlider->setEnabled(stretchActive); + shadowsVal->setEnabled(stretchActive); + shadowsLabel->setEnabled(stretchActive); + midtonesSlider->setEnabled(stretchActive); + midtonesVal->setEnabled(stretchActive); + midtonesLabel->setEnabled(stretchActive); + highlightsSlider->setEnabled(stretchActive); + highlightsVal->setEnabled(stretchActive); + highlightsLabel->setEnabled(stretchActive); +} + +// Adjusts the maxShadows value so that we have room to adjust the slider. +void FITSTab::rescaleShadows() +{ + if (!view) return; + StretchParams1Channel params = view->getStretchParams().grey_red; + maxShadows = std::max(0.002f, std::min(1.0f, params.shadows * 2.0f)); + setStretchUIValues(true); +} + +// Adjusts the maxMidtones value so that we have room to adjust the slider. +void FITSTab::rescaleMidtones() +{ + if (!view) return; + StretchParams1Channel params = view->getStretchParams().grey_red; + maxMidtones = std::max(.002f, std::min(1.0f, params.midtones * 2.0f)); + setStretchUIValues(true); +} + +QHBoxLayout* FITSTab::setupStretchBar() +{ + constexpr int fontSize = 12; + + QHBoxLayout *stretchBarLayout = new QHBoxLayout(); + + stretchButton.reset(new QPushButton()); + setupStretchButton(stretchButton.get(), "transform-move", kStretchOffToolTip, stretchBarLayout); + + // Shadows + shadowsLabel.reset(new QLabel()); + shadowsVal.reset(new QLabel()); + shadowsSlider.reset(new QSlider(Qt::Horizontal, this)); + setupStretchSlider(shadowsSlider.get(), shadowsLabel.get(), shadowsVal.get(), fontSize, "Shadows", stretchBarLayout); + + // Midtones + midtonesLabel.reset(new QLabel()); + midtonesVal.reset(new QLabel()); + midtonesSlider.reset(new QSlider(Qt::Horizontal, this)); + setupStretchSlider(midtonesSlider.get(), midtonesLabel.get(), midtonesVal.get(), fontSize, "Midtones", stretchBarLayout); + + // Highlights + highlightsLabel.reset(new QLabel()); + highlightsVal.reset(new QLabel()); + highlightsSlider.reset(new QSlider(Qt::Horizontal, this)); + setupStretchSlider(highlightsSlider.get(), highlightsLabel.get(), highlightsVal.get(), fontSize, "Hightlights", stretchBarLayout); + + // Separator + QFrame* line4 = new QFrame(); + line4->setFrameShape(QFrame::VLine); + line4->setFrameShadow(QFrame::Sunken); + stretchBarLayout->addWidget(line4); + + autoButton.reset(new QPushButton()); + setupStretchButton(autoButton.get(), "tools-wizard", kAutoToolTip, stretchBarLayout); + + connect(stretchButton.get(), &QPushButton::clicked, [=]() { + // This will toggle whether we're currently stretching. + view->setStretch(!view->isImageStretched()); + }); + + // Make rough displays for the slider movement. + connect(shadowsSlider.get(), &QSlider::sliderMoved, [=](int value) { + StretchParams params = view->getStretchParams(); + params.grey_red.shadows = this->maxShadows * value / 10000.0f; + view->setSampling(4); + view->setStretchParams(params); + view->setSampling(1); + }); + connect(midtonesSlider.get(), &QSlider::sliderMoved, [=](int value) { + StretchParams params = view->getStretchParams(); + params.grey_red.midtones = this->maxMidtones * value / 10000.0f; + view->setSampling(4); + view->setStretchParams(params); + view->setSampling(1); + }); + connect(highlightsSlider.get(), &QSlider::sliderMoved, [=](int value) { + StretchParams params = view->getStretchParams(); + params.grey_red.highlights = this->maxHighlights * value / 10000.0f; + view->setSampling(4); + view->setStretchParams(params); + view->setSampling(1); + }); + + // Make a final full-res display when the slider is released. + connect(shadowsSlider.get(), &QSlider::sliderReleased, [=]() { + if (!view) return; + rescaleShadows(); + StretchParams params = view->getStretchParams(); + view->setStretchParams(params); + }); + connect(midtonesSlider.get(), &QSlider::sliderReleased, [=]() { + if (!view) return; + rescaleMidtones(); + StretchParams params = view->getStretchParams(); + view->setStretchParams(params); + }); + connect(highlightsSlider.get(), &QSlider::sliderReleased, [=]() { + if (!view) return; + StretchParams params = view->getStretchParams(); + view->setStretchParams(params); + }); + + connect(autoButton.get(), &QPushButton::clicked, [=]() { + // If we're not currently using automatic stretch parameters, turn that on. + // If we're already using automatic parameters, don't do anything. + // User can just move the sliders to take manual control. + if (!view->getAutoStretch()) + view->setAutoStretchParams(); + else + KMessageBox::information(this, "You are already using automatic stretching. To manually stretch, drag a slider."); + setStretchUIValues(false); + }); + + // This is mostly useful right at the start, when the image is displayed without any user interaction. + // Check for slider-in-use, as we don't wont to rescale while the user is active. + connect(view.get(), &FITSView::newStatus, [=](const QString &ignored) { + Q_UNUSED(ignored) + bool slidersInUse = shadowsSlider->isSliderDown() || midtonesSlider->isSliderDown() || + highlightsSlider->isSliderDown(); + if (!slidersInUse) + { + rescaleShadows(); + rescaleMidtones(); + } + setStretchUIValues(!slidersInUse); + }); + + return stretchBarLayout; +} + bool FITSTab::setupView(FITSMode mode, FITSScale filter) { if (view.get() == nullptr) { view.reset(new FITSView(this, mode, filter)); view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QVBoxLayout *vlayout = new QVBoxLayout(); fitsSplitter = new QSplitter(Qt::Horizontal, this); fitsTools = new QToolBox(); stat.setupUi(statWidget); for (int i = 0; i <= STAT_STDDEV; i++) { for (int j = 0; j < 3; j++) { stat.statsTable->setItem(i, j, new QTableWidgetItem()); stat.statsTable->item(i, j)->setTextAlignment(Qt::AlignHCenter); } // Set col span for items up to HFR if (i <= STAT_HFR) stat.statsTable->setSpan(i, 0, 1, 3); } fitsTools->addItem(statWidget, i18n("Statistics")); fitsTools->addItem(histogram, i18n("Histogram")); header.setupUi(fitsHeaderDialog); fitsTools->addItem(fitsHeaderDialog, i18n("FITS Header")); QVBoxLayout *recentPanelLayout = new QVBoxLayout(); QWidget *recentPanel = new QWidget(fitsSplitter); recentPanel->setLayout(recentPanelLayout); fitsTools->addItem(recentPanel, i18n("Recent Images")); recentImages = new QListWidget(recentPanel); recentPanelLayout->addWidget(recentImages); QPushButton *clearRecent = new QPushButton(i18n("Clear")); recentPanelLayout->addWidget(clearRecent); connect(clearRecent, &QPushButton::pressed, this, &FITSTab::clearRecentFITS); connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); QScrollArea *scrollFitsPanel = new QScrollArea(fitsSplitter); scrollFitsPanel->setWidgetResizable(true); scrollFitsPanel->setWidget(fitsTools); fitsSplitter->addWidget(scrollFitsPanel); fitsSplitter->addWidget(view.get()); //This code allows the fitsTools to start in a closed state fitsSplitter->setSizes(QList() << 0 << view->width() ); vlayout->addWidget(fitsSplitter); + vlayout->addLayout(setupStretchBar()); connect(fitsSplitter, &QSplitter::splitterMoved, histogram, &FITSHistogram::resizePlot); - setLayout(vlayout); connect(view.get(), &FITSView::newStatus, this, &FITSTab::newStatus); connect(view.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled); // On Failure to load connect(view.get(), &FITSView::failed, this, &FITSTab::failed); return true; } // returns false if no setup needed. return false; } void FITSTab::loadFITS(const QUrl &imageURL, FITSMode mode, FITSScale filter, bool silent) { if (setupView(mode, filter)) { // On Success loading image connect(view.get(), &FITSView::loaded, [&]() { processData(); emit loaded(); }); } currentURL = imageURL; view->setFilter(filter); view->loadFITS(imageURL.toLocalFile(), silent); } void FITSTab::processData() { FITSData *image_data = view->getImageData(); histogram->reset(); image_data->setHistogram(histogram); // Only construct histogram if it is actually visible // Otherwise wait until histogram is needed before creating it. if (fitsSplitter->sizes().at(0) != 0) { histogram->constructHistogram(); } evaluateStats(); if (viewer->isStarsMarked()) view->toggleStars(true); loadFITSHeader(); // Don't add it to the list if it is already there if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0) { if(!image_data->isTempFile()) //Don't add it to the list if it is a preview { disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); recentImages->addItem(currentURL.toLocalFile()); recentImages->setCurrentRow(recentImages->count() - 1); connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS); } } view->updateFrame(); } bool FITSTab::loadFITSFromData(FITSData* data, const QUrl &imageURL, FITSMode mode, FITSScale filter) { setupView(mode, filter); currentURL = imageURL; view->setFilter(filter); if (!view->loadFITSFromData(data, imageURL.toLocalFile())) { // On Failure to load // connect(view.get(), &FITSView::failed, this, &FITSTab::failed); return false; } processData(); return true; } void FITSTab::modifyFITSState(bool clean) { if (clean) { if (undoStack->isClean() == false) undoStack->setClean(); mDirty = false; } else mDirty = true; emit changeStatus(clean); } int FITSTab::saveFITS(const QString &filename) { return view->saveFITS(filename); } void FITSTab::copyFITS() { QApplication::clipboard()->setImage(view->getDisplayImage()); } void FITSTab::histoFITS() { if (!histogram->isConstructed()) { histogram->constructHistogram(); evaluateStats(); } fitsTools->setCurrentIndex(1); if(view->width() > 200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } void FITSTab::evaluateStats() { FITSData *image_data = view->getImageData(); stat.statsTable->item(STAT_WIDTH, 0)->setText(QString::number(image_data->width())); stat.statsTable->item(STAT_HEIGHT, 0)->setText(QString::number(image_data->height())); stat.statsTable->item(STAT_BITPIX, 0)->setText(QString::number(image_data->bpp())); stat.statsTable->item(STAT_HFR, 0)->setText(QString::number(image_data->getHFR(), 'f', 3)); if (image_data->channels() == 1) { for (int i = STAT_MIN; i <= STAT_STDDEV; i++) { if (stat.statsTable->columnSpan(i, 0) != 3) stat.statsTable->setSpan(i, 0, 1, 3); } stat.statsTable->horizontalHeaderItem(0)->setText(i18n("Value")); stat.statsTable->hideColumn(1); stat.statsTable->hideColumn(2); } else { for (int i = STAT_MIN; i <= STAT_STDDEV; i++) { if (stat.statsTable->columnSpan(i, 0) != 1) stat.statsTable->setSpan(i, 0, 1, 1); } stat.statsTable->horizontalHeaderItem(0)->setText(i18nc("Red", "R")); stat.statsTable->showColumn(1); stat.statsTable->showColumn(2); } if (image_data->getMedian() == 0.0 && !histogram->isConstructed()) histogram->constructHistogram(); for (int i = 0; i < image_data->channels(); i++) { stat.statsTable->item(STAT_MIN, i)->setText(QString::number(image_data->getMin(i), 'f', 3)); stat.statsTable->item(STAT_MAX, i)->setText(QString::number(image_data->getMax(i), 'f', 3)); stat.statsTable->item(STAT_MEAN, i)->setText(QString::number(image_data->getMean(i), 'f', 3)); stat.statsTable->item(STAT_MEDIAN, i)->setText(QString::number(image_data->getMedian(i), 'f', 3)); stat.statsTable->item(STAT_STDDEV, i)->setText(QString::number(image_data->getStdDev(i), 'f', 3)); } } void FITSTab::statFITS() { fitsTools->setCurrentIndex(0); if(view->width() > 200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } void FITSTab::loadFITSHeader() { FITSData *image_data = view->getImageData(); int nkeys = image_data->getRecords().size(); int counter = 0; header.tableWidget->setRowCount(nkeys); for (FITSData::Record *oneRecord : image_data->getRecords()) { QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord->key); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 0, tempItem); tempItem = new QTableWidgetItem(oneRecord->value.toString()); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 1, tempItem); tempItem = new QTableWidgetItem(oneRecord->comment); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 2, tempItem); counter++; } header.tableWidget->setColumnWidth(0, 100); header.tableWidget->setColumnWidth(1, 100); header.tableWidget->setColumnWidth(2, 250); } void FITSTab::headerFITS() { fitsTools->setCurrentIndex(2); if(view->width() > 200) fitsSplitter->setSizes(QList() << 200 << view->width() - 200); else fitsSplitter->setSizes(QList() << 50 << 50); } bool FITSTab::saveFile() { QUrl backupCurrent = currentURL; QUrl currentDir(Options::fitsDir()); currentDir.setScheme("file"); if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp")) currentURL.clear(); // If no changes made, return. if (mDirty == false && !currentURL.isEmpty()) return false; if (currentURL.isEmpty()) { currentURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save FITS"), currentDir, "FITS (*.fits *.fits.gz *.fit)"); // if user presses cancel if (currentURL.isEmpty()) { currentURL = backupCurrent; return false; } if (currentURL.toLocalFile().contains('.') == 0) currentURL.setPath(currentURL.toLocalFile() + ".fits"); // Already display by dialog /*if (QFile::exists(currentURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n( "A file named \"%1\" already exists. " "Overwrite it?", currentURL.fileName() ), i18n( "Overwrite File?" ), KGuiItem(i18n( "&Overwrite" )) ); if(r==KMessageBox::Cancel) return false; }*/ } if (currentURL.isValid()) { int err_status = 0; if ((err_status = saveFITS('!' + currentURL.toLocalFile())) != 0) { // -1000 = user canceled if (err_status == -1000) return false; char err_text[FLEN_STATUS]; fits_get_errstatus(err_status, err_text); // Use KMessageBox or something here KSNotification::error(i18n("FITS file save error: %1", QString::fromUtf8(err_text)), i18n("FITS Save")); return false; } //statusBar()->changeItem(i18n("File saved."), 3); emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE); modifyFITSState(); return true; } else { QString message = i18n("Invalid URL: %1", currentURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); return false; } } bool FITSTab::saveFileAs() { currentURL.clear(); return saveFile(); } void FITSTab::ZoomIn() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomIn(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomOut() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomOut(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomDefault() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomDefault(); view->cleanUpZoom(oldCenter); } void FITSTab::tabPositionUpdated() { undoStack->setActive(true); emit newStatus(QString("%1%").arg(view->getCurrentZoom()), FITS_ZOOM); emit newStatus(QString("%1x%2").arg(view->getImageData()->width()).arg(view->getImageData()->height()), FITS_RESOLUTION); } diff --git a/kstars/fitsviewer/fitstab.h b/kstars/fitsviewer/fitstab.h index 035007d0a..84c11bdd7 100644 --- a/kstars/fitsviewer/fitstab.h +++ b/kstars/fitsviewer/fitstab.h @@ -1,173 +1,188 @@ /*************************************************************************** FITS Tab ------------------- copyright : (C) 2012 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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 "fitscommon.h" #include #include #include #include #include #include "ui_fitsheaderdialog.h" #include "ui_statform.h" #include #include #include +#include +#include +#include #include class FITSHistogram; class FITSView; class FITSViewer; class FITSData; /** * @brief The FITSTab class holds information on the current view (drawing area) in addition to the undo/redo stacks * and status of current document (clean or dirty). It also creates the corresponding histogram associated with the * image data that is stored in the FITSView class. * @author Jasem Mutlaq */ class FITSTab : public QWidget { Q_OBJECT public: explicit FITSTab(FITSViewer *parent); virtual ~FITSTab() override; enum { STAT_WIDTH, STAT_HEIGHT, STAT_BITPIX, STAT_HFR, STAT_MIN, STAT_MAX, STAT_MEAN, STAT_MEDIAN, STAT_STDDEV }; void clearRecentFITS(); void selectRecentFITS(int i); void loadFITS(const QUrl &imageURL, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, bool silent = true); bool loadFITSFromData(FITSData *data, const QUrl &imageURL, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE); int saveFITS(const QString &filename); inline QUndoStack *getUndoStack() { return undoStack; } inline QUrl *getCurrentURL() { return ¤tURL; } inline FITSView *getView() { return view.get(); } inline QPointer getHistogram() { return histogram; } inline QPointer getViewer() { return viewer; } bool saveFile(); bool saveFileAs(); void copyFITS(); void loadFITSHeader(); void headerFITS(); void histoFITS(); void evaluateStats(); void statFITS(); void setUID(int newID) { uid = newID; } int getUID() { return uid; } void saveUnsaved(); void tabPositionUpdated(); void selectGuideStar(); QString getPreviewText() const; void setPreviewText(const QString &value); public slots: void modifyFITSState(bool clean = true); void ZoomIn(); void ZoomOut(); void ZoomDefault(); protected: virtual void closeEvent(QCloseEvent *ev) override; private: bool setupView(FITSMode mode, FITSScale filter); + + QHBoxLayout* setupStretchBar(); + void setStretchUIValues(bool adjustSliders); + void rescaleShadows(); + void rescaleMidtones(); + void processData(); /** Ask user whether he wants to save changes and save if he do. */ /// The FITSTools Toolbox QPointer fitsTools; /// The Splitter for th FITSTools Toolbox QPointer fitsSplitter; /// The FITS Header Panel QPointer fitsHeaderDialog; Ui::fitsHeaderDialog header; /// The Statistics Panel QPointer statWidget; Ui::statForm stat; /// FITS Histogram QPointer histogram; QPointer viewer; QPointer recentImages; /// FITS image object std::unique_ptr view; /// History for undo/redo QUndoStack *undoStack { nullptr }; /// FITS File name and path QUrl currentURL; bool mDirty { false }; QString previewText; int uid { 0 }; - //QFuture histogramFuture; + // Stretch bar widgets + std::unique_ptr shadowsLabel, midtonesLabel, highlightsLabel; + std::unique_ptr shadowsVal, midtonesVal, highlightsVal; + std::unique_ptr shadowsSlider, midtonesSlider, highlightsSlider; + std::unique_ptr stretchButton, autoButton; + float maxShadows {0.5}, maxMidtones {0.5}, maxHighlights {1.0}; + //QFuture histogramFuture; - signals: +signals: void debayerToggled(bool); void newStatus(const QString &msg, FITSBar id); void changeStatus(bool clean); void loaded(); void failed(); }; diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index 6bc848fc0..4b3c2ad30 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1718 +1,1811 @@ /* FITS View Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster 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 "config-kstars.h" #include "fitsview.h" #include "fitsdata.h" #include "fitslabel.h" #include "kspopupmenu.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymap.h" #include "fits_debug.h" #include "stretch.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include #include #include #include #include #include #define BASE_OFFSET 50 -#define ZOOM_DEFAULT 100.0 +#define ZOOM_DEFAULT 100.0f #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 -namespace -{ +namespace { + +// Derive the Green and Blue stretch parameters from their previous values and the +// changes made to the Red parameters. We apply the same offsets used for Red to the +// other channels' parameters, but clip them. +void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params) { + float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows; + float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights; + float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones; + + params->green.shadows = params->green.shadows + shadow_diff; + params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f); + params->green.highlights = params->green.highlights + highlight_diff; + params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f); + params->green.midtones = params->green.midtones + midtones_diff; + params->green.midtones = std::max(params->green.midtones, 0.0f); -void doStretch(FITSData *data, QImage *outputImage, bool stretchOn) + params->blue.shadows = params->blue.shadows + shadow_diff; + params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f); + params->blue.highlights = params->blue.highlights + highlight_diff; + params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f); + params->blue.midtones = params->blue.midtones + midtones_diff; + params->blue.midtones = std::max(params->blue.midtones, 0.0f); +} + +} // namespace + +// Runs the stretch checking the variables to see which parameters to use. +// We call stretch even if we're not stretching, as the stretch code still +// converts the image to the uint8 output image which will be displayed. +// In that case, it will use an identity stretch. +void FITSView::doStretch(FITSData *data, QImage *outputImage) { 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); + + StretchParams tempParams; + if (!stretchImage) + tempParams = StretchParams(); // Keeping it linear + else if (autoStretch) + { + // Compute new auto-stretch params. + stretchParams = stretch.computeParams(data->getImageBuffer()); + tempParams = stretchParams; + } + else + // Use the existing stretch params. + tempParams = stretchParams; + + stretch.setParams(tempParams); + stretch.run(data->getImageBuffer(), outputImage, sampling); } -} // namespace +// Store stretch parameters, and turn on stretching if it isn't already on. +void FITSView::setStretchParams(const StretchParams& params) +{ + if (imageData->channels() == 3) + ComputeGBStretchParams(params, &stretchParams); + + stretchParams.grey_red = params.grey_red; + stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f); + stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f); + stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f); + + autoStretch = false; + stretchImage = true; + + if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) + updateFrame(); +} + +// Turn on or off stretching, and if on, use whatever parameters are currently stored. +void FITSView::setStretch(bool onOff) +{ + if (stretchImage != onOff) + { + stretchImage = onOff; + if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) + updateFrame(); + } +} +// Turn on stretching, using automatically generated parameters. +void FITSView::setAutoStretchParams() +{ + stretchImage = true; + autoStretch = true; + if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) + updateFrame(); +} FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { + // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters. + // The user may enter his/her own. stretchImage = Options::autoStretch(); - + // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides + // by adjusting the stretchBar's sliders. + autoStretch = true; + grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); filter = filterType; mode = fitsMode; setBackgroundRole(QPalette::Dark); markerCrosshair.setX(0); markerCrosshair.setY(0); setBaseSize(740, 530); connect(image_frame.get(), SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar))); connect(image_frame.get(), SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int))); connect(image_frame.get(), SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int))); connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState())); connect(&fitsWatcher, &QFutureWatcher::finished, this, &FITSView::loadInFrame); image_frame->setMouseTracking(true); setCursorMode( selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode noImageLabel = new QLabel(); noImage.load(":/images/noimage.png"); noImageLabel->setPixmap(noImage); noImageLabel->setAlignment(Qt::AlignCenter); this->setWidget(noImageLabel); redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); } FITSView::~FITSView() { fitsWatcher.waitForFinished(); wcsWatcher.waitForFinished(); delete (imageData); } /** This method looks at what mouse mode is currently selected and updates the cursor to match. */ void FITSView::updateMouseCursor() { if (cursorMode == dragCursor) { if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) { if (!image_frame->getMouseButtonDown()) viewport()->setCursor(Qt::PointingHandCursor); else viewport()->setCursor(Qt::ClosedHandCursor); } else viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == selectCursor) { viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == scopeCursor) { viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); } else if (cursorMode == crosshairCursor) { viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); } } /** This is how the mouse mode gets set. The default for a FITSView in a FITSViewer should be the dragMouse The default for a FITSView in the Focus or Align module should be the selectMouse The different defaults are accomplished by putting making the actual default mouseMode the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse. */ void FITSView::setCursorMode(CursorMode mode) { cursorMode = mode; updateMouseCursor(); if (mode == scopeCursor && imageHasWCS()) { if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } } } void FITSView::resizeEvent(QResizeEvent * event) { if ((imageData == nullptr) && noImageLabel != nullptr) { noImageLabel->setPixmap( noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); noImageLabel->setFixedSize(width() - 5, height() - 5); } QScrollArea::resizeEvent(event); } void FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case image is still loading, wait until it is done. fitsWatcher.waitForFinished(); // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } bool FITSView::loadFITSFromData(FITSData *data, const QString &inFilename) { + Q_UNUSED(inFilename) if (imageData != nullptr) { delete imageData; imageData = nullptr; } if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); // Takes control of the objects passed in. imageData = data; return processData(); } bool FITSView::processData() { // Set current width and height + if (!imageData) return false; currentWidth = imageData->width(); currentHeight = imageData->height(); - image_width = currentWidth; - image_height = currentHeight; + int image_width = currentWidth; + int image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image initDisplayImage(); imageData->applyFilter(filter); // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); return false; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); return false; } } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } else syncWCSState(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); if (showStarProfile) { if(floatingToolBar != nullptr) toggleProfileAction->setChecked(true); //Need to wait till the Focus module finds stars, if its the Focus module. QTimer::singleShot(100, this, SLOT(viewStarProfile())); } scaledImage = QImage(); updateFrame(); return true; } void FITSView::loadInFrame() { // Check if the loading was OK if (fitsWatcher.result() == false) { m_LastError = imageData->getLastError(); emit failed(); return; } // Notify if there is debayer data. emit debayerToggled(imageData->hasDebayer()); if (processData()) emit loaded(); else emit failed(); } int FITSView::saveFITS(const QString &newFilename) { return imageData->saveFITS(newFilename); } bool FITSView::rescale(FITSZoom type) { switch (imageData->property("dataType").toInt()) { case TBYTE: return rescale(type); case TSHORT: return rescale(type); case TUSHORT: return rescale(type); case TLONG: return rescale(type); case TULONG: return rescale(type); case TFLOAT: return rescale(type); case TLONGLONG: return rescale(type); case TDOUBLE: return rescale(type); default: break; } return false; } FITSView::CursorMode FITSView::getCursorMode() { return cursorMode; } void FITSView::enterEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(0.2); a->setEndValue(1); a->setEasingCurve(QEasingCurve::InBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } void FITSView::leaveEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(1); a->setEndValue(0.2); a->setEasingCurve(QEasingCurve::OutBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } template bool FITSView::rescale(FITSZoom type) { if (rawImage.isNull()) return false; - if (true || image_height != imageData->height() || image_width != imageData->width()) - { - image_width = imageData->width(); - image_height = imageData->height(); - - initDisplayImage(); - - 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) return false; + int image_width = imageData->width(); + int image_height = imageData->height(); + currentWidth = image_width; + currentHeight = image_height; - doStretch(imageData, &rawImage, stretchImage); - - scaledImage = QImage(); + if (isVisible()) + emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); switch (type) { case ZOOM_FIT_WINDOW: - if ((rawImage.width() > width() || rawImage.height() > height())) + if ((image_width > width() || image_height > height())) { double w = baseSize().width() - BASE_OFFSET; double h = baseSize().height() - BASE_OFFSET; if (!firstLoad) { w = viewport()->rect().width() - BASE_OFFSET; h = viewport()->rect().height() - BASE_OFFSET; } // Find the zoom level which will enclose the current FITS in the current window size double zoomX = floor((w / static_cast(currentWidth)) * 100.); double zoomY = floor((h / static_cast(currentHeight)) * 100.); (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); if (currentZoom <= ZOOM_MIN) emit actionUpdated("view_zoom_out", false); } else { currentZoom = 100; currentWidth = image_width; currentHeight = image_height; } break; case ZOOM_KEEP_LEVEL: { currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); } break; default: currentZoom = 100; break; } + initDisplayImage(); + image_frame->setScaledContents(true); + doStretch(imageData, &rawImage); + scaledImage = QImage(); setWidget(image_frame.get()); - if (type != ZOOM_KEEP_LEVEL) - emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); + // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI. + emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); return true; } void FITSView::ZoomIn() { if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) { emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); return; } if (currentZoom < ZOOM_DEFAULT) currentZoom += ZOOM_LOW_INCR; else currentZoom += ZOOM_HIGH_INCR; emit actionUpdated("view_zoom_out", true); if (currentZoom >= ZOOM_MAX) { currentZoom = ZOOM_MAX; emit actionUpdated("view_zoom_in", false); } - currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); - currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); + if (!imageData) return; + currentWidth = imageData->width() * (currentZoom / ZOOM_DEFAULT); + currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomOut() { if (currentZoom <= ZOOM_DEFAULT) currentZoom -= ZOOM_LOW_INCR; else currentZoom -= ZOOM_HIGH_INCR; if (currentZoom <= ZOOM_MIN) { currentZoom = ZOOM_MIN; emit actionUpdated("view_zoom_out", false); } emit actionUpdated("view_zoom_in", true); - currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); - currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); + if (!imageData) return; + currentWidth = imageData->width() * (currentZoom / ZOOM_DEFAULT); + currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomToFit() { if (rawImage.isNull() == false) { rescale(ZOOM_FIT_WINDOW); updateFrame(); } } void FITSView::setStarFilterRange(float const innerRadius, float const outerRadius) { starFilter.innerRadius = innerRadius; starFilter.outerRadius = outerRadius; } int FITSView::filterStars() { return starFilter.used() ? imageData->filterStars(starFilter.innerRadius, starFilter.outerRadius) : imageData->getStarCenters().count(); } void FITSView::updateFrame() { bool ok = false; if (toggleStretchAction) toggleStretchAction->setChecked(stretchImage); if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary if (scaledImage.isNull() || currentWidth != lastWidth || currentHeight != lastHeight) { scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); lastWidth = currentWidth; lastHeight = currentHeight; } ok = displayPixmap.convertFromImage(scaledImage); } else ok = displayPixmap.convertFromImage(rawImage); if (!ok) return; QPainter painter(&displayPixmap); drawOverlay(&painter); if (starFilter.used()) { double const diagonal = std::sqrt(currentWidth * currentWidth + currentHeight * currentHeight) / 2; int const innerRadius = std::lround(diagonal * starFilter.innerRadius); int const outerRadius = std::lround(diagonal * starFilter.outerRadius); QPoint const center(currentWidth / 2, currentHeight / 2); painter.save(); painter.setPen(QPen(Qt::blue, 1, Qt::DashLine)); painter.setOpacity(0.7); painter.setBrush(QBrush(Qt::transparent)); painter.drawEllipse(center, outerRadius, outerRadius); painter.setBrush(QBrush(Qt::blue, Qt::FDiagPattern)); painter.drawEllipse(center, innerRadius, innerRadius); painter.restore(); } image_frame->setPixmap(displayPixmap); image_frame->resize(currentWidth, currentHeight); } void FITSView::ZoomDefault() { if (image_frame != nullptr) { emit actionUpdated("view_zoom_out", true); emit actionUpdated("view_zoom_in", true); currentZoom = ZOOM_DEFAULT; - currentWidth = image_width; - currentHeight = image_height; + currentWidth = imageData->width(); + currentHeight = imageData->height(); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter * painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) drawTrackingBox(painter); if (!markerCrosshair.isNull()) drawMarker(painter); if (showCrosshair) drawCrosshair(painter); if (showObjects) drawObjectNames(painter); if (showEQGrid) drawEQGrid(painter); if (showPixelGrid) drawPixelGrid(painter); if (markStars) drawStarCentroid(painter); } void FITSView::updateMode(FITSMode fmode) { mode = fmode; } void FITSView::drawMarker(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 2)); painter->setBrush(Qt::NoBrush); float pxperdegree = (currentZoom / ZOOM_DEFAULT) * (57.3 / 1.8); float s1 = 0.5 * pxperdegree; float s2 = pxperdegree; float s3 = 2.0 * pxperdegree; float x0 = markerCrosshair.x() * (currentZoom / ZOOM_DEFAULT); float y0 = markerCrosshair.y() * (currentZoom / ZOOM_DEFAULT); float x1 = x0 - 0.5 * s1; float y1 = y0 - 0.5 * s1; float x2 = x0 - 0.5 * s2; float y2 = y0 - 0.5 * s2; float x3 = x0 - 0.5 * s3; float y3 = y0 - 0.5 * s3; //Draw radial lines painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); //Draw circles at 0.5 & 1 degrees painter->drawEllipse(QRectF(x1, y1, s1, s1)); painter->drawEllipse(QRectF(x2, y2, s2, s2)); } void FITSView::drawStarCentroid(QPainter * painter) { float const ratio = currentZoom / ZOOM_DEFAULT; if (showStarsHFR) { QFont painterFont; // If we need to print the HFR out, give an arbitrarily sized font to the painter painterFont.setPointSizeF(painterFont.pointSizeF() * 3 * ratio); painter->setFont(painterFont); } painter->setPen(QPen(Qt::red, 2)); QFontMetrics const fontMetrics = painter->fontMetrics(); QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height()); foreach (auto const &starCenter, imageData->getStarCenters()) { int const x1 = std::round((starCenter->x - starCenter->width / 2.0f) * ratio); int const y1 = std::round((starCenter->y - starCenter->width / 2.0f) * ratio); int const w = std::round(starCenter->width * ratio); // Draw a circle around the detected star painter->drawEllipse(x1, y1, w, w); if (showStarsHFR) { // Ask the painter how large will the HFR text be QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2); QSize const hfrSize = fontMetrics.size(Qt::TextSingleLine, hfr); // Store the HFR text in a rect QPoint const hfrBottomLeft(x1+w+5, y1+w/2); QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height()); // Render the HFR text only if it can be displayed entirely if (boundingRect.contains(hfrRect)) { painter->setPen(QPen(Qt::red, 3)); painter->drawText(hfrBottomLeft, hfr); painter->setPen(QPen(Qt::red, 2)); } } } } void FITSView::drawTrackingBox(QPainter * painter) { painter->setPen(QPen(Qt::green, 2)); if (trackingBox.isNull()) return; int x1 = trackingBox.x() * (currentZoom / ZOOM_DEFAULT); int y1 = trackingBox.y() * (currentZoom / ZOOM_DEFAULT); int w = trackingBox.width() * (currentZoom / ZOOM_DEFAULT); int h = trackingBox.height() * (currentZoom / ZOOM_DEFAULT); painter->drawRect(x1, y1, w, h); } /** This Method draws a large Crosshair in the center of the image, it is like a set of axes. */ void FITSView::drawCrosshair(QPainter * painter) { + if (!imageData) return; + int image_width = imageData->width(); + int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); float midX = (float)image_width / 2 * scale; float midY = (float)image_height / 2 * scale; float maxX = (float)image_width * scale; float maxY = (float)image_height * scale; float r = 50 * scale; painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")))); //Horizontal Line to Circle painter->drawLine(0, midY, midX - r, midY); //Horizontal Line past Circle painter->drawLine(midX + r, midY, maxX, midY); //Vertical Line to Circle painter->drawLine(midX, 0, midX, midY - r); //Vertical Line past Circle painter->drawLine(midX, midY + r, midX, maxY); //Circles painter->drawEllipse(c, r, r); painter->drawEllipse(c, r / 2, r / 2); } /** This method is intended to draw a pixel grid onto the image. It first determines useful information from the image. Then it draws the axes on the image if the crosshairs are not displayed. Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. Note: This has to start drawing at the center not at the edges because the center axes must be in the center of the image. */ void FITSView::drawPixelGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); - double width = image_width * scale; - double height = image_height * scale; - double cX = width / 2; - double cY = height / 2; - double deltaX = width / 10; - double deltaY = height / 10; + float width = imageData->width() * scale; + float height = imageData->height() * scale; + float cX = width / 2; + float cY = height / 2; + float deltaX = width / 10; + float deltaY = height / 10; //draw the Axes painter->setPen(QPen(Qt::red)); painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); painter->drawText(width - 30, cY - 5, QString::number((int)((cY) / scale))); if (!showCrosshair) { painter->drawLine(cX, 0, cX, height); painter->drawLine(0, cY, width, cY); } painter->setPen(QPen(Qt::gray)); //Start one iteration past the Center and draw 4 lines on either side of 0 for (int x = deltaX; x < cX - deltaX; x += deltaX) { painter->drawText(cX + x - 30, height - 5, QString::number((int)((cX + x) / scale))); painter->drawText(cX - x - 30, height - 5, QString::number((int)((cX - x) / scale))); painter->drawLine(cX - x, 0, cX - x, height); painter->drawLine(cX + x, 0, cX + x, height); } //Start one iteration past the Center and draw 4 lines on either side of 0 for (int y = deltaY; y < cY - deltaY; y += deltaY) { painter->drawText(width - 30, cY + y - 5, QString::number((int)((cY + y) / scale))); painter->drawText(width - 30, cY - y - 5, QString::number((int)((cY - y) / scale))); painter->drawLine(0, cY + y, width, cY + y); painter->drawLine(0, cY - y, width, cY - y); } } bool FITSView::imageHasWCS() { if (imageData != nullptr) return imageData->hasWCS(); return false; } void FITSView::drawObjectNames(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); float scale = (currentZoom / ZOOM_DEFAULT); foreach (FITSSkyObject * listObject, imageData->getSkyObjects()) { painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); } } /** This method will paint EQ Gridlines in an overlay if there is WCS data present. It determines the minimum and maximum RA and DEC, then it uses that information to judge which gridLines to draw. Then it calls the drawEQGridlines methods below to draw gridlines at those specific RA and Dec values. */ void FITSView::drawEQGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); + int image_width = imageData->width(); + int image_height = imageData->height(); if (imageData->hasWCS()) { wcs_point * wcs_coord = imageData->getWCSCoord(); if (wcs_coord != nullptr) { int size = image_width * image_height; double maxRA = -1000; double minRA = 1000; double maxDec = -1000; double minDec = 1000; for (int i = 0; i < (size); i++) { double ra = wcs_coord[i].ra; double dec = wcs_coord[i].dec; if (ra > maxRA) maxRA = ra; if (ra < minRA) minRA = ra; if (dec > maxDec) maxDec = dec; if (dec < minDec) minDec = dec; } auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop auto maxDecMinutes = (int)(maxDec * 12); auto minRAMinutes = (int)(minRA / 15.0 * 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. if (maxDec > 50 || minDec < -50) { minRAMinutes = (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees maxRAMinutes = (int)(maxRA / 15.0 * 60.0); raConvert = 15 / 60.0; } if (maxDec > 80 || minDec < -80) { minRAMinutes = (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees maxRAMinutes = (int)(maxRA / 15.0 * 30); raConvert = 15 / 30.0; } if (maxDec > 85 || minDec < -85) { minRAMinutes = (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees maxRAMinutes = (int)(maxRA / 15.0 * 6); raConvert = 15 / 6.0; } if (maxDec >= 89.25 || minDec <= -89.25) { minRAMinutes = (int)(minRA / 15); //This will force the scale to whole hours of RA in the loop really close to the poles maxRAMinutes = (int)(maxRA / 15); raConvert = 15; } painter->setPen(QPen(Qt::yellow)); QPointF pixelPoint, imagePoint, pPoint; //This section draws the RA Gridlines for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) { painter->setPen(QPen(Qt::yellow)); double target = targetRA * raConvert; if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxDec - minDec) / 100.0); //This will determine how many points to use to create the RA Line for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) { SkyPoint pointToGet(target / 15.0, targetDec); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) { if (maxDec > 50 || maxDec < -50) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + '\''); else painter->drawText(pt.x() - 20, pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + "' " + QString::number(dms(target).second()) + "''"); } } } //This section draws the DEC Gridlines for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) { if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxRA - minRA) / 100.0); //This will determine how many points to use to create the Dec Line double target = targetDec * decConvert; for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) { SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''); } } //This Section Draws the North Celestial Pole if present SkyPoint NCP(0, 90); bool NCPtest = imageData->wcsToPixel(NCP, pPoint, imagePoint); if (NCPtest) { bool NCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (NCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("North Celestial Pole", "NCP")); } } //This Section Draws the South Celestial Pole if present SkyPoint SCP(0, -90); bool SCPtest = imageData->wcsToPixel(SCP, pPoint, imagePoint); if (SCPtest) { bool SCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (SCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("South Celestial Pole", "SCP")); } } } } } bool FITSView::pointIsInImage(QPointF pt, bool scaled) { + int image_width = imageData->width(); + int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); if (scaled) return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; else return pt.x() < image_width && pt.y() < image_height && pt.x() > 0 && pt.y() > 0; } QPointF FITSView::getPointForGridLabel() { + int image_width = imageData->width(); + int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); //These get the maximum X and Y points in the list that are in the image QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() > maxXPt.x() && pointIsInImage(p, true)) maxXPt = p; } QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() > maxYPt.y() && pointIsInImage(p, true)) maxYPt = p; } QPointF minXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() < minXPt.x() && pointIsInImage(p, true)) minXPt = p; } QPointF minYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() < minYPt.y() && pointIsInImage(p, true)) minYPt = p; } //This gives preference to points that are on the right hand side and bottom. //But if the line doesn't intersect the right or bottom, it then tries for the top and left. //If no points are found in the image, it returns a point off the screen //If all else fails, like in the case of a circle on the image, it returns the far right point. if (image_width * scale - maxXPt.x() < 10) { return QPointF( image_width * scale - 50, maxXPt.y() - 10); //This will draw the text on the right hand side, up and to the left of the point where the line intersects } if (image_height * scale - maxYPt.y() < 10) return QPointF( maxYPt.x() - 40, image_height * scale - 10); //This will draw the text on the bottom side, up and to the left of the point where the line intersects if (minYPt.y() * scale < 30) return QPointF( minYPt.x() + 10, 20); //This will draw the text on the top side, down and to the right of the point where the line intersects if (minXPt.x() * scale < 30) return QPointF( 10, minXPt.y() + 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) return QPointF(-100, -100); //All of the points were off the screen return QPoint(maxXPt.x() - 40, maxXPt.y() - 10); } void FITSView::setFirstLoad(bool value) { firstLoad = value; } QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) { if (trackingBox.isNull()) return trackingBoxPixmap; int x1 = (trackingBox.x() - margin) * (currentZoom / ZOOM_DEFAULT); int y1 = (trackingBox.y() - margin) * (currentZoom / ZOOM_DEFAULT); int w = (trackingBox.width() + margin * 2) * (currentZoom / ZOOM_DEFAULT); int h = (trackingBox.height() + margin * 2) * (currentZoom / ZOOM_DEFAULT); trackingBoxPixmap = image_frame->grab(QRect(x1, y1, w, h)); return trackingBoxPixmap; } void FITSView::setTrackingBox(const QRect &rect) { if (rect != trackingBox) { trackingBox = rect; updateFrame(); if(showStarProfile) viewStarProfile(); } } void FITSView::resizeTrackingBox(int newSize) { int x = trackingBox.x() + trackingBox.width() / 2; int y = trackingBox.y() + trackingBox.height() / 2; int delta = newSize / 2; setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } bool FITSView::isImageStretched() { return stretchImage; } bool FITSView::isCrosshairShown() { return showCrosshair; } bool FITSView::isEQGridShown() { return showEQGrid; } bool FITSView::areObjectsShown() { return showObjects; } bool FITSView::isPixelGridShown() { return showPixelGrid; } void FITSView::toggleCrosshair() { showCrosshair = !showCrosshair; updateFrame(); } void FITSView::toggleEQGrid() { showEQGrid = !showEQGrid; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleObjects() { showObjects = !showObjects; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStars() { toggleStars(!markStars); if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStretch() { stretchImage = !stretchImage; if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) updateFrame(); } void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(showStarProfile) { //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views. //So for Normal and Align views, we need to set up the tracking box. if(mode == FITS_NORMAL || mode == FITS_ALIGN) { setCursorMode(selectCursor); connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); trackingBox = QRect(0, 0, 128, 128); setTrackingBoxEnabled(true); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); } else { //This shuts down the tracking box for Normal and Align Views //It doesn't touch Guide and Focus Views because they still need a tracking box if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); setTrackingBoxEnabled(false); if(starProfileWidget) disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) { disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); starProfileWidget->close(); starProfileWidget = nullptr; } emit starProfileWindowClosed(); } updateFrame(); #endif } void FITSView::move3DTrackingBox(int x, int y) { int boxSize = trackingBox.width(); QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize); setTrackingBox(starRect); } void FITSView::viewStarProfile() { #ifdef HAVE_DATAVISUALIZATION if(!trackingBoxEnabled) { setTrackingBoxEnabled(true); setTrackingBox(QRect(0, 0, 128, 128)); } if(!starProfileWidget) { starProfileWidget = new StarProfileViewer(this); //This is a band-aid to fix a QT bug with createWindowContainer //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow QWidget * superParent = this->parentWidget(); while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) superParent = superParent->parentWidget(); superParent->setCursor(Qt::ArrowCursor); //This is the end of the band-aid connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); if(mode == FITS_ALIGN || mode == FITS_NORMAL) { starProfileWidget->enableTrackingBox(true); imageData->setStarAlgorithm(ALGORITHM_CENTROID); connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } } QList starCenters = imageData->getStarCentersInSubFrame(trackingBox); if(starCenters.size() == 0) { // FIXME, the following does not work anymore. //imageData->findStars(&trackingBox, true); // FIXME replacing it with this imageData->findStars(ALGORITHM_CENTROID, trackingBox); starCenters = imageData->getStarCentersInSubFrame(trackingBox); } starProfileWidget->loadData(imageData, trackingBox, starCenters); starProfileWidget->show(); starProfileWidget->raise(); if(markStars) updateFrame(); //this is to update for the marked stars #endif } void FITSView::togglePixelGrid() { showPixelGrid = !showPixelGrid; updateFrame(); } int FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) { int count = 0; if(trackingBoxEnabled) count = imageData->findStars(algorithm, trackingBox); else count = imageData->findStars(algorithm, searchBox); return count; } void FITSView::toggleStars(bool enable) { markStars = enable; if (markStars && !imageData->areStarsSearched()) { QApplication::setOverrideCursor(Qt::WaitCursor); emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); qApp->processEvents(); int count = findStars(); if (count >= 0 && isVisible()) emit newStatus(i18np("1 star detected.", "%1 stars detected.", count), FITS_MESSAGE); QApplication::restoreOverrideCursor(); } } void FITSView::processPointSelection(int x, int y) { emit trackingStarSelected(x, y); } void FITSView::processMarkerSelection(int x, int y) { markerCrosshair.setX(x); markerCrosshair.setY(y); updateFrame(); } void FITSView::setTrackingBoxEnabled(bool enable) { if (enable != trackingBoxEnabled) { trackingBoxEnabled = enable; //updateFrame(); } } void FITSView::wheelEvent(QWheelEvent * event) { //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad //It should still do the zoom if it is a mouse wheel if (event->source() == Qt::MouseEventSynthesizedBySystem) { QScrollArea::wheelEvent(event); } else { QPoint mouseCenter = getImagePoint(event->pos()); if (event->angleDelta().y() > 0) ZoomIn(); else ZoomOut(); event->accept(); cleanUpZoom(mouseCenter); } } /** This method is intended to keep key locations in an image centered on the screen while zooming. If there is a marker or tracking box, it centers on those. If not, it uses the point called viewCenter that was passed as a parameter. */ void FITSView::cleanUpZoom(QPoint viewCenter) { int x0 = 0; int y0 = 0; double scale = (currentZoom / ZOOM_DEFAULT); if (!markerCrosshair.isNull()) { x0 = markerCrosshair.x() * scale; y0 = markerCrosshair.y() * scale; } else if (trackingBoxEnabled) { x0 = trackingBox.center().x() * scale; y0 = trackingBox.center().y() * scale; } else { x0 = viewCenter.x() * scale; y0 = viewCenter.y() * scale; } ensureVisible(x0, y0, width() / 2, height() / 2); updateMouseCursor(); } /** This method converts a point from the ViewPort Coordinate System to the Image Coordinate System. */ QPoint FITSView::getImagePoint(QPoint viewPortPoint) { QWidget * w = widget(); if (w == nullptr) return QPoint(0, 0); double scale = (currentZoom / ZOOM_DEFAULT); QPoint widgetPoint = w->mapFromParent(viewPortPoint); QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); return imagePoint; } void FITSView::initDisplayImage() { + // Account for leftover when sampling. Thus a 5-wide image sampled by 2 + // would result in a width of 3 (samples 0, 2 and 4). + int w = (imageData->width() + sampling - 1) / sampling; + int h = (imageData->height() + sampling - 1) / sampling; if (imageData->channels() == 1) { - rawImage = QImage(image_width, image_height, QImage::Format_Indexed8); + rawImage = QImage(w, h, QImage::Format_Indexed8); rawImage.setColorCount(256); for (int i = 0; i < 256; i++) rawImage.setColor(i, qRgb(i, i, i)); } else { - rawImage = QImage(image_width, image_height, QImage::Format_RGB32); + rawImage = QImage(w, h, QImage::Format_RGB32); } } /** The Following two methods allow gestures to work with trackpads. Specifically, we are targeting the pinch events, so that if one is generated, Then the pinchTriggered method will be called. If the event is not a pinch gesture, then the event is passed back to the other event handlers. */ bool FITSView::event(QEvent * event) { if (event->type() == QEvent::Gesture) return gestureEvent(dynamic_cast(event)); return QScrollArea::event(event); } bool FITSView::gestureEvent(QGestureEvent * event) { if (QGesture * pinch = event->gesture(Qt::PinchGesture)) pinchTriggered(dynamic_cast(pinch)); return true; } /** This Method works with Trackpads to use the pinch gesture to scroll in and out It stores a point to keep track of the location where the gesture started so that while you are zooming, it tries to keep that initial point centered in the view. **/ void FITSView::pinchTriggered(QPinchGesture * gesture) { if (!zooming) { zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); zooming = true; } if (gesture->state() == Qt::GestureFinished) { zooming = false; } zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. if (zoomTime > 10000) //This ensures zoomtime never gets too big. zoomTime = 0; if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. { if (gesture->totalScaleFactor() > 1) ZoomIn(); else ZoomOut(); } cleanUpZoom(zoomLocation); } /*void FITSView::handleWCSCompletion() { //bool hasWCS = wcsWatcher.result(); if(imageData->hasWCS()) this->updateFrame(); emit wcsToggled(imageData->hasWCS()); }*/ void FITSView::syncWCSState() { bool hasWCS = imageData->hasWCS(); bool wcsLoaded = imageData->isWCSLoaded(); if (hasWCS && wcsLoaded) this->updateFrame(); emit wcsToggled(hasWCS); if (toggleEQGridAction != nullptr) toggleEQGridAction->setEnabled(hasWCS); if (toggleObjectsAction != nullptr) toggleObjectsAction->setEnabled(hasWCS); if (centerTelescopeAction != nullptr) centerTelescopeAction->setEnabled(hasWCS); } void FITSView::createFloatingToolBar() { if (floatingToolBar != nullptr) return; floatingToolBar = new QToolBar(this); auto * eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); eff->setOpacity(0.2); floatingToolBar->setVisible(false); floatingToolBar->setStyleSheet( "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" "QToolButton{background: transparent; border:none; color: yellow}" "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); floatingToolBar->setFloatable(true); floatingToolBar->setIconSize(QSize(25, 25)); //floatingToolBar->setMovable(true); QAction * action = nullptr; floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this, SLOT(ZoomIn())); floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this, SLOT(ZoomOut())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), i18n("Default Zoom"), this, SLOT(ZoomDefault())); 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"), i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); action->setCheckable(true); action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); action->setCheckable(true); toggleStarsAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), i18n("Detect Stars in Image"), this, SLOT(toggleStars())); toggleStarsAction->setCheckable(true); #ifdef HAVE_DATAVISUALIZATION toggleProfileAction = floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), i18n("View Star Profile"), this, SLOT(toggleStarProfile())); toggleProfileAction->setCheckable(true); #endif if (mode == FITS_NORMAL || mode == FITS_ALIGN) { floatingToolBar->addSeparator(); toggleEQGridAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid())); toggleEQGridAction->setCheckable(true); toggleEQGridAction->setEnabled(false); toggleObjectsAction = floatingToolBar->addAction(QIcon::fromTheme("help-hint"), i18n("Show Objects in Image"), this, SLOT(toggleObjects())); toggleObjectsAction->setCheckable(true); toggleEQGridAction->setEnabled(false); centerTelescopeAction = floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), i18n("Center Telescope"), this, SLOT(centerTelescope())); centerTelescopeAction->setCheckable(true); centerTelescopeAction->setEnabled(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSView::centerTelescope() { if (imageHasWCS()) { if (getCursorMode() == FITSView::scopeCursor) { setCursorMode(lastMouseMode); } else { lastMouseMode = getCursorMode(); setCursorMode(FITSView::scopeCursor); } updateFrame(); } updateScopeButton(); } void FITSView::updateScopeButton() { if (centerTelescopeAction != nullptr) { if (getCursorMode() == FITSView::scopeCursor) { centerTelescopeAction->setChecked(true); } else { centerTelescopeAction->setChecked(false); } } } /** This method just verifies if INDI is online, a telescope present, and is connected */ bool FITSView::isTelescopeActive() { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { return false; } foreach (ISD::GDInterface * gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice * bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; return bd->isConnected(); } return false; #else return false; #endif } void FITSView::setStarsEnabled(bool enable) { markStars = enable; if (floatingToolBar != nullptr) { foreach (QAction * action, floatingToolBar->actions()) { if (action->text() == i18n("Detect Stars in Image")) { action->setChecked(markStars); break; } } } } void FITSView::setStarsHFREnabled(bool enable) { showStarsHFR = enable; } diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index 53fdd14b6..9bac55c92 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,367 +1,401 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster 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 "fitscommon.h" #include +#include "stretch.h" #ifdef HAVE_DATAVISUALIZATION #include "starprofileviewer.h" #endif #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include #include #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QAction; class QEvent; class QGestureEvent; class QImage; class QLabel; class QPinchGesture; class QResizeEvent; class QToolBar; class FITSData; class FITSLabel; class FITSView : public QScrollArea { Q_OBJECT public: explicit FITSView(QWidget *parent = nullptr, FITSMode fitsMode = FITS_NORMAL, FITSScale filterType = FITS_NONE); virtual ~FITSView() override; typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and displays it in a FITSView frame * @param inFilename FITS File name * @param silent if set, error popups are suppressed. * @note If image is successfully, loaded() signal is emitted, otherwise failed() signal is emitted. * Obtain error by calling lastError() */ void loadFITS(const QString &inFilename, bool silent = true); /** * @brief loadFITSFromData Takes ownership of the FITSData instance passed in and displays it in a FITSView frame * @param inFilename FITS File name to use */ bool loadFITSFromData(FITSData *data, const QString &inFilename); // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired bool rescale(FITSZoom type); // Access functions FITSData *getImageData() const { return imageData; } double getCurrentZoom() const { return currentZoom; } QImage getDisplayImage() const { return rawImage; } const QPixmap &getDisplayPixmap() const { return displayPixmap; } // Tracking square void setTrackingBoxEnabled(bool enable); bool isTrackingBoxEnabled() const { return trackingBoxEnabled; } QPixmap &getTrackingBoxPixmap(uint8_t margin = 0); void setTrackingBox(const QRect &rect); const QRect &getTrackingBox() const { return trackingBox; } // last error const QString &lastError() const { return m_LastError; } // Overlay virtual void drawOverlay(QPainter *); // Overlay objects void drawStarCentroid(QPainter *); void drawTrackingBox(QPainter *); void drawMarker(QPainter *); void drawCrosshair(QPainter *); void drawEQGrid(QPainter *); 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(); void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; CursorMode getCursorMode(); void setCursorMode(CursorMode mode); void updateMouseCursor(); void updateScopeButton(); void setScopeButton(QAction *action) { centerTelescopeAction = action; } // Zoom related void cleanUpZoom(QPoint viewCenter); QPoint getImagePoint(QPoint viewPortPoint); uint16_t zoomedWidth() { return currentWidth; } uint16_t zoomedHeight() { return currentHeight; } // Star Detection int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &searchBox = QRect()); void toggleStars(bool enable); void setStarsEnabled(bool enable); void setStarsHFREnabled(bool enable); void setStarFilterRange(float const innerRadius, float const outerRadius); int filterStars(); // FITS Mode void updateMode(FITSMode fmode); FITSMode getMode() { return mode; } void setFilter(FITSScale newFilter) { filter = newFilter; } void setFirstLoad(bool value); void pushFilter(FITSScale value) { filterStack.push(value); } FITSScale popFilter() { return filterStack.pop(); } CursorMode lastMouseMode { selectCursor }; bool isStarProfileShown() { return showStarProfile; } // Floating toolbar void createFloatingToolBar(); //void setLoadWCSEnabled(bool value); + // Returns the params set to stretch the image. + StretchParams getStretchParams() const { return stretchParams; } + + // Returns true if we're automatically generating stretch parameters. + // Note: this is not whether we're stretching, that's controlled by stretchImage. + bool getAutoStretch() const { return autoStretch; } + + // Sets the params for stretching. Will also stretch and re-display the image. + // This only sets the first channel stretch params. For RGB images, the G&B channel + // stretch parameters are a function of the Red input param and the existing RGB params. + void setStretchParams(const StretchParams& params); + + // Sets whether to stretch the image or not. + // Will also re-display the image if onOff != stretchImage. + void setStretch(bool onOff); + + // Automatically generates stretch parameters and use them to re-display the image. + void setAutoStretchParams(); + + // When sampling is > 1, we will display the image at a lower resolution. + void setSampling(int value) { if (value > 0) sampling = value; } + public slots: void wheelEvent(QWheelEvent *event) override; void resizeEvent(QResizeEvent *event) override; void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); // Grids void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggleCrosshair(); // Stars void toggleStars(); void toggleStarProfile(); void viewStarProfile(); void centerTelescope(); void toggleStretch(); virtual void processPointSelection(int x, int y); virtual void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); void resizeTrackingBox(int newSize); protected slots: /** * @brief syncWCSState Update toolbar and actions depending on whether WCS is available or not */ void syncWCSState(); bool event(QEvent *event) override; bool gestureEvent(QGestureEvent *event); void pinchTriggered(QPinchGesture *gesture); protected: template bool rescale(FITSZoom type); double average(); double stddev(); void calculateMaxPixel(double min, double max); void initDisplayImage(); QPointF getPointForGridLabel(); bool pointIsInImage(QPointF pt, bool scaled); void loadInFrame(); /// WCS Future Watcher QFutureWatcher wcsWatcher; /// FITS Future Watcher QFutureWatcher fitsWatcher; /// Cross hair QPointF markerCrosshair; /// Pointer to FITSData object FITSData *imageData { nullptr }; /// Current zoom level double currentZoom { 0 }; private: bool processData(); + void doStretch(FITSData *data, QImage *outputImage); QLabel *noImageLabel { nullptr }; QPixmap noImage; QVector eqGridPoints; std::unique_ptr image_frame; - uint32_t image_width { 0 }; - uint32_t image_height { 0 }; - /// Current width due to zoom uint16_t currentWidth { 0 }; uint16_t lastWidth { 0 }; /// Current height due to zoom uint16_t currentHeight { 0 }; uint16_t lastHeight { 0 }; /// Image zoom factor const double zoomFactor; // Original full-size image QImage rawImage; // Scaled images QImage scaledImage; // Actual pixmap after all the overlays QPixmap displayPixmap; bool firstLoad { true }; bool markStars { false }; bool showStarProfile { false }; bool showCrosshair { false }; bool showObjects { false }; bool showEQGrid { false }; bool showPixelGrid { false }; bool showStarsHFR { false }; + // Should the image be displayed in linear (false) or stretched (true). + // Initial value controlled by Options::autoStretch. bool stretchImage { false }; - + + // When stretching, should we automatically compute parameters. + // When first displaying, this should be true, but may be set to false + // if the user has overridden the automatically set parameters. + bool autoStretch { true }; + + // Params for stretching image. + StretchParams stretchParams; + + // Resolution for display. Sampling=2 means display every other sample. + int sampling { 1 }; + struct { bool used() const { return innerRadius != 0.0f || outerRadius != 1.0f; } float innerRadius { 0.0f }; float outerRadius { 1.0f }; } starFilter; CursorMode cursorMode { selectCursor }; bool zooming { false }; int zoomTime { 0 }; QPoint zoomLocation; QString filename; FITSMode mode; FITSScale filter; QString m_LastError; QStack filterStack; // Tracking box bool trackingBoxEnabled { false }; QRect trackingBox; QPixmap trackingBoxPixmap; // Scope pixmap QPixmap redScopePixmap; // Magenta Scope Pixmap QPixmap magentaScopePixmap; // Floating toolbar QToolBar *floatingToolBar { nullptr }; QAction *centerTelescopeAction { nullptr }; QAction *toggleEQGridAction { nullptr }; QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; QAction *toggleStretchAction { nullptr }; //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION QPointer starProfileWidget; #endif - signals: +signals: void newStatus(const QString &msg, FITSBar id); void debayerToggled(bool); void wcsToggled(bool); void actionUpdated(const QString &name, bool enable); void trackingStarSelected(int x, int y); void loaded(); void failed(); void starProfileWindowClosed(); friend class FITSLabel; }; diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp index ba11c933d..061e933d5 100644 --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -1,1033 +1,1005 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com 2006-03-03 Using CFITSIO, Porting to Qt4 ***************************************************************************/ /*************************************************************************** * * * This program 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 "fitsviewer.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsdebayer.h" #include "fitstab.h" #include "fitsview.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #ifdef HAVE_INDI #include "indi/indilistener.h" #endif #include #include #include #include #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #define INITIAL_W 785 #define INITIAL_H 640 QStringList FITSViewer::filterTypes = QStringList() << I18N_NOOP("Auto Stretch") << I18N_NOOP("High Contrast") << I18N_NOOP("Equalize") << I18N_NOOP("High Pass") << I18N_NOOP("Median") << I18N_NOOP("Rotate Right") << I18N_NOOP("Rotate Left") << I18N_NOOP("Flip Horizontal") << I18N_NOOP("Flip Vertical"); FITSViewer::FITSViewer(QWidget *parent) : KXmlGuiWindow(parent) { #ifdef Q_OS_OSX if (Options::independentWindowFITS()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #endif fitsTabWidget = new QTabWidget(this); undoGroup = new QUndoGroup(this); lastURL = QUrl(QDir::homePath()); fitsTabWidget->setTabsClosable(true); setWindowIcon(QIcon::fromTheme("kstars_fitsviewer")); setCentralWidget(fitsTabWidget); connect(fitsTabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabFocusUpdated(int))); connect(fitsTabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int))); //These two connections will enable or disable the scope button if a scope is available or not. //Of course this is also dependent on the presence of WCS data in the image. #ifdef HAVE_INDI connect(INDIListener::Instance(), SIGNAL(newTelescope(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); connect(INDIListener::Instance(), SIGNAL(deviceRemoved(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); #endif led.setColor(Qt::green); fitsPosition.setAlignment(Qt::AlignCenter); fitsValue.setAlignment(Qt::AlignCenter); //fitsPosition.setFixedWidth(100); //fitsValue.setFixedWidth(100); fitsWCS.setVisible(false); statusBar()->insertPermanentWidget(FITS_WCS, &fitsWCS); statusBar()->insertPermanentWidget(FITS_VALUE, &fitsValue); statusBar()->insertPermanentWidget(FITS_POSITION, &fitsPosition); statusBar()->insertPermanentWidget(FITS_ZOOM, &fitsZoom); statusBar()->insertPermanentWidget(FITS_RESOLUTION, &fitsResolution); statusBar()->insertPermanentWidget(FITS_LED, &led); QAction *action = actionCollection()->addAction("rotate_right", this, SLOT(rotateCW())); action->setText(i18n("Rotate Right")); action->setIcon(QIcon::fromTheme("object-rotate-right")); action = actionCollection()->addAction("rotate_left", this, SLOT(rotateCCW())); action->setText(i18n("Rotate Left")); action->setIcon(QIcon::fromTheme("object-rotate-left")); action = actionCollection()->addAction("flip_horizontal", this, SLOT(flipHorizontal())); action->setText(i18n("Flip Horizontal")); action->setIcon( QIcon::fromTheme("object-flip-horizontal")); action = actionCollection()->addAction("flip_vertical", this, SLOT(flipVertical())); action->setText(i18n("Flip Vertical")); action->setIcon(QIcon::fromTheme("object-flip-vertical")); action = actionCollection()->addAction("image_histogram"); action->setText(i18n("Histogram")); connect(action, SIGNAL(triggered(bool)), SLOT(histoFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); action->setIcon(QIcon(":/icons/histogram.png")); action = KStandardAction::open(this, SLOT(openFile()), actionCollection()); action->setIcon(QIcon::fromTheme("document-open")); saveFileAction = KStandardAction::save(this, SLOT(saveFile()), actionCollection()); saveFileAction->setIcon(QIcon::fromTheme("document-save")); saveFileAsAction = KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection()); saveFileAsAction->setIcon( QIcon::fromTheme("document-save_as")); action = actionCollection()->addAction("fits_header"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_H)); action->setIcon(QIcon::fromTheme("document-properties")); action->setText(i18n("FITS Header")); connect(action, SIGNAL(triggered(bool)), SLOT(headerFITS())); action = actionCollection()->addAction("fits_debayer"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D)); action->setIcon(QIcon::fromTheme("view-preview")); action->setText(i18n("Debayer...")); connect(action, SIGNAL(triggered(bool)), SLOT(debayerFITS())); - action = actionCollection()->addAction("image_stretch"); - 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 = KStandardAction::close(this, SLOT(close()), actionCollection()); action->setIcon(QIcon::fromTheme("window-close")); action = KStandardAction::copy(this, SLOT(copyFITS()), actionCollection()); action->setIcon(QIcon::fromTheme("edit-copy")); action = KStandardAction::zoomIn(this, SLOT(ZoomIn()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-in")); action = KStandardAction::zoomOut(this, SLOT(ZoomOut()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-out")); action = KStandardAction::actualSize(this, SLOT(ZoomDefault()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-fit-best")); QAction *kundo = KStandardAction::undo(undoGroup, SLOT(undo()), actionCollection()); kundo->setIcon(QIcon::fromTheme("edit-undo")); QAction *kredo = KStandardAction::redo(undoGroup, SLOT(redo()), actionCollection()); kredo->setIcon(QIcon::fromTheme("edit-redo")); connect(undoGroup, SIGNAL(canUndoChanged(bool)), kundo, SLOT(setEnabled(bool))); connect(undoGroup, SIGNAL(canRedoChanged(bool)), kredo, SLOT(setEnabled(bool))); action = actionCollection()->addAction("image_stats"); action->setIcon(QIcon::fromTheme("view-statistics")); action->setText(i18n("Statistics")); connect(action, SIGNAL(triggered(bool)), SLOT(statFITS())); action = actionCollection()->addAction("view_crosshair"); action->setIcon(QIcon::fromTheme("crosshairs")); action->setText(i18n("Show Cross Hairs")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleCrossHair())); action = actionCollection()->addAction("view_pixel_grid"); action->setIcon(QIcon::fromTheme("map-flat")); action->setText(i18n("Show Pixel Gridlines")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(togglePixelGrid())); action = actionCollection()->addAction("view_eq_grid"); action->setIcon(QIcon::fromTheme("kstars_grid")); action->setText(i18n("Show Equatorial Gridlines")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleEQGrid())); action = actionCollection()->addAction("view_objects"); action->setIcon(QIcon::fromTheme("help-hint")); action->setText(i18n("Show Objects in Image")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleObjects())); action = actionCollection()->addAction("center_telescope"); action->setIcon(QIcon(":/icons/center_telescope.svg")); action->setText(i18n("Center Telescope\n*No Telescopes Detected*")); action->setDisabled(true); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(centerTelescope())); action = actionCollection()->addAction("view_zoom_fit"); action->setIcon(QIcon::fromTheme("zoom-fit-width")); action->setText(i18n("Zoom To Fit")); connect(action, SIGNAL(triggered(bool)), SLOT(ZoomToFit())); #ifdef HAVE_DATAVISUALIZATION action = actionCollection()->addAction("toggle_3D_graph"); action->setIcon(QIcon::fromTheme("star_profile", QIcon(":/icons/star_profile.svg"))); action->setText(i18n("View 3D Graph")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggle3DGraph())); #endif action = actionCollection()->addAction("mark_stars"); action->setText(i18n("Mark Stars")); connect(action, SIGNAL(triggered(bool)), SLOT(toggleStars())); int filterCounter = 1; for (auto& filter : FITSViewer::filterTypes) { action = actionCollection()->addAction(QString("filter%1").arg(filterCounter)); action->setText(i18n(filter.toUtf8().constData())); connect(action, &QAction::triggered, this, [this, filterCounter] { applyFilter(filterCounter);}); filterCounter++; } /* Create GUI */ createGUI("fitsviewerui.rc"); setWindowTitle(i18n("KStars FITS Viewer")); /* initially resize in accord with KDE rules */ show(); resize(INITIAL_W, INITIAL_H); } void FITSViewer::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } FITSViewer::~FITSViewer() { // if (KStars::Instance()) // { // for (QPointer fv : KStars::Instance()->getFITSViewersList()) // { // if (fv.data() == this) // { // KStars::Instance()->getFITSViewersList().removeOne(this); // break; // } // } // } fitsTabWidget->disconnect(); qDeleteAll(fitsTabs); fitsTabs.clear(); } void FITSViewer::closeEvent(QCloseEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); QList viewers = KStars::Instance()->findChildren(); if (a && viewers.count() == 1) { a->setEnabled(false); a->setChecked(false); } } } void FITSViewer::hideEvent(QHideEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { QList viewers = KStars::Instance()->findChildren(); if (viewers.count() <= 1) a->setChecked(false); } } } void FITSViewer::showEvent(QShowEvent * /*event*/) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { a->setEnabled(true); a->setChecked(true); } } bool FITSViewer::addFITSCommon(FITSTab *tab, const QUrl &imageName, FITSMode mode, const QString &previewText) { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex != -1) return false; lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); QApplication::restoreOverrideCursor(); tab->setPreviewText(previewText); // Connect tab signals connect(tab, &FITSTab::newStatus, this, &FITSViewer::updateStatusBar); connect(tab, &FITSTab::changeStatus, this, &FITSViewer::updateTabStatus); connect(tab, &FITSTab::debayerToggled, this, &FITSViewer::setDebayerAction); // Connect tab view signals connect(tab->getView(), &FITSView::actionUpdated, this, &FITSViewer::updateAction); connect(tab->getView(), &FITSView::wcsToggled, this, &FITSViewer::updateWCSFunctions); connect(tab->getView(),&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); switch (mode) { case FITS_NORMAL: fitsTabWidget->addTab(tab, previewText.isEmpty() ? imageName.fileName() : previewText); break; case FITS_CALIBRATE: fitsTabWidget->addTab(tab, i18n("Calibrate")); break; case FITS_FOCUS: fitsTabWidget->addTab(tab, i18n("Focus")); break; case FITS_GUIDE: fitsTabWidget->addTab(tab, i18n("Guide")); break; case FITS_ALIGN: fitsTabWidget->addTab(tab, i18n("Align")); break; } saveFileAction->setEnabled(true); saveFileAsAction->setEnabled(true); undoGroup->addStack(tab->getUndoStack()); fitsTabs.push_back(tab); fitsMap[fitsID] = tab; fitsTabWidget->setCurrentWidget(tab); actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); tab->tabPositionUpdated(); tab->setUID(fitsID); led.setColor(Qt::green); updateStatusBar(i18n("Ready."), FITS_MESSAGE); tab->getView()->setCursorMode(FITSView::dragCursor); - updateButtonStatus("image_stretch", i18n("Toggle Auto stretch"), tab->getView()->isImageStretched()); - updateWCSFunctions(); return true; } void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); connect(tab, &FITSTab::failed, [&]() { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); }); connect(tab, &FITSTab::loaded, [=]() { if (addFITSCommon(tab, imageName, mode, previewText)) emit loaded(fitsID++); }); tab->loadFITS(imageName, mode, filter, silent); } bool FITSViewer::addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, FITSMode mode, FITSScale filter, const QString &previewText) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); if (!tab->loadFITSFromData(data, imageName, mode, filter)) { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); return false; } if (!addFITSCommon(tab, imageName, mode, previewText)) return false; *tab_uid = fitsID++; return true; } bool FITSViewer::removeFITS(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; return false; } int index = fitsTabs.indexOf(tab); if (index >= 0) { closeTab(index); return true; } return false; } void FITSViewer::updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter, bool silent) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; emit failed(); return; } if (tab->isVisible()) led.setColor(Qt::yellow); // On tab load success auto conn = std::make_shared(); *conn = connect(tab, &FITSTab::loaded, this, [=]() { if (updateFITSCommon(tab, imageName)) { QObject::disconnect(*conn); emit loaded(tab->getUID()); } }); tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); } bool FITSViewer::updateFITSCommon(FITSTab *tab, const QUrl &imageName) { // On tab load success int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex == -1) return false; if (tab->getView()->getMode() == FITS_NORMAL) { if ((imageName.path().startsWith(QLatin1String("/tmp")) || imageName.path().contains("/Temp")) && Options::singlePreviewFITS()) fitsTabWidget->setTabText(tabIndex, tab->getPreviewText().isEmpty() ? i18n("Preview") : tab->getPreviewText()); else fitsTabWidget->setTabText(tabIndex, imageName.fileName()); } tab->getUndoStack()->clear(); if (tab->isVisible()) led.setColor(Qt::green); return true; } bool FITSViewer::updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, int *tab_uid, FITSScale filter) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) return false; if (tab->isVisible()) led.setColor(Qt::yellow); if (!tab->loadFITSFromData(data, imageName, tab->getView()->getMode(), filter)) return false; if (!updateFITSCommon(tab, imageName)) return false; *tab_uid = tab->getUID(); return true; } void FITSViewer::tabFocusUpdated(int currentIndex) { if (currentIndex < 0 || fitsTabs.empty()) return; fitsTabs[currentIndex]->tabPositionUpdated(); FITSView *view = fitsTabs[currentIndex]->getView(); view->toggleStars(markStars); if (isVisible()) view->updateFrame(); if (markStars) updateStatusBar(i18np("%1 star detected.", "%1 stars detected.", view->getImageData()->getDetectedStars()), FITS_MESSAGE); else updateStatusBar("", FITS_MESSAGE); if (view->getImageData()->hasDebayer()) { actionCollection()->action("fits_debayer")->setEnabled(true); if (debayerDialog) { BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); } } else actionCollection()->action("fits_debayer")->setEnabled(false); updateStatusBar("", FITS_WCS); connect(view,&FITSView::starProfileWindowClosed, this, &FITSViewer::starProfileButtonOff); updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), getCurrentView()->isStarProfileShown()); updateButtonStatus("view_crosshair", i18n("Cross Hairs"), getCurrentView()->isCrosshairShown()); 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(); } void FITSViewer::starProfileButtonOff() { updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), false); } void FITSViewer::openFile() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open FITS Image"), lastURL, "FITS (*.fits *.fits.fz *.fit *.fts)"); if (fileURL.isEmpty()) return; lastURL = QUrl(fileURL.url(QUrl::RemoveFilename)); QString fpath = fileURL.toLocalFile(); QString cpath; // Make sure we don't have it open already, if yes, switch to it foreach (FITSTab *tab, fitsTabs) { cpath = tab->getCurrentURL()->path(); if (fpath == cpath) { fitsTabWidget->setCurrentWidget(tab); return; } } addFITS(fileURL, FITS_NORMAL, FITS_NONE, QString(), false); } void FITSViewer::saveFile() { fitsTabs[fitsTabWidget->currentIndex()]->saveFile(); } void FITSViewer::saveFileAs() { if (fitsTabs.empty()) return; if (fitsTabs[fitsTabWidget->currentIndex()]->saveFileAs() && fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() == FITS_NORMAL) fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), fitsTabs[fitsTabWidget->currentIndex()]->getCurrentURL()->fileName()); } void FITSViewer::copyFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->copyFITS(); } void FITSViewer::histoFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->histoFITS(); } void FITSViewer::statFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->statFITS(); } void FITSViewer::rotateCW() { applyFilter(FITS_ROTATE_CW); } void FITSViewer::rotateCCW() { applyFilter(FITS_ROTATE_CCW); } void FITSViewer::flipHorizontal() { applyFilter(FITS_FLIP_H); } void FITSViewer::flipVertical() { applyFilter(FITS_FLIP_V); } void FITSViewer::headerFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->headerFITS(); } void FITSViewer::debayerFITS() { if (debayerDialog == nullptr) { debayerDialog = new FITSDebayer(this); } FITSView *view = getCurrentView(); if (view == nullptr) return; BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); debayerDialog->show(); } void FITSViewer::updateStatusBar(const QString &msg, FITSBar id) { switch (id) { case FITS_POSITION: fitsPosition.setText(msg); break; case FITS_RESOLUTION: fitsResolution.setText(msg); break; case FITS_ZOOM: fitsZoom.setText(msg); break; case FITS_WCS: fitsWCS.setVisible(true); fitsWCS.setText(msg); break; case FITS_VALUE: fitsValue.setText(msg); break; case FITS_MESSAGE: statusBar()->showMessage(msg); break; default: break; } } void FITSViewer::ZoomIn() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomIn(); } void FITSViewer::ZoomOut() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomOut(); } void FITSViewer::ZoomDefault() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomDefault(); } void FITSViewer::ZoomToFit() { if (fitsTabs.empty()) return; getCurrentView()->ZoomToFit(); } void FITSViewer::updateAction(const QString &name, bool enable) { QAction *toolAction = actionCollection()->action(name); if (toolAction != nullptr) toolAction->setEnabled(enable); } void FITSViewer::updateTabStatus(bool clean) { if (fitsTabs.empty() || (fitsTabWidget->currentIndex() >= fitsTabs.size())) return; if (fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() != FITS_NORMAL) return; //QString tabText = fitsImages[fitsTab->currentIndex()]->getCurrentURL()->fileName(); QString tabText = fitsTabWidget->tabText(fitsTabWidget->currentIndex()); fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), clean ? tabText.remove('*') : tabText + '*'); } void FITSViewer::closeTab(int index) { if (fitsTabs.empty()) return; FITSTab *tab = fitsTabs[index]; int UID = tab->getUID(); fitsMap.remove(UID); fitsTabs.removeOne(tab); delete tab; if (fitsTabs.empty()) { saveFileAction->setEnabled(false); saveFileAsAction->setEnabled(false); } emit closed(UID); } /** This is helper function to make it really easy to make the update the state of toggle buttons that either show or hide information in the Current view. This method would get called both when one of them gets pushed and also when tabs are switched. */ void FITSViewer::updateButtonStatus(const QString& action, const QString& item, bool showing) { QAction *a = actionCollection()->action(action); if (a == nullptr) return; if (showing) { a->setText(i18n("Hide %1", item)); a->setChecked(true); } else { a->setText(i18n("Show %1", item)); a->setChecked(false); } } /** This is a method that either enables or disables the WCS based features in the Current View. */ void FITSViewer::updateWCSFunctions() { if (getCurrentView() == nullptr) return; if (getCurrentView()->imageHasWCS()) { actionCollection()->action("view_eq_grid")->setDisabled(false); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines")); actionCollection()->action("view_objects")->setDisabled(false); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image")); if (getCurrentView()->isTelescopeActive()) { actionCollection()->action("center_telescope")->setDisabled(false); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*Ready*")); } else { actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No Telescopes Detected*")); } } else { actionCollection()->action("view_eq_grid")->setDisabled(true); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines\n*No WCS Info*")); actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No WCS Info*")); actionCollection()->action("view_objects")->setDisabled(true); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image\n*No WCS Info*")); } } void FITSViewer::updateScopeButton() { if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { actionCollection()->action("center_telescope")->setChecked(true); } else { actionCollection()->action("center_telescope")->setChecked(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSViewer::centerTelescope() { getCurrentView()->setScopeButton(actionCollection()->action("center_telescope")); if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { getCurrentView()->setCursorMode(getCurrentView()->lastMouseMode); } else { getCurrentView()->lastMouseMode = getCurrentView()->getCursorMode(); getCurrentView()->setCursorMode(FITSView::scopeCursor); } updateScopeButton(); } void FITSViewer::toggleCrossHair() { if (fitsTabs.empty()) return; getCurrentView()->toggleCrosshair(); updateButtonStatus("view_crosshair", i18n("Cross Hairs"), getCurrentView()->isCrosshairShown()); } void FITSViewer::toggleEQGrid() { if (fitsTabs.empty()) return; getCurrentView()->toggleEQGrid(); updateButtonStatus("view_eq_grid", i18n("Equatorial Gridlines"), getCurrentView()->isEQGridShown()); } void FITSViewer::toggleObjects() { if (fitsTabs.empty()) return; getCurrentView()->toggleObjects(); updateButtonStatus("view_objects", i18n("Objects in Image"), getCurrentView()->areObjectsShown()); } void FITSViewer::togglePixelGrid() { if (fitsTabs.empty()) return; getCurrentView()->togglePixelGrid(); updateButtonStatus("view_pixel_grid", i18n("Pixel Gridlines"), getCurrentView()->isPixelGridShown()); } void FITSViewer::toggle3DGraph() { if (fitsTabs.empty()) return; getCurrentView()->toggleStarProfile(); updateButtonStatus("toggle_3D_graph", i18n("View 3D Graph"), getCurrentView()->isStarProfileShown()); } void FITSViewer::toggleStars() { if (markStars) { markStars = false; actionCollection()->action("mark_stars")->setText(i18n("Mark Stars")); } else { markStars = true; actionCollection()->action("mark_stars")->setText(i18n("Unmark Stars")); } foreach (FITSTab *tab, fitsTabs) { tab->getView()->toggleStars(markStars); tab->getView()->updateFrame(); } } void FITSViewer::applyFilter(int ftype) { if (fitsTabs.empty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); updateStatusBar(i18n("Processing %1...", filterTypes[ftype - 1]), FITS_MESSAGE); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getHistogram()->applyFilter(static_cast(ftype)); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getView()->updateFrame(); QApplication::restoreOverrideCursor(); 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); if (tab) return tab->getView(); return nullptr; } FITSView *FITSViewer::getCurrentView() { if (fitsTabs.empty() || fitsTabWidget->currentIndex() >= fitsTabs.count()) return nullptr; return fitsTabs[fitsTabWidget->currentIndex()]->getView(); } void FITSViewer::setDebayerAction(bool enable) { actionCollection()->addAction("fits_debayer")->setEnabled(enable); } diff --git a/kstars/fitsviewer/fitsviewer.h b/kstars/fitsviewer/fitsviewer.h index 5374b00c3..e1b672cd8 100644 --- a/kstars/fitsviewer/fitsviewer.h +++ b/kstars/fitsviewer/fitsviewer.h @@ -1,163 +1,162 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program 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. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "fitscommon.h" #include #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include class QCloseEvent; class QUndoGroup; class QTabWidget; class FITSDebayer; class FITSTab; class FITSView; class FITSData; /** * @class FITSViewer * @short Primary window to view monochrome and color FITS images. * The FITSviewer can open multiple images each in a separate. It supports simple filters, histogram transforms, flip and rotation operations, and star detection. * * @author Jasem Mutlaq * @version 1.0 */ class FITSViewer : public KXmlGuiWindow { Q_OBJECT public: /** Constructor. */ explicit FITSViewer(QWidget *parent); ~FITSViewer(); void addFITS(const QUrl &imageName, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString(), bool silent = true); bool addFITSFromData(FITSData *data, const QUrl &imageName, int *tab_uid, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE, const QString &previewText = QString()); void updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter = FITS_NONE, bool silent = true); bool updateFITSFromData(FITSData *data, const QUrl &imageName, int fitsUID, int *tab_uid, FITSScale filter = FITS_NONE); bool removeFITS(int fitsUID); bool isStarsMarked() { return markStars; } bool empty() const { return fitsTabs.empty(); } QList getTabs() { return fitsTabs; } FITSView *getView(int fitsUID); FITSView *getCurrentView(); static QStringList filterTypes; protected: void closeEvent(QCloseEvent *) override; void hideEvent(QHideEvent *) override; void showEvent(QShowEvent *) override; public slots: void changeAlwaysOnTop(Qt::ApplicationState state); void openFile(); void saveFile(); void saveFileAs(); void copyFITS(); void statFITS(); void headerFITS(); void debayerFITS(); void histoFITS(); void tabFocusUpdated(int currentIndex); void updateStatusBar(const QString &msg, FITSBar id); void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); void updateAction(const QString &name, bool enable); void updateTabStatus(bool clean); void closeTab(int index); void toggleStars(); void toggleCrossHair(); void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggle3DGraph(); void starProfileButtonOff(); void centerTelescope(); void updateWCSFunctions(); void applyFilter(int ftype); - void toggleStretch(); void rotateCW(); void rotateCCW(); void flipHorizontal(); void flipVertical(); void setDebayerAction(bool); void updateScopeButton(); private: void updateButtonStatus(const QString &action, const QString &item, bool showing); // Shared utilites between the standard and "FromData" addFITS and updateFITS. bool addFITSCommon(FITSTab *tab, const QUrl &imageName, FITSMode mode, const QString &previewText); bool updateFITSCommon(FITSTab *tab, const QUrl &imageName); QTabWidget *fitsTabWidget { nullptr }; QUndoGroup *undoGroup { nullptr }; FITSDebayer *debayerDialog { nullptr }; KLed led; QLabel fitsPosition, fitsValue, fitsResolution, fitsZoom, fitsWCS; QAction *saveFileAction { nullptr }; QAction *saveFileAsAction { nullptr }; QList fitsTabs; int fitsID { 0 }; bool markStars { false }; QMap fitsMap; QUrl lastURL; signals: void trackingStarSelected(int x, int y); void loaded(int tabUID); void closed(int tabUID); void failed(); }; diff --git a/kstars/fitsviewer/stretch.cpp b/kstars/fitsviewer/stretch.cpp index 2966a4ef3..75d291b2d 100644 --- a/kstars/fitsviewer/stretch.cpp +++ b/kstars/fitsviewer/stretch.cpp @@ -1,415 +1,442 @@ #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. +// Returns the median value 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 rough max of the buffer. +template +T sampledMax(T *values, int size, int sampleBy) +{ + T maxVal = 0; + for (int i = 0; i < size; i+= sampleBy) + if (maxVal < values[i]) + maxVal = values[i]; + return maxVal; +} + // 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; i < downsampled_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. +// Sampling is applied to the output (that is, with sampling=2, we compute every other output +// sample both in width and height, so the output would have about 4X fewer pixels. template void stretchOneChannel(T *input_buffer, QImage *output_image, const StretchParams& stretch_params, - int input_range, int image_height, int image_width) + int input_range, int image_height, int image_width, int sampling) { 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); + const float hsRangeFactor = highlights == shadows ? 1.0f : 1.0f / (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++) + // Increment the input index by the sampling, the output index increments by 1. + for (int j = 0, jout = 0; j < image_height; j+=sampling, jout++) { futures.append(QtConcurrent::run([ = ]() { T * inputLine = input_buffer + j * image_width; - auto * scanLine = output_image->scanLine(j); + auto * scanLine = output_image->scanLine(jout); - for (int i = 0; i < image_width; i++) + for (int i = 0, iout = 0; i < image_width; i+=sampling, iout++) { const T input = inputLine[i]; - if (input < nativeShadows) scanLine[i] = 0; - else if (input >= nativeHighlights) scanLine[i] = maxOutput; + if (input < nativeShadows) scanLine[iout] = 0; + else if (input >= nativeHighlights) scanLine[iout] = maxOutput; else { const T inputFloored = (input - nativeShadows); - scanLine[i] = (inputFloored * k1) / (inputFloored * k2 - midtones); + scanLine[iout] = (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. +// Sampling is applied to the output (that is, with sampling=2, we compute every other output +// sample both in width and height, so the output would have about 4X fewer pixels. template void stretchThreeChannels(T *inputBuffer, QImage *outputImage, const StretchParams& stretchParams, - int inputRange, int imageHeight, int imageWidth) + int inputRange, int imageHeight, int imageWidth, int sampling) { 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); + const float hsRangeFactorR = highlightsR == shadowsR ? 1.0f : 1.0f / (highlightsR - shadowsR); + const float hsRangeFactorG = highlightsG == shadowsG ? 1.0f : 1.0f / (highlightsG - shadowsG); + const float hsRangeFactorB = highlightsB == shadowsB ? 1.0f : 1.0f / (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++) + for (int j = 0, jout = 0; j < imageHeight; j+=sampling, jout++) { 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)); + auto * scanLine = reinterpret_cast(outputImage->scanLine(jout)); - for (int i = 0; i < imageWidth; i++) + for (int i = 0, iout = 0; i < imageWidth; i+=sampling, iout++) { 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); + scanLine[iout] = 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) + int input_range, int image_height, int image_width, int num_channels, int sampling) { if (num_channels == 1) stretchOneChannel(input_buffer, output_image, stretch_params, input_range, - image_height, image_width); + image_height, image_width, sampling); else if (num_channels == 3) stretchThreeChannels(input_buffer, output_image, stretch_params, input_range, - image_height, image_width); + image_height, image_width, sampling); } // 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); + 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; i < numSamples; ++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; + if (X == 0) midtones = 0.0f; + else if (X == M) midtones = 0.5f; + else if (X == 1) midtones = 1.0f; 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) +void Stretch::run(uint8_t *input, QImage *outputImage, int sampling) { + Q_ASSERT(outputImage->width() == (image_width + sampling - 1) / sampling); + Q_ASSERT(outputImage->height() == (image_height + sampling - 1) / sampling); + recalculateInputRange(input); + switch (dataType) { case TBYTE: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TSHORT: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TUSHORT: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TLONG: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TFLOAT: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TLONGLONG: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; case TDOUBLE: stretchChannels(reinterpret_cast(input), outputImage, params, - input_range, image_height, image_width, image_channels); + input_range, image_height, image_width, image_channels, sampling); break; default: break; } } +// The input range for float/double is ambiguous, and we can't tell without the buffer, +// so we set it to 64K and possibly reduce it when we see the data. +void Stretch::recalculateInputRange(uint8_t *input) +{ + if (input_range <= 1) return; + if (dataType != TFLOAT && dataType != TDOUBLE) return; + + float mx = 0; + if (dataType == TFLOAT) + mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); + else if (dataType == TDOUBLE) + mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); + if (mx <= 1.01f) input_range = 1; +} + StretchParams Stretch::computeParams(uint8_t *input) { + recalculateInputRange(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; } diff --git a/kstars/fitsviewer/stretch.h b/kstars/fitsviewer/stretch.h index 3a2ca32c0..dca625537 100644 --- a/kstars/fitsviewer/stretch.h +++ b/kstars/fitsviewer/stretch.h @@ -1,90 +1,99 @@ /* 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. + * @param input the raw data buffer. + * @param output_image a QImage pointer that should be the same size as the input if + * sampling is 1 otherwise, the proper size if the input is downsampled by sampling. + * @param sampling The sampling parameter. Applies to both width and height. + * Sampling is applied to the output (that is, with sampling=2, we compute every other output + * sample both in width and height, so the output would have about 4X fewer pixels. */ - void run(uint8_t *input, QImage *output_image); + void run(uint8_t *input, QImage *output_image, int sampling=1); private: + // Adjusts input_range for float and double types. + void recalculateInputRange(uint8_t *input); + // Inputs. int image_width; int image_height; int image_channels; int input_range; int dataType; // Parameters. StretchParams params; };