• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Stellarium / stellarium / 15291801018

28 May 2025 04:52AM UTC coverage: 11.931% (-0.02%) from 11.951%
15291801018

push

github

alex-w
Added new set of navigational stars (XIX century)

0 of 6 new or added lines in 2 files covered. (0.0%)

14124 existing lines in 74 files now uncovered.

14635 of 122664 relevant lines covered (11.93%)

18291.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/plugins/LensDistortionEstimator/src/gui/LensDistortionEstimatorDialog.cpp
1
/*
2
 * Stellarium: Lens distortion estimator plugin
3
 * Copyright (C) 2023 Ruslan Kabatsayev
4
 *
5
 * This program is free software; you can redistribute it and/or
6
 * modify it under the terms of the GNU General Public License
7
 * as published by the Free Software Foundation; either version 2
8
 * of the License, or (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program; if not, write to the Free Software
17
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
18
*/
19

20
#include "LensDistortionEstimatorDialog.hpp"
21
#include "LensDistortionEstimator.hpp"
22
#include "StarMgr.hpp"
23
#include "StelApp.hpp"
24
#include "StelGui.hpp"
25
#include "StelCore.hpp"
26
#include "StelUtils.hpp"
27
#include "StelMainView.hpp"
28
#include "StelModuleMgr.hpp"
29
#include "StelObjectMgr.hpp"
30
#include "StelMovementMgr.hpp"
31
#include <cmath>
32
#include <array>
33
#include <limits>
34
#include <cassert>
35
#include <complex>
36
#include <QTimeZone>
37
#include <QFileInfo>
38
#include <QValueAxis>
39
#include <QFileDialog>
40
#include <QScatterSeries>
41
#ifdef HAVE_EXIV2
42
# include <exiv2/exiv2.hpp>
43
#endif
44
#ifdef HAVE_NLOPT
45
# include <nlopt.hpp>
46
#endif
47
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
48
using namespace QtCharts;
49
#endif
50
#include "ui_lensDistortionEstimatorDialog.h"
51

52
namespace
53
{
54
struct Column
55
{
56
enum
57
{
58
        ObjectName,
59
        x,
60
        y,
61
};
62
};
63
struct Role
64
{
65
enum
66
{
67
        ObjectEnglishName = Qt::UserRole,
68
};
69
};
70
struct Page
71
{
72
enum
73
{
74
        ImageLoading,
75
        Fitting,
76
        Settings,
77
        About,
78
};
79
};
80
template<typename T> auto sqr(T x) { return x*x; }
×
81
template<typename T> auto cube(T x) { return x*x*x; }
82
using DistortionModel = LensDistortionEstimator::DistortionModel;
83

84
//! Finds real roots of the polynomial ax^2+bx+c, sorted in ascending order
85
std::vector<double> solveQuadratic(const double a, const double b, const double c)
×
86
{
87
        if(a==0 && b==0) return {};
×
88
        const auto D = b*b - 4*a*c;
×
89
        if(D < 0) return {};
×
90
        const auto sqrtD = std::sqrt(D);
×
91
        const auto signB = b < 0 ? -1. : 1.;
×
92
        const auto u = -b - signB * sqrtD;
×
93
        const auto x1 = u / (2*a);
×
94
        const auto x2 = 2*c / u;
×
95
        if(!std::isfinite(x1)) return {x2}; // a~0, the equation is linear
×
96
        return x1 < x2 ? std::vector<double>{x1, x2} : std::vector<double>{x2, x1};
×
97
}
98

99
// Locates the root t in [tMin, tMax] of the equation t^3+Bt^2+Ct-1=0. The root must be unique.
100
double locateRootInCubicPoly(const double B, const double C, double tMin, double tMax)
×
101
{
102
        for(int n = 0; n < 100 && tMin != tMax; ++n)
×
103
        {
104
                const auto t = (tMin + tMax) / 2;
×
105
                const auto poly = -1+t*(C+t*(B+t));
×
106
                if(poly > 0)
×
107
                        tMax = t;
×
108
                else
109
                        tMin = t;
×
110
        }
111
        return (tMin + tMax) / 2;
×
112
}
113

114
//! Finds real roots of the polynomial Ax^3+ax^2+bx+c, sorted in ascending order
115
std::vector<double> solveCubic(double A, double a, double b, double c)
×
116
{
117
        // The algorithm here is intended to handle the hard cases like
118
        // A~1e-17, a~1, b~1, c~1 well, without introducing arbitrary epsilons.
119

120
        using namespace std;
121

122
        if(abs(A*A*A) < std::numeric_limits<double>::min())
×
123
                return solveQuadratic(a,b,c);
×
124

125
        if(c == 0)
×
126
        {
127
                // Easy special case: x*(Ax^2+ax+b)=0. It must be handled to
128
                // avoid division by zero in the subsequent general solution.
129
                auto roots = solveQuadratic(A,a,b);
×
130
                roots.push_back(0);
×
131
                sort(roots.begin(), roots.end());
×
132
                return roots;
×
133
        }
×
134

135
        // Bring the equation to the form x^3+ax^2+bx+c=0
136
        a /= A;
×
137
        b /= A;
×
138
        c /= A;
×
139
        A = 1;
×
140

141
        // Let's bracket the first root following https://www.johndcook.com/blog/2022/04/05/cubic/
142
        const auto t2x = -cbrt(c); // Changing variable x = t2x*t
×
143
        const auto B = -a/cbrt(c);
×
144
        const auto C = b/sqr(cbrt(c));
×
145
        // New equation: f(t)=0, i.e. t^3+Bt^2+Ct-1=0.
146
        // f(0) = -1
147
        const auto f1 = B+C; // f(1)
×
148
        double t;
149
        if(f1==0)
×
150
        {
151
                t = 1;
×
152
        }
153
        else if(f1 > 0)
×
154
        {
155
                // We have a root t in (0,1)
156
                t = locateRootInCubicPoly(B,C, 0,1);
×
157
        }
158
        else
159
        {
160
                // We have a root t in (1,inf). Change variable t=1/p:
161
                // p^3-Cp^2-Bp-1=0.
162
                const auto p = locateRootInCubicPoly(-C,-B, 0,1);
×
163
                t = 1/p;
×
164
        }
165
        const auto x1 = t2x*t;
×
166

167
        // Long polynomial division of the LHS of the equation by (x-x1) allows us to reach this decomposition:
168
        // (x - x1) (A x^2 + (A x1 + a) x + (A x1^2 + a x1 + b)) + (A x1^3 + a x1^2 + b x1 + c) = A x^3 + a x^2 + b x + c
169
        // Now, assuming that x1 is a root, we can drop the remainder (A x1^3 + a x1^2 + b x1 + c),
170
        // since it must be zero, so we get a quadratic equation for the remaining two roots:
171
        // A x^2 + (A x1 + a) x + (A x1^2 + a x1 + b) = 0
172
        auto roots = solveQuadratic(A, A*x1 + a, A*x1*x1 + a*x1 + b);
×
173
        roots.push_back(x1);
×
174
        sort(roots.begin(), roots.end());
×
175
        return roots;
×
176
}
×
177

178
}
179

180
LensDistortionEstimatorDialog::LensDistortionEstimatorDialog(LensDistortionEstimator* lde)
×
181
        : StelDialog("LensDistortionEstimator")
182
        , lde_(lde)
×
183
        , ui_(new Ui_LDEDialog{})
×
184
        , errorsChart_(new QChart)
×
185
{
186
        warningCheckTimer_.setInterval(1000);
×
187
        connect(&warningCheckTimer_, &QTimer::timeout, this, &LensDistortionEstimatorDialog::periodicWarningsCheck);
×
188

189
        // Same as in AstroCalcChart
190
        errorsChart_->setBackgroundBrush(QBrush(QColor(86, 87, 90)));
×
191
        errorsChart_->setTitleBrush(QBrush(Qt::white));
×
192
        errorsChart_->setMargins(QMargins(2, 1, 2, 1)); // set to 0/0/0/0 for max space usage. This is between the title/axis labels and the enclosing QChartView.
×
193
        errorsChart_->layout()->setContentsMargins(0, 0, 0, 0);
×
194
        errorsChart_->setBackgroundRoundness(0); // remove rounded corners
×
195
}
×
196

197
LensDistortionEstimatorDialog::~LensDistortionEstimatorDialog()
×
198
{
199
}
×
200

201
void LensDistortionEstimatorDialog::retranslate()
×
202
{
203
        if(dialog)
×
204
        {
205
                ui_->retranslateUi(dialog);
×
206
                setAboutText();
×
207
        }
208
}
×
209

210
void LensDistortionEstimatorDialog::createDialogContent()
×
211
{
212
        ui_->setupUi(dialog);
×
213
        ui_->tabs->setCurrentIndex(0);
×
214

215
        // Kinetic scrolling
216
        kineticScrollingList << ui_->about;
×
217
        StelGui* gui= dynamic_cast<StelGui*>(StelApp::getInstance().getGui());
×
218
        if(gui)
×
219
        {
220
                enableKineticScrolling(gui->getFlagUseKineticScrolling());
×
221
                connect(gui, &StelGui::flagUseKineticScrollingChanged, this, &LensDistortionEstimatorDialog::enableKineticScrolling);
×
222
        }
223

224
        connect(&StelApp::getInstance(), &StelApp::languageChanged, this, &LensDistortionEstimatorDialog::retranslate);
×
225
        connect(ui_->titleBar, &TitleBar::closeClicked, this, &StelDialog::close);
×
226
        connect(ui_->titleBar, &TitleBar::movedTo, this, &LensDistortionEstimatorDialog::handleMovedTo);
×
227

228
        core_ = StelApp::getInstance().getCore();
×
229
        starMgr_ = GETSTELMODULE(StarMgr);
×
230
        objectMgr_ = GETSTELMODULE(StelObjectMgr);
×
231
        Q_ASSERT(objectMgr_);
×
232
        connect(objectMgr_, &StelObjectMgr::selectedObjectChanged, this, &LensDistortionEstimatorDialog::handleSelectedObjectChange);
×
233
        handleSelectedObjectChange(StelModule::ReplaceSelection);
×
234

235
        // Main tabs
236
        clearWarnings();
×
237
        connect(ui_->imageFilePath, &QLineEdit::editingFinished, this, &LensDistortionEstimatorDialog::updateImage);
×
238
        connect(ui_->imageFilePath, &QLineEdit::textChanged, this, &LensDistortionEstimatorDialog::updateImagePathStatus);
×
239
        connect(ui_->imageBrowseBtn, &QAbstractButton::clicked, this, &LensDistortionEstimatorDialog::browseForImage);
×
240
        connect(ui_->pickImagePointBtn, &QAbstractButton::clicked, this, &LensDistortionEstimatorDialog::startImagePointPicking);
×
241
        connect(ui_->removePointBtn, &QAbstractButton::clicked, this, &LensDistortionEstimatorDialog::removeImagePoint);
×
242
        connect(ui_->imagePointsPicked, &QTreeWidget::itemSelectionChanged, this, &LensDistortionEstimatorDialog::handlePointSelectionChange);
×
243
        connect(ui_->imagePointsPicked, &QTreeWidget::itemDoubleClicked, this, &LensDistortionEstimatorDialog::handlePointDoubleClick);
×
244
        connect(ui_->repositionBtn, &QAbstractButton::clicked, this, &LensDistortionEstimatorDialog::repositionImageByPoints);
×
245
        connect(ui_->fixWarningBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::fixWarning);
×
246
        connect(ui_->exportPointsBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::exportPoints);
×
247
        connect(ui_->importPointsBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::importPoints);
×
248
        connect(ui_->distortionModelCB, qOverload<int>(&QComboBox::currentIndexChanged), this,
×
249
                &LensDistortionEstimatorDialog::updateDistortionCoefControls);
250
        connect(ui_->resetPlacementBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::resetImagePlacement);
×
251
        connect(ui_->resetDistortionBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::resetDistortion);
×
252
#ifdef HAVE_NLOPT
253
        connect(ui_->optimizeBtn, &QAbstractButton::clicked, this, &LensDistortionEstimatorDialog::optimizeParameters);
×
254
#else
255
        ui_->optimizeBtn->hide();
256
#endif
257
        connect(ui_->generateLensfunModelBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::generateLensfunModel);
×
258
        connect(ui_->lensMake, &QLineEdit::textChanged, this, &LensDistortionEstimatorDialog::setupGenerateLensfunModelButton);
×
259
        connect(ui_->lensModel, &QLineEdit::textChanged, this, &LensDistortionEstimatorDialog::setupGenerateLensfunModelButton);
×
260
        connect(ui_->lensMount, &QLineEdit::textChanged, this, &LensDistortionEstimatorDialog::setupGenerateLensfunModelButton);
×
261
        connect(ui_->imageFilePath, &QLineEdit::textChanged, this, &LensDistortionEstimatorDialog::setupGenerateLensfunModelButton);
×
262
        setupGenerateLensfunModelButton();
×
263

264
        ui_->errorsChartView->setChart(errorsChart_);
×
265
        ui_->errorsChartView->setRenderHint(QPainter::Antialiasing);
×
266
        ui_->imagePointsPicked->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
×
267
        ui_->listChartSplitter->setStretchFactor(0, 1);
×
268
        ui_->listChartSplitter->setStretchFactor(1, 4);
×
269
        updateDistortionCoefControls();
×
270

271
        // Settings tab
272
        connectBoolProperty(ui_->imageEnabled, "LensDistortionEstimator.imageEnabled");
×
273
        connectBoolProperty(ui_->imageAxesEnabled, "LensDistortionEstimator.imageAxesEnabled");
×
274
        connectBoolProperty(ui_->markPickedPoints, "LensDistortionEstimator.pointMarkersEnabled");
×
275
        connectBoolProperty(ui_->markProjectionCenter, "LensDistortionEstimator.projectionCenterMarkerEnabled");
×
276
        connectBoolProperty(ui_->imgPosResetOnLoadingEnabled, "LensDistortionEstimator.imgPosResetOnLoadingEnabled");
×
277
        ui_->imageAxesColor->setup("LensDistortionEstimator.imageAxesColor", "LensDistortionEstimator/image_axes_color");
×
278
        ui_->pointMarkerColor->setup("LensDistortionEstimator.pointMarkerColor", "LensDistortionEstimator/point_marker_color");
×
UNCOV
279
        ui_->selectedPointMarkerColor->setup("LensDistortionEstimator.selectedPointMarkerColor",
×
280
                                             "LensDistortionEstimator/selected_point_marker_color");
UNCOV
281
        ui_->projectionCenterMarkerColor->setup("LensDistortionEstimator.projectionCenterMarkerColor",
×
282
                                                "LensDistortionEstimator/center_of_projection_marker_color");
UNCOV
283
        connect(ui_->restoreDefaultsBtn, &QPushButton::clicked, this, &LensDistortionEstimatorDialog::restoreDefaults);
×
284

285
        // About tab
286
        setAboutText();
×
UNCOV
287
        if(gui)
×
288
        {
UNCOV
289
                ui_->about->document()->setDefaultStyleSheet(QString(gui->getStelStyle().htmlStyleSheet));
×
290
        }
UNCOV
291
        init();
×
292

293
        initialized_ = true;
×
UNCOV
294
}
×
295

