Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
New polar-alignment scheme
murveit committed Jan 10, 2021
1 parent 4635b3b commit cbe3311
Showing 19 changed files with 1,567 additions and 1,296 deletions.
7 changes: 7 additions & 0 deletions Tests/polaralign/CMakeLists.txt
@@ -1,3 +1,10 @@
ADD_EXECUTABLE( test_poleaxis test_poleaxis.cpp )
TARGET_LINK_LIBRARIES( test_poleaxis ${TEST_LIBRARIES})
ADD_TEST( NAME TestPoleAxis COMMAND test_poleaxis )
ADD_EXECUTABLE( test_polaralign test_polaralign.cpp )
TARGET_LINK_LIBRARIES( test_polaralign ${TEST_LIBRARIES})
ADD_TEST( NAME TestPolarAlign COMMAND test_polaralign )
ADD_CUSTOM_COMMAND( TARGET test_polaralign POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/../fitsviewer/ngc4535-autofocus1.fits
${CMAKE_CURRENT_BINARY_DIR}/ngc4535-autofocus1.fits)
556 changes: 446 additions & 110 deletions Tests/polaralign/test_polaralign.cpp

Large diffs are not rendered by default.

61 changes: 26 additions & 35 deletions Tests/polaralign/test_polaralign.h
@@ -1,35 +1,27 @@
/***************************************************************************
test_polaralign.h - KStars Planetarium
-------------------
begin : Tue 27 Sep 2016 20:51:21 CDT
copyright : (c) 2020 by Chris Rowland
email :
***************************************************************************/

/***************************************************************************
* *
* 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. *
* *
***************************************************************************/

#ifndef TEST_POLARALIGN_H
#define TEST_POLARALIGN_H
/* TestPolarAlign class.
Copyright (C) 2021 Hy Murveit
This application is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
*/

#pragma once

#include <QtTest/QtTest>
#include <QDebug>
#include <QString>

#define UNIT_TEST

#include "../../kstars/ekos/align/poleaxis.h"
#include "../../kstars/ekos/align/polaralign.h"

/**
* @class TestPolarAlign
* @short Tests for some polar align operations
* @author Chris Rowland
* @short Tests for the PolarAlign class.
* @author Hy Murveit
*/

class TestPolarAlign : public QObject
@@ -40,19 +32,18 @@ class TestPolarAlign : public QObject
TestPolarAlign();
~TestPolarAlign() override;

private slots:
void testDirCos_data();
void testDirCos();
void testPriSec_data();
void testPriSec();
void testPoleAxis_data();
void testPoleAxis();

private:
void compare(QVector3D v, double x, double y, double z);
void compare(double a, double e, QString msg = "");
void compare(float a, double e, QString msg = "") { compare (static_cast<double>(a), e, msg); }
};
private slots:
void testRunPAA();
void testAlt();
void testRotate();
void testRotate_data();

private:
void compare(double a, double e, QString msg = "", double tolerance = 0.0003);
void compare(const QPointF &point, double x, double y, double tolerance = 0.0001);

void getAzAlt(const KStarsDateTime &time, const GeoLocation &geo,
const QPointF &pixel, double ra, double dec, double orientation,
double pixScale, double *az, double *alt);

#endif
};
161 changes: 161 additions & 0 deletions Tests/polaralign/test_poleaxis.cpp
@@ -0,0 +1,161 @@
/***************************************************************************
test_poleaxis.cpp - KStars Planetarium
-------------------
begin : Tue 27 Sep 2016 20:54:28 CDT
copyright : (c) 2016 by Akarsh Simha
email : akarsh.simha@kdemail.net
***************************************************************************/

/***************************************************************************
* *
* 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. *
* *
***************************************************************************/

/* Project Includes */
#include "test_poleaxis.h"
#include "ksnumbers.h"
#include "time/kstarsdatetime.h"
#include "auxiliary/dms.h"
#include "Options.h"
#include <libnova/libnova.h>

TestPoleAxis::TestPoleAxis() : QObject()
{
}

TestPoleAxis::~TestPoleAxis()
{
}

void TestPoleAxis::compare(PoleAxis::V3 v, double x, double y, double z)
{
QVERIFY2(std::fabs(v.x() - x) < 0.00001,
qPrintable(QString("dc.x %1, x %2 error %3").arg(v.x()).arg(x).arg(((v.x() - x) * 3600.0), 6, 'f', 1)));
QVERIFY2(std::fabs(v.y() - y) < 0.00001,
qPrintable(QString("dc.y %1, y %2 error %3").arg(v.y()).arg(y).arg(((v.y() - y) * 3600.0), 6, 'f', 1)));
QVERIFY2(std::fabs(v.z() - z) < 0.00001,
qPrintable(QString("dc.z %1, z %2 error %3").arg(v.z()).arg(z).arg(((v.z() - z) * 3600.0), 6, 'f', 1)));
}

void TestPoleAxis::compare(double a, double e, QString msg)
{
QVERIFY2(std::fabs(a - e) < 0.0003,
qPrintable(QString("%1: actual %2, expected %3 error %4").arg(msg).arg(a).arg(e).arg(((a - e) * 3600.0), 6, 'f', 1)));
}


void TestPoleAxis::testDirCos_data()
{
QTest::addColumn<double>("Ha");
QTest::addColumn<double>("Dec");
QTest::addColumn<double>("X");
QTest::addColumn<double>("Y");
QTest::addColumn<double>("Z");

QTest::newRow("HaDec0") << 0.0 << 0.0 << 1.0 << 0.0 << 0.0;
QTest::newRow("Ha0Dec45") << 0.0 << 45.0 << 0.707107 << 0.0 << 0.707107;
QTest::newRow("Ha6Dec45") << 6.0 << 45.0 << 0.0 << 0.707107 << 0.707107;
QTest::newRow("Ha-6Dec45") << -6.0 << 45.0 << 0.0 << -0.707107 << 0.707107;
QTest::newRow("at Pole") << 0.0 << 90.0 << 0.0 << 0.0 << 1.0;
QTest::newRow("near S Pole") << -3.0 << -85.0 << 0.0616284 << -0.0616284 << -0.996195;
}

void TestPoleAxis::testDirCos()
{
dms h;
dms d;
QFETCH(double, Ha);
QFETCH(double, Dec);
h.setH(Ha);
d.setD(Dec);


PoleAxis::V3 dc;
dc = PoleAxis::dirCos(h, d);


QFETCH(double, X);
QFETCH(double, Y);
QFETCH(double, Z);

compare(dc, X, Y, Z);

SkyPoint sp(Ha, Dec);
dc = PoleAxis::dirCos(sp);
compare(dc.length(), 1.0, "length");
compare(dc, X, Y, Z);
}

void TestPoleAxis::testPriSec_data()
{
testDirCos_data();
}

