1 /* ExternalSextractorSolver, StellarSolver Internal Library developed by Robert Lancaster, 2020
2
3 This application is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public
5 License as published by the Free Software Foundation; either
6 version 2 of the License, or (at your option) any later version.
7 */
8 #include "externalsextractorsolver.h"
9 #include <QTextStream>
10 #include <QMessageBox>
11 #include <qmath.h>
12 #include <wcshdr.h>
13 #include <wcsfix.h>
14
15 static int solverNum = 1;
16
ExternalSextractorSolver(ProcessType type,ExtractorType sexType,SolverType solType,FITSImage::Statistic imagestats,uint8_t const * imageBuffer,QObject * parent)17 ExternalSextractorSolver::ExternalSextractorSolver(ProcessType type, ExtractorType sexType, SolverType solType,
18 FITSImage::Statistic imagestats, uint8_t const *imageBuffer, QObject *parent) : InternalSextractorSolver(type, sexType,
19 solType, imagestats, imageBuffer, parent)
20 {
21
22 //This sets the base name used for the temp files.
23 m_BaseName = "externalSextractorSolver_" + QString::number(solverNum++);
24
25 //The code below sets default paths for these key external file settings.
26
27 #if defined(Q_OS_OSX)
28 if(QFile("/usr/local/bin/solve-field").exists())
29 setExternalFilePaths(getMacHomebrewPaths());
30 else
31 setExternalFilePaths(getMacInternalPaths());
32 #elif defined(Q_OS_LINUX)
33 setExternalFilePaths(getLinuxDefaultPaths());
34 #else //Windows
35 setExternalFilePaths(getWinANSVRPaths());
36 #endif
37
38 }
39
~ExternalSextractorSolver()40 ExternalSextractorSolver::~ExternalSextractorSolver()
41 {
42 delete xcol;
43 delete ycol;
44 delete magcol;
45 delete colFormat;
46 delete colUnits;
47 delete magUnits;
48 }
49
50 //The following methods are available to get the default paths for different operating systems and configurations.
51
getLinuxDefaultPaths()52 ExternalProgramPaths ExternalSextractorSolver::getLinuxDefaultPaths()
53 {
54 return ExternalProgramPaths
55 {
56 "/etc/astrometry.cfg", //confPath
57 "/usr/bin/sextractor", //sextractorBinaryPath
58 "/usr/bin/solve-field", //solverPath
59 (QFile("/bin/astap").exists()) ?
60 "/bin/astap" : //astapBinaryPath
61 "/opt/astap/astap",
62 "/usr/bin/wcsinfo" //wcsPath
63 };
64 }
65
getLinuxInternalPaths()66 ExternalProgramPaths ExternalSextractorSolver::getLinuxInternalPaths()
67 {
68 return ExternalProgramPaths
69 {
70 "$HOME/.local/share/kstars/astrometry/astrometry.cfg", //confPath
71 "/usr/bin/sextractor", //sextractorBinaryPath
72 "/usr/bin/solve-field", //solverPath
73 (QFile("/bin/astap").exists()) ?
74 "/bin/astap" : //astapBinaryPath
75 "/opt/astap/astap",
76 "/usr/bin/wcsinfo" //wcsPath
77 };
78 }
79
getMacHomebrewPaths()80 ExternalProgramPaths ExternalSextractorSolver::getMacHomebrewPaths()
81 {
82 return ExternalProgramPaths
83 {
84 "/usr/local/etc/astrometry.cfg", //confPath
85 "/usr/local/bin/sex", //sextractorBinaryPath
86 "/usr/local/bin/solve-field", //solverPath
87 "/Applications/ASTAP.app/Contents/MacOS/astap", //astapBinaryPath
88 "/usr/local/bin/wcsinfo" //wcsPath
89 };
90 }
91
getMacInternalPaths()92 ExternalProgramPaths ExternalSextractorSolver::getMacInternalPaths()
93 {
94 return ExternalProgramPaths
95 {
96 "/Applications/KStars.app/Contents/MacOS/astrometry/bin/astrometry.cfg", //confPath
97 "/Applications/KStars.app/Contents/MacOS/astrometry/bin/sex", //sextractorBinaryPath
98 "/Applications/KStars.app/Contents/MacOS/astrometry/bin/solve-field", //solverPath
99 "/Applications/ASTAP.app/Contents/MacOS/astap", //astapBinaryPath
100 "/Applications/KStars.app/Contents/MacOS/astrometry/bin/wcsinfo" //wcsPath
101 };
102 }
103
getWinANSVRPaths()104 ExternalProgramPaths ExternalSextractorSolver::getWinANSVRPaths()
105 {
106 return ExternalProgramPaths
107 {
108 QDir::homePath() + "/AppData/Local/cygwin_ansvr/etc/astrometry/backend.cfg", //confPath
109 "", //sextractorBinaryPath
110 QDir::homePath() + "/AppData/Local/cygwin_ansvr/lib/astrometry/bin/solve-field.exe", //solverPath
111 "C:/Program Files/astap/astap.exe", //astapBinaryPath
112 QDir::homePath() + "/AppData/Local/cygwin_ansvr/lib/astrometry/bin/wcsinfo.exe" //wcsPath
113 };
114 }
115
getWinCygwinPaths()116 ExternalProgramPaths ExternalSextractorSolver::getWinCygwinPaths()
117 {
118 return ExternalProgramPaths
119 {
120 "C:/cygwin64/usr/etc/astrometry.cfg", //confPath
121 "", //sextractorBinaryPath
122 "C:/cygwin64/bin/solve-field", //solverPath
123 "C:/Program Files/astap/astap.exe", //astapBinaryPath
124 "C:/cygwin64/bin/wcsinfo" //wcsPath
125 };
126 }
127
setExternalFilePaths(ExternalProgramPaths paths)128 void ExternalSextractorSolver::setExternalFilePaths(ExternalProgramPaths paths)
129 {
130 confPath = paths.confPath;
131 sextractorBinaryPath = paths.sextractorBinaryPath;
132 solverPath = paths.solverPath;
133 astapBinaryPath = paths.astapBinaryPath;
134 wcsPath = paths.wcsPath;
135 }
136
extract()137 int ExternalSextractorSolver::extract()
138 {
139 if(m_ExtractorType == EXTRACTOR_EXTERNAL)
140 {
141 #ifdef _WIN32 //Note that this is just a warning, if the user has Sextractor installed somehow on Windows, they could use it.
142 emit logOutput("Sextractor is not easily installed on windows. Please select the Internal Sextractor and External Solver.");
143 #endif
144
145 if(!QFileInfo(sextractorBinaryPath).exists())
146 {
147 emit logOutput("There is no sextractor at " + sextractorBinaryPath + ", Aborting");
148 return -1;
149 }
150 }
151
152 if(sextractorFilePath == "")
153 {
154 sextractorFilePathIsTempFile = true;
155 sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
156 }
157
158 if(m_ProcessType == EXTRACT_WITH_HFR || m_ProcessType == EXTRACT)
159 return runExternalSextractor();
160 else
161 {
162 int fail = 0;
163 if(m_ExtractorType == EXTRACTOR_INTERNAL)
164 {
165 fail = runSEPSextractor();
166 if(fail != 0)
167 return fail;
168 return(writeSextractorTable());
169 }
170 else if(m_ExtractorType == EXTRACTOR_EXTERNAL)
171 return(runExternalSextractor());
172 }
173 return -1;
174 }
175
run()176 void ExternalSextractorSolver::run()
177 {
178 if(computingWCS)
179 {
180 if(m_HasSolved)
181 {
182 computeWCSCoord();
183 emit finished(0);
184 }
185 else
186 emit finished(-1);
187 computingWCS = false;
188 return;
189 }
190
191 if(m_AstrometryLogLevel != LOG_NONE && m_LogToFile)
192 {
193 if(m_LogFileName == "")
194 m_LogFileName = m_BasePath + "/" + m_BaseName + ".log.txt";
195 if(QFile(m_LogFileName).exists())
196 QFile(m_LogFileName).remove();
197 }
198
199 if(cancelfn == "")
200 cancelfn = m_BasePath + "/" + m_BaseName + ".cancel";
201 if(solvedfn == "")
202 solvedfn = m_BasePath + "/" + m_BaseName + ".solved";
203 if(solutionFile == "")
204 solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
205
206 QFile solvedFile(solvedfn);
207 solvedFile.setPermissions(solvedFile.permissions() | QFileDevice::WriteOther);
208 solvedFile.remove();
209
210 QFile(cancelfn).remove();
211
212 //These are the solvers that use External Astrometry.
213 if(m_SolverType == SOLVER_LOCALASTROMETRY)
214 {
215 if(!QFileInfo(solverPath).exists())
216 {
217 emit logOutput("There is no astrometry solver at " + solverPath + ", Aborting");
218 emit finished(-1);
219 return;
220 }
221 #ifdef _WIN32
222 if(m_ActiveParameters.inParallel)
223 {
224 emit logOutput("The external ANSVR solver on windows does not handle the inparallel option well, disabling it for this run.");
225 m_ActiveParameters.inParallel = false;
226 }
227 #endif
228 }
229 else if(m_SolverType == SOLVER_ASTAP)
230 {
231 if(!QFileInfo(astapBinaryPath).exists())
232 {
233 emit logOutput("There is no ASTAP solver at " + astapBinaryPath + ", Aborting");
234 emit finished(-1);
235 return;
236 }
237 }
238
239 if(sextractorFilePath == "")
240 {
241 sextractorFilePathIsTempFile = true;
242 sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
243 }
244
245 switch(m_ProcessType)
246 {
247 case EXTRACT:
248 case EXTRACT_WITH_HFR:
249 {
250 int result = extract();
251 cleanupTempFiles();
252 emit finished(result);
253 }
254 break;
255
256 case SOLVE:
257 {
258 if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_LOCALASTROMETRY)
259 {
260 int result = runExternalSolver();
261 cleanupTempFiles();
262 emit finished(result);
263 }
264 else if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_ASTAP)
265 {
266 int result = runExternalASTAPSolver();
267 cleanupTempFiles();
268 emit finished(result);
269 }
270 else
271 {
272 if(!m_HasExtracted)
273 {
274 int fail = extract();
275 if(fail != 0)
276 {
277 cleanupTempFiles();
278 emit finished(fail);
279 return;
280 }
281 if(m_ExtractedStars.size() == 0)
282 {
283 cleanupTempFiles();
284 emit logOutput("No stars were found, so the image cannot be solved");
285 emit finished(-1);
286 return;
287 }
288 }
289
290 if(m_HasExtracted)
291 {
292 if(m_SolverType == SOLVER_ASTAP)
293 {
294 int result = runExternalASTAPSolver();
295 cleanupTempFiles();
296 emit finished(result);
297 }
298 else
299 {
300 int result = runExternalSolver();
301 cleanupTempFiles();
302 emit finished(result);
303 }
304 }
305 else
306 {
307 cleanupTempFiles();
308 emit finished(-1);
309 }
310 }
311
312 }
313 break;
314
315 default:
316 break;
317 }
318 }
319
320 //This method generates child solvers with the options of the current solver
spawnChildSolver(int n)321 SextractorSolver* ExternalSextractorSolver::spawnChildSolver(int n)
322 {
323 ExternalSextractorSolver *solver = new ExternalSextractorSolver(m_ProcessType, m_ExtractorType, m_SolverType, m_Statistics,
324 m_ImageBuffer, nullptr);
325 solver->m_ExtractedStars = m_ExtractedStars;
326 solver->m_BasePath = m_BasePath;
327 solver->m_BaseName = m_BaseName + "_" + QString::number(n);
328 solver->m_HasExtracted = true;
329 solver->sextractorFilePath = sextractorFilePath;
330 solver->sextractorFilePathIsTempFile = sextractorFilePathIsTempFile;
331 solver->fileToProcess = fileToProcess;
332 solver->sextractorBinaryPath = sextractorBinaryPath;
333 solver->confPath = confPath;
334 solver->solverPath = solverPath;
335 solver->astapBinaryPath = astapBinaryPath;
336 solver->wcsPath = wcsPath;
337 solver->cleanupTemporaryFiles = cleanupTemporaryFiles;
338 solver->autoGenerateAstroConfig = autoGenerateAstroConfig;
339 solver->onlySendFITSFiles = onlySendFITSFiles;
340
341 solver->isChildSolver = true;
342 solver->m_ActiveParameters = m_ActiveParameters;
343 solver->indexFolderPaths = indexFolderPaths;
344 //Set the log level one less than the main solver
345 if(m_SSLogLevel == LOG_VERBOSE )
346 solver->m_SSLogLevel = LOG_NORMAL;
347 if(m_SSLogLevel == LOG_NORMAL || m_SSLogLevel == LOG_OFF)
348 solver->m_SSLogLevel = LOG_OFF;
349 if(m_UseScale)
350 solver->setSearchScale(scalelo, scalehi, scaleunit);
351 if(m_UsePosition)
352 solver->setSearchPositionInDegrees(search_ra, search_dec);
353 if(m_AstrometryLogLevel != SSolver::LOG_NONE || m_SSLogLevel != SSolver::LOG_OFF)
354 connect(solver, &SextractorSolver::logOutput, this, &SextractorSolver::logOutput);
355 //This way they all share a solved and cancel fn
356 solver->solutionFile = solutionFile;
357 //solver->cancelfn = cancelfn;
358 //solver->solvedfn = basePath + "/" + baseName + ".solved";
359 return solver;
360 }
361
362 //This is the abort method. For the external sextractor and solver, it uses the kill method to abort the processes
abort()363 void ExternalSextractorSolver::abort()
364 {
365 QFile file(cancelfn);
366 if(QFileInfo(file).dir().exists())
367 {
368 file.open(QIODevice::WriteOnly);
369 file.write("Cancel");
370 file.close();
371 }
372 if(!isChildSolver)
373 emit logOutput("Aborting ...");
374 m_WasAborted = true;
375 }
376
cleanupTempFiles()377 void ExternalSextractorSolver::cleanupTempFiles()
378 {
379 if(cleanupTemporaryFiles)
380 {
381 QDir temp(m_BasePath);
382 //Sextractor Files
383 temp.remove(m_BaseName + ".param");
384 temp.remove(m_BaseName + ".conv");
385 temp.remove(m_BaseName + ".cfg");
386
387 //ASTAP files
388 temp.remove(m_BaseName + ".ini");
389
390 //Astrometry Files
391 temp.remove(m_BaseName + ".corr");
392 temp.remove(m_BaseName + ".rdls");
393 temp.remove(m_BaseName + ".axy");
394 temp.remove(m_BaseName + ".corr");
395 temp.remove(m_BaseName + ".new");
396 temp.remove(m_BaseName + ".match");
397 temp.remove(m_BaseName + "-indx.xyls");
398 temp.remove(m_BaseName + ".solved");
399
400 //Other Files
401 QFile solvedFile(solvedfn);
402 solvedFile.setPermissions(solvedFile.permissions() | QFileDevice::WriteOther);
403 solvedFile.remove();
404 QFile(solutionFile).remove();
405 QFile(cancelfn).remove();
406 if(sextractorFilePathIsTempFile)
407 QFile(sextractorFilePath).remove();
408 if(fileToProcessIsTempFile)
409 QFile(fileToProcess).remove();
410 }
411 }
412
413 //This method is copied and pasted and modified from the code I wrote to use sextractor in OfflineAstrometryParser in KStars
414 //It creates key files needed to run Sextractor from the desired options, then runs the sextractor program using the options.
runExternalSextractor()415 int ExternalSextractorSolver::runExternalSextractor()
416 {
417 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
418 emit logOutput("Configuring external Sextractor");
419 QFileInfo file(fileToProcess);
420 if(!file.exists())
421 return -1;
422 if(file.suffix() != "fits" && file.suffix() != "fit")
423 {
424 int ret = saveAsFITS();
425 if(ret != 0)
426 return ret;
427 }
428 else
429 {
430 QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
431 QFile::copy(fileToProcess, newFileURL);
432 fileToProcess = newFileURL;
433 fileToProcessIsTempFile = true;
434 }
435
436 //Configuration arguments for sextractor
437 QStringList sextractorArgs;
438 //This one is not really that necessary, it will use the defaults if it can't find it
439 //We will set all of the things we need in the parameters below
440 //sextractorArgs << "-c" << "/usr/local/share/sextractor/default.sex";
441
442 sextractorArgs << "-CATALOG_NAME" << sextractorFilePath;
443 sextractorArgs << "-CATALOG_TYPE" << "FITS_1.0";
444
445 //sextractor needs a default.param file in the working directory
446 //This creates that file with the options we need for astrometry.net and sextractor
447
448 QString paramPath = m_BasePath + "/" + m_BaseName + ".param";
449 QFile paramFile(paramPath);
450 if (paramFile.open(QIODevice::WriteOnly) == false)
451 {
452 QMessageBox::critical(nullptr, "Message", "Sextractor file write error.");
453 return -1;
454 }
455 else
456 {
457 QTextStream out(¶mFile);
458 out << "X_IMAGE\n";// Object position along x [pixel]
459 out << "Y_IMAGE\n";// Object position along y [pixel]
460 out << "MAG_AUTO\n";// Kron-like elliptical aperture magnitude [mag]
461 out << "FLUX_AUTO\n";// Flux within a Kron-like elliptical aperture [count]
462 out << "FLUX_MAX\n";// Peak flux above background [count]
463 out << "CXX_IMAGE\n";// Cxx object ellipse parameter [pixel**(-2)]
464 out << "CYY_IMAGE\n";// Cyy object ellipse parameter [pixel**(-2)]
465 out << "CXY_IMAGE\n";// Cxy object ellipse parameter [pixel**(-2)]
466 if(m_ProcessType == EXTRACT_WITH_HFR)
467 out << "FLUX_RADIUS\n";// Fraction-of-light radii [pixel]
468 paramFile.close();
469 }
470 sextractorArgs << "-PARAMETERS_NAME" << paramPath;
471
472
473 //sextractor needs a default.conv file in the working directory
474 //This creates the default one
475
476 QString convPath = m_BasePath + "/" + m_BaseName + ".conv";
477 QFile convFile(convPath);
478 if (convFile.open(QIODevice::WriteOnly) == false)
479 {
480 QMessageBox::critical(nullptr, "Message", "Sextractor CONV filter write error.");
481 return -1;
482 }
483 else
484 {
485 QTextStream out(&convFile);
486 out << "CONV Filter Generated by StellarSolver Internal Library\n";
487 int c = 0;
488 for(int i = 0; i < m_ActiveParameters.convFilter.size(); i++)
489 {
490 out << m_ActiveParameters.convFilter.at(i);
491
492 //We want the last one before the sqrt of the size to spark a new line
493 if(c < sqrt(m_ActiveParameters.convFilter.size()) - 1)
494 {
495 out << " ";
496 c++;
497 }
498 else
499 {
500 out << "\n";
501 c = 0;
502 }
503 }
504 convFile.close();
505 }
506
507 //Arguments from the default.sex file
508 //------------------------------- Extraction ----------------------------------
509 sextractorArgs << "-DETECT_TYPE" << "CCD";
510 sextractorArgs << "-DETECT_MINAREA" << QString::number(m_ActiveParameters.minarea);
511
512 //sextractorArgs << "-DETECT_THRESH" << QString::number();
513 //sextractorArgs << "-ANALYSIS_THRESH" << QString::number(minarea);
514
515 sextractorArgs << "-FILTER" << "Y";
516 sextractorArgs << "-FILTER_NAME" << convPath;
517
518 sextractorArgs << "-DEBLEND_NTHRESH" << QString::number(m_ActiveParameters.deblend_thresh);
519 sextractorArgs << "-DEBLEND_MINCONT" << QString::number(m_ActiveParameters.deblend_contrast);
520
521 sextractorArgs << "-CLEAN" << ((m_ActiveParameters.clean == 1) ? "Y" : "N");
522 sextractorArgs << "-CLEAN_PARAM" << QString::number(m_ActiveParameters.clean_param);
523
524 //------------------------------ Photometry -----------------------------------
525 sextractorArgs << "-PHOT_AUTOPARAMS" << QString::number(m_ActiveParameters.kron_fact) + "," + QString::number(
526 m_ActiveParameters.r_min);
527 sextractorArgs << "-MAG_ZEROPOINT" << QString::number(m_ActiveParameters.magzero);
528
529 sextractorArgs << fileToProcess;
530
531 sextractorProcess.clear();
532 sextractorProcess = new QProcess();
533
534 sextractorProcess->setWorkingDirectory(m_BasePath);
535 sextractorProcess->setProcessChannelMode(QProcess::MergedChannels);
536 if(m_SSLogLevel != LOG_OFF)
537 connect(sextractorProcess, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSextractor);
538
539 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
540 emit logOutput("Starting external sextractor with the " + m_ActiveParameters.listName + " profile...");
541 emit logOutput(sextractorBinaryPath + " " + sextractorArgs.join(' '));
542
543 sextractorProcess->start(sextractorBinaryPath, sextractorArgs);
544 sextractorProcess->waitForFinished(30000); //Will timeout after 30 seconds
545 emit logOutput(sextractorProcess->readAllStandardError().trimmed());
546
547 if(sextractorProcess->exitCode() != 0 || sextractorProcess->exitStatus() == QProcess::CrashExit)
548 return sextractorProcess->exitCode();
549
550 int exitCode = getStarsFromXYLSFile();
551 if(exitCode != 0)
552 return exitCode;
553
554 if(m_UseSubframe)
555 {
556 for(int i = 0; i < m_ExtractedStars.size(); i++)
557 {
558 FITSImage::Star star = m_ExtractedStars.at(i);
559 if(!m_SubFrameRect.contains(star.x, star.y))
560 {
561 m_ExtractedStars.removeAt(i);
562 i--;
563 }
564 }
565 }
566
567 applyStarFilters(m_ExtractedStars);
568
569 m_HasExtracted = true;
570
571 return 0;
572
573 }
574
575 //The code for this method is copied and pasted and modified from OfflineAstrometryParser in KStars
576 //It runs the astrometry.net external program using the options selected.
runExternalSolver()577 int ExternalSextractorSolver::runExternalSolver()
578 {
579 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
580 if(m_ExtractorType == EXTRACTOR_BUILTIN)
581 emit logOutput("Configuring external Astrometry.net solver classically: using python and without Sextractor first");
582 else
583 emit logOutput("Configuring external Astrometry.net solver using an xylist input");
584
585 if(m_ExtractorType == EXTRACTOR_BUILTIN)
586 {
587 QFileInfo file(fileToProcess);
588 if(!file.exists())
589 {
590 emit logOutput("The requested file to solve does not exist");
591 return -1;
592 }
593
594 if( !isChildSolver && onlySendFITSFiles && file.suffix() != "fits" && file.suffix() != "fit")
595 {
596 int ret = saveAsFITS();
597 if(ret != 0)
598 {
599 emit logOutput("Failed to Save the image as a FITS File.");
600 return ret;
601 }
602 }
603 else
604 {
605 //Making a copy of the file and putting it in the temp directory
606 //So that we can find all the temporary files and delete them later
607 //That way we don't pollute the directory the original image is located in
608 QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
609 QFile::copy(fileToProcess, newFileURL);
610 fileToProcess = newFileURL;
611 fileToProcessIsTempFile = true;
612 }
613 }
614 else
615 {
616 QFileInfo sextractorFile(sextractorFilePath);
617 if(!sextractorFile.exists())
618 {
619 emit logOutput("Please Sextract the image first");
620 }
621 if(isChildSolver)
622 {
623 QString newFileURL = m_BasePath + "/" + m_BaseName + "." + sextractorFile.suffix();
624 QFile::copy(sextractorFilePath, newFileURL);
625 sextractorFilePath = newFileURL;
626 sextractorFilePathIsTempFile = true;
627 }
628 }
629
630 QStringList solverArgs = getSolverArgsList();
631
632 if(m_ExtractorType == EXTRACTOR_BUILTIN)
633 {
634 solverArgs << "--keep-xylist" << sextractorFilePath;
635 solverArgs << fileToProcess;
636 }
637 else
638 solverArgs << sextractorFilePath;
639
640 solver.clear();
641 solver = new QProcess();
642
643 solver->setProcessChannelMode(QProcess::MergedChannels);
644 if(m_AstrometryLogLevel != LOG_NONE)
645 connect(solver, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSolver);
646
647 #ifdef _WIN32 //This will set up the environment so that the ANSVR internal solver will work when started from this program. This is needed for all types of astrometry solvers using ANSVR
648 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
649 QString path = env.value("Path", "");
650 QString ansvrPath = QDir::homePath() + "/AppData/Local/cygwin_ansvr/";
651 QString pathsToInsert = ansvrPath + "bin;";
652 pathsToInsert += ansvrPath + "lib/lapack;";
653 pathsToInsert += ansvrPath + "lib/astrometry/bin;";
654 env.insert("Path", pathsToInsert + path);
655 solver->setProcessEnvironment(env);
656 #endif
657
658 #ifdef Q_OS_OSX //This is needed so that astrometry.net can find netpbm and python on Mac when started from this program. It is not needed when using an alternate sextractor
659 if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_LOCALASTROMETRY)
660 {
661 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
662 QString path = env.value("PATH", "");
663 QString pythonExecPath = "/usr/local/opt/python/libexec/bin";
664 env.insert("PATH", "/Applications/KStars.app/Contents/MacOS/netpbm/bin:" + pythonExecPath + ":/usr/local/bin:" + path);
665 solver->setProcessEnvironment(env);
666 }
667 #endif
668
669 solver->start(solverPath, solverArgs);
670 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
671 emit logOutput("Starting external Astrometry.net solver with the " + m_ActiveParameters.listName + " profile...");
672 emit logOutput("Command: " + solverPath + " " + solverArgs.join(" "));
673
674 solver->waitForFinished(m_ActiveParameters.solverTimeLimit * 1000 *
675 1.2); //Set to timeout in a little longer than the timeout
676 if(solver->error() == QProcess::Timedout)
677 {
678 emit logOutput("Solver timed out, aborting");
679 abort();
680 return solver->exitCode();
681 }
682 if(solver->exitCode() != 0)
683 return solver->exitCode();
684 if(solver->exitStatus() == QProcess::CrashExit)
685 return -1;
686 if(m_WasAborted)
687 return -1;
688 if(!getSolutionInformation())
689 return -1;
690 loadWCS(); //Attempt to Load WCS, but don't totally fail if you don't find it.
691 m_HasSolved = true;
692 return 0;
693 }
694
695 //The code for this method is copied and pasted and modified from OfflineAstrometryParser in KStars
696 //It runs the astrometry.net external program using the options selected.
runExternalASTAPSolver()697 int ExternalSextractorSolver::runExternalASTAPSolver()
698 {
699 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
700 emit logOutput("Configuring external ASTAP solver");
701
702 if(m_ExtractorType != EXTRACTOR_BUILTIN)
703 {
704 QFileInfo file(fileToProcess);
705 if(!file.exists())
706 return -1;
707
708 QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
709 QFile::copy(fileToProcess, newFileURL);
710 fileToProcess = newFileURL;
711 fileToProcessIsTempFile = true;
712 }
713
714 QStringList solverArgs;
715
716 QString astapSolutionFile = m_BasePath + "/" + m_BaseName + ".ini";
717 solverArgs << "-o" << astapSolutionFile;
718 solverArgs << "-speed" << "auto";
719 solverArgs << "-f" << fileToProcess;
720 solverArgs << "-wcs";
721 if(m_ActiveParameters.downsample > 1)
722 solverArgs << "-z" << QString::number(m_ActiveParameters.downsample);
723 else
724 solverArgs << "-z" << "0";
725 if(m_UseScale)
726 {
727 double scalemid = (scalehi + scalelo) / 2;
728 double degreesFOV = convertToDegreeHeight(scalemid);
729 solverArgs << "-fov" << QString::number(degreesFOV);
730 }
731 if(m_UsePosition)
732 {
733 solverArgs << "-ra" << QString::number(search_ra / 15.0); //Convert ra to hours
734 solverArgs << "-spd" << QString::number(search_dec + 90); //Convert dec to spd
735 solverArgs << "-r" << QString::number(m_ActiveParameters.search_radius);
736 }
737 if(m_AstrometryLogLevel != LOG_NONE)
738 solverArgs << "-log";
739
740 solver.clear();
741 solver = new QProcess();
742
743 solver->setProcessChannelMode(QProcess::MergedChannels);
744 if(m_AstrometryLogLevel != LOG_NONE)
745 connect(solver, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSolver);
746
747 solver->start(astapBinaryPath, solverArgs);
748
749 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
750 emit logOutput("Starting external ASTAP Solver with the " + m_ActiveParameters.listName + " profile...");
751 emit logOutput("Command: " + astapBinaryPath + " " + solverArgs.join(" "));
752
753 solver->waitForFinished(m_ActiveParameters.solverTimeLimit * 1000 *
754 1.2); //Set to timeout in a little longer than the timeout
755
756 if(m_AstrometryLogLevel != LOG_NONE)
757 {
758 QFile logFile(m_BasePath + "/" + m_BaseName + ".log");
759 if(logFile.exists())
760 {
761 if(m_LogToFile)
762 {
763 if(m_LogFileName != "")
764 logFile.copy(m_LogFileName);
765 }
766 else
767 {
768 if (logFile.open(QIODevice::ReadOnly))
769 {
770 QTextStream in(&logFile);
771 emit logOutput(in.readAll());
772 }
773 else
774 emit logOutput("Failed to open ASTAP log file" + logFile.fileName());
775 }
776 }
777 else
778 emit logOutput("ASTAP log file " + logFile.fileName() + " does not exist.");
779 }
780
781 if(solver->error() == QProcess::Timedout)
782 {
783 emit logOutput("Solver timed out, aborting");
784 abort();
785 return solver->exitCode();
786 }
787 if(solver->exitCode() != 0)
788 return solver->exitCode();
789 if(solver->exitStatus() == QProcess::CrashExit)
790 return -1;
791 if(!getASTAPSolutionInformation())
792 return -1;
793 loadWCS(); //Attempt to Load WCS, but don't totally fail if you don't find it.
794 m_HasSolved = true;
795 return 0;
796 }
797
798 //This method is copied and pasted and modified from getSolverOptionsFromFITS in Align in KStars
799 //Then it was split in two parts (The other part is located in the MainWindow class)
800 //This part generates the argument list from the options for the external solver only
getSolverArgsList()801 QStringList ExternalSextractorSolver::getSolverArgsList()
802 {
803 QStringList solverArgs;
804
805 // Start with always-used arguments. The way we are using Astrometry.net,
806 //We really would prefer that it always overwrite existing files, that it not waste any time
807 //writing plots to a file, and that it doesn't run the verification.
808 //We would also like the Coordinates found to be the center of the image
809 solverArgs << "-O" << "--no-plots" << "--no-verify" << "--crpix-center";
810
811 //We would also prefer that it not write these temporary files, because they are a waste of
812 //resources, time, hard disk space, and memory card life since we aren't using them.
813 solverArgs << "--match" << "none";
814 solverArgs << "--corr" << "none";
815 solverArgs << "--new-fits" << "none";
816 solverArgs << "--rdls" << "none";
817
818 //This parameter controls whether to resort the stars or not.
819 if(m_ActiveParameters.resort)
820 solverArgs << "--resort";
821
822 if(depthhi != -1 && depthlo != -1)
823 solverArgs << "--depth" << QString("%1-%2").arg(depthlo).arg(depthhi);
824
825 if(m_ActiveParameters.keepNum != 0)
826 solverArgs << "--objs" << QString("%1").arg(m_ActiveParameters.keepNum);
827
828 //This will shrink the image so that it is easier to solve. It is only useful if you are sending an image.
829 //It is not used if you are solving an xylist as in the classic astrometry.net solver
830 if(m_ActiveParameters.downsample > 1 && m_ExtractorType == EXTRACTOR_BUILTIN)
831 solverArgs << "--downsample" << QString::number(m_ActiveParameters.downsample);
832
833 //I am not sure if we want to provide these options or not. They do make a huge difference in solving time
834 //But changing them can be dangerous because it can cause false positive solves.
835 solverArgs << "--odds-to-solve" << QString::number(exp(m_ActiveParameters.logratio_tosolve));
836 solverArgs << "--odds-to-tune-up" << QString::number(exp(m_ActiveParameters.logratio_totune));
837 //solverArgs << "--odds-to-keep" << QString::number(logratio_tokeep); I'm not sure if this is one we need.
838
839 if (m_UseScale)
840 solverArgs << "-L" << QString::number(scalelo) << "-H" << QString::number(scalehi) << "-u" << getScaleUnitString();
841
842 if (m_UsePosition)
843 solverArgs << "-3" << QString::number(search_ra) << "-4" << QString::number(search_dec) << "-5" << QString::number(
844 m_ActiveParameters.search_radius);
845
846 //The following options are unnecessary if you are sending an image to Astrometry.net
847 //And not an xylist
848 if(m_ExtractorType != EXTRACTOR_BUILTIN)
849 {
850 //These options are required to use an xylist, so all solvers that don't send an image
851 //to Astrometry.net must send these 4 options.
852 solverArgs << "--width" << QString::number(m_Statistics.width);
853 solverArgs << "--height" << QString::number(m_Statistics.height);
854 solverArgs << "--x-column" << "X_IMAGE";
855 solverArgs << "--y-column" << "Y_IMAGE";
856
857 //These sort options are required to sort when you have an xylist input
858 //We only want to send them if the resort option is selected though.
859 if(m_ActiveParameters.resort)
860 {
861 solverArgs << "--sort-column" << "MAG_AUTO";
862 solverArgs << "--sort-ascending";
863 }
864 }
865
866 //Note This set of items is NOT NEEDED for Sextractor or for astrometry.net to solve, it is needed to avoid python usage.
867 //On many user's systems especially on Mac and sometimes on Windows, there is an issue in the Python setup that causes astrometry to fail.
868 //This should avoid those problems as long as you send a FITS file or a xy list to astrometry.
869 solverArgs << "--no-remove-lines";
870 solverArgs << "--uniformize" << "0";
871 if(onlySendFITSFiles && m_ExtractorType == EXTRACTOR_BUILTIN)
872 solverArgs << "--fits-image";
873
874 //Don't need any argument for default level
875 if(m_AstrometryLogLevel == LOG_MSG || m_AstrometryLogLevel == LOG_ERROR)
876 solverArgs << "-v";
877 else if(m_AstrometryLogLevel == LOG_VERB || m_AstrometryLogLevel == LOG_ALL)
878 solverArgs << "-vv";
879
880 if(autoGenerateAstroConfig || !QFile(confPath).exists())
881 generateAstrometryConfigFile();
882
883 //This sends the path to the config file. Note that backend-config seems to be more universally recognized across
884 //the different solvers than config
885 solverArgs << "--backend-config" << confPath;
886
887 //This sets the cancel filename for astrometry.net. Astrometry will monitor for the creation of this file
888 //In order to shut down and stop processing
889 solverArgs << "--cancel" << cancelfn;
890
891 //This sets the wcs file for astrometry.net. This file will be very important for reading in WCS info later on
892 solverArgs << "-W" << solutionFile;
893
894 return solverArgs;
895 }
896
897 //This will generate a temporary Astrometry.cfg file to use for solving so that we have more control over these options
898 //for the external solvers from inside the program.
generateAstrometryConfigFile()899 bool ExternalSextractorSolver::generateAstrometryConfigFile()
900 {
901 confPath = m_BasePath + "/" + m_BaseName + ".cfg";
902 QFile configFile(confPath);
903 if (configFile.open(QIODevice::WriteOnly) == false)
904 {
905 QMessageBox::critical(nullptr, "Message", "Config file write error.");
906 return false;
907 }
908 else
909 {
910 QTextStream out(&configFile);
911 if(m_ActiveParameters.inParallel)
912 out << "inparallel\n";
913 out << "minwidth " << m_ActiveParameters.minwidth << "\n";
914 out << "maxwidth " << m_ActiveParameters.maxwidth << "\n";
915 out << "cpulimit " << m_ActiveParameters.solverTimeLimit << "\n";
916 out << "autoindex\n";
917 foreach(QString folder, indexFolderPaths)
918 {
919 out << "add_path " << folder << "\n";
920 }
921 configFile.close();
922 }
923 return true;
924 }
925
926
927 //These methods are for the logging of information to the textfield at the bottom of the window.
928
logSextractor()929 void ExternalSextractorSolver::logSextractor()
930 {
931 if(sextractorProcess->canReadLine())
932 {
933 QString rawText(sextractorProcess->readLine().trimmed());
934 QString cleanedString = rawText.remove("[1M>").remove("[1A");
935 if(!cleanedString.isEmpty())
936 {
937 emit logOutput(cleanedString);
938 if(m_LogToFile)
939 {
940 QFile file(m_LogFileName);
941 if (file.open(QIODevice::Append | QIODevice::Text))
942 {
943 QTextStream outstream(&file);
944 outstream << cleanedString << endl;
945 file.close();
946 }
947 else
948 emit logOutput(("Log File Write Error"));
949 }
950 }
951 }
952 }
953
logSolver()954 void ExternalSextractorSolver::logSolver()
955 {
956 if(solver->canReadLine())
957 {
958 QString solverLine(solver->readLine().trimmed());
959 if(!solverLine.isEmpty())
960 {
961 emit logOutput(solverLine);
962 if(m_LogToFile)
963 {
964 QFile file(m_LogFileName);
965 if (file.open(QIODevice::Append | QIODevice::Text))
966 {
967 QTextStream outstream(&file);
968 outstream << solverLine << endl;
969 file.close();
970 }
971 else
972 emit logOutput(("Log File Write Error"));
973 }
974 }
975 }
976 }
977
978 //This method is copied and pasted and modified from tablist.c in astrometry.net
979 //This is needed to load in the stars sextracted by an extrnal sextractor to get them into the table
getStarsFromXYLSFile()980 int ExternalSextractorSolver::getStarsFromXYLSFile()
981 {
982 QFile sextractorFile(sextractorFilePath);
983 if(!sextractorFile.exists())
984 {
985 emit logOutput("Can't get sextractor file since it doesn't exist.");
986 return -1;
987 }
988
989 fitsfile * new_fptr;
990 char error_status[512];
991
992 /* FITS file pointer, defined in fitsio.h */
993 char *val, value[1000], nullptrstr[] = "*";
994 int status = 0; /* CFITSIO status value MUST be initialized to zero! */
995 int hdunum, hdutype = ANY_HDU, ncols, ii, anynul;
996 long nelements[1000];
997 long jj, nrows, kk;
998
999 if (fits_open_diskfile(&new_fptr, sextractorFilePath.toLatin1(), READONLY, &status))
1000 {
1001 fits_report_error(stderr, status);
1002 fits_get_errstatus(status, error_status);
1003 emit logOutput(QString::fromUtf8(error_status));
1004 return status;
1005 }
1006
1007 if ( fits_get_hdu_num(new_fptr, &hdunum) == 1 )
1008 /* This is the primary array; try to move to the */
1009 /* first extension and see if it is a table */
1010 fits_movabs_hdu(new_fptr, 2, &hdutype, &status);
1011 else
1012 fits_get_hdu_type(new_fptr, &hdutype, &status); /* Get the HDU type */
1013
1014 if (!(hdutype == ASCII_TBL || hdutype == BINARY_TBL))
1015 {
1016 emit logOutput("Wrong type of file");
1017 return -1;
1018 }
1019
1020 fits_get_num_rows(new_fptr, &nrows, &status);
1021 fits_get_num_cols(new_fptr, &ncols, &status);
1022
1023 for (jj = 1; jj <= ncols; jj++)
1024 fits_get_coltype(new_fptr, jj, nullptr, &nelements[jj], nullptr, &status);
1025
1026 m_ExtractedStars.clear();
1027
1028 /* read each column, row by row */
1029 val = value;
1030 for (jj = 1; jj <= nrows && !status; jj++)
1031 {
1032 float starx = 0;
1033 float stary = 0;
1034 float mag = 0;
1035 float flux = 0;
1036 float peak = 0;
1037 float xx = 0;
1038 float yy = 0;
1039 float xy = 0;
1040 float HFR = 0;
1041
1042 for (ii = 1; ii <= ncols; ii++)
1043 {
1044 for (kk = 1; kk <= nelements[ii]; kk++)
1045 {
1046 /* read value as a string, regardless of intrinsic datatype */
1047 if (fits_read_col_str (new_fptr, ii, jj, kk, 1, nullptrstr,
1048 &val, &anynul, &status) )
1049 break; /* jump out of loop on error */
1050 if(m_SolverType == SOLVER_LOCALASTROMETRY || m_SolverType == SOLVER_ONLINEASTROMETRY)
1051 {
1052 if(ii == 1)
1053 starx = QString(value).trimmed().toFloat();
1054 if(ii == 2)
1055 stary = QString(value).trimmed().toFloat();
1056 if(ii == 3)
1057 flux = QString(value).trimmed().toFloat();
1058 }
1059 else if(m_SolverType == SOLVER_ASTAP)
1060 {
1061 if(ii == 1)
1062 starx = QString(value).trimmed().toFloat();
1063 if(ii == 2)
1064 stary = QString(value).trimmed().toFloat();
1065 }
1066 else
1067 {
1068 if(ii == 1)
1069 starx = QString(value).trimmed().toFloat();
1070 if(ii == 2)
1071 stary = QString(value).trimmed().toFloat();
1072 if(ii == 3)
1073 mag = QString(value).trimmed().toFloat();
1074 if(ii == 4)
1075 flux = QString(value).trimmed().toFloat();
1076 if(ii == 5)
1077 peak = QString(value).trimmed().toFloat();
1078 if(ii == 6)
1079 xx = QString(value).trimmed().toFloat();
1080 if(ii == 7)
1081 yy = QString(value).trimmed().toFloat();
1082 if(ii == 8)
1083 xy = QString(value).trimmed().toFloat();
1084 if(m_ProcessType == EXTRACT_WITH_HFR && ii == 9)
1085 HFR = QString(value).trimmed().toFloat();
1086 }
1087 }
1088 }
1089
1090 // xx xy or a b
1091 // xy yy b c
1092 //Note, I got this translation from these two sources which agree:
1093 //https://books.google.com/books?id=JNEn23UyHuAC&pg=PA84&lpg=PA84&dq=ellipse+xx+yy+xy&source=bl&ots=ynAWge4jlb&sig=ACfU3U1pqZTkx8Teu9pBTygI9F-WcTncrg&hl=en&sa=X&ved=2ahUKEwj0s-7C3I7oAhXblnIEHacAAf0Q6AEwBHoECAUQAQ#v=onepage&q=ellipse%20xx%20yy%20xy&f=false
1094 //https://cookierobotics.com/007/
1095 float a = 0;
1096 float b = 0;
1097 float theta = 0;
1098 if(m_SolverType != SOLVER_LOCALASTROMETRY && m_SolverType != SOLVER_ONLINEASTROMETRY && m_SolverType != SOLVER_ASTAP)
1099 {
1100 float thing = sqrt( pow(xx - yy, 2) + 4 * pow(xy, 2) );
1101 float lambda1 = (xx + yy + thing) / 2;
1102 float lambda2 = (xx + yy - thing) / 2;
1103 a = sqrt(lambda1);
1104 b = sqrt(lambda2);
1105 theta = qRadiansToDegrees(atan(xy / (lambda1 - yy)));
1106 }
1107
1108 FITSImage::Star star = {starx, stary, mag, flux, peak, HFR, a, b, theta, 0, 0};
1109
1110 m_ExtractedStars.append(star);
1111 }
1112 fits_close_file(new_fptr, &status);
1113
1114 if (status) fits_report_error(stderr, status); /* print any error message */
1115
1116 return 0;
1117 }
1118
1119 //This method was based on a method in KStars.
1120 //It reads the information from the Solution file from Astrometry.net and puts it into the solution
getSolutionInformation()1121 bool ExternalSextractorSolver::getSolutionInformation()
1122 {
1123 if(solutionFile == "")
1124 solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
1125 QFileInfo solutionInfo(solutionFile);
1126 if(!solutionInfo.exists())
1127 {
1128 emit logOutput("Solution file doesn't exist");
1129 return false;
1130 }
1131 QProcess wcsProcess;
1132
1133 #ifdef _WIN32 //This will set up the environment so that the ANSVR internal wcsinfo will work
1134 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
1135 QString path = env.value("Path", "");
1136 QString ansvrPath = QDir::homePath() + "/AppData/Local/cygwin_ansvr/";
1137 QString pathsToInsert = ansvrPath + "bin;";
1138 pathsToInsert += ansvrPath + "lib/lapack;";
1139 pathsToInsert += ansvrPath + "lib/astrometry/bin;";
1140 env.insert("Path", pathsToInsert + path);
1141 wcsProcess.setProcessEnvironment(env);
1142 #endif
1143
1144 wcsProcess.start(wcsPath, QStringList(solutionFile));
1145 wcsProcess.waitForFinished(30000); //Will timeout after 30 seconds
1146 QString wcsinfo_stdout = wcsProcess.readAllStandardOutput();
1147
1148 //This is a quick way to find out what keys are available
1149 // emit logOutput(wcsinfo_stdout);
1150
1151 QStringList wcskeys = wcsinfo_stdout.split(QRegExp("[\n]"));
1152
1153 QStringList key_value;
1154
1155 double ra = 0, dec = 0, orient = 0;
1156 double fieldw = 0, fieldh = 0, pixscale = 0;
1157 QString rastr, decstr;
1158 QString parity;
1159
1160 for (auto &key : wcskeys)
1161 {
1162 key_value = key.split(' ');
1163
1164 if (key_value.size() > 1)
1165 {
1166 if (key_value[0] == "ra_center")
1167 ra = key_value[1].toDouble();
1168 else if (key_value[0] == "dec_center")
1169 dec = key_value[1].toDouble();
1170 else if (key_value[0] == "orientation_center")
1171 orient = key_value[1].toDouble();
1172 else if (key_value[0] == "fieldw")
1173 fieldw = key_value[1].toDouble();
1174 else if (key_value[0] == "fieldh")
1175 fieldh = key_value[1].toDouble();
1176 else if (key_value[0] == "ra_center_hms")
1177 rastr = key_value[1];
1178 else if (key_value[0] == "dec_center_dms")
1179 decstr = key_value[1];
1180 else if (key_value[0] == "pixscale")
1181 pixscale = key_value[1].toDouble();
1182 else if (key_value[0] == "parity")
1183 parity = (key_value[1].toInt() == -1) ? "pos" : "neg";
1184 }
1185 }
1186
1187 if(usingDownsampledImage)
1188 pixscale /= m_ActiveParameters.downsample;
1189
1190 double raErr = 0;
1191 double decErr = 0;
1192 if(m_UsePosition)
1193 {
1194 raErr = (search_ra - ra) * 3600;
1195 decErr = (search_dec - dec) * 3600;
1196 }
1197
1198 m_Solution = {fieldw, fieldh, ra, dec, orient, pixscale, parity, raErr, decErr};
1199
1200 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
1201 emit logOutput(QString("Field center: (RA,Dec) = (%1, %2) deg.").arg( ra).arg( dec));
1202 emit logOutput(QString("Field center: (RA H:M:S, Dec D:M:S) = (%1, %2).").arg( rastr).arg( decstr));
1203 if(m_UsePosition)
1204 emit logOutput(QString("Field is: (%1, %2) deg from search coords.").arg( raErr).arg( decErr));
1205 emit logOutput(QString("Field size: %1 x %2 arcminutes").arg( fieldw).arg( fieldh));
1206 emit logOutput(QString("Pixel Scale: %1\"").arg( pixscale ));
1207 emit logOutput(QString("Field rotation angle: up is %1 degrees E of N").arg( orient));
1208 emit logOutput(QString("Field parity: %1\n").arg( parity));
1209
1210 return true;
1211 }
1212
1213 //This method was based on a method in KStars.
1214 //It reads the information from the Solution file from Astrometry.net and puts it into the solution
getASTAPSolutionInformation()1215 bool ExternalSextractorSolver::getASTAPSolutionInformation()
1216 {
1217 QFile results(m_BasePath + "/" + m_BaseName + ".ini");
1218
1219 if (!results.open(QIODevice::ReadOnly))
1220 {
1221 emit logOutput("Failed to open solution file" + m_BasePath + "/" + m_BaseName + ".ini");
1222 return false;
1223 }
1224
1225 QTextStream in(&results);
1226 QString line = in.readLine();
1227
1228 QStringList ini = line.split("=");
1229 if(ini.count() <= 1)
1230 {
1231 emit logOutput("Results file is empty, try again.");
1232 return false;
1233 }
1234 if (ini[1] == "F")
1235 {
1236 line = in.readLine();
1237 //If the plate solve failed, we still need to search for any error or warning messages and print them out.
1238 while (!line.isNull())
1239 {
1240 QStringList ini = line.split("=");
1241 if (ini[0] == "WARNING")
1242 emit logOutput(line.mid(8).trimmed());
1243 else if (ini[0] == "ERROR")
1244 emit logOutput(line.mid(6).trimmed());
1245
1246 line = in.readLine();
1247 }
1248 emit logOutput("Solver failed. Try again.");
1249 return false;
1250 }
1251 double ra = 0, dec = 0, orient = 0;
1252 double fieldw = 0, fieldh = 0, pixscale = 0;
1253 char rastr[32], decstr[32];
1254 QString parity = "";
1255 double cd11 = 0;
1256 double cd22 = 0;
1257 double cd12 = 0;
1258 double cd21 = 0;
1259 bool ok[8] = {false};
1260
1261 line = in.readLine();
1262 while (!line.isNull())
1263 {
1264 QStringList ini = line.split("=");
1265 if (ini[0] == "CRVAL1")
1266 ra = ini[1].trimmed().toDouble(&ok[0]);
1267 else if (ini[0] == "CRVAL2")
1268 dec = ini[1].trimmed().toDouble(&ok[1]);
1269 else if (ini[0] == "CDELT1")
1270 pixscale = ini[1].trimmed().toDouble(&ok[2]) * 3600.0;
1271 else if (ini[0] == "CROTA2")
1272 orient = ini[1].trimmed().toDouble(&ok[3]);
1273 else if (ini[0] == "CD1_1")
1274 cd11 = ini[1].trimmed().toDouble(&ok[4]);
1275 else if (ini[0] == "CD1_2")
1276 cd12 = ini[1].trimmed().toDouble(&ok[5]);
1277 else if (ini[0] == "CD2_1")
1278 cd21 = ini[1].trimmed().toDouble(&ok[6]);
1279 else if (ini[0] == "CD2_2")
1280 cd22 = ini[1].trimmed().toDouble(&ok[7]);
1281 else if (ini[0] == "WARNING")
1282 emit logOutput(line.mid(8).trimmed());
1283 else if (ini[0] == "ERROR")
1284 emit logOutput(line.mid(6).trimmed());
1285
1286 line = in.readLine();
1287 }
1288
1289 if ( ok[0] && ok[1] && ok[2] && ok[3] )
1290 {
1291 ra2hmsstring(ra, rastr);
1292 dec2dmsstring(dec, decstr);
1293 fieldw = m_Statistics.width * pixscale / 60;
1294 fieldh = m_Statistics.height * pixscale / 60;
1295
1296 if ( ok[4] && ok[5] && ok[6] && ok[7] )
1297 {
1298 // Note, negative determinant = positive parity.
1299 double det = cd11 * cd22 - cd12 * cd21;
1300 if(det > 0)
1301 parity = "neg";
1302 else
1303 parity = "pos";
1304 }
1305
1306 double raErr = 0;
1307 double decErr = 0;
1308 if(m_UsePosition)
1309 {
1310 raErr = (search_ra - ra) * 3600;
1311 decErr = (search_dec - dec) * 3600;
1312 }
1313
1314 m_Solution = {fieldw, fieldh, ra, dec, orient, pixscale, parity, raErr, decErr};
1315 emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
1316 emit logOutput(QString("Field center: (RA,Dec) = (%1, %2) deg.").arg( ra).arg( dec));
1317 emit logOutput(QString("Field center: (RA H:M:S, Dec D:M:S) = (%1, %2).").arg( rastr).arg( decstr));
1318 if(m_UsePosition)
1319 emit logOutput(QString("Field is: (%1, %2) deg from search coords.").arg( raErr).arg( decErr));
1320 emit logOutput(QString("Field size: %1 x %2 arcminutes").arg( fieldw).arg( fieldh));
1321 emit logOutput(QString("Pixel Scale: %1\"").arg( pixscale ));
1322 emit logOutput(QString("Field rotation angle: up is %1 degrees E of N").arg( orient));
1323 emit logOutput(QString("Field parity: %1\n").arg( parity));
1324
1325 return true;
1326 }
1327 else
1328 {
1329 emit logOutput("Solver failed. Try again.");
1330 return false;
1331 }
1332 }
1333
1334 //This method writes the table to the file
1335 //I had to create it from the examples on NASA's website
1336 //When I first made this program, I needed it to generate an xyls file from the internal sextraction
1337 //Now it is just used on Windows for the external solving because it needs to use the internal sextractor and the external solver.
1338 //https://heasarc.gsfc.nasa.gov/docs/software/fitsio/quick/node10.html
1339 //https://heasarc.gsfc.nasa.gov/docs/software/fitsio/cookbook/node16.html
writeSextractorTable()1340 int ExternalSextractorSolver::writeSextractorTable()
1341 {
1342 int status = 0;
1343 fitsfile * new_fptr;
1344
1345 if(m_ExtractorType == EXTRACTOR_INTERNAL && m_SolverType == SOLVER_ASTAP)
1346 {
1347 QFileInfo file(fileToProcess);
1348 if(!file.exists())
1349 return -1;
1350
1351 if(file.suffix() != "fits" && file.suffix() != "fit")
1352 {
1353 int ret = saveAsFITS();
1354 if(ret != 0)
1355 return ret;
1356 }
1357 else
1358 {
1359 QString newFileURL = m_BasePath + "/" + m_BaseName + ".fits";
1360 QFile::copy(fileToProcess, newFileURL);
1361 fileToProcess = newFileURL;
1362 fileToProcessIsTempFile = true;
1363 }
1364
1365 if (fits_open_diskfile(&new_fptr, fileToProcess.toLatin1(), READWRITE, &status))
1366 {
1367 fits_report_error(stderr, status);
1368 return status;
1369 }
1370 }
1371 else
1372 {
1373 if(sextractorFilePath == "")
1374 {
1375 sextractorFilePathIsTempFile = true;
1376 sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
1377 }
1378
1379 QFile sextractorFile(sextractorFilePath);
1380 if(sextractorFile.exists())
1381 sextractorFile.remove();
1382
1383 if (fits_create_file(&new_fptr, sextractorFilePath.toLatin1(), &status))
1384 {
1385 fits_report_error(stderr, status);
1386 return status;
1387 }
1388 }
1389
1390 int tfields = 3;
1391 int nrows = m_ExtractedStars.size();
1392 QString extname = "Sextractor_File";
1393
1394 //Columns: X_IMAGE, double, pixels, Y_IMAGE, double, pixels, MAG_AUTO, double, mag
1395 char* ttype[] = { xcol, ycol, magcol };
1396 char* tform[] = { colFormat, colFormat, colFormat };
1397 char* tunit[] = { colUnits, colUnits, magUnits };
1398 const char* extfile = "Sextractor_File";
1399
1400 float *xArray = new float[m_ExtractedStars.size()];
1401 float *yArray = new float[m_ExtractedStars.size()];
1402 float *magArray = new float[m_ExtractedStars.size()];
1403
1404 for (int i = 0; i < m_ExtractedStars.size(); i++)
1405 {
1406 xArray[i] = m_ExtractedStars.at(i).x;
1407 yArray[i] = m_ExtractedStars.at(i).y;
1408 magArray[i] = m_ExtractedStars.at(i).mag;
1409 }
1410
1411 int firstrow = 1; /* first row in table to write */
1412 int firstelem = 1;
1413 int column = 1;
1414
1415 if(fits_create_tbl(new_fptr, BINARY_TBL, nrows, tfields,
1416 ttype, tform, tunit, extfile, &status))
1417 {
1418 emit logOutput(QString("Could not create binary table."));
1419 goto exit;
1420 }
1421
1422 if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, xArray, &status))
1423 {
1424 emit logOutput(QString("Could not write x pixels in binary table."));
1425 goto exit;
1426 }
1427
1428 column = 2;
1429 if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, yArray, &status))
1430 {
1431 emit logOutput(QString("Could not write y pixels in binary table."));
1432 goto exit;
1433 }
1434
1435 column = 3;
1436 if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, magArray, &status))
1437 {
1438 emit logOutput(QString("Could not write magnitudes in binary table."));
1439 goto exit;
1440 }
1441
1442 if(fits_close_file(new_fptr, &status))
1443 {
1444 emit logOutput(QString("Error closing file."));
1445 goto exit;
1446 }
1447 status = 0;
1448
1449 exit:
1450 delete[] xArray;
1451 delete[] yArray;
1452 delete[] magArray;
1453
1454 return status;
1455 }
1456
1457 //This is very necessary for solving non-fits images with external Sextractor
1458 //This was copied and pasted and modified from ImageToFITS in fitsdata in KStars
saveAsFITS()1459 int ExternalSextractorSolver::saveAsFITS()
1460 {
1461 QFileInfo fileInfo(fileToProcess.toLatin1());
1462 QString newFilename = m_BasePath + "/" + m_BaseName + ".fits";
1463
1464 int status = 0;
1465 fitsfile * new_fptr;
1466
1467 //I am hoping that this is correct.
1468 //I"m trying to set these two variables based on the ndim variable since this class doesn't have access to these variables.
1469 long naxis;
1470 int channels;
1471 if (m_Statistics.ndim < 3)
1472 {
1473 channels = 1;
1474 naxis = 2;
1475 }
1476 else
1477 {
1478 channels = 3;
1479 naxis = 3;
1480 }
1481
1482 long nelements, exposure;
1483 long naxes[3] = { m_Statistics.width, m_Statistics.height, channels };
1484 char error_status[512] = {0};
1485
1486 QFileInfo newFileInfo(newFilename);
1487 if(newFileInfo.exists())
1488 QFile(newFilename).remove();
1489
1490 nelements = m_Statistics.samples_per_channel * channels;
1491
1492 /* Create a new File, overwriting existing*/
1493 if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status))
1494 {
1495 fits_report_error(stderr, status);
1496 return status;
1497 }
1498
1499 int bitpix;
1500 switch(m_Statistics.dataType)
1501 {
1502 case SEP_TBYTE:
1503 bitpix = BYTE_IMG;
1504 break;
1505 case TSHORT:
1506 bitpix = SHORT_IMG;
1507 break;
1508 case TUSHORT:
1509 bitpix = USHORT_IMG;
1510 break;
1511 case TLONG:
1512 bitpix = LONG_IMG;
1513 break;
1514 case TULONG:
1515 bitpix = ULONG_IMG;
1516 break;
1517 case TFLOAT:
1518 bitpix = FLOAT_IMG;
1519 break;
1520 case TDOUBLE:
1521 bitpix = DOUBLE_IMG;
1522 break;
1523 default:
1524 bitpix = BYTE_IMG;
1525 }
1526
1527 fitsfile *fptr = new_fptr;
1528 if (fits_create_img(fptr, bitpix, naxis, naxes, &status))
1529 {
1530 emit logOutput(QString("fits_create_img failed: %1").arg(error_status));
1531 status = 0;
1532 fits_flush_file(fptr, &status);
1533 fits_close_file(fptr, &status);
1534 return status;
1535 }
1536
1537 /* Write Data */
1538 if (fits_write_img(fptr, m_Statistics.dataType, 1, nelements, const_cast<void *>(reinterpret_cast<const void *>(m_ImageBuffer)), &status))
1539 {
1540 fits_report_error(stderr, status);
1541 return status;
1542 }
1543
1544 /* Write keywords */
1545
1546 exposure = 1;
1547 fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);
1548
1549 // NAXIS1
1550 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(m_Statistics.width), "length of data axis 1", &status))
1551 {
1552 fits_report_error(stderr, status);
1553 return status;
1554 }
1555
1556 // NAXIS2
1557 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(m_Statistics.height), "length of data axis 2", &status))
1558 {
1559 fits_report_error(stderr, status);
1560 return status;
1561 }
1562
1563 // ISO Date
1564 if (fits_write_date(fptr, &status))
1565 {
1566 fits_report_error(stderr, status);
1567 return status;
1568 }
1569
1570 fileToProcess = newFilename;
1571 fileToProcessIsTempFile = true;
1572
1573 fits_flush_file(fptr, &status);
1574
1575 if(fits_close_file(fptr, &status))
1576 {
1577 emit logOutput(QString("Error closing file."));
1578 return status;
1579 }
1580
1581 emit logOutput("Saved FITS file:" + fileToProcess);
1582
1583 return 0;
1584 }
1585
1586 //This was essentially copied from KStars' loadWCS method and split in half with some modifications.
loadWCS()1587 int ExternalSextractorSolver::loadWCS()
1588 {
1589 if(solutionFile == "")
1590 solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
1591
1592 emit logOutput("Loading WCS from file...");
1593
1594 QFile solution(solutionFile);
1595 if(!solution.exists())
1596 {
1597 emit logOutput("WCS File does not exist.");
1598 return -1;
1599 }
1600
1601 int status = 0;
1602 char * header { nullptr };
1603 int nkeyrec, nreject, nwcs;
1604
1605 fitsfile *fptr { nullptr };
1606
1607 if (fits_open_diskfile(&fptr, solutionFile.toLatin1(), READONLY, &status))
1608 {
1609 char errmsg[512];
1610 fits_get_errstatus(status, errmsg);
1611 emit logOutput(QString("Error opening fits file %1, %2").arg(solutionFile).arg(errmsg));
1612 return status;
1613 }
1614
1615 if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status))
1616 {
1617 char errmsg[512];
1618 fits_get_errstatus(status, errmsg);
1619 emit logOutput(QString("ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]));
1620 return status;
1621 }
1622
1623 if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &m_wcs)) != 0)
1624 {
1625 free(header);
1626 wcsvfree(&m_nwcs, &m_wcs);
1627 m_wcs = nullptr;
1628 m_HasWCS = false;
1629 emit logOutput(QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]));
1630 return status;
1631 }
1632 fits_close_file(fptr, &status);
1633
1634 #ifndef _WIN32 //For some very strange reason, this causes a crash on Windows??
1635 free(header);
1636 #endif
1637
1638 if (m_wcs == nullptr)
1639 {
1640 emit logOutput("No world coordinate systems found.");
1641 m_HasWCS = false;
1642 return status;
1643 }
1644 else
1645 m_HasWCS = true;
1646
1647 // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now.
1648 if (m_wcs->crpix[0] == 0)
1649 {
1650 wcsvfree(&m_nwcs, &m_wcs);
1651 m_wcs = nullptr;
1652 m_HasWCS = false;
1653 emit logOutput("No world coordinate systems found.");
1654 return status;
1655 }
1656
1657 if ((status = wcsset(m_wcs)) != 0)
1658 {
1659 wcsvfree(&m_nwcs, &m_wcs);
1660 m_wcs = nullptr;
1661 m_HasWCS = false;
1662 emit logOutput(QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1663 return status;
1664 }
1665
1666 emit logOutput("Finished Loading WCS...");
1667
1668 return 0;
1669 }
1670
1671 //This was essentially copied from KStars' loadWCS method and split in half with some modifications
computeWCSCoord()1672 void ExternalSextractorSolver::computeWCSCoord()
1673 {
1674 if(!m_HasWCS)
1675 {
1676 emit logOutput("There is no WCS Data. Did you solve the image first?");
1677 return;
1678 }
1679 int w = m_Statistics.width;
1680 int h = m_Statistics.height;
1681 wcs_coord = new FITSImage::wcs_point[w * h];
1682 FITSImage::wcs_point * p = wcs_coord;
1683 double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2];
1684 int status;
1685 int stat[2];
1686
1687 for (int i = 0; i < h; i++)
1688 {
1689 for (int j = 0; j < w; j++)
1690 {
1691 pixcrd[0] = j;
1692 pixcrd[1] = i;
1693
1694 if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0)
1695 {
1696 emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1697 }
1698 else
1699 {
1700 p->ra = world[0];
1701 p->dec = world[1];
1702
1703 p++;
1704 }
1705 }
1706 }
1707 }
1708
pixelToWCS(const QPointF & pixelPoint,FITSImage::wcs_point & skyPoint)1709 bool ExternalSextractorSolver::pixelToWCS(const QPointF &pixelPoint, FITSImage::wcs_point &skyPoint)
1710 {
1711 if(!hasWCSData())
1712 {
1713 emit logOutput("There is no WCS Data.");
1714 return false;
1715 }
1716 double imgcrd[2], phi, pixcrd[2], theta, world[2];
1717 int stat[2];
1718 pixcrd[0] = pixelPoint.x();
1719 pixcrd[1] = pixelPoint.y();
1720
1721 int status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]);
1722 if(status != 0)
1723 {
1724 emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1725 return false;
1726 }
1727 else
1728 {
1729 skyPoint.ra = world[0];
1730 skyPoint.dec = world[1];
1731 }
1732 return true;
1733 }
1734
wcsToPixel(const FITSImage::wcs_point & skyPoint,QPointF & pixelPoint)1735 bool ExternalSextractorSolver::wcsToPixel(const FITSImage::wcs_point &skyPoint, QPointF &pixelPoint)
1736 {
1737 if(!hasWCSData())
1738 {
1739 emit logOutput("There is no WCS Data.");
1740 return false;
1741 }
1742 double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2];
1743 int stat[2];
1744 worldcrd[0] = skyPoint.ra;
1745 worldcrd[1] = skyPoint.dec;
1746
1747 int status = wcss2p(m_wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0]);
1748 if(status != 0)
1749 {
1750 emit logOutput(QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1751 return false;
1752 }
1753 pixelPoint.setX(pixcrd[0]);
1754 pixelPoint.setY(pixcrd[1]);
1755 return true;
1756 }
1757
appendStarsRAandDEC(QList<FITSImage::Star> & stars)1758 bool ExternalSextractorSolver::appendStarsRAandDEC(QList<FITSImage::Star> &stars)
1759 {
1760 if(!m_HasWCS)
1761 {
1762 emit logOutput("There is no WCS Data. Did you solve the image first?");
1763 return false;
1764 }
1765
1766 double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2];
1767 int stat[2];
1768
1769 for(auto &oneStar : stars)
1770 {
1771 int status = 0;
1772 double ra = HUGE_VAL;
1773 double dec = HUGE_VAL;
1774 pixcrd[0] = oneStar.x;
1775 pixcrd[1] = oneStar.y;
1776
1777 if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0)
1778 {
1779 emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1780 return false;
1781 }
1782 else
1783 {
1784 ra = world[0];
1785 dec = world[1];
1786 }
1787
1788 oneStar.ra = ra;
1789 oneStar.dec = dec;
1790 }
1791
1792 return true;
1793 }
1794