diff --git a/Tests/kstars_ui/CMakeLists.txt b/Tests/kstars_ui/CMakeLists.txt index 1b1b93d35..ce98ccd46 100644 --- a/Tests/kstars_ui/CMakeLists.txt +++ b/Tests/kstars_ui/CMakeLists.txt @@ -1,25 +1,26 @@ SET(KSTARS_UI_TESTS_SRC kstars_ui_tests.cpp test_ekos.cpp + test_ekos_focus.cpp test_ekos_simulator.cpp) include_directories(${CFITSIO_INCLUDE_DIR}) ECM_ADD_APP_ICON(KSTARS_UI_TESTS_SRC ICONS ../../kstars/icons/16-apps-kstars.png ../../kstars/icons/32-apps-kstars.png ../../kstars/icons/48-apps-kstars.png ../../kstars/icons/64-apps-kstars.png ../../kstars/icons/128-apps-kstars.png ) QT5_ADD_RESOURCES(KSTARS_UI_TESTS_SRC ../../kstars/data/kstars.qrc) ADD_EXECUTABLE(kstars_ui_tests ${KSTARS_UI_TESTS_SRC}) TARGET_LINK_LIBRARIES(kstars_ui_tests ${TEST_LIBRARIES} ${CFITSIO_LIBRARIES}) IF (INDI_FOUND) INCLUDE_DIRECTORIES(${INDI_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(kstars_ui_tests ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES} z) ENDIF () diff --git a/Tests/kstars_ui/kstars_ui_tests.cpp b/Tests/kstars_ui/kstars_ui_tests.cpp index 350150fb7..4c1127809 100644 --- a/Tests/kstars_ui/kstars_ui_tests.cpp +++ b/Tests/kstars_ui/kstars_ui_tests.cpp @@ -1,174 +1,188 @@ /* KStars UI tests Copyright (C) 2017 Csaba Kertesz 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 "kstars_ui_tests.h" #include "test_ekos.h" #include "test_ekos_simulator.h" +#include "test_ekos_focus.h" #include "auxiliary/kspaths.h" #if defined(HAVE_INDI) #include "ekos/manager.h" #include "ekos/profileeditor.h" #endif #include #include #include #include #include #include #include #include #include #include struct KStarsUiTests::_InitialConditions const KStarsUiTests::m_InitialConditions; KStarsUiTests::KStarsUiTests(QObject *parent): QObject(parent) { } void KStarsUiTests::initTestCase() { } void KStarsUiTests::cleanupTestCase() { } void KStarsUiTests::init() { if (KStars::Instance() != nullptr) KTRY_SHOW_KSTARS(); } void KStarsUiTests::cleanup() { foreach (QDialog * d, KStars::Instance()->findChildren()) if (d->isVisible()) d->hide(); } // All QTest features are macros returning with no error code. // Therefore, in order to bail out at first failure, tests cannot use functions to run sub-tests and are required to use grouping macros too. void KStarsUiTests::createInstanceTest() { // Initialize our instance /*QBENCHMARK_ONCE*/ { // Create our test instance KTipDialog::setShowOnStart(false); KStars::createInstance(true, KStarsUiTests::m_InitialConditions.clockRunning, KStarsUiTests::m_InitialConditions.dateTime.toString()); QApplication::processEvents(); } QVERIFY(KStars::Instance() != nullptr); // Initialize our location GeoLocation * const g = KStars::Instance()->data()->locationNamed("Greenwich"); QVERIFY(g != nullptr); KStars::Instance()->data()->setLocation(*g); QCOMPARE(KStars::Instance()->data()->geo()->lat()->Degrees(), g->lat()->Degrees()); QCOMPARE(KStars::Instance()->data()->geo()->lng()->Degrees(), g->lng()->Degrees()); } void KStarsUiTests::raiseKStarsTest() { KTRY_SHOW_KSTARS(); } void KStarsUiTests::initialConditionsTest() { QVERIFY(KStars::Instance() != nullptr); QVERIFY(KStars::Instance()->data() != nullptr); QVERIFY(KStars::Instance()->data()->clock() != nullptr); QCOMPARE(KStars::Instance()->data()->clock()->isActive(), m_InitialConditions.clockRunning); QEXPECT_FAIL("", "Initial KStars clock is set from system local time, not geolocation, and is untestable for now.", Continue); QCOMPARE(KStars::Instance()->data()->clock()->utc().toString(), m_InitialConditions.dateTime.toString()); QEXPECT_FAIL("", "Precision of KStars local time conversion to local time does not allow strict millisecond comparison.", Continue); QCOMPARE(KStars::Instance()->data()->clock()->utc().toLocalTime(), m_InitialConditions.dateTime); #if QT_VERSION >= 0x050800 // However comparison down to nearest second is expected to be OK QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), m_InitialConditions.dateTime.toSecsSinceEpoch()); + + // Test setting time + KStars::Instance()->data()->clock()->setUTC(KStarsDateTime(m_InitialConditions.dateTime)); + QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), m_InitialConditions.dateTime.toSecsSinceEpoch()); #endif -} + } // We want to launch the application before running our tests // Thus we want to explicitly call QApplication::exec(), and run our tests in parallel of the event loop // We then reimplement QTEST_MAIN(KStarsUiTests); // The same will have to be done when interacting with a modal dialog: exec() in main thread, tests in timer-based thread QT_BEGIN_NAMESPACE QTEST_ADD_GPU_BLACKLIST_SUPPORT_DEFS QT_END_NAMESPACE int main(int argc, char *argv[]) { // Create our test application QApplication app(argc, argv); app.setAttribute(Qt::AA_Use96Dpi, true); QTEST_ADD_GPU_BLACKLIST_SUPPORT QTEST_SET_MAIN_SOURCE_PATH QApplication::processEvents(); // Prepare our configuration srand((unsigned int)time(nullptr)); QDir writableDir; writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation)); KCrash::initialize(); // The instance will be created (and benchmarked) as first test in KStarsUiTests qDebug("Deferring instance creation to tests."); // This holds the final result of the test session int result = 0; // Execute tests in sequence, eventually skipping sub-tests based on prior ones QTimer::singleShot(1000, &app, [&] { qDebug("Starting tests..."); // This creates our KStars instance KStarsUiTests * tc = new KStarsUiTests(); result = QTest::qExec(tc, argc, argv); #if defined(HAVE_INDI) - if (!result) + //if (!result) { TestEkos * ek = new TestEkos(); result |= QTest::qExec(ek, argc, argv); + delete ek; + } + + //if (!result) + { + TestEkosSimulator * ek = new TestEkosSimulator(); + result |= QTest::qExec(ek, argc, argv); + delete ek; } - if (!result) + //if (!result) { - TestEkosSimulator * eks = new TestEkosSimulator(); - result |= QTest::qExec(eks, argc, argv); + TestEkosFocus * ek = new TestEkosFocus(); + result |= QTest::qExec(ek, argc, argv); + delete ek; } #endif // Done testing, successful or not qDebug("Tests are done."); app.quit(); }); // Limit execution duration QTimer::singleShot(5*60*1000, &app, &QCoreApplication::quit); app.exec(); KStars::Instance()->close(); delete KStars::Instance(); return result; } diff --git a/Tests/kstars_ui/test_ekos_focus.cpp b/Tests/kstars_ui/test_ekos_focus.cpp new file mode 100644 index 000000000..9ed09d411 --- /dev/null +++ b/Tests/kstars_ui/test_ekos_focus.cpp @@ -0,0 +1,180 @@ +/* KStars UI tests + Copyright (C) 2020 + Eric Dejouhanet + + 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 "kstars_ui_tests.h" +#include "test_ekos.h" +#include "test_ekos_simulator.h" +#include "test_ekos_focus.h" +#include "ekos/manager.h" +#include "kstars.h" + +#if defined(HAVE_INDI) + +TestEkosFocus::TestEkosFocus(QObject *parent) : QObject(parent) +{ + +} + +void TestEkosFocus::initTestCase() +{ + KVERIFY_EKOS_IS_HIDDEN(); + KTRY_OPEN_EKOS(); + KVERIFY_EKOS_IS_OPENED(); + KTRY_EKOS_START_SIMULATORS(); + + // HACK: Reset clock to initial conditions + KStars::Instance()->data()->clock()->setUTC(KStarsDateTime(KStarsUiTests::m_InitialConditions.dateTime)); +} + +void TestEkosFocus::cleanupTestCase() +{ + KTRY_EKOS_STOP_SIMULATORS(); + KTRY_CLOSE_EKOS(); + KVERIFY_EKOS_IS_HIDDEN(); +} + +void TestEkosFocus::init() +{ + +} + +void TestEkosFocus::cleanup() +{ + +} + +void TestEkosFocus::testStarDetection_data() +{ +#if QT_VERSION < 0x050900 + QSKIP("Skipping fixture-based test on old QT version."); +#else + QTest::addColumn("NAME"); + QTest::addColumn("RA"); + QTest::addColumn("DEC"); + + // Altitude computation taken from SchedulerJob::findAltitude + GeoLocation * const geo = KStarsData::Instance()->geo(); + KStarsDateTime const now(KStarsData::Instance()->lt()); + KSNumbers const numbers(now.djd()); + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst()); + + std::list Objects = { "Polaris", "Mizar", "M 51", "M 13", "M 47", "Pherkab", "Dubhe", "Vega", "NGC 2238", "M 81" }; + size_t count = 0; + + foreach (char const *name, Objects) + { + SkyObject const * const so = KStars::Instance()->data()->objectNamed(name); + if (so != nullptr) + { + SkyObject o(*so); + o.updateCoordsNow(&numbers); + o.EquatorialToHorizontal(&LST, geo->lat()); + if (10.0 < o.alt().Degrees()) + { + QTest::addRow("%s", name) + << name + << o.ra().toHMSString() + << o.dec().toDMSString(); + count++; + } + else QWARN(QString("Fixture '%1' altitude is '%2' degrees, discarding.").arg(name).arg(so->alt().Degrees()).toStdString().c_str()); + } + } + + if (!count) + QSKIP("No usable fixture objects, bypassing test."); +#endif +} + +void TestEkosFocus::testStarDetection() +{ +#if QT_VERSION < 0x050900 + QSKIP("Skipping fixture-based test on old QT version."); +#else + Ekos::Manager * const ekos = Ekos::Manager::Instance(); + + QFETCH(QString, NAME); + QFETCH(QString, RA); + QFETCH(QString, DEC); + qDebug("Test focusing on '%s' RA '%s' DEC '%s'", + NAME.toStdString().c_str(), + RA.toStdString().c_str(), + DEC.toStdString().c_str()); + + // Just sync to RA/DEC to make the mount teleport to the object + QWARN("During this test, the mount is not tracking - we leave it as is for the feature in the CCD simulator to trigger a failure."); + QTRY_VERIFY_WITH_TIMEOUT(ekos->mountModule() != nullptr, 5000); + QVERIFY(ekos->mountModule()->sync(RA, DEC)); + + // Wait for Focus to come up, switch to Focus tab + QTRY_VERIFY_WITH_TIMEOUT(ekos->focusModule() != nullptr, 5000); + KTRY_EKOS_GADGET(QTabWidget, toolsWidget); + toolsWidget->setCurrentWidget(ekos->focusModule()); + QTRY_COMPARE_WITH_TIMEOUT(toolsWidget->currentWidget(), ekos->focusModule(), 1000); + + QWARN("The Focus capture button toggles after Ekos is started, leave a bit of time for it to settle."); + QTest::qWait(500); + + KTRY_FOCUS_GADGET(QLineEdit, starsOut); + + // Run the focus procedure for SEP + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0); + KTRY_FOCUS_CAPTURE(1, 1); + QWARN("No way to know when star detection procedure is fininshed."); + QTest::qWait(1000); + QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); + + // Run the focus procedure for Centroid + KTRY_FOCUS_CONFIGURE("Centroid", "Iterative", 0.0, 100.0); + KTRY_FOCUS_CAPTURE(1, 1); + QWARN("No way to know when star detection procedure is fininshed."); + QTest::qWait(1000); + QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); + + // Run the focus procedure for Threshold + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 100.0); + KTRY_FOCUS_CAPTURE(1, 1); + QWARN("No way to know when procedure is fininshed."); + QTest::qWait(1000); + QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); + + // Run the focus procedure for Gradient + KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 100.0); + KTRY_FOCUS_CAPTURE(1, 1); + QWARN("No way to know when procedure is fininshed."); + QTest::qWait(1000); + QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); + + // Run the focus procedure again to cover more code + // Filtering annulus is independent of the detection method + // Run the HFR average over three frames with SEP to avoid + for (double inner = 0.0; inner < 100.0; inner += 23.0) + { + for (double outer = 100.0; inner < outer; outer -= 22.0) + { + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", inner, outer); + KTRY_FOCUS_CAPTURE(0.5, 5); + QTest::qWait(1000); + } + } + + // Test threshold + for (double threshold = 80.0; threshold < 200.0; threshold += 13.3) + { + KTRY_FOCUS_GADGET(QDoubleSpinBox, thresholdSpin); + thresholdSpin->setValue(threshold); + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 100.0); + KTRY_FOCUS_CAPTURE(0.5, 1); + QTest::qWait(1000); + } +#endif +} + +#endif diff --git a/Tests/kstars_ui/test_ekos_focus.h b/Tests/kstars_ui/test_ekos_focus.h new file mode 100644 index 000000000..d813f999c --- /dev/null +++ b/Tests/kstars_ui/test_ekos_focus.h @@ -0,0 +1,110 @@ +/* KStars UI tests + Copyright (C) 2020 + Eric Dejouhanet + + 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. + */ + +#ifndef TESTEKOSFOCUS_H +#define TESTEKOSFOCUS_H + +#include "config-kstars.h" + +#if defined(HAVE_INDI) + +#include + +/** @brief Helper to retrieve a gadget in the Focus tab specifically. + * @param klass is the class of the gadget to look for. + * @param name is the gadget name to look for in the UI configuration. + * @warning Fails the test if the gadget "name" of class "klass" does not exist in the Focus module + */ +#define KTRY_FOCUS_GADGET(klass, name) klass * const name = Ekos::Manager::Instance()->focusModule()->findChild(#name); \ + QVERIFY2(name != nullptr, QString(#klass " '%1' does not exist and cannot be used").arg(#name).toStdString().c_str()) + +/** @brief Helper to click a button in the Focus tab specifically. + * @param button is the gadget name of the button to look for in the UI configuration. + * @warning Fails the test if the button is not currently enabled. + */ +#define KTRY_FOCUS_CLICK(button) do { \ + QTimer::singleShot(200, Ekos::Manager::Instance(), []() { \ + KTRY_FOCUS_GADGET(QPushButton, button); \ + QVERIFY2(button->isEnabled(), QString("QPushButton '%1' is disabled and cannot be clicked").arg(#button).toStdString().c_str()); \ + QTest::mouseClick(button, Qt::LeftButton); }); } while(false) + +/** @brief Helper to set a string text into a QComboBox in the Focus module. + * @param combobox is the gadget name of the QComboBox to look for in the UI configuration. + * @param text is the string text to set in the gadget. + * @note This is a contrived method to set a text into a QComboBox programmatically *and* emit the "activated" message. + * @warning Fails the test if the name does not exist in the Focus UI or if the text cannot be set in the gadget. + */ +#define KTRY_FOCUS_COMBO_SET(combobox, text) do { \ + KTRY_FOCUS_GADGET(QComboBox, combobox); \ + int const cbIndex = combobox->findText(text); \ + QVERIFY(0 <= cbIndex); \ + combobox->setCurrentIndex(cbIndex); \ + combobox->activated(cbIndex); \ + QCOMPARE(combobox->currentText(), QString(text)); } while(false); + +/** @brief Helper for exposure. + * @param exposure is the amount of seconds to expose for. + * @param averaged is the number of frames the procedure should average before computation. + * @note The Focus capture button is disabled during exposure. + * @warning Fails the test if the exposure cannot be entered or if the capture button is + * disabled or does not toggle during exposure or if the stop button is not the opposite of the capture button. + */ +#define KTRY_FOCUS_CAPTURE(exposure, averaged) do { \ + KTRY_FOCUS_GADGET(QDoubleSpinBox, exposureIN); \ + exposureIN->setValue(exposure); \ + KTRY_FOCUS_GADGET(QSpinBox, focusFramesSpin); \ + focusFramesSpin->setValue(averaged); \ + KTRY_FOCUS_GADGET(QPushButton, captureB); \ + KTRY_FOCUS_GADGET(QPushButton, stopFocusB); \ + QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 1000); \ + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); \ + KTRY_FOCUS_CLICK(captureB); \ + QTRY_VERIFY_WITH_TIMEOUT(!captureB->isEnabled(), 1000); \ + QTRY_VERIFY_WITH_TIMEOUT(stopFocusB->isEnabled(), 1000); \ + QTest::qWait(1.2*exposure*averaged); \ + QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 5000); \ + QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 5000); } while (false) + +/** brief Helper to configure main star detection parameters. + * @param detection is the name of the star detection method to use. + * @param algorithm is the name of the autofocus algorithm to use. + * @param fieldin is the lower radius of the annulus filtering stars. + * @param fieldout is the upper radius of the annulus filtering stars. + * @warning Fails the test if detection, algorithm, full-field checkbox or annulus fields cannot be used. + */ +#define KTRY_FOCUS_CONFIGURE(detection, algorithm, fieldin, fieldout) do { \ + KTRY_FOCUS_GADGET(QCheckBox, useFullField); \ + useFullField->setCheckState(Qt::CheckState::Checked); \ + KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldInnerRing); \ + fullFieldInnerRing->setValue(fieldin); \ + KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldOuterRing); \ + fullFieldOuterRing->setValue(fieldout); \ + KTRY_FOCUS_COMBO_SET(focusDetectionCombo, detection); \ + KTRY_FOCUS_COMBO_SET(focusAlgorithmCombo, algorithm); } while (false) + +class TestEkosFocus : public QObject +{ + Q_OBJECT +public: + explicit TestEkosFocus(QObject *parent = nullptr); + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void init(); + void cleanup(); + + void testStarDetection_data(); + void testStarDetection(); +}; + +#endif +#endif // TESTEKOSFOCUS_H diff --git a/Tests/kstars_ui/test_ekos_simulator.cpp b/Tests/kstars_ui/test_ekos_simulator.cpp index f7618719d..62a570637 100644 --- a/Tests/kstars_ui/test_ekos_simulator.cpp +++ b/Tests/kstars_ui/test_ekos_simulator.cpp @@ -1,163 +1,165 @@ /* KStars UI tests Copyright (C) 2020 Eric Dejouhanet 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 "test_ekos_simulator.h" #if defined(HAVE_INDI) #include "kstars_ui_tests.h" #include "test_ekos.h" #include "ekos/manager.h" #include "kstars.h" #include "ksmessagebox.h" TestEkosSimulator::TestEkosSimulator(QObject *parent) : QObject(parent) { } void TestEkosSimulator::initTestCase() { KVERIFY_EKOS_IS_HIDDEN(); KTRY_OPEN_EKOS(); KVERIFY_EKOS_IS_OPENED(); KTRY_EKOS_START_SIMULATORS(); + // HACK: Reset clock to initial conditions + KStars::Instance()->data()->clock()->setUTC(KStarsDateTime(KStarsUiTests::m_InitialConditions.dateTime)); } void TestEkosSimulator::cleanupTestCase() { KTRY_EKOS_STOP_SIMULATORS(); KTRY_CLOSE_EKOS(); KVERIFY_EKOS_IS_HIDDEN(); } void TestEkosSimulator::init() { } void TestEkosSimulator::cleanup() { } void TestEkosSimulator::testMountSlew_data() { #if QT_VERSION < 0x050900 QSKIP("Skipping fixture-based test on old QT version."); #else QTest::addColumn("NAME"); QTest::addColumn("RA"); QTest::addColumn("DEC"); // Altitude computation taken from SchedulerJob::findAltitude GeoLocation * const geo = KStarsData::Instance()->geo(); KStarsDateTime const now(KStarsData::Instance()->lt()); KSNumbers const numbers(now.djd()); CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst()); // Build a list of Messier objects, 5 degree over the horizon - for (int i = 1; i < 103; i++) + for (int i = 1; i < 103; i += 20) { QString name = QString("M %1").arg(i); SkyObject const * const so = KStars::Instance()->data()->objectNamed(name); if (so != nullptr) { SkyObject o(*so); o.updateCoordsNow(&numbers); o.EquatorialToHorizontal(&LST, geo->lat()); - if (5.0 < so->alt().Degrees()) + if (5.0 < o.alt().Degrees()) QTest::addRow("%s", name.toStdString().c_str()) << name - << so->ra().toHMSString() - << so->dec().toDMSString(); + << o.ra().toHMSString() + << o.dec().toDMSString(); } } #endif } void TestEkosSimulator::testMountSlew() { #if QT_VERSION < 0x050900 QSKIP("Skipping fixture-based test on old QT version."); #else Ekos::Manager * const ekos = Ekos::Manager::Instance(); QFETCH(QString, NAME); QFETCH(QString, RA); QFETCH(QString, DEC); qDebug("Test slewing to '%s' RA '%s' DEC '%s'", NAME.toStdString().c_str(), RA.toStdString().c_str(), DEC.toStdString().c_str()); #if 0 // In the mount tab, open the mount control KTRY_EKOS_CLICK("mountToolBoxB"); QTextObject * raInput = Ekos::Manager::Instance()->findChild("targetRATextObject"); QVERIFY(raInput != nullptr); raInput->setProperty("text", QVariant("07h 37m 30s")); QTest::qWait(1000); QTextObject * deInput = Ekos::Manager::Instance()->findChild("targetDETextObject"); QVERIFY(deInput != nullptr); deInput->setProperty("text", QVariant("-14° 31' 50")); QTest::qWait(1000); // Too bad, not accessible... QPushButton * gotoButton = Ekos::Manager::Instance()->findChild(""); QTest::qWait(5000); KTRY_EKOS_CLICK("mountToolBoxB"); #else QVERIFY(ekos->mountModule()->abort()); // Catch the unexpected "under horizon" and the "sun is close" dialogs // This will not catch the altitude security interval bool under_horizon_or_close_to_sun = false; QTimer::singleShot(1000, [&] { QDialog * const dialog = qobject_cast (QApplication::activeModalWidget()); if(dialog != nullptr) { under_horizon_or_close_to_sun = true; emit dialog->reject(); } }); bool const slew_result = ekos->mountModule()->slew(RA, DEC); if (under_horizon_or_close_to_sun) QEXPECT_FAIL(NAME.toStdString().c_str(), QString("Slew target '%1' is expected to be over the horizon during night time.").arg(NAME).toStdString().c_str(), Abort); QVERIFY(slew_result); #endif // DEC slews are precise at plus/minus one arcsecond - expected or not? auto clampRA = [](QString v) { return CachingDms(v, false).arcsec(); }; auto clampDE = [](QString v) { return CachingDms(v, true).arcsec(); }; QLineEdit * raOut = ekos->findChild("raOUT"); QVERIFY(raOut != nullptr); QTRY_VERIFY_WITH_TIMEOUT(abs(clampRA(RA) - clampRA(raOut->text())) < 2, 15000); QTest::qWait(100); if (clampRA(RA) != clampRA(raOut->text())) - QWARN(QString("Target '%1' slewed to with offset RA %2").arg(NAME).arg(RA).toStdString().c_str()); + QWARN(QString("Target '%1' slewed to with offset RA %2").arg(NAME).arg(abs(clampRA(RA) - clampRA(raOut->text()))).toStdString().c_str()); QLineEdit * deOut = Ekos::Manager::Instance()->findChild("decOUT"); QVERIFY(deOut != nullptr); QTRY_VERIFY_WITH_TIMEOUT(abs(clampDE(DEC) - clampDE(deOut->text())) < 2, 15000); QTest::qWait(100); - if (clampRA(DEC) != clampRA(deOut->text())) - QWARN(QString("Target '%1' slewed to with coordinate offset DEC %2").arg(NAME).arg(DEC).toStdString().c_str()); + if (clampDE(DEC) != clampDE(deOut->text())) + QWARN(QString("Target '%1' slewed to with coordinate offset DEC %2").arg(NAME).arg(abs(clampDE(DEC) - clampDE(deOut->text()))).toStdString().c_str()); QVERIFY(Ekos::Manager::Instance()->mountModule()->abort()); #endif } #endif diff --git a/Tests/kstars_ui/test_ekos_simulator.h b/Tests/kstars_ui/test_ekos_simulator.h index bf1bdc016..c8f3b3554 100644 --- a/Tests/kstars_ui/test_ekos_simulator.h +++ b/Tests/kstars_ui/test_ekos_simulator.h @@ -1,65 +1,68 @@ /* KStars UI tests Copyright (C) 2020 Eric Dejouhanet 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. */ #ifndef TESTEKOSSIMULATOR_H #define TESTEKOSSIMULATOR_H #include "config-kstars.h" #ifdef HAVE_INDI #include #include #define KTRY_EKOS_SELECT_PROFILE(profile) do { \ QString const p(profile); \ QComboBox* profileCBox = Ekos::Manager::Instance()->findChild("profileCombo"); \ QVERIFY(profileCBox != nullptr); \ profileCBox->setCurrentText(p); \ QTRY_COMPARE(profileCBox->currentText(), p); } while(false) +#define KTRY_EKOS_GADGET(klass, name) klass * const name = Ekos::Manager::Instance()->findChild(#name); \ + QVERIFY2(name != nullptr, QString(#klass "'%1' does not exist and cannot be used").arg(#name).toStdString().c_str()) + #define KTRY_EKOS_CLICK(button) do { \ - QPushButton * const b = Ekos::Manager::Instance()->findChild(button); \ - QVERIFY2(b != nullptr, QString("QPushButton '%1' does not exist and cannot be clicked").arg(button).toStdString().c_str()); \ - QVERIFY2(b->isEnabled(), QString("QPushButton '%1' is disabled and cannot be clicked").arg(button).toStdString().c_str()); \ - QTimer::singleShot(200, Ekos::Manager::Instance(), [&]() { QTest::mouseClick(b, Qt::LeftButton); }); } while(false) + QTimer::singleShot(200, Ekos::Manager::Instance(), []() { \ + KTRY_EKOS_GADGET(QPushButton, button); \ + QVERIFY2(button->isEnabled(), QString("QPushButton '%1' is disabled and cannot be clicked").arg(#button).toStdString().c_str()); \ + QTest::mouseClick(button, Qt::LeftButton); }); } while(false) #define KTRY_EKOS_START_SIMULATORS() do { \ KTRY_EKOS_SELECT_PROFILE("Simulators"); \ - KTRY_EKOS_CLICK("processINDIB"); \ + KTRY_EKOS_CLICK(processINDIB); \ QWARN("HACK HACK HACK adding delay here for devices to connect"); \ - QTest::qWait(5000); } while(false) + QTest::qWait(10000); } while(false) #define KTRY_EKOS_STOP_SIMULATORS() do { \ - KTRY_EKOS_CLICK("disconnectB"); \ + KTRY_EKOS_CLICK(disconnectB); \ QWARN("Intentionally leaving a delay here for BZ398192"); \ QTest::qWait(5000); \ - KTRY_EKOS_CLICK("processINDIB"); \ + KTRY_EKOS_CLICK(processINDIB); \ QTest::qWait(1000); } while(false) class TestEkosSimulator : public QObject { Q_OBJECT public: explicit TestEkosSimulator(QObject *parent = nullptr); private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testMountSlew_data(); void testMountSlew(); }; #endif #endif // TESTEKOSSIMULATOR_H diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt index e8307e1fb..b2526c918 100644 --- a/kstars/CMakeLists.txt +++ b/kstars/CMakeLists.txt @@ -1,1201 +1,1206 @@ add_subdirectory( data ) add_subdirectory( icons ) add_subdirectory( htmesh ) if (${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) SET(HAVE_KF5WIT 1) # if(NOT BUILD_KSTARS_LITE) # add_subdirectory( tools/whatsinteresting/qml) # endif(NOT BUILD_KSTARS_LITE) else() SET(HAVE_KF5WIT 0) endif() if (ANDROID AND CMAKE_TOOLCHAIN_FILE) include(${CMAKE_TOOLCHAIN_FILE}) endif () if (NOT ANDROID) find_package(ZLIB REQUIRED) find_package(Threads REQUIRED) endif () if(MSVC) add_definitions(-D_USE_MATH_DEFINES=1) add_definitions(-DNOMINMAX) endif() include_directories( ${kstars_SOURCE_DIR}/kstars ${kstars_SOURCE_DIR}/kstars/skyobjects ${kstars_SOURCE_DIR}/kstars/skycomponents ${kstars_SOURCE_DIR}/kstars/auxiliary ${kstars_SOURCE_DIR}/kstars/time ${kstars_SOURCE_DIR}/kstars/tools ) if (INDI_FOUND) if(BUILD_KSTARS_LITE) set (fits_klite_SRCS fitsviewer/fitsdata.cpp ) set (fits2_klite_SRCS fitsviewer/bayer.c fitsviewer/fpack.c fitsviewer/fpackutil.c ) include_directories(${CFITSIO_INCLUDE_DIR}) include_directories(${NOVA_INCLUDE_DIR}) set (indi_klite_SRCS indi/clientmanagerlite.cpp indi/inditelescopelite.cpp kstarslite/skyitems/skynodes/crosshairnode.cpp kstarslite/skyitems/telescopesymbolsitem.cpp ) endif () set(indiui_SRCS indi/streamform.ui indi/drivermanager.ui indi/opsindi.ui indi/indihostconf.ui indi/customdrivers.ui #indi/telescopewizard.ui ) set(indi_SRCS indi/drivermanager.cpp indi/servermanager.cpp indi/clientmanager.cpp indi/blobmanager.cpp indi/guimanager.cpp indi/driverinfo.cpp indi/deviceinfo.cpp indi/indidevice.cpp indi/indigroup.cpp indi/indiproperty.cpp indi/indielement.cpp indi/indistd.cpp indi/indilistener.cpp indi/inditelescope.cpp indi/indiccd.cpp indi/wsmedia.cpp indi/indifocuser.cpp indi/indifilter.cpp indi/indidome.cpp indi/indiweather.cpp indi/indicap.cpp indi/indilightbox.cpp indi/indidbus.cpp indi/opsindi.cpp #indi/telescopewizardprocess.cpp indi/streamwg.cpp indi/videowg.cpp indi/indiwebmanager.cpp indi/customdrivers.cpp ) if (CFITSIO_FOUND) set(ekosui_SRCS ekos/opsekos.ui ekos/manager.ui ekos/profileeditor.ui ekos/profilewizard.ui # Scheduler ekos/scheduler/scheduler.ui ekos/scheduler/mosaic.ui # Capture ekos/capture/capture.ui ekos/capture/calibrationoptions.ui ekos/capture/dslrinfo.ui ekos/capture/rotatorsettings.ui ekos/capture/customproperties.ui # Align ekos/align/align.ui ekos/align/opsastrometry.ui ekos/align/opsalign.ui ekos/align/opsastrometrycfg.ui ekos/align/opsastrometryindexfiles.ui ekos/align/mountmodel.ui ekos/align/opsastap.ui # Focus ekos/focus/focus.ui # Mount ekos/mount/mount.ui # Guide ekos/guide/guide.ui ekos/guide/opscalibration.ui ekos/guide/opsguide.ui ekos/guide/manualdither.ui ekos/observatory/observatory.ui #TODO remove from GIT #ekos/guide/guider.ui #ekos/guide/rcalibration.ui # Auxiliary ekos/auxiliary/filtersettings.ui ekos/auxiliary/opslogs.ui ekos/auxiliary/serialportassistant.ui # Ekos Live ekos/ekoslive/ekoslivedialog.ui # INDI Hub ekos/indihub.ui ) set(ekos_SRCS ekos/ekos.cpp ekos/manager.cpp ekos/profileeditor.cpp ekos/profilewizard.cpp ekos/qMDNS.cpp ekos/opsekos.cpp # Auxiliary ekos/auxiliary/dome.cpp ekos/auxiliary/weather.cpp ekos/auxiliary/dustcap.cpp ekos/auxiliary/darklibrary.cpp ekos/auxiliary/filtermanager.cpp ekos/auxiliary/filterdelegate.cpp ekos/auxiliary/opslogs.cpp ekos/auxiliary/serialportassistant.cpp # Capture ekos/capture/capture.cpp ekos/capture/sequencejob.cpp ekos/capture/dslrinfodialog.cpp ekos/capture/rotatorsettings.cpp ekos/capture/customproperties.cpp # Scheduler ekos/scheduler/schedulerjob.cpp ekos/scheduler/scheduler.cpp ekos/scheduler/mosaic.cpp # Focus ekos/focus/focus.cpp ekos/focus/focusalgorithms.cpp ekos/focus/polynomialfit.cpp # Mount ekos/mount/mount.cpp # Align ekos/align/align.cpp ekos/align/alignview.cpp ekos/align/astrometryparser.cpp ekos/align/opsastrometry.cpp ekos/align/opsalign.cpp ekos/align/opsastrometrycfg.cpp ekos/align/opsastrometryindexfiles.cpp ekos/align/opsastap.cpp ekos/align/offlineastrometryparser.cpp ekos/align/onlineastrometryparser.cpp ekos/align/remoteastrometryparser.cpp ekos/align/astapastrometryparser.cpp # Guide ekos/guide/guide.cpp ekos/guide/guideinterface.cpp ekos/guide/opscalibration.cpp ekos/guide/opsguide.cpp # Internal Guide ekos/guide/internalguide/gmath.cpp ekos/guide/internalguide/internalguider.cpp #ekos/guide/internalguide/guider.cpp ekos/guide/internalguide/matr.cpp #ekos/guide/internalguide/rcalibration.cpp ekos/guide/internalguide/vect.cpp ekos/guide/internalguide/imageautoguiding.cpp # External Guide ekos/guide/externalguide/phd2.cpp ekos/guide/externalguide/linguider.cpp #Observatory ekos/observatory/observatory.cpp ekos/observatory/observatorymodel.cpp ekos/observatory/observatorydomemodel.cpp ekos/observatory/observatoryweathermodel.cpp # Ekos Live ekos/ekoslive/ekosliveclient.cpp ekos/ekoslive/message.cpp ekos/ekoslive/media.cpp ekos/ekoslive/cloud.cpp ) endif(CFITSIO_FOUND) include_directories(${INDI_INCLUDE_DIR}) endif (INDI_FOUND) if (CFITSIO_FOUND) set (sep_SRCS fitsviewer/sep/analyse.c fitsviewer/sep/aperture.c fitsviewer/sep/background.c fitsviewer/sep/convolve.c fitsviewer/sep/deblend.c fitsviewer/sep/extract.c fitsviewer/sep/lutz.c fitsviewer/sep/util.c ) set (fits_SRCS fitsviewer/fitslabel.cpp fitsviewer/fitsviewer.cpp fitsviewer/stretch.cpp fitsviewer/fitstab.cpp fitsviewer/fitsdebayer.cpp fitsviewer/opsfits.cpp ) if (Qt5DataVisualization_FOUND) set(fits_SRCS ${fits_SRCS} fitsviewer/starprofileviewer.cpp) endif() set (fits2_SRCS fitsviewer/bayer.c fitsviewer/fpack.c fitsviewer/fpackutil.c fitsviewer/fitshistogram.cpp fitsviewer/fitsview.cpp fitsviewer/fitsdata.cpp + fitsviewer/fitsstardetector.cpp + fitsviewer/fitsthresholddetector.cpp + fitsviewer/fitsgradientdetector.cpp + fitsviewer/fitscentroiddetector.cpp + fitsviewer/fitssepdetector.cpp ) set (fitsui_SRCS fitsviewer/fitsheaderdialog.ui fitsviewer/statform.ui fitsviewer/fitsdebayer.ui indi/streamform.ui indi/recordingoptions.ui fitsviewer/fitshistogramui.ui fitsviewer/opsfits.ui ) include_directories(${CFITSIO_INCLUDE_DIR}) endif(CFITSIO_FOUND) IF (CFITSIO_FOUND) IF (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")) IF (SANITIZERS) SET_SOURCE_FILES_PROPERTIES(fitsviewer/bayer.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitsdata.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitshistogram.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fitsview.cpp PROPERTIES COMPILE_FLAGS "-fno-sanitize=address,undefined -fomit-frame-pointer") ELSE () SET_SOURCE_FILES_PROPERTIES(fitsviewer/bayer.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") ENDIF () SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/analyse.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/aperture.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -Wno-pointer-arith") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/background.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/deblend.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align -Wno-incompatible-pointer-types-discards-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/extract.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/lutz.c PROPERTIES COMPILE_FLAGS "-Wno-cast-align") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/util.c PROPERTIES COMPILE_FLAGS "-Wno-incompatible-pointer-types-discards-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpack.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpackutil.c PROPERTIES COMPILE_FLAGS "-Wno-error") ELSEIF (NOT WIN32) SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpack.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/fpackutil.c PROPERTIES COMPILE_FLAGS "-Wno-error") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/aperture.c PROPERTIES COMPILE_FLAGS "-Wno-pointer-arith") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/deblend.c PROPERTIES COMPILE_FLAGS "-Wno-discarded-qualifiers") SET_SOURCE_FILES_PROPERTIES(fitsviewer/sep/util.c PROPERTIES COMPILE_FLAGS "-Wno-discarded-qualifiers") ENDIF () ENDIF () if(WCSLIB_FOUND) include_directories( ${WCSLIB_INCLUDE_DIR} ) endif(WCSLIB_FOUND) set(xplanet_SRCS xplanet/opsxplanet.cpp ) set(xplanetui_SRCS xplanet/opsxplanet.ui ) ########### next target ############### set(libkstarstools_SRCS tools/altvstime.cpp tools/avtplotwidget.cpp tools/calendarwidget.cpp tools/conjunctions.cpp tools/eclipsetool.cpp tools/eclipsehandler.cpp tools/eclipsetool/lunareclipsehandler.cpp tools/jmoontool.cpp tools/approachsolver.cpp tools/ksconjunct.cpp tools/eqplotwidget.cpp tools/astrocalc.cpp tools/modcalcangdist.cpp tools/modcalcapcoord.cpp tools/modcalcaltaz.cpp tools/modcalcdaylength.cpp tools/modcalceclipticcoords.cpp tools/modcalcvizequinox.cpp tools/modcalcgalcoord.cpp tools/modcalcgeodcoord.cpp tools/modcalcjd.cpp tools/modcalcplanets.cpp tools/modcalcsidtime.cpp tools/modcalcvlsr.cpp tools/observinglist.cpp tools/obslistpopupmenu.cpp tools/sessionsortfilterproxymodel.cpp tools/obslistwizard.cpp tools/planetviewer.cpp tools/pvplotwidget.cpp tools/scriptargwidgets.cpp tools/scriptbuilder.cpp tools/scriptfunction.cpp tools/skycalendar.cpp tools/wutdialog.cpp tools/flagmanager.cpp tools/horizonmanager.cpp tools/nameresolver.cpp tools/polarishourangle.cpp #FIXME Port to KF5 #tools/moonphasetool.cpp tools/starhopper.cpp tools/eyepiecefield.cpp tools/exporteyepieceview.cpp tools/starhopperdialog.cpp tools/adddeepskyobject.cpp ) if(${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) set(libkstarstools_SRCS ${libkstarstools_SRCS} tools/whatsinteresting/skyobjlistmodel.cpp tools/whatsinteresting/wiview.cpp tools/whatsinteresting/modelmanager.cpp tools/whatsinteresting/skyobjitem.cpp tools/whatsinteresting/wilpsettings.cpp tools/whatsinteresting/wiequipsettings.cpp tools/whatsinteresting/obsconditions.cpp tools/whatsinteresting/skyobjdescription.cpp ) endif() ki18n_wrap_ui(libkstarstools_ui_SRCS tools/altvstime.ui tools/argchangeviewoption.ui tools/argexportimage.ui tools/argloadcolorscheme.ui tools/arglooktoward.ui tools/argfindobject.ui tools/argprintimage.ui tools/argsetaltaz.ui tools/argsetcolor.ui tools/argsetgeolocation.ui tools/argsetlocaltime.ui tools/argsetradec.ui tools/argsettrack.ui tools/argtimescale.ui tools/argwaitfor.ui tools/argwaitforkey.ui tools/argzoom.ui tools/conjunctions.ui tools/eclipsetool.ui tools/modcalcangdist.ui tools/modcalcapcoord.ui tools/modcalcaltaz.ui tools/modcalcdaylength.ui tools/modcalceclipticcoords.ui tools/modcalcvizequinox.ui tools/modcalcgalcoord.ui tools/modcalcgeod.ui tools/modcalcjd.ui tools/modcalcplanets.ui tools/modcalcsidtime.ui tools/modcalcvlsr.ui tools/observinglist.ui tools/obslistwizard.ui tools/optionstreeview.ui tools/planetviewer.ui tools/scriptbuilder.ui tools/scriptnamedialog.ui tools/skycalendar.ui tools/wutdialog.ui tools/flagmanager.ui tools/starhopperdialog.ui tools/horizonmanager.ui tools/adddeepskyobject.ui tools/polarishourangle.ui ) if (${KF5_VERSION} VERSION_EQUAL 5.18.0 OR ${KF5_VERSION} VERSION_GREATER 5.18.0) ki18n_wrap_ui(libkstarstools_ui_SRCS tools/whatsinteresting/wilpsettings.ui tools/whatsinteresting/wiequipsettings.ui ) endif() set(libkstarswidgets_SRCS widgets/clicklabel.cpp widgets/dmsbox.cpp widgets/draglistbox.cpp widgets/fovwidget.cpp widgets/logedit.cpp widgets/magnitudespinbox.cpp widgets/mapcanvas.cpp widgets/thumbimage.cpp widgets/timespinbox.cpp widgets/timestepbox.cpp widgets/timeunitbox.cpp widgets/infoboxwidget.cpp # widgets/genericcalendarwidget.cpp # widgets/moonphasecalendarwidget.cpp widgets/kshelplabel.cpp widgets/unitspinboxwidget.cpp ) ki18n_wrap_ui(libkstarswidgets_ui_SRCS # widgets/genericcalendarwidget.ui widgets/unitspinboxwidget.ui ) set(kstars_KCFG_SRCS Options.kcfgc) set(kstars_options_SRCS options/opsadvanced.cpp options/opscatalog.cpp options/opscolors.cpp options/opsguides.cpp options/opssolarsystem.cpp options/opssatellites.cpp options/opssupernovae.cpp ) set(kstars_optionsui_SRCS options/opsadvanced.ui options/opscatalog.ui options/opscolors.ui options/opsguides.ui options/opssolarsystem.ui options/opssatellites.ui options/opssupernovae.ui ) set(kstars_dialogs_SRCS dialogs/addcatdialog.cpp dialogs/addlinkdialog.cpp dialogs/detaildialog.cpp dialogs/finddialog.cpp dialogs/focusdialog.cpp dialogs/fovdialog.cpp dialogs/locationdialog.cpp dialogs/timedialog.cpp dialogs/exportimagedialog.cpp ) set(kstars_dialogsui_SRCS dialogs/addcatdialog.ui dialogs/addlinkdialog.ui dialogs/details_database.ui dialogs/details_data.ui dialogs/details_data_comet.ui dialogs/details_links.ui dialogs/details_log.ui dialogs/details_position.ui dialogs/finddialog.ui dialogs/focusdialog.ui dialogs/fovdialog.ui dialogs/locationdialog.ui dialogs/wizwelcome.ui dialogs/wizlocation.ui dialogs/wizdownload.ui dialogs/wizdata.ui dialogs/newfov.ui dialogs/exportimagedialog.ui ) set(hips_SRCS hips/healpix.cpp hips/hipsrenderer.cpp hips/scanrender.cpp hips/pixcache.cpp hips/urlfiledownload.cpp hips/opships.cpp ) set(hips_manager_SRCS hips/hipsmanager.cpp ) set(oal_SRCS oal/log.cpp oal/observer.cpp oal/site.cpp oal/session.cpp oal/scope.cpp oal/eyepiece.cpp oal/filter.cpp oal/observation.cpp oal/lens.cpp oal/equipmentwriter.cpp oal/observeradd.cpp oal/execute.cpp ) set(printing_SRCS printing/detailstable.cpp printing/finderchart.cpp printing/foveditordialog.cpp printing/fovsnapshot.cpp printing/kstarsdocument.cpp printing/legend.cpp printing/loggingform.cpp printing/printingwizard.cpp printing/pwizchartconfig.cpp printing/pwizchartcontents.cpp printing/pwizfovbrowse.cpp printing/pwizfovconfig.cpp printing/pwizfovmanual.cpp printing/pwizfovsh.cpp printing/pwizfovtypeselection.cpp printing/pwizobjectselection.cpp printing/pwizprint.cpp printing/shfovexporter.cpp printing/simplefovexporter.cpp ) set(printingui_SRCS printing/foveditordialog.ui printing/pwizchartconfig.ui printing/pwizchartcontents.ui printing/pwizfovbrowse.ui printing/pwizfovconfig.ui printing/pwizfovmanual.ui printing/pwizfovsh.ui printing/pwizfovtypeselection.ui printing/pwizobjectselection.ui printing/pwizprint.ui printing/pwizwelcome.ui ) set( kstars_KCFG_SRCS Options.kcfgc ) set(libkstarscomponents_SRCS skycomponents/skylabeler.cpp skycomponents/highpmstarlist.cpp skycomponents/skymapcomposite.cpp skycomponents/skymesh.cpp skycomponents/linelistindex.cpp skycomponents/linelistlabel.cpp skycomponents/noprecessindex.cpp skycomponents/listcomponent.cpp skycomponents/pointlistcomponent.cpp skycomponents/solarsystemsinglecomponent.cpp skycomponents/solarsystemlistcomponent.cpp skycomponents/earthshadowcomponent.cpp skycomponents/asteroidscomponent.cpp skycomponents/cometscomponent.cpp skycomponents/planetmoonscomponent.cpp skycomponents/solarsystemcomposite.cpp skycomponents/satellitescomponent.cpp skycomponents/starcomponent.cpp skycomponents/deepstarcomponent.cpp skycomponents/deepskycomponent.cpp skycomponents/catalogcomponent.cpp skycomponents/syncedcatalogcomponent.cpp skycomponents/constellationartcomponent.cpp skycomponents/constellationboundarylines.cpp skycomponents/constellationlines.cpp skycomponents/constellationnamescomponent.cpp skycomponents/supernovaecomponent.cpp skycomponents/coordinategrid.cpp skycomponents/equatorialcoordinategrid.cpp skycomponents/horizontalcoordinategrid.cpp skycomponents/localmeridiancomponent.cpp skycomponents/ecliptic.cpp skycomponents/equator.cpp skycomponents/artificialhorizoncomponent.cpp skycomponents/hipscomponent.cpp skycomponents/horizoncomponent.cpp skycomponents/milkyway.cpp skycomponents/skycomponent.cpp skycomponents/skycomposite.cpp skycomponents/starblock.cpp skycomponents/starblocklist.cpp skycomponents/starblockfactory.cpp skycomponents/culturelist.cpp skycomponents/flagcomponent.cpp skycomponents/targetlistcomponent.cpp ) #LIST(APPEND libkstarscomponents_SRCS # #skycomponents/notifyupdatesui.cpp # ) IF (BUILD_KSTARS_LITE) set(libkstarstools_ui_klite_SRCS tools/nameresolver.cpp ) ENDIF () set(kstars_skyobjects_SRCS skyobjects/constellationsart.cpp skyobjects/deepskyobject.cpp skyobjects/jupitermoons.cpp skyobjects/planetmoons.cpp skyobjects/ksasteroid.cpp skyobjects/kscomet.cpp skyobjects/ksmoon.cpp skyobjects/ksearthshadow.cpp skyobjects/ksplanetbase.cpp skyobjects/ksplanet.cpp #skyobjects/kspluto.cpp skyobjects/kssun.cpp skyobjects/skyline.cpp skyobjects/skyobject.cpp skyobjects/skypoint.cpp skyobjects/starobject.cpp skyobjects/trailobject.cpp skyobjects/satellite.cpp skyobjects/satellitegroup.cpp skyobjects/supernova.cpp ) set(kstars_projection_SRCS projections/projector.cpp projections/lambertprojector.cpp projections/gnomonicprojector.cpp projections/stereographicprojector.cpp projections/orthographicprojector.cpp projections/azimuthalequidistantprojector.cpp projections/equirectangularprojector.cpp ) set(kstars_extra_SRCS auxiliary/colorscheme.cpp auxiliary/dms.cpp auxiliary/cachingdms.cpp auxiliary/geolocation.cpp auxiliary/ksfilereader.cpp auxiliary/ksuserdb.cpp auxiliary/binfilehelper.cpp auxiliary/ksutils.cpp auxiliary/ksdssimage.cpp auxiliary/ksdssdownloader.cpp auxiliary/nonlineardoublespinbox.cpp auxiliary/profileinfo.cpp auxiliary/filedownloader.cpp auxiliary/kspaths.cpp auxiliary/QRoundProgressBar.cpp auxiliary/skyobjectlistmodel.cpp auxiliary/ksnotification.cpp auxiliary/ksmessagebox.cpp auxiliary/QProgressIndicator.cpp auxiliary/ctkrangeslider.cpp time/simclock.cpp time/kstarsdatetime.cpp time/timezonerule.cpp ksnumbers.cpp kstarsdata.cpp texturemanager.cpp #to minimize number of indef KSTARS_LITE skypainter.cpp ) SET(kstars_extra_kstars_SRCS auxiliary/thememanager.cpp auxiliary/schememanager.cpp auxiliary/imageviewer.cpp auxiliary/xplanetimageviewer.cpp auxiliary/fov.cpp auxiliary/thumbnailpicker.cpp auxiliary/thumbnaileditor.cpp auxiliary/imageexporter.cpp auxiliary/kswizard.cpp auxiliary/qcustomplot.cpp kstarsdbus.cpp kspopupmenu.cpp ksalmanac.cpp kstarsactions.cpp kstarsinit.cpp kstars.cpp kstarssplash.cpp skymap.cpp skymapdrawabstract.cpp skymapqdraw.cpp skymapevents.cpp skyqpainter.cpp ) # Temporary solution to allow use of qml files from source dir DELETE SET(KSTARSLITE_CPP_OPTIONS -DSOURCE_DIR=\"${kstars_SOURCE_DIR}\" -DQML_IMPORT="${CMAKE_CURRENT_SOURCE_DIR}") set(klite_SRCS kstarslite.cpp kstarsliteinit.cpp skymaplite.cpp skymapliteevents.cpp #Wrappers kstarslite/skypointlite.cpp kstarslite/skyobjectlite.cpp #ImageProvider kstarslite/imageprovider.cpp #Dialogs kstarslite/dialogs/detaildialoglite.cpp kstarslite/dialogs/finddialoglite.cpp kstarslite/dialogs/locationdialoglite.cpp #RootNode kstarslite/skyitems/rootnode.cpp kstarslite/skyitems/skyopacitynode.cpp kstarslite/skyitems/typedeflite.h #SkyItems kstarslite/skyitems/skyitem.cpp kstarslite/skyitems/planetsitem.cpp kstarslite/skyitems/asteroidsitem.cpp kstarslite/skyitems/cometsitem.cpp kstarslite/skyitems/horizonitem.cpp kstarslite/skyitems/labelsitem.cpp kstarslite/skyitems/constellationnamesitem.cpp kstarslite/skyitems/staritem.cpp kstarslite/skyitems/deepstaritem.cpp kstarslite/skyitems/deepskyitem.cpp kstarslite/skyitems/constellationartitem.cpp kstarslite/skyitems/satellitesitem.cpp kstarslite/skyitems/supernovaeitem.cpp kstarslite/skyitems/fovitem.cpp kstarslite/skyitems/syncedcatalogitem.cpp #Line kstarslite/skyitems/lines/linesitem.cpp kstarslite/skyitems/lines/equatoritem.cpp kstarslite/skyitems/lines/eclipticitem.cpp kstarslite/skyitems/lines/milkywayitem.cpp #SkyNodes kstarslite/skyitems/skynodes/planetnode.cpp kstarslite/skyitems/skynodes/skynode.cpp kstarslite/skyitems/skynodes/pointsourcenode.cpp kstarslite/skyitems/skynodes/planetmoonsnode.cpp kstarslite/skyitems/skynodes/horizonnode.cpp kstarslite/skyitems/skynodes/labelnode.cpp kstarslite/skyitems/skynodes/guidelabelnode.cpp kstarslite/skyitems/skynodes/deepskynode.cpp kstarslite/skyitems/skynodes/dsosymbolnode.cpp kstarslite/skyitems/skynodes/skypolygonnode.cpp kstarslite/skyitems/skynodes/constellationartnode.cpp kstarslite/skyitems/skynodes/satellitenode.cpp kstarslite/skyitems/skynodes/supernovanode.cpp kstarslite/skyitems/skynodes/trixelnode.cpp kstarslite/skyitems/skynodes/fovsymbolnode.cpp #Nodes kstarslite/skyitems/skynodes/nodes/pointnode.cpp kstarslite/skyitems/skynodes/nodes/polynode.cpp kstarslite/skyitems/skynodes/nodes/linenode.cpp kstarslite/skyitems/skynodes/nodes/ellipsenode.cpp kstarslite/skyitems/skynodes/nodes/rectnode.cpp #Other kstarslite/deviceorientation.cpp ) set(kstarslite_libtess_SRC #libtess libtess/gluos.h libtess/priorityq-sort.h libtess/sweep.c libtess/tessmono.c libtess/dict-list.h libtess/glu.h libtess/tessellate.c libtess/dict.c libtess/geom.c libtess/memalloc.c libtess/mesh.c libtess/normal.c libtess/priorityq.c libtess/priorityq-heap.c libtess/render.c libtess/tess.c ) IF (BUILD_KSTARS_LITE) ADD_CUSTOM_TARGET(convert_translations ${CMAKE_SOURCE_DIR}/tools/convert_translations.sh ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) ADD_DEPENDENCIES(convert_translations fetch-translations) IF (ANDROID) ADD_CUSTOM_TARGET(convert_translations_to_android ${CMAKE_SOURCE_DIR}/tools/convert_translations.sh ${CMAKE_BINARY_DIR}/packaging/android/export/share/kstars WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) ADD_DEPENDENCIES(convert_translations_to_android fetch-translations) ENDIF () ENDIF () IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") SET_SOURCE_FILES_PROPERTIES(${kstarslite_libtess_SRC} PROPERTIES COMPILE_FLAGS "-Wno-error") ENDIF () #Qml files will be probably moved to user's data dir, but for use #with QtCreator it is more convenient to have them here set(kstarsliteqml_SRCS kstarslite/qml/main.qml kstarslite/qml/constants/Constants.qml kstarslite/qml/modules/SkyMapLiteWrapper.qml kstarslite/qml/modules/BottomMenu.qml kstarslite/qml/modules/KSPage.qml kstarslite/qml/modules/KSListView.qml kstarslite/qml/modules/KSLabel.qml kstarslite/qml/modules/KSText.qml kstarslite/qml/modules/KSTabButton.qml kstarslite/qml/modules/KSTab.qml kstarslite/qml/modules/KSTabBarArrow.qml kstarslite/qml/modules/KSTextField.qml kstarslite/qml/modules/KSButton.qml kstarslite/qml/modules/TopMenu.qml kstarslite/qml/modules/helpers/TopMenuButton.qml kstarslite/qml/modules/helpers/BottomMenuButton.qml kstarslite/qml/modules/Splash.qml kstarslite/qml/modules/helpers/TimeSpinBox.qml kstarslite/qml/modules/TimePage.qml #Popups kstarslite/qml/modules/popups/ProjectionsPopup.qml kstarslite/qml/modules/popups/FOVPopup.qml kstarslite/qml/modules/popups/ColorSchemePopup.qml #Menus kstarslite/qml/modules/menus/ContextMenu.qml #Helpers kstarslite/qml/modules/helpers/PassiveNotification.qml kstarslite/qml/modules/helpers/KSMenuItem.qml kstarslite/qml/modules/helpers/TelescopeControl.qml #Dialogs kstarslite/qml/dialogs/FindDialog.qml kstarslite/qml/dialogs/LocationDialog.qml kstarslite/qml/dialogs/DetailsDialog.qml kstarslite/qml/dialogs/AboutDialog.qml kstarslite/qml/dialogs/helpers/DetailsItem.qml kstarslite/qml/dialogs/helpers/DetailsAddLink.qml kstarslite/qml/dialogs/helpers/LocationEdit.qml kstarslite/qml/dialogs/helpers/LocationLoading.qml kstarslite/qml/dialogs/menus/DetailsLinkMenu.qml kstarslite/qml/dialogs/menus/LocationsGeoMenu.qml #INDI kstarslite/qml/indi/INDIControlPanel.qml kstarslite/qml/indi/DevicePanel.qml kstarslite/qml/indi/ImagePreview.qml kstarslite/qml/indi/modules/MotionControl.qml kstarslite/qml/indi/modules/Led.qml kstarslite/qml/indi/modules/KSLed.qml kstarslite/qml/indi/modules/Property.qml kstarslite/qml/indi/modules/KSComboBox.qml kstarslite/qml/indi/modules/KSButtonSwitch.qml kstarslite/qml/indi/modules/KSCheckBox.qml kstarslite/qml/indi/modules/KSINDIText.qml kstarslite/qml/indi/modules/KSINDITextField.qml kstarslite/qml/indi/modules/KSButtonsSwitchRow.qml #Tutorial kstarslite/qml/modules/tutorial/TutorialPopup.qml kstarslite/qml/modules/tutorial/TutorialExitPopup.qml kstarslite/qml/modules/tutorial/TutorialStep1.qml kstarslite/qml/modules/tutorial/TutorialStep2.qml kstarslite/qml/modules/tutorial/TutorialStep3.qml kstarslite/qml/modules/tutorial/TutorialStep4.qml kstarslite/qml/modules/tutorial/TutorialStep5.qml kstarslite/qml/modules/tutorial/TutorialPane.qml ) add_subdirectory(kstarslite/qml) ADD_CUSTOM_TARGET(kstarsliteqml SOURCES ${kstarsliteqml_SRCS}) if(ANDROID) add_subdirectory(kstarslite/res) endif(ANDROID) set(kstars_SRCS ${indi_SRCS} ${fits_SRCS} ${ekos_SRCS} ${libkstarswidgets_SRCS} ${libkstarscomponents_SRCS} ${libkstarstools_SRCS} ${kstars_extra_SRCS} ${kstars_extra_kstars_SRCS} ${kstars_projection_SRCS} ${xplanet_SRCS} ${kstars_options_SRCS} ${kstars_skyobjects_SRCS} ${kstars_dialogs_SRCS} ${hips_SRCS} ${oal_SRCS} ${printing_SRCS} #KStars Lite ${kstarslite_SRCS} # Generated files ${libkstarstools_ui_SRCS} ${libkstarswidgets_ui_SRCS} ) set(kstarslite_SRCS ${indi_klite_SRCS} ${libkstarscomponents_SRCS} ${kstars_extra_SRCS} ${kstars_projection_SRCS} ${kstars_skyobjects_SRCS} # KStars Lite sources ${klite_SRCS} # Generated files ${libkstarstools_ui_klite_SRCS} ) # Generate all the necessary QLoggingCategory files ecm_qt_declare_logging_category(kstars_SRCS HEADER kstars_debug.h IDENTIFIER KSTARS CATEGORY_NAME org.kde.kstars) ecm_qt_declare_logging_category(kstars_SRCS HEADER indi_debug.h IDENTIFIER KSTARS_INDI CATEGORY_NAME org.kde.kstars.indi) ecm_qt_declare_logging_category(kstars_SRCS HEADER fits_debug.h IDENTIFIER KSTARS_FITS CATEGORY_NAME org.kde.kstars.fits) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_debug.h IDENTIFIER KSTARS_EKOS CATEGORY_NAME org.kde.kstars.ekos) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_capture_debug.h IDENTIFIER KSTARS_EKOS_CAPTURE CATEGORY_NAME org.kde.kstars.ekos.capture) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_focus_debug.h IDENTIFIER KSTARS_EKOS_FOCUS CATEGORY_NAME org.kde.kstars.ekos.focus) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_align_debug.h IDENTIFIER KSTARS_EKOS_ALIGN CATEGORY_NAME org.kde.kstars.ekos.align) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_guide_debug.h IDENTIFIER KSTARS_EKOS_GUIDE CATEGORY_NAME org.kde.kstars.ekos.guide) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_mount_debug.h IDENTIFIER KSTARS_EKOS_MOUNT CATEGORY_NAME org.kde.kstars.ekos.mount) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_scheduler_debug.h IDENTIFIER KSTARS_EKOS_SCHEDULER CATEGORY_NAME org.kde.kstars.ekos.scheduler) ecm_qt_declare_logging_category(kstars_SRCS HEADER ekos_observatory_debug.h IDENTIFIER KSTARS_EKOS_OBSERVATORY CATEGORY_NAME org.kde.kstars.ekos.observatory) kconfig_add_kcfg_files(kstars_SRCS ${kstars_KCFG_SRCS}) ecm_qt_declare_logging_category(kstarslite_SRCS HEADER kstars_debug.h IDENTIFIER KSTARS CATEGORY_NAME org.kde.kstars) ecm_qt_declare_logging_category(kstarslite_SRCS HEADER fits_debug.h IDENTIFIER KSTARS_FITS CATEGORY_NAME org.kde.kstars.fits) kconfig_add_kcfg_files(kstarslite_SRCS ${kstars_KCFG_SRCS}) IF (UNITY_BUILD) ENABLE_UNITY_BUILD(kstars kstars_SRCS 10 cpp) ENABLE_UNITY_BUILD(kstarslite kstarslite_SRCS 10 cpp) ENDIF () set(kstars_SRCS ${kstars_SRCS} ${fits2_SRCS} ${sep_SRCS} ${hips_manager_SRCS}) set(kstarslite_SRCS ${kstarslite_SRCS} ${fits_klite_SRCS} ${sep_SRCS} ${fits2_klite_SRCS} ${kstarslite_libtess_SRC}) IF (NOT ANDROID) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.xml kstars.h KStars) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.SimClock.xml simclock.h SimClock) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.FOV.xml fov.h FOV) IF (INDI_FOUND) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.INDI.xml indi/indidbus.h INDIDBus) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.xml ekos/manager.h Ekos::Manager) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Capture.xml ekos/capture/capture.h Ekos::Capture) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Focus.xml ekos/focus/focus.h Ekos::Focus) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Guide.xml ekos/guide/guide.h Ekos::Guide) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Align.xml ekos/align/align.h Ekos::Align) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Mount.xml ekos/mount/mount.h Ekos::Mount) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Dome.xml ekos/auxiliary/dome.h Ekos::Dome) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Weather.xml ekos/auxiliary/weather.h Ekos::Weather) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.DustCap.xml ekos/auxiliary/dustcap.h Ekos::DustCap) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Scheduler.xml ekos/scheduler/scheduler.h Ekos::Scheduler) qt5_add_dbus_adaptor(kstars_SRCS org.kde.kstars.Ekos.Observatory.xml ekos/observatory/observatory.h Ekos::Observatory) ENDIF () ki18n_wrap_ui(kstars_SRCS ${indiui_SRCS} ${ui_SRCS} ${fitsui_SRCS} ${ekosui_SRCS} ${xplanetui_SRCS} ${kstars_optionsui_SRCS} ${kstars_dialogsui_SRCS} ${printingui_SRCS} auxiliary/thumbnailpicker.ui auxiliary/thumbnaileditor.ui oal/observeradd.ui oal/equipmentwriter.ui oal/execute.ui hips/opships.ui hips/opshipsdisplay.ui hips/opshipscache.ui #skycomponents/notifyupdatesui.ui ) add_library(KStarsLib STATIC ${kstars_SRCS}) include(GenerateExportHeader) generate_export_header(KStarsLib) target_link_libraries(KStarsLib LibKSDataHandlers htmesh KF5::Crash KF5::I18n KF5::NewStuff KF5::KIOFileWidgets KF5::WidgetsAddons KF5::Plotting KF5::Notifications Qt5::Gui Qt5::PrintSupport Qt5::Sql Qt5::Svg Qt5::Qml Qt5::Quick Qt5::Network #Qt5::Positioning Qt5::Concurrent Qt5::WebSockets ${ZLIB_LIBRARIES} ) if (Qt5Keychain_FOUND) target_include_directories(KStarsLib PUBLIC ${QTKEYCHAIN_INCLUDE_DIRS}) target_link_libraries(KStarsLib ${QTKEYCHAIN_LIBRARIES}) endif(Qt5Keychain_FOUND) if (Qt5DataVisualization_FOUND) target_link_libraries(KStarsLib Qt5::DataVisualization) endif(Qt5DataVisualization_FOUND) if (KF5NotifyConfig_FOUND) target_link_libraries(KStarsLib KF5::NotifyConfig) endif(KF5NotifyConfig_FOUND) if(NOT WIN32) target_link_libraries(KStarsLib m) endif(NOT WIN32) ENDIF () if (BUILD_KSTARS_LITE) add_library(KStarsLiteLib STATIC ${kstarslite_SRCS}) target_link_libraries(KStarsLiteLib LibKSDataHandlers htmesh KF5::I18n KF5::Plotting KF5::ConfigGui Qt5::Gui Qt5::Sql Qt5::Qml Qt5::Quick Qt5::QuickControls2 Qt5::Positioning Qt5::PositioningQuick Qt5::Concurrent ${ZLIB_LIBRARIES} ) if (ANDROID) target_link_libraries(KStarsLiteLib Qt5::AndroidExtras) endif () endif () if (CFITSIO_FOUND) if (NOT ANDROID) target_include_directories(KStarsLib PUBLIC ${CFITSIO_INCLUDE_DIR}) target_link_libraries(KStarsLib ${CFITSIO_LIBRARIES}) endif() if (BUILD_KSTARS_LITE) target_include_directories(KStarsLiteLib PUBLIC ${CFITSIO_INCLUDE_DIR}) target_link_libraries(KStarsLiteLib ${CFITSIO_LIBRARIES}) endif() endif(CFITSIO_FOUND) if(INDI_FOUND) if (NOT ANDROID) find_package(Nova REQUIRED) include_directories(${NOVA_INCLUDE_DIR}) endif () ## Support Multiple Platforms. All Require INDI ## WIN32 Desktop: Requires INDI Qt5 Client + GSL ## WIN32 Lite: Requires INDI Qt5 Client ## Linux + MacOS Desktop: Requires INDI Client + GSL ## Linux + MacOS Lite: Requires INDI Qt5 Client ## Android: Requires INDI Qt5 Client built for Android if (NOT ANDROID) find_package(GSL REQUIRED) include_directories(${GSL_INCLUDE_DIRS}) target_link_libraries(KStarsLib ${GSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} KF5::Notifications) endif () if(WIN32 OR ANDROID) if(ANDROID) target_link_libraries(KStarsLiteLib ${INDI_CLIENT_ANDROID_LIBRARIES} ${CFITSIO_LIBRARIES} ${LIBRAW_LIBRARIES}) target_compile_options(KStarsLiteLib PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) else(ANDROID) target_link_libraries(KStarsLib ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES}) endif(ANDROID) else(WIN32 OR ANDROID) if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${INDI_CLIENT_QT_LIBRARIES} ${NOVA_LIBRARIES} z) target_compile_options(KStarsLiteLib PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) endif(BUILD_KSTARS_LITE) target_link_libraries(KStarsLib ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES} z) endif(WIN32 OR ANDROID) endif(INDI_FOUND) if(WCSLIB_FOUND) target_link_libraries(KStarsLib ${WCSLIB_LIBRARIES}) if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${WCSLIB_LIBRARIES}) endif() endif (WCSLIB_FOUND) if(LibRaw_FOUND) if (NOT ANDROID) target_link_libraries(KStarsLib ${LibRaw_LIBRARIES}) endif() if (BUILD_KSTARS_LITE) target_link_libraries(KStarsLiteLib ${LibRaw_LIBRARIES}) endif() endif (LibRaw_FOUND) #FIXME Enable OpenGL Later #if( OPENGL_FOUND ) # target_link_libraries(KStarsLib # ${OPENGL_LIBRARIES} # ${QT_QTOPENGL_LIBRARY} # ) #endif( OPENGL_FOUND ) set (KSTARS_APP_SRCS main.cpp ) # add icon to application sources ecm_add_app_icon(KSTARS_APP_SRCS ICONS ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-kstars.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-kstars.png ) qt5_add_resources(KSTARS_APP_SRCS data/kstars.qrc) if (ANDROID) add_library(kstars SHARED ${KSTARS_APP_SRCS}) target_compile_options(kstars PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) add_dependencies(KStarsLiteLib cfitsio indi raw) target_link_libraries(kstars KStarsLiteLib) else () if (BUILD_KSTARS_LITE) add_executable(kstars_lite ${KSTARS_APP_SRCS}) target_compile_options(kstars_lite PRIVATE ${KSTARSLITE_CPP_OPTIONS} -DUSE_QT5_INDI -DKSTARS_LITE) target_link_libraries(kstars_lite KStarsLiteLib) endif() add_executable(kstars ${KSTARS_APP_SRCS}) target_link_libraries(kstars KStarsLib) endif () install(TARGETS kstars ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install(PROGRAMS org.kde.kstars.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kstars.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES kstars.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) if(INDI_FOUND) #install(FILES ekos/mount/mountbox.qml DESTINATION ${KDE_INSTALL_DATADIR}/kstars/ekos/mount/qml) #install(DIRECTORY ekos/mount/ DESTINATION ${KDE_INSTALL_DATADIR}/kstars/ekos/mount/qml # FILES_MATCHING PATTERN "*.png") endif() if (NOT ANDROID AND BUILD_KSTARS_LITE) install(TARGETS kstars_lite ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) endif() diff --git a/kstars/ekos/auxiliary/darklibrary.cpp b/kstars/ekos/auxiliary/darklibrary.cpp index 2490b1770..ed34c708c 100644 --- a/kstars/ekos/auxiliary/darklibrary.cpp +++ b/kstars/ekos/auxiliary/darklibrary.cpp @@ -1,549 +1,549 @@ /* Ekos Dark Library Handler Copyright (C) 2016 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 "darklibrary.h" #include "auxiliary/ksmessagebox.h" #include "Options.h" #include "kstars.h" #include "kspaths.h" #include "kstarsdata.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" namespace Ekos { DarkLibrary *DarkLibrary::_DarkLibrary = nullptr; DarkLibrary *DarkLibrary::Instance() { if (_DarkLibrary == nullptr) _DarkLibrary = new DarkLibrary(KStars::Instance()); return _DarkLibrary; } DarkLibrary::DarkLibrary(QObject *parent) : QObject(parent) { KStarsData::Instance()->userdb()->GetAllDarkFrames(darkFrames); subtractParams.duration = 0; subtractParams.offsetX = 0; subtractParams.offsetY = 0; subtractParams.targetChip = nullptr; subtractParams.targetImage = nullptr; QDir writableDir; writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "darks"); captureSubtractTimer.setInterval(1000); captureSubtractTimer.setSingleShot(true); } DarkLibrary::~DarkLibrary() { qDeleteAll(darkFiles); } void DarkLibrary::refreshFromDB() { KStarsData::Instance()->userdb()->GetAllDarkFrames(darkFrames); } FITSData *DarkLibrary::getDarkFrame(ISD::CCDChip *targetChip, double duration) { for (auto &map : darkFrames) { // First check CCD name matches and check if we are on the correct chip if (map["ccd"].toString() == targetChip->getCCD()->getDeviceName() && map["chip"].toInt() == static_cast(targetChip->getType())) { int binX, binY; targetChip->getBinning(&binX, &binY); // Then check if binning is the same if (map["binX"].toInt() == binX && map["binY"].toInt() == binY) { // Then check for temperature if (targetChip->getCCD()->hasCooler()) { double temperature = 0; targetChip->getCCD()->getTemperature(&temperature); // TODO make this configurable value, the threshold if (fabs(map["temperature"].toDouble() - temperature) > Options::maxDarkTemperatureDiff()) continue; } // Then check for duration // TODO make this value configurable if (fabs(map["duration"].toDouble() - duration) > 0.05) continue; // Finally check if the duration is acceptable QDateTime frameTime = QDateTime::fromString(map["timestamp"].toString(), Qt::ISODate); if (frameTime.daysTo(QDateTime::currentDateTime()) > Options::darkLibraryDuration()) continue; QString filename = map["filename"].toString(); if (darkFiles.contains(filename)) return darkFiles[filename]; // Finally we made it, let's put it in the hash if (loadDarkFile(filename)) return darkFiles[filename]; else { // Remove bad dark frame emit newLog(i18n("Removing bad dark frame file %1", filename)); darkFiles.remove(filename); QFile::remove(filename); KStarsData::Instance()->userdb()->DeleteDarkFrame(filename); return nullptr; } } } } return nullptr; } bool DarkLibrary::loadDarkFile(const QString &filename) { FITSData *darkData = new FITSData(); bool rc = darkData->loadFITS(filename); if (rc) darkFiles[filename] = darkData; else { emit newLog(i18n("Failed to load dark frame file %1", filename)); delete (darkData); } return rc; } bool DarkLibrary::saveDarkFile(FITSData *darkData) { // 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 ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "darks/darkframe_" + ts + ".fits"; if (darkData->saveFITS(path) != 0) { qCritical() << "DarkLibrary: Failed to save dark frame " << path; return false; } darkFiles[path] = darkData; QVariantMap map; int binX, binY; double temperature = 0; subtractParams.targetChip->getBinning(&binX, &binY); subtractParams.targetChip->getCCD()->getTemperature(&temperature); map["ccd"] = subtractParams.targetChip->getCCD()->getDeviceName(); map["chip"] = static_cast(subtractParams.targetChip->getType()); map["binX"] = binX; map["binY"] = binY; map["temperature"] = temperature; map["duration"] = subtractParams.duration; map["filename"] = path; darkFrames.append(map); emit newLog(i18n("Dark frame saved to %1", path)); KStarsData::Instance()->userdb()->AddDarkFrame(map); return true; } void DarkLibrary::subtract(FITSData *darkData, FITSView *lightImage, FITSScale filter, uint16_t offsetX, uint16_t offsetY) { Q_ASSERT(darkData); Q_ASSERT(lightImage); switch (darkData->property("dataType").toInt()) { case TBYTE: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TSHORT: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TUSHORT: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TLONG: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TULONG: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TFLOAT: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TLONGLONG: subtract(darkData, lightImage, filter, offsetX, offsetY); break; case TDOUBLE: subtract(darkData, lightImage, filter, offsetX, offsetY); break; default: break; } } template void DarkLibrary::subtract(FITSData *darkData, FITSView *lightImage, FITSScale filter, uint16_t offsetX, uint16_t offsetY) { // If telescope is covered, let's uncover it auto checkTelescopeCover = [this]() { if (m_RemoteCap && subtractParams.targetChip && subtractParams.targetChip->getCCD()->getTelescopeType() == ISD::CCD::TELESCOPE_PRIMARY) { // Uncover dust cap if it was covered already if (m_RemoteCap->isUnParked() == false && m_RemoteCap->status() != ISD::DustCap::CAP_UNPARKING) { emit newLog(i18n("UnParking dust cap...")); m_RemoteCap->UnPark(); } } else if (!m_ConfirmationPending) { QString deviceName = subtractParams.targetChip->getCCD()->getDeviceName(); bool hasNoShutter = Options::shutterlessCCDs().contains(deviceName); if (hasNoShutter) // Only ask if no shutter and is temporary file // For regular files, the data is already loaded so no need to ask user to remove cover // since dark data is loaded from disk. { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_ConfirmationPending = false; m_TelescopeCovered = false; }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_ConfirmationPending = false; emit darkFrameCompleted(false); }); m_ConfirmationPending = true; KSMessageBox::Instance()->warningContinueCancel(i18n("Remove cover from the telescope in order to continue."), i18n("Dark Exposure")); } } }; if (m_TelescopeCovered) { checkTelescopeCover(); // Otherwise, call this function again QTimer::singleShot(1000, this, [this, darkData, lightImage, filter, offsetX, offsetY] { subtract(darkData, lightImage, filter, offsetX, offsetY); }); return; } FITSData *lightData = lightImage->getImageData(); - T *lightBuffer = reinterpret_cast(lightData->getImageBuffer()); + T *lightBuffer = reinterpret_cast(lightData->getWritableImageBuffer()); int lightW = lightData->width(); int lightH = lightData->height(); int darkW = darkData->width(); int darkoffset = offsetX + offsetY * darkW; - T *darkBuffer = reinterpret_cast(darkData->getImageBuffer()) + darkoffset; + T const *darkBuffer = reinterpret_cast(darkData->getImageBuffer()) + darkoffset; for (int i = 0; i < lightH; i++) { for (int j = 0; j < lightW; j++) lightBuffer[j] = (lightBuffer[j] > darkBuffer[j]) ? (lightBuffer[j] - darkBuffer[j]) : 0; lightBuffer += lightW; darkBuffer += darkW; } #if 0 int lightOffset = 0; for (int i = 0; i < lightH; i++) { for (int j = 0; j < lightW; j++) { if (lightBuffer[j + lightOffset] > darkBuffer[j + darkoffset]) lightBuffer[j + lightOffset] -= darkBuffer[j + darkoffset]; else lightBuffer[j + lightOffset] = 0; } lightOffset += lightW; darkoffset += darkW; } #endif lightData->applyFilter(filter); //if (Options::autoStretch()) // lightData->applyFilter(FITS_AUTO_STRETCH); //else if (filter == FITS_NONE) // lightData->calculateStats(true); if (filter == FITS_NONE) lightData->calculateStats(true); lightImage->rescale(ZOOM_KEEP_LEVEL); lightImage->updateFrame(); emit darkFrameCompleted(true); } void DarkLibrary::captureAndSubtract(ISD::CCDChip *targetChip, FITSView *targetImage, double duration, uint16_t offsetX, uint16_t offsetY) { auto startTimer = [this, targetChip, targetImage, duration, offsetX, offsetY]() { captureSubtractTimer.disconnect(this); connect(&captureSubtractTimer, &QTimer::timeout, this, [this, targetChip, targetImage, duration, offsetX, offsetY]() { captureAndSubtract(targetChip, targetImage, duration, offsetX, offsetY); }); captureSubtractTimer.start(); }; QStringList shutterfulCCDs = Options::shutterfulCCDs(); QStringList shutterlessCCDs = Options::shutterlessCCDs(); QString deviceName = targetChip->getCCD()->getDeviceName(); bool hasShutter = shutterfulCCDs.contains(deviceName); bool hasNoShutter = shutterlessCCDs.contains(deviceName); // If no information is available either way, then ask the user if (hasShutter == false && hasNoShutter == false) { if (m_ConfirmationPending) { startTimer(); return; } // If DSLR then it is considered to have no shutter // since the camera needs to open the shutter to take dark frames if (targetChip->getISOList().empty() == false) { hasNoShutter = true; shutterlessCCDs.append(deviceName); Options::setShutterlessCCDs(shutterlessCCDs); } else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [ &, deviceName]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterfulCCDs = Options::shutterfulCCDs(); shutterfulCCDs.append(deviceName); Options::setShutterfulCCDs(shutterfulCCDs); m_ConfirmationPending = false; }); connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [ &, deviceName]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); QStringList shutterlessCCDs = Options::shutterlessCCDs(); shutterlessCCDs.append(deviceName); Options::setShutterlessCCDs(shutterlessCCDs); m_ConfirmationPending = false; }); m_ConfirmationPending = true; KSMessageBox::Instance()->questionYesNo(i18n("Does %1 have a shutter?", deviceName), i18n("Dark Exposure")); startTimer(); return; } } // Check if we have a dust cap and the current chip belongs on the primary scope the cap // is covering if (m_RemoteCap && targetChip->getCCD()->getTelescopeType() == ISD::CCD::TELESCOPE_PRIMARY) { // Cover dust cap if we have one if (m_RemoteCap->isParked() == false && m_RemoteCap->status() != ISD::DustCap::CAP_PARKING) { emit newLog(i18n("Parking dust cap...")); m_RemoteCap->Park(); startTimer(); return; } if (m_RemoteCap->hasLight() && m_RemoteCap->isLightOn()) m_RemoteCap->SetLightEnabled(false); } else if (hasNoShutter) { if (!m_TelescopeCovered) { if (m_ConfirmationPending) { startTimer(); return; } // Continue connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); m_TelescopeCovered = true; m_ConfirmationPending = false; }); // Cancel connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [&]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, nullptr); KSMessageBox::Instance()->disconnect(this); captureSubtractTimer.stop(); m_TelescopeCovered = false; m_ConfirmationPending = false; emit darkFrameCompleted(false); }); // if (KMessageBox::warningContinueCancel( // nullptr, i18n("Cover the telescope in order to take a dark exposure."), i18n("Dark Exposure"), // KStandardGuiItem::cont(), KStandardGuiItem::cancel(), // "cover_scope_dialog_notification", KMessageBox::WindowModal | KMessageBox::Notify) == KMessageBox::Cancel) // { // abort(); // return IPS_ALERT; // } m_ConfirmationPending = true; KSMessageBox::Instance()->warningContinueCancel(i18n("Cover the telescope in order to take a dark exposure.") , i18n("Dark Exposure"), Options::manualCoverTimeout()); startTimer(); return; } } targetChip->resetFrame(); targetChip->setCaptureMode(FITS_CALIBRATE); targetChip->setFrameType(FRAME_DARK); subtractParams.targetChip = targetChip; subtractParams.targetImage = targetImage; subtractParams.duration = duration; subtractParams.offsetX = offsetX; subtractParams.offsetY = offsetY; connect(targetChip->getCCD(), SIGNAL(BLOBUpdated(IBLOB*)), this, SLOT(newFITS(IBLOB*))); emit newLog(i18n("Capturing dark frame...")); targetChip->capture(duration); } void DarkLibrary::newFITS(IBLOB * bp) { INDI_UNUSED(bp); Q_ASSERT(subtractParams.targetChip); disconnect(subtractParams.targetChip->getCCD(), SIGNAL(BLOBUpdated(IBLOB*)), this, SLOT(newFITS(IBLOB*))); FITSView *calibrationView = subtractParams.targetChip->getImageView(FITS_CALIBRATE); if (calibrationView == nullptr) { emit darkFrameCompleted(false); return; } emit newLog(i18n("Dark frame received.")); FITSData *calibrationData = new FITSData(); // Deep copy of the data if (calibrationData->loadFITS(calibrationView->getImageData()->filename())) { saveDarkFile(calibrationData); subtract(calibrationData, subtractParams.targetImage, subtractParams.targetChip->getCaptureFilter(), subtractParams.offsetX, subtractParams.offsetY); } else { delete calibrationData; emit darkFrameCompleted(false); emit newLog(i18n("Warning: Cannot load calibration file %1", calibrationView->getImageData()->filename())); } } void DarkLibrary::setRemoteCap(ISD::GDInterface *remoteCap) { if (m_RemoteCap) m_RemoteCap->disconnect(this); m_RemoteCap = dynamic_cast(remoteCap); connect(m_RemoteCap, &ISD::DustCap::newStatus, this, [this](ISD::DustCap::Status status) { m_TelescopeCovered = (status == ISD::DustCap::CAP_PARKED); }); } void DarkLibrary::removeDevice(ISD::GDInterface *device) { if (m_RemoteCap && (m_RemoteCap->getDeviceName() == device->getDeviceName())) { m_RemoteCap->disconnect(this); m_RemoteCap = nullptr; } } void DarkLibrary::reset() { m_RemoteCap = nullptr; subtractParams.duration = 0; subtractParams.offsetX = 0; subtractParams.offsetY = 0; subtractParams.targetChip = nullptr; subtractParams.targetImage = nullptr; } } diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index d4544780a..843ac527f 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3711 +1,3715 @@ /* 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) { for (auto &oneFilter : Filters) { if (oneFilter->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. 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")); // 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, filter()); linearFocuser.reset(MakeLinearFocuser(params)); linearRequestedPosition = linearFocuser->initialPosition(); const int newPosition = adjustLinearPosition(position, linearRequestedPosition); 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) << "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 (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(); } +double Focus::analyzeSources(FITSData *image_data) +{ + // 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()) + { + Q_ASSERT_X(focusView->getTrackingBox().isNull(), __FUNCTION__, "Tracking box is disabled when detecting in full-field"); + + 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(); + + // Get the average HFR of the whole frame + return image_data->getHFR(HFR_AVERAGE); + } + else + { + // If star is already selected then use whatever algorithm currently selected. + if (starSelected) + { + focusView->findStars(focusDetection); + return 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); + + // Get maximum HFR in the frame + return image_data->getHFR(HFR_MAX); + } + } +} + +bool Focus::appendHFR(double newHFR) +{ + // Add new HFR to existing values, even if invalid + HFRFrames.append(newHFR); + + // Prepare a work vector with valid HFR values + QVector samples(HFRFrames); + samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double HFR) { return HFR == -1; }), samples.end()); + + // Perform simple sigma clipping if more than a few samples + if (samples.count() > 3) + { + // Sort all HFRs and extract the median + std::sort(samples.begin(), samples.end()); + const auto median = + ((samples.size() % 2) ? + samples[samples.size() / 2] : + (static_cast(samples[samples.size() / 2 - 1]) + samples[samples.size() / 2]) * .5); + + // Extract the mean + const auto mean = std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); + + // Extract the variance + double variance = 0; + foreach (auto val, samples) + variance += (val - mean) * (val - mean); + + // Deduce the standard deviation + const double stddev = sqrt(variance / samples.size()); + + // Reject those 2 sigma away from median + const double sigmaHigh = median + stddev * 2; + const double sigmaLow = median - stddev * 2; + + // FIXME: why is the first value not considered? + // FIXME: what if there are less than 3 samples after clipping? + QMutableVectorIterator i(samples); + while (i.hasNext()) + { + auto val = i.next(); + if (val > sigmaHigh || val < sigmaLow) + i.remove(); + } + } + + // Consolidate the average HFR + currentHFR = samples.isEmpty() ? -1 : std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); + + // Return whether we need more frame based on user requirement + return HFRFrames.count() < focusFramesSpin->value(); +} + 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); - } - } + currentHFR = analyzeSources(image_data); + focusView->updateFrame(); } // 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 + // Take the new HFR into account, eventually continue to stack samples + if (appendHFR(currentHFR)) { - // If we need to capture more frames to average the HFR, let's do that now. capture(); return; } - + else HFRFrames.clear(); // 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 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; } + // Get target chip + ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); + + // Get target chip binning + int subBinX = 1, subBinY = 1; + if (!targetChip->getBinning(&subBinX, &subBinY)) + qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1."; + // 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(); defaultScale = static_cast(filterCombo->currentIndex()); // 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(); return; } // 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(); 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 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()))); // True for the position-based algorithms and those that simulate position. if (inFocusLoop == false && (canAbsMove || canRelMove || (focusAlgorithm == FOCUS_LINEAR))) { 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); } bool Focus::autoFocusChecks() { if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); 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 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 false; } } else noStarCount = 0; return true; } void Focus::autoFocusLinear() { if (!autoFocusChecks()) return; if (!canAbsMove && !canRelMove && canTimerMove) { const bool kFixPosition = true; if (kFixPosition && (linearRequestedPosition != static_cast(currentPosition))) { qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to " << linearRequestedPosition; currentPosition = linearRequestedPosition; } } hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); 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)) { 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(); } } linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentHFR); const int nextPosition = adjustLinearPosition(static_cast(currentPosition), linearRequestedPosition); if (linearRequestedPosition == -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; } 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) { qCDebug(KSTARS_EKOS_FOCUS) << QString("processFocusNumber %1 %2") .arg(nvp->name).arg(nvp->s == IPS_OK ? "OK" : "ERROR"); // Return if it is not our current focuser if (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; qCDebug(KSTARS_EKOS_FOCUS) << QString("Abs Focuser position changed to %1").arg(currentPosition); 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); qCDebug(KSTARS_EKOS_FOCUS) << QString("Rel Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); 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); qCDebug(KSTARS_EKOS_FOCUS) << QString("Timer Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); } 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 (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 (ccd->getDeviceName() == deviceRemoved->getDeviceName()) { CCDs.removeAll(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); checkCCD(); resetButtons(); } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (filter->getDeviceName() == deviceRemoved->getDeviceName()) { Filters.removeAll(filter); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName())); if (Filters.empty()) { currentFilter = nullptr; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); 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()); else Options::setFocusExposure(exposureIN->value()); }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //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 /////////////////////////////////////////////////////////////////////////// // Default Exposure exposureIN->setValue(Options::focusExposure()); // 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 74b95a4f7..ce6ef3fb2 100644 --- a/kstars/ekos/focus/focus.h +++ b/kstars/ekos/focus/focus.h @@ -1,631 +1,643 @@ /* 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(); + /** @internal Search for stars using the method currently configured, and return the consolidated HFR. + * @param image_data is the FITS frame to work with. + * @return the HFR of the star or field of stars in the frame, depending on the consolidation method, or -1 if it cannot be estimated. + */ + double analyzeSources(FITSData *image_data); + + /** @internal Add a new HFR for the current focuser position. + * @param newHFR is the new HFR to consider for the current focuser position. + * @return true if a new sample is required, else false. + */ + bool appendHFR(double newHFR); + /// 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; // Linear focuser. std::unique_ptr linearFocuser; int focuserAdditionalMovement { 0 }; int linearRequestedPosition { 0 }; bool hasDeviation { false }; }; } diff --git a/kstars/ekos/guide/internalguide/gmath.cpp b/kstars/ekos/guide/internalguide/gmath.cpp index 89e51af38..cb90e21d7 100644 --- a/kstars/ekos/guide/internalguide/gmath.cpp +++ b/kstars/ekos/guide/internalguide/gmath.cpp @@ -1,1808 +1,1808 @@ /* Ekos guide tool Copyright (C) 2012 Andrew Stepanenko Modified by Jasem Mutlaq for KStars. 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 "gmath.h" #include "imageautoguiding.h" #include "Options.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "auxiliary/kspaths.h" #include "ekos_guide_debug.h" #include #include #include #define DEF_SQR_0 (8 - 0) #define DEF_SQR_1 (16 - 0) #define DEF_SQR_2 (32 - 0) #define DEF_SQR_3 (64 - 0) #define DEF_SQR_4 (128 - 0) const guide_square_t guide_squares[] = { { DEF_SQR_0, DEF_SQR_0 *DEF_SQR_0 * 1.0 }, { DEF_SQR_1, DEF_SQR_1 *DEF_SQR_1 * 1.0 }, { DEF_SQR_2, DEF_SQR_2 *DEF_SQR_2 * 1.0 }, { DEF_SQR_3, DEF_SQR_3 *DEF_SQR_3 * 1.0 }, { DEF_SQR_4, DEF_SQR_4 *DEF_SQR_4 * 1.0 }, { -1, -1 } }; const square_alg_t guide_square_alg[] = { { SMART_THRESHOLD, "Smart" }, { SEP_THRESHOLD, "SEP" }, { CENTROID_THRESHOLD, "Fast" }, { AUTO_THRESHOLD, "Auto" }, { NO_THRESHOLD, "No thresh." }, { -1, { 0 } } }; struct Peak { int x; int y; float val; Peak() = default; Peak(int x_, int y_, float val_) : x(x_), y(y_), val(val_) { } bool operator<(const Peak &rhs) const { return val < rhs.val; } }; // JM: Why not use QPoint? typedef struct { int x, y; } point_t; cgmath::cgmath() : QObject() { // sys... ROT_Z = Ekos::Matrix(0); // sky coord. system vars. star_pos = Vector(0); scr_star_pos = Vector(0); reticle_pos = Vector(0); reticle_orts[0] = Vector(0); reticle_orts[1] = Vector(0); reticle_angle = 0; ditherRate[0] = ditherRate[1] = -1; // processing in_params.reset(); out_params.reset(); channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift[GUIDE_RA] = new double[MAX_ACCUM_CNT]; drift[GUIDE_DEC] = new double[MAX_ACCUM_CNT]; memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; QString logFileName = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "guide_log.txt"; logFile.setFileName(logFileName); } cgmath::~cgmath() { delete[] drift[GUIDE_RA]; delete[] drift[GUIDE_DEC]; foreach (float *region, referenceRegions) delete[] region; referenceRegions.clear(); } bool cgmath::setVideoParameters(int vid_wd, int vid_ht, int binX, int binY) { if (vid_wd <= 0 || vid_ht <= 0) return false; video_width = vid_wd / binX; video_height = vid_ht / binY; subBinX = binX; subBinY = binY; //set_reticle_params( video_width/2, video_height/2, -1 ); // keep orientation return true; } void cgmath::setGuideView(FITSView *image) { guideView = image; /*if (guideView) { FITSData *image_data = guideView->getImageData(); setDataBuffer(image_data->getImageBuffer()); setVideoParameters(image_data->getWidth(), image_data->getHeight()); }*/ } bool cgmath::setGuiderParameters(double ccd_pix_wd, double ccd_pix_ht, double guider_aperture, double guider_focal) { if (ccd_pix_wd < 0) ccd_pix_wd = 0; if (ccd_pix_ht < 0) ccd_pix_ht = 0; if (guider_focal <= 0) guider_focal = 1; ccd_pixel_width = ccd_pix_wd / 1000.0; // from mkm to mm ccd_pixel_height = ccd_pix_ht / 1000.0; // from mkm to mm aperture = guider_aperture; focal = guider_focal; return true; } void cgmath::getGuiderParameters(double *ccd_pix_wd, double *ccd_pix_ht, double *guider_aperture, double *guider_focal) { *ccd_pix_wd = ccd_pixel_width * 1000.0; *ccd_pix_ht = ccd_pixel_height * 1000.0; *guider_aperture = aperture; *guider_focal = focal; } void cgmath::createGuideLog() { logFile.close(); logFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&logFile); out << "Guiding rate,x15 arcsec/sec: " << Options::guidingRate() << endl; out << "Focal,mm: " << focal << endl; out << "Aperture,mm: " << aperture << endl; out << "F/D: " << focal / aperture << endl; out << "Frame #, Time Elapsed (ms), RA Error (arcsec), RA Correction (ms), RA Correction Direction, DEC Error " "(arcsec), DEC Correction (ms), DEC Correction Direction" << endl; logTime.restart(); } bool cgmath::setReticleParameters(double x, double y, double ang) { // check frame ranges if (x < 0) x = 0; if (y < 0) y = 0; if (x >= (double)video_width - 1) x = (double)video_width - 1; if (y >= (double)video_height - 1) y = (double)video_height - 1; reticle_pos = Vector(x, y, 0); if (ang >= 0) reticle_angle = ang; ROT_Z = Ekos::RotateZ(-M_PI * reticle_angle / 180.0); // NOTE!!! sing '-' derotates star coordinate system reticle_orts[0] = Vector(1, 0, 0) * 100; reticle_orts[1] = Vector(0, 1, 0) * 100; reticle_orts[0] = reticle_orts[0] * ROT_Z; reticle_orts[1] = reticle_orts[1] * ROT_Z; return true; } bool cgmath::getReticleParameters(double *x, double *y, double *ang) const { *x = reticle_pos.x; *y = reticle_pos.y; if (ang) *ang = reticle_angle; return true; } int cgmath::getSquareAlgorithmIndex(void) const { return square_alg_idx; } info_params_t cgmath::getInfoParameters(void) const { info_params_t ret; Vector p; ret.aperture = aperture; ret.focal = focal; ret.focal_ratio = focal / aperture; p = Vector(video_width, video_height, 0); p = point2arcsec(p); p /= 60; // convert to minutes ret.fov_wd = p.x; ret.fov_ht = p.y; return ret; } uint32_t cgmath::getTicks(void) const { return ticks; } void cgmath::getStarDrift(double *dx, double *dy) const { *dx = star_pos.x; *dy = star_pos.y; } void cgmath::getStarScreenPosition(double *dx, double *dy) const { *dx = scr_star_pos.x; *dy = scr_star_pos.y; } bool cgmath::reset(void) { // square_alg_idx = AUTO_THRESHOLD; // // sky coord. system vars. // star_pos = Vector(0); // scr_star_pos = Vector(0); // setReticleParameters(video_width / 2, video_height / 2, 0.0); ticks = 0; channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; out_params.reset(); memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); // cleanup stat vars. sum = 0; return true; } /*void cgmath::move_square( double newx, double newy ) { square_pos.x = newx; square_pos.y = newy; // check frame ranges if (lastBinX == subBinX) { if( square_pos.x < 0 ) square_pos.x = 0; if( square_pos.y < 0 ) square_pos.y = 0; if( square_pos.x+(double)square_size > (double)video_width ) square_pos.x = (double)(video_width - square_size); if( square_pos.y+(double)square_size > (double)video_height ) square_pos.y = (double)(video_height - square_size); } // FITS Image takes center coords if (guide_frame) { guide_frame->setTrackingBoxEnabled(true); //guide_frame->setTrackingBoxCenter(QPointF(square_pos.x+square_size/2, square_pos.y+square_size/2)); guide_frame->setTrackingBox(QRect(square_pos.x, square_pos.y, square_size/subBinX, square_size/subBinY)); } } void cgmath::resize_square( int size_idx ) { if( size_idx < 0 || size_idx >= (int)(sizeof(guide_squares)/sizeof(guide_square_t))-1) return; if (square_size != guide_squares[size_idx].size) { square_pos.x += (square_size-guide_squares[size_idx].size)/2; square_pos.y += (square_size-guide_squares[size_idx].size)/2; } square_size = guide_squares[size_idx].size; square_square = guide_squares[size_idx].square; square_idx = size_idx; // check position if (guide_frame) { guide_frame->setTrackingBoxEnabled(true); //guide_frame->setTrackingBoxSize(QSize(square_size,square_size)); guide_frame->setTrackingBox(QRect(square_pos.x/subBinX, square_pos.y/subBinY, square_size/subBinX, square_size/subBinY)); } }*/ void cgmath::setSquareAlgorithm(int alg_idx) { if (alg_idx < 0 || alg_idx >= (int)(sizeof(guide_square_alg) / sizeof(square_alg_t)) - 1) return; square_alg_idx = alg_idx; in_params.threshold_alg_idx = square_alg_idx; } Vector cgmath::point2arcsec(const Vector &p) const { Vector arcs; // arcs = 3600*180/pi * (pix*ccd_pix_sz) / focal_len arcs.x = 206264.8062470963552 * p.x * ccd_pixel_width / focal; arcs.y = 206264.8062470963552 * p.y * ccd_pixel_height / focal; return arcs; } bool cgmath::calculateAndSetReticle1D(double start_x, double start_y, double end_x, double end_y, int RATotalPulse) { double phi; phi = calculatePhi(start_x, start_y, end_x, end_y); if (phi < 0) return false; setReticleParameters(start_x, start_y, phi); if (RATotalPulse > 0) { double x = end_x - start_x; double y = end_y - start_y; double len = sqrt(x * x + y * y); // Total pulse includes start --> end --> start ditherRate[GUIDE_RA] = RATotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither RA Rate " << ditherRate[GUIDE_RA] << " ms/Pixel"; } return true; } bool cgmath::calculateAndSetReticle2D(double start_ra_x, double start_ra_y, double end_ra_x, double end_ra_y, double start_dec_x, double start_dec_y, double end_dec_x, double end_dec_y, bool *swap_dec, int RATotalPulse, int DETotalPulse) { double phi_ra = 0; // angle calculated by GUIDE_RA drift double phi_dec = 0; // angle calculated by GUIDE_DEC drift double phi = 0; Vector ra_vect = Normalize(Vector(end_ra_x - start_ra_x, -(end_ra_y - start_ra_y), 0)); Vector dec_vect = Normalize(Vector(end_dec_x - start_dec_x, -(end_dec_y - start_dec_y), 0)); Vector try_increase = dec_vect * Ekos::RotateZ(M_PI / 2); Vector try_decrease = dec_vect * Ekos::RotateZ(-M_PI / 2); double cos_increase = try_increase & ra_vect; double cos_decrease = try_decrease & ra_vect; bool do_increase = cos_increase > cos_decrease ? true : false; phi_ra = calculatePhi(start_ra_x, start_ra_y, end_ra_x, end_ra_y); if (phi_ra < 0) return false; phi_dec = calculatePhi(start_dec_x, start_dec_y, end_dec_x, end_dec_y); if (phi_dec < 0) return false; if (do_increase) phi_dec += 90; else phi_dec -= 90; if (phi_dec > 360) phi_dec -= 360.0; if (phi_dec < 0) phi_dec += 360.0; if (fabs(phi_dec - phi_ra) > 180) { if (phi_ra > phi_dec) phi_ra -= 360; else phi_dec -= 360; } // average angles phi = (phi_ra + phi_dec) / 2; if (phi < 0) phi += 360.0; // check DEC if (swap_dec) *swap_dec = dec_swap = do_increase ? false : true; setReticleParameters(start_ra_x, start_ra_y, phi); if (RATotalPulse > 0) { double x = end_ra_x - start_ra_x; double y = end_ra_y - start_ra_y; double len = sqrt(x * x + y * y); ditherRate[GUIDE_RA] = RATotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither RA Rate " << ditherRate[GUIDE_RA] << " ms/Pixel"; } if (DETotalPulse > 0) { double x = end_dec_x - start_dec_x; double y = end_dec_y - start_dec_y; double len = sqrt(x * x + y * y); ditherRate[GUIDE_DEC] = DETotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither DEC Rate " << ditherRate[GUIDE_DEC] << " ms/Pixel"; } return true; } double cgmath::calculatePhi(double start_x, double start_y, double end_x, double end_y) const { double delta_x, delta_y; double phi; delta_x = end_x - start_x; delta_y = -(end_y - start_y); //if( (!Vector(delta_x, delta_y, 0)) < 2.5 ) // JM 2015-12-10: Lower threshold to 1 pixel if ((!Vector(delta_x, delta_y, 0)) < 1) return -1; // 90 or 270 degrees if (fabs(delta_x) < fabs(delta_y) / 1000000.0) { phi = delta_y > 0 ? 90.0 : 270; } else { phi = 180.0 / M_PI * atan2(delta_y, delta_x); if (phi < 0) phi += 360.0; } return phi; } void cgmath::do_ticks(void) { ticks++; channel_ticks[GUIDE_RA]++; channel_ticks[GUIDE_DEC]++; if (channel_ticks[GUIDE_RA] >= MAX_ACCUM_CNT) channel_ticks[GUIDE_RA] = 0; if (channel_ticks[GUIDE_DEC] >= MAX_ACCUM_CNT) channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA]++; accum_ticks[GUIDE_DEC]++; if (accum_ticks[GUIDE_RA] >= in_params.accum_frame_cnt[GUIDE_RA]) accum_ticks[GUIDE_RA] = 0; if (accum_ticks[GUIDE_DEC] >= in_params.accum_frame_cnt[GUIDE_DEC]) accum_ticks[GUIDE_DEC] = 0; } //-------------------- Processing --------------------------- void cgmath::start(void) { ticks = 0; channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; out_params.reset(); memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); // cleanup stat vars. sum = 0; preview_mode = false; if (focal > 0 && aperture > 0) createGuideLog(); // Create reference Image if (imageGuideEnabled) { foreach (float *region, referenceRegions) delete[] region; referenceRegions.clear(); referenceRegions = partitionImage(); reticle_pos = Vector(0, 0, 0); } } void cgmath::stop(void) { preview_mode = true; } void cgmath::suspend(bool mode) { suspended = mode; } bool cgmath::isSuspended(void) const { return suspended; } bool cgmath::isStarLost(void) const { return lost_star; } void cgmath::setLostStar(bool is_lost) { lost_star = is_lost; } float *cgmath::createFloatImage(FITSData *target) const { FITSData *imageData = target; if (imageData == nullptr) imageData = guideView->getImageData(); // #1 Convert to float array // We only process 1st plane if it is a color image uint32_t imgSize = imageData->width() * imageData->height(); float *imgFloat = new float[imgSize]; if (imgFloat == nullptr) { qCritical() << "Not enough memory for float image array!"; return nullptr; } switch (imageData->property("dataType").toInt()) { case TBYTE: { - uint8_t *buffer = imageData->getImageBuffer(); + uint8_t const *buffer = imageData->getImageBuffer(); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TSHORT: { - int16_t *buffer = reinterpret_cast(imageData->getImageBuffer()); + int16_t const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TUSHORT: { - uint16_t *buffer = reinterpret_cast(imageData->getImageBuffer()); + uint16_t const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TLONG: { - int32_t *buffer = reinterpret_cast(imageData->getImageBuffer()); + int32_t const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TULONG: { - uint32_t *buffer = reinterpret_cast(imageData->getImageBuffer()); + uint32_t const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TFLOAT: { - float *buffer = reinterpret_cast(imageData->getImageBuffer()); + float const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TLONGLONG: { - int64_t *buffer = reinterpret_cast(imageData->getImageBuffer()); + int64_t const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TDOUBLE: { - double *buffer = reinterpret_cast(imageData->getImageBuffer()); + double const *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; default: delete[] imgFloat; return nullptr; } return imgFloat; } QVector cgmath::partitionImage() const { QVector regions; FITSData *imageData = guideView->getImageData(); float *imgFloat = createFloatImage(); if (imgFloat == nullptr) return regions; const uint16_t width = imageData->width(); const uint16_t height = imageData->height(); uint8_t xRegions = floor(width / regionAxis); uint8_t yRegions = floor(height / regionAxis); // Find number of regions to divide the image //uint8_t regions = xRegions * yRegions; float *regionPtr = imgFloat; for (uint8_t i = 0; i < yRegions; i++) { for (uint8_t j = 0; j < xRegions; j++) { // Allocate space for one region float *oneRegion = new float[regionAxis * regionAxis]; // Create points to region and current location of the source image in the desired region float *oneRegionPtr = oneRegion, *imgFloatPtr = regionPtr + j * regionAxis; // copy from image to region line by line for (uint32_t line = 0; line < regionAxis; line++) { memcpy(oneRegionPtr, imgFloatPtr, regionAxis); oneRegionPtr += regionAxis; imgFloatPtr += width; } regions.append(oneRegion); } // Move regionPtr block by (width * regionAxis) elements regionPtr += width * regionAxis; } // We're done with imgFloat delete[] imgFloat; return regions; } void cgmath::setRegionAxis(const uint32_t &value) { regionAxis = value; } Vector cgmath::findLocalStarPosition(void) const { if (useRapidGuide) { return Vector(rapidDX, rapidDY, 0); } FITSData *imageData = guideView->getImageData(); if (imageGuideEnabled) { float xshift = 0, yshift = 0; QVector shifts; float xsum = 0, ysum = 0; QVector imagePartition = partitionImage(); if (imagePartition.isEmpty()) { qWarning() << "Failed to partition regions in image!"; return Vector(-1, -1, -1); } if (imagePartition.count() != referenceRegions.count()) { qWarning() << "Mismatch between reference regions #" << referenceRegions.count() << "and image partition regions #" << imagePartition.count(); // Clear memory in case of mis-match foreach (float *region, imagePartition) { delete[] region; } return Vector(-1, -1, -1); } for (uint8_t i = 0; i < imagePartition.count(); i++) { ImageAutoGuiding::ImageAutoGuiding1(referenceRegions[i], imagePartition[i], regionAxis, &xshift, &yshift); Vector shift(xshift, yshift, -1); qCDebug(KSTARS_EKOS_GUIDE) << "Region #" << i << ": X-Shift=" << xshift << "Y-Shift=" << yshift; xsum += xshift; ysum += yshift; shifts.append(shift); } // Delete partitions foreach (float *region, imagePartition) { delete[] region; } imagePartition.clear(); float average_x = xsum / referenceRegions.count(); float average_y = ysum / referenceRegions.count(); float median_x = shifts[referenceRegions.count() / 2 - 1].x; float median_y = shifts[referenceRegions.count() / 2 - 1].y; qCDebug(KSTARS_EKOS_GUIDE) << "Average : X-Shift=" << average_x << "Y-Shift=" << average_y; qCDebug(KSTARS_EKOS_GUIDE) << "Median : X-Shift=" << median_x << "Y-Shift=" << median_y; return Vector(median_x, median_y, -1); } switch (imageData->property("dataType").toInt()) { case TBYTE: return findLocalStarPosition(); case TSHORT: return findLocalStarPosition(); case TUSHORT: return findLocalStarPosition(); case TLONG: return findLocalStarPosition(); case TULONG: return findLocalStarPosition(); case TFLOAT: return findLocalStarPosition(); case TLONGLONG: return findLocalStarPosition(); case TDOUBLE: return findLocalStarPosition(); default: break; } return Vector(-1, -1, -1); } template Vector cgmath::findLocalStarPosition(void) const { static const double P0 = 0.906, P1 = 0.584, P2 = 0.365, P3 = 0.117, P4 = 0.049, P5 = -0.05, P6 = -0.064, P7 = -0.074, P8 = -0.094; Vector ret; int i, j; double resx, resy, mass, threshold, pval; - T *psrc = nullptr; - T *porigin = nullptr; - T *pptr; + T const *psrc = nullptr; + T const *porigin = nullptr; + T const *pptr; QRect trackingBox = guideView->getTrackingBox(); if (trackingBox.isValid() == false) return Vector(-1, -1, -1); FITSData *imageData = guideView->getImageData(); if (imageData == nullptr) { qCWarning(KSTARS_EKOS_GUIDE) << "Cannot process a nullptr image."; return Vector(-1, -1, -1); } if (square_alg_idx == SEP_THRESHOLD) { int count = imageData->findStars(ALGORITHM_SEP, trackingBox); if (count > 0) { imageData->getHFR(HFR_MAX); Edge *star = imageData->getMaxHFRStar(); if (star) ret = Vector(star->x, star->y, 0); else ret = Vector(-1, -1, -1); //ret = Vector(star->x, star->y, 0) - Vector(trackingBox.x(), trackingBox.y(), 0); } else ret = Vector(-1, -1, -1); return ret; } - T *pdata = reinterpret_cast(imageData->getImageBuffer()); + T const *pdata = reinterpret_cast(imageData->getImageBuffer()); qCDebug(KSTARS_EKOS_GUIDE) << "Tracking Square " << trackingBox; double square_square = trackingBox.width() * trackingBox.width(); psrc = porigin = pdata + trackingBox.y() * video_width + trackingBox.x(); resx = resy = 0; threshold = mass = 0; // several threshold adaptive smart algorithms switch (square_alg_idx) { case CENTROID_THRESHOLD: { int width = trackingBox.width(); int height = trackingBox.width(); float i0, i1, i2, i3, i4, i5, i6, i7, i8; int ix = 0, iy = 0; int xM4; - T *p; + T const *p; double average, fit, bestFit = 0; int minx = 0; int maxx = width; int miny = 0; int maxy = height; for (int x = minx; x < maxx; x++) for (int y = miny; y < maxy; y++) { i0 = i1 = i2 = i3 = i4 = i5 = i6 = i7 = i8 = 0; xM4 = x - 4; p = psrc + (y - 4) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 3) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 2) * video_width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 1) * video_width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 0) * video_width + xM4; i8 += *p++; i6 += *p++; i3 += *p++; i1 += *p++; i0 += *p++; i1 += *p++; i3 += *p++; i6 += *p++; i8 += *p++; p = psrc + (y + 1) * video_width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 2) * video_width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 3) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 4) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; average = (i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8) / 85.0; fit = P0 * (i0 - average) + P1 * (i1 - 4 * average) + P2 * (i2 - 4 * average) + P3 * (i3 - 4 * average) + P4 * (i4 - 8 * average) + P5 * (i5 - 4 * average) + P6 * (i6 - 4 * average) + P7 * (i7 - 8 * average) + P8 * (i8 - 48 * average); if (bestFit < fit) { bestFit = fit; ix = x; iy = y; } } if (bestFit > 50) { double sumX = 0; double sumY = 0; double total = 0; for (int y = iy - 4; y <= iy + 4; y++) { p = psrc + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { double w = *p++; sumX += x * w; sumY += y * w; total += w; } } if (total > 0) { ret = (Vector(trackingBox.x(), trackingBox.y(), 0) + Vector(sumX / total, sumY / total, 0)); return ret; } } return Vector(-1, -1, -1); } break; // Alexander's Stepanenko smart threshold algorithm case SMART_THRESHOLD: { point_t bbox_lt = { trackingBox.x() - SMART_FRAME_WIDTH, trackingBox.y() - SMART_FRAME_WIDTH }; point_t bbox_rb = { trackingBox.x() + trackingBox.width() + SMART_FRAME_WIDTH, trackingBox.y() + trackingBox.width() + SMART_FRAME_WIDTH }; int offset = 0; // clip frame if (bbox_lt.x < 0) bbox_lt.x = 0; if (bbox_lt.y < 0) bbox_lt.y = 0; if (bbox_rb.x > video_width) bbox_rb.x = video_width; if (bbox_rb.y > video_height) bbox_rb.y = video_height; // calc top bar int box_wd = bbox_rb.x - bbox_lt.x; int box_ht = trackingBox.y() - bbox_lt.y; int pix_cnt = 0; if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = bbox_lt.y; j < trackingBox.y(); ++j) { offset = j * video_width; for (i = bbox_lt.x; i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc left bar box_wd = trackingBox.x() - bbox_lt.x; box_ht = trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y(); j < trackingBox.y() + box_ht; ++j) { offset = j * video_width; for (i = bbox_lt.x; i < trackingBox.x(); ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc right bar box_wd = bbox_rb.x - trackingBox.x() - trackingBox.width(); box_ht = trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y(); j < trackingBox.y() + box_ht; ++j) { offset = j * video_width; for (i = trackingBox.x() + trackingBox.width(); i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc bottom bar box_wd = bbox_rb.x - bbox_lt.x; box_ht = bbox_rb.y - trackingBox.y() - trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y() + trackingBox.width(); j < bbox_rb.y; ++j) { offset = j * video_width; for (i = bbox_lt.x; i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // find maximum double max_val = 0; for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; if (*pptr > max_val) max_val = *pptr; } psrc += video_width; } if (pix_cnt != 0) threshold /= (double)pix_cnt; // cut by 10% higher then average threshold if (max_val > threshold) threshold += (max_val - threshold) * SMART_CUT_FACTOR; //log_i("smart thr. = %f cnt = %d", threshold, pix_cnt); break; } // simple adaptive threshold case AUTO_THRESHOLD: { for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; threshold += *pptr; } psrc += video_width; } threshold /= square_square; break; } // no threshold subtracion default: { } } psrc = porigin; for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; pval = *pptr - threshold; pval = pval < 0 ? 0 : pval; resx += (double)i * pval; resy += (double)j * pval; mass += pval; } psrc += video_width; } if (mass == 0) mass = 1; resx /= mass; resy /= mass; ret = Vector(trackingBox.x(), trackingBox.y(), 0) + Vector(resx, resy, 0); return ret; } void cgmath::process_axes(void) { int cnt = 0; double t_delta = 0; qCDebug(KSTARS_EKOS_GUIDE) << "Processing Axes"; in_params.proportional_gain[0] = Options::rAProportionalGain(); in_params.proportional_gain[1] = Options::dECProportionalGain(); in_params.integral_gain[0] = Options::rAIntegralGain(); in_params.integral_gain[1] = Options::rAIntegralGain(); in_params.derivative_gain[0] = Options::rADerivativeGain(); in_params.derivative_gain[1] = Options::dECDerivativeGain(); in_params.enabled[0] = Options::rAGuideEnabled(); in_params.enabled[1] = Options::dECGuideEnabled(); in_params.min_pulse_length[0] = Options::rAMinimumPulse(); in_params.min_pulse_length[1] = Options::dECMinimumPulse(); in_params.max_pulse_length[0] = Options::rAMaximumPulse(); in_params.max_pulse_length[1] = Options::dECMaximumPulse(); // RA W/E enable // East RA+ enabled? in_params.enabled_axis1[0] = Options::eastRAGuideEnabled(); // West RA- enabled? in_params.enabled_axis2[0] = Options::westRAGuideEnabled(); // DEC N/S enable // North DEC+ enabled? in_params.enabled_axis1[1] = Options::northDECGuideEnabled(); // South DEC- enabled? in_params.enabled_axis2[1] = Options::southDECGuideEnabled(); // process axes... for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { // zero all out commands out_params.pulse_dir[k] = NO_DIR; if (accum_ticks[k] < in_params.accum_frame_cnt[k] - 1) continue; t_delta = 0; drift_integral[k] = 0; cnt = in_params.accum_frame_cnt[k]; for (int i = 0, idx = channel_ticks[k]; i < cnt; ++i) { t_delta += drift[k][idx]; qCDebug(KSTARS_EKOS_GUIDE) << "At #" << idx << "drift[" << k << "][" << idx << "] = " << drift[k][idx] << " , t_delta: " << t_delta; if (idx > 0) --idx; else idx = MAX_ACCUM_CNT - 1; } for (int i = 0; i < MAX_ACCUM_CNT; ++i) drift_integral[k] += drift[k][i]; out_params.delta[k] = t_delta / (double)cnt; drift_integral[k] /= (double)MAX_ACCUM_CNT; qCDebug(KSTARS_EKOS_GUIDE) << "delta [" << k << "]= " << out_params.delta[k]; qCDebug(KSTARS_EKOS_GUIDE) << "drift_integral[" << k << "]= " << drift_integral[k]; out_params.pulse_length[k] = fabs(out_params.delta[k] * in_params.proportional_gain[k] + drift_integral[k] * in_params.integral_gain[k]); out_params.pulse_length[k] = out_params.pulse_length[k] <= in_params.max_pulse_length[k] ? out_params.pulse_length[k] : in_params.max_pulse_length[k]; qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length [" << k << "]= " << out_params.pulse_length[k]; // calc direction // We do not send pulse if direction is disabled completely, or if direction in a specific axis (e.g. N or S) is disabled if (!in_params.enabled[k] || (out_params.delta[k] > 0 && !in_params.enabled_axis1[k]) || (out_params.delta[k] < 0 && !in_params.enabled_axis2[k])) { out_params.pulse_dir[k] = NO_DIR; out_params.pulse_length[k] = 0; continue; } if (out_params.pulse_length[k] >= in_params.min_pulse_length[k]) { if (k == GUIDE_RA) out_params.pulse_dir[k] = out_params.delta[k] > 0 ? RA_DEC_DIR : RA_INC_DIR; // GUIDE_RA. right dir - decreases GUIDE_RA else { out_params.pulse_dir[k] = out_params.delta[k] > 0 ? DEC_INC_DIR : DEC_DEC_DIR; // GUIDE_DEC. // Reverse DEC direction if we are looking eastward //if (ROT_Z.x[0][0] > 0 || (ROT_Z.x[0][0] ==0 && ROT_Z.x[0][1] > 0)) //out_params.pulse_dir[k] = (out_params.pulse_dir[k] == DEC_INC_DIR) ? DEC_DEC_DIR : DEC_INC_DIR; } } else out_params.pulse_dir[k] = NO_DIR; qCDebug(KSTARS_EKOS_GUIDE) << "Direction : " << get_direction_string(out_params.pulse_dir[k]); } //emit newAxisDelta(out_params.delta[0], out_params.delta[1]); if (Options::guideLogging()) { QTextStream out(&logFile); out << ticks << "," << logTime.elapsed() << "," << out_params.delta[0] << "," << out_params.pulse_length[0] << "," << get_direction_string(out_params.pulse_dir[0]) << "," << out_params.delta[1] << "," << out_params.pulse_length[1] << "," << get_direction_string(out_params.pulse_dir[1]) << endl; } } void cgmath::performProcessing(void) { Vector arc_star_pos, arc_reticle_pos; // do nothing if suspended if (suspended) return; // find guiding star location in scr_star_pos = star_pos = findLocalStarPosition(); if (star_pos.x == -1 || std::isnan(star_pos.x)) { lost_star = true; return; } else lost_star = false; // move square overlay //TODO FIXME //moveSquare( round(star_pos.x) - (double)square_size/(2*subBinX), round(star_pos.y) - (double)square_size/(2*subBinY) ); QVector3D starCenter(star_pos.x, star_pos.y, 0); emit newStarPosition(starCenter, true); if (preview_mode) return; qCDebug(KSTARS_EKOS_GUIDE) << "################## BEGIN PROCESSING ##################"; // translate star coords into sky coord. system // convert from pixels into arcsecs arc_star_pos = point2arcsec(star_pos); arc_reticle_pos = point2arcsec(reticle_pos); qCDebug(KSTARS_EKOS_GUIDE) << "Star X : " << star_pos.x << " Y : " << star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Reticle X : " << reticle_pos.x << " Y :" << reticle_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Star RA: " << arc_star_pos.x << " DEC: " << arc_star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Reticle RA: " << arc_reticle_pos.x << " DEC: " << arc_reticle_pos.y; // translate into sky coords. star_pos = arc_star_pos - arc_reticle_pos; star_pos.y = -star_pos.y; // invert y-axis as y picture axis is inverted qCDebug(KSTARS_EKOS_GUIDE) << "-------> BEFORE ROTATION Diff RA: " << star_pos.x << " DEC: " << star_pos.y; star_pos = star_pos * ROT_Z; // both coords are ready for math processing //put coord to drift list drift[GUIDE_RA][channel_ticks[GUIDE_RA]] = star_pos.x; drift[GUIDE_DEC][channel_ticks[GUIDE_DEC]] = star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "-------> AFTER ROTATION Diff RA: " << star_pos.x << " DEC: " << star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "RA channel ticks: " << channel_ticks[GUIDE_RA] << " DEC channel ticks: " << channel_ticks[GUIDE_DEC]; // make decision by axes process_axes(); // process statistics calc_square_err(); // finally process tickers do_ticks(); qCDebug(KSTARS_EKOS_GUIDE) << "################## FINISH PROCESSING ##################"; } void cgmath::calc_square_err(void) { if (!do_statistics) return; // through MAX_ACCUM_CNT values if (ticks == 0) return; for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { double sqr_avg = 0; for (int i = 0; i < MAX_ACCUM_CNT; ++i) sqr_avg += drift[k][i] * drift[k][i]; out_params.sigma[k] = sqrt(sqr_avg / (double)MAX_ACCUM_CNT); } } void cgmath::setRapidGuide(bool enable) { useRapidGuide = enable; } double cgmath::getDitherRate(int axis) { if (axis < 0 || axis > 1) return -1; return ditherRate[axis]; } void cgmath::setRapidStarData(double dx, double dy) { rapidDX = dx; rapidDY = dy; } const char *cgmath::get_direction_string(GuideDirection dir) { switch (dir) { case RA_DEC_DIR: return "Decrease RA"; break; case RA_INC_DIR: return "Increase RA"; break; case DEC_DEC_DIR: return "Decrease DEC"; break; case DEC_INC_DIR: return "Increase DEC"; break; default: break; } return "NO DIR"; } bool cgmath::isImageGuideEnabled() const { return imageGuideEnabled; } void cgmath::setImageGuideEnabled(bool value) { imageGuideEnabled = value; } static void psf_conv(float *dst, const float *src, int width, int height) { //dst.Init(src.Size); // A B1 B2 C1 C2 C3 D1 D2 D3 const double PSF[] = { 0.906, 0.584, 0.365, .117, .049, -0.05, -.064, -.074, -.094 }; //memset(dst.px, 0, src.NPixels * sizeof(float)); /* PSF Grid is: D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D2 D1 D2 D3 D3 D3 D3 D3 C3 C2 C1 C2 C3 D3 D3 D3 D2 C2 B2 B1 B2 C2 D2 D3 D3 D1 C1 B1 A B1 C1 D1 D3 D3 D2 C2 B2 B1 B2 C2 D2 D3 D3 D3 C3 C2 C1 C2 C3 D3 D3 D3 D3 D3 D2 D1 D2 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 1@A 4@B1, B2, C1, C3, D1 8@C2, D2 44 * D3 */ int psf_size = 4; for (int y = psf_size; y < height - psf_size; y++) { for (int x = psf_size; x < width - psf_size; x++) { float A, B1, B2, C1, C2, C3, D1, D2, D3; #define PX(dx, dy) *(src + width * (y + (dy)) + x + (dx)) A = PX(+0, +0); B1 = PX(+0, -1) + PX(+0, +1) + PX(+1, +0) + PX(-1, +0); B2 = PX(-1, -1) + PX(+1, -1) + PX(-1, +1) + PX(+1, +1); C1 = PX(+0, -2) + PX(-2, +0) + PX(+2, +0) + PX(+0, +2); C2 = PX(-1, -2) + PX(+1, -2) + PX(-2, -1) + PX(+2, -1) + PX(-2, +1) + PX(+2, +1) + PX(-1, +2) + PX(+1, +2); C3 = PX(-2, -2) + PX(+2, -2) + PX(-2, +2) + PX(+2, +2); D1 = PX(+0, -3) + PX(-3, +0) + PX(+3, +0) + PX(+0, +3); D2 = PX(-1, -3) + PX(+1, -3) + PX(-3, -1) + PX(+3, -1) + PX(-3, +1) + PX(+3, +1) + PX(-1, +3) + PX(+1, +3); D3 = PX(-4, -2) + PX(-3, -2) + PX(+3, -2) + PX(+4, -2) + PX(-4, -1) + PX(+4, -1) + PX(-4, +0) + PX(+4, +0) + PX(-4, +1) + PX(+4, +1) + PX(-4, +2) + PX(-3, +2) + PX(+3, +2) + PX(+4, +2); #undef PX int i; const float *uptr; uptr = src + width * (y - 4) + (x - 4); for (i = 0; i < 9; i++) D3 += *uptr++; uptr = src + width * (y - 3) + (x - 4); for (i = 0; i < 3; i++) D3 += *uptr++; uptr += 3; for (i = 0; i < 3; i++) D3 += *uptr++; uptr = src + width * (y + 3) + (x - 4); for (i = 0; i < 3; i++) D3 += *uptr++; uptr += 3; for (i = 0; i < 3; i++) D3 += *uptr++; uptr = src + width * (y + 4) + (x - 4); for (i = 0; i < 9; i++) D3 += *uptr++; double mean = (A + B1 + B2 + C1 + C2 + C3 + D1 + D2 + D3) / 81.0; double PSF_fit = PSF[0] * (A - mean) + PSF[1] * (B1 - 4.0 * mean) + PSF[2] * (B2 - 4.0 * mean) + PSF[3] * (C1 - 4.0 * mean) + PSF[4] * (C2 - 8.0 * mean) + PSF[5] * (C3 - 4.0 * mean) + PSF[6] * (D1 - 4.0 * mean) + PSF[7] * (D2 - 8.0 * mean) + PSF[8] * (D3 - 44.0 * mean); dst[width * y + x] = (float) PSF_fit; } } } static void GetStats(double *mean, double *stdev, int width, const float *img, const QRect &win) { // Determine the mean and standard deviation double sum = 0.0; double a = 0.0; double q = 0.0; double k = 1.0; double km1 = 0.0; const float *p0 = img + win.top() * width + win.left(); for (int y = 0; y < win.height(); y++) { const float *end = p0 + win.height(); for (const float *p = p0; p < end; p++) { double const x = (double) * p; sum += x; double const a0 = a; a += (x - a) / k; q += (x - a0) * (x - a); km1 = k; k += 1.0; } p0 += width; } *mean = sum / km1; *stdev = sqrt(q / km1); } static void RemoveItems(std::set &stars, const std::set &to_erase) { int n = 0; for (std::set::iterator it = stars.begin(); it != stars.end(); n++) { if (to_erase.find(n) != to_erase.end()) { std::set::iterator next = it; ++next; stars.erase(it); it = next; } else ++it; } } // Based on PHD2 algorithm QList cgmath::PSFAutoFind(int extraEdgeAllowance) { //Debug.Write(wxString::Format("Star::AutoFind called with edgeAllowance = %d searchRegion = %d\n", extraEdgeAllowance, searchRegion)); // run a 3x3 median first to eliminate hot pixels //usImage smoothed; //smoothed.CopyFrom(image); //Median3(smoothed); FITSData *smoothed = new FITSData(guideView->getImageData()); smoothed->applyFilter(FITS_MEDIAN); int searchRegion = guideView->getTrackingBox().width(); int subW = smoothed->width(); int subH = smoothed->height(); int size = subW * subH; // convert to floating point float *conv = createFloatImage(smoothed); // run the PSF convolution { float *tmp = new float[size]; memset(tmp, 0, size * sizeof(float)); psf_conv(tmp, conv, subW, subH); delete [] conv; // Swap conv = tmp; } enum { CONV_RADIUS = 4 }; int dw = subW; // width of the downsampled image int dh = subH; // height of the downsampled image QRect convRect(CONV_RADIUS, CONV_RADIUS, dw - 2 * CONV_RADIUS, dh - 2 * CONV_RADIUS); // region containing valid data enum { TOP_N = 100 }; // keep track of the brightest stars std::set stars; // sorted by ascending intensity double global_mean, global_stdev; GetStats(&global_mean, &global_stdev, subW, conv, convRect); //Debug.Write(wxString::Format("AutoFind: global mean = %.1f, stdev %.1f\n", global_mean, global_stdev)); const double threshold = 0.1; //Debug.Write(wxString::Format("AutoFind: using threshold = %.1f\n", threshold)); // find each local maximum int srch = 4; for (int y = convRect.top() + srch; y <= convRect.bottom() - srch; y++) { for (int x = convRect.left() + srch; x <= convRect.right() - srch; x++) { float val = conv[dw * y + x]; bool ismax = false; if (val > 0.0) { ismax = true; for (int j = -srch; j <= srch; j++) { for (int i = -srch; i <= srch; i++) { if (i == 0 && j == 0) continue; if (conv[dw * (y + j) + (x + i)] > val) { ismax = false; break; } } } } if (!ismax) continue; // compare local maximum to mean value of surrounding pixels const int local = 7; double local_mean, local_stdev; QRect localRect(x - local, y - local, 2 * local + 1, 2 * local + 1); localRect = localRect.intersected(convRect); GetStats(&local_mean, &local_stdev, subW, conv, localRect); // this is our measure of star intensity double h = (val - local_mean) / global_stdev; if (h < threshold) { // Debug.Write(wxString::Format("AG: local max REJECT [%d, %d] PSF %.1f SNR %.1f\n", imgx, imgy, val, SNR)); continue; } // coordinates on the original image int downsample = 1; int imgx = x * downsample + downsample / 2; int imgy = y * downsample + downsample / 2; stars.insert(Peak(imgx, imgy, h)); if (stars.size() > TOP_N) stars.erase(stars.begin()); } } //for (std::set::const_reverse_iterator it = stars.rbegin(); it != stars.rend(); ++it) //qCDebug(KSTARS_EKOS_GUIDE) << "AutoFind: local max [" << it->x << "," << it->y << "]" << it->val; // merge stars that are very close into a single star { const int minlimitsq = 5 * 5; repeat: for (std::set::const_iterator a = stars.begin(); a != stars.end(); ++a) { std::set::const_iterator b = a; ++b; for (; b != stars.end(); ++b) { int dx = a->x - b->x; int dy = a->y - b->y; int d2 = dx * dx + dy * dy; if (d2 < minlimitsq) { // very close, treat as single star //Debug.Write(wxString::Format("AutoFind: merge [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); // erase the dimmer one stars.erase(a); goto repeat; } } } } // exclude stars that would fit within a single searchRegion box { // build a list of stars to be excluded std::set to_erase; const int extra = 5; // extra safety margin const int fullw = searchRegion + extra; for (std::set::const_iterator a = stars.begin(); a != stars.end(); ++a) { std::set::const_iterator b = a; ++b; for (; b != stars.end(); ++b) { int dx = abs(a->x - b->x); int dy = abs(a->y - b->y); if (dx <= fullw && dy <= fullw) { // stars closer than search region, exclude them both // but do not let a very dim star eliminate a very bright star if (b->val / a->val >= 5.0) { //Debug.Write(wxString::Format("AutoFind: close dim-bright [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); } else { //Debug.Write(wxString::Format("AutoFind: too close [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); to_erase.insert(std::distance(stars.begin(), a)); to_erase.insert(std::distance(stars.begin(), b)); } } } } RemoveItems(stars, to_erase); } // exclude stars too close to the edge { enum { MIN_EDGE_DIST = 40 }; int edgeDist = MIN_EDGE_DIST;//pConfig->Profile.GetInt("/StarAutoFind/MinEdgeDist", MIN_EDGE_DIST); if (edgeDist < searchRegion) edgeDist = searchRegion; edgeDist += extraEdgeAllowance; std::set::iterator it = stars.begin(); while (it != stars.end()) { std::set::iterator next = it; ++next; if (it->x <= edgeDist || it->x >= subW - edgeDist || it->y <= edgeDist || it->y >= subH - edgeDist) { //Debug.Write(wxString::Format("AutoFind: too close to edge [%d, %d] %.1f\n", it->x, it->y, it->val)); stars.erase(it); } it = next; } } QList centers; for (std::set::reverse_iterator it = stars.rbegin(); it != stars.rend(); ++it) { Edge *center = new Edge; center->x = it->x; center->y = it->y; center->val = it->val; centers.append(center); } delete [] conv; delete (smoothed); return centers; } //--------------------------------------------------------------------------------------- cproc_in_params::cproc_in_params() { reset(); } void cproc_in_params::reset(void) { threshold_alg_idx = CENTROID_THRESHOLD; average = true; for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { enabled[k] = true; accum_frame_cnt[k] = 1; integral_gain[k] = 0; derivative_gain[k] = 0; max_pulse_length[k] = 5000; min_pulse_length[k] = 100; } } cproc_out_params::cproc_out_params() { reset(); } void cproc_out_params::reset(void) { for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { delta[k] = 0; pulse_dir[k] = NO_DIR; pulse_length[k] = 0; sigma[k] = 0; } } diff --git a/kstars/fitsviewer/fitscentroiddetector.cpp b/kstars/fitsviewer/fitscentroiddetector.cpp new file mode 100644 index 000000000..54b7c69bc --- /dev/null +++ b/kstars/fitsviewer/fitscentroiddetector.cpp @@ -0,0 +1,468 @@ +/*************************************************************************** + fitscentroiddetector.cpp - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#include + +#include "fitscentroiddetector.h" +#include "fits_debug.h" + +FITSStarDetector& FITSCentroidDetector::configure(const QString &setting, const QVariant &value) +{ + if (!setting.compare("initStdDev", Qt::CaseInsensitive)) + { + bool ok = false; + double const result = value.toDouble(&ok); + if (ok) + m_initStdDev = result; + } + if (!setting.compare("minEdgeWidth", Qt::CaseInsensitive)) + { + bool ok = false; + int const result = value.toInt(&ok); + if (ok) + m_initStdDev = result; + } + + return *this; +} + +bool FITSCentroidDetector::checkCollision(Edge * s1, Edge * s2) const +{ + int dis; //distance + + int diff_x = s1->x - s2->x; + int diff_y = s1->y - s2->y; + + dis = std::abs(sqrt(diff_x * diff_x + diff_y * diff_y)); + dis -= s1->width / 2; + dis -= s2->width / 2; + + if (dis <= 0) //collision + return true; + + //no collision + return false; +} + +/*** Find center of stars and calculate Half Flux Radius */ +int FITSCentroidDetector::findSources(QList &starCenters, const QRect &boundary) +{ + switch (parent()->property("dataType").toInt()) + { + case TBYTE: + return findSources(starCenters, boundary); + + case TSHORT: + return findSources(starCenters, boundary); + + case TUSHORT: + return findSources(starCenters, boundary); + + case TLONG: + return findSources(starCenters, boundary); + + case TULONG: + return findSources(starCenters, boundary); + + case TFLOAT: + return findSources(starCenters, boundary); + + case TLONGLONG: + return findSources(starCenters, boundary); + + case TDOUBLE: + return findSources(starCenters, boundary); + + default: + return -1; + } +} + +template +int FITSCentroidDetector::findSources(QList &starCenters, const QRect &boundary) +{ + FITSData const * const image_data = reinterpret_cast(parent()); + + if (image_data == nullptr) + return 0; + + FITSData::Statistic const &stats = image_data->getStatistics(); + FITSMode const m_Mode = static_cast (parent()->property("mode").toInt()); + + double threshold = 0, sum = 0, avg = 0, min = 0; + int starDiameter = 0; + int pixVal = 0; + int minimumEdgeCount = MINIMUM_EDGE_LIMIT; + + auto * buffer = reinterpret_cast(image_data->getImageBuffer()); + + double JMIndex = 100; + +#if 0//ndef KSTARS_LITE + if (histogram) + { + if (!histogram->isConstructed()) + histogram->constructHistogram(); + JMIndex = histogram->getJMIndex(); + } +#endif + + float dispersion_ratio = 1.5; + + QList edges; + + if (JMIndex < DIFFUSE_THRESHOLD) + { + m_minEdgeWidth = JMIndex * 35 + 1; + minimumEdgeCount = m_minEdgeWidth - 1; + } + else + { + m_minEdgeWidth = 6; + minimumEdgeCount = 4; + } + + while (m_initStdDev >= 1) + { + m_minEdgeWidth--; + minimumEdgeCount--; + + m_minEdgeWidth = qMax(3, m_minEdgeWidth); + minimumEdgeCount = qMax(3, minimumEdgeCount); + + if (JMIndex < DIFFUSE_THRESHOLD) + { + // Taking the average out seems to have better result for noisy images + threshold = stats.max[0] - stats.mean[0] * ((MINIMUM_STDVAR - m_initStdDev) * 0.5 + 1); + + min = stats.min[0]; + if (threshold - min < 0) + { + threshold = stats.mean[0] * ((MINIMUM_STDVAR - m_initStdDev) * 0.5 + 1); + min = 0; + } + + dispersion_ratio = 1.4 - (MINIMUM_STDVAR - m_initStdDev) * 0.08; + } + else + { + threshold = stats.mean[0] + stats.stddev[0] * m_initStdDev * (0.3 - (MINIMUM_STDVAR - m_initStdDev) * 0.05); + min = stats.min[0]; + // Ratio between centeroid center and edge + dispersion_ratio = 1.8 - (MINIMUM_STDVAR - m_initStdDev) * 0.2; + } + + qCDebug(KSTARS_FITS) << "SNR: " << stats.SNR; + qCDebug(KSTARS_FITS) << "The threshold level is " << threshold << "(actual " << threshold - min + << ") minimum edge width" << m_minEdgeWidth << " minimum edge limit " << minimumEdgeCount; + + threshold -= min; + + int subX, subY, subW, subH; + + if (boundary.isNull()) + { + if (m_Mode == FITS_GUIDE || m_Mode == FITS_FOCUS) + { + // Only consider the central 70% + subX = round(stats.width * 0.15); + subY = round(stats.height * 0.15); + subW = stats.width - subX; + subH = stats.height - subY; + } + else + { + // Consider the complete area 100% + subX = 0; + subY = 0; + subW = stats.width; + subH = stats.height; + } + } + else + { + subX = boundary.x(); + subY = boundary.y(); + subW = subX + boundary.width(); + subH = subY + boundary.height(); + } + + // Detect "edges" that are above threshold + for (int i = subY; i < subH; i++) + { + starDiameter = 0; + + for (int j = subX; j < subW; j++) + { + pixVal = buffer[j + (i * stats.width)] - min; + + // If pixel value > threshold, let's get its weighted average + if (pixVal >= threshold) + { + avg += j * pixVal; + sum += pixVal; + starDiameter++; + } + // Value < threshold but avg exists + else if (sum > 0) + { + // We found a potential centroid edge + if (starDiameter >= m_minEdgeWidth) + { + float center = avg / sum + 0.5; + if (center > 0) + { + int i_center = std::floor(center); + + // Check if center is 10% or more brighter than edge, if not skip + if (((buffer[i_center + (i * stats.width)] - min) / + (buffer[i_center + (i * stats.width) - starDiameter / 2] - min) >= + dispersion_ratio) && + ((buffer[i_center + (i * stats.width)] - min) / + (buffer[i_center + (i * stats.width) + starDiameter / 2] - min) >= + dispersion_ratio)) + { + qCDebug(KSTARS_FITS) + << "Edge center is " << buffer[i_center + (i * stats.width)] - min + << " Edge is " << buffer[i_center + (i * stats.width) - starDiameter / 2] - min + << " and ratio is " + << ((buffer[i_center + (i * stats.width)] - min) / + (buffer[i_center + (i * stats.width) - starDiameter / 2] - min)) + << " located at X: " << center << " Y: " << i + 0.5; + + auto * newEdge = new Edge(); + + newEdge->x = center; + newEdge->y = i + 0.5; + newEdge->scanned = 0; + newEdge->val = buffer[i_center + (i * stats.width)] - min; + newEdge->width = starDiameter; + newEdge->HFR = 0; + newEdge->sum = sum; + + edges.append(newEdge); + } + } + } + + // Reset + avg = sum = starDiameter = 0; + } + } + } + + qCDebug(KSTARS_FITS) << "Total number of edges found is: " << edges.count(); + + // In case of hot pixels + if (edges.count() == 1 && m_initStdDev > 1) + { + m_initStdDev--; + continue; + } + + if (edges.count() >= MAX_EDGE_LIMIT) + { + qCWarning(KSTARS_FITS) << "Too many edges, aborting... " << edges.count(); + qDeleteAll(edges); + return -1; + } + + if (edges.count() >= minimumEdgeCount) + break; + + qDeleteAll(edges); + edges.clear(); + m_initStdDev--; + } + + int cen_count = 0; + int cen_x = 0; + int cen_y = 0; + int cen_v = 0; + int cen_w = 0; + int width_sum = 0; + + // Let's sort edges, starting with widest + auto const greaterThan = [](Edge const *a, Edge const *b) { return a->sum > b->sum; }; + std::sort(edges.begin(), edges.end(), greaterThan); + + // Now, let's scan the edges and find the maximum centroid vertically + for (int i = 0; i < edges.count(); i++) + { + qCDebug(KSTARS_FITS) << "# " << i << " Edge at (" << edges[i]->x << "," << edges[i]->y << ") With a value of " + << edges[i]->val << " and width of " << edges[i]->width << " pixels. with sum " << edges[i]->sum; + + // If edge scanned already, skip + if (edges[i]->scanned == 1) + { + qCDebug(KSTARS_FITS) << "Skipping check for center " << i << " because it was already counted"; + continue; + } + + qCDebug(KSTARS_FITS) << "Investigating edge # " << i << " now ..."; + + // Get X, Y, and Val of edge + cen_x = edges[i]->x; + cen_y = edges[i]->y; + cen_v = edges[i]->sum; + cen_w = edges[i]->width; + + float avg_x = 0; + float avg_y = 0; + + sum = 0; + cen_count = 0; + + // Now let's compare to other edges until we hit a maxima + for (int j = 0; j < edges.count(); j++) + { + if (edges[j]->scanned) + continue; + + if (checkCollision(edges[j], edges[i])) + { + if (edges[j]->sum >= cen_v) + { + cen_v = edges[j]->sum; + cen_w = edges[j]->width; + } + + edges[j]->scanned = 1; + cen_count++; + + avg_x += edges[j]->x * edges[j]->val; + avg_y += edges[j]->y * edges[j]->val; + sum += edges[j]->val; + + continue; + } + } + + int cen_limit = (MINIMUM_ROWS_PER_CENTER - (MINIMUM_STDVAR - m_initStdDev)); + + if (edges.count() < LOW_EDGE_CUTOFF_1) + { + if (edges.count() < LOW_EDGE_CUTOFF_2) + cen_limit = 1; + else + cen_limit = 2; + } + + qCDebug(KSTARS_FITS) << "center_count: " << cen_count << " and initstdDev= " << m_initStdDev << " and limit is " + << cen_limit; + + if (cen_limit < 1) + continue; + + // If centroid count is within acceptable range + //if (cen_limit >= 2 && cen_count >= cen_limit) + if (cen_count >= cen_limit) + { + // We detected a centroid, let's init it + auto * rCenter = new Edge(); + + rCenter->x = avg_x / sum; + rCenter->y = avg_y / sum; + width_sum += rCenter->width; + rCenter->width = cen_w; + + qCDebug(KSTARS_FITS) << "Found a real center with number with (" << rCenter->x << "," << rCenter->y << ")"; + + // Calculate Total Flux From Center, Half Flux, Full Summation + double TF = 0; + double HF = 0; + double FSum = 0; + + cen_x = (int)std::floor(rCenter->x); + cen_y = (int)std::floor(rCenter->y); + + if (cen_x < 0 || cen_x > stats.width || cen_y < 0 || cen_y > stats.height) + { + delete rCenter; + continue; + } + + // Complete sum along the radius + //for (int k=0; k < rCenter->width; k++) + for (int k = rCenter->width / 2; k >= -(rCenter->width / 2); k--) + { + FSum += buffer[cen_x - k + (cen_y * stats.width)] - min; + //qDebug() << image_buffer[cen_x-k+(cen_y*stats.width)] - min; + } + + // Half flux + HF = FSum / 2.0; + + // Total flux starting from center + TF = buffer[cen_y * stats.width + cen_x] - min; + + int pixelCounter = 1; + + // Integrate flux along radius axis until we reach half flux + for (int k = 1; k < rCenter->width / 2; k++) + { + if (TF >= HF) + { + qCDebug(KSTARS_FITS) << "Stopping at TF " << TF << " after #" << k << " pixels."; + break; + } + + TF += buffer[cen_y * stats.width + cen_x + k] - min; + TF += buffer[cen_y * stats.width + cen_x - k] - min; + + pixelCounter++; + } + + // Calculate weighted Half Flux Radius + rCenter->HFR = pixelCounter * (HF / TF); + // Store full flux + rCenter->val = FSum; + + qCDebug(KSTARS_FITS) << "HFR for this center is " << rCenter->HFR << " pixels and the total flux is " << FSum; + + starCenters.append(rCenter); + } + } + + if (starCenters.count() > 1 && m_Mode != FITS_FOCUS) + { + float width_avg = (float)width_sum / starCenters.count(); + float lsum = 0, sdev = 0; + + for (auto ¢er : starCenters) + lsum += (center->width - width_avg) * (center->width - width_avg); + + sdev = (std::sqrt(lsum / (starCenters.count() - 1))) * 4; + + // Reject stars > 4 * stddev + foreach (Edge * center, starCenters) + if (center->width > sdev) + starCenters.removeOne(center); + + //foreach(Edge *center, starCenters) + //qDebug() << center->x << "," << center->y << "," << center->width << "," << center->val << endl; + } + + // Release memory + qDeleteAll(edges); + + return starCenters.count(); +} + + diff --git a/kstars/fitsviewer/fitscentroiddetector.h b/kstars/fitsviewer/fitscentroiddetector.h new file mode 100644 index 000000000..7af17c1d7 --- /dev/null +++ b/kstars/fitsviewer/fitscentroiddetector.h @@ -0,0 +1,78 @@ +/*************************************************************************** + fitscentroiddetector.h - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#ifndef FITSCENTROIDDETECTOR_H +#define FITSCENTROIDDETECTOR_H + +#include +#include "fitsstardetector.h" + +class FITSCentroidDetector: public FITSStarDetector +{ + Q_OBJECT + +public: + explicit FITSCentroidDetector(FITSData *parent): FITSStarDetector(parent) {}; + +public: + /** @brief Find sources in the parent FITS data file. + * @see FITSStarDetector::findSources(). + */ + int findSources(QList &starCenters, QRect const &boundary = QRect()) override; + + /** @brief Configure the detection method. + * @see FITSStarDetector::configure(). + * @note Parameter "initStdDev" defaults to MINIMUM_STDVAR. + * @note Parameter "minEdgeWidth" defaults to MINIMUM_PIXEL_RANGE. + * @todo Provide all constants of this class as parameters, and explain their use. + */ + FITSStarDetector & configure(const QString &setting, const QVariant &value) override; + +public: + /** @group Detection internals + * @{ */ + static constexpr int MINIMUM_STDVAR { 5 }; + static constexpr int MINIMUM_PIXEL_RANGE { 5 }; + static constexpr int MINIMUM_EDGE_LIMIT { 2 }; + static constexpr int MAX_EDGE_LIMIT { 10000 }; + static constexpr double DIFFUSE_THRESHOLD { 0.15 }; + static constexpr int MINIMUM_ROWS_PER_CENTER { 3 }; + static constexpr int LOW_EDGE_CUTOFF_1 { 50 }; + static constexpr int LOW_EDGE_CUTOFF_2 { 10 }; + /** @} */ + +protected: + /** @internal Find sources in the parent FITS data file, dependent of the pixel depth. + * @see FITSGradientDetector::findSources. + */ + template + int findSources(QList &starCenters, const QRect &boundary); + + /** @internal Check whether two sources overlap. + * @param s1, s2 are the two sources to check collision on. + * @return true if the sources collide, else false. + */ + bool checkCollision(Edge * s1, Edge * s2) const; + +protected: + int m_initStdDev { MINIMUM_STDVAR }; + int m_minEdgeWidth { MINIMUM_PIXEL_RANGE }; +}; + +#endif // FITSCENTROIDDETECTOR_H diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp index b58bcbb6a..31ab36781 100644 --- a/kstars/fitsviewer/fitsdata.cpp +++ b/kstars/fitsviewer/fitsdata.cpp @@ -1,4393 +1,3196 @@ /*************************************************************************** FITSImage.cpp - FITS Image ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #include "fitsdata.h" +#include "fitsthresholddetector.h" +#include "fitsgradientdetector.h" +#include "fitscentroiddetector.h" +#include "fitssepdetector.h" -#include "sep/sep.h" #include "fpack.h" #include "kstarsdata.h" #include "ksutils.h" #include "kspaths.h" #include "Options.h" #include "skymapcomposite.h" #include "auxiliary/ksnotification.h" #include #include #include #include #include #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) #include #include #endif #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #include #include #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 -const int MINIMUM_ROWS_PER_CENTER = 3; const QString FITSData::m_TemporaryPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); -#define DIFFUSE_THRESHOLD 0.15 -#define MAX_EDGE_LIMIT 10000 -#define LOW_EDGE_CUTOFF_1 50 -#define LOW_EDGE_CUTOFF_2 10 -#define MINIMUM_EDGE_LIMIT 2 - -bool greaterThan(Edge * s1, Edge * s2) -{ - //return s1->width > s2->width; - return s1->sum > s2->sum; -} FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; } FITSData::FITSData(const FITSData * other) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; this->m_Mode = other->m_Mode; this->m_DataType = other->m_DataType; this->m_Channels = other->m_Channels; memcpy(&stats, &(other->stats), sizeof(stats)); m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; memcpy(m_ImageBuffer, other->m_ImageBuffer, stats.samples_per_channel * m_Channels * stats.bytesPerPixel); } FITSData::~FITSData() { int status = 0; clearImageBuffers(); #ifdef HAVE_WCSLIB if (m_wcs != nullptr) wcsvfree(&m_nwcs, &m_wcs); #endif if (starCenters.count() > 0) qDeleteAll(starCenters); delete[] wcs_coord; if (objList.count() > 0) qDeleteAll(objList); if (fptr != nullptr) { fits_flush_file(fptr, &status); fits_close_file(fptr, &status); fptr = nullptr; if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); } qDeleteAll(records); } void FITSData::loadCommon(const QString &inFilename) { int status = 0; qDeleteAll(starCenters); starCenters.clear(); if (fptr != nullptr) { fits_flush_file(fptr, &status); fits_close_file(fptr, &status); fptr = nullptr; // If current file is temporary AND // Auto Remove Temporary File is Set AND // New filename is different from existing filename // THen remove it. We have to check for name since we cannot delete // the same filename and try to open it below! if (m_isTemporary && autoRemoveTemporaryFITS && inFilename != m_Filename) QFile::remove(m_Filename); } m_Filename = inFilename; } bool FITSData::loadFITSFromMemory(const QString &inFilename, void *fits_buffer, size_t fits_buffer_size, bool silent) { loadCommon(inFilename); qCDebug(KSTARS_FITS) << "Reading FITS file buffer (" << KFormat().formatByteSize(fits_buffer_size) << ")"; return privateLoad(fits_buffer, fits_buffer_size, silent); } QFuture FITSData::loadFITS(const QString &inFilename, bool silent) { loadCommon(inFilename); qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; QFuture result = QtConcurrent::run( this, &FITSData::privateLoad, nullptr, 0, silent); return result; } namespace { // Common code for reporting fits read errors. Always returns false. bool fitsOpenError(int status, const QString &message, bool silent) { char error_status[512]; fits_report_error(stderr, status); fits_get_errstatus(status, error_status); QString errMessage = message; errMessage.append(i18n(" Error: %1", QString::fromUtf8(error_status))); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } } bool FITSData::privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent) { int status = 0, anynull = 0; long naxes[3]; QString errMessage; m_isTemporary = m_Filename.startsWith(m_TemporaryPath); if (fits_buffer == nullptr && m_Filename.endsWith(".fz")) { // Store so we don't lose. m_compressedFilename = m_Filename; QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove( QRegularExpression("[-{}]"))); fpstate fpvar; fp_init (&fpvar); if (fp_unpack(m_Filename.toLatin1().data(), uncompressedFile.toLatin1().data(), fpvar) < 0) { errMessage = i18n("Failed to unpack compressed fits"); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } // Remove compressed .fz if it was temporary if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); m_Filename = uncompressedFile; m_isTemporary = true; m_isCompressed = true; } if (fits_buffer == nullptr) { // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) return fitsOpenError(status, i18n("Error opening fits file %1", m_Filename), silent); else stats.size = QFile(m_Filename).size(); } else { // Read the FITS file from a memory buffer. void *temp_buffer = fits_buffer; size_t temp_size = fits_buffer_size; if (fits_open_memfile(&fptr, m_Filename.toLatin1().data(), READONLY, &temp_buffer, &temp_size, 0, nullptr, &status)) return fitsOpenError(status, i18n("Error reading fits buffer."), silent); else stats.size = fits_buffer_size; } if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) return fitsOpenError(status, i18n("Could not locate image HDU."), silent); if (fits_get_img_param(fptr, 3, &(stats.bitpix), &(stats.ndim), naxes, &status)) return fitsOpenError(status, i18n("FITS file open error (fits_get_img_param)."), silent); if (stats.ndim < 2) { errMessage = i18n("1D FITS images are not supported in KStars."); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } switch (stats.bitpix) { case BYTE_IMG: m_DataType = TBYTE; stats.bytesPerPixel = sizeof(uint8_t); break; case SHORT_IMG: // Read SHORT image as USHORT m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(int16_t); break; case USHORT_IMG: m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(uint16_t); break; case LONG_IMG: // Read LONG image as ULONG m_DataType = TULONG; stats.bytesPerPixel = sizeof(int32_t); break; case ULONG_IMG: m_DataType = TULONG; stats.bytesPerPixel = sizeof(uint32_t); break; case FLOAT_IMG: m_DataType = TFLOAT; stats.bytesPerPixel = sizeof(float); break; case LONGLONG_IMG: m_DataType = TLONGLONG; stats.bytesPerPixel = sizeof(int64_t); break; case DOUBLE_IMG: m_DataType = TDOUBLE; stats.bytesPerPixel = sizeof(double); break; default: errMessage = i18n("Bit depth %1 is not supported.", stats.bitpix); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 3) naxes[2] = 1; if (naxes[0] == 0 || naxes[1] == 0) { errMessage = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.width = naxes[0]; stats.height = naxes[1]; stats.samples_per_channel = stats.width * stats.height; clearImageBuffers(); m_Channels = naxes[2]; // Channels always set to #1 if we are not required to process 3D Cubes // Or if mode is not FITS_NORMAL (guide, focus..etc) if (m_Mode != FITS_NORMAL || !Options::auto3DCube()) m_Channels = 1; m_ImageBufferSize = stats.samples_per_channel * m_Channels * stats.bytesPerPixel; m_ImageBuffer = new uint8_t[m_ImageBufferSize]; if (m_ImageBuffer == nullptr) { qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: " << m_ImageBufferSize << " bytes."; clearImageBuffers(); return false; } rotCounter = 0; flipHCounter = 0; flipVCounter = 0; long nelements = stats.samples_per_channel * m_Channels; if (fits_read_img(fptr, m_DataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status)) return fitsOpenError(status, i18n("Error reading image."), silent); parseHeader(); if (Options::autoDebayer() && checkDebayer()) { //m_BayerBuffer = m_ImageBuffer; if (debayer()) calculateStats(); } else calculateStats(); WCSLoaded = false; if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN) checkForWCS(); starsSearched = false; return true; } int FITSData::saveFITS(const QString &newFilename) { if (newFilename == m_Filename) return 0; if (m_isCompressed) { KSNotification::error(i18n("Saving compressed files is not supported.")); return -1; } int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; if (HasDebayer) { fits_flush_file(fptr, &status); /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } // Skip "!" in the beginning of the new file name QString finalFileName(newFilename); finalFileName.remove('!'); // Remove first otherwise copy will fail below if file exists QFile::remove(finalFileName); if (!QFile::copy(m_Filename, finalFileName)) { qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName; fptr = nullptr; return -1; } if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = finalFileName; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status); fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status); return 0; } nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } // if (fits_movabs_hdu(fptr, 1, &exttype, &status)) // { // fits_report_error(stderr, status); // return status; // } if (fits_copy_header(fptr, new_fptr, &status)) { fits_report_error(stderr, status); return status; } fits_flush_file(fptr, &status); /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } status = 0; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_report_error(stderr, status); return status; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_report_error(stderr, status); return status; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_report_error(stderr, status); return status; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_report_error(stderr, status); return status; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_report_error(stderr, status); return status; } // ISO Date if (fits_write_date(fptr, &status)) { fits_report_error(stderr, status); return status; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } int rot = 0, mirror = 0; if (rotCounter > 0) { rot = (90 * rotCounter) % 360; if (rot < 0) rot += 360; } if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0) mirror = 1; if ((rot != 0) || (mirror != 0)) rotWCSFITS(rot, mirror); rotCounter = flipHCounter = flipVCounter = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = newFilename; fits_flush_file(fptr, &status); qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename; return status; } void FITSData::clearImageBuffers() { delete[] m_ImageBuffer; m_ImageBuffer = nullptr; //m_BayerBuffer = nullptr; } void FITSData::calculateStats(bool refresh) { // Calculate min max calculateMinMax(refresh); // Get standard deviation and mean in one run switch (m_DataType) { case TBYTE: runningAverageStdDev(); break; case TSHORT: runningAverageStdDev(); break; case TUSHORT: runningAverageStdDev(); break; case TLONG: runningAverageStdDev(); break; case TULONG: runningAverageStdDev(); break; case TFLOAT: runningAverageStdDev(); break; case TLONGLONG: runningAverageStdDev(); break; case TDOUBLE: runningAverageStdDev(); break; default: return; } // FIXME That's not really SNR, must implement a proper solution for this value stats.SNR = stats.mean[0] / stats.stddev[0]; if (refresh && markStars) // Let's try to find star positions again after transformation starsSearched = false; } int FITSData::calculateMinMax(bool refresh) { int status, nfound = 0; status = 0; if ((fptr != nullptr) && !refresh) { if (fits_read_key_dbl(fptr, "DATAMIN", &(stats.min[0]), nullptr, &status) == 0) nfound++; if (fits_read_key_dbl(fptr, "DATAMAX", &(stats.max[0]), nullptr, &status) == 0) nfound++; // If we found both keywords, no need to calculate them, unless they are both zeros if (nfound == 2 && !(stats.min[0] == 0 && stats.max[0] == 0)) return 0; } stats.min[0] = 1.0E30; stats.max[0] = -1.0E30; stats.min[1] = 1.0E30; stats.max[1] = -1.0E30; stats.min[2] = 1.0E30; stats.max[2] = -1.0E30; switch (m_DataType) { case TBYTE: calculateMinMax(); break; case TSHORT: calculateMinMax(); break; case TUSHORT: calculateMinMax(); break; case TLONG: calculateMinMax(); break; case TULONG: calculateMinMax(); break; case TFLOAT: calculateMinMax(); break; case TLONGLONG: calculateMinMax(); break; case TDOUBLE: calculateMinMax(); break; default: break; } //qDebug() << "DATAMIN: " << stats.min << " - DATAMAX: " << stats.max; return 0; } template QPair FITSData::getParitionMinMax(uint32_t start, uint32_t stride) { auto * buffer = reinterpret_cast(m_ImageBuffer); T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { if (buffer[i] < min) min = buffer[i]; else if (buffer[i] > max) max = buffer[i]; } return qMakePair(min, max); } template void FITSData::calculateMinMax() { T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); if (result.first < min) min = result.first; if (result.second > max) max = result.second; } stats.min[n] = min; stats.max[n] = max; } } template QPair FITSData::getSquaredSumAndMean(uint32_t start, uint32_t stride) { uint32_t m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; auto * buffer = reinterpret_cast(m_ImageBuffer); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } return qMakePair(m_newM, m_newS); } template void FITSData::runningAverageStdDev() { // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getSquaredSumAndMean, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } double mean = 0, squared_sum = 0; // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); mean += result.first; squared_sum += result.second; } double variance = squared_sum / stats.samples_per_channel; stats.mean[n] = mean / nThreads; stats.stddev[n] = sqrt(variance); } } void FITSData::setMinMax(double newMin, double newMax, uint8_t channel) { stats.min[channel] = newMin; stats.max[channel] = newMax; } bool FITSData::parseHeader() { char * header = nullptr; int status = 0, nkeys = 0; if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status)) { fits_report_error(stderr, status); free(header); return false; } QString recordList = QString(header); for (int i = 0; i < nkeys; i++) { Record * oneRecord = new Record; // Quotes cause issues for simplified below so we're removing them. QString record = recordList.mid(i * 80, 80).remove("'"); QStringList properties = record.split(QRegExp("[=/]")); // If it is only a comment if (properties.size() == 1) { oneRecord->key = properties[0].mid(0, 7); oneRecord->comment = properties[0].mid(8).simplified(); } else { oneRecord->key = properties[0].simplified(); oneRecord->value = properties[1].simplified(); if (properties.size() > 2) oneRecord->comment = properties[2].simplified(); // Try to guess the value. // Test for integer & double. If neither, then leave it as "string". bool ok = false; // Is it Integer? oneRecord->value.toInt(&ok); if (ok) oneRecord->value.convert(QMetaType::Int); else { // Is it double? oneRecord->value.toDouble(&ok); if (ok) oneRecord->value.convert(QMetaType::Double); } } records.append(oneRecord); } free(header); return true; } bool FITSData::getRecordValue(const QString &key, QVariant &value) const { for (Record * oneRecord : records) { if (oneRecord->key == key) { value = oneRecord->value; return true; } } return false; } -bool FITSData::checkCollision(Edge * s1, Edge * s2) -{ - int dis; //distance - - int diff_x = s1->x - s2->x; - int diff_y = s1->y - s2->y; - - dis = std::abs(sqrt(diff_x * diff_x + diff_y * diff_y)); - dis -= s1->width / 2; - dis -= s2->width / 2; - - if (dis <= 0) //collision - return true; - - //no collision - return false; -} - -int FITSData::findCannyStar(FITSData * data, const QRect &boundary) -{ - switch (data->property("dataType").toInt()) - { - case TBYTE: - return FITSData::findCannyStar(data, boundary); - - case TSHORT: - return FITSData::findCannyStar(data, boundary); - - case TUSHORT: - return FITSData::findCannyStar(data, boundary); - - case TLONG: - return FITSData::findCannyStar(data, boundary); - - case TULONG: - return FITSData::findCannyStar(data, boundary); - - case TFLOAT: - return FITSData::findCannyStar(data, boundary); - - case TLONGLONG: - return FITSData::findCannyStar(data, boundary); - - case TDOUBLE: - return FITSData::findCannyStar(data, boundary); - - default: - break; - } - - return 0; -} - int FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox) { int count = 0; starAlgorithm = algorithm; qDeleteAll(starCenters); starCenters.clear(); switch (algorithm) { case ALGORITHM_SEP: - count = findSEPStars(trackingBox); + count = FITSSEPDetector(this) + .findSources(starCenters, trackingBox); break; case ALGORITHM_GRADIENT: - count = findCannyStar(this, trackingBox); + count = FITSGradientDetector(this) + .findSources(starCenters, trackingBox); break; case ALGORITHM_CENTROID: - count = findCentroid(trackingBox); + count = FITSCentroidDetector(this) + .findSources(starCenters, trackingBox); break; case ALGORITHM_THRESHOLD: - count = findOneStar(trackingBox); + count = FITSThresholdDetector(this) + .configure("Threshold", Options::focusThreshold()) + .findSources(starCenters, trackingBox); break; } starsSearched = true; return count; } int FITSData::filterStars(const float innerRadius, const float outerRadius) { long const sqDiagonal = this->width() * this->width() / 4 + this->height() * this->height() / 4; long const sqInnerRadius = std::lround(sqDiagonal * innerRadius * innerRadius); long const sqOuterRadius = std::lround(sqDiagonal * outerRadius * outerRadius); starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(), [&](Edge * edge) { long const x = edge->x - this->width() / 2; long const y = edge->y - this->height() / 2; long const sqRadius = x * x + y * y; return sqRadius < sqInnerRadius || sqOuterRadius < sqRadius; }), starCenters.end()); return starCenters.count(); } -template -int FITSData::findCannyStar(FITSData * data, const QRect &boundary) -{ - int subX = qMax(0, boundary.isNull() ? 0 : boundary.x()); - int subY = qMax(0, boundary.isNull() ? 0 : boundary.y()); - int subW = (boundary.isNull() ? data->width() : boundary.width()); - int subH = (boundary.isNull() ? data->height() : boundary.height()); - - int BBP = data->getBytesPerPixel(); - - uint16_t dataWidth = data->width(); - - // #1 Find offsets - uint32_t size = subW * subH; - uint32_t offset = subX + subY * dataWidth; - - // #2 Create new buffer - auto * buffer = new uint8_t[size * BBP]; - // If there is no offset, copy whole buffer in one go - if (offset == 0) - memcpy(buffer, data->getImageBuffer(), size * BBP); - else - { - uint8_t * dataPtr = buffer; - uint8_t * origDataPtr = data->getImageBuffer(); - uint32_t lineOffset = 0; - // Copy data line by line - for (int height = subY; height < (subY + subH); height++) - { - lineOffset = (subX + height * dataWidth) * BBP; - memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP); - dataPtr += (subW * BBP); - } - } - - // #3 Create new FITSData to hold it - auto * boundedImage = new FITSData(); - boundedImage->stats.width = subW; - boundedImage->stats.height = subH; - boundedImage->stats.bitpix = data->stats.bitpix; - boundedImage->stats.bytesPerPixel = data->stats.bytesPerPixel; - boundedImage->stats.samples_per_channel = size; - boundedImage->stats.ndim = 2; - - boundedImage->setProperty("dataType", data->property("dataType")); - - // #4 Set image buffer and calculate stats. - boundedImage->setImageBuffer(buffer); - - boundedImage->calculateStats(true); - - // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain - boundedImage->applyFilter(FITS_MEDIAN); - boundedImage->applyFilter(FITS_HIGH_CONTRAST); - - // #6 Perform Sobel to find gradients and their directions - QVector gradients; - QVector directions; - - // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed - // and discarded whenever necessary. It won't work on noisy images unless this is done. - boundedImage->sobel(gradients, directions); - - QVector ids(gradients.size()); - - int maxID = boundedImage->partition(subW, subH, gradients, ids); - - // Not needed anymore - delete boundedImage; - - if (maxID == 0) - return 0; - - typedef struct - { - float massX = 0; - float massY = 0; - float totalMass = 0; - } massInfo; - - QMap masses; - - // #7 Calculate center of mass for all detected regions - for (int y = 0; y < subH; y++) - { - for (int x = 0; x < subW; x++) - { - int index = x + y * subW; - - int regionID = ids[index]; - if (regionID > 0) - { - float pixel = gradients[index]; - - masses[regionID].totalMass += pixel; - masses[regionID].massX += x * pixel; - masses[regionID].massY += y * pixel; - } - } - } - - // Compare multiple masses, and only select the highest total mass one as the desired star - int maxRegionID = 1; - int maxTotalMass = masses[1].totalMass; - double totalMassRatio = 1e6; - for (auto key : masses.keys()) - { - massInfo oneMass = masses.value(key); - if (oneMass.totalMass > maxTotalMass) - { - totalMassRatio = oneMass.totalMass / maxTotalMass; - maxTotalMass = oneMass.totalMass; - maxRegionID = key; - } - } - - // If image has many regions and there is no significant relative center of mass then it's just noise and no stars - // are probably there above a useful threshold. - if (maxID > 10 && totalMassRatio < 1.5) - return 0; - - auto * center = new Edge; - center->width = -1; - center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5; - center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5; - center->HFR = 1; - - // Maximum Radius - int maxR = qMin(subW - 1, subH - 1) / 2; - - for (int r = maxR; r > 1; r--) - { - int pass = 0; - - for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0) - { - int testX = center->x + std::cos(theta) * r; - int testY = center->y + std::sin(theta) * r; - - // if out of bound, break; - if (testX < 0 || testX >= subW || testY < 0 || testY >= subH) - break; - - if (gradients[testX + testY * subW] > 0) - //if (thresholded[testX + testY * subW] > 0) - { - if (++pass >= 24) - { - center->width = r * 2; - // Break of outer loop - r = 0; - break; - } - } - } - } - - qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width; - - // If no stars were detected - if (center->width == -1) - { - delete center; - return 0; - } - - // 30% fuzzy - //center->width += center->width*0.3 * (running_threshold / threshold); - - double FSum = 0, HF = 0, TF = 0; - const double resolution = 1.0 / 20.0; - - int cen_y = qRound(center->y); - - double rightEdge = center->x + center->width / 2.0; - double leftEdge = center->x - center->width / 2.0; - - QVector subPixels; - subPixels.reserve(center->width / resolution); - - const T * origBuffer = reinterpret_cast(data->getImageBuffer()) + offset; - - for (double x = leftEdge; x <= rightEdge; x += resolution) - { - double slice = resolution * (origBuffer[static_cast(floor(x)) + cen_y * dataWidth]); - FSum += slice; - subPixels.append(slice); - } - - // Half flux - HF = FSum / 2.0; - - int subPixelCenter = (center->width / resolution) / 2; - - // Start from center - TF = subPixels[subPixelCenter]; - double lastTF = TF; - // Integrate flux along radius axis until we reach half flux - //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) - for (int k = 1; k < subPixelCenter; k++) - { - TF += subPixels[subPixelCenter + k]; - TF += subPixels[subPixelCenter - k]; - - if (TF >= HF) - { - // We overpassed HF, let's calculate from last TF how much until we reach HF - - // #1 Accurate calculation, but very sensitive to small variations of flux - center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; - - // #2 Less accurate calculation, but stable against small variations of flux - //center->HFR = (k - 1) * resolution; - break; - } - - lastTF = TF; - } - - // Correct center for subX and subY - center->x += subX; - center->y += subY; - - data->appendStar(center); - - qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR; - - return 1; -} - -int FITSData::findOneStar(const QRect &boundary) -{ - switch (m_DataType) - { - case TBYTE: - return findOneStar(boundary); - - case TSHORT: - return findOneStar(boundary); - - case TUSHORT: - return findOneStar(boundary); - - case TLONG: - return findOneStar(boundary); - - case TULONG: - return findOneStar(boundary); - - case TFLOAT: - return findOneStar(boundary); - - case TLONGLONG: - return findOneStar(boundary); - - case TDOUBLE: - return findOneStar(boundary); - - default: - break; - } - - return 0; -} - -template -int FITSData::findOneStar(const QRect &boundary) -{ - if (boundary.isEmpty()) - return -1; - - int subX = boundary.x(); - int subY = boundary.y(); - int subW = subX + boundary.width(); - int subH = subY + boundary.height(); - - float massX = 0, massY = 0, totalMass = 0; - - auto * buffer = reinterpret_cast(m_ImageBuffer); - - // TODO replace magic number with something more useful to understand - double threshold = stats.mean[0] * Options::focusThreshold() / 100.0; - - for (int y = subY; y < subH; y++) - { - for (int x = subX; x < subW; x++) - { - T pixel = buffer[x + y * stats.width]; - if (pixel > threshold) - { - totalMass += pixel; - massX += x * pixel; - massY += y * pixel; - } - } - } - - qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << massX / totalMass << " Y: " << massY / totalMass; - - auto * center = new Edge; - center->width = -1; - center->x = massX / totalMass + 0.5; - center->y = massY / totalMass + 0.5; - center->HFR = 1; - - // Maximum Radius - int maxR = qMin(subW - 1, subH - 1) / 2; - - // Critical threshold - double critical_threshold = threshold * 0.7; - double running_threshold = threshold; - - while (running_threshold >= critical_threshold) - { - for (int r = maxR; r > 1; r--) - { - int pass = 0; - - for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 10.0) - { - int testX = center->x + std::cos(theta) * r; - int testY = center->y + std::sin(theta) * r; - - // if out of bound, break; - if (testX < subX || testX > subW || testY < subY || testY > subH) - break; - - if (buffer[testX + testY * stats.width] > running_threshold) - pass++; - } - - //qDebug() << "Testing for radius " << r << " passes # " << pass << " @ threshold " << running_threshold; - //if (pass >= 6) - if (pass >= 5) - { - center->width = r * 2; - break; - } - } - - if (center->width > 0) - break; - - // Increase threshold fuzziness by 10% - running_threshold -= running_threshold * 0.1; - } - - // If no stars were detected - if (center->width == -1) - { - delete center; - return 0; - } - - // 30% fuzzy - //center->width += center->width*0.3 * (running_threshold / threshold); - - starCenters.append(center); - - double FSum = 0, HF = 0, TF = 0, min = stats.min[0]; - const double resolution = 1.0 / 20.0; - - int cen_y = qRound(center->y); - - double rightEdge = center->x + center->width / 2.0; - double leftEdge = center->x - center->width / 2.0; - - QVector subPixels; - subPixels.reserve(center->width / resolution); - - for (double x = leftEdge; x <= rightEdge; x += resolution) - { - //subPixels[x] = resolution * (image_buffer[static_cast(floor(x)) + cen_y * stats.width] - min); - double slice = resolution * (buffer[static_cast(floor(x)) + cen_y * stats.width] - min); - FSum += slice; - subPixels.append(slice); - } - - // Half flux - HF = FSum / 2.0; - - //double subPixelCenter = center->x - fmod(center->x,resolution); - int subPixelCenter = (center->width / resolution) / 2; - - // Start from center - TF = subPixels[subPixelCenter]; - double lastTF = TF; - // Integrate flux along radius axis until we reach half flux - //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) - for (int k = 1; k < subPixelCenter; k++) - { - TF += subPixels[subPixelCenter + k]; - TF += subPixels[subPixelCenter - k]; - - if (TF >= HF) - { - // We have two ways to calculate HFR. The first is the correct method but it can get quite variable within 10% due to random fluctuations of the measured star. - // The second method is not truly HFR but is much more resistant to noise. - - // #1 Approximate HFR, accurate and reliable but quite variable to small changes in star flux - center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; - - // #2 Not exactly HFR, but much more stable - //center->HFR = (k*resolution) * (HF/TF); - break; - } - - lastTF = TF; - } - - return 1; -} - -/*** Find center of stars and calculate Half Flux Radius */ -int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) -{ - switch (m_DataType) - { - case TBYTE: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TSHORT: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TUSHORT: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TLONG: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TULONG: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TFLOAT: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TLONGLONG: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - case TDOUBLE: - return findCentroid(boundary, initStdDev, minEdgeWidth); - - default: - return -1; - } -} - -template -int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) -{ - double threshold = 0, sum = 0, avg = 0, min = 0; - int starDiameter = 0; - int pixVal = 0; - int minimumEdgeCount = MINIMUM_EDGE_LIMIT; - - auto * buffer = reinterpret_cast(m_ImageBuffer); - - double JMIndex = 100; -#ifndef KSTARS_LITE - if (histogram) - { - if (!histogram->isConstructed()) - histogram->constructHistogram(); - JMIndex = histogram->getJMIndex(); - } -#endif - - float dispersion_ratio = 1.5; - - QList edges; - - if (JMIndex < DIFFUSE_THRESHOLD) - { - minEdgeWidth = JMIndex * 35 + 1; - minimumEdgeCount = minEdgeWidth - 1; - } - else - { - minEdgeWidth = 6; - minimumEdgeCount = 4; - } - - while (initStdDev >= 1) - { - minEdgeWidth--; - minimumEdgeCount--; - - minEdgeWidth = qMax(3, minEdgeWidth); - minimumEdgeCount = qMax(3, minimumEdgeCount); - - if (JMIndex < DIFFUSE_THRESHOLD) - { - // Taking the average out seems to have better result for noisy images - threshold = stats.max[0] - stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); - - min = stats.min[0]; - if (threshold - min < 0) - { - threshold = stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); - min = 0; - } - - dispersion_ratio = 1.4 - (MINIMUM_STDVAR - initStdDev) * 0.08; - } - else - { - threshold = stats.mean[0] + stats.stddev[0] * initStdDev * (0.3 - (MINIMUM_STDVAR - initStdDev) * 0.05); - min = stats.min[0]; - // Ratio between centeroid center and edge - dispersion_ratio = 1.8 - (MINIMUM_STDVAR - initStdDev) * 0.2; - } - - qCDebug(KSTARS_FITS) << "SNR: " << stats.SNR; - qCDebug(KSTARS_FITS) << "The threshold level is " << threshold << "(actual " << threshold - min - << ") minimum edge width" << minEdgeWidth << " minimum edge limit " << minimumEdgeCount; - - threshold -= min; - - int subX, subY, subW, subH; - - if (boundary.isNull()) - { - if (m_Mode == FITS_GUIDE || m_Mode == FITS_FOCUS) - { - // Only consider the central 70% - subX = round(stats.width * 0.15); - subY = round(stats.height * 0.15); - subW = stats.width - subX; - subH = stats.height - subY; - } - else - { - // Consider the complete area 100% - subX = 0; - subY = 0; - subW = stats.width; - subH = stats.height; - } - } - else - { - subX = boundary.x(); - subY = boundary.y(); - subW = subX + boundary.width(); - subH = subY + boundary.height(); - } - - // Detect "edges" that are above threshold - for (int i = subY; i < subH; i++) - { - starDiameter = 0; - - for (int j = subX; j < subW; j++) - { - pixVal = buffer[j + (i * stats.width)] - min; - - // If pixel value > threshold, let's get its weighted average - if (pixVal >= threshold) - { - avg += j * pixVal; - sum += pixVal; - starDiameter++; - } - // Value < threshold but avg exists - else if (sum > 0) - { - // We found a potential centroid edge - if (starDiameter >= minEdgeWidth) - { - float center = avg / sum + 0.5; - if (center > 0) - { - int i_center = std::floor(center); - - // Check if center is 10% or more brighter than edge, if not skip - if (((buffer[i_center + (i * stats.width)] - min) / - (buffer[i_center + (i * stats.width) - starDiameter / 2] - min) >= - dispersion_ratio) && - ((buffer[i_center + (i * stats.width)] - min) / - (buffer[i_center + (i * stats.width) + starDiameter / 2] - min) >= - dispersion_ratio)) - { - qCDebug(KSTARS_FITS) - << "Edge center is " << buffer[i_center + (i * stats.width)] - min - << " Edge is " << buffer[i_center + (i * stats.width) - starDiameter / 2] - min - << " and ratio is " - << ((buffer[i_center + (i * stats.width)] - min) / - (buffer[i_center + (i * stats.width) - starDiameter / 2] - min)) - << " located at X: " << center << " Y: " << i + 0.5; - - auto * newEdge = new Edge(); - - newEdge->x = center; - newEdge->y = i + 0.5; - newEdge->scanned = 0; - newEdge->val = buffer[i_center + (i * stats.width)] - min; - newEdge->width = starDiameter; - newEdge->HFR = 0; - newEdge->sum = sum; - - edges.append(newEdge); - } - } - } - - // Reset - avg = sum = starDiameter = 0; - } - } - } - - qCDebug(KSTARS_FITS) << "Total number of edges found is: " << edges.count(); - - // In case of hot pixels - if (edges.count() == 1 && initStdDev > 1) - { - initStdDev--; - continue; - } - - if (edges.count() >= MAX_EDGE_LIMIT) - { - qCWarning(KSTARS_FITS) << "Too many edges, aborting... " << edges.count(); - qDeleteAll(edges); - return -1; - } - - if (edges.count() >= minimumEdgeCount) - break; - - qDeleteAll(edges); - edges.clear(); - initStdDev--; - } - - int cen_count = 0; - int cen_x = 0; - int cen_y = 0; - int cen_v = 0; - int cen_w = 0; - int width_sum = 0; - - // Let's sort edges, starting with widest - std::sort(edges.begin(), edges.end(), greaterThan); - - // Now, let's scan the edges and find the maximum centroid vertically - for (int i = 0; i < edges.count(); i++) - { - qCDebug(KSTARS_FITS) << "# " << i << " Edge at (" << edges[i]->x << "," << edges[i]->y << ") With a value of " - << edges[i]->val << " and width of " << edges[i]->width << " pixels. with sum " << edges[i]->sum; - - // If edge scanned already, skip - if (edges[i]->scanned == 1) - { - qCDebug(KSTARS_FITS) << "Skipping check for center " << i << " because it was already counted"; - continue; - } - - qCDebug(KSTARS_FITS) << "Investigating edge # " << i << " now ..."; - - // Get X, Y, and Val of edge - cen_x = edges[i]->x; - cen_y = edges[i]->y; - cen_v = edges[i]->sum; - cen_w = edges[i]->width; - - float avg_x = 0; - float avg_y = 0; - - sum = 0; - cen_count = 0; - - // Now let's compare to other edges until we hit a maxima - for (int j = 0; j < edges.count(); j++) - { - if (edges[j]->scanned) - continue; - - if (checkCollision(edges[j], edges[i])) - { - if (edges[j]->sum >= cen_v) - { - cen_v = edges[j]->sum; - cen_w = edges[j]->width; - } - - edges[j]->scanned = 1; - cen_count++; - - avg_x += edges[j]->x * edges[j]->val; - avg_y += edges[j]->y * edges[j]->val; - sum += edges[j]->val; - - continue; - } - } - - int cen_limit = (MINIMUM_ROWS_PER_CENTER - (MINIMUM_STDVAR - initStdDev)); - - if (edges.count() < LOW_EDGE_CUTOFF_1) - { - if (edges.count() < LOW_EDGE_CUTOFF_2) - cen_limit = 1; - else - cen_limit = 2; - } - - qCDebug(KSTARS_FITS) << "center_count: " << cen_count << " and initstdDev= " << initStdDev << " and limit is " - << cen_limit; - - if (cen_limit < 1) - continue; - - // If centroid count is within acceptable range - //if (cen_limit >= 2 && cen_count >= cen_limit) - if (cen_count >= cen_limit) - { - // We detected a centroid, let's init it - auto * rCenter = new Edge(); - - rCenter->x = avg_x / sum; - rCenter->y = avg_y / sum; - width_sum += rCenter->width; - rCenter->width = cen_w; - - qCDebug(KSTARS_FITS) << "Found a real center with number with (" << rCenter->x << "," << rCenter->y << ")"; - - // Calculate Total Flux From Center, Half Flux, Full Summation - double TF = 0; - double HF = 0; - double FSum = 0; - - cen_x = (int)std::floor(rCenter->x); - cen_y = (int)std::floor(rCenter->y); - - if (cen_x < 0 || cen_x > stats.width || cen_y < 0 || cen_y > stats.height) - { - delete rCenter; - continue; - } - - // Complete sum along the radius - //for (int k=0; k < rCenter->width; k++) - for (int k = rCenter->width / 2; k >= -(rCenter->width / 2); k--) - { - FSum += buffer[cen_x - k + (cen_y * stats.width)] - min; - //qDebug() << image_buffer[cen_x-k+(cen_y*stats.width)] - min; - } - - // Half flux - HF = FSum / 2.0; - - // Total flux starting from center - TF = buffer[cen_y * stats.width + cen_x] - min; - - int pixelCounter = 1; - - // Integrate flux along radius axis until we reach half flux - for (int k = 1; k < rCenter->width / 2; k++) - { - if (TF >= HF) - { - qCDebug(KSTARS_FITS) << "Stopping at TF " << TF << " after #" << k << " pixels."; - break; - } - - TF += buffer[cen_y * stats.width + cen_x + k] - min; - TF += buffer[cen_y * stats.width + cen_x - k] - min; - - pixelCounter++; - } - - // Calculate weighted Half Flux Radius - rCenter->HFR = pixelCounter * (HF / TF); - // Store full flux - rCenter->val = FSum; - - qCDebug(KSTARS_FITS) << "HFR for this center is " << rCenter->HFR << " pixels and the total flux is " << FSum; - - starCenters.append(rCenter); - } - } - - if (starCenters.count() > 1 && m_Mode != FITS_FOCUS) - { - float width_avg = (float)width_sum / starCenters.count(); - float lsum = 0, sdev = 0; - - for (auto ¢er : starCenters) - lsum += (center->width - width_avg) * (center->width - width_avg); - - sdev = (std::sqrt(lsum / (starCenters.count() - 1))) * 4; - - // Reject stars > 4 * stddev - foreach (Edge * center, starCenters) - if (center->width > sdev) - starCenters.removeOne(center); - - //foreach(Edge *center, starCenters) - //qDebug() << center->x << "," << center->y << "," << center->width << "," << center->val << endl; - } - - // Release memory - qDeleteAll(edges); - - return starCenters.count(); -} - double FITSData::getHFR(HFRType type) { // This method is less susceptible to noise // Get HFR for the brightest star only, instead of averaging all stars // It is more consistent. // TODO: Try to test this under using a real CCD. if (starCenters.empty()) return -1; if (type == HFR_MAX) { maxHFRStar = nullptr; int maxVal = 0; int maxIndex = 0; for (int i = 0; i < starCenters.count(); i++) { if (starCenters[i]->HFR > maxVal) { maxIndex = i; maxVal = starCenters[i]->HFR; } } maxHFRStar = starCenters[maxIndex]; return static_cast(starCenters[maxIndex]->HFR); } QVector HFRs; for (auto center : starCenters) HFRs << center->HFR; std::sort(HFRs.begin(), HFRs.end()); double sum = std::accumulate(HFRs.begin(), HFRs.end(), 0.0); double m = sum / HFRs.size(); if (HFRs.size() > 3) { double accum = 0.0; std::for_each (HFRs.begin(), HFRs.end(), [&](const double d) { accum += (d - m) * (d - m); }); double stddev = sqrt(accum / (HFRs.size() - 1)); // Remove stars over 2 standard deviations away. auto end1 = std::remove_if(HFRs.begin(), HFRs.end(), [m, stddev](const double hfr) { return hfr > (m + stddev * 2); }); auto end2 = std::remove_if(HFRs.begin(), end1, [m, stddev](const double hfr) { return hfr < (m - stddev * 2); }); // New mean sum = std::accumulate(HFRs.begin(), end2, 0.0); const int num_remaining = std::distance(HFRs.begin(), end2); if (num_remaining > 0) m = sum / num_remaining; } return m; } double FITSData::getHFR(int x, int y) { if (starCenters.empty()) return -1; for (int i = 0; i < starCenters.count(); i++) { if (std::fabs(starCenters[i]->x - x) <= starCenters[i]->width / 2 && std::fabs(starCenters[i]->y - y) <= starCenters[i]->width / 2) { return starCenters[i]->HFR; } } return -1; } void FITSData::applyFilter(FITSScale type, uint8_t * image, QVector * min, QVector * max) { if (type == FITS_NONE) return; QVector dataMin(3); QVector dataMax(3); if (min) dataMin = *min; if (max) dataMax = *max; switch (type) { case FITS_AUTO_STRETCH: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] - stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_CONTRAST: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] + stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_PASS: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i]; } } break; default: break; } switch (m_DataType) { case TBYTE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT8_MAX ? UINT8_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT16_MIN ? INT16_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT16_MAX ? INT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TUSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT16_MAX ? UINT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT_MIN ? INT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT_MAX ? INT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TULONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT_MAX ? UINT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TFLOAT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < FLT_MIN ? FLT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > FLT_MAX ? FLT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONGLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < LLONG_MIN ? LLONG_MIN : dataMin[i]; dataMax[i] = dataMax[i] > LLONG_MAX ? LLONG_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TDOUBLE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < DBL_MIN ? DBL_MIN : dataMin[i]; dataMax[i] = dataMax[i] > DBL_MAX ? DBL_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; default: return; } if (min != nullptr) *min = dataMin; if (max != nullptr) *max = dataMax; } template void FITSData::applyFilter(FITSScale type, uint8_t * targetImage, QVector * targetMin, QVector * targetMax) { bool calcStats = false; T * image = nullptr; if (targetImage) image = reinterpret_cast(targetImage); else { image = reinterpret_cast(m_ImageBuffer); calcStats = true; } T min[3], max[3]; for (int i = 0; i < 3; i++) { min[i] = (*targetMin)[i] < std::numeric_limits::min() ? std::numeric_limits::min() : (*targetMin)[i]; max[i] = (*targetMax)[i] > std::numeric_limits::max() ? std::numeric_limits::max() : (*targetMax)[i]; } // Create N threads const uint8_t nThreads = 16; uint32_t width = stats.width; uint32_t height = stats.height; //QTime timer; //timer.start(); switch (type) { case FITS_AUTO: case FITS_LINEAR: case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: case FITS_LOG: case FITS_SQRT: case FITS_HIGH_PASS: { // List of futures QList> futures; QVector coeff(3); if (type == FITS_LOG) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / std::log(1 + max[i]); } else if (type == FITS_SQRT) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / sqrt(max[i]); } for (int n = 0; n < m_Channels; n++) { if (type == FITS_HIGH_PASS) min[n] = stats.mean[n]; uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); T * runningBuffer = image + cStart; if (type == FITS_LOG) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * std::log(1 + qBound(min[n], a, max[n])))), max[n]); })); runningBuffer += tStride; } } else if (type == FITS_SQRT) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * a)), max[n]); })); } runningBuffer += tStride; } else { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, n](T & a) { a = qBound(min[n], a, max[n]); })); runningBuffer += tStride; } } } for (int i = 0; i < nThreads * m_Channels; i++) futures[i].waitForFinished(); if (calcStats) { for (int i = 0; i < 3; i++) { stats.min[i] = min[i]; stats.max[i] = max[i]; } //if (type != FITS_AUTO && type != FITS_LINEAR) runningAverageStdDev(); //QtConcurrent::run(this, &FITSData::runningAverageStdDev); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; if (!histogram->isConstructed()) histogram->constructHistogram(); T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); uint32_t row = 0; uint32_t index = 0; for (int i = 0; i < m_Channels; i++) { uint32_t offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min[i]) / histogram->getBinWidth(i); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min[i], static_cast(round(coeff * cumulativeFreq[bufferVal])), max[i]); } } } #endif } if (calcStats) calculateStats(true); break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { uint8_t BBP = stats.bytesPerPixel; auto * extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < m_Channels; ch++) { uint32_t offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9 * sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; default: break; } } QList FITSData::getStarCentersInSubFrame(QRect subFrame) const { QList starCentersInSubFrame; for (int i = 0; i < starCenters.count(); i++) { int x = static_cast(starCenters[i]->x); int y = static_cast(starCenters[i]->y); if(subFrame.contains(x, y)) { starCentersInSubFrame.append(starCenters[i]); } } return starCentersInSubFrame; } -void FITSData::getCenterSelection(int * x, int * y) -{ - if (starCenters.count() == 0) - return; - - auto * pEdge = new Edge(); - pEdge->x = *x; - pEdge->y = *y; - pEdge->width = 1; - - foreach (Edge * center, starCenters) - if (checkCollision(pEdge, center)) - { - *x = static_cast(center->x); - *y = static_cast(center->y); - break; - } - - delete (pEdge); -} - bool FITSData::checkForWCS() { #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB int status = 0; char * header; int nkeyrec, nreject; // Free wcs before re-use if (m_wcs != nullptr) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; } if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &m_nwcs, &m_wcs)) != 0) { free(header); wcsvfree(&m_nwcs, &m_wcs); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (m_wcs->crpix[0] == 0) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(m_wcs)) != 0) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } HasWCS = true; #endif #endif return HasWCS; } bool FITSData::loadWCS() { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) if (WCSLoaded) { qWarning() << "WCS data already loaded"; return true; } if (m_wcs != nullptr) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; } qCDebug(KSTARS_FITS) << "Started WCS Data Processing..."; int status = 0; char * header; int nkeyrec, nreject, nwcs, stat[2]; double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2]; int w = width(); int h = height(); if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &m_wcs)) != 0) { free(header); wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (m_wcs->crpix[0] == 0) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(m_wcs)) != 0) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } delete[] wcs_coord; wcs_coord = new wcs_point[w * h]; if (wcs_coord == nullptr) { wcsvfree(&m_nwcs, &m_wcs); m_wcs = nullptr; lastError = "Not enough memory for WCS data!"; return false; } wcs_point * p = wcs_coord; for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { pixcrd[0] = j; pixcrd[1] = i; if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); } else { p->ra = world[0]; p->dec = world[1]; p++; } } } findObjectsInImage(&world[0], phi, theta, &imgcrd[0], &pixcrd[0], &stat[0]); WCSLoaded = true; HasWCS = true; qCDebug(KSTARS_FITS) << "Finished WCS Data processing..."; return true; #else return false; #endif } bool FITSData::wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2]; if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } worldcrd[0] = wcsCoord.ra0().Degrees(); worldcrd[1] = wcsCoord.dec0().Degrees(); if ((status = wcss2p(m_wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { lastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } wcsImagePoint.setX(imgcrd[0]); wcsImagePoint.setY(imgcrd[1]); wcsPixelPoint.setX(pixcrd[0]); wcsPixelPoint.setY(pixcrd[1]); return true; #else Q_UNUSED(wcsCoord); Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsImagePoint); return false; #endif } bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], phi, pixcrd[2], theta, world[2]; if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } pixcrd[0] = wcsPixelPoint.x(); pixcrd[1] = wcsPixelPoint.y(); if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } else { wcsCoord.setRA0(world[0] / 15.0); wcsCoord.setDec0(world[1]); } return true; #else Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsCoord); return false; #endif } #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) void FITSData::findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]) { int w = width(); int h = height(); int status = 0; char date[64]; KSNumbers * num = nullptr; if (fits_read_keyword(fptr, "DATE-OBS", date, nullptr, &status) == 0) { QString tsString(date); tsString = tsString.remove('\'').trimmed(); // Add Zulu time to indicate UTC tsString += "Z"; QDateTime ts = QDateTime::fromString(tsString, Qt::ISODate); if (ts.isValid()) num = new KSNumbers(KStarsDateTime(ts).djd()); } if (num == nullptr) num = new KSNumbers(KStarsData::Instance()->ut().djd()); //Set to current time if the above does not work. SkyMapComposite * map = KStarsData::Instance()->skyComposite(); wcs_point * wcs_coord = getWCSCoord(); if (wcs_coord != nullptr) { int size = w * h; objList.clear(); SkyPoint p1; p1.setRA0(dms(wcs_coord[0].ra)); p1.setDec0(dms(wcs_coord[0].dec)); p1.updateCoordsNow(num); SkyPoint p2; p2.setRA0(dms(wcs_coord[size - 1].ra)); p2.setDec0(dms(wcs_coord[size - 1].dec)); p2.updateCoordsNow(num); QList list = map->findObjectsInArea(p1, p2); foreach (SkyObject * object, list) { int type = object->type(); if (object->name() == "star" || type == SkyObject::PLANET || type == SkyObject::ASTEROID || type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON || type == SkyObject::SATELLITE) { //DO NOT DISPLAY, at least for now, because these things move and change. } int x = -100; int y = -100; world[0] = object->ra0().Degrees(); world[1] = object->dec0().Degrees(); if ((status = wcss2p(m_wcs, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { fprintf(stderr, "wcsp2s ERROR %d: %s.\n", status, wcs_errmsg[status]); } else { x = pixcrd[0]; //The X and Y are set to the found position if it does work. y = pixcrd[1]; } if (x > 0 && y > 0 && x < w && y < h) objList.append(new FITSSkyObject(object, x, y)); } } delete (num); } #endif QList FITSData::getSkyObjects() { return objList; } FITSSkyObject::FITSSkyObject(SkyObject * object, int xPos, int yPos) : QObject() { skyObjectStored = object; xLoc = xPos; yLoc = yPos; } SkyObject * FITSSkyObject::skyObject() { return skyObjectStored; } int FITSSkyObject::x() { return xLoc; } int FITSSkyObject::y() { return yLoc; } void FITSSkyObject::setX(int xPos) { xLoc = xPos; } void FITSSkyObject::setY(int yPos) { yLoc = yPos; } int FITSData::getFlipVCounter() const { return flipVCounter; } void FITSData::setFlipVCounter(int value) { flipVCounter = value; } int FITSData::getFlipHCounter() const { return flipHCounter; } void FITSData::setFlipHCounter(int value) { flipHCounter = value; } int FITSData::getRotCounter() const { return rotCounter; } void FITSData::setRotCounter(int value) { rotCounter = value; } /* Rotate an image by 90, 180, or 270 degrees, with an optional * reflection across the vertical or horizontal axis. * verbose generates extra info on stdout. * return nullptr if successful or rotated image. */ template bool FITSData::rotFITS(int rotate, int mirror) { int ny, nx; int x1, y1, x2, y2; uint8_t * rotimage = nullptr; int offset = 0; if (rotate == 1) rotate = 90; else if (rotate == 2) rotate = 180; else if (rotate == 3) rotate = 270; else if (rotate < 0) rotate = rotate + 360; nx = stats.width; ny = stats.height; int BBP = stats.bytesPerPixel; /* Allocate buffer for rotated image */ rotimage = new uint8_t[stats.samples_per_channel * m_Channels * BBP]; if (rotimage == nullptr) { qWarning() << "Unable to allocate memory for rotated image buffer!"; return false; } auto * rotBuffer = reinterpret_cast(rotimage); auto * buffer = reinterpret_cast(m_ImageBuffer); /* Mirror image without rotation */ if (rotate < 45 && rotate > -45) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } /* Rotate by 90 degrees */ else if (rotate >= 45 && rotate < 135) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* Rotate by 180 degrees */ else if (rotate >= 135 && rotate < 225) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } } /* Rotate by 270 degrees */ else if (rotate >= 225 && rotate < 315) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = y1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* If rotating by more than 315 degrees, assume top-bottom reflection */ else if (rotate >= 315 && mirror) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) { x2 = y1; y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } delete[] m_ImageBuffer; m_ImageBuffer = rotimage; return true; } void FITSData::rotWCSFITS(int angle, int mirror) { int status = 0; char comment[100]; double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2; int WCS_DECIMALS = 6; naxis1 = stats.width; naxis2 = stats.height; if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { // No WCS keywords return; } /* Reset CROTAn and CD matrix if axes have been exchanged */ if (angle == 90) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); } status = 0; /* Negate rotation angle if mirrored */ if (mirror != 0) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status); } status = 0; /* Unbin CRPIX and CD matrix */ if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) { if (ctemp1 != 1.0) { if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status)) if (ctemp1 == ctemp2) { double ltv1 = 0.0; double ltv2 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "LTV1", <v1, comment, &status)) fits_delete_key(fptr, "LTV1", &status); if (!fits_read_key_dbl(fptr, "LTV2", <v2, comment, &status)) fits_delete_key(fptr, "LTV2", &status); status = 0; if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); status = 0; fits_delete_key(fptr, "LTM1_1", &status); fits_delete_key(fptr, "LTM1_2", &status); } } } status = 0; /* Reset CRPIXn */ if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } } } status = 0; /* Reset CDELTn (degrees per pixel) */ if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Reset CD matrix, if present */ ctemp1 = 0.0; ctemp2 = 0.0; ctemp3 = 0.0; ctemp4 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status); fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status); fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status); status = 0; if (mirror != 0) { if (angle == 0) { fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); } else if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Delete any polynomial solution */ /* (These could maybe be switched, but I don't want to work them out yet */ status = 0; if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status)) { int i; char keyword[16]; for (i = 1; i < 13; i++) { sprintf(keyword, "CO1_%d", i); fits_delete_key(fptr, keyword, &status); } for (i = 1; i < 13; i++) { sprintf(keyword, "CO2_%d", i); fits_delete_key(fptr, keyword, &status); } } } -uint8_t * FITSData::getImageBuffer() +uint8_t * FITSData::getWritableImageBuffer() +{ + return m_ImageBuffer; +} + +uint8_t const * FITSData::getImageBuffer() const { return m_ImageBuffer; } void FITSData::setImageBuffer(uint8_t * buffer) { delete[] m_ImageBuffer; m_ImageBuffer = buffer; } bool FITSData::checkDebayer() { int status = 0; char bayerPattern[64]; // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status)) return false; if (stats.bitpix != 16 && stats.bitpix != 8) { KSNotification::error(i18n("Only 8 and 16 bits bayered images supported."), i18n("Debayer error")); return false; } QString pattern(bayerPattern); pattern = pattern.remove('\'').trimmed(); if (pattern == "RGGB") debayerParams.filter = DC1394_COLOR_FILTER_RGGB; else if (pattern == "GBRG") debayerParams.filter = DC1394_COLOR_FILTER_GBRG; else if (pattern == "GRBG") debayerParams.filter = DC1394_COLOR_FILTER_GRBG; else if (pattern == "BGGR") debayerParams.filter = DC1394_COLOR_FILTER_BGGR; // We return unless we find a valid pattern else { KSNotification::error(i18n("Unsupported bayer pattern %1.", pattern), i18n("Debayer error")); return false; } fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status); fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status); if (debayerParams.offsetX == 1) { // This may leave odd values in the 0th column if the color filter is not there // in the sensor, but otherwise should process the offset correctly. // Only offsets of 0 or 1 are implemented in debayer_8bit() and debayer_16bit(). switch (debayerParams.filter) { case DC1394_COLOR_FILTER_RGGB: debayerParams.filter = DC1394_COLOR_FILTER_GRBG; break; case DC1394_COLOR_FILTER_GBRG: debayerParams.filter = DC1394_COLOR_FILTER_BGGR; break; case DC1394_COLOR_FILTER_GRBG: debayerParams.filter = DC1394_COLOR_FILTER_RGGB; break; case DC1394_COLOR_FILTER_BGGR: debayerParams.filter = DC1394_COLOR_FILTER_GBRG; break; } debayerParams.offsetX = 0; } if (debayerParams.offsetX != 0 || debayerParams.offsetY > 1 || debayerParams.offsetY < 0) { KSNotification::error(i18n("Unsupported bayer offsets %1 %2.", debayerParams.offsetX, debayerParams.offsetY), i18n("Debayer error")); return false; } HasDebayer = true; return true; } void FITSData::getBayerParams(BayerParams * param) { param->method = debayerParams.method; param->filter = debayerParams.filter; param->offsetX = debayerParams.offsetX; param->offsetY = debayerParams.offsetY; } void FITSData::setBayerParams(BayerParams * param) { debayerParams.method = param->method; debayerParams.filter = param->filter; debayerParams.offsetX = param->offsetX; debayerParams.offsetY = param->offsetY; } bool FITSData::debayer() { // if (m_ImageBuffer == nullptr) // { // int anynull = 0, status = 0; // //m_BayerBuffer = m_ImageBuffer; // if (fits_read_img(fptr, m_DataType, 1, stats.samples_per_channel, nullptr, m_ImageBuffer, &anynull, &status)) // { // char errmsg[512]; // fits_get_errstatus(status, errmsg); // KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error")); // return false; // } // } switch (m_DataType) { case TBYTE: return debayer_8bit(); case TUSHORT: return debayer_16bit(); default: return false; } } bool FITSData::debayer_8bit() { dc1394error_t error_code; uint32_t rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; auto * bayer_source_buffer = reinterpret_cast(m_ImageBuffer); auto * bayer_destination_buffer = reinterpret_cast(destinationBuffer); if (bayer_destination_buffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; auto dc1394_source = bayer_source_buffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } // offsetX == 1 is handled in checkDebayer() and should be 0 here. error_code = dc1394_bayer_decoding_8bit(dc1394_source, bayer_destination_buffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_ImageBufferSize != rgb_size) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } m_ImageBufferSize = rgb_size; } auto bayered_buffer = reinterpret_cast(m_ImageBuffer); // Data in R1G1B1, we need to copy them into 3 layers for FITS uint8_t * rBuff = bayered_buffer; uint8_t * gBuff = bayered_buffer + (stats.width * stats.height); uint8_t * bBuff = bayered_buffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = bayer_destination_buffer[i]; *gBuff++ = bayer_destination_buffer[i + 1]; *bBuff++ = bayer_destination_buffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; return true; } bool FITSData::debayer_16bit() { dc1394error_t error_code; uint32_t rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; auto * bayer_source_buffer = reinterpret_cast(m_ImageBuffer); auto * bayer_destination_buffer = reinterpret_cast(destinationBuffer); if (bayer_destination_buffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; auto dc1394_source = bayer_source_buffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } // offsetX == 1 is handled in checkDebayer() and should be 0 here. error_code = dc1394_bayer_decoding_16bit(dc1394_source, bayer_destination_buffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method, 16); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_ImageBufferSize != rgb_size) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } m_ImageBufferSize = rgb_size; } auto bayered_buffer = reinterpret_cast(m_ImageBuffer); // Data in R1G1B1, we need to copy them into 3 layers for FITS uint16_t * rBuff = bayered_buffer; uint16_t * gBuff = bayered_buffer + (stats.width * stats.height); uint16_t * bBuff = bayered_buffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = bayer_destination_buffer[i]; *gBuff++ = bayer_destination_buffer[i + 1]; *bBuff++ = bayer_destination_buffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; return true; } double FITSData::getADU() const { double adu = 0; for (int i = 0; i < m_Channels; i++) adu += stats.mean[i]; return (adu / static_cast(m_Channels)); } -/* CannyDetector, Implementation of Canny edge detector in Qt/C++. - * Copyright (C) 2015 Gonzalo Exequiel Pedone - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Email : hipersayan DOT x AT gmail DOT com - * Web-Site: https://github.com/hipersayanX/CannyDetector - */ - -template -void FITSData::sobel(QVector &gradient, QVector &direction) -{ - //int size = image.width() * image.height(); - gradient.resize(stats.samples_per_channel); - direction.resize(stats.samples_per_channel); - - for (int y = 0; y < stats.height; y++) - { - size_t yOffset = y * stats.width; - const T * grayLine = reinterpret_cast(m_ImageBuffer) + yOffset; - - const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width; - const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width; - - float * gradientLine = gradient.data() + yOffset; - float * directionLine = direction.data() + yOffset; - - for (int x = 0; x < stats.width; x++) - { - int x_m1 = x < 1 ? x : x - 1; - int x_p1 = x >= stats.width - 1 ? x : x + 1; - - int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; - - int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; - - gradientLine[x] = qAbs(gradX) + qAbs(gradY); - - /* Gradient directions are classified in 4 possible cases - * - * dir 0 - * - * x x x - * - - - - * x x x - * - * dir 1 - * - * x x / - * x / x - * / x x - * - * dir 2 - * - * \ x x - * x \ x - * x x \ - * - * dir 3 - * - * x | x - * x | x - * x | x - */ - if (gradX == 0 && gradY == 0) - directionLine[x] = 0; - else if (gradX == 0) - directionLine[x] = 3; - else - { - qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; - - if (a >= -22.5 && a < 22.5) - directionLine[x] = 0; - else if (a >= 22.5 && a < 67.5) - directionLine[x] = 2; - else if (a >= -67.5 && a < -22.5) - directionLine[x] = 1; - else - directionLine[x] = 3; - } - } - } -} - -int FITSData::partition(int width, int height, QVector &gradient, QVector &ids) -{ - int id = 0; - - for (int y = 1; y < height - 1; y++) - { - for (int x = 1; x < width - 1; x++) - { - int index = x + y * width; - float val = gradient[index]; - if (val > 0 && ids[index] == 0) - { - trace(width, height, ++id, gradient, ids, x, y); - } - } - } - - // Return max id - return id; -} - -void FITSData::trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) -{ - int yOffset = y * width; - float * cannyLine = image.data() + yOffset; - int * idLine = ids.data() + yOffset; - - if (idLine[x] != 0) - return; - - idLine[x] = id; - - for (int j = -1; j < 2; j++) - { - int nextY = y + j; - - if (nextY < 0 || nextY >= height) - continue; - - float * cannyLineNext = cannyLine + j * width; - - for (int i = -1; i < 2; i++) - { - int nextX = x + i; - - if (i == j || nextX < 0 || nextX >= width) - continue; - - if (cannyLineNext[nextX] > 0) - { - // Trace neighbors. - trace(width, height, id, image, ids, nextX, nextY); - } - } - } -} - QString FITSData::getLastError() const { return lastError; } bool FITSData::getAutoRemoveTemporaryFITS() const { return autoRemoveTemporaryFITS; } void FITSData::setAutoRemoveTemporaryFITS(bool value) { autoRemoveTemporaryFITS = value; } template void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-align" auto * buffer = (T *)getImageBuffer(); #pragma GCC diagnostic pop const T limit = std::numeric_limits::max(); T bMin = dataMin < 0 ? 0 : dataMin; T bMax = dataMax > limit ? limit : dataMax; uint16_t w = width(); uint16_t h = height(); uint32_t size = w * h; double val; if (channels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { unsigned char * scanLine = image.scanLine(j); for (int i = 0; i < w; i++) { val = qBound(bMin, buffer[j * w + i], bMax); val = val * scale + zero; scanLine[i] = qBound(0, (unsigned char)val, 255); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { auto * scanLine = reinterpret_cast((image.scanLine(j))); for (int i = 0; i < w; i++) { rval = qBound(bMin, buffer[j * w + i], bMax); gval = qBound(bMin, buffer[j * w + i + size], bMax); bval = qBound(bMin, buffer[j * w + i + size * 2], bMax); value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero); scanLine[i] = value; } } } } QImage FITSData::FITSToImage(const QString &filename) { QImage fitsImage; double min, max; FITSData data; QFuture future = data.loadFITS(filename); // Wait synchronously future.waitForFinished(); if (future.result() == false) return fitsImage; data.getMinMax(&min, &max); if (min == max) { fitsImage.fill(Qt::white); return fitsImage; } if (data.channels() == 1) { fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8); fitsImage.setColorCount(256); for (int i = 0; i < 256; i++) fitsImage.setColor(i, qRgb(i, i, i)); } else { fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32); } double dataMin = data.stats.mean[0] - data.stats.stddev[0]; double dataMax = data.stats.mean[0] + data.stats.stddev[0] * 3; double bscale = 255. / (dataMax - dataMin); double bzero = (-dataMin) * (255. / (dataMax - dataMin)); // Long way to do this since we do not want to use templated functions here switch (data.property("dataType").toInt()) { case TBYTE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TUSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TULONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TFLOAT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONGLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TDOUBLE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; default: break; } return fitsImage; } bool FITSData::ImageToFITS(const QString &filename, const QString &format, QString &output) { if (QImageReader::supportedImageFormats().contains(format.toLatin1()) == false) { qCCritical(KSTARS_FITS) << "Failed to convert" << filename << "to FITS since format" << format << "is not supported in Qt"; return false; } QImage input; if (input.load(filename, format.toLatin1()) == false) { qCCritical(KSTARS_FITS) << "Failed to open image" << filename; return false; } output = QString(KSPaths::writableLocation(QStandardPaths::TempLocation) + QFileInfo(filename).fileName() + ".fits"); //This section sets up the FITS File fitsfile *fptr = nullptr; int status = 0; long fpixel = 1, naxis = input.allGray() ? 2 : 3, nelements, exposure; long naxes[3] = { input.width(), input.height(), naxis == 3 ? 3 : 1 }; char error_status[512] = {0}; if (fits_create_file(&fptr, QString('!' + output).toLatin1().data(), &status)) { fits_get_errstatus(status, error_status); qCCritical(KSTARS_FITS) << "Failed to create FITS file. Error:" << error_status; return false; } if (fits_create_img(fptr, BYTE_IMG, naxis, naxes, &status)) { qCWarning(KSTARS_FITS) << "fits_create_img failed:" << error_status; status = 0; fits_flush_file(fptr, &status); fits_close_file(fptr, &status); return false; } exposure = 1; fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status); // Gray image if (naxis == 2) { nelements = naxes[0] * naxes[1]; if (fits_write_img(fptr, TBYTE, fpixel, nelements, input.bits(), &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_write_img GRAY failed:" << error_status; status = 0; fits_flush_file(fptr, &status); fits_close_file(fptr, &status); return false; } } // RGB image, we have to convert from ARGB format to R G B for each plane else { nelements = naxes[0] * naxes[1] * 3; uint8_t *srcBuffer = input.bits(); // ARGB uint32_t srcBytes = naxes[0] * naxes[1] * 4 - 4; uint8_t *rgbBuffer = new uint8_t[nelements]; if (rgbBuffer == nullptr) { qCWarning(KSTARS_FITS) << "Not enough memory for RGB buffer"; fits_flush_file(fptr, &status); fits_close_file(fptr, &status); return false; } uint8_t *subR = rgbBuffer; uint8_t *subG = rgbBuffer + naxes[0] * naxes[1]; uint8_t *subB = rgbBuffer + naxes[0] * naxes[1] * 2; for (uint32_t i = 0; i < srcBytes; i += 4) { *subB++ = srcBuffer[i]; *subG++ = srcBuffer[i + 1]; *subR++ = srcBuffer[i + 2]; } if (fits_write_img(fptr, TBYTE, fpixel, nelements, rgbBuffer, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_write_img RGB failed:" << error_status; status = 0; fits_flush_file(fptr, &status); fits_close_file(fptr, &status); delete [] rgbBuffer; return false; } delete [] rgbBuffer; } if (fits_flush_file(fptr, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_flush_file failed:" << error_status; status = 0; fits_close_file(fptr, &status); return false; } if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_close_file failed:" << error_status; return false; } return true; } #if 0 bool FITSData::injectWCS(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale) { int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; char errMsg[512]; qCInfo(KSTARS_FITS) << "Creating new WCS file:" << newWCSFile << "with parameters Orientation:" << orientation << "RA:" << ra << "DE:" << dec << "Pixel Scale:" << pixscale; nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, QString('!' + newWCSFile).toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_copy_file(fptr, new_fptr, 1, 1, 1, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } status = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; qCDebug(KSTARS_FITS) << "Removing FITS File: " << m_Filename; } m_Filename = newWCSFile; m_isTemporary = true; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); // ISO Date if (fits_write_date(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_flush_file(fptr, &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished creating WCS file: " << newWCSFile; return true; } #endif bool FITSData::injectWCS(double orientation, double ra, double dec, double pixscale) { int status = 0; fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished update WCS info."; return true; } bool FITSData::contains(const QPointF &point) const { return (point.x() >= 0 && point.y() >= 0 && point.x() <= stats.width && point.y() <= stats.height); } -int FITSData::findSEPStars(const QRect &boundary) -{ - int x = 0, y = 0, w = stats.width, h = stats.height, maxRadius = 50; - - if (!boundary.isNull()) - { - x = boundary.x(); - y = boundary.y(); - w = boundary.width(); - h = boundary.height(); - maxRadius = w; - } - - auto * data = new float[w * h]; - - switch (stats.bitpix) - { - case BYTE_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case SHORT_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case USHORT_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case LONG_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case ULONG_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case FLOAT_IMG: - delete [] data; - data = reinterpret_cast(m_ImageBuffer); - break; - case LONGLONG_IMG: - getFloatBuffer(data, x, y, w, h); - break; - case DOUBLE_IMG: - getFloatBuffer(data, x, y, w, h); - break; - default: - delete [] data; - return -1; - } - - float * imback = nullptr; - double * flux = nullptr, *fluxerr = nullptr, *area = nullptr; - short * flag = nullptr; - short flux_flag = 0; - int status = 0; - sep_bkg * bkg = nullptr; - sep_catalog * catalog = nullptr; - float conv[] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; - double flux_fractions[2] = {0}; - double requested_frac[2] = { 0.5, 0.99 }; - QList edges; - - // #0 Create SEP Image structure - sep_image im = {data, nullptr, nullptr, SEP_TFLOAT, 0, 0, w, h, 0.0, SEP_NOISE_NONE, 1.0, 0.0}; - - // #1 Background estimate - status = sep_background(&im, 64, 64, 3, 3, 0.0, &bkg); - if (status != 0) goto exit; - - // #2 Background evaluation - imback = (float *)malloc((w * h) * sizeof(float)); - status = sep_bkg_array(bkg, imback, SEP_TFLOAT); - if (status != 0) goto exit; - - // #3 Background subtraction - status = sep_bkg_subarray(bkg, im.data, im.dtype); - if (status != 0) goto exit; - - // #4 Source Extraction - // Note that we set deblend_cont = 1.0 to turn off deblending. - status = sep_extract(&im, 2 * bkg->globalrms, SEP_THRESH_ABS, 10, conv, 3, 3, SEP_FILTER_CONV, 32, 1.0, 1, 1.0, &catalog); - if (status != 0) goto exit; - - // TODO - // Must detect edge detection - // Must limit to brightest 100 (by flux) centers - // Should probably use ellipse to draw instead of simple circle? - // Useful for galaxies and also elenogated stars. - for (int i = 0; i < catalog->nobj; i++) - { - double flux = catalog->flux[i]; - // Get HFR - sep_flux_radius(&im, catalog->x[i], catalog->y[i], maxRadius, 5, 0, &flux, requested_frac, 2, flux_fractions, &flux_flag); - - auto * center = new Edge(); - center->x = catalog->x[i] + x + 0.5; - center->y = catalog->y[i] + y + 0.5; - center->val = catalog->peak[i]; - center->sum = flux; - center->HFR = center->width = flux_fractions[0]; - if (flux_fractions[1] < maxRadius) - center->width = flux_fractions[1] * 2; - edges.append(center); - } - - // Let's sort edges, starting with widest - std::sort(edges.begin(), edges.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->width > edge2->width;}); - - // Take only the first 100 stars - { - int starCount = qMin(100, edges.count()); - for (int i = 0; i < starCount; i++) - starCenters.append(edges[i]); - } - - edges.clear(); - - qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << "#" << "#X" << "#Y" << "#Flux" << "#Width" << "#HFR"; - for (int i = 0; i < starCenters.count(); i++) - qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << i << starCenters[i]->x << starCenters[i]->y - << starCenters[i]->sum << starCenters[i]->width << starCenters[i]->HFR; - -exit: - if (stats.bitpix != FLOAT_IMG) - delete [] data; - sep_bkg_free(bkg); - sep_catalog_free(catalog); - free(imback); - free(flux); - free(fluxerr); - free(area); - free(flag); - - if (status != 0) - { - char errorMessage[512]; - sep_get_errmsg(status, errorMessage); - qCritical(KSTARS_FITS) << errorMessage; - return -1; - } - - return starCenters.count(); -} - -template -void FITSData::getFloatBuffer(float * buffer, int x, int y, int w, int h) -{ - auto * rawBuffer = reinterpret_cast(m_ImageBuffer); - - float * floatPtr = buffer; - - int x2 = x + w; - int y2 = y + h; - - for (int y1 = y; y1 < y2; y1++) - { - int offset = y1 * stats.width; - for (int x1 = x; x1 < x2; x1++) - { - *floatPtr++ = rawBuffer[offset + x1]; - } - } -} - void FITSData::saveStatistics(Statistic &other) { other = stats; } void FITSData::restoreStatistics(Statistic &other) { stats = other; } diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h index d53f0e030..99684a4a0 100644 --- a/kstars/fitsviewer/fitsdata.h +++ b/kstars/fitsviewer/fitsdata.h @@ -1,569 +1,534 @@ /*************************************************************************** fitsimage.cpp - FITS Image ------------------- begin : Tue Feb 24 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "config-kstars.h" #include "bayer.h" #include "fitscommon.h" +#include "fitsstardetector.h" #ifdef WIN32 // This header must be included before fitsio.h to avoid compiler errors with Visual Studio #include #endif #include #include #include #include #include #ifndef KSTARS_LITE #include #ifdef HAVE_WCSLIB #include #endif #endif -#define MINIMUM_PIXEL_RANGE 5 -#define MINIMUM_STDVAR 5 - class QProgressDialog; class SkyObject; class SkyPoint; class FITSHistogram; typedef struct { float ra; float dec; } wcs_point; -class Edge -{ - public: - float x; - float y; - int val; - int scanned; - float width; - float HFR; - float sum; -}; +class Edge; class FITSSkyObject : public QObject { Q_OBJECT public: explicit FITSSkyObject(SkyObject *object, int xPos, int yPos); SkyObject *skyObject(); int x(); int y(); void setX(int xPos); void setY(int yPos); private: SkyObject *skyObjectStored; int xLoc; int yLoc; }; class FITSData : public QObject { Q_OBJECT // Name of FITS file Q_PROPERTY(QString filename READ filename) // Size of file in bytes Q_PROPERTY(qint64 size READ size) // Width in pixels Q_PROPERTY(quint16 width READ width) // Height in pixels Q_PROPERTY(quint16 height READ height) // FITS MODE --> Normal, Focus, Guide..etc Q_PROPERTY(FITSMode mode MEMBER m_Mode) // 1 channel (grayscale) or 3 channels (RGB) Q_PROPERTY(quint8 channels READ channels) // Data type (BYTE, SHORT, INT..etc) Q_PROPERTY(quint32 dataType MEMBER m_DataType) // Bits per pixel Q_PROPERTY(quint8 bpp READ bpp WRITE setBPP) // Does FITS have WSC header? Q_PROPERTY(bool hasWCS READ hasWCS) // Does FITS have bayer data? Q_PROPERTY(bool hasDebyaer READ hasDebayer) public: explicit FITSData(FITSMode fitsMode = FITS_NORMAL); explicit FITSData(const FITSData *other); ~FITSData(); /** Structure to hold FITS Header records */ typedef struct { QString key; /** FITS Header Key */ QVariant value; /** FITS Header Value */ QString comment; /** FITS Header Comment, if any */ } Record; /// Stats struct to hold statisical data about the FITS data typedef struct { double min[3] = {0}, max[3] = {0}; double mean[3] = {0}; double stddev[3] = {0}; double median[3] = {0}; double SNR { 0 }; int bitpix { 8 }; int bytesPerPixel { 1 }; int ndim { 2 }; int64_t size { 0 }; uint32_t samples_per_channel { 0 }; uint16_t width { 0 }; uint16_t height { 0 }; } Statistic; /** * @brief loadFITS Loading FITS file asynchronously. * @param inFilename Path to FITS file (or compressed fits.gz) * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return A QFuture that can be watched until the async operation is complete. */ QFuture loadFITS(const QString &inFilename, bool silent = true); /** * @brief loadFITSFromMemory Loading FITS from memory buffer. * @param inFilename Potential future path to FITS file (or compressed fits.gz), stored in a fitsdata class variable * @param fits_buffer The memory buffer containing the fits data. * @param fits_buffer_size The size in bytes of the buffer. * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return bool indicating success or failure. */ bool loadFITSFromMemory(const QString &inFilename, void *fits_buffer, size_t fits_buffer_size, bool silent); /* Save FITS */ int saveFITS(const QString &newFilename); /* Rescale image lineary from image_buffer, fit to window if desired */ int rescale(FITSZoom type); /* Calculate stats */ void calculateStats(bool refresh = false); /* Check if a particular point exists within the image */ bool contains(const QPointF &point) const; // Access functions void clearImageBuffers(); void setImageBuffer(uint8_t *buffer); - uint8_t *getImageBuffer(); + uint8_t const *getImageBuffer() const; + uint8_t *getWritableImageBuffer(); // Statistics void saveStatistics(Statistic &other); void restoreStatistics(Statistic &other); + Statistic const &getStatistics() const { return stats; }; uint16_t width() const { return stats.width; } uint16_t height() const { return stats.height; } int64_t size() const { return stats.size; } int channels() const { return m_Channels; } double getMin(uint8_t channel = 0) const { return stats.min[channel]; } double getMax(uint8_t channel = 0) const { return stats.max[channel]; } void setMinMax(double newMin, double newMax, uint8_t channel = 0); void getMinMax(double *min, double *max, uint8_t channel = 0) const { *min = stats.min[channel]; *max = stats.max[channel]; } void setStdDev(double value, uint8_t channel = 0) { stats.stddev[channel] = value; } double getStdDev(uint8_t channel = 0) const { return stats.stddev[channel]; } void setMean(double value, uint8_t channel = 0) { stats.mean[channel] = value; } double getMean(uint8_t channel = 0) const { return stats.mean[channel]; } void setMedian(double val, uint8_t channel = 0) { stats.median[channel] = val; } double getMedian(uint8_t channel = 0) const { return stats.median[channel]; } int getBytesPerPixel() const { return stats.bytesPerPixel; } void setSNR(double val) { stats.SNR = val; } double getSNR() const { return stats.SNR; } void setBPP(uint8_t value) { stats.bitpix = value; } uint32_t bpp() const { return stats.bitpix; } double getADU() const; // FITS Record bool getRecordValue(const QString &key, QVariant &value) const; const QList &getRecords() const { return records; } // Star Detection - Native KStars implementation void setStarAlgorithm(StarAlgorithm algorithm) { starAlgorithm = algorithm; } int getDetectedStars() const { return starCenters.count(); } bool areStarsSearched() const { return starsSearched; } void appendStar(Edge *newCenter) { starCenters.append(newCenter); } QList getStarCenters() const { return starCenters; } QList getStarCentersInSubFrame(QRect subFrame) const; int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &trackingBox = QRect()); - void getCenterSelection(int *x, int *y); - int findOneStar(const QRect &boundary); - - // Star Detection - Partially customized Canny edge detection algorithm - static int findCannyStar(FITSData *data, const QRect &boundary = QRect()); - template - static int findCannyStar(FITSData *data, const QRect &boundary); - // Use SEP (Sextractor Library) to find stars template - void getFloatBuffer(float *buffer, int x, int y, int w, int h); - int findSEPStars(const QRect &boundary = QRect()); + void getFloatBuffer(float *buffer, int x, int y, int w, int h) const; + int findSEPStars(QList&, const QRect &boundary = QRect()) const; // Apply ring filter to searched stars int filterStars(const float innerRadius, const float outerRadius); // Half Flux Radius Edge *getMaxHFRStar() const { return maxHFRStar; } double getHFR(HFRType type = HFR_AVERAGE); double getHFR(int x, int y); // WCS // Check if image has valid WCS header information and set HasWCS accordingly. Call in loadFITS() bool checkForWCS(); // Does image have valid WCS? bool hasWCS() { return HasWCS; } // Load WCS data bool loadWCS(); // Is WCS Image loaded? bool isWCSLoaded() { return WCSLoaded; } wcs_point *getWCSCoord() { return wcs_coord; } /** * @brief wcsToPixel Given J2000 (RA0,DE0) coordinates. Find in the image the corresponding pixel coordinates. * @param wcsCoord Coordinates of target * @param wcsPixelPoint Return XY FITS coordinates * @param wcsImagePoint Return XY Image coordinates * @return True if conversion is successful, false otherwise. */ bool wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint); /** * @brief pixelToWCS Convert Pixel coordinates to J2000 world coordinates * @param wcsPixelPoint Pixel coordinates in XY Image space. * @param wcsCoord Store back WCS world coordinate in wcsCoord * @return True if successful, false otherwise. */ bool pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord); /** * @brief injectWCS Add WCS keywords to file * @param orientation Solver orientation, degrees E of N. * @param ra J2000 Right Ascension * @param dec J2000 Declination * @param pixscale Pixel scale in arcsecs per pixel * @return True if file is successfully updated with WCS info. */ bool injectWCS(double orientation, double ra, double dec, double pixscale); // Debayer bool hasDebayer() { return HasDebayer; } bool debayer(); bool debayer_8bit(); bool debayer_16bit(); void getBayerParams(BayerParams *param); void setBayerParams(BayerParams *param); // Histogram #ifndef KSTARS_LITE void setHistogram(FITSHistogram *inHistogram) { histogram = inHistogram; } #endif // Filter void applyFilter(FITSScale type, uint8_t *image = nullptr, QVector *targetMin = nullptr, QVector *targetMax = nullptr); // Rotation counter. We keep count to rotate WCS keywords on save int getRotCounter() const; void setRotCounter(int value); // Filename const QString &filename() const { return m_Filename; } const QString &compressedFilename() const { return m_compressedFilename; } bool isTempFile() const { return m_isTemporary; } bool isCompressed() const { return m_isCompressed; } // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipHCounter() const; void setFlipHCounter(int value); // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipVCounter() const; void setFlipVCounter(int value); #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB void findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]); #endif #endif QList getSkyObjects(); QList objList; //Does this need to be public?? // Create autostretch image from FITS File static QImage FITSToImage(const QString &filename); /** * @brief ImageToFITS Convert an image file with supported format to a FITS file. * @param filename full path to filename without extension * @param format file extension. Supported formats are whatever supported by Qt (e.g. PNG, JPG,..etc) * @param output Output temporary file path. The created file is generated by the function and store in output. * @return True if conversion is successful, false otherwise. */ static bool ImageToFITS(const QString &filename, const QString &format, QString &output); bool getAutoRemoveTemporaryFITS() const; void setAutoRemoveTemporaryFITS(bool value); QString getLastError() const; signals: void converted(QImage); private: void loadCommon(const QString &inFilename); bool privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent); void rotWCSFITS(int angle, int mirror); - bool checkCollision(Edge *s1, Edge *s2); int calculateMinMax(bool refresh = false); bool checkDebayer(); void readWCSKeys(); // FITS Record bool parseHeader(); //int getFITSRecord(QString &recordList, int &nkeys); // Templated functions template bool debayer(); template bool rotFITS(int rotate, int mirror); // Apply Filter template void applyFilter(FITSScale type, uint8_t *targetImage, QVector * min = nullptr, QVector * max = nullptr); - // Star Detect - Centroid - template - int findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth); - int findCentroid(const QRect &boundary = QRect(), int initStdDev = MINIMUM_STDVAR, - int minEdgeWidth = MINIMUM_PIXEL_RANGE); - // Star Detect - Threshold - template - int findOneStar(const QRect &boundary); template void calculateMinMax(); template QPair getParitionMinMax(uint32_t start, uint32_t stride); /* Calculate running average & standard deviation using Welford’s method for computing variance */ template void runningAverageStdDev(); template QPair getSquaredSumAndMean(uint32_t start, uint32_t stride); - // Sobel detector by Gonzalo Exequiel Pedone - template - void sobel(QVector &gradient, QVector &direction); - template void convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image); - // Give unique IDs to each contiguous region - int partition(int width, int height, QVector &gradient, QVector &ids); - void trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y); - #ifndef KSTARS_LITE FITSHistogram *histogram { nullptr }; // Pointer to the FITS data histogram #endif /// Pointer to CFITSIO FITS file struct fitsfile *fptr { nullptr }; /// FITS image data type (TBYTE, TUSHORT, TINT, TFLOAT, TLONG, TDOUBLE) uint32_t m_DataType { 0 }; /// Number of channels uint8_t m_Channels { 1 }; /// Generic data image buffer uint8_t *m_ImageBuffer { nullptr }; /// Above buffer size in bytes uint32_t m_ImageBufferSize { 0 }; /// Is this a temporary file or one loaded from disk? bool m_isTemporary { false }; /// is this file compress (.fits.fz)? bool m_isCompressed { false }; /// Did we search for stars yet? bool starsSearched { false }; ///Star Selection Algorithm StarAlgorithm starAlgorithm { ALGORITHM_GRADIENT }; /// Do we have WCS keywords in this FITS data? bool HasWCS { false }; /// Is the image debayarable? bool HasDebayer { false }; /// Is WCS data loaded? bool WCSLoaded { false }; /// Do we need to mark stars for the user? bool markStars { false }; /// Our very own file name QString m_Filename, m_compressedFilename; /// FITS Mode (Normal, WCS, Guide, Focus..etc) FITSMode m_Mode; /// How many times the image was rotated? Useful for WCS keywords rotation on save. int rotCounter { 0 }; /// How many times the image was flipped horizontally? int flipHCounter { 0 }; /// How many times the image was flipped vertically? int flipVCounter { 0 }; /// Pointer to WCS coordinate data, if any. wcs_point *wcs_coord { nullptr }; /// WCS Struct struct wcsprm *m_wcs { nullptr }; int m_nwcs = 0; /// All the stars we detected, if any. QList starCenters; QList localStarCenters; /// The biggest fattest star in the image. Edge *maxHFRStar { nullptr }; //uint8_t *m_BayerBuffer { nullptr }; /// Bayer parameters BayerParams debayerParams; Statistic stats; // A list of header records QList records; /// Remove temporary files after closing bool autoRemoveTemporaryFITS { true }; QString lastError; static const QString m_TemporaryPath; }; diff --git a/kstars/fitsviewer/fitsgradientdetector.cpp b/kstars/fitsviewer/fitsgradientdetector.cpp new file mode 100644 index 000000000..91fcf6b0d --- /dev/null +++ b/kstars/fitsviewer/fitsgradientdetector.cpp @@ -0,0 +1,461 @@ +/*************************************************************************** + fitsgradientdetector.cpp - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#include + +#include "fits_debug.h" +#include "fitsgradientdetector.h" + +FITSStarDetector& FITSGradientDetector::configure(const QString &, const QVariant &) +{ + return *this; +} + +int FITSGradientDetector::findSources(QList &starCenters, const QRect &boundary) +{ + switch (parent()->property("dataType").toInt()) + { + case TBYTE: + return findSources(starCenters, boundary); + + case TSHORT: + return findSources(starCenters, boundary); + + case TUSHORT: + return findSources(starCenters, boundary); + + case TLONG: + return findSources(starCenters, boundary); + + case TULONG: + return findSources(starCenters, boundary); + + case TFLOAT: + return findSources(starCenters, boundary); + + case TLONGLONG: + return findSources(starCenters, boundary); + + case TDOUBLE: + return findSources(starCenters, boundary); + + default: + break; + } + + return 0; +} + +template +int FITSGradientDetector::findSources(QList &starCenters, const QRect &boundary) +{ + FITSData const * const data = reinterpret_cast(parent()); + + if (data == nullptr) + return 0; + + int subX = qMax(0, boundary.isNull() ? 0 : boundary.x()); + int subY = qMax(0, boundary.isNull() ? 0 : boundary.y()); + int subW = (boundary.isNull() ? data->width() : boundary.width()); + int subH = (boundary.isNull() ? data->height() : boundary.height()); + + int BBP = data->getBytesPerPixel(); + + uint16_t dataWidth = data->width(); + + // #1 Find offsets + uint32_t size = subW * subH; + uint32_t offset = subX + subY * dataWidth; + + // #2 Create new buffer + auto * buffer = new uint8_t[size * BBP]; + // If there is no offset, copy whole buffer in one go + if (offset == 0) + memcpy(buffer, data->getImageBuffer(), size * BBP); + else + { + uint8_t * dataPtr = buffer; + uint8_t const * origDataPtr = data->getImageBuffer(); + uint32_t lineOffset = 0; + // Copy data line by line + for (int height = subY; height < (subY + subH); height++) + { + lineOffset = (subX + height * dataWidth) * BBP; + memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP); + dataPtr += (subW * BBP); + } + } + + // #3 Create new FITSData to hold it + auto * boundedImage = new FITSData(); + FITSData::Statistic stats; + stats.width = subW; + stats.height = subH; + stats.bitpix = data->getStatistics().bitpix; + stats.bytesPerPixel = data->getStatistics().bytesPerPixel; + stats.samples_per_channel = size; + stats.ndim = 2; + boundedImage->restoreStatistics(stats); + + boundedImage->setProperty("dataType", parent()->property("dataType")); + + // #4 Set image buffer and calculate stats. + boundedImage->setImageBuffer(buffer); + + boundedImage->calculateStats(true); + + // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain + boundedImage->applyFilter(FITS_MEDIAN); + boundedImage->applyFilter(FITS_HIGH_CONTRAST); + + // #6 Perform Sobel to find gradients and their directions + QVector gradients; + QVector directions; + + // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed + // and discarded whenever necessary. It won't work on noisy images unless this is done. + sobel(boundedImage, gradients, directions); + + QVector ids(gradients.size()); + + int maxID = partition(subW, subH, gradients, ids); + + // Not needed anymore + delete boundedImage; + + if (maxID == 0) + return 0; + + typedef struct + { + float massX = 0; + float massY = 0; + float totalMass = 0; + } massInfo; + + QMap masses; + + // #7 Calculate center of mass for all detected regions + for (int y = 0; y < subH; y++) + { + for (int x = 0; x < subW; x++) + { + int index = x + y * subW; + + int regionID = ids[index]; + if (regionID > 0) + { + float pixel = gradients[index]; + + masses[regionID].totalMass += pixel; + masses[regionID].massX += x * pixel; + masses[regionID].massY += y * pixel; + } + } + } + + // Compare multiple masses, and only select the highest total mass one as the desired star + int maxRegionID = 1; + int maxTotalMass = masses[1].totalMass; + double totalMassRatio = 1e6; + for (auto key : masses.keys()) + { + massInfo oneMass = masses.value(key); + if (oneMass.totalMass > maxTotalMass) + { + totalMassRatio = oneMass.totalMass / maxTotalMass; + maxTotalMass = oneMass.totalMass; + maxRegionID = key; + } + } + + // If image has many regions and there is no significant relative center of mass then it's just noise and no stars + // are probably there above a useful threshold. + if (maxID > 10 && totalMassRatio < 1.5) + return 0; + + auto * center = new Edge; + center->width = -1; + center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5; + center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5; + center->HFR = 1; + + // Maximum Radius + int maxR = qMin(subW - 1, subH - 1) / 2; + + for (int r = maxR; r > 1; r--) + { + int pass = 0; + + for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0) + { + int testX = center->x + std::cos(theta) * r; + int testY = center->y + std::sin(theta) * r; + + // if out of bound, break; + if (testX < 0 || testX >= subW || testY < 0 || testY >= subH) + break; + + if (gradients[testX + testY * subW] > 0) + //if (thresholded[testX + testY * subW] > 0) + { + if (++pass >= 24) + { + center->width = r * 2; + // Break of outer loop + r = 0; + break; + } + } + } + } + + qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width; + + // If no stars were detected + if (center->width == -1) + { + delete center; + return 0; + } + + // 30% fuzzy + //center->width += center->width*0.3 * (running_threshold / threshold); + + double FSum = 0, HF = 0, TF = 0; + const double resolution = 1.0 / 20.0; + + int cen_y = qRound(center->y); + + double rightEdge = center->x + center->width / 2.0; + double leftEdge = center->x - center->width / 2.0; + + QVector subPixels; + subPixels.reserve(center->width / resolution); + + const T * origBuffer = reinterpret_cast(data->getImageBuffer()) + offset; + + for (double x = leftEdge; x <= rightEdge; x += resolution) + { + double slice = resolution * (origBuffer[static_cast(floor(x)) + cen_y * dataWidth]); + FSum += slice; + subPixels.append(slice); + } + + // Half flux + HF = FSum / 2.0; + + int subPixelCenter = (center->width / resolution) / 2; + + // Start from center + TF = subPixels[subPixelCenter]; + double lastTF = TF; + // Integrate flux along radius axis until we reach half flux + //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) + for (int k = 1; k < subPixelCenter; k++) + { + TF += subPixels[subPixelCenter + k]; + TF += subPixels[subPixelCenter - k]; + + if (TF >= HF) + { + // We overpassed HF, let's calculate from last TF how much until we reach HF + + // #1 Accurate calculation, but very sensitive to small variations of flux + center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; + + // #2 Less accurate calculation, but stable against small variations of flux + //center->HFR = (k - 1) * resolution; + break; + } + + lastTF = TF; + } + + // Correct center for subX and subY + center->x += subX; + center->y += subY; + + //data->appendStar(center); + starCenters.append(center); + + qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR; + + return 1; +} + +/* CannyDetector, Implementation of Canny edge detector in Qt/C++. + * Copyright (C) 2015 Gonzalo Exequiel Pedone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Email : hipersayan DOT x AT gmail DOT com + * Web-Site: https://github.com/hipersayanX/CannyDetector + */ + +template +void FITSGradientDetector::sobel(FITSData const *data, QVector &gradient, QVector &direction) const +{ + if (data == nullptr) + return; + + FITSData::Statistic const &stats = data->getStatistics(); + + //int size = image.width() * image.height(); + gradient.resize(stats.samples_per_channel); + direction.resize(stats.samples_per_channel); + + for (int y = 0; y < stats.height; y++) + { + size_t yOffset = y * stats.width; + const T * grayLine = reinterpret_cast(data->getImageBuffer()) + yOffset; + + const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width; + const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width; + + float * gradientLine = gradient.data() + yOffset; + float * directionLine = direction.data() + yOffset; + + for (int x = 0; x < stats.width; x++) + { + int x_m1 = x < 1 ? x : x - 1; + int x_p1 = x >= stats.width - 1 ? x : x + 1; + + int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - + 2 * grayLine[x_m1] - grayLine_p1[x_m1]; + + int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - + 2 * grayLine_p1[x] - grayLine_p1[x_p1]; + + gradientLine[x] = qAbs(gradX) + qAbs(gradY); + + /* Gradient directions are classified in 4 possible cases + * + * dir 0 + * + * x x x + * - - - + * x x x + * + * dir 1 + * + * x x / + * x / x + * / x x + * + * dir 2 + * + * \ x x + * x \ x + * x x \ + * + * dir 3 + * + * x | x + * x | x + * x | x + */ + if (gradX == 0 && gradY == 0) + directionLine[x] = 0; + else if (gradX == 0) + directionLine[x] = 3; + else + { + qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; + + if (a >= -22.5 && a < 22.5) + directionLine[x] = 0; + else if (a >= 22.5 && a < 67.5) + directionLine[x] = 2; + else if (a >= -67.5 && a < -22.5) + directionLine[x] = 1; + else + directionLine[x] = 3; + } + } + } +} + +int FITSGradientDetector::partition(int width, int height, QVector &gradient, QVector &ids) const +{ + int id = 0; + + for (int y = 1; y < height - 1; y++) + { + for (int x = 1; x < width - 1; x++) + { + int index = x + y * width; + float val = gradient[index]; + if (val > 0 && ids[index] == 0) + { + trace(width, height, ++id, gradient, ids, x, y); + } + } + } + + // Return max id + return id; +} + +void FITSGradientDetector::trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) const +{ + int yOffset = y * width; + float * cannyLine = image.data() + yOffset; + int * idLine = ids.data() + yOffset; + + if (idLine[x] != 0) + return; + + idLine[x] = id; + + for (int j = -1; j < 2; j++) + { + int nextY = y + j; + + if (nextY < 0 || nextY >= height) + continue; + + float * cannyLineNext = cannyLine + j * width; + + for (int i = -1; i < 2; i++) + { + int nextX = x + i; + + if (i == j || nextX < 0 || nextX >= width) + continue; + + if (cannyLineNext[nextX] > 0) + { + // Trace neighbors. + trace(width, height, id, image, ids, nextX, nextY); + } + } + } +} diff --git a/kstars/fitsviewer/fitsgradientdetector.h b/kstars/fitsviewer/fitsgradientdetector.h new file mode 100644 index 000000000..d35536a3c --- /dev/null +++ b/kstars/fitsviewer/fitsgradientdetector.h @@ -0,0 +1,76 @@ +/*************************************************************************** + fitsgradientdetector.h - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#ifndef FITSGRADIENTDETECTOR_H +#define FITSGRADIENTDETECTOR_H + +#include "fitsstardetector.h" + +class FITSGradientDetector: public FITSStarDetector +{ + Q_OBJECT + +public: + explicit FITSGradientDetector(FITSData *parent): FITSStarDetector(parent) {}; + +public: + /** @brief Find sources in the parent FITS data file. + * @see FITSStarDetector::findSources(). + */ + int findSources(QList &starCenters, QRect const &boundary = QRect()) override; + + /** @brief Configure the detection method. + * @see FITSStarDetector::configure(). + * @note No parameters are currently available for configuration. + */ + FITSStarDetector & configure(const QString &setting, const QVariant &value) override; + +protected: + /** @internal Find sources in the parent FITS data file, dependent of the pixel depth. + * @see FITSGradientDetector::findSources. + */ + template + int findSources(QList &starCenters, const QRect &boundary); + + /** @internal Implementation of the Canny Edge detection (CannyEdgeDetector). + * @copyright 2015 Gonzalo Exequiel Pedone (https://github.com/hipersayanX/CannyDetector). + * @param data is the FITS data to run the detection onto. + * @param gradient is the vector storing the amount of change in pixel sequences. + * @param direction is the vector storing the four directions (horizontal, vertical and two diagonals) the changes stored in 'gradient' are detected in. + */ + template + void sobel(FITSData const * data, QVector &gradient, QVector &direction) const; + + /** @internal Identify gradient connections. + * @param width, height are the dimensions of the frame to work on. + * @param gradient is the vector holding the amount of change in pixel sequences. + * @param ids is the vector storing which gradient was identified for each pixel. + */ + int partition(int width, int height, QVector &gradient, QVector &ids) const; + + /** @internal Trace gradient neighbors. + * @param width, height are the dimensions of the frame to work on. + * @param image is the image to work on, actually gradients extracted using the sobel algorithm. + * @param ids is the vector storing which gradient was identified for each pixel. + * @param x, y locate the pixel to trace from. + */ + void trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) const; +}; + +#endif // FITSGRADIENTDETECTOR_H diff --git a/kstars/fitsviewer/fitshistogram.cpp b/kstars/fitsviewer/fitshistogram.cpp index 5bb32cd86..5f9e808e8 100644 --- a/kstars/fitsviewer/fitshistogram.cpp +++ b/kstars/fitsviewer/fitshistogram.cpp @@ -1,849 +1,849 @@ /* FITS Histogram Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.com) 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 "fitshistogram.h" #include "fits_debug.h" #include "Options.h" #include "fitsdata.h" #include "fitstab.h" #include "fitsview.h" #include "fitsviewer.h" #include #include #include #include histogramUI::histogramUI(QDialog * parent) : QDialog(parent) { setupUi(parent); setModal(false); } FITSHistogram::FITSHistogram(QWidget * parent) : QDialog(parent) { ui = new histogramUI(this); tab = dynamic_cast(parent); customPlot = ui->histogramPlot; customPlot->setBackground(QBrush(Qt::black)); customPlot->xAxis->setBasePen(QPen(Qt::white, 1)); customPlot->yAxis->setBasePen(QPen(Qt::white, 1)); customPlot->xAxis->setTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setTickLabelColor(Qt::white); customPlot->yAxis->setTickLabelColor(Qt::white); customPlot->xAxis->setLabelColor(Qt::white); customPlot->yAxis->setLabelColor(Qt::white); // Reserve 3 channels cumulativeFrequency.resize(3); intensity.resize(3); frequency.resize(3); FITSMin.fill(0, 3); FITSMax.fill(0, 3); binWidth.fill(0, 3); rgbWidgets.resize(3); rgbWidgets[RED_CHANNEL] << ui->RLabel << ui->minREdit << ui->redSlider << ui->maxREdit; rgbWidgets[GREEN_CHANNEL] << ui->GLabel << ui->minGEdit << ui->greenSlider << ui->maxGEdit; rgbWidgets[BLUE_CHANNEL] << ui->BLabel << ui->minBEdit << ui->blueSlider << ui->maxBEdit; minBoxes << ui->minREdit << ui->minGEdit << ui->minBEdit; maxBoxes << ui->maxREdit << ui->maxGEdit << ui->maxBEdit; sliders << ui->redSlider << ui->greenSlider << ui->blueSlider; customPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); customPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); connect(ui->applyB, &QPushButton::clicked, this, &FITSHistogram::applyScale); connect(ui->hideSaturated, &QCheckBox::stateChanged, [this]() { constructHistogram(); }); // connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), this, // SLOT(checkRangeLimit(QCPRange))); connect(customPlot, &QCustomPlot::mouseMove, this, &FITSHistogram::driftMouseOverLine); for (int i = 0; i < 3; i++) { // Box --> Slider QVector w = rgbWidgets[i]; connect(qobject_cast(w[1]), &QDoubleSpinBox::editingFinished, [this, i, w]() { double value = qobject_cast(w[1])->value(); w[2]->blockSignals(true); qobject_cast(w[2])->setMinimumPosition((value - FITSMin[i])*sliderScale[i]); w[2]->blockSignals(false); }); connect(qobject_cast(w[3]), &QDoubleSpinBox::editingFinished, [this, i, w]() { double value = qobject_cast(w[3])->value(); w[2]->blockSignals(true); qobject_cast(w[2])->setMaximumPosition((value - FITSMin[i] - sliderTick[i])*sliderScale[i]); w[2]->blockSignals(false); }); // Slider --> Box connect(qobject_cast(w[2]), &ctkRangeSlider::minimumValueChanged, [this, i, w](int position) { qobject_cast(w[1])->setValue(FITSMin[i] + (position / sliderScale[i])); }); connect(qobject_cast(w[2]), &ctkRangeSlider::maximumValueChanged, [this, i, w](int position) { qobject_cast(w[3])->setValue(FITSMin[i] + sliderTick[i] + (position / sliderScale[i])); }); } } void FITSHistogram::showEvent(QShowEvent * event) { Q_UNUSED(event) if (!m_Constructed) constructHistogram(); syncGUI(); } void FITSHistogram::constructHistogram() { FITSData * imageData = tab->getView()->getImageData(); isGUISynced = false; switch (imageData->property("dataType").toInt()) { case TBYTE: constructHistogram(); break; case TSHORT: constructHistogram(); break; case TUSHORT: constructHistogram(); break; case TLONG: constructHistogram(); break; case TULONG: constructHistogram(); break; case TFLOAT: constructHistogram(); break; case TLONGLONG: constructHistogram(); break; case TDOUBLE: constructHistogram(); break; default: break; } m_Constructed = true; if (isVisible()) syncGUI(); } template void FITSHistogram::constructHistogram() { FITSData * imageData = tab->getView()->getImageData(); uint16_t width = imageData->width(), height = imageData->height(); uint8_t channels = imageData->channels(); - auto * buffer = reinterpret_cast(imageData->getImageBuffer()); + auto * const buffer = reinterpret_cast(imageData->getImageBuffer()); double min, max; for (int i = 0 ; i < 3; i++) { imageData->getMinMax(&min, &max, i); FITSMin[i] = min; FITSMax[i] = max; } uint32_t samples = width * height; const uint32_t sampleBy = samples > 1000000 ? samples / 1000000 : 1; //binCount = static_cast(sqrt(samples)); binCount = qMin(FITSMax[0] - FITSMin[0], 400.0); if (binCount <= 0) binCount = 400; for (int n = 0; n < channels; n++) { intensity[n].fill(0, binCount); frequency[n].fill(0, binCount); cumulativeFrequency[n].fill(0, binCount); binWidth[n] = (FITSMax[n] - FITSMin[n]) / (binCount - 1); // Initialize the median to 0 in case the computation below fails. imageData->setMedian(0, n); } QVector> futures; for (int n = 0; n < channels; n++) { futures.append(QtConcurrent::run([ = ]() { for (int i = 0; i < binCount; i++) intensity[n][i] = FITSMin[n] + (binWidth[n] * i); })); } for (int n = 0; n < channels; n++) { futures.append(QtConcurrent::run([ = ]() { uint32_t offset = n * samples; for (uint32_t i = 0; i < samples; i += sampleBy) { int32_t id = rint((buffer[i + offset] - FITSMin[n]) / binWidth[n]); if (id < 0) id = 0; frequency[n][id] += sampleBy; } })); } for (QFuture future : futures) future.waitForFinished(); futures.clear(); for (int n = 0; n < channels; n++) { futures.append(QtConcurrent::run([ = ]() { uint32_t accumulator = 0; for (int i = 0; i < binCount; i++) { accumulator += frequency[n][i]; cumulativeFrequency[n].replace(i, accumulator); } })); } for (QFuture future : futures) future.waitForFinished(); futures.clear(); for (int n = 0; n < channels; n++) { futures.append(QtConcurrent::run([ = ]() { double median[3] = {0}; const bool cutoffSpikes = ui->hideSaturated->isChecked(); const uint32_t halfCumulative = cumulativeFrequency[n][binCount - 1] / 2; // Find which bin contains the median. int median_bin = -1; for (int i = 0; i < binCount; i++) { if (cumulativeFrequency[n][i] > halfCumulative) { median_bin = i; break; } } if (median_bin >= 0) { // The number of items in the median bin const uint32_t median_bin_size = frequency[n][median_bin] / sampleBy; if (median_bin_size > 0) { // The median is this element inside the sorted median_bin; const uint32_t samples_before_median_bin = median_bin == 0 ? 0 : cumulativeFrequency[n][median_bin - 1]; uint32_t median_position = (halfCumulative - samples_before_median_bin) / sampleBy; if (median_position >= median_bin_size) median_position = median_bin_size - 1; if (median_position >= 0 && median_position < median_bin_size) { // Fill a vector with the values in the median bin (sampled by sampleBy). std::vector median_bin_samples(median_bin_size); uint32_t upto = 0; const uint32_t offset = n * samples; for (uint32_t i = 0; i < samples; i += sampleBy) { if (upto >= median_bin_size) break; const int32_t id = rint((buffer[i + offset] - FITSMin[n]) / binWidth[n]); if (id == median_bin) median_bin_samples[upto++] = buffer[i + offset]; } // Get the Nth value using N = the median position. if (upto > 0) { if (median_position >= upto) median_position = upto - 1; std::nth_element(median_bin_samples.begin(), median_bin_samples.begin() + median_position, median_bin_samples.begin() + upto); median[n] = median_bin_samples[median_position]; } } } } imageData->setMedian(median[n], n); if (cutoffSpikes) { QVector sortedFreq = frequency[n]; std::sort(sortedFreq.begin(), sortedFreq.end()); double cutoff = sortedFreq[binCount * 0.99]; for (int i = 0; i < binCount; i++) { if (frequency[n][i] >= cutoff) frequency[n][i] = cutoff; } } })); } for (QFuture future : futures) future.waitForFinished(); // Custom index to indicate the overall contrast of the image if (cumulativeFrequency[RED_CHANNEL][binCount / 4] > 0) JMIndex = cumulativeFrequency[RED_CHANNEL][binCount / 8] / cumulativeFrequency[RED_CHANNEL][binCount / 4]; else JMIndex = 1; qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex; sliderTick.clear(); sliderScale.clear(); for (int n = 0; n < channels; n++) { sliderTick << fabs(FITSMax[n] - FITSMin[n]) / 99.0; sliderScale << 99.0 / (FITSMax[n] - FITSMin[n] - sliderTick[n]); } } void FITSHistogram::syncGUI() { if (isGUISynced) return; FITSData * imageData = tab->getView()->getImageData(); bool isColor = imageData->channels() > 1; // R/K is always enabled for (auto w : rgbWidgets[RED_CHANNEL]) w->setEnabled(true); // G Channel for (auto w : rgbWidgets[GREEN_CHANNEL]) w->setEnabled(isColor); // B Channel for (auto w : rgbWidgets[BLUE_CHANNEL]) w->setEnabled(isColor); ui->meanEdit->setText(QString::number(imageData->getMean())); ui->medianEdit->setText(QString::number(imageData->getMedian())); for (int n = 0; n < imageData->channels(); n++) { double median = imageData->getMedian(n); if (median > 100) numDecimals << 0; else if (median > 1) numDecimals << 2; else if (median > .01) numDecimals << 4; else if (median > .0001) numDecimals << 6; else numDecimals << 10; minBoxes[n]->setDecimals(numDecimals[n]); minBoxes[n]->setSingleStep(fabs(FITSMax[n] - FITSMin[n]) / 20.0); minBoxes[n]->setMinimum(FITSMin[n]); minBoxes[n]->setMaximum(FITSMax[n] - sliderTick[n]); minBoxes[n]->setValue(FITSMin[n] + (sliders[n]->minimumValue() / sliderScale[n])); maxBoxes[n]->setDecimals(numDecimals[n]); maxBoxes[n]->setSingleStep(fabs(FITSMax[n] - FITSMin[n]) / 20.0); maxBoxes[n]->setMinimum(FITSMin[n] + sliderTick[n]); maxBoxes[n]->setMaximum(FITSMax[n]); maxBoxes[n]->setValue(FITSMin[n] + sliderTick[n] + (sliders[n]->maximumValue() / sliderScale[n])); } customPlot->clearGraphs(); graphs.clear(); for (int n = 0; n < imageData->channels(); n++) { graphs.append(customPlot->addGraph()); graphs[n]->setData(intensity[n], frequency[n]); } graphs[RED_CHANNEL]->setBrush(QBrush(QColor(170, 40, 80))); graphs[RED_CHANNEL]->setPen(QPen(Qt::red)); if (isColor) { graphs[GREEN_CHANNEL]->setBrush(QBrush(QColor(80, 40, 170))); graphs[GREEN_CHANNEL]->setPen(QPen(Qt::green)); graphs[BLUE_CHANNEL]->setBrush(QBrush(QColor(170, 40, 80))); graphs[BLUE_CHANNEL]->setPen(QPen(Qt::blue)); } customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal); customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal); customPlot->xAxis->setLabel(i18n("Intensity")); customPlot->yAxis->setLabel(i18n("Frequency")); // customPlot->xAxis->setRange(fits_min - ui->minEdit->singleStep(), // fits_max + ui->maxEdit->singleStep()); customPlot->xAxis->rescale(); customPlot->yAxis->rescale(); customPlot->setInteraction(QCP::iRangeDrag, true); customPlot->setInteraction(QCP::iRangeZoom, true); customPlot->setInteraction(QCP::iSelectPlottables, true); customPlot->replot(); resizePlot(); isGUISynced = true; } void FITSHistogram::resizePlot() { if (!m_Constructed) constructHistogram(); if (customPlot->width() < 300) customPlot->yAxis->setTickLabels(false); else customPlot->yAxis->setTickLabels(true); customPlot->xAxis->ticker()->setTickCount(customPlot->width() / 100); } double FITSHistogram::getJMIndex() const { return JMIndex; } void FITSHistogram::applyScale() { QVector min, max; min << minBoxes[0]->value() << minBoxes[1]->value() << minBoxes[2]->value(); max << maxBoxes[0]->value() << maxBoxes[1]->value() << maxBoxes[2]->value(); FITSHistogramCommand * histC; if (ui->logR->isChecked()) type = FITS_LOG; else type = FITS_LINEAR; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } void FITSHistogram::applyFilter(FITSScale ftype) { QVector min, max; min.append(ui->minREdit->value()); FITSHistogramCommand * histC; type = ftype; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } QVector FITSHistogram::getCumulativeFrequency(int channel) const { return cumulativeFrequency[channel]; } FITSHistogramCommand::FITSHistogramCommand(QWidget * parent, FITSHistogram * inHisto, FITSScale newType, const QVector &lmin, const QVector &lmax) { tab = dynamic_cast(parent); type = newType; histogram = inHisto; min = lmin; max = lmax; } FITSHistogramCommand::~FITSHistogramCommand() { delete[] delta; } bool FITSHistogramCommand::calculateDelta(const uint8_t * buffer) { FITSData * imageData = tab->getView()->getImageData(); - uint8_t * image_buffer = imageData->getImageBuffer(); + uint8_t const * image_buffer = imageData->getImageBuffer(); int totalPixels = imageData->width() * imageData->height() * imageData->channels(); unsigned long totalBytes = totalPixels * imageData->getBytesPerPixel(); auto * raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { qWarning() << "Error! not enough memory to create image delta" << endl; return false; } for (unsigned int i = 0; i < totalBytes; i++) raw_delta[i] = buffer[i] ^ image_buffer[i]; compressedBytes = sizeof(uint8_t) * totalBytes + totalBytes / 64 + 16 + 3; delete[] delta; delta = new uint8_t[compressedBytes]; if (delta == nullptr) { delete[] raw_delta; qCCritical(KSTARS_FITS) << "FITSHistogram Error: Ran out of memory compressing delta"; return false; } int r = compress2(delta, &compressedBytes, raw_delta, totalBytes, 5); if (r != Z_OK) { delete[] raw_delta; /* this should NEVER happen */ qCCritical(KSTARS_FITS) << "FITSHistogram Error: Failed to compress raw_delta"; return false; } // qDebug() << "compressed bytes size " << compressedBytes << " bytes" << // endl; delete[] raw_delta; return true; } bool FITSHistogramCommand::reverseDelta() { FITSView * image = tab->getView(); FITSData * imageData = image->getImageData(); - uint8_t * image_buffer = (imageData->getImageBuffer()); + uint8_t const * image_buffer = (imageData->getImageBuffer()); int totalPixels = imageData->width() * imageData->height() * imageData->channels(); unsigned long totalBytes = totalPixels * imageData->getBytesPerPixel(); auto * output_image = new uint8_t[totalBytes]; if (output_image == nullptr) { qWarning() << "Error! not enough memory to create output image" << endl; return false; } auto * raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { delete[] output_image; qWarning() << "Error! not enough memory to create image delta" << endl; return false; } int r = uncompress(raw_delta, &totalBytes, delta, compressedBytes); if (r != Z_OK) { qCCritical(KSTARS_FITS) << "FITSHistogram compression error in reverseDelta()"; delete[] output_image; delete[] raw_delta; return false; } for (unsigned int i = 0; i < totalBytes; i++) output_image[i] = raw_delta[i] ^ image_buffer[i]; imageData->setImageBuffer(output_image); delete[] raw_delta; return true; } void FITSHistogramCommand::redo() { FITSView * image = tab->getView(); FITSData * imageData = image->getImageData(); - uint8_t * image_buffer = imageData->getImageBuffer(); + uint8_t const * image_buffer = imageData->getImageBuffer(); uint8_t * buffer = nullptr; unsigned int size = imageData->width() * imageData->height() * imageData->channels(); int BBP = imageData->getBytesPerPixel(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) { FITSData::Statistic prevStats; imageData->saveStatistics(prevStats); reverseDelta(); imageData->restoreStatistics(stats); stats = prevStats; } else { imageData->saveStatistics(stats); // If it's rotation of flip, no need to calculate delta if (type >= FITS_ROTATE_CW && type <= FITS_FLIP_V) { - imageData->applyFilter(type, image_buffer); + imageData->applyFilter(type); } else { buffer = new uint8_t[size * BBP]; if (buffer == nullptr) { qWarning() << "Error! not enough memory to create image buffer in redo()" << endl; QApplication::restoreOverrideCursor(); return; } memcpy(buffer, image_buffer, size * BBP); QVector dataMin = min, dataMax = max; switch (type) { case FITS_AUTO: case FITS_LINEAR: imageData->applyFilter(FITS_LINEAR, nullptr, &dataMin, &dataMax); break; case FITS_LOG: imageData->applyFilter(FITS_LOG, nullptr, &dataMin, &dataMax); break; case FITS_SQRT: imageData->applyFilter(FITS_SQRT, nullptr, &dataMin, &dataMax); break; default: imageData->applyFilter(type); break; } calculateDelta(buffer); delete[] buffer; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) imageData->findStars(); } image->pushFilter(type); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } void FITSHistogramCommand::undo() { FITSView * image = tab->getView(); FITSData * imageData = image->getImageData(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) { FITSData::Statistic prevStats; imageData->saveStatistics(prevStats); reverseDelta(); imageData->restoreStatistics(stats); stats = prevStats; } else { switch (type) { case FITS_ROTATE_CW: imageData->applyFilter(FITS_ROTATE_CCW); break; case FITS_ROTATE_CCW: imageData->applyFilter(FITS_ROTATE_CW); break; case FITS_FLIP_H: case FITS_FLIP_V: imageData->applyFilter(type); break; default: break; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) imageData->findStars(); } image->popFilter(); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } QString FITSHistogramCommand::text() const { switch (type) { case FITS_AUTO: return i18n("Auto Scale"); case FITS_LINEAR: return i18n("Linear Scale"); case FITS_LOG: return i18n("Logarithmic Scale"); case FITS_SQRT: return i18n("Square Root Scale"); default: if (type - 1 <= FITSViewer::filterTypes.count()) return FITSViewer::filterTypes.at(type - 1); break; } return i18n("Unknown"); } void FITSHistogram::driftMouseOverLine(QMouseEvent * event) { double intensity = customPlot->xAxis->pixelToCoord(event->localPos().x()); FITSData * imageData = tab->getView()->getImageData(); uint8_t channels = imageData->channels(); QVector freq(3, -1); QVector inRange(3, false); for (int n = 0; n < channels; n++) { if (intensity >= imageData->getMin(n) && intensity <= imageData->getMax(n)) inRange[n] = true; } if ( (channels == 1 && inRange[0] == false) || (!inRange[0] && !inRange[1] && !inRange[2]) ) { QToolTip::hideText(); return; } if (customPlot->xAxis->range().contains(intensity)) { for (int n = 0; n < channels; n++) { int index = graphs[n]->findBegin(intensity, true); freq[n] = graphs[n]->dataMainValue(index); } if (channels == 1 && freq[0] > 0) { QToolTip::showText( event->globalPos(), i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;", "" "" "" "
Intensity: %1
R Frequency: %2
", QString::number(intensity, 'f', numDecimals[0]), QString::number(freq[0], 'f', 0))); } else if (freq[1] > 0) { QToolTip::showText( event->globalPos(), i18nc("Histogram tooltip; %1 is intensity; %2 is frequency;", "" "" "" "" "" "
Intensity: %1
R Frequency: %2
G Frequency: %3
B Frequency: %4
", QString::number(intensity, 'f', numDecimals[0]), QString::number(freq[0], 'f', 0), QString::number(freq[1], 'f', 0), QString::number(freq[2], 'f', 0))); } else QToolTip::hideText(); customPlot->replot(); } } diff --git a/kstars/fitsviewer/fitslabel.cpp b/kstars/fitsviewer/fitslabel.cpp index 892b1bbe4..fa09826a4 100644 --- a/kstars/fitsviewer/fitslabel.cpp +++ b/kstars/fitsviewer/fitslabel.cpp @@ -1,368 +1,368 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "fitslabel.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsview.h" #include "kspopupmenu.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "skymap.h" #include "ksnotification.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 FITSLabel::FITSLabel(FITSView *view, QWidget *parent) : QLabel(parent) { this->view = view; } void FITSLabel::setSize(double w, double h) { width = w; height = h; size = w * h; } bool FITSLabel::getMouseButtonDown() { return mouseButtonDown; } /** This method was added to make the panning function work. If the mouse button is released, it resets mouseButtonDown variable and the mouse cursor. */ void FITSLabel::mouseReleaseEvent(QMouseEvent *e) { Q_UNUSED(e) if (view->getCursorMode() == FITSView::dragCursor) { mouseButtonDown = false; view->updateMouseCursor(); } } /** I added some things to the top of this method to allow panning and Scope slewing to function. If you are in the dragMouse mode and the mousebutton is pressed, The method checks the difference between the location of the last point stored and the current event point to see how the mouse has moved. Then it moves the scrollbars and thus the view to the right location. Then it stores the current point so next time it can do it again. */ void FITSLabel::mouseMoveEvent(QMouseEvent *e) { float scale = (view->getCurrentZoom() / ZOOM_DEFAULT); if (view->getCursorMode() == FITSView::dragCursor && mouseButtonDown) { QPoint newPoint = e->globalPos(); int dx = newPoint.x() - lastMousePoint.x(); int dy = newPoint.y() - lastMousePoint.y(); view->horizontalScrollBar()->setValue(view->horizontalScrollBar()->value() - dx); view->verticalScrollBar()->setValue(view->verticalScrollBar()->value() - dy); lastMousePoint = newPoint; } double x, y; FITSData *view_data = view->getImageData(); - uint8_t *buffer = view_data->getImageBuffer(); + uint8_t const *buffer = view_data->getImageBuffer(); if (buffer == nullptr) return; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); emit newStatus(QString("X:%1 Y:%2").arg(static_cast(x)).arg(static_cast(y)), FITS_POSITION); // Range is 0 to dim-1 when accessing array x -= 1; y -= 1; int index = y * width + x; QString stringValue; switch (view_data->property("dataType").toInt()) { case TBYTE: stringValue = QLocale().toString(buffer[index]); break; case TSHORT: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); break; case TUSHORT: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); break; case TLONG: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); break; case TULONG: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index]); break; case TFLOAT: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index], 'f', 5); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index], 'f', 5); break; case TLONGLONG: - stringValue = QLocale().toString(static_cast((reinterpret_cast(buffer))[index])); + stringValue = QLocale().toString(static_cast((reinterpret_cast(buffer))[index])); break; case TDOUBLE: - stringValue = QLocale().toString((reinterpret_cast(buffer))[index], 'f', 5); + stringValue = QLocale().toString((reinterpret_cast(buffer))[index], 'f', 5); break; default: break; } emit newStatus(stringValue, FITS_VALUE); if (view_data->hasWCS() && view->getCursorMode() != FITSView::selectCursor) { int index = x + y * width; wcs_point *wcs_coord = view_data->getWCSCoord(); if (wcs_coord) { if (index > size) return; ra.setD(wcs_coord[index].ra); dec.setD(wcs_coord[index].dec); emit newStatus(QString("%1 , %2").arg(ra.toHMSString(), dec.toDMSString()), FITS_WCS); } bool objFound = false; foreach (FITSSkyObject *listObject, view_data->objList) { if ((std::abs(listObject->x() - x) < 5 / scale) && (std::abs(listObject->y() - y) < 5 / scale)) { QToolTip::showText(e->globalPos(), listObject->skyObject()->name() + '\n' + listObject->skyObject()->longname(), this); objFound = true; break; } } if (!objFound) QToolTip::hideText(); } double HFR = view->getImageData()->getHFR(x, y); if (HFR > 0) QToolTip::showText(e->globalPos(), i18nc("Half Flux Radius", "HFR: %1", QString::number(HFR, 'g', 3)), this); //setCursor(Qt::CrossCursor); e->accept(); } /** I added some things to the top of this method to allow panning and Scope slewing to function. If in dragMouse mode, the Panning function works by storing the cursor position when the mouse was pressed and setting the mouseButtonDown variable to true. If in ScopeMouse mode and the mouse is clicked, if there is WCS data and a scope is available, the method will verify that you actually do want to slew to the WCS coordinates associated with the click location. If so, it calls the centerTelescope function. */ void FITSLabel::mousePressEvent(QMouseEvent *e) { float scale = (view->getCurrentZoom() / ZOOM_DEFAULT); if (view->getCursorMode() == FITSView::dragCursor) { mouseButtonDown = true; lastMousePoint = e->globalPos(); view->updateMouseCursor(); } else if (e->buttons() & Qt::LeftButton && view->getCursorMode() == FITSView::scopeCursor) { #ifdef HAVE_INDI FITSData *view_data = view->getImageData(); if (view_data->hasWCS()) { wcs_point *wcs_coord = view_data->getWCSCoord(); if (wcs_coord) { double x, y; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); int index = x + y * width; if (KMessageBox::Continue == KMessageBox::warningContinueCancel( nullptr, "Slewing to Coordinates: \nRA: " + dms(wcs_coord[index].ra).toHMSString() + "\nDec: " + dms(wcs_coord[index].dec).toDMSString(), i18n("Continue Slew"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "continue_slew_warning")) { centerTelescope(wcs_coord[index].ra / 15.0, wcs_coord[index].dec); view->setCursorMode(view->lastMouseMode); view->updateScopeButton(); } } } #endif } double x, y; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); #ifdef HAVE_INDI FITSData *view_data = view->getImageData(); if (e->buttons() & Qt::RightButton && view->getCursorMode() != FITSView::scopeCursor) { mouseReleaseEvent(e); if (view_data->hasWCS()) { foreach (FITSSkyObject *listObject, view_data->objList) { if ((std::abs(listObject->x() - x) < 5 / scale) && (std::abs(listObject->y() - y) < 5 / scale)) { SkyObject *object = listObject->skyObject(); KSPopupMenu *pmenu; pmenu = new KSPopupMenu(); object->initPopupMenu(pmenu); QList actions = pmenu->actions(); foreach (QAction *action, actions) { if (action->text().left(7) == "Starhop") pmenu->removeAction(action); if (action->text().left(7) == "Angular") pmenu->removeAction(action); if (action->text().left(8) == "Add flag") pmenu->removeAction(action); if (action->text().left(12) == "Attach Label") pmenu->removeAction(action); } pmenu->popup(e->globalPos()); KStars::Instance()->map()->setClickedObject(object); break; } } } if (fabs(view->markerCrosshair.x() - x) <= 15 && fabs(view->markerCrosshair.y() - y) <= 15) emit markerSelected(0, 0); } #endif if (e->buttons() & Qt::LeftButton) { if (view->getCursorMode() == FITSView::selectCursor) emit pointSelected(x, y); else if (view->getCursorMode() == FITSView::crosshairCursor) emit pointSelected(x + 5 / scale, y + 5 / scale); } } void FITSLabel::mouseDoubleClickEvent(QMouseEvent *e) { double x, y; x = round(e->x() / (view->getCurrentZoom() / ZOOM_DEFAULT)); y = round(e->y() / (view->getCurrentZoom() / ZOOM_DEFAULT)); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); emit markerSelected(x, y); return; } void FITSLabel::centerTelescope(double raJ2000, double decJ2000) { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { KSNotification::sorry(i18n("KStars did not find any active telescopes.")); return; } foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice *bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; if (bd->isConnected() == false) { KSNotification::error(i18n("Telescope %1 is offline. Please connect and retry again.", gd->getDeviceName())); return; } ISD::GDSetCommand SlewCMD(INDI_SWITCH, "ON_COORD_SET", "TRACK", ISS_ON, this); SkyObject selectedObject; selectedObject.setRA0(raJ2000); selectedObject.setDec0(decJ2000); selectedObject.apparentCoord(J2000, KStarsData::Instance()->ut().djd()); gd->setProperty(&SlewCMD); gd->runCommand(INDI_SEND_COORDS, &selectedObject); return; } KSNotification::sorry(i18n("KStars did not find any active telescopes.")); #else Q_UNUSED(raJ2000); Q_UNUSED(decJ2000); #endif } diff --git a/kstars/fitsviewer/fitssepdetector.cpp b/kstars/fitsviewer/fitssepdetector.cpp new file mode 100644 index 000000000..19f377331 --- /dev/null +++ b/kstars/fitsviewer/fitssepdetector.cpp @@ -0,0 +1,201 @@ +/*************************************************************************** + fitssepdetector.cpp - FITS Image + ------------------- + begin : Sun March 29 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#include + +#include "sep/sep.h" +#include "fits_debug.h" +#include "fitssepdetector.h" + +FITSStarDetector& FITSSEPDetector::configure(const QString &, const QVariant &) +{ + return *this; +} + +int FITSSEPDetector::findSources(QList &starCenters, const QRect &boundary) +{ + FITSData const * const image_data = reinterpret_cast(parent()); + + if (image_data == nullptr) + return 0; + + FITSData::Statistic const &stats = image_data->getStatistics(); + + int x = 0, y = 0, w = stats.width, h = stats.height, maxRadius = 50; + + if (!boundary.isNull()) + { + x = boundary.x(); + y = boundary.y(); + w = boundary.width(); + h = boundary.height(); + maxRadius = w; + } + + auto * data = new float[w * h]; + + switch (stats.bitpix) + { + case BYTE_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case SHORT_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case USHORT_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case LONG_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case ULONG_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case FLOAT_IMG: + memcpy(data, image_data->getImageBuffer(), sizeof(float)*w*h); + break; + case LONGLONG_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + case DOUBLE_IMG: + getFloatBuffer(data, x, y, w, h, image_data); + break; + default: + delete [] data; + return -1; + } + + float * imback = nullptr; + double * flux = nullptr, *fluxerr = nullptr, *area = nullptr; + short * flag = nullptr; + short flux_flag = 0; + int status = 0; + sep_bkg * bkg = nullptr; + sep_catalog * catalog = nullptr; + float conv[] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; + double flux_fractions[2] = {0}; + double requested_frac[2] = { 0.5, 0.99 }; + QList edges; + + // #0 Create SEP Image structure + sep_image im = {data, nullptr, nullptr, SEP_TFLOAT, 0, 0, w, h, 0.0, SEP_NOISE_NONE, 1.0, 0.0}; + + // #1 Background estimate + status = sep_background(&im, 64, 64, 3, 3, 0.0, &bkg); + if (status != 0) goto exit; + + // #2 Background evaluation + imback = (float *)malloc((w * h) * sizeof(float)); + status = sep_bkg_array(bkg, imback, SEP_TFLOAT); + if (status != 0) goto exit; + + // #3 Background subtraction + status = sep_bkg_subarray(bkg, im.data, im.dtype); + if (status != 0) goto exit; + + // #4 Source Extraction + // Note that we set deblend_cont = 1.0 to turn off deblending. + status = sep_extract(&im, 2 * bkg->globalrms, SEP_THRESH_ABS, 10, conv, 3, 3, SEP_FILTER_CONV, 32, 1.0, 1, 1.0, &catalog); + if (status != 0) goto exit; + + // TODO + // Must detect edge detection + // Must limit to brightest 100 (by flux) centers + // Should probably use ellipse to draw instead of simple circle? + // Useful for galaxies and also elenogated stars. + for (int i = 0; i < catalog->nobj; i++) + { + double flux = catalog->flux[i]; + // Get HFR + sep_flux_radius(&im, catalog->x[i], catalog->y[i], maxRadius, 5, 0, &flux, requested_frac, 2, flux_fractions, &flux_flag); + + auto * center = new Edge(); + center->x = catalog->x[i] + x + 0.5; + center->y = catalog->y[i] + y + 0.5; + center->val = catalog->peak[i]; + center->sum = flux; + center->HFR = center->width = flux_fractions[0]; + if (flux_fractions[1] < maxRadius) + center->width = flux_fractions[1] * 2; + edges.append(center); + } + + // Let's sort edges, starting with widest + std::sort(edges.begin(), edges.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->width > edge2->width;}); + + // Take only the first 100 stars + { + int starCount = qMin(100, edges.count()); + for (int i = 0; i < starCount; i++) + starCenters.append(edges[i]); + } + + edges.clear(); + + qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << "#" << "#X" << "#Y" << "#Flux" << "#Width" << "#HFR"; + for (int i = 0; i < starCenters.count(); i++) + qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << i << starCenters[i]->x << starCenters[i]->y + << starCenters[i]->sum << starCenters[i]->width << starCenters[i]->HFR; + +exit: + if (stats.bitpix != FLOAT_IMG) + delete [] data; + sep_bkg_free(bkg); + sep_catalog_free(catalog); + free(imback); + free(flux); + free(fluxerr); + free(area); + free(flag); + + if (status != 0) + { + char errorMessage[512]; + sep_get_errmsg(status, errorMessage); + qCritical(KSTARS_FITS) << errorMessage; + return -1; + } + + return starCenters.count(); +} + +template +void FITSSEPDetector::getFloatBuffer(float * buffer, int x, int y, int w, int h, FITSData const *data) const +{ + auto * rawBuffer = reinterpret_cast(data->getImageBuffer()); + + if (buffer == nullptr) + return; + + float * floatPtr = buffer; + + int x2 = x + w; + int y2 = y + h; + + FITSData::Statistic const &stats = data->getStatistics(); + + for (int y1 = y; y1 < y2; y1++) + { + int offset = y1 * stats.width; + for (int x1 = x; x1 < x2; x1++) + { + *floatPtr++ = rawBuffer[offset + x1]; + } + } +} diff --git a/kstars/fitsviewer/fitssepdetector.h b/kstars/fitsviewer/fitssepdetector.h new file mode 100644 index 000000000..67d7ce4fa --- /dev/null +++ b/kstars/fitsviewer/fitssepdetector.h @@ -0,0 +1,57 @@ +/*************************************************************************** + fitssepdetector.h - FITS Image + ------------------- + begin : Sun March 29 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#ifndef FITSSEPDETECTOR_H +#define FITSSEPDETECTOR_H + +#include + +#include "fitsstardetector.h" + +class FITSSEPDetector : public FITSStarDetector +{ + Q_OBJECT + +public: + explicit FITSSEPDetector(FITSData *parent): FITSStarDetector(parent) {}; + +public: + /** @brief Find sources in the parent FITS data file. + * @see FITSStarDetector::findSources(). + */ + int findSources(QList &starCenters, QRect const &boundary = QRect()) override; + + /** @brief Configure the detection method. + * @see FITSStarDetector::configure(). + * @note No parameters are currently available for configuration. + * @todo Provide parameters for detection configuration. + */ + FITSStarDetector & configure(const QString &setting, const QVariant &value) override; + +protected: + /** @internal Consolidate a float data buffer from FITS data. + * @param buffer is the destination float block. + * @param x, y, w, h define a (x,y)-(x+w,y+h) sub-frame to extract from the FITS data out to block 'buffer'. + * @param image_data is the FITS data block to extract from. + */ + template + void getFloatBuffer(float * buffer, int x, int y, int w, int h, FITSData const * image_data) const; +}; + +#endif // FITSSEPDETECTOR_H diff --git a/kstars/fitsviewer/fitsstardetector.cpp b/kstars/fitsviewer/fitsstardetector.cpp new file mode 100644 index 000000000..3e250f9ee --- /dev/null +++ b/kstars/fitsviewer/fitsstardetector.cpp @@ -0,0 +1,11 @@ +#include "fitsstardetector.h" + +FITSStarDetector& FITSStarDetector::configure(QStandardItemModel const &settings) +{ + Q_ASSERT(2 <= settings.columnCount()); + + for (int row = 0; row < settings.rowCount(); row++) + configure(settings.item(row, 0)->data().toString(), settings.item(row, 1)->data()); + + return *this; +} diff --git a/kstars/fitsviewer/fitsstardetector.h b/kstars/fitsviewer/fitsstardetector.h new file mode 100644 index 000000000..c25aff14d --- /dev/null +++ b/kstars/fitsviewer/fitsstardetector.h @@ -0,0 +1,78 @@ +/*************************************************************************** + fitsstardetector.h - FITS Image + ------------------- + begin : Fri March 27 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#ifndef FITSSTARDETECTION_H +#define FITSSTARDETECTION_H + +#include +#include +#include + +#include "fitsdata.h" + +class FITSData; + +class Edge +{ +public: + float x {0}; + float y {0}; + int val {0}; + int scanned {0}; + float width {0}; + float HFR {-1}; + float sum {0}; +}; + +class FITSStarDetector : public QObject +{ + Q_OBJECT + +public: + /** @brief Instantiate a detector for a FITS data file. + */ + explicit FITSStarDetector(FITSData *parent): + QObject(reinterpret_cast(parent)) + {}; + +public: + /** @brief Find sources in the parent FITS data file. + * @param starCenters is the list of sources to append to. + * @param boundary is the rectangle in which to find sources, by default the full frame. + * @return The number of sources detected by the procedure. + */ + virtual int findSources(QList &starCenters, QRect const &boundary = QRect()) = 0; + + /** @brief Configure the detection method. + * @param setting is the name of a detection setting. + * @param value is the value of the detection setting identified by 'setting'. + * @return The detector as a chain to call the overriden findSources. + */ + virtual FITSStarDetector & configure(const QString &setting, const QVariant &value) = 0; + +public: + /** @brief Helper to configure the detection method from a data model. + * @param settings is the list of key/value pairs for the method to use settings from. + * @note Data model 'settings' is considered a key/value list, using column 1 text as case-insensitive keys and column 2 data as values. + * @return The detector as a chain to call the overriden findSources. + */ + FITSStarDetector & configure(QStandardItemModel const &settings); +}; + +#endif // FITSSTARDETECTION_H diff --git a/kstars/fitsviewer/fitsthresholddetector.cpp b/kstars/fitsviewer/fitsthresholddetector.cpp new file mode 100644 index 000000000..69999972f --- /dev/null +++ b/kstars/fitsviewer/fitsthresholddetector.cpp @@ -0,0 +1,227 @@ +/*************************************************************************** + fitsthresholddetector.cpp - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#include + +#include "fits_debug.h" +#include "fitsthresholddetector.h" + +FITSStarDetector& FITSThresholdDetector::configure(const QString &setting, const QVariant &value) +{ + bool ok = false; + + if (!setting.compare("threshold", Qt::CaseInsensitive)) + { + double const _focusThreshold = value.toDouble(&ok); + if (ok) + focusThreshold = _focusThreshold; + } + + return *this; +} + +int FITSThresholdDetector::findSources(QList &starCenters, QRect const &boundary) +{ + switch (parent()->property("dataType").toInt()) + { + case TBYTE: + return findOneStar(starCenters, boundary); + + case TSHORT: + return findOneStar(starCenters, boundary); + + case TUSHORT: + return findOneStar(starCenters, boundary); + + case TLONG: + return findOneStar(starCenters, boundary); + + case TULONG: + return findOneStar(starCenters, boundary); + + case TFLOAT: + return findOneStar(starCenters, boundary); + + case TLONGLONG: + return findOneStar(starCenters, boundary); + + case TDOUBLE: + return findOneStar(starCenters, boundary); + + default: + break; + } + + return 0; +} + +template +int FITSThresholdDetector::findOneStar(QList &starCenters, const QRect &boundary) const +{ + FITSData const * const image_data = reinterpret_cast(parent()); + + if (image_data == nullptr) + return 0; + + FITSData::Statistic const &stats = image_data->getStatistics(); + + if (boundary.isEmpty()) + return -1; + + int subX = boundary.x(); + int subY = boundary.y(); + int subW = subX + boundary.width(); + int subH = subY + boundary.height(); + + float massX = 0, massY = 0, totalMass = 0; + + auto * buffer = reinterpret_cast(image_data->getImageBuffer()); + + // TODO replace magic number with something more useful to understand + double threshold = stats.mean[0] * focusThreshold / 100.0; + + for (int y = subY; y < subH; y++) + { + for (int x = subX; x < subW; x++) + { + T pixel = buffer[x + y * stats.width]; + if (pixel > threshold) + { + totalMass += pixel; + massX += x * pixel; + massY += y * pixel; + } + } + } + + qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << massX / totalMass << " Y: " << massY / totalMass; + + auto * center = new Edge; + center->width = -1; + center->x = massX / totalMass + 0.5; + center->y = massY / totalMass + 0.5; + center->HFR = 1; + + // Maximum Radius + int maxR = qMin(subW - 1, subH - 1) / 2; + + // Critical threshold + double critical_threshold = threshold * 0.7; + double running_threshold = threshold; + + while (running_threshold >= critical_threshold) + { + for (int r = maxR; r > 1; r--) + { + int pass = 0; + + for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 10.0) + { + int testX = center->x + std::cos(theta) * r; + int testY = center->y + std::sin(theta) * r; + + // if out of bound, break; + if (testX < subX || testX > subW || testY < subY || testY > subH) + break; + + if (buffer[testX + testY * stats.width] > running_threshold) + pass++; + } + + //qDebug() << "Testing for radius " << r << " passes # " << pass << " @ threshold " << running_threshold; + //if (pass >= 6) + if (pass >= 5) + { + center->width = r * 2; + break; + } + } + + if (center->width > 0) + break; + + // Increase threshold fuzziness by 10% + running_threshold -= running_threshold * 0.1; + } + + // If no stars were detected + if (center->width == -1) + { + delete center; + return 0; + } + + // 30% fuzzy + //center->width += center->width*0.3 * (running_threshold / threshold); + + double FSum = 0, HF = 0, TF = 0, min = stats.min[0]; + const double resolution = 1.0 / 20.0; + + int cen_y = qRound(center->y); + + double rightEdge = center->x + center->width / 2.0; + double leftEdge = center->x - center->width / 2.0; + + QVector subPixels; + subPixels.reserve(center->width / resolution); + + for (double x = leftEdge; x <= rightEdge; x += resolution) + { + //subPixels[x] = resolution * (image_buffer[static_cast(floor(x)) + cen_y * stats.width] - min); + double slice = resolution * (buffer[static_cast(floor(x)) + cen_y * stats.width] - min); + FSum += slice; + subPixels.append(slice); + } + + // Half flux + HF = FSum / 2.0; + + //double subPixelCenter = center->x - fmod(center->x,resolution); + int subPixelCenter = (center->width / resolution) / 2; + + // Start from center + TF = subPixels[subPixelCenter]; + double lastTF = TF; + // Integrate flux along radius axis until we reach half flux + //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) + for (int k = 1; k < subPixelCenter; k++) + { + TF += subPixels[subPixelCenter + k]; + TF += subPixels[subPixelCenter - k]; + + if (TF >= HF) + { + // We have two ways to calculate HFR. The first is the correct method but it can get quite variable within 10% due to random fluctuations of the measured star. + // The second method is not truly HFR but is much more resistant to noise. + + // #1 Approximate HFR, accurate and reliable but quite variable to small changes in star flux + center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; + + // #2 Not exactly HFR, but much more stable + //center->HFR = (k*resolution) * (HF/TF); + break; + } + + lastTF = TF; + } + + starCenters.append(center); + + return 1; +} diff --git a/kstars/fitsviewer/fitsthresholddetector.h b/kstars/fitsviewer/fitsthresholddetector.h new file mode 100644 index 000000000..b86a73b86 --- /dev/null +++ b/kstars/fitsviewer/fitsthresholddetector.h @@ -0,0 +1,62 @@ +/*************************************************************************** + fitsthresholddetector.h - FITS Image + ------------------- + begin : Sat March 28 2020 + copyright : (C) 2004 by Jasem Mutlaq, (C) 2020 by Eric Dejouhanet + email : eric.dejouhanet@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* + * See http://members.aol.com/pkirchg for more details. * + ***************************************************************************/ + +#ifndef FITSTHRESHOLDDETECTOR_H +#define FITSTHRESHOLDDETECTOR_H + +#include "fitsstardetector.h" + +class FITSThresholdDetector: public FITSStarDetector +{ + Q_OBJECT + +public: + explicit FITSThresholdDetector(FITSData *parent): FITSStarDetector(parent) {}; + +public: + /** @brief Find sources in the parent FITS data file. + * @see FITSStarDetector::findSources(). + */ + int findSources(QList &starCenters, QRect const &boundary = QRect()) override; + + /** @brief Configure the detection method. + * @see FITSStarDetector::configure(). + * @note Parameter "threshold" defaults to THRESHOLD_PERCENTAGE of the mean pixel value of the frame. + * @todo Provide parameters for detection configuration. + */ + FITSStarDetector & configure(const QString &setting, const QVariant &value) override; + +public: + /** @group Detection internals + * @{ */ + static constexpr int THRESHOLD_PERCENTAGE { 120 }; + /** @} */ + +protected: + /** @internal Find sources in the parent FITS data file, dependent of the pixel depth. + * @see FITSGradientDetector::findSources. + */ + template + int findOneStar(QList &starCenters, const QRect &boundary) const; + +protected: + double focusThreshold { THRESHOLD_PERCENTAGE }; +}; + +#endif // FITSTHRESHOLDDETECTOR_H diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index 9bac55c92..4ffac690a 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,401 +1,398 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "fitscommon.h" #include #include "stretch.h" #ifdef HAVE_DATAVISUALIZATION #include "starprofileviewer.h" #endif #include #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include #include -#define MINIMUM_PIXEL_RANGE 5 -#define MINIMUM_STDVAR 5 - class QAction; class QEvent; class QGestureEvent; class QImage; class QLabel; class QPinchGesture; class QResizeEvent; class QToolBar; class FITSData; class FITSLabel; class FITSView : public QScrollArea { Q_OBJECT public: explicit FITSView(QWidget *parent = nullptr, FITSMode fitsMode = FITS_NORMAL, FITSScale filterType = FITS_NONE); virtual ~FITSView() override; typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and displays it in a FITSView frame * @param inFilename FITS File name * @param silent if set, error popups are suppressed. * @note If image is successfully, loaded() signal is emitted, otherwise failed() signal is emitted. * Obtain error by calling lastError() */ void loadFITS(const QString &inFilename, bool silent = true); /** * @brief loadFITSFromData Takes ownership of the FITSData instance passed in and displays it in a FITSView frame * @param inFilename FITS File name to use */ bool loadFITSFromData(FITSData *data, const QString &inFilename); // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired bool rescale(FITSZoom type); // Access functions FITSData *getImageData() const { return imageData; } double getCurrentZoom() const { return currentZoom; } QImage getDisplayImage() const { return rawImage; } const QPixmap &getDisplayPixmap() const { return displayPixmap; } // Tracking square void setTrackingBoxEnabled(bool enable); bool isTrackingBoxEnabled() const { return trackingBoxEnabled; } QPixmap &getTrackingBoxPixmap(uint8_t margin = 0); void setTrackingBox(const QRect &rect); const QRect &getTrackingBox() const { return trackingBox; } // last error const QString &lastError() const { return m_LastError; } // Overlay virtual void drawOverlay(QPainter *); // Overlay objects void drawStarCentroid(QPainter *); void drawTrackingBox(QPainter *); void drawMarker(QPainter *); void drawCrosshair(QPainter *); void drawEQGrid(QPainter *); void drawObjectNames(QPainter *painter); void drawPixelGrid(QPainter *painter); bool isImageStretched(); bool isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); // Setup the graphics. void updateFrame(); bool isTelescopeActive(); void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; CursorMode getCursorMode(); void setCursorMode(CursorMode mode); void updateMouseCursor(); void updateScopeButton(); void setScopeButton(QAction *action) { centerTelescopeAction = action; } // Zoom related void cleanUpZoom(QPoint viewCenter); QPoint getImagePoint(QPoint viewPortPoint); uint16_t zoomedWidth() { return currentWidth; } uint16_t zoomedHeight() { return currentHeight; } // Star Detection int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &searchBox = QRect()); void toggleStars(bool enable); void setStarsEnabled(bool enable); void setStarsHFREnabled(bool enable); void setStarFilterRange(float const innerRadius, float const outerRadius); int filterStars(); // FITS Mode void updateMode(FITSMode fmode); FITSMode getMode() { return mode; } void setFilter(FITSScale newFilter) { filter = newFilter; } void setFirstLoad(bool value); void pushFilter(FITSScale value) { filterStack.push(value); } FITSScale popFilter() { return filterStack.pop(); } CursorMode lastMouseMode { selectCursor }; bool isStarProfileShown() { return showStarProfile; } // Floating toolbar void createFloatingToolBar(); //void setLoadWCSEnabled(bool value); // Returns the params set to stretch the image. StretchParams getStretchParams() const { return stretchParams; } // Returns true if we're automatically generating stretch parameters. // Note: this is not whether we're stretching, that's controlled by stretchImage. bool getAutoStretch() const { return autoStretch; } // Sets the params for stretching. Will also stretch and re-display the image. // This only sets the first channel stretch params. For RGB images, the G&B channel // stretch parameters are a function of the Red input param and the existing RGB params. void setStretchParams(const StretchParams& params); // Sets whether to stretch the image or not. // Will also re-display the image if onOff != stretchImage. void setStretch(bool onOff); // Automatically generates stretch parameters and use them to re-display the image. void setAutoStretchParams(); // When sampling is > 1, we will display the image at a lower resolution. void setSampling(int value) { if (value > 0) sampling = value; } public slots: void wheelEvent(QWheelEvent *event) override; void resizeEvent(QResizeEvent *event) override; void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); // Grids void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggleCrosshair(); // Stars void toggleStars(); void toggleStarProfile(); void viewStarProfile(); void centerTelescope(); void toggleStretch(); virtual void processPointSelection(int x, int y); virtual void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); void resizeTrackingBox(int newSize); protected slots: /** * @brief syncWCSState Update toolbar and actions depending on whether WCS is available or not */ void syncWCSState(); bool event(QEvent *event) override; bool gestureEvent(QGestureEvent *event); void pinchTriggered(QPinchGesture *gesture); protected: template bool rescale(FITSZoom type); double average(); double stddev(); void calculateMaxPixel(double min, double max); void initDisplayImage(); QPointF getPointForGridLabel(); bool pointIsInImage(QPointF pt, bool scaled); void loadInFrame(); /// WCS Future Watcher QFutureWatcher wcsWatcher; /// FITS Future Watcher QFutureWatcher fitsWatcher; /// Cross hair QPointF markerCrosshair; /// Pointer to FITSData object FITSData *imageData { nullptr }; /// Current zoom level double currentZoom { 0 }; private: bool processData(); void doStretch(FITSData *data, QImage *outputImage); QLabel *noImageLabel { nullptr }; QPixmap noImage; QVector eqGridPoints; std::unique_ptr image_frame; /// Current width due to zoom uint16_t currentWidth { 0 }; uint16_t lastWidth { 0 }; /// Current height due to zoom uint16_t currentHeight { 0 }; uint16_t lastHeight { 0 }; /// Image zoom factor const double zoomFactor; // Original full-size image QImage rawImage; // Scaled images QImage scaledImage; // Actual pixmap after all the overlays QPixmap displayPixmap; bool firstLoad { true }; bool markStars { false }; bool showStarProfile { false }; bool showCrosshair { false }; bool showObjects { false }; bool showEQGrid { false }; bool showPixelGrid { false }; bool showStarsHFR { false }; // Should the image be displayed in linear (false) or stretched (true). // Initial value controlled by Options::autoStretch. bool stretchImage { false }; // When stretching, should we automatically compute parameters. // When first displaying, this should be true, but may be set to false // if the user has overridden the automatically set parameters. bool autoStretch { true }; // Params for stretching image. StretchParams stretchParams; // Resolution for display. Sampling=2 means display every other sample. int sampling { 1 }; struct { bool used() const { return innerRadius != 0.0f || outerRadius != 1.0f; } float innerRadius { 0.0f }; float outerRadius { 1.0f }; } starFilter; CursorMode cursorMode { selectCursor }; bool zooming { false }; int zoomTime { 0 }; QPoint zoomLocation; QString filename; FITSMode mode; FITSScale filter; QString m_LastError; QStack filterStack; // Tracking box bool trackingBoxEnabled { false }; QRect trackingBox; QPixmap trackingBoxPixmap; // Scope pixmap QPixmap redScopePixmap; // Magenta Scope Pixmap QPixmap magentaScopePixmap; // Floating toolbar QToolBar *floatingToolBar { nullptr }; QAction *centerTelescopeAction { nullptr }; QAction *toggleEQGridAction { nullptr }; QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; QAction *toggleStretchAction { nullptr }; //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION QPointer starProfileWidget; #endif signals: void newStatus(const QString &msg, FITSBar id); void debayerToggled(bool); void wcsToggled(bool); void actionUpdated(const QString &name, bool enable); void trackingStarSelected(int x, int y); void loaded(); void failed(); void starProfileWindowClosed(); friend class FITSLabel; }; diff --git a/kstars/fitsviewer/stretch.cpp b/kstars/fitsviewer/stretch.cpp index 064457940..dc67cbdcb 100644 --- a/kstars/fitsviewer/stretch.cpp +++ b/kstars/fitsviewer/stretch.cpp @@ -1,440 +1,440 @@ /* Stretch This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "stretch.h" #include #include #include namespace { // Returns the median value of the vector. // The vector is modified in an undefined way. template T median(std::vector& values) { const int middle = values.size() / 2; std::nth_element(values.begin(), values.begin() + middle, values.end()); return values[middle]; } // Returns the rough max of the buffer. template -T sampledMax(T *values, int size, int sampleBy) +T sampledMax(T const *values, int size, int sampleBy) { T maxVal = 0; for (int i = 0; i < size; i+= sampleBy) if (maxVal < values[i]) maxVal = values[i]; return maxVal; } // Returns the median of the sample values. // The values are not modified. template -T median(T *values, int size, int sampleBy) +T median(T const *values, int size, int sampleBy) { const int downsampled_size = size / sampleBy; std::vector samples(downsampled_size); for (int index = 0, i = 0; i < downsampled_size; ++i, index += sampleBy) samples[i] = values[index]; return median(samples); } // This stretches one channel given the input parameters. // Based on the spec in section 8.5.6 // https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html // Uses multiple threads, blocks until done. // The extension parameters are not used. // Sampling is applied to the output (that is, with sampling=2, we compute every other output // sample both in width and height, so the output would have about 4X fewer pixels. template void stretchOneChannel(T *input_buffer, QImage *output_image, const StretchParams& stretch_params, int input_range, int image_height, int image_width, int sampling) { QVector> futures; // We're outputting uint8, so the max output is 255. constexpr int maxOutput = 255; // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). const float maxInput = input_range > 1 ? input_range - 1 : input_range; const float midtones = stretch_params.grey_red.midtones; const float highlights = stretch_params.grey_red.highlights; const float shadows = stretch_params.grey_red.shadows; // Precomputed expressions moved out of the loop. // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. const float hsRangeFactor = highlights == shadows ? 1.0f : 1.0f / (highlights - shadows); // Shadow and highlight values translated to the ADU scale. const T nativeShadows = shadows * maxInput; const T nativeHighlights = highlights * maxInput; // Constants based on above needed for the stretch calculations. const float k1 = (midtones - 1) * hsRangeFactor * maxOutput / maxInput; const float k2 = ((2 * midtones) - 1) * hsRangeFactor / maxInput; // Increment the input index by the sampling, the output index increments by 1. for (int j = 0, jout = 0; j < image_height; j+=sampling, jout++) { futures.append(QtConcurrent::run([ = ]() { T * inputLine = input_buffer + j * image_width; auto * scanLine = output_image->scanLine(jout); for (int i = 0, iout = 0; i < image_width; i+=sampling, iout++) { const T input = inputLine[i]; if (input < nativeShadows) scanLine[iout] = 0; else if (input >= nativeHighlights) scanLine[iout] = maxOutput; else { const T inputFloored = (input - nativeShadows); scanLine[iout] = (inputFloored * k1) / (inputFloored * k2 - midtones); } } })); } for(QFuture future : futures) future.waitForFinished(); } // This is like the above 1-channel stretch, but extended for 3 channels. // This could have been more modular, but the three channels are combined // into a single qRgb value at the end, so it seems the simplest thing is to // replicate the code. It is assume the colors are not interleaved--the red image // is stored fully, then the green, then the blue. // Sampling is applied to the output (that is, with sampling=2, we compute every other output // sample both in width and height, so the output would have about 4X fewer pixels. template void stretchThreeChannels(T *inputBuffer, QImage *outputImage, const StretchParams& stretchParams, int inputRange, int imageHeight, int imageWidth, int sampling) { QVector> futures; // We're outputting uint8, so the max output is 255. constexpr int maxOutput = 255; // Maximum possible input value (e.g. 1024*64 - 1 for a 16 bit unsigned int). const float maxInput = inputRange > 1 ? inputRange - 1 : inputRange; const float midtonesR = stretchParams.grey_red.midtones; const float highlightsR = stretchParams.grey_red.highlights; const float shadowsR = stretchParams.grey_red.shadows; const float midtonesG = stretchParams.green.midtones; const float highlightsG = stretchParams.green.highlights; const float shadowsG = stretchParams.green.shadows; const float midtonesB = stretchParams.blue.midtones; const float highlightsB = stretchParams.blue.highlights; const float shadowsB = stretchParams.blue.shadows; // Precomputed expressions moved out of the loop. // hightlights - shadows, protecting for divide-by-0, in a 0->1.0 scale. const float hsRangeFactorR = highlightsR == shadowsR ? 1.0f : 1.0f / (highlightsR - shadowsR); const float hsRangeFactorG = highlightsG == shadowsG ? 1.0f : 1.0f / (highlightsG - shadowsG); const float hsRangeFactorB = highlightsB == shadowsB ? 1.0f : 1.0f / (highlightsB - shadowsB); // Shadow and highlight values translated to the ADU scale. const T nativeShadowsR = shadowsR * maxInput; const T nativeShadowsG = shadowsG * maxInput; const T nativeShadowsB = shadowsB * maxInput; const T nativeHighlightsR = highlightsR * maxInput; const T nativeHighlightsG = highlightsG * maxInput; const T nativeHighlightsB = highlightsB * maxInput; // Constants based on above needed for the stretch calculations. const float k1R = (midtonesR - 1) * hsRangeFactorR * maxOutput / maxInput; const float k1G = (midtonesG - 1) * hsRangeFactorG * maxOutput / maxInput; const float k1B = (midtonesB - 1) * hsRangeFactorB * maxOutput / maxInput; const float k2R = ((2 * midtonesR) - 1) * hsRangeFactorR / maxInput; const float k2G = ((2 * midtonesG) - 1) * hsRangeFactorG / maxInput; const float k2B = ((2 * midtonesB) - 1) * hsRangeFactorB / maxInput; const int size = imageWidth * imageHeight; for (int j = 0, jout = 0; j < imageHeight; j+=sampling, jout++) { futures.append(QtConcurrent::run([ = ]() { // R, G, B input images are stored one after another. T * inputLineR = inputBuffer + j * imageWidth; T * inputLineG = inputLineR + size; T * inputLineB = inputLineG + size; auto * scanLine = reinterpret_cast(outputImage->scanLine(jout)); for (int i = 0, iout = 0; i < imageWidth; i+=sampling, iout++) { const T inputR = inputLineR[i]; const T inputG = inputLineG[i]; const T inputB = inputLineB[i]; uint8_t red, green, blue; if (inputR < nativeShadowsR) red = 0; else if (inputR >= nativeHighlightsR) red = maxOutput; else { const T inputFloored = (inputR - nativeShadowsR); red = (inputFloored * k1R) / (inputFloored * k2R - midtonesR); } if (inputG < nativeShadowsG) green = 0; else if (inputG >= nativeHighlightsG) green = maxOutput; else { const T inputFloored = (inputG - nativeShadowsG); green = (inputFloored * k1G) / (inputFloored * k2G - midtonesG); } if (inputB < nativeShadowsB) blue = 0; else if (inputB >= nativeHighlightsB) blue = maxOutput; else { const T inputFloored = (inputB - nativeShadowsB); blue = (inputFloored * k1B) / (inputFloored * k2B - midtonesB); } scanLine[iout] = qRgb(red, green, blue); } })); } for(QFuture future : futures) future.waitForFinished(); } template void stretchChannels(T *input_buffer, QImage *output_image, const StretchParams& stretch_params, int input_range, int image_height, int image_width, int num_channels, int sampling) { if (num_channels == 1) stretchOneChannel(input_buffer, output_image, stretch_params, input_range, image_height, image_width, sampling); else if (num_channels == 3) stretchThreeChannels(input_buffer, output_image, stretch_params, input_range, image_height, image_width, sampling); } // See section 8.5.7 in above link https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html template -void computeParamsOneChannel(T *buffer, StretchParams1Channel *params, +void computeParamsOneChannel(T const *buffer, StretchParams1Channel *params, int inputRange, int height, int width) { // Find the median sample. constexpr int maxSamples = 500000; const int sampleBy = width * height < maxSamples ? 1 : width * height / maxSamples; T medianSample = median(buffer, width * height, sampleBy); // Find the Median deviation: 1.4826 * median of abs(sample[i] - median). const int numSamples = width * height / sampleBy; std::vector deviations(numSamples); for (int index = 0, i = 0; i < numSamples; ++i, index += sampleBy) { if (medianSample > buffer[index]) deviations[i] = medianSample - buffer[index]; else deviations[i] = buffer[index] - medianSample; } // Shift everything to 0 -> 1.0. const float medDev = median(deviations); const float normalizedMedian = medianSample / static_cast(inputRange); const float MADN = 1.4826 * medDev / static_cast(inputRange); const bool upperHalf = normalizedMedian > 0.5; const float shadows = (upperHalf || MADN == 0) ? 0.0 : fmin(1.0, fmax(0.0, (normalizedMedian + -2.8 * MADN))); const float highlights = (!upperHalf || MADN == 0) ? 1.0 : fmin(1.0, fmax(0.0, (normalizedMedian - -2.8 * MADN))); float X, M; constexpr float B = 0.25; if (!upperHalf) { X = normalizedMedian - shadows; M = B; } else { X = B; M = highlights - normalizedMedian; } float midtones; if (X == 0) midtones = 0.0f; else if (X == M) midtones = 0.5f; else if (X == 1) midtones = 1.0f; else midtones = ((M - 1) * X) / ((2 * M - 1) * X - M); // Store the params. params->shadows = shadows; params->highlights = highlights; params->midtones = midtones; params->shadows_expansion = 0.0; params->highlights_expansion = 1.0; } // Need to know the possible range of input values. // Using the type of the sample and guessing. // Perhaps we should examine the contents for the file // (e.g. look at maximum value and extrapolate from that). int getRange(int data_type) { switch (data_type) { case TBYTE: return 256; case TSHORT: return 64*1024; case TUSHORT: return 64*1024; case TLONG: return 64*1024; case TFLOAT: return 64*1024; case TLONGLONG: return 64*1024; case TDOUBLE: return 64*1024; default: return 64*1024; } } } // namespace Stretch::Stretch(int width, int height, int channels, int data_type) { image_width = width; image_height = height; image_channels = channels; dataType = data_type; input_range = getRange(dataType); } -void Stretch::run(uint8_t *input, QImage *outputImage, int sampling) +void Stretch::run(uint8_t const *input, QImage *outputImage, int sampling) { Q_ASSERT(outputImage->width() == (image_width + sampling - 1) / sampling); Q_ASSERT(outputImage->height() == (image_height + sampling - 1) / sampling); recalculateInputRange(input); switch (dataType) { case TBYTE: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TSHORT: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TUSHORT: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TLONG: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TFLOAT: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TLONGLONG: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; case TDOUBLE: - stretchChannels(reinterpret_cast(input), outputImage, params, + stretchChannels(reinterpret_cast(input), outputImage, params, input_range, image_height, image_width, image_channels, sampling); break; default: break; } } // The input range for float/double is ambiguous, and we can't tell without the buffer, // so we set it to 64K and possibly reduce it when we see the data. -void Stretch::recalculateInputRange(uint8_t *input) +void Stretch::recalculateInputRange(uint8_t const *input) { if (input_range <= 1) return; if (dataType != TFLOAT && dataType != TDOUBLE) return; float mx = 0; if (dataType == TFLOAT) - mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); + mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); else if (dataType == TDOUBLE) - mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); + mx = sampledMax(reinterpret_cast(input), image_height * image_width, 1000); if (mx <= 1.01f) input_range = 1; } -StretchParams Stretch::computeParams(uint8_t *input) +StretchParams Stretch::computeParams(uint8_t const *input) { recalculateInputRange(input); StretchParams result; for (int channel = 0; channel < image_channels; ++channel) { int offset = channel * image_width * image_height; StretchParams1Channel *params = channel == 0 ? &result.grey_red : (channel == 1 ? &result.green : &result.blue); switch (dataType) { case TBYTE: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TSHORT: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TUSHORT: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TLONG: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TFLOAT: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TLONGLONG: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } case TDOUBLE: { - auto buffer = reinterpret_cast(input); + auto buffer = reinterpret_cast(input); computeParamsOneChannel(buffer + offset, params, input_range, image_height, image_width); break; } default: break; } } return result; } diff --git a/kstars/fitsviewer/stretch.h b/kstars/fitsviewer/stretch.h index dca625537..25eedd924 100644 --- a/kstars/fitsviewer/stretch.h +++ b/kstars/fitsviewer/stretch.h @@ -1,99 +1,99 @@ /* Stretch This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include #include struct StretchParams1Channel { // Stretch algorithm parameters float shadows;; float highlights; float midtones; // The extension parameters are not yet used. float shadows_expansion; float highlights_expansion; // The default parameters result in no stretch at all. StretchParams1Channel() { shadows = 0.0; highlights = 1.0; midtones = 0.5; shadows_expansion = 0.0; highlights_expansion = 1.0; } }; struct StretchParams { StretchParams1Channel grey_red, green, blue; }; class Stretch { public: /** * @brief Stretch Constructor for Stretch class * @param image_buffer pointer to the image memory * @param width the image width * @param height the image height * @param channels should be 1 or 3 * @note The image should either be 1-channel or 3-channel * The image buffer is not copied, so it should not be deleted while the object is in use */ explicit Stretch(int width, int height, int channels, int data_type); ~Stretch() {} /** * @brief setParams Sets the stretch parameters. * @param param The desired parameter values. * @note This set method used for both 1-channel and 3-channel images. * In 1-channel images, the _g and _b parameters are ignored. * The parameter scale is 0-1 for all data types. */ void setParams(StretchParams input_params) { params = input_params; } /** * @brief getParams Returns the stretch parameters (computed by computeParameters()). */ StretchParams getParams() { return params; } /** * @brief computeParams Automatically generates and sets stretch parameters from the image. */ - StretchParams computeParams(uint8_t *input); + StretchParams computeParams(const uint8_t *input); /** * @brief run run the stretch algorithm according to the params given * placing the output in output_image. * @param input the raw data buffer. * @param output_image a QImage pointer that should be the same size as the input if * sampling is 1 otherwise, the proper size if the input is downsampled by sampling. * @param sampling The sampling parameter. Applies to both width and height. * Sampling is applied to the output (that is, with sampling=2, we compute every other output * sample both in width and height, so the output would have about 4X fewer pixels. */ - void run(uint8_t *input, QImage *output_image, int sampling=1); + void run(uint8_t const *input, QImage *output_image, int sampling=1); private: // Adjusts input_range for float and double types. - void recalculateInputRange(uint8_t *input); + void recalculateInputRange(const uint8_t *input); // Inputs. int image_width; int image_height; int image_channels; int input_range; int dataType; // Parameters. StretchParams params; };