UNCOV
296
void LensDistortionEstimatorDialog::restoreDefaults()
×
297
{
UNCOV
298
        if(askConfirmation())
×
299
        {
300
                qDebug() << "[LensDistortionEstimator] restore defaults...";
×
UNCOV
301
                GETSTELMODULE(LensDistortionEstimator)->restoreDefaultSettings();
×
302
        }
303
        else
304
        {
UNCOV
305
                qDebug() << "[LensDistortionEstimator] restore defaults is canceled...";
×
306
        }
UNCOV
307
}
×
308

UNCOV
309
void LensDistortionEstimatorDialog::emitWarning(const QString& text, const bool autoFixable)
×
310
{
311
        ui_->warnings->setStyleSheet("margin-left: 1px; border-radius: 5px; background-color: #ff5757;");
×
312
        const auto oldText = ui_->warnings->text();
×
313
        if(oldText.isEmpty())
×
UNCOV
314
                ui_->warnings->setText(text);
×
315
        else
316
                ui_->warnings->setText(text+"\n"+oldText);
×
317
        ui_->warnings->show();
×
UNCOV
318
        if(autoFixable)
×
319
        {
320
                ui_->fixWarningBtn->show();
×
UNCOV
321
                ui_->fixWarningBtn->setToolTip(q_("Sets simulation date/time to image EXIF date/time, and freezes simulation time"));
×
322
        }
UNCOV
323
}
×
324

UNCOV
325
void LensDistortionEstimatorDialog::clearWarnings()
×
326
{
327
        ui_->warnings->hide();
×
328
        ui_->warnings->setText("");
×
329
        ui_->fixWarningBtn->hide();
×
UNCOV
330
}
×
331

UNCOV
332
double LensDistortionEstimatorDialog::computeExifTimeDifference() const
×
333
{
334
        const auto jd = core_->getJD();
×
335
        const auto stelTZ = core_->getUTCOffset(jd); // in hours
×
UNCOV
336
        const auto jdc = jd + stelTZ/24.; // UTC -> local TZ
×
337
        int year, month, day, hour, minute, second;
338
        StelUtils::getDateTimeFromJulianDay(jdc, &year, &month, &day, &hour, &minute, &second);
×
339
        const bool exifHasLocalTime = exifDateTime_.timeSpec() == Qt::LocalTime;
×
UNCOV
340
        const auto currentDateTime = exifHasLocalTime ?
×
341
                QDateTime(QDate(year, month, day), QTime(hour, minute, second, 0), Qt::LocalTime) :
342
                QDateTime(QDate(year, month, day), QTime(hour, minute, second, 0), QTimeZone(stelTZ*3600));
×
343
        return exifDateTime_.msecsTo(currentDateTime);
×
UNCOV
344
}
×
345

UNCOV
346
void LensDistortionEstimatorDialog::periodicWarningsCheck()
×
347
{
348
        if(!exifDateTime_.isValid()) return;
×
349
        clearWarnings();
×
350
        const bool exifHasLocalTime = exifDateTime_.timeSpec() == Qt::LocalTime;
×
351
        const auto timeDiff = computeExifTimeDifference();
×
352
        if(std::abs(timeDiff) > 5000)
×
353
                emitWarning(q_("Stellarium time differs from image EXIF time %1 by %2 seconds")
×
UNCOV
354
                              .arg(exifHasLocalTime ? exifDateTime_.toString("yyyy-MM-dd hh:mm:ss")
×
355
                                                    : exifDateTime_.toString("yyyy-MM-dd hh:mm:ss t"))
UNCOV
356
                              .arg(timeDiff/1000.),
×
357
                            true);
358
}
359

UNCOV
360
void LensDistortionEstimatorDialog::fixWarning()
×
361
{
UNCOV
362
        if(!exifDateTime_.isValid()) return;
×
363

364
        const auto timeDiff = computeExifTimeDifference();
×
365
        const auto jd = core_->getJD();
×
366
        core_->setJD(jd - timeDiff/86400e3);
×
UNCOV
367
        core_->setZeroTimeSpeed();
×
368

UNCOV
369
        clearWarnings();
×
370
}
371

UNCOV
372
void LensDistortionEstimatorDialog::exportPoints() const
×
373
{
374
        const auto path = QFileDialog::getSaveFileName(&StelMainView::getInstance(), q_("Save points to file"), {},
×
375
                                                       q_("Comma-separated values (*.csv)"));
×
376
        if(path.isNull()) return;
×
377
        QFile file(path);
×
UNCOV
378
        if(!file.open(QFile::WriteOnly))
×
379
        {
380
                QMessageBox::critical(&StelMainView::getInstance(), q_("Error saving data"),
×
381
                                      q_("Failed to open output file for writing:\n%1").arg(file.errorString()));
×
UNCOV
382
                return;
×
383
        }
384
        QTextStream str(&file);
×
385
        str << "Object name,Localized object name,Image position X,Image position Y,Exact object azimuth,Exact object elevation\n";
×
386
        const auto nMax = ui_->imagePointsPicked->topLevelItemCount();
×
UNCOV
387
        for(int n = 0; n < nMax; ++n)
×
388
        {
389
                const auto item = ui_->imagePointsPicked->topLevelItem(n);
×
390
                const auto objectName = item->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
391
                const auto object = findObjectByName(objectName);
×
392
                double trueAzimuth = NAN, trueElevation = NAN;
×
UNCOV
393
                if(!object)
×
394
                {
UNCOV
395
                        qWarning() << "Object" << objectName << "not found, will export its sky position as NaN";
×
396
                }
397
                else
398
                {
399
                        using namespace std;
400
                        const auto objDir = normalize(object->getAltAzPosApparent(core_));
×
401
                        trueElevation = 180/M_PI * asin(clamp(objDir[2], -1.,1.));
×
UNCOV
402
                        trueAzimuth = 180/M_PI * atan2(objDir[1], -objDir[0]);
×
403
                }
404
                str << objectName << ","
×
405
                    << item->data(Column::ObjectName, Qt::DisplayRole).toString() << ","
×
406
                    << item->data(Column::x, Qt::DisplayRole).toDouble() << ","
×
407
                    << item->data(Column::y, Qt::DisplayRole).toDouble() << ","
×
408
                    << (trueAzimuth < 0 ? 360 : 0) + trueAzimuth << ","
×
409
                    << trueElevation << "\n";
×
410
        }
×
411
        str.flush();
×
UNCOV
412
        if(!file.flush())
×
413
        {
414
                QMessageBox::critical(&StelMainView::getInstance(), q_("Error saving data"),
×
UNCOV
415
                                      q_("Failed to write the output file:\n%1").arg(file.errorString()));
×
416
        }
UNCOV
417
}
×
418

UNCOV
419
void LensDistortionEstimatorDialog::importPoints()
×
420
{
UNCOV
421
        if(ui_->imagePointsPicked->topLevelItemCount())
×
422
        {
423
                const auto ret = QMessageBox::warning(&StelMainView::getInstance(), q_("Confirm deletion of old points"),
×
UNCOV
424
                                                      q_("There are some points defined. They will be deleted "
×
425
                                                         "and replaced with the ones from the file. Proceed?"),
426
                                                      QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel);
UNCOV
427
                if(ret != QMessageBox::Yes) return;
×
428
        }
429

430
        const auto path = QFileDialog::getOpenFileName(&StelMainView::getInstance(), q_("Save points to file"), {},
×
431
                                                       q_("Comma-separated values (*.csv)"));
×
432
        if(path.isNull()) return;
×
433
        QFile file(path);
×
UNCOV
434
        if(!file.open(QFile::ReadOnly))
×
435
        {
436
                QMessageBox::critical(&StelMainView::getInstance(), q_("Error reading data"),
×
437
                                      q_("Failed to open input file for reading:\n%1").arg(file.errorString()));
×
UNCOV
438
                return;
×
439
        }
440

441
        // Keep new items out of the widget till the end, so that, if parsing fails, we didn't destroy the original widget contents
UNCOV
442
        std::vector<std::unique_ptr<QTreeWidgetItem>> newItems;
×
443

444
        QTextStream str(&file);
×
UNCOV
445
        for(int lineNum = 1;; ++lineNum)
×
446
        {
UNCOV
447
                if(str.atEnd()) break;
×
448

449
                const auto entries = str.readLine().split(",");
×
450
                constexpr int minEntryCount = 4;
×
UNCOV
451
                if(entries.size() < minEntryCount)
×
452
                {
453
                        QMessageBox::critical(&StelMainView::getInstance(), q_("File format error"),
×
454
                                              q_("Can't import file: expected to find at least %1 entries at line %2, but got %3.")
×
455
                                                  .arg(minEntryCount).arg(lineNum).arg(entries.size()));
×
UNCOV
456
                        return;
×
457
                }
458
                if(lineNum==1) continue;
×
459
                const auto objectName = entries[0];
×
460
                const auto object = StelApp::getInstance().getStelObjectMgr().searchByName(objectName);
×
UNCOV
461
                if(!object)
×
462
                {
463
                        QMessageBox::critical(&StelMainView::getInstance(), q_("File format error"),
×
464
                                              q_("Can't import file: object \"%1\" not found at line %2.").arg(objectName).arg(lineNum));
×
UNCOV
465
                        return;
×
466
                }
467
                const auto locObjName = entries[1];
×
468
                bool ok = false;
×
469
                const auto imgPosX = entries[2].toDouble(&ok);
×
UNCOV
470
                if(!ok)
×
471
                {
472
                        QMessageBox::critical(&StelMainView::getInstance(), q_("File format error"),
×
473
                                              q_("Can't import file: failed to parse image position X at line %1.").arg(lineNum));
×
UNCOV
474
                        return;
×
475
                }
476
                const auto imgPosY = entries[3].toDouble(&ok);
×
UNCOV
477
                if(!ok)
×
478
                {
479
                        QMessageBox::critical(&StelMainView::getInstance(), q_("File format error"),
×
480
                                              q_("Can't import file: failed to parse image position Y at line %1.").arg(lineNum));
×
UNCOV
481
                        return;
×
482
                }
483
                const auto item = new QTreeWidgetItem({locObjName,
484
                                                       QString("%1").arg(imgPosX),
×
485
                                                       QString("%1").arg(imgPosY)});
×
486
                item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled);
×
487
                item->setData(Column::ObjectName, Role::ObjectEnglishName, objectName);
×
488
                newItems.emplace_back(item);
×
UNCOV
489
        }
