diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index 5bdd2b39b..d868fce93 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3105 +1,3150 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq 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 "focus.h" #include "focusadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "indi/indifilter.h" #include "ksnotification.h" #include #include #include #include #include #define FOCUS_TIMEOUT_THRESHOLD 120000 #define MAXIMUM_ABS_ITERATIONS 30 #define MAXIMUM_RESET_ITERATIONS 2 #define AUTO_STAR_TIMEOUT 45000 #define MINIMUM_PULSE_TIMER 32 #define MAX_RECAPTURE_RETRIES 3 #define MINIMUM_POLY_SOLUTIONS 2 namespace Ekos { Focus::Focus() { setupUi(this); qRegisterMetaType("Ekos::FocusState"); qDBusRegisterMetaType(); new FocusAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this); //frameModified = false; waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT); connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo); //fy=fw=fh=0; HFRFrames.clear(); FilterDevicesCombo->addItem("--"); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer); toggleFullScreenB->setIcon( QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen); // Exposure Timeout captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout); connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start); connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::checkStopFocus); - connect(focusOutB, &QPushButton::clicked, [&]() { + connect(focusOutB, &QPushButton::clicked, [&]() + { focusOut(); }); - connect(focusInB, &QPushButton::clicked, [&]() { + connect(focusInB, &QPushButton::clicked, [&]() + { focusIn(); }); connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture); connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming); connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::toggleSubframe); connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame); - connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::setDefaultCCD); + connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkCCD); connect(useFullField, &QCheckBox::toggled, [&](bool toggled) { Options::setFocusUseFullField(toggled); + fullFieldInnerRing->setEnabled(toggled); + fullFieldOuterRing->setEnabled(toggled); if (toggled) { useSubFrame->setChecked(false); useAutoStar->setChecked(false); } + else + { + // Disable the overlay + focusView->setStarFilterRange(0, 1); + } + }); + + connect(fullFieldInnerRing, static_cast(&QDoubleSpinBox::valueChanged), + [ = ](double d) + { + Options::setFocusFullFieldInnerRadius(d); + }); + connect(fullFieldOuterRing, static_cast(&QDoubleSpinBox::valueChanged), + [ = ](double d) + { + Options::setFocusFullFieldOuterRadius(d); }); FocusSettleTime->setValue(Options::focusSettleTime()); connect(FocusSettleTime, static_cast(&QDoubleSpinBox::valueChanged), - [=](double d) { Options::setFocusSettleTime(d); }); + [ = ](double d) + { + Options::setFocusSettleTime(d); + }); GuideSettleTime->setValue(Options::guideSettleTime()); connect(GuideSettleTime, static_cast(&QDoubleSpinBox::valueChanged), - [=](double d) { Options::setGuideSettleTime(d); }); + [ = ](double d) + { + Options::setGuideSettleTime(d); + }); - connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::setDefaultFocuser); + connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::setDefaultFocuser); connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFocuser); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFilter); connect(setAbsTicksB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::setActiveBinning); connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Ekos::Focus::updateBoxSize); focusDetection = static_cast(Options::focusDetection()); focusDetectionCombo->setCurrentIndex(focusDetection); - connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, [&](int index) { + connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, [&](int index) + { focusDetection = static_cast(index); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); Options::setFocusDetection(index); }); focusAlgorithm = static_cast(Options::focusAlgorithm()); focusAlgorithmCombo->setCurrentIndex(focusAlgorithm); - connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, [&](int index) { + connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, [&](int index) + { focusAlgorithm = static_cast(index); //toleranceIN->setEnabled(focusAlgorithm == FOCUS_ITERATIVE); Options::setFocusAlgorithm(index); }); activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); focusFramesSpin->setValue(Options::focusFramesCount()); connect(clearDataB, &QPushButton::clicked, this, &Ekos::Focus::clearDataPoints); profileDialog = new QDialog(this); profileDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); QVBoxLayout *profileLayout = new QVBoxLayout(profileDialog); profileDialog->setWindowTitle(i18n("Relative Profile")); profilePlot = new QCustomPlot(profileDialog); profilePlot->setBackground(QBrush(Qt::black)); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickLabelColor(Qt::white); profilePlot->yAxis->setTickLabelColor(Qt::white); profilePlot->xAxis->setLabelColor(Qt::white); profilePlot->yAxis->setLabelColor(Qt::white); profileLayout->addWidget(profilePlot); profileDialog->setLayout(profileLayout); profileDialog->resize(400, 300); connect(relativeProfileB, &QPushButton::clicked, profileDialog, &QDialog::show); currentGaus = profilePlot->addGraph(); currentGaus->setLineStyle(QCPGraph::lsLine); currentGaus->setPen(QPen(Qt::red, 2)); lastGaus = profilePlot->addGraph(); lastGaus->setLineStyle(QCPGraph::lsLine); QPen pen(Qt::darkGreen); pen.setStyle(Qt::DashLine); pen.setWidth(2); lastGaus->setPen(pen); HFRPlot->setBackground(QBrush(Qt::black)); HFRPlot->xAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->yAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickLabelColor(Qt::white); HFRPlot->yAxis->setTickLabelColor(Qt::white); HFRPlot->xAxis->setLabelColor(Qt::white); HFRPlot->yAxis->setLabelColor(Qt::white); HFRPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->setLabel(i18n("HFR")); HFRPlot->setInteractions(QCP::iRangeZoom); HFRPlot->setInteraction(QCP::iRangeDrag, true); v_graph = HFRPlot->addGraph(); v_graph->setLineStyle(QCPGraph::lsNone); v_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::red, 3)); resetButtons(); appendLogText(i18n("Idle.")); for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); filterCombo->setCurrentIndex(Options::focusEffect()); defaultScale = static_cast(Options::focusEffect()); connect(filterCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::filterChangeWarning); exposureIN->setValue(Options::focusExposure()); toleranceIN->setValue(Options::focusTolerance()); stepIN->setValue(Options::focusTicks()); useAutoStar->setChecked(Options::focusAutoStarEnabled()); focusBoxSize->setValue(Options::focusBoxSize()); if (Options::focusMaxTravel() > maxTravelIN->maximum()) maxTravelIN->setMaximum(Options::focusMaxTravel()); maxTravelIN->setValue(Options::focusMaxTravel()); useSubFrame->setChecked(Options::focusSubFrame()); suspendGuideCheck->setChecked(Options::suspendGuiding()); darkFrameCheck->setChecked(Options::useFocusDarkFrame()); thresholdSpin->setValue(Options::focusThreshold()); useFullField->setChecked(Options::focusUseFullField()); + fullFieldInnerRing->setValue(Options::focusFullFieldInnerRadius()); + fullFieldOuterRing->setValue(Options::focusFullFieldOuterRadius()); //focusFramesSpin->setValue(Options::focusFrames()); connect(thresholdSpin, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Focus::setThreshold); focusView = new FITSView(focusingWidget, FITS_FOCUS); focusView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); focusView->setBaseSize(focusingWidget->size()); focusView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(focusView); focusingWidget->setLayout(vlayout); connect(focusView, &FITSView::trackingStarSelected, this, &Ekos::Focus::focusStarSelected, Qt::UniqueConnection); focusView->setStarsEnabled(true); // Reset star center on auto star check toggle - connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled) { + connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled) + { if (enabled) { starCenter = QVector3D(); starSelected = false; focusView->setTrackingBox(QRect()); } }); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Focus::~Focus() { if (focusingWidget->parent() == nullptr) toggleFocusingWidgetFullScreen(); } void Focus::resetFrame() { if (currentCCD) { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip) { //fx=fy=fw=fh=0; targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" << 1; QVariantMap settings; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; starSelected = false; starCenter = QVector3D(); subFramed = false; focusView->setTrackingBox(QRect()); } } } bool Focus::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } void Focus::setDefaultCCD(QString ccd) { Options::setDefaultFocusCCD(ccd); } void Focus::setDefaultFocuser(QString focuser) { Options::setDefaultFocusFocuser(focuser); } QString Focus::camera() { - if (currentCCD) - return currentCCD->getDeviceName(); + if (currentCCD) + return currentCCD->getDeviceName(); - return QString(); + return QString(); } void Focus::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum >= 0 && ccdNum <= CCDs.count()) { currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; for (ISD::CCD *oneCCD : CCDs) { if (oneCCD == currentCCD) continue; if (captureInProgress == false) oneCCD->disconnect(this); } if (targetChip) { targetChip->setImageView(focusView, FITS_FOCUS); binningCombo->setEnabled(targetChip->canBin()); useSubFrame->setEnabled(targetChip->canSubframe()); if (targetChip->canBin()) { int subBinX = 1, subBinY = 1; binningCombo->clear(); targetChip->getMaxBin(&subBinX, &subBinY); for (int i = 1; i <= subBinX; i++) binningCombo->addItem(QString("%1x%2").arg(i).arg(i)); binningCombo->setCurrentIndex(activeBin - 1); } QStringList isoList = targetChip->getISOList(); ISOCombo->clear(); if (isoList.isEmpty()) { ISOCombo->setEnabled(false); ISOLabel->setEnabled(false); } else { ISOCombo->setEnabled(true); ISOLabel->setEnabled(true); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); } connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); bool hasGain = currentCCD->hasGain(); gainLabel->setEnabled(hasGain); gainIN->setEnabled(hasGain && currentCCD->getGainPermission() != IP_RO); if (hasGain) { double gain = 0, min = 0, max = 0, step = 1; currentCCD->getGainMinMaxStep(&min, &max, &step); if (currentCCD->getGain(&gain)) { gainIN->setMinimum(min); gainIN->setMaximum(max); if (step > 0) gainIN->setSingleStep(step); gainIN->setValue(gain); } } else gainIN->clear(); } } syncCCDInfo(); } void Focus::syncCCDInfo() { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useSubFrame->setEnabled(targetChip->canSubframe()); if (frameSettings.contains(targetChip) == false) { int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { int binx = 1, biny = 1; targetChip->getBinning(&binx, &biny); if (w > 0 && h > 0) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); QVariantMap settings; settings["x"] = useSubFrame->isChecked() ? x : minX; settings["y"] = useSubFrame->isChecked() ? y : minY; settings["w"] = useSubFrame->isChecked() ? w : maxW; settings["h"] = useSubFrame->isChecked() ? h : maxH; settings["binx"] = binx; settings["biny"] = biny; frameSettings[targetChip] = settings; } } } } void Focus::addFilter(ISD::GDInterface *newFilter) { foreach (ISD::GDInterface *filter, Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); filterManagerB->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); } bool Focus::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Focus::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Focus::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Focus::filter() { return FilterPosCombo->currentText(); } void Focus::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; - currentFilterPosition=-1; + currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) - currentFilter = Filters.at(filterNum-1); + currentFilter = Filters.at(filterNum - 1); filterManager->setCurrentFilterWheel(currentFilter); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); - FilterPosCombo->setCurrentIndex(currentFilterPosition-1); + FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); exposureIN->setValue(filterManager->getFilterExposure()); } void Focus::addFocuser(ISD::GDInterface *newFocuser) { ISD::Focuser *oneFocuser = static_cast(newFocuser); if (Focusers.contains(oneFocuser)) return; focuserCombo->addItem(oneFocuser->getDeviceName()); Focusers.append(oneFocuser); currentFocuser = oneFocuser; checkFocuser(); } -bool Focus::setFocuser(const QString & device) +bool Focus::setFocuser(const QString &device) { for (int i = 0; i < focuserCombo->count(); i++) if (device == focuserCombo->itemText(i)) { focuserCombo->setCurrentIndex(i); checkFocuser(i); return true; } return false; } QString Focus::focuser() { if (currentFocuser) return currentFocuser->getDeviceName(); return QString(); } void Focus::checkFocuser(int FocuserNum) { if (FocuserNum == -1) FocuserNum = focuserCombo->currentIndex(); if (FocuserNum == -1) { currentFocuser = nullptr; return; } if (FocuserNum <= Focusers.count()) currentFocuser = Focusers.at(FocuserNum); filterManager->setFocusReady(currentFocuser->isConnected()); // Disconnect all focusers foreach (ISD::Focuser *oneFocuser, Focusers) { disconnect(oneFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber); } canAbsMove = currentFocuser->canAbsMove(); if (canAbsMove) { getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); setAbsTicksB->setEnabled(true); absTicksSpin->setValue(currentPosition); } else { absTicksSpin->setEnabled(false); absTicksLabel->setEnabled(false); setAbsTicksB->setEnabled(false); } canRelMove = currentFocuser->canRelMove(); // In case we have a purely relative focuser, we pretend // it is an absolute focuser with initial point set at 50,000 // This is done we can use the same algorithm used for absolute focuser if (canAbsMove == false && canRelMove == true) { currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } canTimerMove = currentFocuser->canTimerMove(); focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL; connect(currentFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber, Qt::UniqueConnection); //connect(currentFocuser, SIGNAL(propertyDefined(INDI::Property*)), this, &Ekos::Focus::(registerFocusProperty(INDI::Property*)), Qt::UniqueConnection); resetButtons(); //if (!inAutoFocus && !inFocusLoop && !captureInProgress && !inSequenceFocus) // emit autoFocusFinished(true, -1); } void Focus::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) return; CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); } void Focus::getAbsFocusPosition() { if (!canAbsMove) return; INumberVectorProperty *absMove = currentFocuser->getBaseDevice()->getNumber("ABS_FOCUS_POSITION"); if (absMove) { currentPosition = absMove->np[0].value; absMotionMax = absMove->np[0].max; absMotionMin = absMove->np[0].min; absTicksSpin->setMinimum(absMove->np[0].min); absTicksSpin->setMaximum(absMove->np[0].max); absTicksSpin->setSingleStep(absMove->np[0].step); maxTravelIN->setMinimum(absMove->np[0].min); maxTravelIN->setMaximum(absMove->np[0].max); absTicksLabel->setText(QString::number(static_cast(currentPosition))); //absTicksSpin->setValue(currentPosition); } } void Focus::start() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } lastFocusDirection = FOCUS_NONE; polySolutionFound = 0; waitStarSelectTimer.stop(); starsHFR.clear(); lastHFR = 0; if (canAbsMove) { absIterations = 0; getAbsFocusPosition(); pulseDuration = stepIN->value(); } else if (canRelMove) { - appendLogText(i18n("Setting dummy central position to 50000")); + //appendLogText(i18n("Setting dummy central position to 50000")); absIterations = 0; pulseDuration = stepIN->value(); - currentPosition = 50000; + //currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } else { pulseDuration = stepIN->value(); if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...", MINIMUM_PULSE_TIMER * 5)); return; } } inAutoFocus = true; HFRFrames.clear(); resetButtons(); reverseDir = false; /*if (fw > 0 && fh > 0) starSelected= true; else starSelected= false;*/ clearDataPoints(); if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } Options::setFocusTicks(stepIN->value()); Options::setFocusTolerance(toleranceIN->value()); //Options::setFocusExposure(exposureIN->value()); Options::setFocusMaxTravel(maxTravelIN->value()); Options::setFocusBoxSize(focusBoxSize->value()); Options::setFocusSubFrame(useSubFrame->isChecked()); Options::setFocusAutoStarEnabled(useAutoStar->isChecked()); Options::setSuspendGuiding(suspendGuideCheck->isChecked()); Options::setUseFocusDarkFrame(darkFrameCheck->isChecked()); Options::setFocusFramesCount(focusFramesSpin->value()); + Options::setFocusUseFullField(useFullField->isChecked()); qCDebug(KSTARS_EKOS_FOCUS) << "Starting focus with box size: " << focusBoxSize->value() - << " Step Size: " << stepIN->value() << " Threshold: " << thresholdSpin->value() - << " Tolerance: " << toleranceIN->value() - << " Frames: " << 1 /*focusFramesSpin->value()*/ << " Maximum Travel: " << maxTravelIN->value(); + << " Subframe: " << ( useSubFrame->isChecked() ? "yes" : "no" ) + << " Autostar: " << ( useAutoStar->isChecked() ? "yes" : "no" ) + << " Full frame: " << ( useFullField->isChecked() ? "yes" : "no " ) + << " [" << fullFieldInnerRing->value() << "%," << fullFieldOuterRing->value() << "%]" + << " Step Size: " << stepIN->value() << " Threshold: " << thresholdSpin->value() + << " Tolerance: " << toleranceIN->value() + << " Frames: " << 1 /*focusFramesSpin->value()*/ << " Maximum Travel: " << maxTravelIN->value(); if (useAutoStar->isChecked()) appendLogText(i18n("Autofocus in progress...")); else appendLogText(i18n("Please wait until image capture is complete...")); if (suspendGuideCheck->isChecked()) { m_GuidingSuspended = true; emit suspendGuiding(); } //emit statusUpdated(true); state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Denoise with median filter //defaultScale = FITS_MEDIAN; KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started")); capture(); } void Focus::checkStopFocus() { if (inSequenceFocus == true) { inSequenceFocus = false; setAutoFocusResult(false); } if (captureInProgress && inAutoFocus == false && inFocusLoop == false) { captureB->setEnabled(true); stopFocusB->setEnabled(false); appendLogText(i18n("Capture aborted.")); } abort(); } void Focus::abort() { stop(true); } void Focus::stop(bool aborted) { qCDebug(KSTARS_EKOS_FOCUS) << "Stopppig Focus"; captureTimeout.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); inAutoFocus = false; inFocusLoop = false; // Why starSelected is set to false below? We should retain star selection status under: // 1. Autostar is off, or // 2. Toggle subframe, or // 3. Reset frame // 4. Manual motion? //starSelected = false; polySolutionFound = 0; captureInProgress = false; minimumRequiredHFR = -1; noStarCount = 0; HFRFrames.clear(); //maxHFR=1; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); targetChip->abortExposure(); resetButtons(); absIterations = 0; HFRInc = 0; reverseDir = false; //emit statusUpdated(false); if (aborted) { state = Ekos::FOCUS_ABORTED; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::capture() { captureTimeout.stop(); if (captureInProgress) { qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored."; return; } if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); double seqExpose = exposureIN->value(); if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); return; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } if (currentFilter != nullptr && FilterPosCombo->currentIndex() != -1) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: Lost connection to filter wheel.")); return; } int targetPosition = FilterPosCombo->currentIndex() + 1; QString lockedFilter = filterManager->getFilterLock(FilterPosCombo->currentText()); // We change filter if: // 1. Target position is not equal to current position. // 2. Locked filter of CURRENT filter is a different filter. if (lockedFilter != "--" && lockedFilter != FilterPosCombo->currentText()) { int lockedFilterIndex = FilterPosCombo->findText(lockedFilter); if (lockedFilterIndex >= 0) { // Go back to this filter one we are done fallbackFilterPending = true; fallbackFilterPosition = targetPosition; targetPosition = lockedFilterIndex + 1; } } filterPositionPending = (targetPosition != currentFilterPosition); // If either the target position is not equal to the current position, OR if (filterPositionPending) { // Apply all policies except autofocus since we are already in autofocus module doh. - filterManager->setFilterPosition(targetPosition, static_cast(FilterManager::CHANGE_POLICY|FilterManager::OFFSET_POLICY)); + filterManager->setFilterPosition(targetPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } } if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->setBinning(activeBin, activeBin); targetChip->setCaptureMode(FITS_FOCUS); // Always disable filtering if using a dark frame and then re-apply after subtraction. TODO: Implement this in capture and guide and align if (darkFrameCheck->isChecked()) targetChip->setCaptureFilter(FITS_NONE); else targetChip->setCaptureFilter(defaultScale); if (ISOCombo->isEnabled() && ISOCombo->currentIndex() != -1 && - targetChip->getISOIndex() != ISOCombo->currentIndex()) + targetChip->getISOIndex() != ISOCombo->currentIndex()) targetChip->setISOIndex(ISOCombo->currentIndex()); if (gainIN->isEnabled()) currentCCD->setGain(gainIN->value()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); targetChip->setFrameType(FRAME_LIGHT); if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt()); settings["binx"] = activeBin; settings["biny"] = activeBin; frameSettings[targetChip] = settings; } captureInProgress = true; focusView->setBaseSize(focusingWidget->size()); // Timeout is exposure duration + timeout threshold in seconds captureTimeout.start(seqExpose * 1000 + FOCUS_TIMEOUT_THRESHOLD); targetChip->capture(seqExpose); if (inFocusLoop == false) { appendLogText(i18n("Capturing image...")); if (inAutoFocus == false) { captureB->setEnabled(false); stopFocusB->setEnabled(true); } } } bool Focus::focusIn(int ms) { if (currentFocuser == nullptr) return false; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } if (ms == -1) ms = stepIN->value(); qCDebug(KSTARS_EKOS_FOCUS) << "Focus in (" << ms << ")"; lastFocusDirection = FOCUS_IN; currentFocuser->focusIn(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition - ms); appendLogText(i18n("Focusing inward by %1 steps...", ms)); } else if (canRelMove) { currentFocuser->moveRel(ms); appendLogText(i18n("Focusing inward by %1 steps...", ms)); } else { currentFocuser->moveByTimer(ms); appendLogText(i18n("Focusing inward by %1 ms...", ms)); } return true; } bool Focus::focusOut(int ms) { if (currentFocuser == nullptr) return false; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } lastFocusDirection = FOCUS_OUT; if (ms == -1) ms = stepIN->value(); qCDebug(KSTARS_EKOS_FOCUS) << "Focus out (" << ms << ")"; currentFocuser->focusOut(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition + ms); appendLogText(i18n("Focusing outward by %1 steps...", ms)); } else if (canRelMove) { currentFocuser->moveRel(ms); appendLogText(i18n("Focusing outward by %1 steps...", ms)); } else { currentFocuser->moveByTimer(ms); appendLogText(i18n("Focusing outward by %1 ms...", ms)); } return true; } void Focus::newFITS(IBLOB *bp) { if (bp == nullptr) { capture(); return; } // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; captureTimeout.stop(); captureTimeoutCounter = 0; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); if (darkFrameCheck->isChecked()) { FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); QVariantMap settings = frameSettings[targetChip]; uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt(); uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, &Ekos::Focus::setCaptureComplete); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Focus::appendLogText); targetChip->setCaptureFilter(defaultScale); if (darkData) DarkLibrary::Instance()->subtract(darkData, focusView, defaultScale, offsetX, offsetY); else { bool rc = DarkLibrary::Instance()->captureAndSubtract(targetChip, focusView, exposureIN->value(), offsetX, - offsetY); + offsetY); darkFrameCheck->setChecked(rc); } return; } setCaptureComplete(); } void Focus::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); // Get Binning ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); // If we have a box, sync the bounding box to its position. syncTrackingBoxPosition(); // Notify user if we're not looping if (inFocusLoop == false) appendLogText(i18n("Image received.")); // If we're not looping and not in autofocus, enable user to capture again. if (captureInProgress && inFocusLoop == false && inAutoFocus == false) { captureB->setEnabled(true); stopFocusB->setEnabled(false); currentCCD->setUploadMode(rememberUploadMode); } if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); captureInProgress = false; // Get handle to the image data FITSData *image_data = focusView->getImageData(); // Emit the tracking (bounding) box view emit newStarPixmap(focusView->getTrackingBoxPixmap(10)); // If we are not looping; OR // If we are looping but we already have tracking box enabled; OR // If we are asked to analyze _all_ the stars within the field // THEN let's find stars in the image and get current HFR if (inFocusLoop == false || (inFocusLoop && (focusView->isTrackingBoxEnabled() || Options::focusUseFullField()))) { // First check that we haven't already search for stars // Since star-searching algorithm are time-consuming, we should only search when necessary if (image_data->areStarsSearched() == false) { // Reset current HFR currentHFR = -1; // When we're using FULL field view, we always use either CENTROID algorithm which is the default // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require // a bounding box for them to be effective in near real-time application. if (Options::focusUseFullField()) { if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) focusView->findStars(ALGORITHM_CENTROID); else focusView->findStars(focusDetection); + focusView->setStarFilterRange(static_cast (fullFieldInnerRing->value() / 100.0), + static_cast (fullFieldOuterRing->value() / 100.0)); + focusView->filterStars(); focusView->updateFrame(); // Get the average HFR of the whole frame currentHFR = image_data->getHFR(HFR_AVERAGE); } else { // If star is already selected then use whatever algorithm currently selected. if (starSelected) { focusView->findStars(focusDetection); focusView->updateFrame(); currentHFR = image_data->getHFR(HFR_MAX); } else { - // Disable tracking box - focusView->setTrackingBoxEnabled(false); + // Disable tracking box + focusView->setTrackingBoxEnabled(false); - // If algorithm is set something other than Centeroid or SEP, then force Centroid - // Since it is the most reliable detector when nothing was selected before. - if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) - focusView->findStars(ALGORITHM_CENTROID); - else - // Otherwise, continue to find use using the selected algorithm - focusView->findStars(focusDetection); + // If algorithm is set something other than Centeroid or SEP, then force Centroid + // Since it is the most reliable detector when nothing was selected before. + if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) + focusView->findStars(ALGORITHM_CENTROID); + else + // Otherwise, continue to find use using the selected algorithm + focusView->findStars(focusDetection); - // Reenable tracking box - focusView->setTrackingBoxEnabled(true); + // Reenable tracking box + focusView->setTrackingBoxEnabled(true); - focusView->updateFrame(); + focusView->updateFrame(); - // Get maximum HFR in the frame - currentHFR = image_data->getHFR(HFR_MAX); + // Get maximum HFR in the frame + currentHFR = image_data->getHFR(HFR_MAX); } } } // Let's now report the current HFR qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << HFRFrames.count() + 1 << ": Current HFR " << currentHFR; // Add it to existing frames in case we need to take an average HFRFrames.append(currentHFR); // Check if we need to average more than a single frame if (HFRFrames.count() >= focusFramesSpin->value()) { currentHFR = 0; // Remove all -1 QMutableVectorIterator i(HFRFrames); while (i.hasNext()) { if (i.next() == -1) i.remove(); } if (HFRFrames.isEmpty()) currentHFR = -1; else { // Perform simple sigma clipping if frames count > 3 if (HFRFrames.count() > 3) { // Sort all HFRs std::sort(HFRFrames.begin(), HFRFrames.end()); const auto median = ((HFRFrames.size() % 2) ? - HFRFrames[HFRFrames.size() / 2] : - ((double)HFRFrames[HFRFrames.size() / 2 - 1] + HFRFrames[HFRFrames.size() / 2]) * .5); + HFRFrames[HFRFrames.size() / 2] : + ((double)HFRFrames[HFRFrames.size() / 2 - 1] + HFRFrames[HFRFrames.size() / 2]) * .5); const auto mean = std::accumulate(HFRFrames.begin(), HFRFrames.end(), .0) / HFRFrames.size(); double variance = 0; foreach (auto val, HFRFrames) variance += (val - mean) * (val - mean); const double stddev = sqrt(variance / HFRFrames.size()); // Reject those 2 sigma away from median const double sigmaHigh = median + stddev * 2; const double sigmaLow = median - stddev * 2; QMutableVectorIterator i(HFRFrames); while (i.hasNext()) { auto val = i.next(); if (val > sigmaHigh || val < sigmaLow) i.remove(); } } // Find average HFR currentHFR = std::accumulate(HFRFrames.begin(), HFRFrames.end(), .0) / HFRFrames.size(); HFRFrames.clear(); } } else { // If we need to capture more frames to average the HFR, let's do that now. capture(); return; } // Let signal the current HFR now depending on whether the focuser is absolute or relative if (canAbsMove) emit newHFR(currentHFR, static_cast(currentPosition)); else emit newHFR(currentHFR, -1); // Format the HFR value into a string QString HFRText = QString("%1").arg(currentHFR, 0, 'f', 2); HFROut->setText(HFRText); // Display message in case _last_ HFR was negative if (lastHFR == -1) appendLogText(i18n("FITS received. No stars detected.")); // If we have a valid HFR value if (currentHFR > 0) { // Check if we're done from polynomail fitting algorithm if (focusAlgorithm == FOCUS_POLYNOMIAL && polySolutionFound == MINIMUM_POLY_SOLUTIONS) { polySolutionFound = 0; appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); return; } Edge *maxStarHFR = nullptr; // Center tracking box around selected star (if it valid) either in: // 1. Autofocus // 2. CheckFocus (minimumHFRCheck) // The starCenter _must_ already be defined, otherwise, we proceed until // the latter half of the function searches for a star and define it. if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0) && - (maxStarHFR = image_data->getMaxHFRStar()) != nullptr) + (maxStarHFR = image_data->getMaxHFRStar()) != nullptr) { // Now we have star selected in the frame starSelected = true; starCenter.setX(qMax(0, static_cast(maxStarHFR->x))); starCenter.setY(qMax(0, static_cast(maxStarHFR->y))); syncTrackingBoxPosition(); // Record the star information (X, Y, currentHFR) QVector3D oneStar = starCenter; oneStar.setZ(currentHFR); starsHFR.append(oneStar); } else { // Record the star information (X, Y, currentHFR) QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR); starsHFR.append(oneStar); } if (currentHFR > maxHFR) maxHFR = currentHFR; // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus // that does not support position feedback. if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false)) { if (hfr_position.empty()) hfr_position.append(1); else hfr_position.append(hfr_position.last() + 1); hfr_value.append(currentHFR); drawHFRPlot(); } } else { // Let's record an invalid star result QVector3D oneStar(starCenter.x(), starCenter.y(), -1); starsHFR.append(oneStar); } // Try to average values and find if we have bogus results if (inAutoFocus && starsHFR.count() > 3) { float mean = 0, sum = 0, stddev = 0, noHFR = 0; for (int i = 0; i < starsHFR.count(); i++) { sum += starsHFR[i].x(); if (starsHFR[i].z() == -1) noHFR++; } mean = sum / starsHFR.count(); // Calculate standard deviation for (int i = 0; i < starsHFR.count(); i++) stddev += pow(starsHFR[i].x() - mean, 2); stddev = sqrt(stddev / starsHFR.count()); if (currentHFR == -1 && (stddev > focusBoxSize->value() / 10.0 || noHFR / starsHFR.count() > 0.75)) { appendLogText(i18n("No reliable star is detected. Aborting...")); abort(); setAutoFocusResult(false); return; } } } // If we are just framing, let's capture again if (inFocusLoop) { capture(); return; } // If star is NOT yet selected in a non-full-frame situation // then let's now try to find the star. This step is skipped for full frames // since there isn't a single star to select as we are only interested in the overall average HFR. // We need to check if we can find the star right away, or if we need to _subframe_ around the // selected star. if (Options::focusUseFullField() == false && starCenter.isNull()) { int x = 0, y = 0, w = 0, h = 0; // Let's get the stored frame settings for this particular chip if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); } else // Otherwise let's get the target chip frame coordinates. targetChip->getFrame(&x, &y, &w, &h); // In case auto star is selected. if (useAutoStar->isChecked()) { // Do we have a valid star detected? Edge *maxStar = image_data->getMaxHFRStar(); if (maxStar == nullptr) { appendLogText(i18n("Failed to automatically select a star. Please select a star manually.")); // Center the tracking box in the frame and display it focusView->setTrackingBox(QRect(w - focusBoxSize->value() / (subBinX * 2), h - focusBoxSize->value() / (subBinY * 2), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Use can now move it to select the desired star state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Start the wait timer so we abort after a timeout if the user does not make a choice waitStarSelectTimer.start(); return; } // Do we need to subframe? if (subFramed == false && useSubFrame->isEnabled() && useSubFrame->isChecked()) { int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; int subX = (maxStar->x - offset) * subBinX; int subY = (maxStar->y - offset) * subBinY; int subW = offset * 2 * subBinX; int subH = offset * 2 * subBinY; int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); // Try to limit the subframed selection if (subX < minX) subX = minX; if (subY < minY) subY = minY; if ((subW + subX) > maxW) subW = maxW - subX; if ((subH + subY) > maxH) subH = maxH - subY; // Now we store the subframe coordinates in the target chip frame settings so we // reuse it later when we capture again. QVariantMap settings = frameSettings[targetChip]; settings["x"] = subX; settings["y"] = subY; settings["w"] = subW; settings["h"] = subH; settings["binx"] = subBinX; settings["biny"] = subBinY; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" << subBinX << "binY:" << subBinY; starsHFR.clear(); frameSettings[targetChip] = settings; // Set the star center in the center of the subframed coordinates starCenter.setX(subW / (2 * subBinX)); starCenter.setY(subH / (2 * subBinY)); starCenter.setZ(subBinX); subFramed = true; focusView->setFirstLoad(true); // Now let's capture again for the actual requested subframed image. capture(); } // If we're subframed or don't need subframe, let's record the max star coordinates else { starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); // Let's now capture again if we're autofocusing if (inAutoFocus) capture(); } syncTrackingBoxPosition(); defaultScale = static_cast(filterCombo->currentIndex()); return; } // If manual selection is enabled then let's ask the user to select the focus star else { appendLogText(i18n("Capture complete. Select a star to focus.")); starSelected = false; // Let's now display and set the tracking box in the center of the frame // so that the user moves it around to select the desired star. int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); focusView->setTrackingBox(QRect((w - focusBoxSize->value()) / (subBinX * 2), (h - focusBoxSize->value()) / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Now we wait state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // If the user does not select for a timeout period, we abort. waitStarSelectTimer.start(); return; } } // Check if the focus module is requested to verify if the minimum HFR value is met. if (minimumRequiredHFR >= 0) { // In case we failed to detected, we capture again. if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); // On Last Attempt reset focus frame to capture full frame and recapture star if possible if (noStarCount == MAX_RECAPTURE_RETRIES) resetFrame(); capture(); return; } // If we exceeded maximum tries we abort else { noStarCount = 0; setAutoFocusResult(false); } } // If the detect current HFR is more than the minimum required HFR // then we should start the autofocus process now to bring it down. else if (currentHFR > minimumRequiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR << ". Starting AutoFocus..."; inSequenceFocus = true; start(); } // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success. else { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR << ". Autofocus successful."; setAutoFocusResult(true); drawProfilePlot(); } // We reset minimum required HFR and call it a day. minimumRequiredHFR = -1; return; } // Let's draw the HFR Plot drawProfilePlot(); // If focus logging is enabled, let's save the frame. if (Options::focusLogging()) { QDir dir; QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "autofocus/" + QDateTime::currentDateTime().toString("yyyy-MM-dd"); dir.mkpath(path); // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts QString name = "autofocus_frame_" + QDateTime::currentDateTime().toString("HH-mm-ss") + ".fits"; QString filename = path + QStringLiteral("/") + name; focusView->getImageData()->saveFITS(filename); } // If we are not in autofocus process, we're done. if (inAutoFocus == false) return; // Set state to progress if (state != Ekos::FOCUS_PROGRESS) { state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } // Now let's kick in the algorithms // Position-based algorithms if (canAbsMove || canRelMove) autoFocusAbs(); else - // Time open-looped algorithms + // Time open-looped algorithms autoFocusRel(); } void Focus::clearDataPoints() { maxHFR = 1; hfr_position.clear(); hfr_value.clear(); drawHFRPlot(); } void Focus::drawHFRPlot() { v_graph->setData(hfr_position, hfr_value); if (inFocusLoop == false && (canAbsMove || canRelMove)) { //HFRPlot->xAxis->setLabel(i18n("Position")); HFRPlot->xAxis->setRange(minPos - pulseDuration, maxPos + pulseDuration); HFRPlot->yAxis->setRange(currentHFR / 1.5, maxHFR); } else { //HFRPlot->xAxis->setLabel(i18n("Iteration")); HFRPlot->xAxis->setRange(1, hfr_value.count() + 1); HFRPlot->yAxis->setRange(currentHFR / 1.5, maxHFR * 1.25); } HFRPlot->replot(); } void Focus::drawProfilePlot() { QVector currentIndexes; QVector currentFrequencies; // HFR = 50% * 1.36 = 68% aka one standard deviation double stdDev = currentHFR * 1.36; float start = -stdDev * 4; float end = stdDev * 4; float step = stdDev * 4 / 20.0; for (double x = start; x < end; x += step) { currentIndexes.append(x); currentFrequencies.append((1 / (stdDev * sqrt(2 * M_PI))) * exp(-1 * (x * x) / (2 * (stdDev * stdDev)))); } currentGaus->setData(currentIndexes, currentFrequencies); if (lastGausIndexes.count() > 0) lastGaus->setData(lastGausIndexes, lastGausFrequencies); if (focusType == FOCUS_AUTO && firstGaus == nullptr) { firstGaus = profilePlot->addGraph(); QPen pen; pen.setStyle(Qt::DashDotLine); pen.setWidth(2); pen.setColor(Qt::darkMagenta); firstGaus->setPen(pen); firstGaus->setData(currentIndexes, currentFrequencies); } else if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } profilePlot->rescaleAxes(); profilePlot->replot(); lastGausIndexes = currentIndexes; lastGausFrequencies = currentFrequencies; profilePixmap = profilePlot->grab(); //.scaled(200, 200, Qt::KeepAspectRatio, Qt::SmoothTransformation); emit newProfilePixmap(profilePixmap); } void Focus::autoFocusAbs() { static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0; static double minHFR = 0; double targetPosition = 0, delta = 0; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%"; qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; if (minHFR) appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt)); else appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition)); if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); return; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); noStarCount++; return; } else if (noStarCount == MAX_RECAPTURE_RETRIES) { currentHFR = 20; noStarCount++; } else { appendLogText(i18n("Failed to detect any stars. Reset frame and try again.")); abort(); setAutoFocusResult(false); return; } } else noStarCount = 0; if (hfr_position.empty()) { maxPos = 1; minPos = 1e6; } if (currentPosition > maxPos) maxPos = currentPosition; if (currentPosition < minPos) minPos = currentPosition; hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; initialFocuserAbsPosition = currentPosition; minHFR = currentHFR; minHFRPos = currentPosition; HFRDec = 0; HFRInc = 0; focusOutLimit = 0; focusInLimit = 0; if (focusOut(pulseDuration) == false) { abort(); setAutoFocusResult(false); } break; case FOCUS_IN: case FOCUS_OUT: static int lastHFRPos = 0, initSlopePos = 0; static double initSlopeHFR = 0; if (reverseDir && focusInLimit && focusOutLimit && - fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) + fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { if (absIterations <= 2) { appendLogText( i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.")); abort(); setAutoFocusResult(false); } else if (noStarCount > 0) { appendLogText(i18n("Failed to detect focus star in frame. Capture and select a focus star.")); abort(); setAutoFocusResult(false); } else { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); } break; } else if (currentHFR < lastHFR) { double slope = 0; // Let's try to calculate slope of the V curve. if (initSlopeHFR == 0 && HFRInc == 0 && HFRDec >= 1) { initSlopeHFR = lastHFR; initSlopePos = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Setting initial slop to " << initSlopePos << " @ HFR " << initSlopeHFR; } // Let's now limit the travel distance of the focuser if (lastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1) { focusInLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit; } else if (lastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit && fabs(currentHFR - lastHFR) > 0.1) { focusOutLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit; } // If we have slope, get next target position if (initSlopeHFR && absMotionMax > 50) { double factor = 0.5; slope = (currentHFR - initSlopeHFR) / (currentPosition - initSlopePos); if (fabs(currentHFR - minHFR) * 100.0 < 0.5) factor = 1 - fabs(currentHFR - minHFR) * 10; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; if (targetPosition < 0) { factor = 1; while (targetPosition < 0 && factor > 0) { factor -= 0.005; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; } } qCDebug(KSTARS_EKOS_FOCUS) << "Using slope to calculate target pulse..."; } // Otherwise proceed iteratively else { if (lastFocusDirection == FOCUS_IN) targetPosition = currentPosition - pulseDuration; else targetPosition = currentPosition + pulseDuration; qCDebug(KSTARS_EKOS_FOCUS) << "Proceeding iteratively to next target pulse ..."; } qCDebug(KSTARS_EKOS_FOCUS) << "V-Curve Slope " << slope << " current Position " << currentPosition - << " targetPosition " << targetPosition; + << " targetPosition " << targetPosition; lastHFR = currentHFR; // Let's keep track of the minimum HFR if (lastHFR < minHFR) { minHFR = lastHFR; minHFRPos = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ positioin " << minHFRPos; } lastHFRPos = currentPosition; // HFR is decreasing, we are on the right direction HFRDec++; HFRInc = 0; } else { // HFR increased, let's deal with it. HFRInc++; HFRDec = 0; // Reality Check: If it's first time, let's capture again and see if it changes. /*if (HFRInc <= 1 && reverseDir == false) { capture(); return; } // Looks like we're going away from optimal HFR else {*/ reverseDir = true; lastHFR = currentHFR; lastHFRPos = currentPosition; initSlopeHFR = 0; HFRInc = 0; qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR."; // Let's set new limits if (lastFocusDirection == FOCUS_IN) { focusInLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; if (hfr_position.count() > 3) { focusOutLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; } } else { focusOutLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; if (hfr_position.count() > 3) { focusInLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; } } bool polyMinimumFound = false; if (focusAlgorithm == FOCUS_POLYNOMIAL && hfr_position.count() > 5) { double chisq = 0, min_position = 0, min_hfr = 0; coeff = gsl_polynomial_fit(hfr_position.data(), hfr_value.data(), hfr_position.count(), 3, chisq); polyMinimumFound = findMinimum(minHFRPos, &min_position, &min_hfr); qCDebug(KSTARS_EKOS_FOCUS) << "Polynomial Coefficients c0:" << coeff[0] << "c1:" << coeff[1] << "c2:" << coeff[2] - << "c3:" << coeff[3]; - qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (polyMinimumFound ? "Yes" : "No"); + << "c3:" << coeff[3]; + qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (polyMinimumFound ? "Yes" : "No"); if (polyMinimumFound) { qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position; polySolutionFound++; targetPosition = floor(min_position); appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0))); } } if (polyMinimumFound == false) { // Decrease pulse pulseDuration = pulseDuration * 0.75; // Let's get close to the minimum HFR position so far detected if (lastFocusDirection == FOCUS_OUT) targetPosition = minHFRPos - pulseDuration / 2; else targetPosition = minHFRPos + pulseDuration / 2; } qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition; } // Limit target Pulse to algorithm limits if (focusInLimit != 0 && lastFocusDirection == FOCUS_IN && targetPosition < focusInLimit) { targetPosition = focusInLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition; } else if (focusOutLimit != 0 && lastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit) { targetPosition = focusOutLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition; } // Limit target pulse to focuser limits if (targetPosition < absMotionMin) targetPosition = absMotionMin; else if (targetPosition > absMotionMax) targetPosition = absMotionMax; // Ops, we can't go any further, we're done. if (targetPosition == currentPosition) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); return; } // Ops, deadlock if (focusOutLimit && focusOutLimit == focusInLimit) { appendLogText(i18n("Deadlock reached. Please try again with different settings.")); abort(); setAutoFocusResult(false); return; } if (fabs(targetPosition - initialFocuserAbsPosition) > maxTravelIN->value()) { qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos (" - << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value(); + << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value(); appendLogText("Maximum travel limit reached. Autofocus aborted."); abort(); setAutoFocusResult(false); break; } // Get delta for next move delta = (targetPosition - currentPosition); qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << delta; qCDebug(KSTARS_EKOS_FOCUS) << "Focusing " << ((delta < 0) ? "IN" : "OUT"); // Now cross your fingers and wait bool rc = false; if (delta > 0) rc = focusOut(delta); else rc = focusIn(fabs(delta)); if (rc == false) { abort(); setAutoFocusResult(false); } break; } } void Focus::autoFocusRel() { static int noStarCount = 0; static double minHFR = 1e6; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2); QString minHFRText = QString("%1").arg(minHFR, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText)); if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value.")); abort(); setAutoFocusResult(false); return; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); return; } else currentHFR = 20; } else noStarCount = 0; switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; minHFR = 1e6; focusIn(pulseDuration); break; case FOCUS_IN: case FOCUS_OUT: if (fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); break; } else if (currentHFR < lastHFR) { if (currentHFR < minHFR) minHFR = currentHFR; lastHFR = currentHFR; if (lastFocusDirection == FOCUS_IN) focusIn(pulseDuration); else focusOut(pulseDuration); HFRInc = 0; } else { HFRInc++; lastHFR = currentHFR; HFRInc = 0; pulseDuration *= 0.75; bool rc = false; if (lastFocusDirection == FOCUS_IN) rc = focusOut(pulseDuration); else rc = focusIn(pulseDuration); if (rc == false) { abort(); setAutoFocusResult(false); } } break; default: break; } } /*void Focus::registerFocusProperty(INDI::Property *prop) { // Return if it is not our current focuser if (strcmp(prop->getDeviceName(), currentFocuser->getDeviceName())) return; // Do not make unnecessary function call // Check if current focuser supports absolute mode if (canAbsMove == false && currentFocuser->canAbsMove()) { canAbsMove = true; getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); setAbsTicksB->setEnabled(true); } // Do not make unnecessary function call // Check if current focuser supports relative mode if (canRelMove == false && currentFocuser->canRelMove()) canRelMove = true; if (canTimerMove == false && currentFocuser->canTimerMove()) { canTimerMove = true; resetButtons(); } }*/ void Focus::processFocusNumber(INumberVectorProperty *nvp) { // Return if it is not our current focuser if (strcmp(nvp->device, currentFocuser->getDeviceName())) return; if (!strcmp(nvp->name, "ABS_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION"); if (pos) { currentPosition = pos->value; absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) - QTimer::singleShot(FocusSettleTime->value()*1000, this, &Ekos::Focus::capture); - //capture(); + QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); + //capture(); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } return; } if (canAbsMove) return; if (!strcmp(nvp->name, "REL_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION"); if (pos && nvp->s == IPS_OK) { currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canRelMove && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) - QTimer::singleShot(FocusSettleTime->value()*1000, this, &Ekos::Focus::capture); + QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } return; } if (canRelMove) return; if (!strcmp(nvp->name, "FOCUS_TIMER")) { if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove == false && canRelMove == false && inAutoFocus) { if (nvp->s == IPS_OK && captureInProgress == false) - QTimer::singleShot(FocusSettleTime->value()*1000, this, &Ekos::Focus::capture); + QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); else if (nvp->s == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } return; } } void Focus::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", - QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); + QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_FOCUS) << text; emit newLog(text); } void Focus::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Focus::startFraming() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); inFocusLoop = true; HFRFrames.clear(); clearDataPoints(); //emit statusUpdated(true); state = Ekos::FOCUS_FRAMING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); resetButtons(); appendLogText(i18n("Starting continuous exposure...")); capture(); } void Focus::resetButtons() { if (inFocusLoop) { startFocusB->setEnabled(false); startLoopB->setEnabled(false); stopFocusB->setEnabled(true); captureB->setEnabled(false); return; } if (inAutoFocus) { stopFocusB->setEnabled(true); startFocusB->setEnabled(false); startLoopB->setEnabled(false); captureB->setEnabled(false); focusOutB->setEnabled(false); focusInB->setEnabled(false); setAbsTicksB->setEnabled(false); resetFrameB->setEnabled(false); return; } if (currentFocuser) { focusOutB->setEnabled(true); focusInB->setEnabled(true); startFocusB->setEnabled(focusType == FOCUS_AUTO); setAbsTicksB->setEnabled(canAbsMove); } else { focusOutB->setEnabled(false); focusInB->setEnabled(false); startFocusB->setEnabled(false); setAbsTicksB->setEnabled(false); } stopFocusB->setEnabled(false); startLoopB->setEnabled(true); if (captureInProgress == false) { captureB->setEnabled(true); resetFrameB->setEnabled(true); } } void Focus::updateBoxSize(int value) { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) return; int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); QRect trackBox = focusView->getTrackingBox(); QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2)); trackBox = QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY); focusView->setTrackingBox(trackBox); } void Focus::focusStarSelected(int x, int y) { if (state == Ekos::FOCUS_PROGRESS) return; if (subFramed == false) { rememberStarCenter.setX(x); rememberStarCenter.setY(y); } ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); // If binning was changed outside of the focus module, recapture if (subBinX != activeBin) { capture(); return; } int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; QRect starRect; bool squareMovedOutside = false; if (subFramed == false && useSubFrame->isChecked() && targetChip->canSubframe()) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); //targetChip->getFrame(&fx, &fy, &fw, &fy); x = (x - offset) * subBinX; y = (y - offset) * subBinY; int w = offset * 2 * subBinX; int h = offset * 2 * subBinY; if (x < minX) x = minX; if (y < minY) y = minY; if ((x + w) > maxW) w = maxW - x; if ((y + h) > maxH) h = maxH - y; //fx += x; //fy += y; //fw = w; //fh = h; //targetChip->setFocusFrame(fx, fy, fw, fh); //frameModified=true; QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; subFramed = true; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX << "binY:" << subBinY; focusView->setFirstLoad(true); capture(); //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinY)); } else { //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y)); squareMovedOutside = (dist > ((double)focusBoxSize->value() / subBinX)); starCenter.setX(x); starCenter.setY(y); //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starRect = QRect(starCenter.x() - focusBoxSize->value() / (2 * subBinX), starCenter.y() - focusBoxSize->value() / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY); focusView->setTrackingBox(starRect); } starsHFR.clear(); starCenter.setZ(subBinX); //starSelected=true; defaultScale = static_cast(filterCombo->currentIndex()); - if (starSelected == false) - { - appendLogText(i18n("Focus star is selected.")); - starSelected = true; - } - if (squareMovedOutside && inAutoFocus == false && useAutoStar->isChecked()) { useAutoStar->blockSignals(true); useAutoStar->setChecked(false); useAutoStar->blockSignals(false); appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually.")); starSelected = false; } + else if (starSelected == false) + { + appendLogText(i18n("Focus star is selected.")); + starSelected = true; + capture(); + } waitStarSelectTimer.stop(); state = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } void Focus::checkFocus(double requiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR; minimumRequiredHFR = requiredHFR; capture(); } void Focus::toggleSubframe(bool enable) { if (enable == false) resetFrame(); starSelected = false; starCenter = QVector3D(); + + if (useFullField->isChecked()) + useFullField->setChecked(!enable); } void Focus::filterChangeWarning(int index) { // index = 4 is MEDIAN filter which helps reduce noise if (index != 0 && index != FITS_MEDIAN) appendLogText(i18n("Warning: Only use filters for preview as they may interface with autofocus operation.")); Options::setFocusEffect(index); defaultScale = static_cast(index); } void Focus::setExposure(double value) { exposureIN->setValue(value); } void Focus::setBinning(int subBinX, int subBinY) { INDI_UNUSED(subBinY); binningCombo->setCurrentIndex(subBinX - 1); } void Focus::setImageFilter(const QString &value) { for (int i = 0; i < filterCombo->count(); i++) if (filterCombo->itemText(i) == value) { filterCombo->setCurrentIndex(i); break; } } void Focus::setAutoStarEnabled(bool enable) { useAutoStar->setChecked(enable); Options::setFocusAutoStarEnabled(enable); } void Focus::setAutoSubFrameEnabled(bool enable) { useSubFrame->setChecked(enable); Options::setFocusSubFrame(enable); } void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance) { focusBoxSize->setValue(boxSize); stepIN->setValue(stepSize); maxTravelIN->setValue(maxTravel); toleranceIN->setValue(tolerance); } void Focus::setAutoFocusResult(bool status) { qCDebug(KSTARS_EKOS_FOCUS) << "AutoFocus result:" << status; // In case of failure, go back to last position if the focuser is absolute if (status == false && canAbsMove && currentFocuser && currentFocuser->isConnected() && - initialFocuserAbsPosition >= 0) + initialFocuserAbsPosition >= 0) { currentFocuser->moveAbs(initialFocuserAbsPosition); appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition)); // If we're doing in sequence focusing using an absolute focuser, let's retry focusing starting from last known good position before we give up if (inSequenceFocus && resetFocusIteration++ < MAXIMUM_RESET_ITERATIONS && resetFocus == false) { resetFocus = true; // Reset focus frame in case the star in subframe was lost resetFrame(); return; } } int settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0; // Always resume guiding if we suspended it before if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } resetFocusIteration = 0; if (settleTime > 0) appendLogText(i18n("Settling...")); - QTimer::singleShot(settleTime*1000, this, [&,status,settleTime]() { + QTimer::singleShot(settleTime * 1000, this, [ &, status, settleTime]() + { if (settleTime > 0) appendLogText(i18n("Settling complete.")); if (status) { KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully")); state = Ekos::FOCUS_COMPLETE; } else { KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed with errors"), KSNotification::EVENT_ALERT); state = Ekos::FOCUS_FAILED; } qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); // Do not emit result back yet if we have a locked filter pending return to original filter if (fallbackFilterPending) { - filterManager->setFilterPosition(fallbackFilterPosition, static_cast(FilterManager::CHANGE_POLICY|FilterManager::OFFSET_POLICY)); + filterManager->setFilterPosition(fallbackFilterPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } emit newStatus(state); }); } void Focus::checkAutoStarTimeout() { //if (starSelected == false && inAutoFocus) if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0)) { - if (inAutoFocus) - { - if (rememberStarCenter.isNull() == false) - { - focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y()); - appendLogText(i18n("No star was selected. Using last known position...")); - return; - } - } + if (inAutoFocus) + { + if (rememberStarCenter.isNull() == false) + { + focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y()); + appendLogText(i18n("No star was selected. Using last known position...")); + return; + } + } appendLogText(i18n("No star was selected. Aborting...")); initialFocuserAbsPosition = -1; abort(); setAutoFocusResult(false); } else if (state == FOCUS_WAITING) { state = FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::setAbsoluteFocusTicks() { if (currentFocuser == nullptr) return; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return; } qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus ticks to " << absTicksSpin->value(); currentFocuser->moveAbs(absTicksSpin->value()); } void Focus::setActiveBinning(int bin) { activeBin = bin + 1; Options::setFocusXBin(activeBin); } void Focus::setThreshold(double value) { Options::setFocusThreshold(value); } // TODO remove from kstars.kcfg /*void Focus::setFrames(int value) { Options::setFocusFrames(value); }*/ void Focus::syncTrackingBoxPosition() { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); Q_ASSERT(targetChip); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.isNull() == false) { double boxSize = focusBoxSize->value(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); // If box size is larger than image size, set it to lower index if (boxSize / subBinX >= w || boxSize / subBinY >= h) { focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h); return; } // If binning changed, update coords accordingly if (subBinX != starCenter.z()) { if (starCenter.z() > 0) { starCenter.setX(starCenter.x() * (starCenter.z() / subBinX)); starCenter.setY(starCenter.y() * (starCenter.z() / subBinY)); } starCenter.setZ(subBinX); } QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY), boxSize / subBinX, boxSize / subBinY); focusView->setTrackingBoxEnabled(true); focusView->setTrackingBox(starRect); } } void Focus::showFITSViewer() { FITSData *data = focusView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset) { adjustFocus = true; int relativeOffset = 0; if (useAbsoluteOffset == false) relativeOffset = value; else relativeOffset = value - currentPosition; if (relativeOffset > 0) focusOut(relativeOffset); else focusIn(abs(relativeOffset)); } void Focus::toggleFocusingWidgetFullScreen() { if (focusingWidget->parent() == nullptr) { focusingWidget->setParent(this); rightLayout->insertWidget(0, focusingWidget); focusingWidget->showNormal(); } else { focusingWidget->setParent(nullptr); focusingWidget->setWindowTitle(i18n("Focus Frame")); focusingWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); focusingWidget->showMaximized(); focusingWidget->show(); } } void Focus::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: captureB->setEnabled(false); startFocusB->setEnabled(false); startLoopB->setEnabled(false); break; default: resetButtons(); break; } } double Focus::fn1(double x, void *params) { Focus *module = static_cast(params); return (module->coeff[0] + module->coeff[1] * x + module->coeff[2] * pow(x, 2) + module->coeff[3] * pow(x, 3)); } bool Focus::findMinimum(double expected, double *position, double *hfr) { int status; int iter = 0, max_iter = 100; const gsl_min_fminimizer_type *T; gsl_min_fminimizer *s; double m = expected; double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); ; gsl_function F; F.function = &Focus::fn1; F.params = this; // Must turn off error handler or it aborts on error gsl_set_error_handler_off(); T = gsl_min_fminimizer_brent; s = gsl_min_fminimizer_alloc(T); status = gsl_min_fminimizer_set(s, &F, m, a, b); if (status != GSL_SUCCESS) { qCWarning(KSTARS_EKOS_FOCUS) << "Focus GSL error:" << gsl_strerror(status); return false; } do { iter++; status = gsl_min_fminimizer_iterate(s); m = gsl_min_fminimizer_x_minimum(s); a = gsl_min_fminimizer_x_lower(s); b = gsl_min_fminimizer_x_upper(s); status = gsl_min_test_interval(a, b, 0.01, 0.0); if (status == GSL_SUCCESS) { *position = m; *hfr = fn1(m, this); } - } while (status == GSL_CONTINUE && iter < max_iter); + } + while (status == GSL_CONTINUE && iter < max_iter); gsl_min_fminimizer_free(s); return (status == GSL_SUCCESS); } void Focus::removeDevice(ISD::GDInterface *deviceRemoved) { // Check in Focusers for (ISD::GDInterface *focuser : Focusers) { if (focuser == deviceRemoved) { Focusers.removeOne(dynamic_cast(focuser)); focuserCombo->removeItem(focuserCombo->findText(focuser->getDeviceName())); checkFocuser(); resetButtons(); return; } } // Check in CCDs for (ISD::GDInterface *ccd : CCDs) { if (ccd == deviceRemoved) { CCDs.removeOne(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); checkCCD(); resetButtons(); return; } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (filter == deviceRemoved) { Filters.removeOne(filter); filterCombo->removeItem(filterCombo->findText(filter->getDeviceName())); checkFilter(); resetButtons(); return; } } } void Focus::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { filterPositionPending = false; capture(); } else if (fallbackFilterPending) { fallbackFilterPending = false; emit newStatus(state); } } - ); + ); connect(filterManager.data(), &FilterManager::failed, [this]() { - appendLogText(i18n("Filter operation failed.")); - abort(); + appendLogText(i18n("Filter operation failed.")); + abort(); } - ); + ); connect(this, &Focus::newStatus, [this](Ekos::FocusState state) { if (FilterPosCombo->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE) { filterManager->setFilterAbsoluteFocusPosition(FilterPosCombo->currentIndex(), currentPosition); } }); connect(exposureIN, &QDoubleSpinBox::editingFinished, [this]() { if (currentFilter) filterManager->setFilterExposure(FilterPosCombo->currentIndex(), exposureIN->value()); else Options::setFocusExposure(exposureIN->value()); }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); - FilterPosCombo->setCurrentIndex(currentFilterPosition-1); + FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { currentFilterPosition = filterManager->getFilterPosition(); - FilterPosCombo->setCurrentIndex(currentFilterPosition-1); + FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); }); connect(filterManager.data(), &FilterManager::exposureChanged, this, [this]() { exposureIN->setValue(filterManager->getFilterExposure()); - ;}); + ; + }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), - [=](const QString &text) + [ = ](const QString & text) { exposureIN->setValue(filterManager->getFilterExposure(text)); } - ); + ); } void Focus::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL || KMessageBox::questionYesNo(nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) currentCCD->setBLOBEnabled(true); else return; } currentCCD->setVideoStreamEnabled(enabled); } void Focus::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); } } void Focus::processCaptureTimeout() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure timeout. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + FOCUS_TIMEOUT_THRESHOLD); } } diff --git a/kstars/ekos/focus/focus.ui b/kstars/ekos/focus/focus.ui index 227ac548b..39f1a5616 100644 --- a/kstars/ekos/focus/focus.ui +++ b/kstars/ekos/focus/focus.ui @@ -1,1061 +1,1093 @@ Focus 0 0 - 698 - 465 + 674 + 488 3 3 3 3 3 Qt::Horizontal 1 Focuser Focuser: false Focus In false Focus Out - Ticks: + Steps: false Current absolute focuser position QLineEdit[readOnly="true"] { color: gray } true false 0 0 Desired absolute focus position false Set desired absolute focus position Set Start: false Auto Focus true true false Framing false Capture Settle: Wait for this many seconds after moving the focuser before capturing the next image during AutoFocus. 3 30.000000000000000 false Stop CCD && Filter Wheel 3 3 3 3 3 CCD: false 22 22 22 22 Live Video .. 22 22 true Exposure time in seconds Exp: 1 3 0.001000000000000 300.000000000000000 0.100000000000000 0.500000000000000 22 22 22 22 Toggle Full Screen 22 22 22 22 Show in FITS Viewer Bin: 1 false Exposure time in seconds Gain: false ISO: false false Filter Wheel FW: false false Number of images to capture Filter: 1 false false 22 22 Filter Settings .. Apply filter to image after capture to enhance it Effect: -- Frame: Reset focus subframe to full capture Reset Settings 1 3 3 3 3 3 Subframe around the focus star during the autofocus procedure Sub Frame true - - + + - + Automatically select the best focus star from the image - Dark Frame + Auto Select Star - - + + - Automatically select the best focus star from the image + <html><head/><body><p>Measure average HFR from all stars combined in a full frame. This method defaults to the Centroid detection, but can use SEP detection too. Its performance decreases as the number of stars increases.</p></body></html> - Auto Select Star + Full Field - + - Suspend Guiding while autofocus in progress + <html><body><p>During full field focusing, stars which are inside this percentage of the frame are filtered out of HFR calculation (default 0%). Detection algorithms may also have an inherent filter.</p></body></html> - - Suspend Guiding + + % + + + 1 + + + 10.000000000000000 + + + + + + + <html><body><p>During full field focusing, stars which are outside this percentage of the frame are filtered out of HFR calculation (default 100%). Detection algorithms may also have an inherent filter.</p></body></html> + + + % + + + 1 + + + 10.000000000000000 + + + 100.000000000000000 - + + + + + + Dark Frame + + + + + - <html><head/><body><p>Measure average HFR from all stars combined in a full frame. This method can be slow.</p></body></html> + Suspend Guiding while autofocus in progress - Full Field + Suspend Guiding - + Wait this many seconds before resuming guiding. 60.000000000000000 Qt::Horizontal 10.000000000000000 50000.000000000000000 100.000000000000000 10000.000000000000000 - Maximum travel in ticks before the autofocus process aborts + <html><head/><body><p>Maximum travel in steps before the autofocus process aborts</p></body></html> Max Travel: 0.010000000000000 20.000000000000000 0.100000000000000 1.000000000000000 Increase to restrict the centroid to bright cores. Decrease to enclose fuzzy stars. Threshold (%): <b>Initial</b> step size in ticks to cause a noticeable change in HFR value. For timer based focuser, it is the initial time in milliseconds to move the focuser inward or outward - Step: + Step size: false 90.000000000000000 500.000000000000000 10.000000000000000 150.000000000000000 Delay between two consequent focus images Box Size: Decrease value to narrow optimal focus point solution radius. Increase to expand solution radius Tolerance (%): 16 256 16 32 1 10000 10 250 Number of frames to average Frames: Number of frames to average 1 Gradient Centroid Threshold SEP <html><head/><body><p>Select focus process algorithm:</p> <ul> <li><b>Iterative</b>: Moves focuser by discreet steps initially decided by the step size. Once a curve slope is calculated, further step sizes are calculated to reach optimal solution. The algorithm stops when the measured HFR is within percentage tolerance of the minimum HFR recorded in the procedure.</li> <li><b>Polynomial</b>: Starts with iterative method. Upon crossing to the other side of the V-Curve, polynomial fitting coefficients along with possible minimum solution are calculated. This algorithm can be faster than purely iterative approach given a good data set.</li> </ul> </body></html> Algorithm: <html><head/><body><p>Select star detection algorithm</p></body></html> Detection: Iterative Polynomial Qt::Vertical 20 40 0 0 Qt::Vertical 0 0 320 240 40 30 0 0 0 200 V-Curve 5 3 3 3 3 0 0 200 100 1 HFR: true pixels Qt::Horizontal QSizePolicy::Expanding 40 13 Relative Profile... Clear Data QCustomPlot QWidget
auxiliary/qcustomplot.h
1
focuserCombo focusInB focusOutB absTicksLabel absTicksSpin setAbsTicksB startFocusB startLoopB captureB FocusSettleTime stopFocusB CCDCaptureCombo exposureIN toggleFullScreenB showFITSViewerB binningCombo gainIN ISOCombo FilterDevicesCombo FilterPosCombo filterManagerB filterCombo resetFrameB useAutoStar useSubFrame - useFullField - darkFrameCheck - suspendGuideCheck focusBoxSize maxTravelIN stepIN toleranceIN thresholdSpin focusAlgorithmCombo focusFramesSpin focusDetectionCombo HFROut relativeProfileB clearDataB
diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp index cdd98ee10..dee8c46d6 100644 --- a/kstars/fitsviewer/fitsdata.cpp +++ b/kstars/fitsviewer/fitsdata.cpp @@ -1,4721 +1,4738 @@ /*************************************************************************** FITSImage.cpp - FITS Image ------------------- 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. * ***************************************************************************/ #include "fitsdata.h" #include "sep/sep.h" #include "fpack.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymapcomposite.h" #include "auxiliary/ksnotification.h" #include #include #include #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) #include #include #endif #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #include #include #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 const int MINIMUM_ROWS_PER_CENTER = 3; const QString FITSData::m_TemporaryPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); #define DIFFUSE_THRESHOLD 0.15 #define MAX_EDGE_LIMIT 10000 #define LOW_EDGE_CUTOFF_1 50 #define LOW_EDGE_CUTOFF_2 10 #define MINIMUM_EDGE_LIMIT 2 bool greaterThan(Edge * s1, Edge * s2) { //return s1->width > s2->width; return s1->sum > s2->sum; } FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; } FITSData::FITSData(const FITSData * other) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; this->m_Mode = other->m_Mode; this->m_DataType = other->m_DataType; this->m_Channels = other->m_Channels; memcpy(&stats, &(other->stats), sizeof(stats)); m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; memcpy(m_ImageBuffer, other->m_ImageBuffer, stats.samples_per_channel * m_Channels * stats.bytesPerPixel); } FITSData::~FITSData() { int status = 0; clearImageBuffers(); if (starCenters.count() > 0) qDeleteAll(starCenters); delete[] wcs_coord; if (objList.count() > 0) qDeleteAll(objList); if (fptr != nullptr) { fits_close_file(fptr, &status); fptr = nullptr; if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); } qDeleteAll(records); } QFuture FITSData::loadFITS(const QString &inFilename, bool silent) { int status = 0; qDeleteAll(starCenters); starCenters.clear(); if (fptr != nullptr) { fits_close_file(fptr, &status); fptr = nullptr; // If current file is temporary AND // Auto Remove Temporary File is Set AND // New filename is different from existing filename // THen remove it. We have to check for name since we cannot delete // the same filename and try to open it below! if (m_isTemporary && autoRemoveTemporaryFITS && inFilename != m_Filename) QFile::remove(m_Filename); } m_Filename = inFilename; qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; QFuture result = QtConcurrent::run(this, &FITSData::privateLoad, silent); return result; } bool FITSData::privateLoad(bool silent) { int status = 0, anynull = 0; long naxes[3]; char error_status[512]; QString errMessage; if (m_Filename.endsWith(".fz")) { QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); m_isTemporary = true; fpstate fpvar; std::vector arguments = {"funpack", m_Filename.toLatin1().toStdString()}; std::vector arglist; for (const auto &arg : arguments) arglist.push_back((char *)arg.data()); arglist.push_back(nullptr); int argc = arglist.size() - 1; char ** argv = arglist.data(); // TODO: Check for errors fp_init (&fpvar); fp_get_param (argc, argv, &fpvar); fp_preflight (argc, argv, FUNPACK, &fpvar); fp_loop (argc, argv, FUNPACK, uncompressedFile.toLatin1().data(), fpvar); m_Filename = uncompressedFile; } else if (m_Filename.startsWith(m_TemporaryPath)) m_isTemporary = true; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("Could not open file %1. Error %2", m_Filename, QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.size = QFile(m_Filename).size(); if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("Could not locate image HDU. Error %1", QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (fits_get_img_param(fptr, 3, &(stats.bitpix), &(stats.ndim), naxes, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("FITS file open error (fits_get_img_param): %1", QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 2) { errMessage = i18n("1D FITS images are not supported in KStars."); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } switch (stats.bitpix) { case BYTE_IMG: m_DataType = TBYTE; stats.bytesPerPixel = sizeof(uint8_t); break; case SHORT_IMG: // Read SHORT image as USHORT m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(int16_t); break; case USHORT_IMG: m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(uint16_t); break; case LONG_IMG: // Read LONG image as ULONG m_DataType = TULONG; stats.bytesPerPixel = sizeof(int32_t); break; case ULONG_IMG: m_DataType = TULONG; stats.bytesPerPixel = sizeof(uint32_t); break; case FLOAT_IMG: m_DataType = TFLOAT; stats.bytesPerPixel = sizeof(float); break; case LONGLONG_IMG: m_DataType = TLONGLONG; stats.bytesPerPixel = sizeof(int64_t); break; case DOUBLE_IMG: m_DataType = TDOUBLE; stats.bytesPerPixel = sizeof(double); break; default: errMessage = i18n("Bit depth %1 is not supported.", stats.bitpix); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 3) naxes[2] = 1; if (naxes[0] == 0 || naxes[1] == 0) { errMessage = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.width = naxes[0]; stats.height = naxes[1]; stats.samples_per_channel = stats.width * stats.height; clearImageBuffers(); m_Channels = naxes[2]; // Channels always set to #1 if we are not required to process 3D Cubes // Or if mode is not FITS_NORMAL (guide, focus..etc) if (m_Mode != FITS_NORMAL || !Options::auto3DCube()) m_Channels = 1; //image_buffer = new float[stats.samples_per_channel * channels]; m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; //if (image_buffer == nullptr) if (m_ImageBuffer == nullptr) { qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: " << stats.samples_per_channel * m_Channels * stats.bytesPerPixel << " bytes."; clearImageBuffers(); return false; } rotCounter = 0; flipHCounter = 0; flipVCounter = 0; long nelements = stats.samples_per_channel * m_Channels; if (fits_read_img(fptr, m_DataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); errMessage = i18n("Error reading image: %1", QString(errmsg)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); fits_report_error(stderr, status); qCCritical(KSTARS_FITS) << errMessage; return false; } parseHeader(); if (Options::autoDebayer() && checkDebayer()) { bayerBuffer = m_ImageBuffer; if (debayer()) calculateStats(); } else calculateStats(); WCSLoaded = false; if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN) checkForWCS(); starsSearched = false; return true; } int FITSData::saveFITS(const QString &newFilename) { if (newFilename == m_Filename) return 0; if (m_isCompressed) { KSNotification::error(i18n("Saving compressed files is not supported.")); return -1; } int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; if (HasDebayer) { /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } // Skip "!" in the beginning of the new file name QString finalFileName(newFilename); finalFileName.remove('!'); // Remove first otherwise copy will fail below if file exists QFile::remove(finalFileName); if (!QFile::copy(m_Filename, finalFileName)) { qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName; fptr = nullptr; return -1; } if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = finalFileName; //fits_open_image(&fptr, filename.toLatin1(), READONLY, &status); // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status); fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status); return 0; } nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /*if (fits_copy_file(fptr, new_fptr, 0, 1, 1, &status)) { fits_report_error(stderr, status); return status; }*/ if (fits_copy_header(fptr, new_fptr, &status)) { fits_report_error(stderr, status); return status; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } status = 0; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_report_error(stderr, status); return status; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_report_error(stderr, status); return status; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_report_error(stderr, status); return status; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_report_error(stderr, status); return status; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_report_error(stderr, status); return status; } // ISO Date if (fits_write_date(fptr, &status)) { fits_report_error(stderr, status); return status; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } int rot = 0, mirror = 0; if (rotCounter > 0) { rot = (90 * rotCounter) % 360; if (rot < 0) rot += 360; } if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0) mirror = 1; if ((rot != 0) || (mirror != 0)) rotWCSFITS(rot, mirror); rotCounter = flipHCounter = flipVCounter = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = newFilename; fits_flush_file(fptr, &status); qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename; return status; } void FITSData::clearImageBuffers() { delete[] m_ImageBuffer; m_ImageBuffer = nullptr; bayerBuffer = nullptr; } void FITSData::calculateStats(bool refresh) { // Calculate min max calculateMinMax(refresh); // Get standard deviation and mean in one run switch (m_DataType) { case TBYTE: runningAverageStdDev(); break; case TSHORT: runningAverageStdDev(); break; case TUSHORT: runningAverageStdDev(); break; case TLONG: runningAverageStdDev(); break; case TULONG: runningAverageStdDev(); break; case TFLOAT: runningAverageStdDev(); break; case TLONGLONG: runningAverageStdDev(); break; case TDOUBLE: runningAverageStdDev(); break; default: return; } // FIXME That's not really SNR, must implement a proper solution for this value stats.SNR = stats.mean[0] / stats.stddev[0]; if (refresh && markStars) // Let's try to find star positions again after transformation starsSearched = false; } int FITSData::calculateMinMax(bool refresh) { int status, nfound = 0; status = 0; if ((fptr != nullptr) && !refresh) { if (fits_read_key_dbl(fptr, "DATAMIN", &(stats.min[0]), nullptr, &status) == 0) nfound++; if (fits_read_key_dbl(fptr, "DATAMAX", &(stats.max[0]), nullptr, &status) == 0) nfound++; // If we found both keywords, no need to calculate them, unless they are both zeros if (nfound == 2 && !(stats.min[0] == 0 && stats.max[0] == 0)) return 0; } stats.min[0] = 1.0E30; stats.max[0] = -1.0E30; stats.min[1] = 1.0E30; stats.max[1] = -1.0E30; stats.min[2] = 1.0E30; stats.max[2] = -1.0E30; switch (m_DataType) { case TBYTE: calculateMinMax(); break; case TSHORT: calculateMinMax(); break; case TUSHORT: calculateMinMax(); break; case TLONG: calculateMinMax(); break; case TULONG: calculateMinMax(); break; case TFLOAT: calculateMinMax(); break; case TLONGLONG: calculateMinMax(); break; case TDOUBLE: calculateMinMax(); break; default: break; } //qDebug() << "DATAMIN: " << stats.min << " - DATAMAX: " << stats.max; return 0; } template QPair FITSData::getParitionMinMax(uint32_t start, uint32_t stride) { auto * buffer = reinterpret_cast(m_ImageBuffer); T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { if (buffer[i] < min) min = buffer[i]; else if (buffer[i] > max) max = buffer[i]; } return qMakePair(min, max); } template void FITSData::calculateMinMax() { //QTime timer; //timer.start(); //if (filename.contains("thread")) //{ T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); if (result.first < min) min = result.first; if (result.second > max) max = result.second; } stats.min[n] = min; stats.max[n] = max; } #if 0 } else { T * buffer = reinterpret_cast(imageBuffer); if (channels == 1) { for (unsigned int i = 0; i < stats.samples_per_channel; i++) { if (buffer[i] < stats.min[0]) stats.min[0] = buffer[i]; else if (buffer[i] > stats.max[0]) stats.max[0] = buffer[i]; } } else { int g_offset = stats.samples_per_channel; int b_offset = stats.samples_per_channel * 2; for (unsigned int i = 0; i < stats.samples_per_channel; i++) { if (buffer[i] < stats.min[0]) stats.min[0] = buffer[i]; else if (buffer[i] > stats.max[0]) stats.max[0] = buffer[i]; if (buffer[i + g_offset] < stats.min[1]) stats.min[1] = buffer[i + g_offset]; else if (buffer[i + g_offset] > stats.max[1]) stats.max[1] = buffer[i + g_offset]; if (buffer[i + b_offset] < stats.min[2]) stats.min[2] = buffer[i + b_offset]; else if (buffer[i + b_offset] > stats.max[2]) stats.max[2] = buffer[i + b_offset]; } } } qCInfo(KSTARS_FITS) << filename << "MinMax calculation took" << timer.elapsed() << "ms"; #endif } template QPair FITSData::getSquaredSumAndMean(uint32_t start, uint32_t stride) { uint32_t m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; auto * buffer = reinterpret_cast(m_ImageBuffer); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } return qMakePair(m_newM, m_newS); } template void FITSData::runningAverageStdDev() { //QTime timer; //timer.start(); //if (filename.contains("thread")) //{ // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getSquaredSumAndMean, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } double mean = 0, squared_sum = 0; // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); mean += result.first; squared_sum += result.second; } double variance = squared_sum / stats.samples_per_channel; stats.mean[n] = mean / nThreads; stats.stddev[n] = sqrt(variance); } #if 0 } else { T * buffer = reinterpret_cast(imageBuffer); if (channels == 1) { int m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; for (unsigned int i = 1; i < stats.samples_per_channel; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } double variance = (m_n == 2 ? 0 : m_newS / (m_n - 2)); stats.mean[0] = m_newM; stats.stddev[0] = sqrt(variance); } else { int m_n[3] = {2, 2, 2}; double m_oldM[3] = {0}, m_newM[3] = {0}, m_oldS[3] = {0}, m_newS[3] = {0}; T * rBuffer = buffer; T * gBuffer = buffer + stats.samples_per_channel; T * bBuffer = buffer + stats.samples_per_channel * 2; for (unsigned int i = 1; i < stats.samples_per_channel; i++) { m_newM[0] = m_oldM[0] + (rBuffer[i] - m_oldM[0]) / m_n[0]; m_newS[0] = m_oldS[0] + (rBuffer[i] - m_oldM[0]) * (rBuffer[i] - m_newM[0]); m_oldM[0] = m_newM[0]; m_oldS[0] = m_newS[0]; m_n[0]++; m_newM[1] = m_oldM[1] + (gBuffer[i] - m_oldM[1]) / m_n[1]; m_newS[1] = m_oldS[1] + (gBuffer[i] - m_oldM[1]) * (gBuffer[i] - m_newM[1]); m_oldM[1] = m_newM[1]; m_oldS[1] = m_newS[1]; m_n[1]++; m_newM[2] = m_oldM[2] + (bBuffer[i] - m_oldM[2]) / m_n[2]; m_newS[2] = m_oldS[2] + (bBuffer[i] - m_oldM[2]) * (bBuffer[i] - m_newM[2]); m_oldM[2] = m_newM[2]; m_oldS[2] = m_newS[2]; m_n[2]++; } double variance = (m_n[0] == 2 ? 0 : m_newS[0] / (m_n[0] - 2)); stats.mean[0] = m_newM[0]; stats.stddev[0] = sqrt(variance); variance = (m_n[1] == 2 ? 0 : m_newS[1] / (m_n[1] - 2)); stats.mean[1] = m_newM[1]; stats.stddev[1] = sqrt(variance); variance = (m_n[2] == 2 ? 0 : m_newS[2] / (m_n[2] - 2)); stats.mean[2] = m_newM[2]; stats.stddev[2] = sqrt(variance); } } qCInfo(KSTARS_FITS) << filename << "runningMeanStdDev calculation took" << timer.elapsed() << "ms"; #endif } void FITSData::setMinMax(double newMin, double newMax, uint8_t channel) { stats.min[channel] = newMin; stats.max[channel] = newMax; } bool FITSData::parseHeader() { char * header = nullptr; int status = 0, nkeys = 0; if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status)) { fits_report_error(stderr, status); free(header); return false; } QString recordList = QString(header); for (int i = 0; i < nkeys; i++) { Record * oneRecord = new Record; // Quotes cause issues for simplified below so we're removing them. QString record = recordList.mid(i * 80, 80).remove("'"); QStringList properties = record.split(QRegExp("[=/]")); // If it is only a comment if (properties.size() == 1) { oneRecord->key = properties[0].mid(0, 7); oneRecord->comment = properties[0].mid(8).simplified(); } else { oneRecord->key = properties[0].simplified(); oneRecord->value = properties[1].simplified(); if (properties.size() > 2) oneRecord->comment = properties[2].simplified(); // Try to guess the value. // Test for integer & double. If neither, then leave it as "string". bool ok = false; // Is it Integer? oneRecord->value.toInt(&ok); if (ok) oneRecord->value.convert(QMetaType::Int); else { // Is it double? oneRecord->value.toDouble(&ok); if (ok) oneRecord->value.convert(QMetaType::Double); } } records.append(oneRecord); } free(header); return true; } bool FITSData::getRecordValue(const QString &key, QVariant &value) const { for (Record * oneRecord : records) { if (oneRecord->key == key) { value = oneRecord->value; return true; } } return false; } bool FITSData::checkCollision(Edge * s1, Edge * s2) { int dis; //distance int diff_x = s1->x - s2->x; int diff_y = s1->y - s2->y; dis = std::abs(sqrt(diff_x * diff_x + diff_y * diff_y)); dis -= s1->width / 2; dis -= s2->width / 2; if (dis <= 0) //collision return true; //no collision return false; } int FITSData::findCannyStar(FITSData * data, const QRect &boundary) { switch (data->property("dataType").toInt()) { case TBYTE: return FITSData::findCannyStar(data, boundary); case TSHORT: return FITSData::findCannyStar(data, boundary); case TUSHORT: return FITSData::findCannyStar(data, boundary); case TLONG: return FITSData::findCannyStar(data, boundary); case TULONG: return FITSData::findCannyStar(data, boundary); case TFLOAT: return FITSData::findCannyStar(data, boundary); case TLONGLONG: return FITSData::findCannyStar(data, boundary); case TDOUBLE: return FITSData::findCannyStar(data, boundary); default: break; } return 0; } int FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox) { int count = 0; starAlgorithm = algorithm; qDeleteAll(starCenters); starCenters.clear(); switch (algorithm) { case ALGORITHM_SEP: count = findSEPStars(trackingBox); break; case ALGORITHM_GRADIENT: count = findCannyStar(this, trackingBox); break; case ALGORITHM_CENTROID: count = findCentroid(trackingBox); break; case ALGORITHM_THRESHOLD: count = findOneStar(trackingBox); break; } starsSearched = true; return count; } +int FITSData::filterStars(const float innerRadius, const float outerRadius) +{ + long const sqDiagonal = this->width()*this->width()/4 + this->height()*this->height()/4; + long const sqInnerRadius = std::lround(sqDiagonal * innerRadius * innerRadius); + long const sqOuterRadius = std::lround(sqDiagonal * outerRadius * outerRadius); + + starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(), + [&](Edge *edge) { + long const x = edge->x - this->width()/2; + long const y = edge->y - this->height()/2; + long const sqRadius = x*x + y*y; + return sqRadius < sqInnerRadius || sqOuterRadius < sqRadius; + }), starCenters.end()); + + return starCenters.count(); +} + template int FITSData::findCannyStar(FITSData * data, const QRect &boundary) { int subX = qMax(0, boundary.isNull() ? 0 : boundary.x()); int subY = qMax(0, boundary.isNull() ? 0 : boundary.y()); int subW = (boundary.isNull() ? data->width() : boundary.width()); int subH = (boundary.isNull() ? data->height() : boundary.height()); int BBP = data->getBytesPerPixel(); uint16_t dataWidth = data->width(); // #1 Find offsets uint32_t size = subW * subH; uint32_t offset = subX + subY * dataWidth; // #2 Create new buffer auto * buffer = new uint8_t[size * BBP]; // If there is no offset, copy whole buffer in one go if (offset == 0) memcpy(buffer, data->getImageBuffer(), size * BBP); else { uint8_t * dataPtr = buffer; uint8_t * origDataPtr = data->getImageBuffer(); uint32_t lineOffset = 0; // Copy data line by line for (int height = subY; height < (subY + subH); height++) { lineOffset = (subX + height * dataWidth) * BBP; memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP); dataPtr += (subW * BBP); } } // #3 Create new FITSData to hold it auto * boundedImage = new FITSData(); boundedImage->stats.width = subW; boundedImage->stats.height = subH; boundedImage->stats.bitpix = data->stats.bitpix; boundedImage->stats.bytesPerPixel = data->stats.bytesPerPixel; boundedImage->stats.samples_per_channel = size; boundedImage->stats.ndim = 2; boundedImage->setProperty("dataType", data->property("dataType")); // #4 Set image buffer and calculate stats. boundedImage->setImageBuffer(buffer); boundedImage->calculateStats(true); // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain boundedImage->applyFilter(FITS_MEDIAN); boundedImage->applyFilter(FITS_HIGH_CONTRAST); // #6 Perform Sobel to find gradients and their directions QVector gradients; QVector directions; // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed // and discarded whenever necessary. It won't work on noisy images unless this is done. boundedImage->sobel(gradients, directions); QVector ids(gradients.size()); int maxID = boundedImage->partition(subW, subH, gradients, ids); //QVector thresholded = boundedImage->threshold(boundedImage->stats.mean[0], boundedImage->stats.max[0], gradients); // Not needed anymore delete boundedImage; if (maxID == 0) return 0; typedef struct { float massX = 0; float massY = 0; float totalMass = 0; } massInfo; QMap masses; // #7 Calculate center of mass for all detected regions for (int y = 0; y < subH; y++) { for (int x = 0; x < subW; x++) { int index = x + y * subW; int regionID = ids[index]; if (regionID > 0) { float pixel = gradients[index]; masses[regionID].totalMass += pixel; masses[regionID].massX += x * pixel; masses[regionID].massY += y * pixel; } } } // Compare multiple masses, and only select the highest total mass one as the desired star int maxRegionID = 1; int maxTotalMass = masses[1].totalMass; double totalMassRatio = 1e6; for (auto key : masses.keys()) { massInfo oneMass = masses.value(key); if (oneMass.totalMass > maxTotalMass) { totalMassRatio = oneMass.totalMass / maxTotalMass; maxTotalMass = oneMass.totalMass; maxRegionID = key; } } // If image has many regions and there is no significant relative center of mass then it's just noise and no stars // are probably there above a useful threshold. if (maxID > 10 && totalMassRatio < 1.5) return 0; auto * center = new Edge; center->width = -1; center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5; center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < 0 || testX >= subW || testY < 0 || testY >= subH) break; if (gradients[testX + testY * subW] > 0) //if (thresholded[testX + testY * subW] > 0) { if (++pass >= 24) { center->width = r * 2; // Break of outer loop r = 0; break; } } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width; // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); double FSum = 0, HF = 0, TF = 0; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); const T * origBuffer = reinterpret_cast(data->getImageBuffer()) + offset; /*if (Options::fITSLogging()) { QDebug deb = qDebug(); for (int i=0; i < subW; i++) deb << origBuffer[i + cen_y * dataWidth] << ","; }*/ for (double x = leftEdge; x <= rightEdge; x += resolution) { double slice = resolution * (origBuffer[static_cast(floor(x)) + cen_y * dataWidth]); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We overpassed HF, let's calculate from last TF how much until we reach HF // #1 Accurate calculation, but very sensitive to small variations of flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Less accurate calculation, but stable against small variations of flux //center->HFR = (k - 1) * resolution; break; } lastTF = TF; } // Correct center for subX and subY center->x += subX; center->y += subY; data->appendStar(center); qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR; return 1; } int FITSData::findOneStar(const QRect &boundary) { switch (m_DataType) { case TBYTE: return findOneStar(boundary); break; case TSHORT: return findOneStar(boundary); break; case TUSHORT: return findOneStar(boundary); break; case TLONG: return findOneStar(boundary); break; case TULONG: return findOneStar(boundary); break; case TFLOAT: return findOneStar(boundary); break; case TLONGLONG: return findOneStar(boundary); break; case TDOUBLE: return findOneStar(boundary); break; default: break; } return 0; } template int FITSData::findOneStar(const QRect &boundary) { if (boundary.isEmpty()) return -1; int subX = boundary.x(); int subY = boundary.y(); int subW = subX + boundary.width(); int subH = subY + boundary.height(); float massX = 0, massY = 0, totalMass = 0; auto * buffer = reinterpret_cast(m_ImageBuffer); // TODO replace magic number with something more useful to understand double threshold = stats.mean[0] * Options::focusThreshold() / 100.0; for (int y = subY; y < subH; y++) { for (int x = subX; x < subW; x++) { T pixel = buffer[x + y * stats.width]; if (pixel > threshold) { totalMass += pixel; massX += x * pixel; massY += y * pixel; } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << massX / totalMass << " Y: " << massY / totalMass; auto * center = new Edge; center->width = -1; center->x = massX / totalMass + 0.5; center->y = massY / totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; // Critical threshold double critical_threshold = threshold * 0.7; double running_threshold = threshold; while (running_threshold >= critical_threshold) { for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 10.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < subX || testX > subW || testY < subY || testY > subH) break; if (buffer[testX + testY * stats.width] > running_threshold) pass++; } //qDebug() << "Testing for radius " << r << " passes # " << pass << " @ threshold " << running_threshold; //if (pass >= 6) if (pass >= 5) { center->width = r * 2; break; } } if (center->width > 0) break; // Increase threshold fuzziness by 10% running_threshold -= running_threshold * 0.1; } // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); starCenters.append(center); double FSum = 0, HF = 0, TF = 0, min = stats.min[0]; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); for (double x = leftEdge; x <= rightEdge; x += resolution) { //subPixels[x] = resolution * (image_buffer[static_cast(floor(x)) + cen_y * stats.width] - min); double slice = resolution * (buffer[static_cast(floor(x)) + cen_y * stats.width] - min); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; //double subPixelCenter = center->x - fmod(center->x,resolution); int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We have two ways to calculate HFR. The first is the correct method but it can get quite variable within 10% due to random fluctuations of the measured star. // The second method is not truly HFR but is much more resistant to noise. // #1 Approximate HFR, accurate and reliable but quite variable to small changes in star flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Not exactly HFR, but much more stable //center->HFR = (k*resolution) * (HF/TF); break; } lastTF = TF; } return 1; } /*** Find center of stars and calculate Half Flux Radius */ int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { switch (m_DataType) { case TBYTE: return findCentroid(boundary, initStdDev, minEdgeWidth); case TSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TUSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TULONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TFLOAT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONGLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TDOUBLE: return findCentroid(boundary, initStdDev, minEdgeWidth); default: return -1; } } template int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { double threshold = 0, sum = 0, avg = 0, min = 0; int starDiameter = 0; int pixVal = 0; int minimumEdgeCount = MINIMUM_EDGE_LIMIT; auto * buffer = reinterpret_cast(m_ImageBuffer); double JMIndex = 100; #ifndef KSTARS_LITE if (histogram) JMIndex = histogram->getJMIndex(); #endif float dispersion_ratio = 1.5; QList edges; if (JMIndex < DIFFUSE_THRESHOLD) { minEdgeWidth = JMIndex * 35 + 1; minimumEdgeCount = minEdgeWidth - 1; } else { minEdgeWidth = 6; minimumEdgeCount = 4; } while (initStdDev >= 1) { minEdgeWidth--; minimumEdgeCount--; minEdgeWidth = qMax(3, minEdgeWidth); minimumEdgeCount = qMax(3, minimumEdgeCount); if (JMIndex < DIFFUSE_THRESHOLD) { // Taking the average out seems to have better result for noisy images threshold = stats.max[0] - stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = stats.min[0]; if (threshold - min < 0) { threshold = stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = 0; } dispersion_ratio = 1.4 - (MINIMUM_STDVAR - initStdDev) * 0.08; } else { threshold = stats.mean[0] + stats.stddev[0] * initStdDev * (0.3 - (MINIMUM_STDVAR - initStdDev) * 0.05); min = stats.min[0]; // Ratio between centeroid center and edge dispersion_ratio = 1.8 - (MINIMUM_STDVAR - initStdDev) * 0.2; } qCDebug(KSTARS_FITS) << "SNR: " << stats.SNR; qCDebug(KSTARS_FITS) << "The threshold level is " << threshold << "(actual " << threshold - min << ") minimum edge width" << minEdgeWidth << " minimum edge limit " << minimumEdgeCount; threshold -= min; int subX, subY, subW, subH; if (boundary.isNull()) { if (m_Mode == FITS_GUIDE || m_Mode == FITS_FOCUS) { // Only consider the central 70% subX = round(stats.width * 0.15); subY = round(stats.height * 0.15); subW = stats.width - subX; subH = stats.height - subY; } else { // Consider the complete area 100% subX = 0; subY = 0; subW = stats.width; subH = stats.height; } } else { subX = boundary.x(); subY = boundary.y(); subW = subX + boundary.width(); subH = subY + boundary.height(); } // Detect "edges" that are above threshold for (int i = subY; i < subH; i++) { starDiameter = 0; for (int j = subX; j < subW; j++) { pixVal = buffer[j + (i * stats.width)] - min; // If pixel value > threshold, let's get its weighted average if (pixVal >= threshold) { avg += j * pixVal; sum += pixVal; starDiameter++; } // Value < threshold but avg exists else if (sum > 0) { // We found a potential centroid edge if (starDiameter >= minEdgeWidth) { float center = avg / sum + 0.5; if (center > 0) { int i_center = std::floor(center); // Check if center is 10% or more brighter than edge, if not skip if (((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min) >= dispersion_ratio) && ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) + starDiameter / 2] - min) >= dispersion_ratio)) { qCDebug(KSTARS_FITS) << "Edge center is " << buffer[i_center + (i * stats.width)] - min << " Edge is " << buffer[i_center + (i * stats.width) - starDiameter / 2] - min << " and ratio is " << ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min)) << " located at X: " << center << " Y: " << i + 0.5; auto * newEdge = new Edge(); newEdge->x = center; newEdge->y = i + 0.5; newEdge->scanned = 0; newEdge->val = buffer[i_center + (i * stats.width)] - min; newEdge->width = starDiameter; newEdge->HFR = 0; newEdge->sum = sum; edges.append(newEdge); } } } // Reset avg = sum = starDiameter = 0; } } } qCDebug(KSTARS_FITS) << "Total number of edges found is: " << edges.count(); // In case of hot pixels if (edges.count() == 1 && initStdDev > 1) { initStdDev--; continue; } if (edges.count() >= MAX_EDGE_LIMIT) { qCWarning(KSTARS_FITS) << "Too many edges, aborting... " << edges.count(); qDeleteAll(edges); return -1; } if (edges.count() >= minimumEdgeCount) break; qDeleteAll(edges); edges.clear(); initStdDev--; } int cen_count = 0; int cen_x = 0; int cen_y = 0; int cen_v = 0; int cen_w = 0; int width_sum = 0; // Let's sort edges, starting with widest qSort(edges.begin(), edges.end(), greaterThan); // Now, let's scan the edges and find the maximum centroid vertically for (int i = 0; i < edges.count(); i++) { qCDebug(KSTARS_FITS) << "# " << i << " Edge at (" << edges[i]->x << "," << edges[i]->y << ") With a value of " << edges[i]->val << " and width of " << edges[i]->width << " pixels. with sum " << edges[i]->sum; // If edge scanned already, skip if (edges[i]->scanned == 1) { qCDebug(KSTARS_FITS) << "Skipping check for center " << i << " because it was already counted"; continue; } qCDebug(KSTARS_FITS) << "Investigating edge # " << i << " now ..."; // Get X, Y, and Val of edge cen_x = edges[i]->x; cen_y = edges[i]->y; cen_v = edges[i]->sum; cen_w = edges[i]->width; float avg_x = 0; float avg_y = 0; sum = 0; cen_count = 0; // Now let's compare to other edges until we hit a maxima for (int j = 0; j < edges.count(); j++) { if (edges[j]->scanned) continue; if (checkCollision(edges[j], edges[i])) { if (edges[j]->sum >= cen_v) { cen_v = edges[j]->sum; cen_w = edges[j]->width; } edges[j]->scanned = 1; cen_count++; avg_x += edges[j]->x * edges[j]->val; avg_y += edges[j]->y * edges[j]->val; sum += edges[j]->val; continue; } } int cen_limit = (MINIMUM_ROWS_PER_CENTER - (MINIMUM_STDVAR - initStdDev)); if (edges.count() < LOW_EDGE_CUTOFF_1) { if (edges.count() < LOW_EDGE_CUTOFF_2) cen_limit = 1; else cen_limit = 2; } qCDebug(KSTARS_FITS) << "center_count: " << cen_count << " and initstdDev= " << initStdDev << " and limit is " << cen_limit; if (cen_limit < 1) continue; // If centroid count is within acceptable range //if (cen_limit >= 2 && cen_count >= cen_limit) if (cen_count >= cen_limit) { // We detected a centroid, let's init it auto * rCenter = new Edge(); rCenter->x = avg_x / sum; rCenter->y = avg_y / sum; width_sum += rCenter->width; rCenter->width = cen_w; qCDebug(KSTARS_FITS) << "Found a real center with number with (" << rCenter->x << "," << rCenter->y << ")"; // Calculate Total Flux From Center, Half Flux, Full Summation double TF = 0; double HF = 0; double FSum = 0; cen_x = (int)std::floor(rCenter->x); cen_y = (int)std::floor(rCenter->y); if (cen_x < 0 || cen_x > stats.width || cen_y < 0 || cen_y > stats.height) { delete rCenter; continue; } // Complete sum along the radius //for (int k=0; k < rCenter->width; k++) for (int k = rCenter->width / 2; k >= -(rCenter->width / 2); k--) { FSum += buffer[cen_x - k + (cen_y * stats.width)] - min; //qDebug() << image_buffer[cen_x-k+(cen_y*stats.width)] - min; } // Half flux HF = FSum / 2.0; // Total flux starting from center TF = buffer[cen_y * stats.width + cen_x] - min; int pixelCounter = 1; // Integrate flux along radius axis until we reach half flux for (int k = 1; k < rCenter->width / 2; k++) { if (TF >= HF) { qCDebug(KSTARS_FITS) << "Stopping at TF " << TF << " after #" << k << " pixels."; break; } TF += buffer[cen_y * stats.width + cen_x + k] - min; TF += buffer[cen_y * stats.width + cen_x - k] - min; pixelCounter++; } // Calculate weighted Half Flux Radius rCenter->HFR = pixelCounter * (HF / TF); // Store full flux rCenter->val = FSum; qCDebug(KSTARS_FITS) << "HFR for this center is " << rCenter->HFR << " pixels and the total flux is " << FSum; starCenters.append(rCenter); } } if (starCenters.count() > 1 && m_Mode != FITS_FOCUS) { float width_avg = (float)width_sum / starCenters.count(); float lsum = 0, sdev = 0; for (auto ¢er : starCenters) lsum += (center->width - width_avg) * (center->width - width_avg); sdev = (std::sqrt(lsum / (starCenters.count() - 1))) * 4; // Reject stars > 4 * stddev foreach (Edge * center, starCenters) if (center->width > sdev) starCenters.removeOne(center); //foreach(Edge *center, starCenters) //qDebug() << center->x << "," << center->y << "," << center->width << "," << center->val << endl; } // Release memory qDeleteAll(edges); return starCenters.count(); } double FITSData::getHFR(HFRType type) { // This method is less susceptible to noise // Get HFR for the brightest star only, instead of averaging all stars // It is more consistent. // TODO: Try to test this under using a real CCD. if (starCenters.empty()) return -1; if (type == HFR_MAX) { maxHFRStar = nullptr; int maxVal = 0; int maxIndex = 0; for (int i = 0; i < starCenters.count(); i++) { if (starCenters[i]->val > maxVal) { maxIndex = i; maxVal = starCenters[i]->val; } } maxHFRStar = starCenters[maxIndex]; return static_cast(starCenters[maxIndex]->HFR); } double FSum = 0; double avgHFR = 0; // Weighted average HFR for (int i = 0; i < starCenters.count(); i++) { avgHFR += static_cast(starCenters[i]->val * starCenters[i]->HFR); FSum += starCenters[i]->val; } if (FSum != 0) { //qDebug() << "Average HFR is " << avgHFR / FSum << endl; return (avgHFR / FSum); } else return -1; } double FITSData::getHFR(int x, int y) { if (starCenters.empty()) return -1; for (int i = 0; i < starCenters.count(); i++) { if (std::fabs(starCenters[i]->x - x) <= starCenters[i]->width / 2 && std::fabs(starCenters[i]->y - y) <= starCenters[i]->width / 2) { return starCenters[i]->HFR; } } return -1; } void FITSData::applyFilter(FITSScale type, uint8_t * image, QVector * min, QVector * max) { if (type == FITS_NONE) return; QVector dataMin(3); QVector dataMax(3); if (min) dataMin = *min; if (max) dataMax = *max; switch (type) { case FITS_AUTO_STRETCH: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] - stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_CONTRAST: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] + stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_PASS: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i]; } } break; default: break; } switch (m_DataType) { case TBYTE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT8_MAX ? UINT8_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT16_MIN ? INT16_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT16_MAX ? INT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TUSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT16_MAX ? UINT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT_MIN ? INT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT_MAX ? INT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TULONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT_MAX ? UINT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TFLOAT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < FLT_MIN ? FLT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > FLT_MAX ? FLT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONGLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < LLONG_MIN ? LLONG_MIN : dataMin[i]; dataMax[i] = dataMax[i] > LLONG_MAX ? LLONG_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TDOUBLE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < DBL_MIN ? DBL_MIN : dataMin[i]; dataMax[i] = dataMax[i] > DBL_MAX ? DBL_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; default: return; } if (min != nullptr) *min = dataMin; if (max != nullptr) *max = dataMax; } template void FITSData::applyFilter(FITSScale type, uint8_t * targetImage, QVector * targetMin, QVector * targetMax) { bool calcStats = false; T * image = nullptr; if (targetImage) image = reinterpret_cast(targetImage); else { image = reinterpret_cast(m_ImageBuffer); calcStats = true; } T min[3], max[3]; for (int i = 0; i < 3; i++) { min[i] = (*targetMin)[i] < std::numeric_limits::min() ? std::numeric_limits::min() : (*targetMin)[i]; max[i] = (*targetMax)[i] > std::numeric_limits::max() ? std::numeric_limits::max() : (*targetMax)[i]; } // Create N threads const uint8_t nThreads = 16; uint32_t width = stats.width; uint32_t height = stats.height; //QTime timer; //timer.start(); switch (type) { case FITS_AUTO: case FITS_LINEAR: case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: case FITS_LOG: case FITS_SQRT: case FITS_HIGH_PASS: { // List of futures QList> futures; QVector coeff(3); if (type == FITS_LOG) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / std::log(1 + max[i]); } else if (type == FITS_SQRT) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / sqrt(max[i]); } for (int n = 0; n < m_Channels; n++) { if (type == FITS_HIGH_PASS) min[n] = stats.mean[n]; uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); T * runningBuffer = image + cStart; if (type == FITS_LOG) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * std::log(1 + qBound(min[n], a, max[n])))), max[n]); })); runningBuffer += tStride; } } else if (type == FITS_SQRT) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * a)), max[n]); })); } runningBuffer += tStride; } else { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, n](T & a) { a = qBound(min[n], a, max[n]); })); runningBuffer += tStride; } } } for (int i = 0; i < nThreads * m_Channels; i++) futures[i].waitForFinished(); if (calcStats) { for (int i = 0; i < 3; i++) { stats.min[i] = min[i]; stats.max[i] = max[i]; } if (type != FITS_AUTO && type != FITS_LINEAR) runningAverageStdDev(); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); uint32_t row = 0; uint32_t index = 0; for (int i = 0; i < m_Channels; i++) { uint32_t offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min[i]) / histogram->getBinWidth(i); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min[i], static_cast(round(coeff * cumulativeFreq[bufferVal])), max[i]); } } } #endif } if (calcStats) calculateStats(true); break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { uint8_t BBP = stats.bytesPerPixel; auto * extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < m_Channels; ch++) { uint32_t offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9 * sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; default: break; } #if 0 } else { uint32_t index = 0, row = 0, offset = 0; switch (type) { case FITS_AUTO: case FITS_LINEAR: { for (uint8_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, image[index], max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; } } break; case FITS_LOG: { double coeff = max / log(1 + max); for (int i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, static_cast(round(coeff * log(1 + qBound(min, image[index], max)))), max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; case FITS_SQRT: { double coeff = max / sqrt(max); for (int i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, static_cast(round(coeff * image[index])), max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; // Only difference is how min and max are set case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: { for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) image[k + row] = qBound(min, image[k + row], max); } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min) / histogram->getBinWidth(); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min, static_cast(round(coeff * cumulativeFreq[bufferVal])), max); } } } #endif } if (calcStats) calculateStats(true); break; case FITS_HIGH_PASS: { min = stats.mean[0]; for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, image[index], max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { int BBP = stats.bytesPerPixel; T * extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < channels; ch++) { offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9 * sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; case FITS_CUSTOM: default: return; break; } } qCInfo(KSTARS_FITS) << filename << "Apply Filter calculation took" << timer.elapsed() << "ms"; #endif } QList FITSData::getStarCentersInSubFrame(QRect subFrame) const { QList starCentersInSubFrame; for (int i = 0; i < starCenters.count(); i++) { int x = static_cast(starCenters[i]->x); int y = static_cast(starCenters[i]->y); if(subFrame.contains(x, y)) { starCentersInSubFrame.append(starCenters[i]); } } return starCentersInSubFrame; } void FITSData::getCenterSelection(int * x, int * y) { if (starCenters.count() == 0) return; auto * pEdge = new Edge(); pEdge->x = *x; pEdge->y = *y; pEdge->width = 1; foreach (Edge * center, starCenters) if (checkCollision(pEdge, center)) { *x = static_cast(center->x); *y = static_cast(center->y); break; } delete (pEdge); } bool FITSData::checkForWCS() { #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB int status = 0; char * header; int nkeyrec, nreject, nwcs; if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) { free(header); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } HasWCS = true; #endif #endif return HasWCS; } bool FITSData::loadWCS() { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) if (WCSLoaded) { qWarning() << "WCS data already loaded"; return true; } qCDebug(KSTARS_FITS) << "Started WCS Data Processing..."; int status = 0; char * header; int nkeyrec, nreject, nwcs, stat[2]; double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2]; int w = width(); int h = height(); if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) { free(header); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } delete[] wcs_coord; wcs_coord = new wcs_point[w * h]; if (wcs_coord == nullptr) { lastError = "Not enough memory for WCS data!"; return false; } wcs_point * p = wcs_coord; for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { pixcrd[0] = j; pixcrd[1] = i; if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); } else { p->ra = world[0]; p->dec = world[1]; p++; } } } findObjectsInImage(&world[0], phi, theta, &imgcrd[0], &pixcrd[0], &stat[0]); WCSLoaded = true; HasWCS = true; qCDebug(KSTARS_FITS) << "Finished WCS Data processing..."; return true; #else return false; #endif } bool FITSData::wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2]; if (wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } worldcrd[0] = wcsCoord.ra0().Degrees(); worldcrd[1] = wcsCoord.dec0().Degrees(); if ((status = wcss2p(wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { lastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } wcsImagePoint.setX(imgcrd[0]); wcsImagePoint.setY(imgcrd[1]); wcsPixelPoint.setX(pixcrd[0]); wcsPixelPoint.setY(pixcrd[1]); return true; #else Q_UNUSED(wcsCoord); Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsImagePoint); return false; #endif } bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], phi, pixcrd[2], theta, world[2]; if (wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } pixcrd[0] = wcsPixelPoint.x(); pixcrd[1] = wcsPixelPoint.y(); if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } else { wcsCoord.setRA0(world[0] / 15.0); wcsCoord.setDec0(world[1]); } return true; #else Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsCoord); return false; #endif } #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) void FITSData::findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]) { int w = width(); int h = height(); int status = 0; char date[64]; KSNumbers * num = nullptr; if (fits_read_keyword(fptr, "DATE-OBS", date, nullptr, &status) == 0) { QString tsString(date); tsString = tsString.remove('\'').trimmed(); // Add Zulu time to indicate UTC tsString += "Z"; QDateTime ts = QDateTime::fromString(tsString, Qt::ISODate); if (ts.isValid()) num = new KSNumbers(KStarsDateTime(ts).djd()); } if (num == nullptr) num = new KSNumbers(KStarsData::Instance()->ut().djd()); //Set to current time if the above does not work. SkyMapComposite * map = KStarsData::Instance()->skyComposite(); wcs_point * wcs_coord = getWCSCoord(); if (wcs_coord != nullptr) { int size = w * h; objList.clear(); SkyPoint p1; p1.setRA0(dms(wcs_coord[0].ra)); p1.setDec0(dms(wcs_coord[0].dec)); p1.updateCoordsNow(num); SkyPoint p2; p2.setRA0(dms(wcs_coord[size - 1].ra)); p2.setDec0(dms(wcs_coord[size - 1].dec)); p2.updateCoordsNow(num); QList list = map->findObjectsInArea(p1, p2); foreach (SkyObject * object, list) { int type = object->type(); if (object->name() == "star" || type == SkyObject::PLANET || type == SkyObject::ASTEROID || type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON || type == SkyObject::SATELLITE) { //DO NOT DISPLAY, at least for now, because these things move and change. } int x = -100; int y = -100; world[0] = object->ra0().Degrees(); world[1] = object->dec0().Degrees(); if ((status = wcss2p(wcs, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { fprintf(stderr, "wcsp2s ERROR %d: %s.\n", status, wcs_errmsg[status]); } else { x = pixcrd[0]; //The X and Y are set to the found position if it does work. y = pixcrd[1]; } if (x > 0 && y > 0 && x < w && y < h) objList.append(new FITSSkyObject(object, x, y)); } } delete (num); } #endif QList FITSData::getSkyObjects() { return objList; } FITSSkyObject::FITSSkyObject(SkyObject * object, int xPos, int yPos) : QObject() { skyObjectStored = object; xLoc = xPos; yLoc = yPos; } SkyObject * FITSSkyObject::skyObject() { return skyObjectStored; } int FITSSkyObject::x() { return xLoc; } int FITSSkyObject::y() { return yLoc; } void FITSSkyObject::setX(int xPos) { xLoc = xPos; } void FITSSkyObject::setY(int yPos) { yLoc = yPos; } int FITSData::getFlipVCounter() const { return flipVCounter; } void FITSData::setFlipVCounter(int value) { flipVCounter = value; } int FITSData::getFlipHCounter() const { return flipHCounter; } void FITSData::setFlipHCounter(int value) { flipHCounter = value; } int FITSData::getRotCounter() const { return rotCounter; } void FITSData::setRotCounter(int value) { rotCounter = value; } /* Rotate an image by 90, 180, or 270 degrees, with an optional * reflection across the vertical or horizontal axis. * verbose generates extra info on stdout. * return nullptr if successful or rotated image. */ template bool FITSData::rotFITS(int rotate, int mirror) { int ny, nx; int x1, y1, x2, y2; uint8_t * rotimage = nullptr; int offset = 0; if (rotate == 1) rotate = 90; else if (rotate == 2) rotate = 180; else if (rotate == 3) rotate = 270; else if (rotate < 0) rotate = rotate + 360; nx = stats.width; ny = stats.height; int BBP = stats.bytesPerPixel; /* Allocate buffer for rotated image */ rotimage = new uint8_t[stats.samples_per_channel * m_Channels * BBP]; if (rotimage == nullptr) { qWarning() << "Unable to allocate memory for rotated image buffer!"; return false; } auto * rotBuffer = reinterpret_cast(rotimage); auto * buffer = reinterpret_cast(m_ImageBuffer); /* Mirror image without rotation */ if (rotate < 45 && rotate > -45) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } /* Rotate by 90 degrees */ else if (rotate >= 45 && rotate < 135) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* Rotate by 180 degrees */ else if (rotate >= 135 && rotate < 225) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } } /* Rotate by 270 degrees */ else if (rotate >= 225 && rotate < 315) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = y1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* If rotating by more than 315 degrees, assume top-bottom reflection */ else if (rotate >= 315 && mirror) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) { x2 = y1; y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } delete[] m_ImageBuffer; m_ImageBuffer = rotimage; return true; } void FITSData::rotWCSFITS(int angle, int mirror) { int status = 0; char comment[100]; double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2; int WCS_DECIMALS = 6; naxis1 = stats.width; naxis2 = stats.height; if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { // No WCS keywords return; } /* Reset CROTAn and CD matrix if axes have been exchanged */ if (angle == 90) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); } status = 0; /* Negate rotation angle if mirrored */ if (mirror != 0) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status); } status = 0; /* Unbin CRPIX and CD matrix */ if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) { if (ctemp1 != 1.0) { if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status)) if (ctemp1 == ctemp2) { double ltv1 = 0.0; double ltv2 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "LTV1", <v1, comment, &status)) fits_delete_key(fptr, "LTV1", &status); if (!fits_read_key_dbl(fptr, "LTV2", <v2, comment, &status)) fits_delete_key(fptr, "LTV2", &status); status = 0; if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); status = 0; fits_delete_key(fptr, "LTM1_1", &status); fits_delete_key(fptr, "LTM1_2", &status); } } } status = 0; /* Reset CRPIXn */ if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } } } status = 0; /* Reset CDELTn (degrees per pixel) */ if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Reset CD matrix, if present */ ctemp1 = 0.0; ctemp2 = 0.0; ctemp3 = 0.0; ctemp4 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status); fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status); fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status); status = 0; if (mirror != 0) { if (angle == 0) { fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); } else if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Delete any polynomial solution */ /* (These could maybe be switched, but I don't want to work them out yet */ status = 0; if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status)) { int i; char keyword[16]; for (i = 1; i < 13; i++) { sprintf(keyword, "CO1_%d", i); fits_delete_key(fptr, keyword, &status); } for (i = 1; i < 13; i++) { sprintf(keyword, "CO2_%d", i); fits_delete_key(fptr, keyword, &status); } } } uint8_t * FITSData::getImageBuffer() { return m_ImageBuffer; } void FITSData::setImageBuffer(uint8_t * buffer) { delete[] m_ImageBuffer; m_ImageBuffer = buffer; } bool FITSData::checkDebayer() { int status = 0; char bayerPattern[64]; // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status)) return false; if (stats.bitpix != 16 && stats.bitpix != 8) { KSNotification::error(i18n("Only 8 and 16 bits bayered images supported."), i18n("Debayer error")); return false; } QString pattern(bayerPattern); pattern = pattern.remove('\'').trimmed(); if (pattern == "RGGB") debayerParams.filter = DC1394_COLOR_FILTER_RGGB; else if (pattern == "GBRG") debayerParams.filter = DC1394_COLOR_FILTER_GBRG; else if (pattern == "GRBG") debayerParams.filter = DC1394_COLOR_FILTER_GRBG; else if (pattern == "BGGR") debayerParams.filter = DC1394_COLOR_FILTER_BGGR; // We return unless we find a valid pattern else { KSNotification::error(i18n("Unsupported bayer pattern %1.", pattern), i18n("Debayer error")); return false; } fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status); fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status); HasDebayer = true; return true; } void FITSData::getBayerParams(BayerParams * param) { param->method = debayerParams.method; param->filter = debayerParams.filter; param->offsetX = debayerParams.offsetX; param->offsetY = debayerParams.offsetY; } void FITSData::setBayerParams(BayerParams * param) { debayerParams.method = param->method; debayerParams.filter = param->filter; debayerParams.offsetX = param->offsetX; debayerParams.offsetY = param->offsetY; } bool FITSData::debayer() { if (bayerBuffer == nullptr) { int anynull = 0, status = 0; bayerBuffer = m_ImageBuffer; if (fits_read_img(fptr, m_DataType, 1, stats.samples_per_channel, nullptr, bayerBuffer, &anynull, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error")); return false; } } switch (m_DataType) { case TBYTE: return debayer_8bit(); case TUSHORT: return debayer_16bit(); default: return false; } return false; } bool FITSData::debayer_8bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint8_t * dc1394_source = bayerBuffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_8bit(dc1394_source, destinationBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } // Data in R1G1B1, we need to copy them into 3 layers for FITS uint8_t * rBuff = m_ImageBuffer; uint8_t * gBuff = m_ImageBuffer + (stats.width * stats.height); uint8_t * bBuff = m_ImageBuffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = destinationBuffer[i]; *gBuff++ = destinationBuffer[i + 1]; *bBuff++ = destinationBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } bool FITSData::debayer_16bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; auto * buffer = reinterpret_cast(bayerBuffer); auto * dstBuffer = reinterpret_cast(destinationBuffer); if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint16_t * dc1394_source = buffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_16bit(dc1394_source, dstBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method, 16); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } buffer = reinterpret_cast(m_ImageBuffer); // Data in R1G1B1, we need to copy them into 3 layers for FITS uint16_t * rBuff = buffer; uint16_t * gBuff = buffer + (stats.width * stats.height); uint16_t * bBuff = buffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = dstBuffer[i]; *gBuff++ = dstBuffer[i + 1]; *bBuff++ = dstBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } double FITSData::getADU() const { double adu = 0; for (int i = 0; i < m_Channels; i++) adu += stats.mean[i]; return (adu / static_cast(m_Channels)); } /* CannyDetector, Implementation of Canny edge detector in Qt/C++. * Copyright (C) 2015 Gonzalo Exequiel Pedone * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Email : hipersayan DOT x AT gmail DOT com * Web-Site: http://github.com/hipersayanX/CannyDetector */ #if 0 void FITSData::sobel(const QImage &image, QVector &gradient, QVector &direction) { int size = image.width() * image.height(); gradient.resize(size); direction.resize(size); for (int y = 0; y < image.height(); y++) { size_t yOffset = y * image.width(); const quint8 * grayLine = image.constBits() + yOffset; const quint8 * grayLine_m1 = y < 1 ? grayLine : grayLine - image.width(); const quint8 * grayLine_p1 = y >= image.height() - 1 ? grayLine : grayLine + image.width(); int * gradientLine = gradient.data() + yOffset; int * directionLine = direction.data() + yOffset; for (int x = 0; x < image.width(); x++) { int x_m1 = x < 1 ? x : x - 1; int x_p1 = x >= image.width() - 1 ? x : x + 1; int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; gradientLine[x] = qAbs(gradX) + qAbs(gradY); /* Gradient directions are classified in 4 possible cases * * dir 0 * * x x x * - - - * x x x * * dir 1 * * x x / * x / x * / x x * * dir 2 * * \ x x * x \ x * x x \ * * dir 3 * * x | x * x | x * x | x */ if (gradX == 0 && gradY == 0) directionLine[x] = 0; else if (gradX == 0) directionLine[x] = 3; else { qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; if (a >= -22.5 && a < 22.5) directionLine[x] = 0; else if (a >= 22.5 && a < 67.5) directionLine[x] = 1; else if (a >= -67.5 && a < -22.5) directionLine[x] = 2; else directionLine[x] = 3; } } } } #endif template void FITSData::sobel(QVector &gradient, QVector &direction) { //int size = image.width() * image.height(); gradient.resize(stats.samples_per_channel); direction.resize(stats.samples_per_channel); for (int y = 0; y < stats.height; y++) { size_t yOffset = y * stats.width; const T * grayLine = reinterpret_cast(m_ImageBuffer) + yOffset; const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width; const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width; float * gradientLine = gradient.data() + yOffset; float * directionLine = direction.data() + yOffset; for (int x = 0; x < stats.width; x++) { int x_m1 = x < 1 ? x : x - 1; int x_p1 = x >= stats.width - 1 ? x : x + 1; int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; gradientLine[x] = qAbs(gradX) + qAbs(gradY); /* Gradient directions are classified in 4 possible cases * * dir 0 * * x x x * - - - * x x x * * dir 1 * * x x / * x / x * / x x * * dir 2 * * \ x x * x \ x * x x \ * * dir 3 * * x | x * x | x * x | x */ if (gradX == 0 && gradY == 0) directionLine[x] = 0; else if (gradX == 0) directionLine[x] = 3; else { qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; if (a >= -22.5 && a < 22.5) directionLine[x] = 0; else if (a >= 22.5 && a < 67.5) directionLine[x] = 2; else if (a >= -67.5 && a < -22.5) directionLine[x] = 1; else directionLine[x] = 3; } } } } int FITSData::partition(int width, int height, QVector &gradient, QVector &ids) { int id = 0; for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { int index = x + y * width; float val = gradient[index]; if (val > 0 && ids[index] == 0) { trace(width, height, ++id, gradient, ids, x, y); } } } // Return max id return id; } void FITSData::trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) { int yOffset = y * width; float * cannyLine = image.data() + yOffset; int * idLine = ids.data() + yOffset; if (idLine[x] != 0) return; idLine[x] = id; for (int j = -1; j < 2; j++) { int nextY = y + j; if (nextY < 0 || nextY >= height) continue; float * cannyLineNext = cannyLine + j * width; for (int i = -1; i < 2; i++) { int nextX = x + i; if (i == j || nextX < 0 || nextX >= width) continue; if (cannyLineNext[nextX] > 0) { // Trace neighbors. trace(width, height, id, image, ids, nextX, nextY); } } } } QString FITSData::getLastError() const { return lastError; } bool FITSData::getAutoRemoveTemporaryFITS() const { return autoRemoveTemporaryFITS; } void FITSData::setAutoRemoveTemporaryFITS(bool value) { autoRemoveTemporaryFITS = value; } #if 0 QVector FITSData::thinning(int width, int height, const QVector &gradient, const QVector &direction) { QVector thinned(gradient.size()); for (int y = 0; y < height; y++) { int yOffset = y * width; const int * gradientLine = gradient.constData() + yOffset; const int * gradientLine_m1 = y < 1 ? gradientLine : gradientLine - width; const int * gradientLine_p1 = y >= height - 1 ? gradientLine : gradientLine + width; const int * directionLine = direction.constData() + yOffset; int * thinnedLine = thinned.data() + yOffset; for (int x = 0; x < width; x++) { int x_m1 = x < 1 ? 0 : x - 1; int x_p1 = x >= width - 1 ? x : x + 1; int direction = directionLine[x]; int pixel = 0; if (direction == 0) { /* x x x * - - - * x x x */ if (gradientLine[x] < gradientLine[x_m1] || gradientLine[x] < gradientLine[x_p1]) pixel = 0; else pixel = gradientLine[x]; } else if (direction == 1) { /* x x / * x / x * / x x */ if (gradientLine[x] < gradientLine_m1[x_p1] || gradientLine[x] < gradientLine_p1[x_m1]) pixel = 0; else pixel = gradientLine[x]; } else if (direction == 2) { /* \ x x * x \ x * x x \ */ if (gradientLine[x] < gradientLine_m1[x_m1] || gradientLine[x] < gradientLine_p1[x_p1]) pixel = 0; else pixel = gradientLine[x]; } else { /* x | x * x | x * x | x */ if (gradientLine[x] < gradientLine_m1[x] || gradientLine[x] < gradientLine_p1[x]) pixel = 0; else pixel = gradientLine[x]; } thinnedLine[x] = pixel; } } return thinned; } QVector FITSData::threshold(int thLow, int thHi, const QVector &image) { QVector thresholded(image.size()); for (int i = 0; i < image.size(); i++) thresholded[i] = image[i] <= thLow ? 0 : image[i] >= thHi ? 255 : 127; return thresholded; } QVector FITSData::hysteresis(int width, int height, const QVector &image) { QVector canny(image); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) trace(width, height, canny, x, y); // Remaining gray pixels becomes black. for (int i = 0; i < canny.size(); i++) if (canny[i] == 127) canny[i] = 0; return canny; } #endif template void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-align" auto * buffer = (T *)getImageBuffer(); #pragma GCC diagnostic pop const T limit = std::numeric_limits::max(); T bMin = dataMin < 0 ? 0 : dataMin; T bMax = dataMax > limit ? limit : dataMax; uint16_t w = width(); uint16_t h = height(); uint32_t size = w * h; double val; if (channels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { unsigned char * scanLine = image.scanLine(j); for (int i = 0; i < w; i++) { val = qBound(bMin, buffer[j * w + i], bMax); val = val * scale + zero; scanLine[i] = qBound(0, (unsigned char)val, 255); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { auto * scanLine = reinterpret_cast((image.scanLine(j))); for (int i = 0; i < w; i++) { rval = qBound(bMin, buffer[j * w + i], bMax); gval = qBound(bMin, buffer[j * w + i + size], bMax); bval = qBound(bMin, buffer[j * w + i + size * 2], bMax); value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero); scanLine[i] = value; } } } } QImage FITSData::FITSToImage(const QString &filename) { QImage fitsImage; double min, max; FITSData data; QFuture future = data.loadFITS(filename); // Wait synchronously future.waitForFinished(); if (future.result() == false) return fitsImage; data.getMinMax(&min, &max); if (min == max) { fitsImage.fill(Qt::white); return fitsImage; } if (data.channels() == 1) { fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8); fitsImage.setColorCount(256); for (int i = 0; i < 256; i++) fitsImage.setColor(i, qRgb(i, i, i)); } else { fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32); } double dataMin = data.stats.mean[0] - data.stats.stddev[0]; double dataMax = data.stats.mean[0] + data.stats.stddev[0] * 3; double bscale = 255. / (dataMax - dataMin); double bzero = (-dataMin) * (255. / (dataMax - dataMin)); // Long way to do this since we do not want to use templated functions here switch (data.property("dataType").toInt()) { case TBYTE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TUSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TULONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TFLOAT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONGLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TDOUBLE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; default: break; } return fitsImage; } bool FITSData::createWCSFile(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale) { int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; char errMsg[512]; qCInfo(KSTARS_FITS) << "Creating new WCS file:" << newWCSFile << "with parameters Orientation:" << orientation << "RA:" << ra << "DE:" << dec << "Pixel Scale:" << pixscale; nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, QString('!' + newWCSFile).toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_copy_file(fptr, new_fptr, 1, 1, 1, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } status = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; qCDebug(KSTARS_FITS) << "Removing FITS File: " << m_Filename; } m_Filename = newWCSFile; m_isTemporary = true; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); // ISO Date if (fits_write_date(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_flush_file(fptr, &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished creating WCS file: " << newWCSFile; return true; } bool FITSData::contains(const QPointF &point) const { return (point.x() >= 0 && point.y() >= 0 && point.x() <= stats.width && point.y() <= stats.height); } int FITSData::findSEPStars(const QRect &boundary) { int x = 0, y = 0, w = stats.width, h = stats.height, maxRadius = 50; if (!boundary.isNull()) { x = boundary.x(); y = boundary.y(); w = boundary.width(); h = boundary.height(); maxRadius = w; } auto * data = new float[w * h]; switch (stats.bitpix) { case BYTE_IMG: getFloatBuffer(data, x, y, w, h); break; case SHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case USHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case LONG_IMG: getFloatBuffer(data, x, y, w, h); break; case ULONG_IMG: getFloatBuffer(data, x, y, w, h); break; case FLOAT_IMG: delete [] data; data = reinterpret_cast(m_ImageBuffer); break; case LONGLONG_IMG: getFloatBuffer(data, x, y, w, h); break; case DOUBLE_IMG: getFloatBuffer(data, x, y, w, h); break; default: delete [] data; return -1; } float * imback = nullptr; double * flux = nullptr, *fluxerr = nullptr, *area = nullptr; short * flag = nullptr; short flux_flag = 0; int status = 0; sep_bkg * bkg = nullptr; sep_catalog * catalog = nullptr; float conv[] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; double flux_fractions[2] = {0}; double requested_frac[2] = { 0.5, 0.99 }; QList edges; // #0 Create SEP Image structure sep_image im = {data, nullptr, nullptr, SEP_TFLOAT, 0, 0, w, h, 0.0, SEP_NOISE_NONE, 1.0, 0.0}; // #1 Background estimate status = sep_background(&im, 64, 64, 3, 3, 0.0, &bkg); if (status != 0) goto exit; // #2 Background evaluation imback = (float *)malloc((w * h) * sizeof(float)); status = sep_bkg_array(bkg, imback, SEP_TFLOAT); if (status != 0) goto exit; // #3 Background subtraction status = sep_bkg_subarray(bkg, im.data, im.dtype); if (status != 0) goto exit; // #4 Source Extraction // Note that we set deblend_cont = 1.0 to turn off deblending. status = sep_extract(&im, 2 * bkg->globalrms, SEP_THRESH_ABS, 10, conv, 3, 3, SEP_FILTER_CONV, 32, 1.0, 1, 1.0, &catalog); if (status != 0) goto exit; #if 0 // #4 Aperture photometry im.noise = &(bkg->globalrms); /* set image noise level */ im.ndtype = SEP_TFLOAT; fluxt = flux = (double *)malloc(catalog->nobj * sizeof(double)); fluxerrt = fluxerr = (double *)malloc(catalog->nobj * sizeof(double)); areat = area = (double *)malloc(catalog->nobj * sizeof(double)); flagt = flag = (short *)malloc(catalog->nobj * sizeof(short)); for (int i = 0; i < catalog->nobj; i++, fluxt++, fluxerrt++, flagt++, areat++) sep_sum_circle(&im, catalog->x[i], catalog->y[i], 10.0, 5, 0, fluxt, fluxerrt, areat, flagt); #endif // TODO // Must detect edge detection // Must limit to brightest 100 (by flux) centers // Should probably use ellipse to draw instead of simple circle? // Useful for galaxies and also elenogated stars. for (int i = 0; i < catalog->nobj; i++) { double flux = catalog->flux[i]; // Get HFR sep_flux_radius(&im, catalog->x[i], catalog->y[i], maxRadius, 5, 0, &flux, requested_frac, 2, flux_fractions, &flux_flag); auto * center = new Edge(); center->x = catalog->x[i] + x + 0.5; center->y = catalog->y[i] + y + 0.5; center->val = catalog->peak[i]; center->sum = flux; center->HFR = center->width = flux_fractions[0]; if (flux_fractions[1] < maxRadius) center->width = flux_fractions[1] * 2; edges.append(center); } // Let's sort edges, starting with widest qSort(edges.begin(), edges.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->width > edge2->width;}); // Take only the first 100 stars { int starCount = qMin(100, edges.count()); for (int i = 0; i < starCount; i++) starCenters.append(edges[i]); } edges.clear(); qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << "#" << "#X" << "#Y" << "#Flux" << "#Width" << "#HFR"; for (int i = 0; i < starCenters.count(); i++) qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << i << starCenters[i]->x << starCenters[i]->y << starCenters[i]->sum << starCenters[i]->width << starCenters[i]->HFR; exit: if (stats.bitpix != FLOAT_IMG) delete [] data; sep_bkg_free(bkg); sep_catalog_free(catalog); free(imback); free(flux); free(fluxerr); free(area); free(flag); if (status != 0) { char errorMessage[512]; sep_get_errmsg(status, errorMessage); qCritical(KSTARS_FITS) << errorMessage; return -1; } return starCenters.count(); } template void FITSData::getFloatBuffer(float * buffer, int x, int y, int w, int h) { auto * rawBuffer = reinterpret_cast(m_ImageBuffer); float * floatPtr = buffer; int x2 = x + w; int y2 = y + h; for (int y1 = y; y1 < y2; y1++) { int offset = y1 * stats.width; for (int x1 = x; x1 < x2; x1++) { *floatPtr++ = rawBuffer[offset + x1]; } } } void FITSData::saveStatistics(Statistic &other) { other = stats; } void FITSData::restoreStatistics(Statistic &other) { stats = other; } diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h index 82c2c1b6d..c0a21b792 100644 --- a/kstars/fitsviewer/fitsdata.h +++ b/kstars/fitsviewer/fitsdata.h @@ -1,447 +1,450 @@ /*************************************************************************** fitsimage.cpp - FITS Image ------------------- begin : Tue Feb 24 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 "config-kstars.h" #include "bayer.h" #include "fitscommon.h" #ifdef WIN32 // This header must be included before fitsio.h to avoid compiler errors with Visual Studio #include #endif #include #include #include #include #include #ifndef KSTARS_LITE #include #ifdef HAVE_WCSLIB #include #endif #endif #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QProgressDialog; class SkyObject; class SkyPoint; class FITSHistogram; typedef struct { float ra; float dec; } wcs_point; class Edge { public: float x; float y; int val; int scanned; float width; float HFR; float sum; }; class FITSSkyObject : public QObject { Q_OBJECT public: explicit FITSSkyObject(SkyObject *object, int xPos, int yPos); SkyObject *skyObject(); int x(); int y(); void setX(int xPos); void setY(int yPos); private: SkyObject *skyObjectStored; int xLoc; int yLoc; }; class FITSData : public QObject { Q_OBJECT // Name of FITS file Q_PROPERTY(QString filename READ filename) // Size of file in bytes Q_PROPERTY(qint64 size READ size) // Width in pixels Q_PROPERTY(quint16 width READ width) // Height in pixels Q_PROPERTY(quint16 height READ height) // FITS MODE --> Normal, Focus, Guide..etc Q_PROPERTY(FITSMode mode MEMBER m_Mode) // 1 channel (grayscale) or 3 channels (RGB) Q_PROPERTY(quint8 channels READ channels) // Data type (BYTE, SHORT, INT..etc) Q_PROPERTY(quint32 dataType MEMBER m_DataType) // Bits per pixel Q_PROPERTY(quint8 bpp READ bpp WRITE setBPP) // Does FITS have WSC header? Q_PROPERTY(bool hasWCS READ hasWCS) // Does FITS have bayer data? Q_PROPERTY(bool hasDebyaer READ hasDebayer) public: explicit FITSData(FITSMode fitsMode = FITS_NORMAL); explicit FITSData(const FITSData *other); ~FITSData(); /** Structure to hold FITS Header records */ typedef struct { QString key; /** FITS Header Key */ QVariant value; /** FITS Header Value */ QString comment; /** FITS Header Comment, if any */ } Record; /// Stats struct to hold statisical data about the FITS data typedef struct { double min[3] = {0}, max[3] = {0}; double mean[3] = {0}; double stddev[3] = {0}; double median[3] = {0}; double SNR { 0 }; int bitpix { 8 }; int bytesPerPixel { 1 }; int ndim { 2 }; int64_t size { 0 }; uint32_t samples_per_channel { 0 }; uint16_t width { 0 }; uint16_t height { 0 }; } Statistic; /** * @brief loadFITS Loading FITS file asynchronously. * @param inFilename Path to FITS file (or compressed fits.gz) * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return A QFuture that can be watched until the async operation is complete. */ QFuture loadFITS(const QString &inFilename, bool silent = true); /* Save FITS */ int saveFITS(const QString &newFilename); /* Rescale image lineary from image_buffer, fit to window if desired */ int rescale(FITSZoom type); /* Calculate stats */ void calculateStats(bool refresh = false); /* Check if a particular point exists within the image */ bool contains(const QPointF &point) const; // Access functions void clearImageBuffers(); void setImageBuffer(uint8_t *buffer); uint8_t *getImageBuffer(); // Statistics void saveStatistics(Statistic &other); void restoreStatistics(Statistic &other); uint16_t width() const { return stats.width;} uint16_t height() const { return stats.height;} int64_t size() const { return stats.size; } int channels() const { return m_Channels; } double getMin(uint8_t channel = 0) const { return stats.min[channel]; } double getMax(uint8_t channel = 0) const { return stats.max[channel]; } void setMinMax(double newMin, double newMax, uint8_t channel = 0); void getMinMax(double *min, double *max, uint8_t channel = 0) const { *min = stats.min[channel]; *max = stats.max[channel]; } void setStdDev(double value, uint8_t channel = 0) { stats.stddev[channel] = value; } double getStdDev(uint8_t channel = 0) const { return stats.stddev[channel]; } void setMean(double value, uint8_t channel = 0) { stats.mean[channel] = value; } double getMean(uint8_t channel = 0) const { return stats.mean[channel]; } void setMedian(double val, uint8_t channel = 0) { stats.median[channel] = val; } double getMedian(uint8_t channel = 0) const { return stats.median[channel]; } int getBytesPerPixel() const { return stats.bytesPerPixel; } void setSNR(double val) { stats.SNR = val; } double getSNR() const { return stats.SNR; } void setBPP(uint8_t value) { stats.bitpix = value; } uint32_t bpp() const { return stats.bitpix; } double getADU() const; // FITS Record bool getRecordValue(const QString &key, QVariant &value) const; const QList & getRecords() const {return records;} // Star Detection - Native KStars implementation void setStarAlgorithm(StarAlgorithm algorithm){ starAlgorithm = algorithm; } int getDetectedStars() const { return starCenters.count(); } bool areStarsSearched() const { return starsSearched; } void appendStar(Edge *newCenter) { starCenters.append(newCenter); } QList getStarCenters() const { return starCenters; } QList getStarCentersInSubFrame(QRect subFrame) const; int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &trackingBox = QRect()); void getCenterSelection(int *x, int *y); int findOneStar(const QRect &boundary); // Star Detection - Partially customized Canny edge detection algorithm static int findCannyStar(FITSData *data, const QRect &boundary = QRect()); template static int findCannyStar(FITSData *data, const QRect &boundary); // Use SEP (Sextractor Library) to find stars template void getFloatBuffer(float *buffer, int x, int y, int w, int h); int findSEPStars(const QRect &boundary = QRect()); + // Apply ring filter to searched stars + int filterStars(const float innerRadius, const float outerRadius); + // Half Flux Radius Edge *getMaxHFRStar() const { return maxHFRStar; } double getHFR(HFRType type = HFR_AVERAGE); double getHFR(int x, int y); // WCS // Check if image has valid WCS header information and set HasWCS accordingly. Call in loadFITS() bool checkForWCS(); // Does image have valid WCS? bool hasWCS() { return HasWCS; } // Load WCS data bool loadWCS(); // Is WCS Image loaded? bool isWCSLoaded() { return WCSLoaded; } wcs_point *getWCSCoord() { return wcs_coord; } /** * @brief wcsToPixel Given J2000 (RA0,DE0) coordinates. Find in the image the corresponding pixel coordinates. * @param wcsCoord Coordinates of target * @param wcsPixelPoint Return XY FITS coordinates * @param wcsImagePoint Return XY Image coordinates * @return True if conversion is successful, false otherwise. */ bool wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint); /** * @brief pixelToWCS Convert Pixel coordinates to J2000 world coordinates * @param wcsPixelPoint Pixel coordinates in XY Image space. * @param wcsCoord Store back WCS world coordinate in wcsCoord * @return True if successful, false otherwise. */ bool pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord); /** * @brief createWCSFile Create a new FITS file given the WCS information supplied. Construct the necessary WCS keywords and save the * new file as the current active file * @param newWCSFile New file name * @param orientation Solver orientation, degrees E of N. * @param ra J2000 Right Ascension * @param dec J2000 Declination * @param pixscale Pixel scale in arcsecs per pixel * @return True if file is successfully created and saved, false otherwise. */ bool createWCSFile(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale); // Debayer bool hasDebayer() { return HasDebayer; } bool debayer(); bool debayer_8bit(); bool debayer_16bit(); void getBayerParams(BayerParams *param); void setBayerParams(BayerParams *param); // Histogram #ifndef KSTARS_LITE void setHistogram(FITSHistogram *inHistogram) { histogram = inHistogram; } #endif // Filter void applyFilter(FITSScale type, uint8_t *image = nullptr, QVector *targetMin = nullptr, QVector *targetMax = nullptr); // Rotation counter. We keep count to rotate WCS keywords on save int getRotCounter() const; void setRotCounter(int value); // Filename const QString &filename() const { return m_Filename; } bool isTempFile() const {return m_isTemporary;} bool isCompressed() const {return m_isCompressed;} // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipHCounter() const; void setFlipHCounter(int value); // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipVCounter() const; void setFlipVCounter(int value); #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB void findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]); #endif #endif QList getSkyObjects(); QList objList; //Does this need to be public?? // Create autostretch image from FITS File static QImage FITSToImage(const QString &m_Filename); bool getAutoRemoveTemporaryFITS() const; void setAutoRemoveTemporaryFITS(bool value); QString getLastError() const; signals: void converted(QImage); private: bool privateLoad(bool silent); void rotWCSFITS(int angle, int mirror); bool checkCollision(Edge *s1, Edge *s2); int calculateMinMax(bool refresh = false); bool checkDebayer(); void readWCSKeys(); // FITS Record bool parseHeader(); //int getFITSRecord(QString &recordList, int &nkeys); // Templated functions template bool debayer(); template bool rotFITS(int rotate, int mirror); // Apply Filter template void applyFilter(FITSScale type, uint8_t *targetImage, QVector * min = nullptr, QVector * max = nullptr); // Star Detect - Centroid template int findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth); int findCentroid(const QRect &boundary = QRect(), int initStdDev = MINIMUM_STDVAR, int minEdgeWidth = MINIMUM_PIXEL_RANGE); // Star Detect - Threshold template int findOneStar(const QRect &boundary); template void calculateMinMax(); template QPair getParitionMinMax(uint32_t start, uint32_t stride); /* Calculate running average & standard deviation using Welford’s method for computing variance */ template void runningAverageStdDev(); template QPair getSquaredSumAndMean(uint32_t start, uint32_t stride); // Sobel detector by Gonzalo Exequiel Pedone template void sobel(QVector &gradient, QVector &direction); template void convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image); // Give unique IDs to each contiguous region int partition(int width, int height, QVector &gradient, QVector &ids); void trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y); #if 0 QVector thinning(int width, int height, const QVector &gradient, const QVector &direction); QVector threshold(int thLow, int thHi, const QVector &image); QVector hysteresis(int width, int height, const QVector &image); #endif #ifndef KSTARS_LITE FITSHistogram *histogram { nullptr }; // Pointer to the FITS data histogram #endif /// Pointer to CFITSIO FITS file struct fitsfile *fptr { nullptr }; /// FITS image data type (TBYTE, TUSHORT, TINT, TFLOAT, TLONG, TDOUBLE) uint32_t m_DataType { 0 }; /// Number of channels uint8_t m_Channels { 1 }; /// Generic data image buffer uint8_t *m_ImageBuffer { nullptr }; /// Is this a temporary file or one loaded from disk? bool m_isTemporary { false }; /// is this file compress (.fits.fz)? bool m_isCompressed { false }; /// Did we search for stars yet? bool starsSearched { false }; ///Star Selection Algorithm StarAlgorithm starAlgorithm { ALGORITHM_GRADIENT }; /// Do we have WCS keywords in this FITS data? bool HasWCS { false }; /// Is the image debayarable? bool HasDebayer { false }; /// Is WCS data loaded? bool WCSLoaded { false }; /// Do we need to mark stars for the user? bool markStars { false }; /// Our very own file name QString m_Filename; /// FITS Mode (Normal, WCS, Guide, Focus..etc) FITSMode m_Mode; /// How many times the image was rotated? Useful for WCS keywords rotation on save. int rotCounter { 0 }; /// How many times the image was flipped horizontally? int flipHCounter { 0 }; /// How many times the image was flipped vertically? int flipVCounter { 0 }; /// Pointer to WCS coordinate data, if any. wcs_point *wcs_coord { nullptr }; /// WCS Struct struct wcsprm *wcs { nullptr }; /// All the stars we detected, if any. QList starCenters; QList localStarCenters; /// The biggest fattest star in the image. Edge *maxHFRStar { nullptr }; uint8_t *bayerBuffer { nullptr }; /// Bayer parameters BayerParams debayerParams; Statistic stats; // A list of header records QList records; /// Remove temporary files after closing bool autoRemoveTemporaryFITS { true }; QString lastError; static const QString m_TemporaryPath; }; diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index 2274cd3c8..f5507eccd 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1867 +1,1894 @@ /* 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" #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_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { 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(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); //if (fitsMode == FITS_GUIDE) //connect(image_frame.get(), SIGNAL(pointSelected(int,int)), this, SLOT(processPointSelection(int,int))); // Default size //resize(INITIAL_W, INITIAL_H); } 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); } #if 0 bool FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } QProgressDialog fitsProg(this); bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // 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); if (mode == FITS_NORMAL) { fitsProg.setWindowModality(Qt::WindowModal); fitsProg.setLabelText(i18n("Please hold while loading FITS file...")); fitsProg.setWindowTitle(i18n("Loading FITS")); fitsProg.setValue(10); qApp->processEvents(); } if (!imageData->loadFITS(inFilename, silent)) return false; if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(65); qApp->processEvents(); } } emit debayerToggled(imageData->hasDebayer()); currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); initDisplayImage(); // Rescale to fits window if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) != 0) return false; firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) != 0) return false; } if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(100); qApp->processEvents(); } } 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); QTimer::singleShot(100, this, SLOT(viewStarProfile())); //Need to wait till the Focus module finds stars, if its the Focus module. } updateFrame(); emit imageLoaded(); return true; } #endif 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 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)); } 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()); // Set current width and height currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image initDisplayImage(); uint8_t * ASImageBuffer = nullptr; -// if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) -// { -// // If we perform autostretch, we need to create a buffer to save the raw image data before -// // autostretch filter operation changes the data. -// // After rescaling is done, we -// uint32_t totalBytes = image_width * image_height *imageData->channels() * imageData->getBytesPerPixel(); -// ASImageBuffer = new uint8_t[totalBytes]; -// memcpy(ASImageBuffer, imageData->getImageBuffer(), totalBytes); -// imageData->applyFilter(FITS_AUTO_STRETCH); -// } -// else -// imageData->applyFilter(filter); + // if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) + // { + // // If we perform autostretch, we need to create a buffer to save the raw image data before + // // autostretch filter operation changes the data. + // // After rescaling is done, we + // uint32_t totalBytes = image_width * image_height *imageData->channels() * imageData->getBytesPerPixel(); + // ASImageBuffer = new uint8_t[totalBytes]; + // memcpy(ASImageBuffer, imageData->getImageBuffer(), totalBytes); + // imageData->applyFilter(FITS_AUTO_STRETCH); + // } + // else + // imageData->applyFilter(filter); imageData->applyFilter(filter); if (Options::autoStretch()) imageData->applyFilter(FITS_AUTO_STRETCH); // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); delete [] ASImageBuffer; emit failed(); return; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); delete [] ASImageBuffer; emit failed(); return; } } // Restore original raw buffer after Autostretch if applicable if (ASImageBuffer) { imageData->setImageBuffer(ASImageBuffer); } 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(); emit loaded(); } 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; uint8_t * image_buffer = imageData->getImageBuffer(); uint32_t size = imageData->width() * imageData->height(); #if 0 - int BBP= imageData->getBytesPerPixel(); + int BBP = imageData->getBytesPerPixel(); filter = filterStack.last(); if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) { image_buffer = new uint8_t[size * imageData->channels() * BBP]; memcpy(image_buffer, imageData->getImageBuffer(), size * imageData->channels() * BBP); displayBuffer = true; double data_min = -1; double data_max = -1; imageData->applyFilter(FITS_AUTO_STRETCH, image_buffer, &data_min, &data_max); min = data_min; max = data_max; } else { imageData->applyFilter(filter); imageData->getMinMax(&min, &max); } #endif scaledImage = QImage(); auto * buffer = reinterpret_cast(image_buffer); if (imageData->getMin(0) == imageData->getMax(0)) { rawImage.fill(Qt::white); emit newStatus(i18n("Image is saturated."), FITS_MESSAGE); } else { if (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->channels() == 1) { double range = imageData->getMax(0) - imageData->getMin(0); double bscale = 255. / range; double bzero = (-imageData->getMin(0)) * (255. / range); QVector> futures; /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) { - futures.append(QtConcurrent::run([=]() + futures.append(QtConcurrent::run([ = ]() { - T * runningBuffer = buffer +j*image_width; + T * runningBuffer = buffer + j * image_width; uint8_t * scanLine = rawImage.scanLine(j); for (uint32_t i = 0; i < image_width; i++) { //scanLine[i] = qBound(0, static_cast(runningBuffer[i] * bscale + bzero), 255); scanLine[i] = qBound(0.0, runningBuffer[i] * bscale + bzero, 255.0); } })); } for(QFuture future : futures) future.waitForFinished(); } else { QVector> futures; double bscaleR = 255. / (imageData->getMax(0) - imageData->getMin(0)); double bzeroR = (-imageData->getMin(0)) * (255. / (imageData->getMax(0) - imageData->getMin(0))); double bscaleG = 255. / (imageData->getMax(1) - imageData->getMin(1)); double bzeroG = (-imageData->getMin(1)) * (255. / (imageData->getMax(1) - imageData->getMin(1))); double bscaleB = 255. / (imageData->getMax(2) - imageData->getMin(2)); double bzeroB = (-imageData->getMin(2)) * (255. / (imageData->getMax(2) - imageData->getMin(2))); /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) { - futures.append(QtConcurrent::run([=]() + futures.append(QtConcurrent::run([ = ]() { auto * scanLine = reinterpret_cast((rawImage.scanLine(j))); - T * runningBufferR = buffer + j*image_width; - T * runningBufferG = buffer + j*image_width + size; - T * runningBufferB = buffer + j*image_width + size*2; + T * runningBufferR = buffer + j * image_width; + T * runningBufferG = buffer + j * image_width + size; + T * runningBufferB = buffer + j * image_width + size * 2; for (uint32_t i = 0; i < image_width; i++) { scanLine[i] = qRgb(runningBufferR[i] * bscaleR + bzeroR, runningBufferG[i] * bscaleG + bzeroG, runningBufferB[i] * bscaleB + bzeroB); } })); } for(QFuture future : futures) future.waitForFinished(); } #if 0 if (imageData->getNumOfChannels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { unsigned char * scanLine = display_image->scanLine(j); for (int i = 0; i < image_width; i++) { val = buffer[j * image_width + i] * bscale + bzero; scanLine[i] = qBound(0.0, val, 255.0); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { QRgb * scanLine = reinterpret_cast((display_image->scanLine(j))); for (int i = 0; i < image_width; i++) { rval = buffer[j * image_width + i]; gval = buffer[j * image_width + i + size]; bval = buffer[j * image_width + i + size * 2]; value = qRgb(rval * bscale + bzero, gval * bscale + bzero, bval * bscale + bzero); scanLine[i] = value; } } } #endif } switch (type) { case ZOOM_FIT_WINDOW: if ((rawImage.width() > width() || rawImage.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; } setWidget(image_frame.get()); if (type != ZOOM_KEEP_LEVEL) 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); 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); 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 (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; updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter * painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (markStars) drawStarCentroid(painter); 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); } 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) { painter->setPen(QPen(Qt::red, 2)); // image_data->getStarCenter(); QList starCenters = imageData->getStarCenters(); for (int i = 0; i < starCenters.count(); i++) { int x1 = (starCenters[i]->x - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int y1 = (starCenters[i]->y - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int w = (starCenters[i]->width) * (currentZoom / ZOOM_DEFAULT); painter->drawEllipse(x1, y1, w, w); } } 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) { 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; //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); 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) { 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() { 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); + 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 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::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::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(showStarProfile) { setCursorMode(selectCursor); - connect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); + connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); if(starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); trackingBox = QRect(0, 0, 128, 128); setTrackingBoxEnabled(true); } else { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); - disconnect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); + disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); setTrackingBoxEnabled(false); if(starProfileWidget) 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(); + 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) { //if (mode != FITS_GUIDE) //return; //image_data->getCenterSelection(&x, &y); //setGuideSquare(x,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() { if (imageData->channels() == 1) { rawImage = QImage(image_width, image_height, 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); } } /** 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())); 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; } } } } diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index ee98b5ace..9eeec3720 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,339 +1,348 @@ /* 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 #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); - ~FITSView(); + virtual ~FITSView() override; typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and display it in 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); // 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 isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); 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 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); 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 processPointSelection(int x, int y); 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(); - private: 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(); - public: - CursorMode lastMouseMode { selectCursor }; - bool isStarProfileShown() - { - return showStarProfile; - } - - protected: /// 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: 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 }; + 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 }; //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION QPointer starProfileWidget; #endif 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/kstars.kcfg b/kstars/kstars.kcfg index b2504bf12..13f2fb646 100644 --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -1,2270 +1,2280 @@ ksutils.h The screen coordinates of the Time InfoBox. QPoint(0,0) The screen coordinates of the Focus InfoBox. QPoint(600,0) The screen coordinates of the Geographic Location InfoBox. QPoint(0,600) If true, the Time InfoBox will show only its top line of data. true If true, the Focus InfoBox will show only its top line of data. true If true, the Geographic Location InfoBox will show only its top line of data. true Toggles display of all three InfoBoxes. true Toggles display of the Time InfoBox. true Toggles display of the Focus InfoBox. true Toggles display of the Geographic Location InfoBox. true Is the Time InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 0 0 3 Is the Focus InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 1 0 3 Is the Geographic Location InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 2 0 3 Toggle display of the status bar. true Toggle display of the Horizontal coordinates of the mouse cursor in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the current epoch in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the standard epoch in the status bar. false true 1024 768 true Black Body List of the filenames of custom object catalogs. List of integers toggling display of each custom object catalog (any nonzero value indicates the objects in that catalog will be displayed). List of names for which custom catalogs are to be displayed. Names of objects entered into the find dialog are resolved using online services and stored in the database. This option also toggles the display of such resolved objects on the sky map. true 800 600 true true false Toggle display of crosshairs centered at telescope's pointed position in the KStars sky map. true Toggle display of INDI messages in the KStars statusbar. true Show INDI messages as desktop notifications instead of dialogs. false true false false The default location of saved FITS files KSUtils::getDefaultPath("fitsDir") INDI server will attempt to bind with ports starting from this port 7624 INDI server will attempt to bind with ports ending with this port 9000 List of the aliases for filter wheel slots. PATH to indiserver binary KSUtils::getDefaultPath("indiServer") false PATH to indi drivers directory KSUtils::getDefaultPath("indiDriversDir") false 320 240 false false false false false false false false false false false The City name of the current geographic location. Greenwich The Province name of the current geographic location. This is the name of the state for locations in the U. S. The Country name of the current geographic location. United Kingdom The longitude of the current geographic location, in decimal degrees. 0.0 The latitude of the current geographic location, in decimal degrees. 51.468 -10.0 0.0 Two-letter code that determines the dates on which daylight savings time begins and ends (you can view the rules by pressing the "Explain DST Rules" button in the Geographic Location window). -- If true, focus changes will cause the sky to visibly spin to the new position. Otherwise, the display will "snap" instantly to the new position. true The names of the currently selected field-of-view indicators. The list of defined FOV indicator names is listed in the "Settings|FOV Symbols" menu. Telrad If true, trails attached to solar system bodies will fade into the background sky color. true The right ascension of the initial focus position of the sky map, in decimal hours. This value is volatile; it is reset whenever the program shuts down. 180.0 The declination of the initial focus position of the sky map, in decimal degrees. This value is volatile; it is reset whenever the program shuts down. 45.0 The name of the object that should be centered and tracked on startup. If no object should be centered, set to "nothing". This value is volatile; it is reset whenever the program shuts down. nothing True if the skymap should track on its initial position on startup. This value is volatile; it is reset whenever the program shuts down. false Toggle whether KStars should hide some objects while the display is moving, for smoother motion. true Toggle whether constellation boundaries are hidden while the display is in motion. true Toggle whether constellation lines are hidden while the display is in motion. false Choose sky culture. 11 Toggle whether constellation names are hidden while the display is in motion. false Toggle whether the coordinate grids are hidden while the display is in motion. true Toggle whether the Milky Way contour is hidden while the display is in motion. true Toggle whether IC objects are hidden while the display is in motion. true Toggle whether Messier objects are hidden while the display is in motion. true Toggle whether NGC objects are hidden while the display is in motion. true Toggle whether extra objects are hidden while the display is in motion. true Toggle whether solar system objects are hidden while the display is in motion. false Toggle whether faint stars are hidden while the display is in motion. true Toggle whether name labels are hidden while the display is in motion. true Toggle whether asteroids are drawn in the sky map. true Toggle whether asteroid name labels are drawn in the sky map. false Toggle whether comets are drawn in the sky map. true Toggle whether comet comas are drawn in the sky map. true Toggle whether comet name labels are drawn in the sky map. false Toggle whether supernovae are drawn in the sky map. false Toggle whether supernova name labels are drawn in the sky map. false Set magnitude limit for supernovae to be shown on the skymap. 16 Toggle supernova alerts. true Set magnitude limit for supernovae to be alerted. 13 Toggle whether constellation boundaries are drawn in the sky map. false Toggle whether constellation boundary containing the central focus point is highlighted in the sky map. false Toggle whether constellation lines are drawn in the sky map. false Toggle whether constellation art drawn in the sky map. false Toggle whether constellation name labels are drawn in the sky map. false Toggle whether deep-sky objects are drawn in the sky map. true Toggle whether the ecliptic line is drawn in the sky map. false Toggle whether the equator line is drawn in the sky map. false Coordinate grids will automatically change according to active coordinate system. true Toggle whether the equatorial coordinate grid is drawn in the sky map. false Toggle whether the horizontal coordinate grid is drawn in the sky map. false Toggle whether the local meridian line is drawn in the sky map. false Toggle whether the region below the horizon is opaque. true Toggle whether the horizon line is drawn in the sky map. true Toggle whether flags are drawn in the sky map. true Toggle whether IC objects are drawn in the sky map. false Toggle whether NGC objects are drawn in the sky map. true Toggle whether Messier objects are drawn in the sky map. true Toggle whether Messier objects are rendered as images in the sky map. true Toggle whether extra objects are drawn in the sky map. true Toggle whether the Milky Way contour is drawn in the sky map. true Toggle whether the Milky Way contour is filled. When this option is false, the Milky Way is shown as an outline. true Meta-option to control whether all major planets (and the Sun and Moon) are drawn in the sky map. true Toggle whether major planets (and the Sun and Moon) are rendered as images in the sky map. true Toggle whether major planets (and the Sun and Moon) are labeled in the sky map. true Toggle whether the Sun is drawn in the sky map. true Toggle whether the Moon is drawn in the sky map. true Toggle whether Mercury is drawn in the sky map. true Toggle whether Venus is drawn in the sky map. true Toggle whether Mars is drawn in the sky map. true Toggle whether Jupiter is drawn in the sky map. true Toggle whether Saturn is drawn in the sky map. true Toggle whether Uranus is drawn in the sky map. true Toggle whether Neptune is drawn in the sky map. true Toggle whether Pluto is drawn in the sky map. true Toggle whether stars are drawn in the sky map. true Toggle whether star magnitude (brightness) labels are shown in the sky map. false Toggle whether star name labels are shown in the sky map. true Toggle whether deep-sky object magnitude (brightness) labels are shown in the sky map. false Toggle whether deep-sky object name labels are shown in the sky map. false The timescale above which slewing mode is forced on at all times. 60 The background fill mode for the on-screen information boxes: 0="no BG"; 1="semi-transparent BG"; 2="opaque BG" 1 Algorithm for the mapping projection. 0 Use official IAU abbreviations for constellation names. false Use Latin constellation names. false Use localized constellation names (if localized names are not available, default to Latin names). true Display the sky with horizontal coordinates (when false, equatorial coordinates will be used). true Toggle whether a centered object automatically gets a name label attached. true Toggle whether a centered solar system object automatically gets a trail attached, as long as it remains centered. true Toggle whether the object under the mouse cursor gets a transient name label. true Toggle whether object positions are corrected for the effects of atmospheric refraction (only applies when horizontal coordinates are used). true Toggle whether corrections due to bending of light around the sun are taken into account false Toggle whether the sky is rendered using antialiasing. Lines and shapes are smoother with antialiasing, but rendering the screen will take more time. true The zoom level, measured in pixels per radian. 250. 250. 5000000. When zooming in or out, change zoom speed factor by this multiplier. 0.2 0.01 1.0 The faint magnitude limit for drawing asteroids. 15.0 The maximum magnitude (visibility) to filter the asteroid data download from JPL. 12.000 Controls the relative number of asteroid name labels drawn in the map. 4.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed in. 16.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed out. 5.0 When enabled, objects whose magnitudes are unknown, or not available to KStars, are drawn irrespective of the faint limits set. true Sets the density of stars in the field of view 5 The faint magnitude limit for drawing stars, when the map is in motion (only applicable if faint stars are set to be hidden while the map is in motion). 5.0 The relative density for drawing star name and magnitude labels. 2.0 The relative density for drawing deep-sky object name and magnitude labels. 5.0 If true, long names (common names) for deep-sky objects are shown in the labels. false The maximum solar distance for drawing comets. 3.0 Use experimental OpenGL backend (deprecated). false The state of the clock (running or not) true Objects in the observing list will be highlighted with a symbol in the map. true Objects in the observing list will be highlighted with a colored name label in the map. false The observing list will prefer DSS imagery while downloading imagery. true The observing list will prefer SDSS imagery while downloading imagery. false Check this if you use a large Dobsonian telescope. Sorting by percentage current altitude is an easy way of determining what objects are well-placed for observation. However, when using a large Dobsonian telescope, objects close to the zenith are hard to observe. Since tracking there corresponds to a rotation in azimuth, it is both counterintuitive and requires the observer to frequently move the ladder. The region around the zenith where this is particularly frustrating is called the Dobsonian hole. This checkbox makes the observing list consider objects present in the hole as unfit for observation. false This specifies the angular radius of the Dobsonian hole, i.e. the region where a large Dobsonian telescope cannot be pointed easily. 15.00 The name of the color scheme moonless-night.colors The method for rendering stars: 0="realistic colors"; 1="solid red"; 2="solid black"; 3="solid white"; 4="solid real colors" 0 4 The color saturation level of stars (only applicable when using "realistic colors" mode). 6 10 The color for the angular-distance measurement ruler. #FFF The background color of the on-screen information boxes. #000 The text color for the on-screen information boxes, when activated by a mouse click. #F00 The normal text color of the on-screen information boxes. #FFF The color for the constellation boundary lines. #222 The color for the constellation boundary lines. #222 The color for the constellation figure lines. #555 The color for the constellation names. #AA7 The color for the cardinal compass point labels. #002 The color for the ecliptic line. #663 The color for the equator line. #FFF The color for the equatorial coordinate grid lines. #456 The color for the horizontal coordinate grid lines. #5A3 The color for objects which have extra URL links available. #A00 The color for the horizon line and opaque ground. #5A3 The color for the local meridian line. #0059b3 The color for Messier object symbols. #0F0 The color for NGC object symbols. #066 The color for IC object symbols. #439 The color for the Milky Way contour. #123 The color for star name labels. #7AA The color for deep-sky object name labels. #7AA The color for solar system object labels. #439 The color for solar system object trails. #963 The color for the sky background. #002 The color for the artificial horizon region. #C82828 The color for telescope target symbols. #8B8 Color of visible satellites. #00FF00 Color of invisible satellites. #FF0000 Color of satellites labels. #640000 Color of supernova #FFA500 The color for user-added object labels. #439 The color for RA Guide Error bar in Ekos guide module. #00FF00 The color for DEC Guide Error bar in Ekos guide module. #00A5FF The color for solver FOV box in Ekos alignment module. #FFFF00 false Xplanet binary path KSUtils::getDefaultPath("XplanetPath") Option to use a FIFO file instead of saving to the hard disk true How long to wait for XPlanet before giving up in milliseconds 1000 How long to pause between frames in the XPlanet Animation 100 Width of xplanet window 640 Height of xplanet window 480 If true, display a label in the upper right corner. false Show local time. true Show GMT instead of local time. false Specify the text of the first line of the label. By default, it says something like "Looking at Earth". Any instances of %t will be replaced by the target name, and any instances of %o will be replaced by the origin name. Specify the point size. 12 Set the color for the label. #F00 Specify the format for the date/time label. This format string is passed to strftime(3). The default is "%c %Z", which shows the date, time, and time zone in the locale’s appropriate date and time representation. %c %Z false true false false Draw a glare around the sun with a radius of the specified value larger than the Sun. The default value is 28. 28 Place the observer above a random latitude and longitude false Place the observer above the specified longitude and latitude true Render the target body as seen from above the specified latitude (in degrees). The default value is 0. 0 Place the observer above the specified longitude (in degrees). Longitude is positive going east, negative going west (for the earth and moon), so for example Los Angeles is at -118 or 242. The default value is 0. 0 The default is no projection. Multiple bodies will not be shown if this option is specified, although shadows will still be drawn. 0 Use a file as the background image, with the planet to be superimposed upon it. This option is only meaningful with the -projection option. A color may also be supplied. false Use a file as the background image. false The path of the background image. Use a color as the background. true The color of the background. #000 A star of the specified magnitude will have a pixel brightness of 1. The default value is 10. Stars will be drawn more brightly if this number is larger. 10 If checked, use an arc file to be plotted against the background stars. false Specify an arc file to be plotted against the background stars. If checked, use a config file. false Use the specified configuration file. If checked, use kstars's FOV. false If checked, use the specified marker file. false Specify a file containing user-defined marker data to display against the background stars. If checked, write coordinates of the bounding box for each marker in a file. false Write coordinates of the bounding box for each marker to this file. If checked, use star map file to draw the background stars. false Star map file path This option is only used when creating JPEG images. The quality can range from 0 to 100. The default value is 80. 80 Toggle whether satellite tracks are drawn in the sky map. false Toggle whether satellite tracks are drawn in the sky map. false If selected, satellites will be draw like stars, otherwise, draw satellites as small colored square. false Toggle whether satellite labels are drawn in the sky map. false List of selected satellites. Checking this option causes recomputation of current equatorial coordinates from catalog coordinates (i.e. application of precession, nutation and aberration corrections) for every redraw of the map. This makes processing slower when there are many stars to handle, but is more likely to be bug free. There are known bugs in the rendering of stars when this recomputation is avoided. false The default size for DSS images downloaded from the Internet. 15.0 To include parts of the star field, we add some extra padding around DSS images of deep-sky objects. This option configures the total (both sides) padding added to either dimension of the field. 10.0 Checking this option causes KStars to generate verbose debug information for diagnostic purposes. This may cause slowdown of KStars. false Checking this option causes KStars to generate regular debug information. true Checking this option causes KStars to stop generating ANY debug information. false Checking this option causes KStars log debug messages to the default output used by the platform (e.g. Standard Error). true Checking this option causes KStars log debug messages to a log file as specified. false Log FITS Data activity. false Log INDI devices activity. false Log Ekos Capture Module activity. false Log Ekos Focus Module activity. false Log Ekos Guide Module activity. false Log Ekos Alignment Module activity. false Log Ekos Mount Module activity. false true Display all captured FITS images in a single tab instead of multiple tabs per image. true Display all captured FITS images in a single FITS Viewer window. By default each camera create its own FITS Viewer instance false Display all opened FITS images in a single FITS Viewer window. true Bring the FITSViewer window to the foreground when receiving a new image. true false true true true false 4 false false 40.0 0 600 600 true false true true true false Simulators false true false true 1 Minimum telescope altitude limit. If the telescope is below this limit, it will be commanded to stop. 0 Maximum telescope altitude limit. If the telescope is above this limit, it will be commanded to stop. 90.0 false false 1 0 If guide deviation exceeds this limit, the exposure will be automatically aborted and only resumed when the deviation is within this limit. 2 If HFR deviation exceeds this limit, the autofocus routine will be automatically started. 0.5 false false false Sets the time interval before forced autofocus attempts during a capture sequence. 60 If the target hour angle exceeds this value, Ekos will command a meridian flip and if successful it will resume guiding and capture operations. false false If set, Ekos will capture a few flat images to determine the optimal exposure time to achieve the desired ADU value. 0 Maximum difference between measured and target ADU values to deem the value as acceptable. 1000 0 0 0 0 0.1 0 false false 2.5 true false 1 30 true true true 0 KSUtils::getDefaultPath("fitsDir") Step size of the absolute focuser. The step size TICKS should be adjusted so that when the focuser moves TICKS steps, the difference in HFR is more than 0.1 pixels. Lower the value when you are close to optimal focus. 100 Wait for this many seconds after moving the focuser before capturing the next image during AutoFocus. 0 Wait for this many seconds after resuming guide. 0 The tolerance specifies the percentage difference between the current focusing position and the minimum obtained during the focusing run. Adjustment of this value is necessary to prevent the focusing algorithm from oscillating back and forth. 1 Set the maximum travel distance of an absolute focuser. 10000 Specifies exposure value of CCD when performing focusing. Lower this value to avoid saturation of bright stars which adversely affects HFR measurement. Increase the value if no stars are detected. 0.5 Set box size to select a focus star. 64 Set horizontal binning of CCD camera while in focus mode. 1 Set vertical binning of CCD camera while in focus mode. 1 true - + false + + + During full field focusing, stars which are inside this percentage of the frame are filtered out of HFR calculation (default 0%). Detection algorithms may also have an inherent filter. + 0.0 + + + + During full field focusing, stars which are outside this percentage of the frame are filtered out of HFR calculation (default 100%). Detection algorithms may also have an inherent filter. + 100.0 + false true false 0 150 0 0 1 Specifies exposure value of CCD in seconds when performing plate solving. 1 Set binning index of CCD camera while in alignment mode. Default values 0-3 corresponding to 1x1 to 4x4 binning. 4 is max binning. 4 Use rotator when performing load and slew. false Threshold between measured and FITS position angles in arcminutes to consider the load and slew operation successful. 30 0 0 1 true false false 30 0 false 1500 true true true true true 1 true 2 true true 30 false Path to astrometry.net solver location. KSUtils::getDefaultPath("AstrometrySolverBinary") false Path to astrometry.net wcsinfo location. KSUtils::getDefaultPath("AstrometryWCSInfo") false Path to astrometry.net file location. KSUtils::getDefaultPath("AstrometryConfFile") Astrometry.net Index files Location. KSUtils::getDefaultPath("AstrometryIndexFileLocation") false Key to access astrometry.net online web services. You must register with astrometry.net to obtain a key. iczikaqstszeptgs http://nova.astrometry.net true 180 -1 1.0 0 0 localhost 4400 localhost 5656 0 1000 2 false 1 false false false 5 60 10 true false false false 2 1 0 1 45 10 500 false false false 2 true true true true true true 133.33 133.33 0 0 0 0 5000 5000 100 100 0.5 2 true true false false Log Ekos Scheduler Module activity. false Sort scheduler jobs by priority and altitude. true true false false false false true false 2 true 5 30 0 0 0 0 0 0 7624 8624 300 1000 None false false Toggle whether the HIPS sources are drawn in the sky map. false