void TestPoleAxis::testPriSec()
{
QFETCH(double, Ha);
QFETCH(double, Dec);
QFETCH(double, X);
QFETCH(double, Y);
QFETCH(double, Z);
PoleAxis::V3 dc(X, Y, Z);
dms p = PoleAxis::primary(dc);
compare(p.HoursHa(), Ha, "Ha");
compare(PoleAxis::secondary(dc).Degrees(), Dec, "Dec");
}


void TestPoleAxis::testPoleAxis_data()
{
QTest::addColumn<double>("Ha1");
QTest::addColumn<double>("Dec1");
QTest::addColumn<double>("Ha2");
QTest::addColumn<double>("Dec2");
QTest::addColumn<double>("Ha3");
QTest::addColumn<double>("Dec3");
QTest::addColumn<double>("X");
QTest::addColumn<double>("Y");
QTest::addColumn<double>("Z");

QTest::newRow("Ha-606Dec0") << -6.0 << 0.0 << 0.0 << 0.0 << 6.0 << 0.0 << 0.0 << 0.0 << 1.0;
QTest::newRow("Ha20-2Dec89") << 2.0 << 89.0 << 0.0 << 89.0 << -2.0 << 89.0 << 0.0 << 0.0 << -1.0;
QTest::newRow("Ha0-22Dec89v") << 0.0 << 89.0 << -2.0 << 89.1 << 2.0 << 88.9 << -0.0006 << -0.003386 << -0.99999;
QTest::newRow("Ha2-20Dec-89") << 2.0 << -89.0 << -2.0 << -89.0 << 0.0 << -89.0 << 0.0 << 0.0 << 1.0;
QTest::newRow("Ha20-2Dec89") << 2.0 << 89.0 << 0.0 << 89.0 << -2.0 << 88.0 << 0.05633683 << 0.0150954 << 0.998298;
// failure cases, 2 or more points the same should ruturn a null matrix
QTest::newRow("Ha000Dec0") << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0;
QTest::newRow("Ha100Dec0") << 1.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0;
QTest::newRow("Ha110Dec0") << 1.0 << 0.0 << 1.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0 << 0.0;
QTest::newRow("Ha011Dec0") << 0.0 << 0.0 << 1.0 << 0.0 << 1.0 << 0.0 << 0.0 << 0.0 << 0.0;
}

void TestPoleAxis::testPoleAxis()
{
QFETCH(double, Ha1);
QFETCH(double, Dec1);
QFETCH(double, Ha2);
QFETCH(double, Dec2);
QFETCH(double, Ha3);
QFETCH(double, Dec3);

QFETCH(double, X);
QFETCH(double, Y);
QFETCH(double, Z);

SkyPoint p1(Ha1, Dec1);
SkyPoint p2(Ha2, Dec2);
SkyPoint p3(Ha3, Dec3);


PoleAxis::V3 pa = PoleAxis::poleAxis(p1, p2, p3);

compare(pa.x(), X, "X");
compare(pa.y(), Y, "Y");
compare(pa.z(), Z, "Z");
}

QTEST_GUILESS_MAIN(TestPoleAxis)
62 changes: 62 additions & 0 deletions Tests/polaralign/test_poleaxis.h
@@ -0,0 +1,62 @@
/***************************************************************************
test_poleaxis.h - KStars Planetarium
-------------------
begin : Tue 27 Sep 2016 20:51:21 CDT
copyright : (c) 2020 by Chris Rowland
email :
***************************************************************************/

/***************************************************************************
* *
* 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. *
* *
***************************************************************************/

#ifndef TEST_POLEAXIS_H
#define TEST_POLEAXIS_H

#include <QtTest/QtTest>
#include <QDebug>
#include <QString>

#define UNIT_TEST

#include "../../kstars/ekos/align/poleaxis.h"

/**
* @class TestPoleAxis
* @short Tests for some polar align operations
* @author Chris Rowland
*/

class TestPoleAxis : public QObject
{
Q_OBJECT

public:
TestPoleAxis();
~TestPoleAxis() override;

private slots:
void testDirCos_data();
void testDirCos();
void testPriSec_data();
void testPriSec();
void testPoleAxis_data();
void testPoleAxis();

private:
void compare(PoleAxis::V3 v, double x, double y, double z);
void compare(double a, double e, QString msg = "");
void compare(float a, double e, QString msg = "")
{
compare (static_cast<double>(a), e, msg);
}

};


#endif
1 change: 1 addition & 0 deletions kstars/CMakeLists.txt
@@ -207,6 +207,7 @@ if (INDI_FOUND)
#ekos/align/onlineastrometryparser.cpp
ekos/align/remoteastrometryparser.cpp
#ekos/align/astapastrometryparser.cpp
ekos/align/poleaxis.cpp
ekos/align/polaralign.cpp

# Guide
882 changes: 111 additions & 771 deletions kstars/ekos/align/align.cpp

Large diffs are not rendered by default.

42 changes: 19 additions & 23 deletions kstars/ekos/align/align.h
@@ -9,6 +9,7 @@

#pragma once


#include "ui_align.h"
#include "ui_mountmodel.h"
#include "ui_manualrotator.h"
@@ -19,6 +20,7 @@
#include "indi/indidome.h"
#include "ksuserdb.h"
#include "ekos/auxiliary/filtermanager.h"
#include "polaralign.h"

#include <QTime>
#include <QTimer>
@@ -504,6 +506,8 @@ class Align : public QWidget, public Ui::Align
void setCaptureStatus(Ekos::CaptureState newState);
// Update Mount module status
void setMountStatus(ISD::Telescope::Status newState);
void setMountCoords(const QString &ra, const QString &dec, const QString &az,
const QString &alt, int pierSide, const QString &ha);

// PAH Ekos Live
QString getPAHStage() const
@@ -645,6 +649,11 @@ class Align : public QWidget, public Ui::Align
bool m_SolveBlindly = false;
KPageWidgetItem *indexFilesPage;
QString savedOptionsProfiles;
/**
* @brief Warns the user if the polar alignment might cross the meridian.
*/
bool checkPAHForMeridianCrossing();

/**
* @brief Calculate Field of View of CCD+Telescope combination that we need to pass to astrometry.net solver.
*/
@@ -727,19 +736,13 @@ class Align : public QWidget, public Ui::Align
*/
void syncCorrectionVector();

void setupCorrectionGraphics(const QPointF &pixel, QLineF *correctionVector);

/**
* @brief processPAHStage After solver is complete, handle PAH Stage processing
*/
void processPAHStage(double orientation, double ra, double dec, double pixscale);

CircleSolution findCircleSolutions(const QPointF &p1, const QPointF p2, double angle,
QPair<QPointF, QPointF> &circleSolutions);

double distance(const QPointF &p1, const QPointF &p2);
bool findRACircle(QVector3D &RACircle);
bool isPerpendicular(const QPointF &p1, const QPointF &p2, const QPointF &p3);
bool calcCircle(const QPointF &p1, const QPointF &p2, const QPointF &p3, QVector3D &RACircle);

void resizeEvent(QResizeEvent *event) override;

