diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index d6fc42e27..91e592690 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3595 +1,3650 @@ /* 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 "focusalgorithms.h" #include "polynomialfit.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "auxiliary/ksmessagebox.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() { // #1 Set the UI setupUi(this); // #2 Register DBus qRegisterMetaType("Ekos::FocusState"); qDBusRegisterMetaType(); new FocusAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this); // #3 Init connections initConnections(); // #4 Init Plots initPlots(); // #5 Init View initView(); // #6 Reset all buttons to default states resetButtons(); // #7 Image Effects 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); // #8 Load All settings loadSettings(); // #9 Init Setting Connection now initSettingsConnections(); //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); appendLogText(i18n("Idle.")); } 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; } QString Focus::camera() { if (currentCCD) return currentCCD->getDeviceName(); 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)); activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); } else 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); double defaultGain = Options::focusGain(); if (defaultGain > 0) gainIN->setValue(defaultGain); else 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)); int filterWheelIndex = 1; if (Options::defaultFocusFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultFocusFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); } 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; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); //Options::setDefaultFocusFilterWheel(currentFilter->getDeviceName()); filterManager->setCurrentFilterWheel(currentFilter); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); 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) { 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 for (auto &oneFocuser : Focusers) { disconnect(oneFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber); } hasDeviation = currentFocuser->hasDeviation(); canAbsMove = currentFocuser->canAbsMove(); if (canAbsMove) { getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); startGotoB->setEnabled(true); absTicksSpin->setValue(currentPosition); } else { absTicksSpin->setEnabled(false); absTicksLabel->setEnabled(false); startGotoB->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 + // 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(); + // In case we have a timer-based focuser and using the linear focus algorithm, + // we pretend it is an absolute focuser with initial point set at 50,000. + // These variables don't have in impact on timer-based focusers if the algorithm + // is not the linear focus algorithm. + if (!canAbsMove && !canRelMove && canTimerMove) + { + currentPosition = 50000; + absMotionMax = 100000; + absMotionMin = 0; + } + focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL; bool hasBacklash = currentFocuser->hasBacklash(); focusBacklashSpin->setEnabled(hasBacklash); focusBacklashSpin->disconnect(this); if (hasBacklash) { double min = 0, max = 0, step = 0; currentFocuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step); focusBacklashSpin->setMinimum(min); focusBacklashSpin->setMaximum(max); focusBacklashSpin->setSingleStep(step); focusBacklashSpin->setValue(currentFocuser->getBacklash()); connect(focusBacklashSpin, static_cast(&QSpinBox::valueChanged), this, [this](int value) { if (currentFocuser) currentFocuser->setBacklash(value); }); } else { focusBacklashSpin->setValue(0); } 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))); stepIN->setMaximum(absMove->np[0].max / 2); //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")); absIterations = 0; pulseDuration = stepIN->value(); //currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } else { pulseDuration = stepIN->value(); + absIterations = 0; + absMotionMax = 100000; + absMotionMin = 0; + 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; focuserAdditionalMovement = 0; 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() << " 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")); - if (focusAlgorithm == FOCUS_LINEAR && (canAbsMove || canRelMove)) + // Used for all the focuser types. + if (focusAlgorithm == FOCUS_LINEAR) { const int position = static_cast(currentPosition); FocusAlgorithmInterface::FocusParams params( maxTravelIN->value(), stepIN->value(), position, absMotionMin, absMotionMax, - MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0); + MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0, filter()); linearFocuser.reset(MakeLinearFocuser(params)); const int newPosition = adjustLinearPosition(position, linearFocuser->initialPosition()); if (newPosition != position) { if (!changeFocus(newPosition - position)) { abort(); setAutoFocusResult(false); } // Avoid the capture below. return; } } capture(); } int Focus::adjustLinearPosition(int position, int newPosition) { if (newPosition > position) { constexpr int extraMotionSteps = 5; int adjustment = extraMotionSteps * stepIN->value(); if (newPosition + adjustment > absMotionMax) adjustment = static_cast(absMotionMax) - newPosition; focuserAdditionalMovement = adjustment; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: extending outward movement by %1").arg(adjustment); return newPosition + adjustment; } return newPosition; } 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"; + qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus"; captureTimeout.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); inAutoFocus = false; focuserAdditionalMovement = 0; 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; captureFailureCounter = 0; minimumRequiredHFR = -1; noStarCount = 0; HFRFrames.clear(); //maxHFR=1; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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)); 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->setISOIndex(ISOCombo->currentIndex()); if (gainIN->isEnabled()) currentCCD->setGain(gainIN->value()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); connect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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 (ms == -1) ms = stepIN->value(); return changeFocus(-ms); } bool Focus::focusOut(int ms) { if (ms == -1) ms = stepIN->value(); return changeFocus(ms); } // If amount > 0 we focus out, otherwise in. bool Focus::changeFocus(int amount) { if (currentFocuser == nullptr) return false; + // This needs to be re-thought. Just returning does not set the timer + // and the algorithm ends in limbo. // Ignore zero - if (amount == 0) - return true; + // if (amount == 0) + // return true; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } const int absAmount = abs(amount); const bool focusingOut = amount > 0; const QString dirStr = focusingOut ? i18n("outward") : i18n("inward"); lastFocusDirection = focusingOut ? FOCUS_OUT : FOCUS_IN; qCDebug(KSTARS_EKOS_FOCUS) << "Focus " << dirStr << " (" << absAmount << ")"; if (focusingOut) currentFocuser->focusOut(); else currentFocuser->focusIn(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition + amount); appendLogText(i18n("Focusing %2 by %1 steps...", absAmount, dirStr)); } else if (canRelMove) { currentFocuser->moveRel(absAmount); appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absAmount, dirStr)); } else { currentFocuser->moveByTimer(absAmount); appendLogText(i18n("Focusing %2 by %1 ms...", absAmount, dirStr)); } 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); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); 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, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); darkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Focus::appendLogText); targetChip->setCaptureFilter(defaultScale); if (darkData) DarkLibrary::Instance()->subtract(darkData, focusView, defaultScale, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, focusView, exposureIN->value(), offsetX, offsetY); } 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); // 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); focusView->updateFrame(); // 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 << " Num stars " << (starSelected ? 1 : image_data->getDetectedStars()); // 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] : (static_cast(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); starsOut->setText(QString("%1").arg(image_data->getDetectedStars())); // 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 polynomial 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); graphPolynomialFunction(); 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) { // 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 inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true. + // We'd only want to execute this if the focus linear algorithm is not being used, as that + // algorithm simulates a position-based system even for timer-based focusers. + if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false && + focusAlgorithm != FOCUS_LINEAR)) { 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; } // set the tracking box on maxStar starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); syncTrackingBoxPosition(); // 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(); } 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() && Options::saveFocusImages()) { QDir dir; QDateTime now = KStarsData::Instance()->lt(); QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "autofocus/" + now.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_" + now.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) + if (focusAlgorithm == FOCUS_LINEAR) + autoFocusLinear(); + else if (canAbsMove || canRelMove) + // Position-based algorithms autoFocusAbs(); else // Time open-looped algorithms autoFocusRel(); } void Focus::clearDataPoints() { maxHFR = 1; hfr_position.clear(); hfr_value.clear(); polynomialGraph->data()->clear(); focusPoint->data()->clear(); polynomialGraphIsShown = false; HFRPlot->clearItems(); polynomialFit.reset(); drawHFRPlot(); } void Focus::drawHFRIndeces() { // Put the sample number inside the plot point's circle. for (int i = 0; i < hfr_position.size(); ++i) { QCPItemText *textLabel = new QCPItemText(HFRPlot); textLabel->setPositionAlignment(Qt::AlignCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(hfr_position[i], hfr_value[i]); textLabel->setText(QString::number(i + 1)); textLabel->setFont(QFont(font().family(), 12)); textLabel->setPen(Qt::NoPen); textLabel->setColor(Qt::red); } } void Focus::drawHFRPlot() { // DrawHFRPlot is the base on which other things are built upon. // Clear any previous annotations. HFRPlot->clearItems(); v_graph->setData(hfr_position, hfr_value); drawHFRIndeces(); double minHFRVal = currentHFR / 2.5; if (hfr_value.size() > 0) minHFRVal = std::max(0, static_cast(0.9 * *std::min_element(hfr_value.begin(), hfr_value.end()))); - if (inFocusLoop == false && (canAbsMove || canRelMove)) + // True for the position-based algorithms and those that simulate position. + if (inFocusLoop == false && (canAbsMove || canRelMove || (focusAlgorithm == FOCUS_LINEAR))) { - //HFRPlot->xAxis->setLabel(i18n("Position")); - HFRPlot->xAxis->setRange(minPos - pulseDuration, maxPos + pulseDuration); + const double minPosition = hfr_position.empty() ? + 0 : *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); + const double maxPosition = hfr_position.empty() ? + 1e6 : *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); + HFRPlot->xAxis->setRange(minPosition - pulseDuration, maxPosition + pulseDuration); HFRPlot->yAxis->setRange(minHFRVal, maxHFR); } else { //HFRPlot->xAxis->setLabel(i18n("Iteration")); HFRPlot->xAxis->setRange(1, hfr_value.count() + 1); HFRPlot->yAxis->setRange(currentHFR / 2.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() +bool Focus::autoFocusChecks() { - 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; + return false; } // 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; + return false; } 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; + return false; } } else noStarCount = 0; + return true; +} - if (hfr_position.empty()) - { - maxPos = 1; - minPos = 1e6; - } - - if (currentPosition > maxPos) - maxPos = currentPosition; - if (currentPosition < minPos) - minPos = currentPosition; +void Focus::autoFocusLinear() +{ + if (!autoFocusChecks()) + return; hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); - if (focusAlgorithm == FOCUS_LINEAR) + if (hfr_position.size() > 3) { - if (hfr_position.size() > 3) + polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); + double min_position, min_value; + const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); + double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); + double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); + if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, + searchMin, searchMax, &min_position, &min_value)) { - // For now, just plots, doesn't use the polynomial algorithmically. - polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); - double min_position, min_value; - const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); - double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); - double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); - if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, - searchMin, searchMax, &min_position, &min_value)) - { - polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); - polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); - } + QPen pen; + pen.setWidth(1); + pen.setColor(QColor(180,180,180)); + polynomialGraph->setPen(pen); + + polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); + polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); } + else + { + // During development of this algorithm, we show the polynomial graph in red if + // no minimum was found. That happens when the order-2 polynomial is an inverted U + // instead of a U shape (i.e. it has a maximum, but no minimum). + QPen pen; + pen.setWidth(1); + pen.setColor(QColor(254,0,0)); + polynomialGraph->setPen(pen); + polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); + + polynomialGraph->data()->clear(); + focusPoint->data()->clear(); + } + } - const int nextPosition = adjustLinearPosition( - static_cast(currentPosition), - linearFocuser->newMeasurement(currentPosition, currentHFR)); - if (nextPosition == -1) + const int nextPosition = adjustLinearPosition( + static_cast(currentPosition), + linearFocuser->newMeasurement(currentPosition, currentHFR)); + if (nextPosition == -1) + { + if (linearFocuser->isDone() && linearFocuser->solution() != -1) { - if (linearFocuser->isDone() && linearFocuser->solution() != -1) - { - appendLogText(i18np("Autofocus complete after %1 iteration.", - "Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - } - else - { - qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); - appendLogText("Linear autofocus algorithm aborted."); - abort(); - setAutoFocusResult(false); - } - return; + appendLogText(i18np("Autofocus complete after %1 iteration.", + "Autofocus complete after %1 iterations.", hfr_position.count())); + stop(); + setAutoFocusResult(true); } else { - delta = nextPosition - currentPosition; - if (!changeFocus(static_cast(delta))) - { - abort(); - setAutoFocusResult(false); - } - return; + qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); + appendLogText("Linear autofocus algorithm aborted."); + abort(); + setAutoFocusResult(false); } + return; } + else + { + const int delta = nextPosition - currentPosition; + if (!changeFocus(delta)) + { + abort(); + setAutoFocusResult(false); + } + return; + } +} + +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 (!autoFocusChecks()) + return; + + 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 (!changeFocus(pulseDuration)) { 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) { 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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } 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; lastHFR = currentHFR; // Let's keep track of the minimum HFR if (lastHFR < minHFR) { minHFR = lastHFR; minHFRPos = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << 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) { polynomialFit.reset(new PolynomialFit(3, hfr_position, hfr_value)); double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); double min_position = 0, min_hfr = 0; polyMinimumFound = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr); 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))); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_hfr, font()); } } 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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); 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()) { int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - maxTravelIN->value()); int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + maxTravelIN->value()); // In case we are asked to go below travel limit, but we are not there yet // let us go there and see the result before aborting if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit) { targetPosition = minTravelLimit; } // Same for max travel else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit) { targetPosition = maxTravelLimit; } else { qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos (" << 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; // Limit to Maximum permitted delta (Max Single Step Size) double limitedDelta = qMax(-1.0 * maxSingleStepIN->value(), qMin(1.0 * maxSingleStepIN->value(), delta)); if (std::fabs(limitedDelta - delta) > 0) { qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " << maxSingleStepIN->value(); delta = limitedDelta; } // Now cross your fingers and wait if (!changeFocus(delta)) { abort(); setAutoFocusResult(false); } break; } } void Focus::graphPolynomialFunction() { if (polynomialGraph && polynomialFit) { polynomialGraphIsShown = true; polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); } } 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; changeFocus(-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); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); break; } else if (currentHFR < lastHFR) { if (currentHFR < minHFR) minHFR = currentHFR; lastHFR = currentHFR; changeFocus(lastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration); HFRInc = 0; } else { HFRInc++; lastHFR = currentHFR; HFRInc = 0; pulseDuration *= 0.75; if (!changeFocus(lastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration)) { abort(); setAutoFocusResult(false); } } 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); startGotoB->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::autoFocusProcessPositionChange(IPState state) { if (state == IPS_OK && captureInProgress == false) { // Normally, if we are auto-focusing, after we move the focuser we capture an image. // However, the Linear algorithm, at the start of its passes, requires two // consecutive focuser moves--the first out further than we want, and a second // move back in, so that we eliminate backlash and are always moving in before a capture. if (focuserAdditionalMovement > 0) { int temp = focuserAdditionalMovement; focuserAdditionalMovement = 0; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: un-doing extension. Moving back in by %1").arg(temp); if (!focusIn(temp)) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else { QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); } } else if (state == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } void Focus::processFocusNumber(INumberVectorProperty *nvp) { // Return if it is not our current focuser if (strcmp(nvp->device, currentFocuser->getDeviceName())) return; if (!strcmp(nvp->name, "FOCUS_BACKLASH_STEPS")) { focusBacklashSpin->setValue(nvp->np[0].value); 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) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canAbsMove) return; if (!strcmp(nvp->name, "manualfocusdrive")) { INumber *pos = IUFindNumber(nvp, "manualfocusdrive"); if (pos && nvp->s == IPS_OK) { 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 (canRelMove && inAutoFocus) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); 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) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); 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) { + // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser. + INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE"); + if (pos) + currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } } void Focus::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().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); startGotoB->setEnabled(false); stopGotoB->setEnabled(false); resetFrameB->setEnabled(false); return; } if (currentFocuser) { focusOutB->setEnabled(true); focusInB->setEnabled(true); startFocusB->setEnabled(focusType == FOCUS_AUTO); startGotoB->setEnabled(canAbsMove); stopGotoB->setEnabled(true); } else { focusOutB->setEnabled(false); focusInB->setEnabled(false); startFocusB->setEnabled(false); startGotoB->setEnabled(false); stopGotoB->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 > (static_cast(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 (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; if (status) { // CR add auto focus position, temperature and filter to log in CSV format // this will help with setting up focus offsets and temperature compensation INDI::Property * np = currentFocuser->getProperty("TemperatureNP"); double temperature = -274; // impossible temperature as a signal that it isn't available if (np != nullptr) { INumberVectorProperty * tnp = np->getNumber(); temperature = tnp->np[0].value; } qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position, " << currentPosition << ", temperature, " << temperature << ", filter, " << filter(); } // In case of failure, go back to last position if the focuser is absolute if (status == false && canAbsMove && currentFocuser && currentFocuser->isConnected() && 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]() { 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)); 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; } } 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); //} // 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; changeFocus(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); // If mount is moved while we have a star selected and subframed // let us reset the frame. if (subFramed) resetFrame(); break; default: resetButtons(); break; } } void Focus::removeDevice(ISD::GDInterface *deviceRemoved) { // Check in Focusers for (ISD::GDInterface *focuser : Focusers) { if (!strcmp(focuser->getDeviceName(), deviceRemoved->getDeviceName())) { Focusers.removeAll(dynamic_cast(focuser)); focuserCombo->removeItem(focuserCombo->findText(focuser->getDeviceName())); checkFocuser(); resetButtons(); } } // Check in CCDs for (ISD::GDInterface *ccd : CCDs) { if (!strcmp(ccd->getDeviceName(), deviceRemoved->getDeviceName())) { CCDs.removeAll(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider"))); checkCCD(); resetButtons(); } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (!strcmp(filter->getDeviceName(), deviceRemoved->getDeviceName())) { Filters.removeAll(filter); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName())); if (Filters.empty()) currentFilter = nullptr; checkFilter(); resetButtons(); } } } 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(); } ); 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()); }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::exposureChanged, this, [this]() { exposureIN->setValue(filterManager->getFilterExposure()); }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [ = ](const QString & text) { exposureIN->setValue(filterManager->getFilterExposure(text)); //Options::setDefaultFocusFilterWheelFilter(text); }); } void Focus::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) currentCCD->setBLOBEnabled(true); else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setVideoStreamEnabled(enabled); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?")); } } else 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); } void Focus::processCaptureFailure() { captureFailureCounter++; if (captureFailureCounter >= 3) { captureFailureCounter = 0; appendLogText(i18n("Exposure failure. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure failure. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); } void Focus::syncSettings() { QDoubleSpinBox *dsb = nullptr; QSpinBox *sb = nullptr; QCheckBox *cb = nullptr; QComboBox *cbox = nullptr; if ( (dsb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// if (dsb == FocusSettleTime) Options::setFocusSettleTime(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// else if (dsb == gainIN) Options::setFocusGain(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (dsb == fullFieldInnerRing) Options::setFocusFullFieldInnerRadius(dsb->value()); else if (dsb == fullFieldOuterRing) Options::setFocusFullFieldOuterRadius(dsb->value()); else if (dsb == GuideSettleTime) Options::setGuideSettleTime(dsb->value()); else if (dsb == maxTravelIN) Options::setFocusMaxTravel(dsb->value()); else if (dsb == toleranceIN) Options::setFocusTolerance(dsb->value()); else if (dsb == thresholdSpin) Options::setFocusThreshold(dsb->value()); } else if ( (sb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (sb == focusBoxSize) Options::setFocusBoxSize(sb->value()); else if (sb == stepIN) Options::setFocusTicks(sb->value()); else if (sb == maxSingleStepIN) Options::setFocusMaxSingleStep(sb->value()); else if (sb == focusFramesSpin) Options::setFocusFramesCount(sb->value()); } else if ( (cb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (cb == useAutoStar) Options::setFocusAutoStarEnabled(cb->isChecked()); else if (cb == useSubFrame) Options::setFocusSubFrame(cb->isChecked()); else if (cb == darkFrameCheck) Options::setUseFocusDarkFrame(cb->isChecked()); else if (cb == useFullField) Options::setFocusUseFullField(cb->isChecked()); else if (cb == suspendGuideCheck) Options::setSuspendGuiding(cb->isChecked()); } else if ( (cbox = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// if (cbox == focuserCombo) Options::setDefaultFocusFocuser(cbox->currentText()); else if (cbox == CCDCaptureCombo) Options::setDefaultFocusCCD(cbox->currentText()); else if (cbox == binningCombo) { activeBin = cbox->currentIndex() + 1; Options::setFocusXBin(activeBin); } else if (cbox == FilterDevicesCombo) Options::setDefaultFocusFilterWheel(cbox->currentText()); // Filter Effects already taken care of in filterChangeWarning /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (cbox == focusAlgorithmCombo) Options::setFocusAlgorithm(cbox->currentIndex()); else if (cbox == focusDetectionCombo) Options::setFocusDetection(cbox->currentIndex()); } } void Focus::loadSettings() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// // Focus settle time FocusSettleTime->setValue(Options::focusSettleTime()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// // Binning activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); // Gain gainIN->setValue(Options::focusGain()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// // Auto Star? useAutoStar->setChecked(Options::focusAutoStarEnabled()); // Subframe? useSubFrame->setChecked(Options::focusSubFrame()); // Dark frame? darkFrameCheck->setChecked(Options::useFocusDarkFrame()); // Use full field? useFullField->setChecked(Options::focusUseFullField()); // full field inner ring fullFieldInnerRing->setValue(Options::focusFullFieldInnerRadius()); // full field outer ring fullFieldOuterRing->setValue(Options::focusFullFieldOuterRadius()); // Suspend guiding? suspendGuideCheck->setChecked(Options::suspendGuiding()); // Guide Setting time GuideSettleTime->setValue(Options::guideSettleTime()); // Box Size focusBoxSize->setValue(Options::focusBoxSize()); // Max Travel if (Options::focusMaxTravel() > maxTravelIN->maximum()) maxTravelIN->setMaximum(Options::focusMaxTravel()); maxTravelIN->setValue(Options::focusMaxTravel()); // Step stepIN->setValue(Options::focusTicks()); // Single Max Step maxSingleStepIN->setValue(Options::focusMaxSingleStep()); // Tolerance toleranceIN->setValue(Options::focusTolerance()); // Threshold spin thresholdSpin->setValue(Options::focusThreshold()); // Focus Algorithm focusAlgorithm = static_cast(Options::focusAlgorithm()); focusAlgorithmCombo->setCurrentIndex(focusAlgorithm); // Frames Count focusFramesSpin->setValue(Options::focusFramesCount()); // Focus Detection focusDetection = static_cast(Options::focusDetection()); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); focusDetectionCombo->setCurrentIndex(focusDetection); } void Focus::initSettingsConnections() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FocusSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(gainIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FilterPosCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// connect(useAutoStar, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useFullField, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(fullFieldInnerRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(fullFieldOuterRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(suspendGuideCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(GuideSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(maxTravelIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(stepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(maxSingleStepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(toleranceIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(thresholdSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(focusFramesSpin, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); } void Focus::initPlots() { 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); polynomialGraph = HFRPlot->addGraph(); polynomialGraph->setLineStyle(QCPGraph::lsLine); polynomialGraph->setPen(QPen(QColor(140, 140, 140), 2, Qt::DotLine)); polynomialGraph->setScatterStyle(QCPScatterStyle::ssNone); connect(HFRPlot->xAxis, static_cast(&QCPAxis::rangeChanged), this, [this]() { drawHFRIndeces(); if (polynomialGraphIsShown) { if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } }); connect(HFRPlot, &QCustomPlot::mouseMove, this, [this](QMouseEvent * event) { double key = HFRPlot->xAxis->pixelToCoord(event->localPos().x()); if (HFRPlot->xAxis->range().contains(key)) { QCPGraph *graph = qobject_cast(HFRPlot->plottableAt(event->pos(), false)); if (graph) { if(graph == v_graph) { int positionKey = v_graph->findBegin(key); double focusPosition = v_graph->dataMainKey(positionKey); double halfFluxRadius = v_graph->dataMainValue(positionKey); QToolTip::showText( event->globalPos(), i18nc("HFR graphics tooltip; %1 is the Focus Position; %2 is the Half Flux Radius;", "" "" "" "
POS: %1
HFR: %2
", QString::number(focusPosition, 'f', 0), QString::number(halfFluxRadius, 'f', 2))); } } } }); focusPoint = HFRPlot->addGraph(); focusPoint->setLineStyle(QCPGraph::lsImpulse); focusPoint->setPen(QPen(QColor(140, 140, 140), 2, Qt::SolidLine)); focusPoint->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::yellow, 10)); v_graph = HFRPlot->addGraph(); v_graph->setLineStyle(QCPGraph::lsNone); v_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::white, 14)); } void Focus::initConnections() { // How long do we wait until the user select a star? waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT); connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo); // Show FITS Image in a new window showFITSViewerB->setIcon(QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer); // Toggle FITS View to full screen toggleFullScreenB->setIcon(QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen); // How long do we wait until an exposure times out and needs a retry? captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout); // Start/Stop focus connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start); connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::checkStopFocus); // Focus IN/OUT connect(focusOutB, &QPushButton::clicked, [&]() { focusOut(); }); connect(focusInB, &QPushButton::clicked, [&]() { focusIn(); }); // Capture a single frame connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture); // Start continuous capture connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming); // Use a subframe when capturing connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::toggleSubframe); // Reset frame dimensions to default connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame); // Sync setting if full field setting is toggled. connect(useFullField, &QCheckBox::toggled, [&](bool toggled) { fullFieldInnerRing->setEnabled(toggled); fullFieldOuterRing->setEnabled(toggled); if (toggled) { useSubFrame->setChecked(false); useAutoStar->setChecked(false); } else { // Disable the overlay focusView->setStarFilterRange(0, 1); } }); // Sync settings if the CCD selection is updated. connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkCCD); // Sync settings if the Focuser selection is updated. connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFocuser); // Sync settings if the filter selection is updated. connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFilter); // Set focuser absolute position connect(startGotoB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks); connect(stopGotoB, &QPushButton::clicked, [this]() { if (currentFocuser) currentFocuser->stop(); }); // Update the focuser box size used to enclose a star connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Ekos::Focus::updateBoxSize); // Update the focuser star detection if the detection algorithm selection changes. connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusDetection = static_cast(index); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); }); // Update the focuser solution algorithm if the selection changes. connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusAlgorithm = static_cast(index); }); // Reset star center on auto star check toggle connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled) { if (enabled) { starCenter = QVector3D(); starSelected = false; focusView->setTrackingBox(QRect()); } }); } void Focus::initView() { 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); focusView->setStarsHFREnabled(true); } } diff --git a/kstars/ekos/focus/focus.h b/kstars/ekos/focus/focus.h index c6dd32913..e68a93ed7 100644 --- a/kstars/ekos/focus/focus.h +++ b/kstars/ekos/focus/focus.h @@ -1,628 +1,630 @@ /* Ekos Focus tool 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. */ #pragma once #include "ui_focus.h" #include "ekos/ekos.h" #include "ekos/auxiliary/filtermanager.h" #include "fitsviewer/fitsviewer.h" #include "indi/indiccd.h" #include "indi/indifocuser.h" #include "indi/indistd.h" #include "indi/inditelescope.h" #include namespace Ekos { class FocusAlgorithmInterface; class PolynomialFit; /** * @class Focus * @short Supports manual focusing and auto focusing using relative and absolute INDI focusers. * * @author Jasem Mutlaq * @version 1.5 */ class Focus : public QWidget, public Ui::Focus { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Focus") Q_PROPERTY(Ekos::FocusState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString focuser READ focuser WRITE setFocuser) Q_PROPERTY(QString filterWheel READ filterWheel WRITE setFilterWheel) Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(double HFR READ getHFR NOTIFY newHFR) Q_PROPERTY(double exposure READ exposure WRITE setExposure) public: Focus(); ~Focus(); typedef enum { FOCUS_NONE, FOCUS_IN, FOCUS_OUT } FocusDirection; typedef enum { FOCUS_MANUAL, FOCUS_AUTO } FocusType; typedef enum { FOCUS_ITERATIVE, FOCUS_POLYNOMIAL, FOCUS_LINEAR } FocusAlgorithm; /** @defgroup FocusDBusInterface Ekos DBus Interface - Focus Module * Ekos::Focus interface provides advanced scripting capabilities to perform manual and automatic focusing operations. */ /*@{*/ /** DBUS interface function. * select the CCD device from the available CCD drivers. * @param device The CCD device name * @return Returns true if CCD device is found and set, false otherwise. */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the focuser device from the available focuser drivers. The focuser device can be the same as the CCD driver if the focuser functionality was embedded within the driver. * @param device The focuser device name * @return Returns true if focuser device is found and set, false otherwise. */ Q_SCRIPTABLE bool setFocuser(const QString &device); Q_SCRIPTABLE QString focuser(); /** DBUS interface function. * select the filter device from the available filter drivers. The filter device can be the same as the CCD driver if the filter functionality was embedded within the driver. * @param device The filter device name * @return Returns true if filter device is found and set, false otherwise. */ Q_SCRIPTABLE bool setFilterWheel(const QString &device); Q_SCRIPTABLE QString filterWheel(); /** DBUS interface function. * select the filter from the available filters. * @param filter The filter name * @return Returns true if filter is found and set, false otherwise. */ Q_SCRIPTABLE bool setFilter(const QString &filter); Q_SCRIPTABLE QString filter(); /** DBUS interface function. * @return Returns True if current focuser supports auto-focusing */ Q_SCRIPTABLE bool canAutoFocus() { return (focusType == FOCUS_AUTO); } /** DBUS interface function. * @return Returns Half-Flux-Radius in pixels. */ Q_SCRIPTABLE double getHFR() { return currentHFR; } /** DBUS interface function. * Set CCD exposure value * @param value exposure value in seconds. */ Q_SCRIPTABLE Q_NOREPLY void setExposure(double value); Q_SCRIPTABLE double exposure() { return exposureIN->value(); } /** DBUS interface function. * Set CCD binning * @param binX horizontal binning * @param binY vertical binning */ Q_SCRIPTABLE Q_NOREPLY void setBinning(int binX, int binY); /** DBUS interface function. * Set image filter to apply to the image after capture. * @param value Image filter (Auto Stretch, High Contrast, Equalize, High Pass) */ Q_SCRIPTABLE Q_NOREPLY void setImageFilter(const QString &value); /** DBUS interface function. * Set Auto Focus options. The options must be set before starting the autofocus operation. If no options are set, the options loaded from the user configuration are used. * @param enable If true, Ekos will attempt to automatically select the best focus star in the frame. If it fails to select a star, the user will be asked to select a star manually. */ Q_SCRIPTABLE Q_NOREPLY void setAutoStarEnabled(bool enable); /** DBUS interface function. * Set Auto Focus options. The options must be set before starting the autofocus operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, Ekos will capture a subframe around the selected focus star. The subframe size is determined by the boxSize parameter. */ Q_SCRIPTABLE Q_NOREPLY void setAutoSubFrameEnabled(bool enable); /** DBUS interface function. * Set Autofocus parameters * @param boxSize the box size around the focus star in pixels. The boxsize is used to subframe around the focus star. * @param stepSize the initial step size to be commanded to the focuser. If the focuser is absolute, the step size is in ticks. For relative focusers, the focuser will be commanded to focus inward for stepSize milliseconds initially. * @param maxTravel the maximum steps permitted before the autofocus operation aborts. * @param tolerance Measure of how accurate the autofocus algorithm is. If the difference between the current HFR and minimum measured HFR is less than %tolerance after the focuser traversed both ends of the V-curve, then the focusing operation * is deemed successful. Otherwise, the focusing operation will continue. */ Q_SCRIPTABLE Q_NOREPLY void setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance); /** DBUS interface function. * resetFrame Resets the CCD frame to its full native resolution. */ Q_SCRIPTABLE Q_NOREPLY void resetFrame(); /** DBUS interface function. * Return state of Focuser modue (Ekos::FocusState) */ Q_SCRIPTABLE Ekos::FocusState status() { return state; } /** @}*/ /** * @brief Add CCD to the list of available CCD. * @param newCCD pointer to CCD device. */ void addCCD(ISD::GDInterface *newCCD); /** * @brief addFocuser Add focuser to the list of available focusers. * @param newFocuser pointer to focuser device. */ void addFocuser(ISD::GDInterface *newFocuser); /** * @brief addFilter Add filter to the list of available filters. * @param newFilter pointer to filter device. */ void addFilter(ISD::GDInterface *newFilter); /** * @brief removeDevice Remove device from Focus module * @param deviceRemoved pointer to device */ void removeDevice(ISD::GDInterface *deviceRemoved); void setFilterManager(const QSharedPointer &manager); void clearLog(); QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } public slots: /** \addtogroup FocusDBusInterface * @{ */ /* Focus */ /** DBUS interface function. * Start the autofocus operation. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * Abort the autofocus operation. */ Q_SCRIPTABLE Q_NOREPLY void abort(); /** DBUS interface function. * Capture a focus frame. */ Q_SCRIPTABLE Q_NOREPLY void capture(); /** DBUS interface function. * Focus inward * @param ms If set, focus inward for ms ticks (Absolute Focuser), or ms milliseconds (Relative Focuser). If not set, it will use the value specified in the options. */ Q_SCRIPTABLE bool focusIn(int ms = -1); /** DBUS interface function. * Focus outward * @param ms If set, focus outward for ms ticks (Absolute Focuser), or ms milliseconds (Relative Focuser). If not set, it will use the value specified in the options. */ Q_SCRIPTABLE bool focusOut(int ms = -1); /** @}*/ /** * @brief startFraming Begins continuous capture of the CCD and calculates HFR every frame. */ void startFraming(); /** * @brief checkStopFocus Perform checks before stopping the autofocus operation. Some checks are necessary for in-sequence focusing. */ void checkStopFocus(); /** * @brief Check CCD and make sure information is updated accordingly. This simply calls syncCCDInfo for the current CCD. * @param CCDNum By default, we check the already selected CCD in the dropdown menu. If CCDNum is specified, the check is made against this specific CCD in the dropdown menu. * CCDNum is the index of the CCD in the dropdown menu. */ void checkCCD(int CCDNum = -1); /** * @brief syncCCDInfo Read current CCD information and update settings accordingly. */ void syncCCDInfo(); /** * @brief Check Focuser and make sure information is updated accordingly. * @param FocuserNum By default, we check the already selected focuser in the dropdown menu. If FocuserNum is specified, the check is made against this specific focuser in the dropdown menu. * FocuserNum is the index of the focuser in the dropdown menu. */ void checkFocuser(int FocuserNum = -1); /** * @brief Check Filter and make sure information is updated accordingly. * @param filterNum By default, we check the already selected filter in the dropdown menu. If filterNum is specified, the check is made against this specific filter in the dropdown menu. * filterNum is the index of the filter in the dropdown menu. */ void checkFilter(int filterNum = -1); /** * @brief clearDataPoints Remove all data points from HFR plots */ void clearDataPoints(); /** * @brief focusStarSelected The user selected a focus star, save its coordinates and subframe it if subframing is enabled. * @param x X coordinate * @param y Y coordinate */ void focusStarSelected(int x, int y); /** * @brief newFITS A new FITS blob is received by the CCD driver. * @param bp pointer to blob data */ void newFITS(IBLOB *bp); /** * @brief processFocusNumber Read focus number properties of interest as they arrive from the focuser driver and process them accordingly. * @param nvp pointer to updated focuser number property. */ void processFocusNumber(INumberVectorProperty *nvp); /** * @brief checkFocus Given the minimum required HFR, check focus and calculate HFR. If current HFR exceeds required HFR, start autofocus process, otherwise do nothing. * @param requiredHFR Minimum HFR to trigger autofocus process. */ void checkFocus(double requiredHFR); /** * @brief setFocusStatus Upon completion of the focusing process, set its status (fail or pass) and reset focus process to clean state. * @param status If true, the focus process finished successfully. Otherwise, it failed. */ void setAutoFocusResult(bool status); /** * @brief filterChangeWarning Warn the user it is not a good idea to apply image filter in the filter process as they can skew the HFR calculations. * @param index Index of image filter selected by the user. */ void filterChangeWarning(int index); // Log void appendLogText(const QString &); // Adjust focuser offset, relative or absolute void adjustFocusOffset(int value, bool useAbsoluteOffset); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); /** * @brief toggleVideo Turn on and off video streaming if supported by the camera. * @param enabled Set to true to start video streaming, false to stop it if active. */ void toggleVideo(bool enabled); private slots: /** * @brief toggleSubframe Process enabling and disabling subfrag. * @param enable If true, subframing is enabled. If false, subframing is disabled. Even if subframing is enabled, it must be supported by the CCD driver. */ void toggleSubframe(bool enable); void checkAutoStarTimeout(); void setAbsoluteFocusTicks(); void updateBoxSize(int value); void processCaptureTimeout(); void processCaptureFailure(); void setCaptureComplete(); void showFITSViewer(); void toggleFocusingWidgetFullScreen(); void setVideoStreamEnabled(bool enabled); void syncSettings(); void graphPolynomialFunction(); signals: void newLog(const QString &text); void newStatus(Ekos::FocusState state); void newHFR(double hfr, int position); void absolutePositionChanged(int value); void focusPositionAdjusted(); void suspendGuiding(); void resumeGuiding(); void newStarPixmap(QPixmap &); void newProfilePixmap(QPixmap &); private: //////////////////////////////////////////////////////////////////// /// Connections //////////////////////////////////////////////////////////////////// void initConnections(); //////////////////////////////////////////////////////////////////// /// Settings //////////////////////////////////////////////////////////////////// /** * @brief initSettings Connect settings to slots to update the value when changed */ void initSettingsConnections(); /** * @brief loadSettings Load setting from Options and set them accordingly. */ void loadSettings(); //////////////////////////////////////////////////////////////////// /// HFR Plot //////////////////////////////////////////////////////////////////// void initPlots(); void drawHFRPlot(); void drawHFRIndeces(); void drawProfilePlot(); //////////////////////////////////////////////////////////////////// /// Positions //////////////////////////////////////////////////////////////////// void getAbsFocusPosition(); + bool autoFocusChecks(); void autoFocusAbs(); + void autoFocusLinear(); void autoFocusRel(); void resetButtons(); void stop(bool aborted = false); void initView(); // Move the focuser in (negative) or out (positive amount). bool changeFocus(int amount); // Start up capture, or occasionally move focuser again, after current focus-move accomplished. void autoFocusProcessPositionChange(IPState state); // For the Linear algorithm, which always scans in (from higher position to lower position) // if we notice the new position is higher than the current position (that is, it is the start // of a new scan), we adjust the new position to be several steps further out than requested // and set focuserAdditionalMovement to the extra motion, so that after this motion completes // we will then scan back in (back to the originally requested position). This "dance" is done // to reduce backlash on such movement changes and so that we've always focused in before capture. int adjustLinearPosition(int position, int newPosition); /** * @brief syncTrackingBoxPosition Sync the tracking box to the current selected star center */ void syncTrackingBoxPosition(); /// Focuser device needed for focus operation ISD::Focuser *currentFocuser { nullptr }; /// CCD device needed for focus operation ISD::CCD *currentCCD { nullptr }; /// Optional device filter ISD::GDInterface *currentFilter { nullptr }; /// Current filter position int currentFilterPosition { -1 }; int fallbackFilterPosition { -1 }; /// True if we need to change filter position and wait for result before continuing capture bool filterPositionPending { false }; bool fallbackFilterPending { false }; /// List of Focusers QList Focusers; /// List of CCDs QList CCDs; /// They're generic GDInterface because they could be either ISD::CCD or ISD::Filter QList Filters; /// As the name implies FocusDirection lastFocusDirection { FOCUS_NONE }; /// What type of focusing are we doing right now? FocusType focusType { FOCUS_MANUAL }; /// Focus HFR & Centeroid algorithms StarAlgorithm focusDetection { ALGORITHM_GRADIENT }; /// Focus Process Algorithm FocusAlgorithm focusAlgorithm { FOCUS_ITERATIVE }; /********************* * HFR Club variables *********************/ /// Current HFR value just fetched from FITS file double currentHFR { 0 }; /// Last HFR value recorded double lastHFR { 0 }; /// If (currentHFR > deltaHFR) we start the autofocus process. double minimumRequiredHFR { -1 }; /// Maximum HFR recorded double maxHFR { 1 }; /// Is HFR increasing? We're going away from the sweet spot! If HFRInc=1, we re-capture just to make sure HFR calculations are correct, if HFRInc > 1, we switch directions int HFRInc { 0 }; /// If HFR decreasing? Well, good job. Once HFR start decreasing, we can start calculating HFR slope and estimating our next move. int HFRDec { 0 }; /**************************** * Absolute position focusers ****************************/ /// Absolute focus position double currentPosition { 0 }; /// What was our position before we started the focus process? int initialFocuserAbsPosition { -1 }; /// Pulse duration in ms for relative focusers that only support timers, or the number of ticks in a relative or absolute focuser int pulseDuration { 1000 }; /// Does the focuser support absolute motion? bool canAbsMove { false }; /// Does the focuser support relative motion? bool canRelMove { false }; /// Does the focuser support timer-based motion? bool canTimerMove { false }; /// Maximum range of motion for our lovely absolute focuser double absMotionMax { 0 }; /// Minimum range of motion for our lovely absolute focuser double absMotionMin { 0 }; /// How many iterations have we completed now in our absolute autofocus algorithm? We can't go forever int absIterations { 0 }; /**************************** * Misc. variables ****************************/ /// Are we in the process of capturing an image? bool captureInProgress { false }; // Was the frame modified by us? Better keep track since we need to return it to its previous state once we are done with the focus operation. //bool frameModified; /// Was the modified frame subFramed? bool subFramed { false }; /// If the autofocus process fails, let's not ruin the capture session probably taking place in the next tab. Instead, we should restart it and try again, but we keep count until we hit MAXIMUM_RESET_ITERATIONS /// and then we truly give up. int resetFocusIteration { 0 }; /// Which filter must we use once the autofocus process kicks in? int lockedFilterIndex { -1 }; /// Keep track of what we're doing right now bool inAutoFocus { false }; bool inFocusLoop { false }; bool inSequenceFocus { false }; bool resetFocus { false }; /// Did we reverse direction? bool reverseDir { false }; /// Did the user or the auto selection process finish selecting our focus star? bool starSelected { false }; /// Adjust the focus position to a target value bool adjustFocus { false }; // Target frame dimensions //int fx,fy,fw,fh; /// If HFR=-1 which means no stars detected, we need to decide how many times should the re-capture process take place before we give up or reverse direction. int noStarCount { 0 }; /// Track which upload mode the CCD is set to. If set to UPLOAD_LOCAL, then we need to switch it to UPLOAD_CLIENT in order to do focusing, and then switch it back to UPLOAD_LOCAL ISD::CCD::UploadMode rememberUploadMode { ISD::CCD::UPLOAD_CLIENT }; /// Previous binning setting int activeBin { 0 }; /// HFR values for captured frames before averages QVector HFRFrames; // CCD Exposure Looping bool rememberCCDExposureLooping = { false }; QStringList m_LogText; ITextVectorProperty *filterName { nullptr }; INumberVectorProperty *filterSlot { nullptr }; /**************************** * Plot variables ****************************/ /// Plot minimum positions double minPos { 1e6 }; /// Plot maximum positions double maxPos { 0 }; /// List of V curve plot points /// V-Curve graph QCPGraph *v_graph { nullptr }; // Last gaussian fit values QVector lastGausIndexes; QVector lastGausFrequencies; QCPGraph *currentGaus { nullptr }; QCPGraph *firstGaus { nullptr }; QCPGraph *lastGaus { nullptr }; QVector hfr_position, hfr_value; // Pixmaps QPixmap profilePixmap; /// State Ekos::FocusState state { Ekos::FOCUS_IDLE }; /// FITS Scale FITSScale defaultScale; /// CCD Chip frame settings QMap frameSettings; /// Selected star coordinates QVector3D starCenter; // Remember last star center coordinates in case of timeout in manual select mode QVector3D rememberStarCenter; /// Focus Frame FITSView *focusView { nullptr }; /// Star Select Timer QTimer waitStarSelectTimer; /// FITS Viewer in case user want to display in it instead of internal view QPointer fv; /// Track star position and HFR to know if we're detecting bogus stars due to detection algorithm false positive results QVector starsHFR; /// Relative Profile QCustomPlot *profilePlot { nullptr }; QDialog *profileDialog { nullptr }; /// Polynomial fitting. std::unique_ptr polynomialFit; int polySolutionFound { 0 }; QCPGraph *polynomialGraph = nullptr; QCPGraph *focusPoint = nullptr; bool polynomialGraphIsShown = false; // Capture timeout timer QTimer captureTimeout; uint8_t captureTimeoutCounter { 0 }; uint8_t captureFailureCounter { 0 }; // Guide Suspend bool m_GuidingSuspended { false }; // Filter Manager QSharedPointer filterManager; // Experimental linear focuser. std::unique_ptr linearFocuser; int focuserAdditionalMovement { 0 }; bool hasDeviation { false }; }; } diff --git a/kstars/ekos/focus/focus.ui b/kstars/ekos/focus/focus.ui index 14baa2433..d57919797 100644 --- a/kstars/ekos/focus/focus.ui +++ b/kstars/ekos/focus/focus.ui @@ -1,1712 +1,1712 @@ Focus 0 0 790 468 3 3 3 3 3 Qt::Horizontal 1 0 0 Focuser 3 6 3 6 3 0 32 false 32 32 Focus Out .. 28 28 false 0 32 Stop Auto Focus process Stop false 0 0 0 32 Desired absolute focus position Focuser: false 32 32 Go to an absolute focus position .. 28 28 false 0 32 Current absolute focuser position QLineEdit[readOnly="true"] { color: gray } true Steps: false 32 32 Stop focuser motion .. 28 28 false 0 32 Start Auto Focus process Auto Focus true true false 29 29 32 32 Focus In .. 28 28 Start: false 32 32 32 32 Start framing .. 28 28 false 32 32 Capture image .. 28 28 0 0 CCD && Filter Wheel 3 3 3 3 3 false 0 32 1 0 32 3 0.001000000000000 300.000000000000000 0.100000000000000 0.500000000000000 32 32 32 32 Toggle Full Screen 28 28 32 32 32 32 Show in FITS Viewer 28 28 false Filter Wheel FW: Bin: false Number of images to capture Filter: false 0 32 -- Exposure time in seconds Exp: 0 32 Reset focus subframe to full capture Reset .. CCD: 1 false 0 32 Exposure time in seconds Gain: 0 32 false ISO: 1 false 0 32 false 32 32 32 32 Filter Settings .. 28 28 1 0 32 false 32 32 32 32 Live Video .. 28 28 true QTabWidget::Rounded 0 Settings 0 3 3 3 3 QLayout::SetDefaultConstraint 6 3 Box: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Automatically select the best focus star from the image Auto Select Star 0 0 <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> Full Field 0 0 <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> % 1 10.000000000000000 0 0 Suspend Guiding while autofocus in progress Suspend Guiding 0 0 Use dark frames from the library. Dark Frame Settle: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Annulus: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Wait this many seconds before resuming guiding. s 60.000000000000000 0 0 <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 0 0 Subframe around the focus star during the autofocus procedure Sub Frame true 0 0 <html><body><p>Size of the subframe to constrain capture to, in pixels.</p></body></html> px 16 256 16 32 Qt::Vertical 20 40 Process 3 3 3 3 3 3 Tolerance: 0 0 Decrease value to narrow optimal focus point solution radius. Increase to expand solution radius % 0.010000000000000 20.000000000000000 0.100000000000000 1.000000000000000 Effect: 0 0 <html><head/><body><p>Select star detection algorithm</p></body></html> Gradient Centroid Threshold SEP Threshold: false 0 0 <html><body><p>Increase to restrict the centroid to bright cores. Decrease to enclose fuzzy stars.</p></body></html> % 90.000000000000000 500.000000000000000 10.000000000000000 150.000000000000000 0 0 <html><head/><body><p>Select focus process algorithm:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Iterative</span>: 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 style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Polynomial</span>: 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><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Linear</span>: Samples focus inward in a regular fashion, using 2 passes. The algorithm can be slow, but it is more resilient to backlash. Start with the focuser positioned near good focus. Set Initial Step Size and Max Travel for the desired sampling interval and range around start focus position. Tolerance should be around 5%.</li></ul></body></html> Iterative Polynomial - Linear (Experimental) + Linear (Experimental v2) Algorithm: Detection: 0 0 Apply filter to image after capture to enhance it -- Average over: 0 0 <html><body><p>Number of frames to capture in order to average the HFR value at the current focuser position.</p></body></html> frames 1 Qt::Vertical 20 40 Mechanics 3 3 3 3 3 3 0 0 <html><head/><body><p>Wait for this many seconds after moving the focuser before capturing the next image during Auto Focus.</p></body></html> s 3 30.000000000000000 Max Travel: 0 0 <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 1 50000 10 250 Initial Step size: Settle: 0 0 <html><head/><body><p>Maximum travel in steps before the autofocus process aborts</p></body></html> 0 10.000000000000000 100000.000000000000000 1000.000000000000000 10000.000000000000000 Max Step size: <html><head/><body><p>The maximum single step size the algorithm is allowed to command as it searches for the critical focus zone. The calculated step size would be limited to this maximum value.</p></body></html> 10 100000 100000 Backlash: 0 0 <html><body><p>For backlash-aware focuser, the amount of backlash to apply when reversing movement direction.</p></body></html> 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: 0 0 <html><body><p>HFR value in pixels consolidated at the current focuser position.</p></body></html> 32767 true true Qt::Horizontal 40 20 Stars: <html><body><p>Number of stars used for HFR computation at the current focuser position.</p></body></html> true Qt::Horizontal QSizePolicy::Expanding 40 13 Relative Profile... Clear Data QCustomPlot QWidget
auxiliary/qcustomplot.h
1
focuserCombo focusInB focusOutB absTicksLabel absTicksSpin startGotoB stopGotoB startFocusB stopFocusB captureB startLoopB CCDCaptureCombo liveVideoB exposureIN toggleFullScreenB showFITSViewerB binningCombo gainIN ISOCombo FilterDevicesCombo FilterPosCombo filterManagerB resetFrameB useAutoStar useFullField fullFieldInnerRing suspendGuideCheck GuideSettleTime maxTravelIN HFROut relativeProfileB clearDataB
diff --git a/kstars/ekos/focus/focusalgorithms.cpp b/kstars/ekos/focus/focusalgorithms.cpp index 97d46a8c7..a29848e43 100644 --- a/kstars/ekos/focus/focusalgorithms.cpp +++ b/kstars/ekos/focus/focusalgorithms.cpp @@ -1,343 +1,395 @@ /* Ekos Copyright (C) 2019 Hy Murveit 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 "focusalgorithms.h" +#include "polynomialfit.h" #include #include "kstars.h" #include namespace Ekos { /** * @class LinearFocusAlgorithm * @short Autofocus algorithm that linearly samples HFR values. * * @author Hy Murveit * @version 1.0 */ class LinearFocusAlgorithm : public FocusAlgorithmInterface { public: // Constructor initializes a linear autofocus algorithm, starting at some initial position, // sampling HFR values, decreasing the position by some step size, until the algorithm believe's // it's seen a minimum. It then tries to find that minimum in a 2nd pass. LinearFocusAlgorithm(const FocusParams ¶ms); // After construction, this should be called to get the initial position desired by the // focus algorithm. It returns the initial position passed to the constructor if // it has no movement request. int initialPosition() override { return requestedPosition; } // Pass in the measurement for the last requested position. Returns the position for the next // requested measurement, or -1 if the algorithm's done or if there's an error. int newMeasurement(int position, double value) override; private: // Determines the desired focus position for the first sample. void computeInitialPosition(); - // Returns a score evaluating whether index is the location of a minumum in the - // HFR vector. -1 means no. A positive integer indicates yes, but can be overruled - // by another index with a better score. - int evaluateMinimum(int index, bool lastSample); - // Sets the internal state for re-finding the minimum, and returns the requested next // position to sample. - int setupSecondPass(int minIndex, double margin = 2.0); + int setupSecondPass(int position, double value, double margin = 2.0); // Used in the 2nd pass. Focus is getting worse. Requires several consecutive samples getting worse. bool gettingWorse(); + // Adds to the debug log a line summarizing the result of running this algorithm. + void debugLog(); + + // Used to time the focus algorithm. + QTime stopWatch; + // A vector containing the HFR values sampled by this algorithm so far. QVector values; // A vector containing the focus positions corresponding to the HFR values stored above. QVector positions; // Focus position requested by this algorithm the previous step. int requestedPosition; - // Number of iterations processed so far. + // The position where the first pass is begun. Usually requestedPosition unless there's a restart. + int passStartPosition; + // Number of iterations processed so far. Used to see it doesn't exceed params.maxIterations. int numSteps; // The best value in the first pass. The 2nd pass attempts to get within // tolerance of this value. - double minValue; - // The sampling interval--the number of focuser steps reduced each iteration. + double firstPassBestValue; + // The position of the minimum found in the first pass. + int firstPassBestPosition; + // The sampling interval--the recommended number of focuser steps moved inward each iteration + // of the first pass. int stepSize; // The minimum focus position to use. Computed from the focuser limits and maxTravel. int minPositionLimit; // The maximum focus position to use. Computed from the focuser limits and maxTravel. int maxPositionLimit; - // The index of the minimum found in the first pass. - int firstPassMinIndex; - // True if the first iteration has already found a minimum. - bool minimumFound; - // True if the 2nd pass re-found the minimum, and thus the algorithm is done. - bool minimumReFound; - // True if the algorithm failed, and thus is done. - bool searchFailed; + // Counter for the number of times a v-curve minimum above the current position was found, + // which implies the initial focus sweep has passed the minimum and should be terminated. + int numPolySolutionsFound; + // Counter for the number of times a v-curve minimum above the passStartPosition was found, + // which implies the sweep should restart at a higher position. + int numRestartSolutionsFound; + // The index (into values and positions) when the most recent 2nd pass started. + int secondPassStartIndex; + // True if performing the first focus sweep. + bool inFirstPass; }; FocusAlgorithmInterface *MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams& params) { return new LinearFocusAlgorithm(params); } LinearFocusAlgorithm::LinearFocusAlgorithm(const FocusParams &focusParams) : FocusAlgorithmInterface(focusParams) { - requestedPosition = params.startPosition; - stepSize = params.initialStepSize; - minimumFound = false; + stopWatch.start(); + // These variables don't get reset if we restart the algorithm. numSteps = 0; - minimumReFound = false; - searchFailed = false; - minValue = 0; - maxPositionLimit = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); minPositionLimit = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9") - .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed) - .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit).arg(maxPositionLimit); computeInitialPosition(); } void LinearFocusAlgorithm::computeInitialPosition() { + // These variables get reset if the algorithm is restarted. + stepSize = params.initialStepSize; + inFirstPass = true; + firstPassBestValue = -1; + numPolySolutionsFound = 0; + numRestartSolutionsFound = 0; + secondPassStartIndex = -1; + + qCDebug(KSTARS_EKOS_FOCUS) + << QString("Linear: 1st pass. Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9") + .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed) + .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit).arg(maxPositionLimit); + const int position = params.startPosition; int start, end; // If the bounds allow, set the focus to half-travel above the current position // and sample focusing in down-to half-travel below the current position. if (position + params.maxTravel <= maxPositionLimit && position - params.maxTravel >= minPositionLimit) { start = position + params.maxTravel; end = position - params.maxTravel; } else if (position + params.maxTravel > maxPositionLimit) { // If the above hits the focus-out bound, start from the highest focus position possible // and sample down the travel amount. start = maxPositionLimit; end = std::max(minPositionLimit, start - params.maxTravel); } else { // If the range above hits the focus-in bound, try to start from max-travel above the min position // and sample down to the min position. start = std::min(minPositionLimit + params.maxTravel, maxPositionLimit); end = minPositionLimit; } // Now that the start and end of the sampling interval is set, // check to see if the params were reasonably set up. // If too many steps (more than half the allotment) are required, honor stepSize over maxTravel. const int nSteps = (start - end) / params.initialStepSize; if (nSteps > params.maxIterations/2) { const int topSize = start - position; const int bottomSize = position - end; if (topSize <= bottomSize) { const int newStart = position + params.initialStepSize * (params.maxIterations/4); start = std::min(newStart, maxPositionLimit); end = start - params.initialStepSize * (params.maxIterations/2); } else { const int newEnd = position - params.initialStepSize * (params.maxIterations/4); end = std::max(newEnd, minPositionLimit); start = end + params.initialStepSize * (params.maxIterations/2); } } requestedPosition = start; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: initialPosition %1 end %2 steps %3 sized %4") + passStartPosition = requestedPosition; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: initialPosition %1 end %2 steps %3 sized %4") .arg(start).arg(end).arg((start-end)/params.initialStepSize).arg(params.initialStepSize); } int LinearFocusAlgorithm::newMeasurement(int position, double value) { + int thisStepSize = stepSize; ++numSteps; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: step %1, newMeasurement(%2, %3)").arg(numSteps).arg(position).arg(value); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: step %1, newMeasurement(%2, %3)").arg(numSteps).arg(position).arg(value); // Not sure how to get a general value for this. Skip this check? constexpr int LINEAR_POSITION_TOLERANCE = 25; if (abs(position - requestedPosition) > LINEAR_POSITION_TOLERANCE) { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error didn't get the requested position"); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error didn't get the requested position"); return requestedPosition; } // Have we already found a solution? if (focusSolution != -1) { doneString = i18n("Called newMeasurement after a solution was found."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error %1").arg(doneString); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString); + debugLog(); return -1; } // Store the sample values. values.push_back(value); positions.push_back(position); - if (!minimumFound) + if (inFirstPass) { - // The first pass. We're looking for the minimum of a v-curve. - // Check all possible indices to see if we have a V-curve minimum position, and, - // if so, which is the best. - int bestScore = -1; - int bestIndex = -1; - // Is this the last sample before we bump into a limit. - bool lastSample = requestedPosition - stepSize < minPositionLimit; - for (int i = 0; i < values.size(); ++i) { - const int score = evaluateMinimum(i, lastSample); - if (score > 0) { - minimumFound = true; - if (score > bestScore) { - bestScore = score; - bestIndex = i; + constexpr int kMinPolynomialPoints = 5; + constexpr int kNumPolySolutionsRequired = 3; + constexpr int kNumRestartSolutionsRequired = 3; + + if (values.size() >= kMinPolynomialPoints) + { + PolynomialFit fit(2, positions, values); + double minPos, minVal; + if (fit.findMinimum(position, 0, 100000, &minPos, &minVal)) + { + const int distanceToMin = static_cast(position - minPos); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: poly fit(%1): %2 = %3 @ %4 distToMin %5") + .arg(positions.size()).arg(minPos).arg(minVal).arg(position).arg(distanceToMin); + if (distanceToMin >= 0) + { + // The minimum is further inward. + numPolySolutionsFound = 0; + numRestartSolutionsFound = 0; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solutions reset %1 = %2").arg(minPos).arg(minVal); + const int stepsToMin = distanceToMin / stepSize; + // Temporarily increase the step size if the minimum is very far inward. + if (stepsToMin >= 8) + thisStepSize = stepSize * 4; + else if (stepsToMin >= 4) + thisStepSize = stepSize * 2; + } + else + { + // We have potentially passed the bottom of the curve, + // but it's possible it is further back than the start of our sweep. + if (minPos > passStartPosition) + { + numRestartSolutionsFound++; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: RESTART Solution #%1 %2 = %3 @ %4") + .arg(numRestartSolutionsFound).arg(minPos).arg(minVal).arg(position); + } + else + { + numPolySolutionsFound++; + numRestartSolutionsFound = 0; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solution #%1: %2 = %3 @ %4") + .arg(numPolySolutionsFound).arg(minPos).arg(minVal).arg(position); + } + } + + if (numPolySolutionsFound >= kNumPolySolutionsRequired) + { + // We found a minimum. Setup the 2nd pass. We could use either the polynomial min or the + // min measured star as the target HFR. Will use the min of both as I've seen using just + // the polynomial minimum to be too conservative. + double minMeasurement = *std::min_element(values.begin(), values.end()); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 1stPass solution @ %1: pos %2 val %3, min measurement %4") + .arg(position).arg(minPos).arg(minVal).arg(minMeasurement); + return setupSecondPass(static_cast(minPos), std::min(minVal, minMeasurement)); + } + else if (numRestartSolutionsFound >= kNumRestartSolutionsRequired) + { + params.startPosition = static_cast(minPos); + computeInitialPosition(); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Restart @ %1: %1 = %2, start at %3") + .arg(position).arg(minPos).arg(minVal).arg(requestedPosition); + return requestedPosition; } } + else + { + // Minimum failed indicating the 2nd-order polynomial is an inverted U--it has a maximum, + // but no minimum. This is, of course, not a sensible solution for the focuser, but can + // happen with noisy data and perhaps small step sizes. We still might be able to take advantage, + // and notice whether the polynomial is increasing or decreasing locally. For now, do nothing. + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: ******** No poly min: Poly must be inverted"); + } } - if (minimumFound) + else { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: Minimum found at index %1").arg(bestIndex); - return setupSecondPass(bestIndex); + // Don't have enough samples to reliably fit a polynomial. + // Simply step the focus in one more time and iterate. } } else { - // We previously found the v-curve minimum. We're now in a 2nd pass looking to recreate it. - if (value < minValue * (1.0 + params.focusTolerance)) + // In a 2nd pass looking to recreate the 1st pass' minimum. + if (value < firstPassBestValue * (1.0 + params.focusTolerance)) { focusSolution = position; - minimumReFound = true; + focusHFR = value; done = true; doneString = i18n("Solution found."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: solution at position %1 value %2 (best %3)").arg(position).arg(value).arg(minValue); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass solution @ %1 = %2 (best %3)") + .arg(position).arg(value).arg(firstPassBestValue); + debugLog(); return -1; } - else + else if (gettingWorse()) { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: %1 %2 not a solution, not < %3").arg(position).arg(value).arg(minValue * (1.0+params.focusTolerance)); - if (gettingWorse()) - { - // Doesn't look like we'll find something close to the min. Retry the 2nd pass. - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: getting worse, re-running 2nd pass"); - return setupSecondPass(firstPassMinIndex); - } + // Doesn't look like we'll find something close to the min. Retry the 2nd pass. + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: getting worse, re-running 2nd pass"); + return setupSecondPass(firstPassBestPosition, firstPassBestValue); } } - if (numSteps == params.maxIterations - 1) + if (numSteps == params.maxIterations - 2) { // If we're close to exceeding the iteration limit, retry this pass near the old minimum position. const int minIndex = static_cast(std::min_element(values.begin(), values.end()) - values.begin()); - return setupSecondPass(minIndex, 0.5); + return setupSecondPass(positions[minIndex], values[minIndex], 0.5); } else if (numSteps > params.maxIterations) { // Fail. Exceeded our alloted number of iterations. - searchFailed = true; done = true; doneString = i18n("Too many steps."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error %1").arg(doneString); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString); + debugLog(); return -1; } // Setup the next sample. - requestedPosition = requestedPosition - stepSize; + requestedPosition = requestedPosition - thisStepSize; // Make sure the next sample is within bounds. if (requestedPosition < minPositionLimit) { // The position is too low. Pick the min value and go to (or retry) a 2nd iteration. const int minIndex = static_cast(std::min_element(values.begin(), values.end()) - values.begin()); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: reached end without Vmin. Restarting %1 pos %2 value %3") + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: reached end without Vmin. Restarting %1 pos %2 value %3") .arg(minIndex).arg(positions[minIndex]).arg(values[minIndex]); - return setupSecondPass(minIndex); + return setupSecondPass(positions[minIndex], values[minIndex]); } - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: requesting position %1").arg(requestedPosition); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: requesting position %1").arg(requestedPosition); return requestedPosition; } -int LinearFocusAlgorithm::setupSecondPass(int minIndex, double margin) +void LinearFocusAlgorithm::debugLog() { - firstPassMinIndex = minIndex; - minimumFound = true; + QString str("Linear: points=["); + for (int i = 0; i < positions.size(); ++i) + { + str.append(QString("(%1, %2)").arg(positions[i]).arg(values[i])); + if (i < positions.size()-1) + str.append(", "); + } + str.append(QString("];iterations=%1").arg(numSteps)); + str.append(QString(";duration=%1").arg(stopWatch.elapsed()/1000)); + str.append(QString(";solution=%1").arg(focusSolution)); + str.append(QString(";HFR=%1").arg(focusHFR)); + str.append(QString(";filter='%1'").arg(params.filterName)); - int bestPosition = positions[minIndex]; - minValue = values[minIndex]; - // Arbitrarily go back "margin" steps above the best position. - // Could be a problem if backlash were worse than that many steps. - requestedPosition = std::min(static_cast(bestPosition + stepSize * margin), maxPositionLimit); - stepSize = params.initialStepSize / 2; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize); - return requestedPosition; + qCDebug(KSTARS_EKOS_FOCUS) << str; } -// Given the values in the two vectors (values & positions), Evaluates whether index -// is the position of a "V". If a negative number is returned, the answer is no. -// A positive return value is a score. Best score wins. -// -// Implemented as simple heuristic. A V point must have: -// - at least 2 points on both sides that have higher values -// - for both sides: # higher values - # lower values > 2 -// Return value is the number of higher values minus the number of lower values -// counted on both sides. -// Note: could have just returned positive for only the minimum value, but this -// allows for others to be possible minima as well, if the min is too close to the edge. - -int LinearFocusAlgorithm::evaluateMinimum(int index, bool lastSample) +int LinearFocusAlgorithm::setupSecondPass(int position, double value, double margin) { - const double indexValue = values[index]; - const int threshold = 2; - - if (index < threshold) - return -1; - if (!lastSample && index >= values.size() - threshold) - return -1; + firstPassBestPosition = position; + firstPassBestValue = value; + inFirstPass = false; + secondPassStartIndex = values.size(); - // Left means lower focus position (focused in), right means higher position. - int right_num_higher = 0, right_num_lower = 0; - int left_num_higher = 0, left_num_lower = 0; - for (int i = 0; i < index; ++i) - { - if (values[i] >= indexValue) ++right_num_higher; - else ++right_num_lower; - } - for (int i = index+1; i < values.size(); ++i) - { - if (values[i] >= indexValue) ++left_num_higher; - else ++left_num_lower; - } - const int right_difference = right_num_higher - right_num_lower; - const int left_difference = left_num_higher - left_num_lower; - if (right_difference >= threshold && left_difference >= threshold) - return right_difference + left_difference; - return -1; + // Arbitrarily go back "margin" steps above the best position. + // Could be a problem if backlash were worse than that many steps. + requestedPosition = std::min(static_cast(firstPassBestPosition + stepSize * margin), maxPositionLimit); + stepSize = params.initialStepSize / 2; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize); + return requestedPosition; } // Return true if there are "streak" consecutive values which are successively worse. bool LinearFocusAlgorithm::gettingWorse() { // Must have this many consecutive values getting worse. constexpr int streak = 3; const int length = values.size(); + if (secondPassStartIndex < 0) + return false; if (length < streak+1) return false; + // This insures that all the values we're checking are in the latest 2nd pass. + if (length - secondPassStartIndex < streak + 1) + return false; for (int i = length-1; i >= length-streak; --i) if (values[i] <= values[i-1]) return false; return true; } } diff --git a/kstars/ekos/focus/focusalgorithms.h b/kstars/ekos/focus/focusalgorithms.h index 03282b157..fa474e0a6 100644 --- a/kstars/ekos/focus/focusalgorithms.h +++ b/kstars/ekos/focus/focusalgorithms.h @@ -1,88 +1,91 @@ /* Ekos Focus Algorithms Copyright (C) 2019 Hy Murveit 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 namespace Ekos { /** * @class FocusAlgorithmInterface * @short Interface intender for autofocus algorithms. * * @author Hy Murveit * @version 1.0 */ class FocusAlgorithmInterface { public: struct FocusParams { // Maximum movement from current position allowed for the algorithm. int maxTravel; // Initial sampling interval for the algorithm. int initialStepSize; // Absolute position of the focuser when the algorithm starts. int startPosition; // Minimum position the focuser is allowed to reach. int minPositionAllowed; // Maximum position the focuser is allowed to reach. int maxPositionAllowed; // Maximum number of iterations (captures) the focuser may try. int maxIterations; // The focus algorithm may terminate if it gets within this fraction of the best focus, e.g. 0.10. double focusTolerance; + // The name of the filter used, if any. + QString filterName; FocusParams(int _maxTravel, int _initialStepSize, int _startPosition, int _minPositionAllowed, int _maxPositionAllowed, - int _maxIterations, double _focusTolerance) : + int _maxIterations, double _focusTolerance, const QString &filterName_) : maxTravel(_maxTravel), initialStepSize(_initialStepSize), startPosition(_startPosition), minPositionAllowed(_minPositionAllowed), maxPositionAllowed(_maxPositionAllowed), maxIterations(_maxIterations), - focusTolerance(_focusTolerance) {} + focusTolerance(_focusTolerance), filterName(filterName_) {} }; // Constructor initializes an autofocus algorithm from the input params. FocusAlgorithmInterface(const FocusParams &_params) : params(_params) {} virtual ~FocusAlgorithmInterface() {} // After construction, this should be called to get the initial position desired by the // focus algorithm. It returns the start position passed to the constructor if // it has no movement request. virtual int initialPosition() = 0; // Pass in the recent measurement. Returns the position for the next measurement, // or -1 if the algorithms done or if there's an error. virtual int newMeasurement(int position, double value) = 0; // Returns true if the algorithm has terminated either successfully or in error. bool isDone() const { return done; } // Returns the best position. Should be called after isDone() returns true. // Returns -1 if there's an error. int solution() const { return focusSolution; } // Returns human-readable extra error information about why the algorithm is done. QString doneReason() const { return doneString;} // Returns the params used to construct this object. const FocusParams& getParams() const { return params; } protected: FocusParams params; bool done = false; int focusSolution = -1; + double focusHFR = -1; QString doneString; }; // Creates a LinearFocuser. Caller responsible for the memory. FocusAlgorithmInterface *MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams& params); } diff --git a/kstars/ekos/focus/polynomialfit.cpp b/kstars/ekos/focus/polynomialfit.cpp index 1efb72e94..c92be9212 100644 --- a/kstars/ekos/focus/polynomialfit.cpp +++ b/kstars/ekos/focus/polynomialfit.cpp @@ -1,118 +1,130 @@ #include "polynomialfit.h" #include "ekos/ekos.h" #include #include #include #include namespace Ekos { PolynomialFit::PolynomialFit(int degree_, const QVector& x_, const QVector& y_) : degree(degree_), x(x_), y(y_) { + Q_ASSERT(x_.size() == y_.size()); + solve(x, y); +} + +PolynomialFit::PolynomialFit(int degree_, const QVector& x_, const QVector& y_) + : degree(degree_), y(y_) +{ + Q_ASSERT(x_.size() == y_.size()); + for (int i = 0; i < x_.size(); ++i) + { + x.push_back(static_cast(x_[i])); + } solve(x, y); } void PolynomialFit::solve(const QVector& x, const QVector& y) { double chisq = 0; coefficients = gsl_polynomial_fit(x.data(), y.data(), x.count(), degree, chisq); } double PolynomialFit::polynomialFunction(double x, void *params) { PolynomialFit *instance = static_cast(params); if (instance && !instance->coefficients.empty()) { const int order = instance->coefficients.size() - 1; double sum = 0; for (int i = 0; i <= order; ++i) sum += instance->coefficients[i] * pow(x, i); return sum; } return -1; } bool PolynomialFit::findMinimum(double expected, double minPosition, double maxPosition, double *position, double *value) { int status; int iter = 0, max_iter = 100; const gsl_min_fminimizer_type *T; gsl_min_fminimizer *s; double m = expected; gsl_function F; F.function = &PolynomialFit::polynomialFunction; 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, expected, minPosition, maxPosition); 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); minPosition = gsl_min_fminimizer_x_lower(s); maxPosition = gsl_min_fminimizer_x_upper(s); status = gsl_min_test_interval(minPosition, maxPosition, 0.01, 0.0); if (status == GSL_SUCCESS) { *position = m; *value = polynomialFunction(m, this); } } while (status == GSL_CONTINUE && iter < max_iter); gsl_min_fminimizer_free(s); return (status == GSL_SUCCESS); } void PolynomialFit::drawPolynomial(QCustomPlot *plot, QCPGraph *graph) { graph->data()->clear(); QCPRange range = plot->xAxis->range(); double interval = range.size() / 20.0; for(double x = range.lower ; x < range.upper ; x += interval) { double y = polynomialFunction(x, this); graph->addData(x, y); } plot->replot(); } void PolynomialFit::drawMinimum(QCustomPlot *plot, QCPGraph *solutionGraph, double solutionPosition, double solutionValue, const QFont& font) { solutionGraph->data()->clear(); solutionGraph->addData(solutionPosition, solutionValue); QCPItemText *textLabel = new QCPItemText(plot); textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(solutionPosition, solutionValue / 2); textLabel->setColor(Qt::red); textLabel->setPadding(QMargins(0, 0, 0, 0)); textLabel->setBrush(Qt::white); textLabel->setPen(Qt::NoPen); textLabel->setFont(QFont(font.family(), 8)); textLabel->setText(QString::number(solutionPosition)); } } // namespace diff --git a/kstars/ekos/focus/polynomialfit.h b/kstars/ekos/focus/polynomialfit.h index 8d59ecbe2..dfe47da5c 100644 --- a/kstars/ekos/focus/polynomialfit.h +++ b/kstars/ekos/focus/polynomialfit.h @@ -1,50 +1,51 @@ /* Ekos polynomial fit utilities Algorithms Copyright (C) 2019 Hy Murveit This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include #include namespace Ekos { class PolynomialFit { public: // Constructor. Pass in the degree of the desired polynomial fit, and a vector with the x and y values. // The constructor solves for the polynomial coefficients. PolynomialFit(int degree, const QVector& x, const QVector& y); + PolynomialFit(int degree, const QVector& x, const QVector& y); // Returns the minimum position and value in the pointers for the solved polynomial. // Returns false if the polynomial couldn't be solved. bool findMinimum(double expected, double minPosition, double maxPosition, double *position, double *value); // Draws the polynomial on the plot's graph. void drawPolynomial(QCustomPlot *plot, QCPGraph *graph); // Annotate's the plot's solution graph with the solution position. void drawMinimum(QCustomPlot *plot, QCPGraph *solutionGraph, double solutionPosition, double solutionValue, const QFont& font); private: // Solves for the polynomial coefficients. void solve(const QVector& positions, const QVector& values); // Calculates the value of the polynomial at x. // Params will be cast to a PolynomialFit*. static double polynomialFunction(double x, void *params); // Polynomial degree. int degree; // The data values. QVector x, y; // The solved polynomial coefficients. std::vector coefficients; }; }