×
490

491
        for(int n = ui_->imagePointsPicked->topLevelItemCount() - 1; n >= 0; --n)
×
UNCOV
492
                delete ui_->imagePointsPicked->topLevelItem(n);
×
493

494
        for(auto& item : newItems)
×
UNCOV
495
                ui_->imagePointsPicked->addTopLevelItem(item.release());
×
496

497
        updateRepositionButtons();
×
498
        updateErrorsChart();
×
499
        ui_->exportPointsBtn->setEnabled(true);
×
UNCOV
500
}
×
501

UNCOV
502
void LensDistortionEstimatorDialog::removeImagePoint()
×
503
{
504
        const auto item = ui_->imagePointsPicked->currentItem();
×
505
        if(!item) return;
×
UNCOV
506
        delete item;
×
507

508
        updateRepositionButtons();
×
509
        updateErrorsChart();
×
510
        if(ui_->imagePointsPicked->topLevelItemCount() == 0)
×
UNCOV
511
                ui_->exportPointsBtn->setDisabled(true);
×
512
}
513

UNCOV
514
void LensDistortionEstimatorDialog::updateRepositionButtons()
×
515
{
516
        ui_->repositionBtn->setEnabled(ui_->imagePointsPicked->topLevelItemCount() >= 2 && !image_.isNull());
×
517
        ui_->optimizeBtn->setEnabled(ui_->imagePointsPicked->topLevelItemCount() >= 3 && !image_.isNull());
×
UNCOV
518
}
×
519

UNCOV
520
void LensDistortionEstimatorDialog::handlePointSelectionChange()
×
521
{
522
        ui_->removePointBtn->setEnabled(!!ui_->imagePointsPicked->currentItem());
×
UNCOV
523
}
×
524

UNCOV
525
void LensDistortionEstimatorDialog::handlePointDoubleClick(QTreeWidgetItem*const item, int)
×
526
{
527
        const auto objectName = item->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
528
        const auto object = findObjectByName(objectName);
×
529
        if(object) objectMgr_->setSelectedObject(object);
×
UNCOV
530
}
×
531

UNCOV
532
void LensDistortionEstimatorDialog::handleSelectedObjectChange(StelModule::StelModuleSelectAction)
×
533
{
UNCOV
534
        if(optimizing_) return;
×
535

UNCOV
536
        if(objectMgr_->getWasSelected() && !image_.isNull())
×
537
        {
UNCOV
538
                ui_->pickImagePointBtn->setEnabled(true);
×
539
        }
540
        else
541
        {
542
                ui_->pickImagePointBtn->setChecked(false);
×
543
                ui_->pickImagePointBtn->setDisabled(true);
×
544
                QApplication::restoreOverrideCursor();
×
UNCOV
545
                isPickingAPoint_ = false;
×
546
        }
547
}
548

UNCOV
549
void LensDistortionEstimatorDialog::startImagePointPicking(const bool buttonChecked)
×
550
{
UNCOV
551
        if(!buttonChecked)
×
552
        {
553
                QApplication::restoreOverrideCursor();
×
554
                isPickingAPoint_ = false;
×
UNCOV
555
                return;
×
556
        }
557
        QApplication::setOverrideCursor(lde_->getPointPickCursor());
×
UNCOV
558
        isPickingAPoint_ = true;
×
559
}
560

UNCOV
561
void LensDistortionEstimatorDialog::togglePointPickingMode()
×
562
{
563
        if(!initialized_ || !ui_->pickImagePointBtn->isEnabled()) return;
×
564
        const bool mustBeChecked = !ui_->pickImagePointBtn->isChecked();
×
565
        ui_->pickImagePointBtn->setChecked(mustBeChecked);
×
UNCOV
566
        startImagePointPicking(mustBeChecked);
×
567
}
568

UNCOV
569
void LensDistortionEstimatorDialog::resetImagePointPicking()
×
570
{
571
        QApplication::restoreOverrideCursor();
×
572
        isPickingAPoint_ = false;
×
573
        ui_->pickImagePointBtn->setChecked(false);
×
UNCOV
574
}
×
575

UNCOV
576
void LensDistortionEstimatorDialog::registerImagePoint(const StelObject& object, const Vec2d& posInImage)
×
577
{
578
        const auto englishName = getObjectName(object);
×
579
        auto name = object.getNameI18n();
×
580
        if(name.isEmpty())
×
UNCOV
581
                name = englishName;
×
582
        const auto item = new QTreeWidgetItem({name,
583
                                               QString("%1").arg(posInImage[0]),
×
584
                                               QString("%1").arg(posInImage[1])});
×
585
        item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled);
×
586
        item->setData(Column::ObjectName, Role::ObjectEnglishName, englishName);
×
587
        ui_->imagePointsPicked->addTopLevelItem(item);
×
UNCOV
588
        ui_->imagePointsPicked->setCurrentItem(item);
×
589

590
        updateRepositionButtons();
×
591
        updateErrorsChart();
×
592
        ui_->exportPointsBtn->setEnabled(true);
×
UNCOV
593
}
×
594

UNCOV
595
void LensDistortionEstimatorDialog::setupChartAxisStyle(QValueAxis& axis)
×
596
{
597
        // Same as in AstroCalcChart
598
        static const QPen          pen(Qt::white, 1,    Qt::SolidLine);
×
599
        static const QPen      gridPen(Qt::white, 0.5,  Qt::SolidLine);
×
UNCOV
600
        static const QPen minorGridPen(Qt::white, 0.35, Qt::DotLine);
×
601

602
        axis.setLinePen(pen);
×
603
        axis.setGridLinePen(gridPen);
×
604
        axis.setMinorGridLinePen(minorGridPen);
×
605
        axis.setTitleBrush(Qt::white);
×
606
        axis.setLabelsBrush(Qt::white);
×
UNCOV
607
}
×
608

UNCOV
609
double LensDistortionEstimatorDialog::roundToNiceAxisValue(const double x)
×
610
{
UNCOV
611
        if(!std::isfinite(x)) return x;
×
612

UNCOV
613
        const bool negative = x<0;
×
614
        // Going through serialization to avoid funny issues with imprecision.
615
        // This function is not supposed to be called in performance-critical tasks.
616
        const auto str = QString::number(std::abs(x), 'e', 0);
×
UNCOV
617
        auto absRounded = str.toDouble();
×
618

619
        if(absRounded >= std::abs(x))
×
UNCOV
620
                return negative ? -absRounded : absRounded;
×
621

622
        // We want to round away from zero, so need to tweak the result
623
        const int firstDigit = str[0].toLatin1() - '0';
×
624
        assert(firstDigit >= 0 && firstDigit <= 9);
×
625
        absRounded *= double(firstDigit+1) / firstDigit;
×
626
        return negative ? -absRounded : absRounded;
×
UNCOV
627
}
×
628

UNCOV
629
void LensDistortionEstimatorDialog::updateErrorsChart()
×
630
{
631
        errorsChart_->removeAllSeries();
×
UNCOV
632
        for(const auto axis : errorsChart_->axes())
×
633
        {
634
                errorsChart_->removeAxis(axis);
×
635
                delete axis;
×
UNCOV
636
        }
×
637

UNCOV
638
        if(image_.isNull()) return;
×
639

640
        using namespace std;
UNCOV
641
        constexpr auto radian = 180/M_PI;
×
642

643
        const auto series = new QScatterSeries;
×
644
        const auto nMax = ui_->imagePointsPicked->topLevelItemCount();
×
645
        double xMin = +INFINITY, xMax = -INFINITY, yMin = +INFINITY, yMax = -INFINITY;
×
UNCOV
646
        for(int n = 0; n < nMax; ++n)
×
647
        {
648
                const auto item = ui_->imagePointsPicked->topLevelItem(n);
×
649
                const auto objectName = item->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
650
                const auto object = findObjectByName(objectName);
×
UNCOV
651
                if(!object)
×
652
                {
653
                        qWarning() << "Object" << objectName << "not found, chart creation fails";
×
654
                        delete series;
×
UNCOV
655
                        return;
×
656
                }
657
                const auto objectDirTrue = normalize(object->getAltAzPosApparent(core_));
×
658
                const Vec2d objectXY(item->data(Column::x, Qt::DisplayRole).toDouble(),
×
659
                                     item->data(Column::y, Qt::DisplayRole).toDouble());
×
660
                const auto objectDirByImg = computeImagePointDir(objectXY);
×
UNCOV
661
                const auto centerDir = projectionCenterDir();
×
662

UNCOV
663
                const auto distFromCenterTrue = acos(clamp(dot(objectDirTrue, centerDir), -1.,1.));
×
664

665
                const auto x = distFromCenterTrue * radian;
×
666
                const auto y = acos(clamp(dot(objectDirByImg,objectDirTrue), -1.,1.)) * radian;
×
667
                if(x < xMin) xMin = x;
×
668
                if(x > xMax) xMax = x;
×
669
                if(y < yMin) yMin = y;
×
670
                if(y > yMax) yMax = y;
×
671
                series->append(x, y);
×
672
        }
×
UNCOV
673
        errorsChart_->addSeries(series);
×
674

675
        const auto xAxis = new QValueAxis;
×
676
        xAxis->setTitleText(q_(u8"Distance from center, \u00b0"));
×
677
        setupChartAxisStyle(*xAxis);
×
678
        const auto xRange = xMax - xMin;
×
UNCOV
679
        xAxis->setRange(0, max({xMax + 0.05 * xRange + 1, min(90., imageSmallerSideFoV()/2 * 1.5)}));
×
680

681
        const auto yAxis = new QValueAxis;
×
682
        yAxis->setTitleText(q_(u8"Error estimate, \u00b0"));
×
683
        setupChartAxisStyle(*yAxis);
×
684
        const auto yRange = yMax - yMin;
×
685
        const auto yRangeToUse = roundToNiceAxisValue(max(abs(yMin), abs(yMax)) + 0.1 * yRange);
×
686
        if(yMin >= 0)
×
687
                yAxis->setRange(0, yRangeToUse);
×
688
        else if(yMax <= 0)
×
UNCOV
689
                yAxis->setRange(-yRangeToUse, 0);
×
690
        else
UNCOV
691
                yAxis->setRange(-yRangeToUse, yRangeToUse);
×
692

693
        errorsChart_->addAxis(xAxis, Qt::AlignBottom);
×
UNCOV
694
        errorsChart_->addAxis(yAxis, Qt::AlignLeft);
×
695

696
        series->setBorderColor(Qt::transparent);
×
697
        series->setMarkerSize(4 * StelApp::getInstance().getDevicePixelsPerPixel());
×
698
        series->attachAxis(xAxis);
×
UNCOV
699
        series->attachAxis(yAxis);
×
700

UNCOV
701
        errorsChart_->legend()->hide();
×
702
}
703