bool alignmentPointsAreBad();
@@ -924,23 +927,9 @@ class Align : public QWidget, public Ui::Align
bool rememberSolverWCS { false };
//bool rememberMeridianFlip { false };

// Sky centers
typedef struct
{
SkyPoint skyCenter;
QPointF celestialPole;
QPointF pixelCenter;
double pixelScale { 0 };
double orientation { 0 };
KStarsDateTime ts;
} PAHImageInfo;

QVector<PAHImageInfo *> pahImageInfos;

// User desired offset when selecting a bright star in the image
QPointF celestialPolePoint, correctionOffset, RACenterPoint;
// Correction vector line between mount RA Axis and celestial pole
QLineF correctionVector;
QPointF correctionTo, correctionFrom, correctionAltTo;

// CCDs using Guide Scope for parameters
//QStringList guideScopeCCDs;
@@ -983,6 +972,10 @@ class Align : public QWidget, public Ui::Align
bool m_isRateSynced = false;
bool domeReady = true;

// Current mount pointing state.
dms mountRa, mountDec, mountAz, mountAlt, mountHa;
ISD::Telescope::PierSide mountPierSide { ISD::Telescope::PierSide::PIER_UNKNOWN };

// CCD Exposure Looping
bool rememberCCDExposureLooping = { false };

@@ -1007,5 +1000,8 @@ class Align : public QWidget, public Ui::Align

// Threshold to stop PAH rotation in degrees
static constexpr uint8_t PAH_ROTATION_THRESHOLD { 5 };

// Class used to estimate alignment error.
PolarAlign polarAlign;
};
}
2 changes: 1 addition & 1 deletion kstars/ekos/align/align.ui
@@ -1516,7 +1516,7 @@
<item>
<widget class="QLabel" name="refreshText">
<property name="text">
<string>&lt;p&gt;Adjust mount's Altitude and Azimuth knobs until the selected star is centered within the crosshair. Click &lt;b&gt;Refresh&lt;/b&gt; to begin continuous capture. You are &lt;b&gt;Done&lt;/b&gt; when star is centered.&lt;/p&gt;</string>
<string>&lt;p&gt;Adjust mount's Altitude knob moving star along yellow line, then adjust Azimuth knobs moving along purple line until the selected star is centered within the crosshair. Click &lt;b&gt;Refresh&lt;/b&gt; to begin continuous capture. You are &lt;b&gt;Done&lt;/b&gt; when star is centered.&lt;/p&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
39 changes: 36 additions & 3 deletions kstars/ekos/align/alignview.cpp
@@ -41,7 +41,7 @@ void AlignView::drawOverlay(QPainter *painter, double scale)
drawLine(painter);
}

bool AlignView::injectWCS(double orientation, double ra, double dec, double pixscale)
bool AlignView::injectWCS(double orientation, double ra, double dec, double pixscale, bool extras)
{
bool rc = imageData->injectWCS(orientation, ra, dec, pixscale);
// If file fails to load, then no WCS data
@@ -55,14 +55,14 @@ bool AlignView::injectWCS(double orientation, double ra, double dec, double pixs
if (wcsWatcher.isRunning() == false && imageData->getWCSState() == FITSData::Idle)
{
// Load WCS async
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS);
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, extras);
wcsWatcher.setFuture(future);
}

return true;
}

void AlignView::setCorrectionParams(QLineF &line)
void AlignView::setCorrectionParams(QLineF &line, QLineF *altOnlyLine)
{
if (imageData.isNull())
return;
@@ -79,6 +79,11 @@ void AlignView::setCorrectionParams(QLineF &line)
}

correctionLine = line;
if (altOnlyLine == nullptr)
altLine = QLineF();
else
altLine = *altOnlyLine;

celestialPolePoint = line.p1();
markerCrosshair = line.p2();

@@ -136,6 +141,34 @@ void AlignView::drawLine(QPainter *painter)

painter->drawLine(x1, y1, x2, y2);

// If there is an alt line, then draw a separate path, first the altLine,
// Then from the 2nd point in the altLine to the point in correctionLine
// that differs from altLine's first point.
if (correctionOffset.isNull() && !altLine.isNull())
{
painter->setPen(QPen(Qt::yellow, 3));
painter->setBrush(Qt::NoBrush);
double x1 = altLine.p1().x() * scale;
double y1 = altLine.p1().y() * scale;
const double x2 = altLine.p2().x() * scale;
const double y2 = altLine.p2().y() * scale;
painter->drawLine(x1, y1, x2, y2);

painter->setPen(QPen(Qt::green, 3));
if ((altLine.p1().x() != correctionLine.p2().x()) ||
(altLine.p1().y() != correctionLine.p2().y()))
{
x1 = correctionLine.p2().x() * scale;
y1 = correctionLine.p2().y() * scale;
}
else
{
x1 = correctionLine.p1().x() * scale;
y1 = correctionLine.p1().y() * scale;
}
painter->drawLine(x2, y2, x1, y1);
}

// In limited memory mode, WCS data is not loaded so no Equatorial Gridlines are drawn
// so we have to at least draw the NCP/SCP locations
if (Options::limitedResourcesMode())
5 changes: 3 additions & 2 deletions kstars/ekos/align/alignview.h
@@ -24,12 +24,12 @@ class AlignView : public FITSView
explicit AlignView(QWidget *parent = nullptr, FITSMode mode = FITS_NORMAL, FITSScale filter = FITS_NONE);

/* Calculate WCS header info and update WCS info */
bool injectWCS(double orientation, double ra, double dec, double pixscale);
bool injectWCS(double orientation, double ra, double dec, double pixscale, bool extras = true);

void drawOverlay(QPainter *, double scale) override;

// Correction line
void setCorrectionParams(QLineF &line);
void setCorrectionParams(QLineF &line, QLineF *altOnlyLine = nullptr);
void setCorrectionOffset(QPointF &newOffset);

void setRACircle(const QVector3D &value);
@@ -44,6 +44,7 @@ class AlignView : public FITSView
private:
// Correction Line
QLineF correctionLine;
QLineF altLine;
QPointF correctionOffset, celestialPolePoint;
QVector3D RACircle;

337 changes: 300 additions & 37 deletions kstars/ekos/align/polaralign.cpp
@@ -1,6 +1,5 @@
/* polaralign.cpp determines the mount polar axis position
Copyright (C) 2020 Chris Rowland <chris.rowland@cherryfield.me.uk>
/* PolarAlign class.
Copyright (C) 2021 Hy Murveit
This application is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
@@ -9,57 +8,321 @@
*/

#include "polaralign.h"
#include "poleaxis.h"

#include <cmath>

QVector3D PolarAlign::dirCos(const dms primary, const dms secondary)
#include "fitsviewer/fitsdata.h"
#include "kstarsdata.h"
#include "skypoint.h"
#include <ekos_align_debug.h>

/******************************************************************
PolarAlign is a class that supports polar alignment by determining the
mount's axis of rotation when given 3 solved images taken with RA mount
rotations between the images.
addPoint(image) is called by the polar alignment UI after it takes and
solves each of its three images. The solutions are store in SkyPoints (see below)
and are processed so that the sky positions correspond to "what's in the sky
now" and "at this geographic localtion".
Addpoint() samples the location of a particular pixel in its image.
When the 3 points are sampled, they should not be taken
from the center of the image, as HA rotations may not move that point
if the telescope and mount are well aligned. Thus, the points are sampled
from the edge of the image.
After all 3 images are sampled, findAxis() is called, which calls
PoleAxis::poleAxis() to solve for the mount's axis of rotation. FindAxis
then transforms poleAxis' result into azimuth and altitude offsets from the pole.
findCorrectedPixel() is given an x,y position on an image, and the offsets
generated by findAxis(), and computes a "corrected position" for that input
x,y point, such that if a user adjusted the GEM mount's altitude and azimuth
controls to move an object in the image's original x,y position to the corrected
position in the image, the mount's axis of rotation should then coincide with the pole.
******************************************************************/


PolarAlign::PolarAlign(const GeoLocation *geo)
{
if (geo == nullptr && KStarsData::Instance() != nullptr)
geoLocation = KStarsData::Instance()->geo();
else
geoLocation = geo;
}

bool PolarAlign::northernHemisphere() const
{
if ((geoLocation == nullptr) || (geoLocation->lat() == nullptr))
return true;
return geoLocation->lat()->Degrees() > 0;
}

void PolarAlign::reset()
{
points.clear();
times.clear();
}

// Gets the pixel's j2000 RA&DEC coordinates, converts to JNow, adjust to
// the local time, and sets up the azimuth and altitude coordinates.
bool PolarAlign::prepareAzAlt(FITSData *image, const QPointF &pixel, SkyPoint *point) const
{
return QVector3D(
static_cast<float>(secondary.cos() * primary.cos()),
static_cast<float>(secondary.cos() * primary.sin()),
static_cast<float>(secondary.sin()));
// WCS must be set up for this image.
SkyPoint coords;
if (image->pixelToWCS(pixel, coords))
{
coords.apparentCoord(static_cast<long double>(J2000), image->getDateTime().djd());
*point = SkyPoint::timeTransformed(&coords, image->getDateTime(), geoLocation, 0);
return true;
}
return false;
}

QVector3D PolarAlign::dirCos(const SkyPoint sp)
bool PolarAlign::addPoint(FITSData *image)
{
return dirCos(sp.ra(), sp.dec());
// Use the HA and DEC from the top-left of the image. Any point far from the center
// of the image will do. The reason we don't use the center is as follows:
// The goal is to measure 3 points with the mount rotated in 3 different anlges, and from
// that deduce the mount's axis. If we use the center of the image, and the telescope is
// very well aligned with the mount's axis, then all three measurements would be about
// the same RA and DEC. This would make it difficult to estimate the mount's axis.
// If, however, we offset the measurement from the center of the image,
// then the measurements will differ in the 3 different rotations.
SkyPoint coords;
auto time = image->getDateTime();
// 0,0 is the upper left corner of the image.
if (!prepareAzAlt(image, QPointF(0, 0), &coords))
return false;

QString debugString = QString("PAA: addPoint ra0 %1 dec0 %2 ra %3 dec %4 az %5 alt %6")
.arg(coords.ra0().Degrees()).arg(coords.dec0().Degrees())
.arg(coords.ra().Degrees()).arg(coords.dec().Degrees())
.arg(coords.az().Degrees()).arg(coords.alt().Degrees());
qCDebug(KSTARS_EKOS_ALIGN) << debugString;
if (points.size() > 2)
return false;
points.push_back(coords);
times.push_back(time);

return true;
}

dms PolarAlign::primary(QVector3D dirCos)
namespace
{
dms p;
p.setRadians(static_cast<double>(std::atan2(dirCos.y(), dirCos.x())));
return p;
// Output is between -180 and +180 degrees.
double convertDegreesRange(double degrees)
{
while (degrees < -180.0)
degrees += 360.0;
while (degrees > 180.0)
degrees -= 360.0;
return degrees;
}
} // namespace

dms PolarAlign::secondary(QVector3D dirCos)
bool PolarAlign::findAxis()
{
dms p;
p.setRadians(static_cast<double>(std::asin(dirCos.z())));
return p;
if (points.size() != 3)
return false;

SkyPoint sp;

// poleAxis uses the .ra() and .dec() coordinates, which have already been converted
// to JNow. The returned unit vector points towards the pole. We need to
// convert it to an azimuth and altitiude.
PoleAxis::V3 pa = PoleAxis::poleAxis(points[0], points[1], points[2]);

if (pa.x() * pa.x() + pa.y() + pa.y() + pa.z() * pa.z() < 0.9)
{
// It failed to normalize the vector, something's wrong.
qCDebug(KSTARS_EKOS_ALIGN) << "Normal vector too short. FindAxis failed.";
return false;
}

// Need to make sure we're pointing to the right pole.
if (northernHemisphere() && (pa.z() < 0))
pa = PoleAxis::V3(-pa.x(), -pa.y(), -pa.z());

if (!northernHemisphere() && pa.z() > 0)
{
pa = PoleAxis::V3(-pa.x(), -pa.y(), -pa.z());
// need to add 12 hours to the RA
dms ra = PoleAxis::primary(pa);
ra.setD(ra.Degrees() + 180.0);
sp.setRA(ra);
}
else
// This else clause runs for everything except a southern "flip".
sp.setRA(PoleAxis::primary(pa));

sp.setDec(PoleAxis::secondary(pa));

SkyPoint transformed = sp.timeTransformed(&sp, times[2], geoLocation, 0);

// Want the results in the range of -180 to +180 degrees
azimuthCenter = convertDegreesRange(transformed.az().Degrees());
altitudeCenter = transformed.alt().Degrees();

return true;
}

SkyPoint PolarAlign::skyPoint(QVector3D dc)
void PolarAlign::getAxis(double *azAxis, double *altAxis) const
{
return SkyPoint(primary(dc), secondary(dc));
*azAxis = azimuthCenter;
*altAxis = altitudeCenter;
}