UNCOV
704
double LensDistortionEstimatorDialog::applyDistortion(const double ru) const
×
705
{
706
        const auto model = distortionModel();
×
707
        const auto ru2 = ru*ru;
×
708
        const auto ru3 = ru2*ru;
×
709
        const auto ru4 = ru2*ru2;
×
UNCOV
710
        switch(model)
×
711
        {
UNCOV
712
        case DistortionModel::Poly3:
×
713
        {
714
                const auto k1 = distortionTerm1();
×
UNCOV
715
                return ru*(1-k1+k1*ru2);
×
716
        }
UNCOV
717
        case DistortionModel::Poly5:
×
718
        {
719
                const auto k1 = distortionTerm1();
×
720
                const auto k2 = distortionTerm2();
×
UNCOV
721
                return ru*(1+k1*ru2+k2*ru4);
×
722
        }
UNCOV
723
        case DistortionModel::PTLens:
×
724
        {
725
                const auto a = distortionTerm1();
×
726
                const auto b = distortionTerm2();
×
727
                const auto c = distortionTerm3();
×
UNCOV
728
                return ru*(a*ru3+b*ru2+c*ru+1-a-b-c);
×
729
        }
730
        }
731
        qWarning() << "Unknown distortion model chosen: " << static_cast<int>(model);
×
UNCOV
732
        return ru;
×
733
}
734

UNCOV
735
double LensDistortionEstimatorDialog::maxUndistortedR() const
×
736
{
737
        // The limit value is defined as the point where the distortion model ceases to be monotonic.
738

UNCOV
739
        const auto model = distortionModel();
×
740
        // The value for the case when the model is monotonic for all ru in [0,inf)
UNCOV
741
        constexpr double unlimited = std::numeric_limits<float>::max();
×
742

743
        using namespace std;
UNCOV
744
        switch(model)
×
745
        {
UNCOV
746
        case DistortionModel::Poly3:
×
747
        {
748
                const auto k1 = distortionTerm1();
×
749
                const auto square = (k1-1)/(3*k1);
×
750
                if(square < 0) return unlimited;
×
UNCOV
751
                return sqrt(square);
×
752
        }
UNCOV
753
        case DistortionModel::Poly5:
×
754
        {
755
                const auto k1 = distortionTerm1();
×
756
                const auto k2 = distortionTerm2();
×
757
                if(k2 == 0 && k1 < 0) return 1/sqrt(-3*k1);
×
758
                const auto D = 9*k1*k1-20*k2;
×
759
                if(D < 0) return unlimited;
×
760
                const auto square = -(3*k1+sqrt(D))/(10*k2);
×
761
                if(square < 0) return unlimited;
×
UNCOV
762
                return sqrt(square);
×
763
        }
UNCOV
764
        case DistortionModel::PTLens:
×
765
        {
766
                const auto a = distortionTerm1();
×
767
                const auto b = distortionTerm2();
×
768
                const auto c = distortionTerm3();
×
769
                const auto roots = solveCubic(4*a, 3*b, 2*c, 1-a-b-c);
×
770
                for(auto root : roots)
×
771
                        if(root > 0) return root;
×
772
                return unlimited;
×
UNCOV
773
        }
×
774
        }
775
        qWarning() << "Unknown distortion model chosen: " << static_cast<int>(model);
×
UNCOV
776
        return unlimited;
×
777
}
778

UNCOV
779
Vec2d LensDistortionEstimatorDialog::imagePointToNormalizedCoords(const Vec2d& imagePoint) const
×
780
{
781
        // This is how it works in lensfun: i increases to the right, j increases towards the bottom of the image, and
782
        // normPos = [(2*i - (width -1)) / min(width-1, height-1) - lensfun.center[0],
783
        //            (2*j - (height-1)) / min(width-1, height-1) - lensfun.center[1]]
784
        // Our coordinates are inverted in y with respect to lensfun, so that they are easier
785
        // to handle, avoiding too many (1,-1) multiplers scattered around the code.
786
        const double W = image_.width();
×
787
        const double H = image_.height();
×
788
        const double w = W - 1;
×
789
        const double h = H - 1;
×
UNCOV
790
        const auto distortedPos = Vec2d(1,-1) * (2. * imagePoint - Vec2d(w, h)) / std::min(w, h) - imageCenterShift();
×
791

792
        using namespace std;
793
        const auto rd = distortedPos.norm();
×
794
        double ruMin = 0;
×
795
        double ruMax = 1.3 * hypot(double(image_.width()),image_.height())/min(image_.width(),image_.height()); // FIXME: make a more sound choice
×
UNCOV
796
        for(int n = 0; n < 53 && ruMin != ruMax; ++n)
×
797
        {
798
                const auto ru = (ruMin + ruMax) / 2;
×
799
                const auto rdCurr = applyDistortion(ru);
×
800
                if(rdCurr < rd)
×
UNCOV
801
                        ruMin = ru;
×
802
                else
UNCOV
803
                        ruMax = ru;
×
804
        }
UNCOV
805
        const auto ru = (ruMin + ruMax) / 2;
×
806

UNCOV
807
        return distortedPos / rd * ru;
×
808
}
809

UNCOV
810
Vec3d LensDistortionEstimatorDialog::computeImagePointDir(const Vec2d& imagePoint) const
×
811
{
812
        using namespace std;
813
        const auto centeredPoint = imagePointToNormalizedCoords(imagePoint);
×
814
        const auto upDir = imageUpDir();
×
815
        const auto centerDir = projectionCenterDir();
×
816
        const auto rightDir = centerDir ^ upDir;
×
UNCOV
817
        const auto smallerSideFoV = M_PI/180 * this->imageSmallerSideFoV();
×
818

819
        const auto x = centeredPoint[0] * tan(smallerSideFoV/2);
×
820
        const auto y = centeredPoint[1] * tan(smallerSideFoV/2);
×
821
        const auto z = 1.;
×
UNCOV
822
        return normalize(x*rightDir + y*upDir + z*centerDir);
×
823
}
824

UNCOV
825
double LensDistortionEstimatorDialog::computeAngleBetweenImagePoints(const Vec2d& pointA, const Vec2d& pointB, const double smallerSideFoV) const
×
826
{
827
        using namespace std;
828

829
        const auto centeredPointA = imagePointToNormalizedCoords(pointA);
×
830
        const auto centeredPointB = imagePointToNormalizedCoords(pointB);
×
UNCOV
831
        const auto tanFoV2 = tan(smallerSideFoV/2);
×
832

833
        auto pointA3D = Vec3d(centeredPointA[0] * tanFoV2,
×
834
                              centeredPointA[1] * tanFoV2,
×
835
                              1);
×
UNCOV
836
        pointA3D.normalize();
×
837

838
        auto pointB3D = Vec3d(centeredPointB[0] * tanFoV2,
×
839
                              centeredPointB[1] * tanFoV2,
×
840
                              1);
×
UNCOV
841
        pointB3D.normalize();
×
842

UNCOV
843
        return acos(clamp(dot(pointA3D, pointB3D), -1., 1.));
×
844
}
845

UNCOV
846
void LensDistortionEstimatorDialog::repositionImageByPoints()
×
847
{
848
        Vec2d object0xy, object1xy;
×
UNCOV
849
        Vec3d object0dirTrue, object1dirTrue;
×
850
        using namespace std;
851

UNCOV
852
        if(optimizing_)
×
853
        {
UNCOV
854
                if(objectPositions_.size() < 2)
×
855
                {
856
                        qWarning() << "Reposition called while number of points less than 2. Ignoring.";
×
UNCOV
857
                        return;
×
858
                }
859
                object0xy = objectPositions_[0].imgPos;
×
860
                object1xy = objectPositions_[1].imgPos;
×
861
                object0dirTrue = objectPositions_[0].skyDir;
×
UNCOV
862
                object1dirTrue = objectPositions_[1].skyDir;
×
863
        }
864
        else
865
        {
UNCOV
866
                if(ui_->imagePointsPicked->topLevelItemCount() < 2)
×
867
                {
868
                        qWarning() << "Reposition called while number of points less than 2. Ignoring.";
×
UNCOV
869
                        return;
×
870
                }
871
                const auto item0 = ui_->imagePointsPicked->topLevelItem(0);
×
872
                const auto object0Name = item0->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
873
                const auto object0 = findObjectByName(object0Name);
×
UNCOV
874
                if(!object0)
×
875
                {
876
                        qWarning() << "Object" << object0Name << "not found, repositioning fails";
×
UNCOV
877
                        return;
×
878
                }
879

880
                const auto item1 = ui_->imagePointsPicked->topLevelItem(1);
×
881
                const auto object1Name = item1->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
882
                const auto object1 = findObjectByName(object1Name);
×
UNCOV
883
                if(!object1)
×
884
                {
885
                        qWarning() << "Object" << object1Name << "not found, repositioning fails";
×
UNCOV
886
                        return;
×
887
                }
888

889
                object0dirTrue = normalize(object0->getAltAzPosApparent(core_));
×
890
                object0xy = Vec2d(item0->data(Column::x, Qt::DisplayRole).toDouble(),
×
UNCOV
891
                                  item0->data(Column::y, Qt::DisplayRole).toDouble());
×
892

893
                object1dirTrue = normalize(object1->getAltAzPosApparent(core_));
×
894
                object1xy = Vec2d(item1->data(Column::x, Qt::DisplayRole).toDouble(),
×
895
                                  item1->data(Column::y, Qt::DisplayRole).toDouble());
×
896
        }
×
897
        auto object0dirByImg = computeImagePointDir(object0xy);
×
UNCOV
898
        auto object1dirByImg = computeImagePointDir(object1xy);
×
899

900
        // Find the correct FoV to make angle between the two image points correct
901
        const auto angleBetweenObjectsTrue = acos(clamp(dot(object0dirTrue, object1dirTrue), -1., 1.));
×
902
        double fovMax = 0.999*M_PI;
×
903
        double fovMin = 0;
×
904
        double fov = (fovMin + fovMax) / 2.;
×
UNCOV
905
        for(int n = 0; n < 53 && fovMin != fovMax; ++n)
×
906
        {
907
                const auto angleBetweenObjectsByImg = computeAngleBetweenImagePoints(object0xy, object1xy, fov);
×
908
                if(angleBetweenObjectsByImg > angleBetweenObjectsTrue)
×
UNCOV
909
                        fovMax = fov;
×
910
                else
911
                        fovMin = fov;
×
UNCOV
912
                fov = (fovMin + fovMax) / 2.;
×
913
        }
UNCOV
914
        setImageSmallerSideFoV(180/M_PI * fov);
×
915

916
        // Recompute the directions after FoV update
917
        object0dirByImg = computeImagePointDir(object0xy);
×
UNCOV
918
        object1dirByImg = computeImagePointDir(object1xy);
×
919

920
        // Find the matrix to rotate two image points to corresponding directions in space
921
        const auto crossTrue = object0dirTrue ^ object1dirTrue;
×
922
        const auto crossByImg = object0dirByImg ^ object1dirByImg;
×
923
        const auto preRotByImg = Mat3d(object0dirByImg[0], object0dirByImg[1], object0dirByImg[2],
×
924
                                       object1dirByImg[0], object1dirByImg[1], object1dirByImg[2],
×
925
                                            crossByImg[0],      crossByImg[1],      crossByImg[2]);
×
926
        const auto preRotTrue = Mat3d(object0dirTrue[0], object0dirTrue[1], object0dirTrue[2],
×
927
                                      object1dirTrue[0], object1dirTrue[1], object1dirTrue[2],
×
928
                                           crossTrue[0],      crossTrue[1],      crossTrue[2]);
×
UNCOV
929
        const auto rotator = preRotTrue * preRotByImg.inverse();
×
930

931
        const auto origUpDir = imageUpDir();
×
932
        const auto origCenterDir = projectionCenterDir();
×
933
        const auto newCenterDir = normalize(rotator * origCenterDir);
×
934
        const auto elevation = asin(clamp(newCenterDir[2], -1.,1.));
×
935
        const auto azimuth = atan2(newCenterDir[1], -newCenterDir[0]);
×
936
        setProjectionCenterAzimuth(180/M_PI * azimuth);
×
UNCOV
937
        setProjectionCenterElevation(180/M_PI * elevation);
×
938

939
        const auto origFromCenterToTop = normalize(origCenterDir + 1e-8 * origUpDir);
×
UNCOV
940
        const auto newFromCenterToTop = normalize(rotator * origFromCenterToTop);
×
941
        // Desired up direction
UNCOV
942
        const auto upDirNew = normalize(newFromCenterToTop - newCenterDir);
×
943
        // Renewed up direction takes into account the new center direction but not the desired orientation yet
944
        setImageFieldRotation(0);
×
945
        const auto renewedUpDir = imageUpDir();
×
946
        const auto upDirCross = renewedUpDir ^ upDirNew;
×
947
        const auto upDirDot = dot(renewedUpDir, upDirNew);
×
948
        const auto dirSign = dot(upDirCross, newCenterDir) > 0 ? -1. : 1.;
×
949
        const auto upDirSinAngle = dirSign * (dirSign * upDirCross).norm();
×
950
        const auto upDirCosAngle = upDirDot;
×
951
        const auto fieldRotation = atan2(upDirSinAngle, upDirCosAngle);
×
UNCOV
952
        setImageFieldRotation(180/M_PI * fieldRotation);
×
953

954
        if(!optimizing_)
×
UNCOV
955
                updateErrorsChart();
×
956
}
957

UNCOV
958
void LensDistortionEstimatorDialog::resetImagePlacement()
×
959
{
UNCOV
960
        if(image_.isNull())
×
961
        {
962
                setImageFieldRotation(0);
×
963
                setProjectionCenterAzimuth(0);
×
964
                setProjectionCenterElevation(0);
×
965
                setImageSmallerSideFoV(60);
×
UNCOV
966
                return;
×
967
        }
968

969
        const auto ret = QMessageBox::warning(&StelMainView::getInstance(), q_("Confirm resetting image placement"),
×
UNCOV
970
                                              q_("This will move the image to the center of the screen. Proceed?"),
×
971
                                              QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel);
UNCOV
972
        if(ret != QMessageBox::Yes) return;
×
973

UNCOV
974
        placeImageInCenterOfScreen();
×
975
}
976

UNCOV
977
void LensDistortionEstimatorDialog::resetDistortion()
×
978
{
979
        const auto ret = QMessageBox::warning(&StelMainView::getInstance(), q_("Confirm resetting distortion"),
×
UNCOV
980
                                              q_("This will zero out all distortion and shift coefficients. Proceed?"),
×
981
                                              QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel);
UNCOV
982
        if(ret != QMessageBox::Yes) return;
×
983

984
        setImageCenterShiftX(0);
×
985
        setImageCenterShiftY(0);
×
986
        setDistortionTerm1(0);
×
987
        setDistortionTerm2(0);
×
UNCOV
988
        setDistortionTerm3(0);
×
989
}
990

UNCOV
991
void LensDistortionEstimatorDialog::optimizeParameters()
×
992
{
993
#ifdef HAVE_NLOPT
UNCOV
994
        const auto funcToMinimize = [](const unsigned n, const double *v, double*, void*const params)
×
995
        {
UNCOV
996
                const auto dialog = static_cast<LensDistortionEstimatorDialog*>(params);
×
997

998
                const double xShift = v[0];
×
999
                const double yShift = v[1];
×
1000
                const double imageFieldRot = v[2];
×
1001
                const double centerAzim = v[3];
×
1002
                const double centerElev = v[4];
×
1003
                const double a = v[5];
×
1004
                const double b = dialog->distortionTerm2inUse_ ? v[6] : 0.;
×
UNCOV
1005
                const double c = dialog->distortionTerm3inUse_ ? v[7] : 0.;
×
1006

1007
                dialog->setImageCenterShiftX(xShift);
×
1008
                dialog->setImageCenterShiftY(yShift);
×
1009
                dialog->setImageFieldRotation(imageFieldRot);
×
1010
                dialog->setProjectionCenterAzimuth(centerAzim);
×
1011
                dialog->setProjectionCenterElevation(centerElev);
×
1012
                dialog->setDistortionTerm1(a);
×
1013
                dialog->setDistortionTerm2(b);
×
UNCOV
1014
                dialog->setDistortionTerm3(c);
×
1015

UNCOV
1016
                dialog->repositionImageByPoints();
×
1017

1018
                double errorMeasure = 0;
×
UNCOV
1019
                for(const auto& obj : dialog->objectPositions_)
×
1020
                {
UNCOV
1021
                        const auto objectDirByImg = dialog->computeImagePointDir(obj.imgPos);
×
1022

1023
                        using namespace std;
1024
                        const auto error = acos(clamp(dot(objectDirByImg,obj.skyDir), -1.,1.));
×
UNCOV
1025
                        errorMeasure += sqr(error);
×
1026
                }
UNCOV
1027
                return errorMeasure;
×
1028
        };
1029

1030
        objectPositions_.clear();
×
UNCOV
1031
        for(int n = 0; n < ui_->imagePointsPicked->topLevelItemCount(); ++n)
×
1032
        {
1033
                const auto item = ui_->imagePointsPicked->topLevelItem(n);
×
1034
                const auto objectName = item->data(Column::ObjectName, Role::ObjectEnglishName).toString();
×
1035
                const auto object = findObjectByName(objectName);
×
UNCOV
1036
                if(!object)
×
1037
                {
1038
                        qWarning() << "Object" << objectName << "not found, skipping it during optimization";
×
UNCOV
1039
                        continue;
×
1040
                }
1041
                const auto objectDirTrue = normalize(object->getAltAzPosApparent(core_));
×
1042
                const Vec2d objectXY(item->data(Column::x, Qt::DisplayRole).toDouble(),
×
1043
                                     item->data(Column::y, Qt::DisplayRole).toDouble());
×
1044
                objectPositions_.push_back({objectXY, objectDirTrue});
×
UNCOV
1045
        }
×
1046

1047
        distortionTerm1_ = distortionTerm1(); distortionTerm1min_ = ui_->distortionTerm1->minimum(); distortionTerm1max_ = ui_->distortionTerm1->maximum();
×
1048
        distortionTerm2_ = distortionTerm2(); distortionTerm2min_ = ui_->distortionTerm2->minimum(); distortionTerm2max_ = ui_->distortionTerm2->maximum();
×
UNCOV
1049
        distortionTerm3_ = distortionTerm3(); distortionTerm3min_ = ui_->distortionTerm3->minimum(); distortionTerm3max_ = ui_->distortionTerm3->maximum();
×
1050

1051
        imageCenterShiftX_ = imageCenterShiftX();
×
1052
        imageCenterShiftXmin_ = ui_->imageCenterShiftX->minimum();
×
UNCOV
1053
        imageCenterShiftXmax_ = ui_->imageCenterShiftX->maximum();
×
1054

1055
        imageCenterShiftY_ = imageCenterShiftY();
×
1056
        imageCenterShiftYmin_ = ui_->imageCenterShiftY->minimum();
×
UNCOV
1057
        imageCenterShiftYmax_ = ui_->imageCenterShiftY->maximum();
×
1058

UNCOV
1059
        imageSmallerSideFoV_ = ui_->imageSmallerSideFoV->value();
×
1060

1061
        imageFieldRotation_ = imageFieldRotation();
×
1062
        imageFieldRotationMin_ = ui_->imageFieldRotation->minimum();
×
UNCOV
1063
        imageFieldRotationMax_ = ui_->imageFieldRotation->maximum();
×
1064

1065
        projectionCenterAzimuth_ = projectionCenterAzimuth();
×
1066
        projectionCenterAzimuthMin_ = ui_->projectionCenterAzimuth->minimum();
×
UNCOV
1067
        projectionCenterAzimuthMax_ = ui_->projectionCenterAzimuth->maximum();
×
1068

1069
        projectionCenterElevation_ = projectionCenterElevation();
×
1070
        projectionCenterElevationMin_ = ui_->projectionCenterElevation->minimum();
×
UNCOV
1071
        projectionCenterElevationMax_ = ui_->projectionCenterElevation->maximum();
×
1072

1073
        distortionModel_ = distortionModel();
×
UNCOV
1074
        optimizing_ = true;
×
1075

UNCOV
1076
        repositionImageByPoints();
×
1077

1078
        const unsigned numVars = 5 +
×
1079
                                 distortionTerm1inUse_ +
×
1080
                                 distortionTerm2inUse_ +
×
UNCOV
1081
                                 distortionTerm3inUse_;
×
1082
        std::vector<double> values{
1083
                                   imageCenterShiftX_,
×
1084
                                   imageCenterShiftY_,
×
1085
                                   imageFieldRotation_,
×
1086
                                   projectionCenterAzimuth_,
×
1087
                                   projectionCenterElevation_,
×
1088
                                   distortionTerm1_,
×
1089
                                   distortionTerm2_,
×
1090
                                   distortionTerm3_,
×
UNCOV
1091
                                  };
×
1092
        std::vector<double> lowerBounds{
1093
                                          imageCenterShiftXmin_,
×
1094
                                          imageCenterShiftYmin_,
×
1095
                                          imageFieldRotationMin_,
×
1096
                                          projectionCenterAzimuthMin_,
×
1097
                                          projectionCenterElevationMin_,
×
1098
                                          distortionTerm1min_,
×
1099
                                          distortionTerm2min_,
×
1100
                                          distortionTerm3min_,
×
UNCOV
1101
                                       };
×
1102
        std::vector<double> upperBounds{
1103
                                          imageCenterShiftXmax_,
×
1104
                                          imageCenterShiftYmax_,
×
1105
                                          imageFieldRotationMax_,
×
1106
                                          projectionCenterAzimuthMax_,
×
1107
                                          projectionCenterElevationMax_,
×
1108
                                          distortionTerm1max_,
×
1109
                                          distortionTerm2max_,
×
1110
                                          distortionTerm3max_,
×
UNCOV
1111
                                       };
×
1112
        // Remove trailing values if needed
1113
        values.resize(numVars);
×
1114
        lowerBounds.resize(numVars);
×
UNCOV
1115
        upperBounds.resize(numVars);
×
1116

1117
        nlopt::opt minimizer(nlopt::algorithm::LN_NELDERMEAD, numVars);
×
1118
        minimizer.set_lower_bounds(lowerBounds);
×
1119
        minimizer.set_upper_bounds(upperBounds);
×
1120
        minimizer.set_min_objective(funcToMinimize, this);
×
UNCOV
1121
        minimizer.set_ftol_rel(1e-3);
×
1122

1123
        try
1124
        {
1125
                double minf = INFINITY;
×
1126
                const auto result = minimizer.optimize(values, minf);
×
1127
                if(result > 0)
×
UNCOV
1128
                        qDebug() << "Found minimum, error measure:" << minf;
×
1129
                else
UNCOV
1130
                        qCritical() << "NLOpt failed with result " << minimizer.get_errmsg();
×
1131
        }
UNCOV
1132
        catch(const std::exception& ex)
×
1133
        {
1134
                qCritical() << "NLOpt failed:" << ex.what();
×
UNCOV
1135
        }
×
1136

1137
        optimizing_ = false;
×
1138
        setDistortionTerm1(distortionTerm1_);
×
1139
        setDistortionTerm2(distortionTerm2_);
×
1140
        setDistortionTerm3(distortionTerm3_);
×
1141
        setImageCenterShiftX(imageCenterShiftX_);
×
1142
        setImageCenterShiftY(imageCenterShiftY_);
×
1143
        setImageFieldRotation(imageFieldRotation_);
×
1144
        setImageSmallerSideFoV(imageSmallerSideFoV_);
×
1145
        setProjectionCenterAzimuth(projectionCenterAzimuth_);
×
UNCOV
1146
        setProjectionCenterElevation(projectionCenterElevation_);
×
1147

UNCOV
1148
        updateErrorsChart();
×
1149
#endif
UNCOV
1150
}
×
1151

UNCOV
1152
void LensDistortionEstimatorDialog::updateImagePathStatus()
×
1153
{
UNCOV
1154
        if(QFileInfo(ui_->imageFilePath->text()).exists())
×
1155
        {
UNCOV
1156
                ui_->imageFilePath->setStyleSheet("");
×
1157
        }
1158
        else
1159
        {
UNCOV
1160
                ui_->imageFilePath->setStyleSheet("color:red;");
×
1161
        }
UNCOV
1162
}
×
1163