QVector3D PolarAlign::poleAxis(SkyPoint p1, SkyPoint p2, SkyPoint p3)
// Find the pixel in image corresponding to the desired azimuth & altitude.
bool PolarAlign::findAzAlt(FITSData *image, double azimuth, double altitude, QPointF *pixel) const
{
// convert the three positions to vectors, these define the plane
// of the Ha axis rotation
QVector3D v1 = PolarAlign::dirCos(p1);
QVector3D v2 = PolarAlign::dirCos(p2);
QVector3D v3 = PolarAlign::dirCos(p3);

// the Ha axis direction is the normal to the plane
QVector3D p = QVector3D::normal(v1, v2, v3);

// p points to the north or south pole depending on the rotation of the points
// the other pole position can be determined by reversing the sign of the Dec and
// adding 12hrs to the Ha value.
// if we want only the north then this would do it
//if (p.z() < 0)
// p = -p;
return p;
SkyPoint spt;
spt.setAz(azimuth);
spt.setAlt(altitude);
dms LST = geoLocation->GSTtoLST(image->getDateTime().gst());
spt.HorizontalToEquatorial(&LST, geoLocation->lat());
SkyPoint j2000Coord = spt.catalogueCoord(image->getDateTime().djd());
QPointF imagePoint;
if (!image->wcsToPixel(j2000Coord, *pixel, imagePoint))
{
QString debugString =
QString("PolarAlign: Couldn't get pixel from WCS for az %1 alt %2 with j2000 RA %3 DEC %4")
.arg(azimuth).arg(altitude).arg(j2000Coord.ra0().toHMSString())
.arg(j2000Coord.dec0().toDMSString());
qCDebug(KSTARS_EKOS_ALIGN) << debugString;
return false;
}
return true;
}

// Calculate the mount's azimuth and altitude error given the known geographic location
// and the azimuth center and altitude center computed in findAxis().
void PolarAlign::calculateAzAltError(double *azError, double *altError) const
{

const double latitudeDegrees = geoLocation->lat()->Degrees();
*altError = northernHemisphere() ?
altitudeCenter - latitudeDegrees : altitudeCenter + latitudeDegrees;
*azError = northernHemisphere() ? azimuthCenter : azimuthCenter + 180.0;
while (*azError > 180.0)
*azError -= 360;

dms polarError(hypot(*altError, *azError));
QString debugString = QString("PAA Error: %1 %2 up: %3 %4 az: %5 %6 latitude %7")
.arg(polarError.toDMSString()).arg(hypot(*altError, *azError), 6, 'f', 3)
.arg(dms(*altError).toDMSString()).arg(*altError, 6, 'f', 3)
.arg(dms(*azError).toDMSString()).arg(*azError, 6, 'f', 3)
.arg(latitudeDegrees, 6, 'f', 3);
qCDebug(KSTARS_EKOS_ALIGN) << debugString;
}

// Given a pixel, find its RA/DEC, then its alt/az, and then search for another pixel
// with the right correction.
bool PolarAlign::findCorrectedPixel(FITSData *image, const QPointF &pixel, QPointF *corrected, bool altOnly)
{
double azOffset, altOffset;
calculateAzAltError(&azOffset, &altOffset);
auto time = image->getDateTime();

// 1. Find the az/alt for the x,y point on the image.
SkyPoint p;
if (!prepareAzAlt(image, pixel, &p))
return false;
double pixelAz = p.az().Degrees(), pixelAlt = p.alt().Degrees();

QString debugString = QString("PAA: Pixel(%1,%2): Az: %3, Alt: %4")
.arg(pixel.x(), 4, 'f', 0).arg(pixel.y(), 4, 'f', 0)
.arg(pixelAz).arg(pixelAlt);
qCDebug(KSTARS_EKOS_ALIGN) << debugString;

// 2. Apply the az/alt offsets.
// We know that the pole's az and alt offsets are effectively rotations
// of a sphere. The offsets that apply to correct different points depend
// on where (in the sphere) those points are. Points close to the pole can probably
// just add the pole's offsets. This calculation is a bit more precise, and is
// necessary if the points are not near the pole.
QPointF rotated;
if (altOnly)
azOffset = 0.0;
PolarAlign::rotate(QPointF(pixelAz, pixelAlt), QPointF(azOffset, altOffset), &rotated);
const double targetAz = rotated.x(), targetAlt = rotated.y();

debugString = QString("PAA: Target%1: Az: %2, Alt: %3")
.arg(altOnly ? "--Only Alt" : "").arg(targetAz, 6, 'f', 3).arg(targetAlt, 6, 'f', 3);
qCDebug(KSTARS_EKOS_ALIGN) << debugString;

// 3. Find a pixel with those alt/az values.
if (!findAzAlt(image, targetAz, targetAlt, corrected))
return false;

return true;
}

double d2r(double degrees)
{
return 2 * M_PI * degrees / 360.0;
}

void PolarAlign::rotate(const QPointF &azAltPoint, const QPointF &azAltRotation, QPointF *azAltResult) const
{
const double alt = azAltPoint.y();

// First rotate Azimuth
const double az = azAltPoint.x() + azAltRotation.x();

const double azRadians = d2r(az);
const double altRadians = d2r(alt);

// Convert the new point to xyz
// See https://mathworld.wolfram.com/SphericalCoordinates.html
// In this coordinate system, x points to the pole
// y points to the left
// z points up.
// In this system, theta is the angle between x & y and is related
// to our azimuth: theta = 90-azimuth.
// Phi is the angle away from z, and is related to our altitude as 90-altitude.
const double theta = -azRadians;
const double phi = (M_PI / 2.0) - altRadians;
const double x = cos(theta) * sin(phi);
const double y = sin(theta) * sin (phi);
const double z = cos(phi);

// Multiply [x,y,z] by the rotate-Y by "angle" rotation matrix
// as in https://en.wikipedia.org/wiki/Rotation_matrix
// cos(angle) 0 sin(angle)
// 0 1 0
// -sin(angle) 0 cos(angle)

const double angle = northernHemisphere() ?
d2r(-azAltRotation.y()) : d2r(azAltRotation.y());

const double x2 = x * cos(angle) + z * sin(angle);
const double y2 = y;
const double z2 = -x * sin(angle) + z * cos(angle);

// Convert back to theta and phi
double theta2;
const double phi2 = acos(z2);

// Deal with degenerate values for the atan along the meridian (y == 0).
if (y == 0.0 && x == 0.0)
{
// Straight overhead
azAltResult->setX(0.0);
azAltResult->setY(90.0);
return;
}
else if (y == 0)
theta2 = 0.0;
else
theta2 = atan2(y2, x2);

// Convert back to az alt
const double az2Radians = -theta2;
const double alt2Radians = (M_PI / 2.0) - phi2;

azAltResult->setX(360 * az2Radians / (2 * M_PI));
azAltResult->setY(360 * alt2Radians / (2 * M_PI));
}
161 changes: 100 additions & 61 deletions kstars/ekos/align/polaralign.h
@@ -1,76 +1,115 @@
/* polaralign.h determines the mount polar axis position
Copyright (C) 2020 Chris Rowland <chris.rowland@cherryfield.me.uk>
/* PolarAlign class.
Copyright (C) 2021 Hy Murveit
This application is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
*/

#ifndef POLARALIGN_H
#define POLARALIGN_H
#pragma once

#include <QVector3D>
#include <dms.h>
#include <skypoint.h>

/**
*@class PolarAlign
*@short PolarAlign class handles determining the mount Ha axis position given three positions taken with the same mount declination.
*
*@author Chris Rowland
*@version 1.0
*/
class FITSData;
class TestPolarAlign;