UNCOV
1164
void LensDistortionEstimatorDialog::computeColorToSubtract()
×
1165
{
1166
        // Compute the (rounded down) median of all the pixels
1167
        // First get the histogram
1168
        std::array<size_t, 256> histogramR{};
×
1169
        std::array<size_t, 256> histogramG{};
×
1170
        std::array<size_t, 256> histogramB{};
×
1171
        const uchar*const data = image_.bits();
×
1172
        const auto stride = image_.bytesPerLine();
×
UNCOV
1173
        for(int j = 0; j < image_.height(); ++j)
×
1174
        {
UNCOV
1175
                for(int i = 0; i < image_.width(); ++i)
×
1176
                {
1177
                        const auto r = data[j*stride+4*i+0];
×
1178
                        const auto g = data[j*stride+4*i+1];
×
1179
                        const auto b = data[j*stride+4*i+2];
×
1180
                        ++histogramR[r];
×
1181
                        ++histogramG[g];
×
UNCOV
1182
                        ++histogramB[b];
×
1183
                }
1184
        }
1185
        // Now use the histogram to find the (rounded down) median
1186
        const auto totalR = std::accumulate(histogramR.begin(), histogramR.end(), size_t(0));
×
1187
        const auto totalG = std::accumulate(histogramG.begin(), histogramG.end(), size_t(0));
×
1188
        const auto totalB = std::accumulate(histogramB.begin(), histogramB.end(), size_t(0));
×
1189
        size_t sumR = 0, sumG = 0, sumB = 0;
×
1190
        int midR = -1, midG = -1, midB = -1;
×
UNCOV
1191
        for(unsigned n = 0; n < histogramR.size(); ++n)
×
1192
        {
1193
                sumR += histogramR[n];
×
1194
                sumG += histogramG[n];
×
1195
                sumB += histogramB[n];
×
1196
                if(midR < 0 && sumR > totalR/2) midR = n;
×
1197
                if(midG < 0 && sumG > totalG/2) midG = n;
×
UNCOV
1198
                if(midB < 0 && sumB > totalB/2) midB = n;
×
1199
        }
1200
        ui_->bgRed->setValue(midR);
×
1201
        ui_->bgGreen->setValue(midG);
×
1202
        ui_->bgBlue->setValue(midB);
×
UNCOV
1203
}
×
1204

UNCOV
1205
void LensDistortionEstimatorDialog::placeImageInCenterOfScreen()
×
1206
{
1207
        using namespace std;
1208

1209
        const auto mvtMan = core_->getMovementMgr();
×
1210
        const auto vFoV = mvtMan->getCurrentFov();
×
UNCOV
1211
        setImageSmallerSideFoV(clamp(0.98*vFoV, 0.,170.));
×
1212

1213
        const auto j2000 = mvtMan->getViewDirectionJ2000();
×
1214
        const auto altAz = core_->j2000ToAltAz(j2000, StelCore::RefractionOff);
×
1215
        const auto elevation = asin(clamp(altAz[2], -1.,1.));
×
1216
        const auto azimuth = atan2(altAz[1], -altAz[0]);
×
1217
        setProjectionCenterElevation(180/M_PI * elevation);
×
1218
        setProjectionCenterAzimuth(180/M_PI * azimuth);
×
UNCOV
1219
        setImageFieldRotation(0);
×
1220

1221
        if(!mvtMan->getEquatorialMount())
×
UNCOV
1222
                return;
×
1223

1224
        const auto proj = core_->getProjection(StelCore::FrameAltAz, StelCore::RefractionMode::RefractionOff);
×
1225
        const auto imageCenterDir = projectionCenterDir();
×
1226
        const auto imageUpperDir = normalize(imageCenterDir + 1e-3 * imageUpDir());
×
1227
        Vec3d imgCenterWin, imgUpWin;
×
1228
        proj->project(imageCenterDir, imgCenterWin);
×
1229
        proj->project(imageUpperDir, imgUpWin);
×
1230
        const auto angle = -atan2(imgUpWin[1]-imgCenterWin[1], imgUpWin[0]-imgCenterWin[0]) + M_PI/2;
×
1231
        setImageFieldRotation(180/M_PI * angle);
×
UNCOV
1232
}
×
1233

UNCOV
1234
void LensDistortionEstimatorDialog::updateImage()
×
1235
{
1236
        const auto path = ui_->imageFilePath->text();
×
1237
        image_ = QImage(path).convertToFormat(QImage::Format_RGBA8888).mirrored(false, true);
×
UNCOV
1238
        updateRepositionButtons();
×
1239

UNCOV
1240
        if(image_.isNull())
×
1241
        {
1242
                ui_->imageFilePath->setStyleSheet("color:red;");
×
UNCOV
1243
                return;
×
1244
        }
1245

1246
        ui_->imageFilePath->setStyleSheet("");
×
1247
        imageChanged_ = true;
×
1248
        setupGenerateLensfunModelButton();
×
1249
        computeColorToSubtract();
×
UNCOV
1250
        if(ui_->imgPosResetOnLoadingEnabled->isChecked())
×
UNCOV
1251
                placeImageInCenterOfScreen();
×
1252

1253
#ifdef HAVE_EXIV2
1254
        try
1255
        {
1256
                const auto image = Exiv2::ImageFactory::open(path.toStdString());
×
1257
                if(!image.get()) return;
×
UNCOV
1258
                image->readMetadata();
×
1259
                const auto& exif = image->exifData();
×
1260

UNCOV
1261
                QString date;
×
1262
                for(const auto& key : {"Exif.Photo.DateTimeOriginal",
×
1263
                                       "Exif.Image.DateTimeOriginal",
1264
                                       "Exif.Image.DateTime"})
×
1265
                {
UNCOV
1266
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
1267
                        if(it!=exif.end())
×
1268
                        {
UNCOV
1269
                                date = QString::fromStdString(it->toString());
×
UNCOV
1270
                                break;
×
1271
                        }
1272
                }
1273

UNCOV
1274
                int timeZone = INT_MIN;
×
1275
                for(const auto& key : {"Exif.CanonTi.TimeZone"})
×
1276
                {
UNCOV
1277
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
UNCOV
1278
                        if(it != exif.end() && it->typeId() == Exiv2::signedLong)
×
1279
                        {
1280
#if EXIV2_MINOR_VERSION < 28
UNCOV
1281
                                const auto num = it->toLong();
×
1282
#else
1283
                                const auto num = it->toInt64();
1284
#endif
UNCOV
1285
                                timeZone = num * 60; // save as seconds
×
UNCOV
1286
                                break;
×
1287
                        }
1288
                }
1289

UNCOV
1290
                QString gpsTime;
×
1291
                for(const auto& key : {"Exif.GPSInfo.GPSTimeStamp"})
×
1292
                {
UNCOV
1293
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
1294
                        if(it!=exif.end())
×
1295
                        {
1296
                                if(it->count() != 3)
×
1297
                                        continue;
×
1298
                                const auto hour = it->toFloat(0);
×
1299
                                const auto min = it->toFloat(1);
×
1300
                                const auto sec = it->toFloat(2);
×
1301
                                if(hour < 0 || hour > 59 || hour - std::floor(hour) != 0 ||
×
1302
                                   min  < 0 || min  > 59 || min  - std::floor(min ) != 0)
×
1303
                                        continue;
×
1304
                                gpsTime = QString("%1:%2:%3").arg(int(hour), 2, 10, QChar('0'))
×
1305
                                                             .arg(int(min), 2, 10, QChar('0'))
×
UNCOV
1306
                                                             .arg(double(sec), 2, 'f', 0, QChar('0'));
×
UNCOV
1307
                                break;
×
1308
                        }
1309
                }
1310

UNCOV
1311
                QString gpsDate;
×
1312
                for(const auto& key : {"Exif.GPSInfo.GPSDateStamp"})
×
1313
                {
UNCOV
1314
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
1315
                        if(it!=exif.end())
×
1316
                        {
UNCOV
1317
                                gpsDate = QString::fromStdString(it->toString());
×
UNCOV
1318
                                break;
×
1319
                        }
1320
                }
1321

1322
                // If GPS date and time are present, take them as more reliable.
1323
                // At the very least, time zone is present there unconditionally.
1324
                if(!gpsDate.isEmpty() && !gpsTime.isEmpty())
×
1325
                {
UNCOV
1326
                        date = gpsDate + " " + gpsTime;
×
UNCOV
1327
                        timeZone = 0;
×
1328
                }
1329

1330
                exifDateTime_ = {};
×
UNCOV
1331
                clearWarnings();
×
1332
                if(date.isEmpty())
×
1333
                {
UNCOV
1334
                        emitWarning(q_("Can't get EXIF date from the image, please make "
×
1335
                                       "sure current date and time setting is correct."));
1336
                }
1337
                else
1338
                {
1339
                        exifDateTime_ = QDateTime::fromString(date, "yyyy:MM:dd HH:mm:ss");
×
1340
                        if(timeZone != INT_MIN)
×
UNCOV
1341
                                exifDateTime_.setTimeZone(QTimeZone(timeZone));
×
1342
                        if(!exifDateTime_.isValid())
×
1343
                        {
UNCOV
1344
                                emitWarning(q_("Failed to parse EXIF date/time, please make sure "
×
1345
                                               "current date and time setting is correct."));
1346
                        }
1347
                        else
UNCOV
1348
                                periodicWarningsCheck();
×
UNCOV
1349
                        warningCheckTimer_.start();
×
1350
                }
1351

1352
                for(const auto& key : {"Exif.Photo.FocalLength", "Exif.Image.FocalLength"})
×
1353
                {
1354
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
1355
                        if(it==exif.end() || it->typeId() != Exiv2::unsignedRational)
×
1356
                                continue;
×
1357
                        const auto [num,denom] = it->toRational();
×
1358
                        if(denom==0) continue;
×
UNCOV
1359
                        ui_->lensFocalLength->setValue(double(num)/denom);
×
UNCOV
1360
                        break;
×
1361
                }
1362

1363
                for(const auto& key : {"Exif.Photo.LensModel"})
×
1364
                {
UNCOV
1365
                        const auto it=exif.findKey(Exiv2::ExifKey(key));
×
1366
                        if(it != exif.end() && it->typeId() == Exiv2::signedLong)
×
1367
                        {
1368
                                const auto model = it->toString();
×
1369
                                if(model.empty()) continue;
×
1370
                                ui_->lensMake->setText(model.c_str());
×
UNCOV
1371
                                break;
×
1372
                        }
×
1373
                }
UNCOV
1374
        }
×
1375
        catch(Exiv2::Error& e)
×
1376
        {
UNCOV
1377
                qDebug() << "exiv2 error:" << e.what();
×
1378
        }
×
1379
#endif
1380
}
×
1381

1382
void LensDistortionEstimatorDialog::browseForImage()
×
1383
{
1384
        const auto path = QFileDialog::getOpenFileName(&StelMainView::getInstance(), q_("Open image file"), {},
×
1385
                                                       q_("Image files (*.png *.bmp *.jpg *.jpeg *.tif *.tiff *.webm *.pbm *.pgm *.ppm *.xbm *.xpm)"));
×
1386
        if(path.isNull()) return;
×
1387
        ui_->imageFilePath->setText(path);
×
UNCOV
1388
        updateImage();
×
1389
}
×
1390

1391
void LensDistortionEstimatorDialog::init()
×
1392
{
1393
}
×
1394

1395
Vec3d LensDistortionEstimatorDialog::imageUpDir() const
×
1396
{
UNCOV
1397
        const auto az = M_PI/180 * projectionCenterAzimuth();
×
1398
        const auto el = M_PI/180 * projectionCenterElevation();
×
1399
        // This unrotated direction is just a derivative of projectionCenterDir with respect to elevation
1400
        const auto sinEl = std::sin(el);
×
1401
        const auto cosEl = std::cos(el);
×
1402
        const auto cosAz = std::cos(az);
×
UNCOV
1403
        const auto sinAz = std::sin(az);
×
1404
        const Vec3d unrotated(sinEl * cosAz, -sinEl * sinAz, cosEl);
×
1405
        // The rotated direction that we return is the one that takes imageFieldRotation into account.
1406
        const Vec3d centerDir(-cosEl * cosAz, cosEl * sinAz, sinEl); // same as projectionCenterDir(), but reusing sines & cosines
×
1407
        const Mat3d rotator = Mat4d::rotation(centerDir, -M_PI/180 * imageFieldRotation()).upper3x3();
×
UNCOV
1408
        const Vec3d rotated = rotator * unrotated;
×
UNCOV
1409
        return Vec3d(rotated[0], rotated[1], rotated[2]);
×
1410
}
1411