/*********************************************************************
Polar alignment support class. Note, the telescope can be pointing anywhere.
It doesn't need to point at the pole.
Use this class as follows:
1) Construct with the geo information.
PolarAlign polarAlign(geoLocation);
2) Start the polar align procedure. Capture, solve and add wcs from the
solve to the FITSData image. Then:
polarAlign.addPoint(image);
3) Rotate the mount in RA ~30 degrees (could be less or more) east (or west)
and capture, solve and add wcs from the solve to the the new image. Then:
polarAlign.addPoint(image);
4) Rotate the mount in RA another ~30 degrees east (or west)
and capture, solve and add wcs from the solve to the the new image. Then:
polarAlign.addPoint(image);
5) Find the mount's axis of rotation as follows:
if (!polarAlign.findAxis())
error();
6) Compute the azimuth and altitude offset for the mount.
double altitudeError = axisAlt - latitudeDegrees;
double azimuthError = axisAz - 0;
7) Compute the overall error
dms polarError(hypot(altitudeError, azimuthError));
8) Compute a target correction
int correctedX, correctedY;
if (!polarAlign.findCorrectedPixel(
imageData, x, y, altitudeError, azimuthError, &correctedX, &correctedY))
error();
9) The user should use the GEM azimuth and altitude adjustments to move the
object at position x,y in the image to position correctedX, correctedY.
*********************************************************************/

class PolarAlign
{
public:
///
/// \brief dirCos converts primary and secondary angles to a directional cosine
/// \param primary angle, can be Ra, Ha, Azimuth or the corresponding axis values
/// \param secondary angle, can be Dec, Altitude. 90 deg is the pole
/// \return QVector3D containing the directional cosine.
static QVector3D dirCos(const dms primary, const dms secondary);

///
/// \brief dirCos converts a SkyPoint to a directional cosine
/// \param sp SkyPoint with the position
/// \return QVector3D containing the directional cosine.
///
static QVector3D dirCos(const SkyPoint sp);

///
/// \brief primary returns the primary dms value in the directional cosine
/// \param dirCos
/// \return primary angle, Ra, Ha, Azimuth etc.
///
static dms primary(QVector3D dirCos);

///
/// \brief secondary returns the secondary dms angle in the directional cosine
/// \param dirCos
/// \return
///
static dms secondary(QVector3D dirCos);

///
/// \brief skyPoint returns a skypoint derived from the directional cosine vector
/// \param dc
/// \return
///
static SkyPoint skyPoint(QVector3D dc);

///
/// \brief poleAxis returns the pole axis vector given three SkyPoints with the same mount declination
/// \param p1
/// \param p2
/// \param p3
/// \return vector giving the direction of the pole. The rotation between the three points determines which pole
/// the other pole can be determined either by reversing the sign of the declination and adding 12 hrs to the Ha or
/// by negating the vector
///
static QVector3D poleAxis(SkyPoint p1, SkyPoint p2, SkyPoint p3);
public:

};
// The polealignment scheme requires the GeoLocation to operate properly.
// Certain aspects can be tested without it.
PolarAlign(const GeoLocation *geo = nullptr);

// Add a sample point.
bool addPoint(FITSData *image);

// Finds the mount's axis of rotation. Three points must have been added.
// Returns false if the axis can't be found.
bool findAxis();

// Returns the image coordinate that pixel x,y should be moved to to correct
// the mount's axis. Image is usually the 3rd PAA image. x,y are image coordinates.
// 3 Points must have been added and findAxis() must have been called.
// Uses the axis determined by findAxis(). Returns correctedX and correctedY,
// the target position that the x,y pixel should move to.
bool findCorrectedPixel(FITSData *image, const QPointF &pixel,
QPointF *corrected, bool altOnly = false);

// Returns the mount's azimuth and altitude error given the known geographic location
// and the azimuth center and altitude center computed in findAxis().
void calculateAzAltError(double *azError, double *altError) const;

#endif // POLARALIGN_H
/// reset starts the process over, removing the points.
void reset();

// Returns the mount's axis--for debugging.
void getAxis(double *azAxis, double *altAxis) const;

private:
// Rotate the az/alt point. Used when assisting the user to correct a polar alignment error.
// Input is the point to be rotated, Azimuth = azAltPoint.x(), Altitude = azAltPoint.y().
// azAltRotation: the rotation angles, which correspond to the error in polar alignment
// that we would like to correct at the pole. azAltResult.x() is the rotated azimuth coordinate.
// azAltResult.y() is the rotated altitude coordinate.
void rotate(const QPointF &azAltPoint, const QPointF &azAltRotation, QPointF *azAltResult) const;

// returns true in the northern hemisphere.
// if no geo location available, defaults to northern.
bool northernHemisphere() const;

// These internal methods find the pixel with the desired azimuth and altitude.
bool findAzAlt(FITSData *image, double azimuth, double altitude, QPointF *pixel) const;

// Does the necessary processing so that azimuth and altitude values
// can be retrieved for the x,y pixel in image.
bool prepareAzAlt(FITSData *image, const QPointF &pixel, SkyPoint *point) const;

// These three positions are used to estimate the polar alignment error.
QVector<SkyPoint> points;
QVector<KStarsDateTime> times;

// The geographic location used to compute altitude and azimuth.
const GeoLocation *geoLocation;

// Values set by the last call to findAxis() that correspond to the mount's axis.
double azimuthCenter { 0 };
double altitudeCenter { 0 };

friend TestPolarAlign;
};
131 changes: 131 additions & 0 deletions kstars/ekos/align/poleaxis.cpp
@@ -0,0 +1,131 @@
/* poleaxis.cpp determines the mount polar axis position
Copyright (C) 2020 Chris Rowland <chris.rowland@cherryfield.me.uk>
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 "poleaxis.h"

#include <cmath>


/******************************************************************
poleAxis(sp1, sp2, sp3) finds the mount's RA axis of rotation determined
by three points sampled by fixing the mount's DEC and sampling a point
a three different RA position.
For each SkyPoint sp, it finds its corresponding x,y,z coordinates,
which are points on a unit sphere. Those 3 coordinates define a plane.
That plane intesects the sphere, and the intersection of a plane and a
sphere is a circle. The center of that circle would be the axis of rotation
defined by the origial 3 points. So, finding the normal to the plane,
and pointing in that (normal) direction from the center of the sphere
(the origin) gives the axis of rotation of the mount.
poleAxis returns the normal. The way to interpret this vector
is a unit vector (direction) in the x,y,z space. To convert this back to an
angle useful for polar alignment, the ra and dec angles can be
retrieved with the primary(V) and secondary(V) functions.
These can then be turned into an altitude and azimuth angles using methods
in the SkyPoint class.
Further detail
Definitions: SkyPoint sp contains an hour angle ha and declication dec,
with the usual convention that dec=0 is the equator, dec=90 is the north pole
about which the axis spins, and ha is the spin angle
(positive is counter clockwise when looking north from the center of the sphere)
where 0-degrees would be pointing "up", e.g. toward the zenith when
looking at the north pole. The sphere has radius 1.0.
dirCos(sp) will return a 3D vector V whose components x,y,z
are 3D cartesean coordinate positions given these ha & dec angles
for points on the surface of a unit sphere.
The z direction points towards the north pole.
The y direction points left when looking at the north pole
The x direction points "up".
primary(V) where V contains the above x,y,z coordinates,
will return the ha angle pointing to V.
secondary(V) where V contains the above x,y,z coordinates,
will return the dec angle pointing to V.
******************************************************************/

PoleAxis::V3 PoleAxis::V3::normal(const V3 &v1, const V3 &v2, const V3 &v3)
{
// First subtract d21 = V2-V1; d31 = V3-V1
const V3 d21 = V3(v2.x() - v1.x(), v2.y() - v1.y(), v2.z() - v1.z());
const V3 d31 = V3(v3.x() - v2.x(), v3.y() - v2.y(), v3.z() - v2.z());
// Now take the cross-product of d21 and d31
const V3 cross = V3(d21.y() * d31.z() - d21.z() * d31.y(),
d21.z() * d31.x() - d21.x() * d31.z(),
d21.x() * d31.y() - d21.y() * d31.x());
// Finally normalize cross so that it is a unit vector.
const double lenSq = cross.x() * cross.x() + cross.y() * cross.y() + cross.z() * cross.z();
if (lenSq == 0.0) return V3();
const double len = sqrt(lenSq);
// Should we also fail if len < e.g. 5e-8 ??
return V3(cross.x() / len, cross.y() / len, cross.z() / len);
}

double PoleAxis::V3::length()
{
return sqrt(X * X + Y * Y + Z * Z);
}

PoleAxis::V3 PoleAxis::dirCos(const dms primary, const dms secondary)
{
return V3(
static_cast<float>(secondary.cos() * primary.cos()),
static_cast<float>(secondary.cos() * primary.sin()),
static_cast<float>(secondary.sin()));
}

PoleAxis::V3 PoleAxis::dirCos(const SkyPoint sp)
{
return dirCos(sp.ra(), sp.dec());
}

dms PoleAxis::primary(V3 dirCos)
{
dms p;
p.setRadians(static_cast<double>(std::atan2(dirCos.y(), dirCos.x())));
return p;
}

dms PoleAxis::secondary(V3 dirCos)
{
dms p;
p.setRadians(static_cast<double>(std::asin(dirCos.z())));
return p;
}

SkyPoint PoleAxis::skyPoint(V3 dc)
{
return SkyPoint(primary(dc), secondary(dc));
}

PoleAxis::V3 PoleAxis::poleAxis(SkyPoint p1, SkyPoint p2, SkyPoint p3)
{
// convert the three positions to vectors, these define the plane
// of the Ha axis rotation
V3 v1 = PoleAxis::dirCos(p1);
V3 v2 = PoleAxis::dirCos(p2);
V3 v3 = PoleAxis::dirCos(p3);

// the Ha axis direction is the normal to the plane
V3 p = V3::normal(v1, v2, v3);

// p points to the north or south pole depending on the rotation of the points
// the other pole position can be determined by reversing the sign of the Dec and
// adding 12hrs to the Ha value.
// if we want only the north then this would do it
//if (p.z() < 0)
// p = -p;
return p;
}
96 changes: 96 additions & 0 deletions kstars/ekos/align/poleaxis.h
@@ -0,0 +1,96 @@
/* poleaxis.h determines the mount polar axis position
Copyright (C) 2020 Chris Rowland <chris.rowland@cherryfield.me.uk>
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 <dms.h>
#include <skypoint.h>

/**
*@class PoleAxis
*@short PoleAxis class handles determining the mount Ha axis position given three positions taken with the same mount declination.
*
*@author Chris Rowland
*@version 1.0
*/
class PoleAxis
{
public:
// Like QVector3D but double precision.
class V3
{
public:
V3(double x, double y, double z) : X(x), Y(y), Z(z) {};
V3() : X(0.0), Y(0.0), Z(0.0) {};
double x() const
{
return X;
}
double y() const
{
return Y;
}
double z() const
{
return Z;
}
static V3 normal(const V3 &v1, const V3 &v2, const V3 &v3);
double length();
private:
double X, Y, Z;
};

///
/// \brief dirCos converts primary and secondary angles to a directional cosine
/// \param primary angle, can be Ra, Ha, Azimuth or the corresponding axis values
/// \param secondary angle, can be Dec, Altitude. 90 deg is the pole
/// \return V3 containing the directional cosine.
static V3 dirCos(const dms primary, const dms secondary);

///
/// \brief dirCos converts a SkyPoint to a directional cosine
/// \param sp SkyPoint with the position
/// \return V3 containing the directional cosine.
///
static V3 dirCos(const SkyPoint sp);

///
/// \brief primary returns the primary dms value in the directional cosine
/// \param dirCos
/// \return primary angle, Ra, Ha, Azimuth etc.
///
static dms primary(V3 dirCos);

///
/// \brief secondary returns the secondary dms angle in the directional cosine
/// \param dirCos
/// \return
///
static dms secondary(V3 dirCos);

///
/// \brief skyPoint returns a skypoint derived from the directional cosine vector
/// \param dc
/// \return
///
static SkyPoint skyPoint(V3 dc);

///
/// \brief poleAxis returns the pole axis vector given three SkyPoints with the same mount declination
/// \param p1
/// \param p2
/// \param p3
/// \return vector giving the direction of the pole. The rotation between the three points determines which pole
/// the other pole can be determined either by reversing the sign of the declination and adding 12 hrs to the Ha or
/// by negating the vector
///
static V3 poleAxis(SkyPoint p1, SkyPoint p2, SkyPoint p3);

};
2 changes: 2 additions & 0 deletions kstars/ekos/manager.cpp
@@ -3554,6 +3554,8 @@ void Manager::connectModules()
{
connect(mountProcess.get(), &Ekos::Mount::newStatus, alignProcess.get(), &Ekos::Align::setMountStatus,
Qt::UniqueConnection);
connect(mountProcess.get(), &Ekos::Mount::newCoords, alignProcess.get(), &Ekos::Align::setMountCoords,
Qt::UniqueConnection);
}

// Mount <---> Guide connections
290 changes: 44 additions & 246 deletions kstars/fitsviewer/fitsdata.cpp
@@ -2163,7 +2163,7 @@ bool FITSData::checkForWCS()
return HasWCS;
}