1412
Vec3d LensDistortionEstimatorDialog::projectionCenterDir() const
×
1413
{
UNCOV
1414
        const auto az = M_PI/180 * projectionCenterAzimuth();
×
1415
        const auto el = M_PI/180 * projectionCenterElevation();
×
1416
        // Following FrameAltAz
1417
        return Vec3d(-std::cos(el) * std::cos(az),
×
UNCOV
1418
                      std::cos(el) * std::sin(az),
×
UNCOV
1419
                      std::sin(el));
×
1420
}
1421

1422
Vec2d LensDistortionEstimatorDialog::imageCenterShift() const
×
1423
{
1424
        return 2. * Vec2d(-imageCenterShiftX(), imageCenterShiftY())
×
1425
                                      /
UNCOV
1426
               std::min(image_.width()-1, image_.height()-1);
×
1427
}
1428

1429
QColor LensDistortionEstimatorDialog::bgToSubtract() const
×
1430
{
1431
        if(ui_->subtractBG->isChecked())
×
UNCOV
1432
                return QColor(ui_->bgRed->value(), ui_->bgGreen->value(), ui_->bgBlue->value());
×
UNCOV
1433
        return QColor(0,0,0);
×
1434
}
1435

1436
double LensDistortionEstimatorDialog::imageBrightness() const
×
1437
{
UNCOV
1438
        return ui_->imageBrightness->value() / 100.;
×
1439
}
1440

1441
double LensDistortionEstimatorDialog::distortionTerm1() const
×
1442
{
UNCOV
1443
        if(optimizing_) return distortionTerm1_;
×
UNCOV
1444
        return ui_->distortionTerm1->value();
×
1445
}
1446

1447
double LensDistortionEstimatorDialog::distortionTerm2() const
×
1448
{
UNCOV
1449
        if(optimizing_) return distortionTerm2_;
×
UNCOV
1450
        return ui_->distortionTerm2->value();
×
1451
}
1452

1453
double LensDistortionEstimatorDialog::distortionTerm3() const
×
1454
{
UNCOV
1455
        if(optimizing_) return distortionTerm3_;
×
UNCOV
1456
        return ui_->distortionTerm3->value();
×
1457
}
1458

1459
double LensDistortionEstimatorDialog::imageCenterShiftX() const
×
1460
{
UNCOV
1461
        if(optimizing_) return imageCenterShiftX_;
×
UNCOV
1462
        return ui_->imageCenterShiftX->value();
×
1463
}
1464

1465
double LensDistortionEstimatorDialog::imageCenterShiftY() const
×
1466
{
UNCOV
1467
        if(optimizing_) return imageCenterShiftY_;
×
UNCOV
1468
        return ui_->imageCenterShiftY->value();
×
1469
}
1470

1471
double LensDistortionEstimatorDialog::imageFieldRotation() const
×
1472
{
UNCOV
1473
        if(optimizing_) return imageFieldRotation_;
×
UNCOV
1474
        return ui_->imageFieldRotation->value();
×
1475
}
1476

1477
double LensDistortionEstimatorDialog::projectionCenterAzimuth() const
×
1478
{
UNCOV
1479
        if(optimizing_) return projectionCenterAzimuth_;
×
UNCOV
1480
        return ui_->projectionCenterAzimuth->value();
×
1481
}
1482

1483
double LensDistortionEstimatorDialog::projectionCenterElevation() const
×
1484
{
UNCOV
1485
        if(optimizing_) return projectionCenterElevation_;
×
UNCOV
1486
        return ui_->projectionCenterElevation->value();
×
1487
}
1488

1489
double LensDistortionEstimatorDialog::imageSmallerSideFoV() const
×
1490
{
UNCOV
1491
        if(optimizing_) return imageSmallerSideFoV_;
×
UNCOV
1492
        return ui_->imageSmallerSideFoV->value();
×
1493
}
1494

1495
DistortionModel LensDistortionEstimatorDialog::distortionModel() const
×
1496
{
UNCOV
1497
        if(optimizing_) return distortionModel_;
×
UNCOV
1498
        return static_cast<DistortionModel>(ui_->distortionModelCB->currentIndex());
×
1499
}
1500

1501
void LensDistortionEstimatorDialog::setDistortionTerm1(const double a)
×
1502
{
1503
        if(optimizing_) distortionTerm1_ = a;
×
UNCOV
1504
        else ui_->distortionTerm1->setValue(a);
×
1505
}
×
1506

1507
void LensDistortionEstimatorDialog::setDistortionTerm2(const double b)
×
1508
{
1509
        if(optimizing_) distortionTerm2_ = b;
×
UNCOV
1510
        else ui_->distortionTerm2->setValue(b);
×
1511
}
×
1512

1513
void LensDistortionEstimatorDialog::setDistortionTerm3(const double c)
×
1514
{
1515
        if(optimizing_) distortionTerm3_ = c;
×
UNCOV
1516
        else ui_->distortionTerm3->setValue(c);
×
1517
}
×
1518

1519
void LensDistortionEstimatorDialog::setImageCenterShiftX(const double xShift)
×
1520
{
1521
        if(optimizing_) imageCenterShiftX_ = xShift;
×
UNCOV
1522
        else ui_->imageCenterShiftX->setValue(xShift);
×
1523
}
×
1524

1525
void LensDistortionEstimatorDialog::setImageCenterShiftY(const double yShift)
×
1526
{
1527
        if(optimizing_) imageCenterShiftY_ = yShift;
×
UNCOV
1528
        else ui_->imageCenterShiftY->setValue(yShift);
×
1529
}
×
1530

1531
void LensDistortionEstimatorDialog::setImageFieldRotation(const double imageFieldRot)
×
1532
{
1533
        if(optimizing_) imageFieldRotation_ = imageFieldRot;
×
UNCOV
1534
        else ui_->imageFieldRotation->setValue(imageFieldRot);
×
1535
}
×
1536

1537
void LensDistortionEstimatorDialog::setProjectionCenterAzimuth(const double centerAzim)
×
1538
{
1539
        if(optimizing_) projectionCenterAzimuth_ = centerAzim;
×
UNCOV
1540
        else ui_->projectionCenterAzimuth->setValue(centerAzim);
×
1541
}
×
1542

1543
void LensDistortionEstimatorDialog::setProjectionCenterElevation(const double centerElev)
×
1544
{
1545
        if(optimizing_) projectionCenterElevation_ = centerElev;
×
UNCOV
1546
        else ui_->projectionCenterElevation->setValue(centerElev);
×
1547
}
×
1548

1549
void LensDistortionEstimatorDialog::setImageSmallerSideFoV(const double fov)
×
1550
{
1551
        if(optimizing_) imageSmallerSideFoV_ = fov;
×
UNCOV
1552
        else ui_->imageSmallerSideFoV->setValue(fov);
×
1553
}
×
1554

1555
auto LensDistortionEstimatorDialog::imagePointDirections() const -> std::vector<ImagePointStatus>
×
1556
{
1557
        std::vector<ImagePointStatus> statuses;
×
UNCOV
1558
        const auto nMax = ui_->imagePointsPicked->topLevelItemCount();
×
1559
        for(int n = 0; n < nMax; ++n)
×
1560
        {
1561
                const auto item = ui_->imagePointsPicked->topLevelItem(n);
×
1562
                const Vec2d objectXY(item->data(Column::x, Qt::DisplayRole).toDouble(),
×
UNCOV
1563
                                     item->data(Column::y, Qt::DisplayRole).toDouble());
×
1564
                statuses.push_back({computeImagePointDir(objectXY), item->isSelected()});
×
1565
        }
UNCOV
1566
        return statuses;
×
1567
}
×
1568

1569
QString LensDistortionEstimatorDialog::getObjectName(const StelObject& object) const
×
1570
{
UNCOV
1571
        auto name = object.getEnglishName();
×
UNCOV
1572
        if(!name.isEmpty()) return name;
×
1573

1574
        double raJ2000, decJ2000;
1575
        StelUtils::rectToSphe(&raJ2000, &decJ2000, object.getJ2000EquatorialPos(core_));
×
UNCOV
1576
        return QString("unnamed{RA=%1;DEC=%2}").arg(raJ2000 * 12/M_PI,0,'g',17).arg(decJ2000 * 180/M_PI,0,'g',17);
×
1577
}
×
1578

1579
StelObjectP LensDistortionEstimatorDialog::findObjectByName(const QString& name) const
×
1580
{
UNCOV
1581
        const QLatin1String unnamedObjectPrefix("unnamed{RA=");
×
1582
        if(!name.startsWith(unnamedObjectPrefix))
×
1583
        {
UNCOV
1584
                const auto obj = objectMgr_->searchByName(name);
×
1585
                if(!obj) return nullptr;
×
1586
                // Sometimes we get a completely wrong object when the correct one isn't found
1587
                if(obj->getEnglishName() != name) return nullptr;
×
UNCOV
1588
                return obj;
×
UNCOV
1589
        }
×
1590

1591
        // Parse the unnamed object
1592
        if(!name.endsWith("}")) return nullptr;
×
1593
        const auto raDecStr = name.mid(unnamedObjectPrefix.size(), name.size() - unnamedObjectPrefix.size() - 1);
×
UNCOV
1594
        const auto raDecStrings = raDecStr.split(";DEC=");
×
1595
        if(raDecStrings.size() != 2 || raDecStrings[0].isEmpty() || raDecStrings[1].isEmpty())
×
1596
        {
UNCOV
1597
                qWarning() << "Failed to split unnamed object name" << raDecStrings;
×
1598
                return nullptr;
×
1599
        }
1600
        bool okRA = false, okDE = false;
×
1601
        const auto ra = raDecStrings[0].toDouble(&okRA);
×
UNCOV
1602
        const auto dec = raDecStrings[1].toDouble(&okDE);
×
1603
        if(!okRA || !okDE)
×
1604
        {
UNCOV
1605
                qWarning() << "Failed to parse RA" << raDecStrings[0] << "or DE" << raDecStrings[1];
×
UNCOV
1606
                return nullptr;
×
1607
        }
1608

1609
        // There's no specific string handle for unnamed objects, so they have to be searched by coordinates
UNCOV
1610
        Vec3d coords(0,0,0);
×
1611
        StelUtils::spheToRect(ra*M_PI/12, dec*M_PI/180, coords);
×
1612

1613
        QList<StelObjectP> stars = starMgr_->searchAround(coords, 1e-4, core_);
×
1614
        StelObjectP object = nullptr;
×
UNCOV
1615
        double minDist = INFINITY;
×
1616
        for (const auto& currObj : stars)
×
1617
        {
UNCOV
1618
                const auto dist = coords.angle(currObj->getJ2000EquatorialPos(core_));
×
1619
                if (dist < minDist)
×
1620
                {
UNCOV
1621
                        minDist = dist;
×
UNCOV
1622
                        object = currObj;
×
1623
                }
1624
        }
UNCOV
1625
        return object;
×
1626
}
×
1627

1628
void LensDistortionEstimatorDialog::updateDistortionCoefControls()
×
1629
{
UNCOV
1630
        const auto model = distortionModel();
×
UNCOV
1631
        distortionTerm1inUse_ = true;
×
1632
        // NOTE: simply hide/show is not sufficient to query this status when the tab is not at
1633
        // the page with these controls, so we use enable/disable in addition to visibility.
1634
        switch(model)
×
1635
        {
1636
        case DistortionModel::Poly3:
×
1637
                ui_->distortionTerm2->hide(); distortionTerm2inUse_ = false;
×
1638
                ui_->distortionTerm3->hide(); distortionTerm3inUse_ = false;
×
1639
                break;
×
1640
        case DistortionModel::Poly5:
×
1641
                ui_->distortionTerm2->show(); distortionTerm2inUse_ = true;
×
1642
                ui_->distortionTerm3->hide(); distortionTerm3inUse_ = false;
×
1643
                break;
×
1644
        case DistortionModel::PTLens:
×
1645
                ui_->distortionTerm2->show(); distortionTerm2inUse_ = true;
×
UNCOV
1646
                ui_->distortionTerm3->show(); distortionTerm3inUse_ = true;
×
1647
                break;
×
1648
        }
1649
}
×
1650