bool FITSData::loadWCS()
bool FITSData::loadWCS(bool extras)
{
#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)

@@ -2250,68 +2250,50 @@ bool FITSData::loadWCS()

m_WCSState = Busy;

const int nThreads = QThread::idealThreadCount();
QList<QFuture<void>> futures;
// Calculate how many elements we process per thread
uint32_t tStride = m_Statistics.samples_per_channel / nThreads;
// Calculate the final stride since we can have some left over due to division above
uint32_t fStride = tStride + (m_Statistics.samples_per_channel - (tStride * nThreads));

for (int i = 0; i < nThreads; i++)
if (extras)
{
uint32_t cStart = i * tStride;
uint32_t cEnd = cStart + ((i == (nThreads - 1)) ? fStride : tStride);
// Run threads
futures.append(QtConcurrent::run([ = ]()
const int nThreads = QThread::idealThreadCount();
QList<QFuture<void>> futures;
// Calculate how many elements we process per thread
uint32_t tStride = m_Statistics.samples_per_channel / nThreads;
// Calculate the final stride since we can have some left over due to division above
uint32_t fStride = tStride + (m_Statistics.samples_per_channel - (tStride * nThreads));

for (int i = 0; i < nThreads; i++)
{
double phi = 0, theta = 0, world[2], pixcrd[2], imgcrd[2];
int stat[2];
FITSImage::wcs_point *wcsPointer = m_WCSCoordinates + cStart;
for (uint32_t i = cStart; i < cEnd; i++)
uint32_t cStart = i * tStride;
uint32_t cEnd = cStart + ((i == (nThreads - 1)) ? fStride : tStride);
// Run threads
futures.append(QtConcurrent::run([ = ]()
{
uint32_t x = i % w;
uint32_t y = i / w;
pixcrd[0] = x;
pixcrd[1] = y;
if (wcsp2s(m_WCSHandle, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]) == 0)
double phi = 0, theta = 0, world[2], pixcrd[2], imgcrd[2];
int stat[2];
FITSImage::wcs_point *wcsPointer = m_WCSCoordinates + cStart;
for (uint32_t i = cStart; i < cEnd; i++)
{
wcsPointer->ra = world[0];
wcsPointer->dec = world[1];
uint32_t x = i % w;
uint32_t y = i / w;
pixcrd[0] = x;
pixcrd[1] = y;
if (wcsp2s(m_WCSHandle, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]) == 0)
{
wcsPointer->ra = world[0];
wcsPointer->dec = world[1];
}
wcsPointer++;
}
wcsPointer++;
}
}));
}

for (auto &oneFuture : futures)
oneFuture.waitForFinished();

// 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++;
// }
// }
// }
}));
}

SkyPoint startPoint(m_WCSCoordinates->ra / 15.0, m_WCSCoordinates->dec);
SkyPoint endPoint( (m_WCSCoordinates + w * h - 1)->ra / 15.0, (m_WCSCoordinates + w * h - 1)->dec);
findObjectsInImage(startPoint, endPoint);
for (auto &oneFuture : futures)
oneFuture.waitForFinished();

SkyPoint startPoint(m_WCSCoordinates->ra / 15.0, m_WCSCoordinates->dec);
SkyPoint endPoint( (m_WCSCoordinates + w * h - 1)->ra / 15.0, (m_WCSCoordinates + w * h - 1)->dec);
findObjectsInImage(startPoint, endPoint);
}
m_WCSState = Success;
FullWCS = extras;
HasWCS = true;

qCDebug(KSTARS_FITS) << "Finished WCS Data processing...";
@@ -2340,6 +2322,10 @@ bool FITSData::wcsToPixel(const SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPoi
if ((status = wcss2p(m_WCSHandle, 1, 2, worldcrd, &phi, &theta, imgcrd, pixcrd, stat)) != 0)
{
lastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]);

fprintf(stderr, "******************* wcss2p(%f,%f) error: %s\n", worldcrd[0], worldcrd[1], lastError.toLatin1().data());//////////////////////////
qCDebug(KSTARS_FITS) << "wcss2p failed with:" << worldcrd[0] << worldcrd[1];///////////////////////

return false;
}

@@ -2395,6 +2381,9 @@ bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord)
#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
void FITSData::findObjectsInImage(SkyPoint startPoint, SkyPoint endPoint)
{
if (KStarsData::Instance() == nullptr)
return;

int w = width();
int h = height();
QVariant date;
@@ -3619,197 +3608,6 @@ bool FITSData::ImageToFITS(const QString &filename, const QString &format, QStri
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 * stats.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, stats.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", &degpix1, "CDELT1", &status);
fits_update_key(fptr, TDOUBLE, "CDELT2", &degpix2, "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;
18 changes: 16 additions & 2 deletions kstars/fitsviewer/fitsdata.h
@@ -324,6 +324,12 @@ class FITSData : public QObject
return m_DateTime;
}

// Set the time, for testing (doesn't set header field)
void setDateTime(const KStarsDateTime &t)
{
m_DateTime = t;
}

// WCS
// Check if image has valid WCS header information and set HasWCS accordingly. Call in loadFITS()
bool checkForWCS();
@@ -332,8 +338,14 @@ class FITSData : public QObject
{
return HasWCS;
}
// The WCS can be loaded without pre-computing each pixel's position. This can make certain
// operations slow. FullWCS() is true if the pixel positions are pre-calculated.
bool fullWCS()
{
return FullWCS;
}
// Load WCS data
bool loadWCS();
bool loadWCS(bool extras = true);
// Get WCS State
WCSState getWCSState() const
{
@@ -532,7 +544,9 @@ class FITSData : public QObject
///Star Selection Algorithm
StarAlgorithm starAlgorithm { ALGORITHM_GRADIENT };
/// Do we have WCS keywords in this FITS data?
bool HasWCS { false };
bool HasWCS { false }; /// Do we have WCS keywords in this FITS data?
/// we can initialize wcs without computing all the image positions.
bool FullWCS { false };
/// Is the image debayarable?
bool HasDebayer { false };

10 changes: 5 additions & 5 deletions kstars/fitsviewer/fitsview.cpp
@@ -275,7 +275,7 @@ void FITSView::setCursorMode(CursorMode mode)
{
if (imageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
{
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS);
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
wcsWatcher.setFuture(future);
}
}
@@ -407,7 +407,7 @@ bool FITSView::processData()
Options::autoWCS() &&
!wcsWatcher.isRunning())
{
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS);
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
wcsWatcher.setFuture(future);
}
else
@@ -1219,7 +1219,7 @@ void FITSView::drawEQGrid(QPainter * painter, double scale)
const int image_width = imageData->width();
const int image_height = imageData->height();

if (imageData->hasWCS())
if (imageData->hasWCS() && imageData->fullWCS())
{
FITSImage::wcs_point * wcs_coord = imageData->getWCSCoord();
if (wcs_coord != nullptr)
@@ -1566,7 +1566,7 @@ void FITSView::toggleEQGrid()

if (imageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
{
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS);
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
wcsWatcher.setFuture(future);
return;
}
@@ -1581,7 +1581,7 @@ void FITSView::toggleObjects()

if (imageData->getWCSState() == FITSData::Idle && !wcsWatcher.isRunning())
{
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS);
QFuture<bool> future = QtConcurrent::run(imageData.data(), &FITSData::loadWCS, true);
wcsWatcher.setFuture(future);
return;
}

0 comments on commit cbe3311

Please sign in to comment.