1651
void LensDistortionEstimatorDialog::setupGenerateLensfunModelButton()
×
1652
{
1653
        const bool anyEmpty = ui_->lensMake->text().isEmpty()  ||
×
1654
                              ui_->lensModel->text().isEmpty() ||
×
1655
                              ui_->lensMount->text().isEmpty() ||
×
1656
                              image_.isNull();
×
1657
        ui_->generateLensfunModelBtn->setEnabled(!anyEmpty);
×
UNCOV
1658
        if(anyEmpty)
×
1659
                ui_->generateLensfunModelBtn->setToolTip(q_("Make, model and mount fields must be filled"));
×
1660
        else
UNCOV
1661
                ui_->generateLensfunModelBtn->setToolTip("");
×
1662
}
×
1663

1664
void LensDistortionEstimatorDialog::generateLensfunModel()
×
1665
{
1666
        const auto lensfunCenter = -imageCenterShift();
×
1667
        const auto model = distortionModel();
×
UNCOV
1668
        QString distModel;
×
1669
        switch(model)
×
1670
        {
1671
        case DistortionModel::Poly3:
×
1672
                distModel = QString(R"(<distortion model="poly3" focal="%1" k1="%2"/>)")
×
1673
                                .arg(ui_->lensFocalLength->value(), 0, 'g', 7)
×
1674
                                .arg(distortionTerm1());
×
1675
                break;
×
1676
        case DistortionModel::Poly5:
×
1677
                distModel = QString(R"(<distortion model="poly5" focal="%1" k1="%2" k2="%3"/>)")
×
1678
                                .arg(ui_->lensFocalLength->value(), 0, 'g', 7)
×
1679
                                .arg(distortionTerm1())
×
1680
                                .arg(distortionTerm2());
×
1681
                break;
×
1682
        case DistortionModel::PTLens:
×
1683
                distModel = QString(R"(<distortion model="ptlens" focal="%1" a="%2" b="%3" c="%4"/>)")
×
1684
                                .arg(ui_->lensFocalLength->value(), 0, 'g', 7)
×
1685
                                .arg(distortionTerm1())
×
1686
                                .arg(distortionTerm2())
×
1687
                                .arg(distortionTerm3());
×
1688
                break;
×
1689
        default:
×
UNCOV
1690
                ui_->lensfunModelXML->setText(q_("Internal error: bad distortion model chosen"));
×
UNCOV
1691
                return;
×
1692
        }
1693

1694
        const auto gcd = std::gcd(image_.width(), image_.height());
×
UNCOV
1695
        const int aspectNum = image_.width() / gcd;
×
1696
        const int aspectDenom = image_.height() / gcd;
×
1697

UNCOV
1698
        ui_->lensfunModelXML->setText(QString(1+R"(
×
1699
<lens>
1700
    <maker>%1</maker>
1701
    <model>%2</model>
1702
    <mount>%3</mount>
1703
    <cropfactor>%4</cropfactor>
1704
    <aspect-ratio>%5:%6</aspect-ratio>
1705
    <center x="%7" y="%8"/>
1706
    <calibration>
1707
        %9
1708
    </calibration>
1709
</lens>
1710
)").arg(ui_->lensMake->text())
×
1711
   .arg(ui_->lensModel->text())
×
1712
   .arg(ui_->lensMount->text())
×
1713
   .arg(computeLensCropFactor(), 0, 'g', 7)
×
1714
   .arg(aspectNum)
×
1715
   .arg(aspectDenom)
×
1716
   .arg(lensfunCenter[0], 0, 'f', 7)
×
1717
   .arg(lensfunCenter[1], 0, 'f', 7)
×
UNCOV
1718
   .arg(distModel));
×
1719
}
×
1720

1721
double LensDistortionEstimatorDialog::computeLensCropFactor() const
×
1722
{
1723
        const double f = ui_->lensFocalLength->value();
×
1724
        const double alpha = M_PI/180 * imageSmallerSideFoV();
×
1725
        const double h = 2*f * std::tan(alpha/2);
×
1726
        const double largerSidePx  = std::max(image_.width(), image_.height());
×
1727
        const double smallerSidePx = std::min(image_.width(), image_.height());
×
1728
        const double aspect = largerSidePx/smallerSidePx;
×
UNCOV
1729
        const double w = h*aspect;
×
UNCOV
1730
        return std::hypot(36,24) / std::hypot(w,h);
×
1731
}
1732

1733
void LensDistortionEstimatorDialog::showSettingsPage()
×
1734
{
UNCOV
1735
        ui_->tabs->setCurrentIndex(Page::Settings);
×
1736
}
×
1737

1738
void LensDistortionEstimatorDialog::showComputationPage()
×
1739
{
1740
        const auto current = ui_->tabs->currentIndex();
×
1741
        if(current != Page::ImageLoading && current != Page::Fitting)
×
UNCOV
1742
                ui_->tabs->setCurrentIndex(Page::ImageLoading);
×
1743
}
×
1744

UNCOV
1745
void LensDistortionEstimatorDialog::setAboutText()
×
1746
{
1747
        QString html = "<html><head></head><body>"
1748
        "<h2>" + q_("Lens Distortion Estimator Plug-in") + "</h2>"
×
1749
        "<table class='layout' width=\"90%\">"
1750
                "<tr width=\"30%\"><td><strong>" + q_("Version") + ":</strong></td><td>" + LENSDISTORTIONESTIMATOR_PLUGIN_VERSION + "</td></tr>"
×
1751
                "<tr><td><strong>" + q_("License") + ":</strong></td><td>" + LENSDISTORTIONESTIMATOR_PLUGIN_LICENSE + "</td></tr>"
×
UNCOV
1752
                "<tr><td><strong>" + q_("Author") + ":</strong></td><td>Ruslan Kabatsayev &lt;b7.10110111+stellarium@gmail.com&gt;</td></tr>"
×
1753
        "</table>";
×
1754
        // Overview
1755
        html += "<h3>" + q_("Overview") + "</h3>";
×
1756

1757
        html += "<p>" + q_("This plugin lets you estimate distortion of the objective lens of your camera and generate "
×
UNCOV
1758
                           "a distortion model for use with the lensfun library.") + "</p>";
×
1759
        html += "<p>" + q_("The process is as follows.") + "</p>";
×
1760
        html += "<ol>"
1761
                "<li>" + q_("Take a photo of the night sky so that you can easily find enough stars scattered from the "
×
1762
                            "center of the frame to the border. Some care must be taken to get a useful shot, see more "
UNCOV
1763
                            "details in <b>Preparing a photo</b> section.\n") + "</li>"
×
UNCOV
1764
                "<li>" + q_("On the <i>Image Loading</i> tab choose the image to load and use the available controls (or "
×
1765
                            "hotkeys) to position it so that it approximately matches the direction where the camera "
1766
                            "looked and corresponding orientation. You need to be able to tell which point in the image "
UNCOV
1767
                            "corresponds to which simulated star/planet.") + "</li>"
×
UNCOV
1768
                "<li>" + q_("Then you switch to the <i>Fitting</i> tab, and pick some points in the image for a set "
×
1769
                            "of celestial objects. To do this,\n"
1770
                            "<ol><li>Select an object as you'd normally do.</li>\n"
1771
                                "<li>Click <i>Pick an image point for selected object...</i> button (or use a hotkey).</li>\n"
1772
                                "<li>Click the point in the image corresponding to this object (hide the simulated stars "
UNCOV
1773
                                    "if they are too confusing at this point).</li></ol>") + "</li>"
×
UNCOV
1774
                "<li>" + q_("Finally, when enough points have been picked to fill the different distances from image "
×
1775
                            "center, click <i>Optimize</i> button. After a few moments the image should appear aligned "
1776
                            "with the stars. If it's not, try picking more points from the ones which don't match, and "
UNCOV
1777
                            "retry optimization.") + "</li>"
×
1778
                "<li>" + q_("On the <i>Model for lensfun</i> tab you can generate the XML entry for lensfun database. "
×
1779
                            "Enter make and model for the lens, and the mount (the name must match one of the mounts "
UNCOV
1780
                            "supported by your camera as listed in the database).") + "</li>"
×
1781
                "</ol>";
×
1782

UNCOV
1783
        html += "<h3>" + q_("Preparing a photo") + "</h3>";
×
1784
        html += q_("To make a useful shot, you should follow some guidelines:\n");
×
1785
        html += "<ul>"
1786
                 "<li>" + q_("Try to keep away from the horizon, because star elevation there is very dependent on "
×
1787
                             "weather conditions due to atmospheric refraction.") + "</li>"
×
UNCOV
1788
                 "<li>" + q_("Be sure to disable any geometry distortion correction, because failure to disable "
×
1789
                            "distortion correction may make good fitting impossible. In particular:") +
×
1790
                  "<ul>"
1791
                   "<li>" + q_("If you're shooting RAW, lens distortion correction must be turned off in RAW development "
×
1792
                             "software like Lightroom or Darktable that you use for processing of your shots.") + "</li>"
×
UNCOV
1793
                   "<li>" + q_("If you're shooting JPEG, lens distortion correction must be turned off in the camera "
×
UNCOV
1794
                             "settings before taking the photo.") + "</li>"
×
1795
                  "</ul>"
1796
                 "</li>"
UNCOV
1797
                 "<li>" + q_("Disable automatic image rotation according to its orientation saved in EXIF tags. While "
×
1798
                             "such rotation may make the image look better, the shifts of the center of projection "
1799
                             "that will be computed during the fitting process will have wrong directions if such "
1800
                             "rotation was applied.") + "</li>"
×
UNCOV
1801
                "</ul>";
×
1802
        html += "<p>" + q_("The image that you input into this estimator should contain as much EXIF information from the "
×
1803
                   "original RAW as possible, this will let the UI tell you if e.g. current time and date set in "
UNCOV
1804
                   "the simulation are not the same as specified in the image.") + "</p>";
×
UNCOV
1805
        html += "<p>" + q_("One method that will take an input RAW image and output a result following the above "
×
1806
                           "guidelines (except those about shooting) is to use <code>dcraw</code> (the original one from "
1807
                           "Dave Coffin) or <code>dcraw_emu</code> (from <code>libRaw</code> package), plus "
UNCOV
1808
                           "<code>exiftool</code> as in the following commands (adjust the file names to your needs):") + "</p>"
×
1809
                "<pre>dcraw_emu -t 0 -T -W -Z 20230921_235104-dcraw.tiff 20230921_235104.dng\n"
1810
                     "exiftool -tagsfromfile 20230921_235104.jpg -Time:all -DateTimeOriginal -gps:all -MakerNotes "
1811
                     "-wm cg 20230921_235104-dcraw.tiff</pre>"
UNCOV
1812
                "<p>" + q_("Here the <code>-t 0</code> option for <code>dcraw_emu</code> prevents automatic rotation, "
×
1813
                           "<code>-T</code> generates a TIFF file instead of the default PPM, <code>-W</code> prevents "
1814
                           "automatic brightening which may make stars not as small as they are in the photo, and "
UNCOV
1815
                           "<code>-Z</code> gives an explicit name to the file.") + "</p>"
×
1816
                "<p>" + q_("The <code>exiftool</code> command copies all time-related and GPS tags from the JPEG sidecar "
×
1817
                           "(which on my Samsung Galaxy S8 appeared to contain more complete EXIF info than the DNG) into "
1818
                           "the TIFF. Your case may differ, you may have to copy from your RAW file instead.") + "</p>";
×
1819

1820
        html += "<h3>" + q_("Hot Keys") + "</h3>";
×
1821
        html += "<ul>"
1822
                "<li>" + q_("Ctrl+Shift & left mouse drag: move the image around the sky") + "</li>"
×
1823
                "<li>" + q_("Ctrl+Shift & right mouse drag: scale and rotate the image around the last drag point") + "</li>"
×
UNCOV
1824
                "<li>" + q_("Keyboard hotkeys can be configured in the Keyboard shortcuts editor (F7).") + "</li>"
×
1825
                "</ul>";
×
1826

UNCOV
1827
        html += StelApp::getInstance().getModuleMgr().getStandardSupportLinksInfo("Lens Distortion Estimator plugin");
×
1828
        html += "</body></html>";
×
1829

UNCOV
1830
        ui_->about->setHtml(html);
×
UNCOV
1831
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc