Kstars

fitsdata.cpp
1/*
2 SPDX-FileCopyrightText: 2004 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5
6 Some code fragments were adapted from Peter Kirchgessner's FITS plugin
7 SPDX-FileCopyrightText: Peter Kirchgessner <http://members.aol.com/pkirchg>
8*/
9
10#include "fitsdata.h"
11#include "fitsbahtinovdetector.h"
12#include "fitsthresholddetector.h"
13#include "fitsgradientdetector.h"
14#include "fitscentroiddetector.h"
15#include "fitssepdetector.h"
16
17#include "fpack.h"
18
19#include "kstarsdata.h"
20#include "ksutils.h"
21#include "kspaths.h"
22#include "Options.h"
23#include "skymapcomposite.h"
24#include "auxiliary/ksnotification.h"
25#include "auxiliary/robuststatistics.h"
26
27#include <KFormat>
28#include <QApplication>
29#include <QImage>
30#include <QtConcurrent>
31#include <QImageReader>
32
33#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
34#include <wcshdr.h>
35#include <wcsfix.h>
36#endif
37
38#if !defined(KSTARS_LITE) && defined(HAVE_LIBRAW)
39#include <libraw/libraw.h>
40#endif
41
42#ifdef HAVE_XISF
43#include <libxisf.h>
44#endif
45
46#include <cfloat>
47#include <cmath>
48
49#include <fits_debug.h>
50
51#define ZOOM_DEFAULT 100.0
52#define ZOOM_MIN 10
53#define ZOOM_MAX 400
54#define ZOOM_LOW_INCR 10
55#define ZOOM_HIGH_INCR 50
56
57QString getTemporaryPath()
58{
59 return QDir(KSPaths::writableLocation(QStandardPaths::TempLocation) + "/" +
60 qAppName()).path();
61}
62
63const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
64
65bool FITSData::readableFilename(const QString &filename)
66{
67 QFileInfo info(filename);
68 QString extension = info.completeSuffix().toLower();
69 return extension.contains("fit") || extension.contains("fz") ||
70 extension.contains("xisf") ||
72 RAWFormats.contains(extension);
73}
74
75FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode)
76{
77 static const QRegularExpression re("[-{}]");
78
79 qRegisterMetaType<FITSMode>("FITSMode");
80
81 debayerParams.method = DC1394_BAYER_METHOD_NEAREST;
82 debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
83 debayerParams.offsetX = debayerParams.offsetY = 0;
84
85 // Reserve 3 channels
86 m_CumulativeFrequency.resize(3);
87 m_HistogramBinWidth.resize(3);
88 m_HistogramFrequency.resize(3);
89 m_HistogramIntensity.resize(3);
90
91 // Set UUID for each view
93 uuid = uuid.remove(re);
94 setObjectName(uuid);
95}
96
97FITSData::FITSData(const QSharedPointer<FITSData> &other)
98{
99 static const QRegularExpression re("[-{}]");
100 qRegisterMetaType<FITSMode>("FITSMode");
101
102 debayerParams.method = DC1394_BAYER_METHOD_NEAREST;
103 debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
104 debayerParams.offsetX = debayerParams.offsetY = 0;
105
106 m_TemporaryDataFile.setFileTemplate("fits_memory_XXXXXX");
107
108 this->m_Mode = other->m_Mode;
109 this->m_Statistics.channels = other->m_Statistics.channels;
110 memcpy(&m_Statistics, &(other->m_Statistics), sizeof(m_Statistics));
111 m_ImageBuffer = new uint8_t[m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel];
112 memcpy(m_ImageBuffer, other->m_ImageBuffer,
113 m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel);
114
115 // Set UUID for each view
117 uuid = uuid.remove(re);
118 setObjectName(uuid);
119}
120
121FITSData::~FITSData()
122{
123 int status = 0;
124
125 if (m_StarFindFuture.isRunning())
126 m_StarFindFuture.waitForFinished();
127
128 clearImageBuffers();
129
130#ifdef HAVE_WCSLIB
131 if (m_WCSHandle != nullptr)
132 {
133 wcsvfree(&m_nwcs, &m_WCSHandle);
134 m_WCSHandle = nullptr;
135 m_nwcs = 0;
136 }
137#endif
138
139 if (starCenters.count() > 0)
140 qDeleteAll(starCenters);
141 starCenters.clear();
142
143 if (m_SkyObjects.count() > 0)
144 qDeleteAll(m_SkyObjects);
145 m_SkyObjects.clear();
146
147 if (fptr != nullptr)
148 {
149 fits_flush_file(fptr, &status);
150 fits_close_file(fptr, &status);
151 free(m_PackBuffer);
152 m_PackBuffer = nullptr;
153 fptr = nullptr;
154 }
155}
156
157void FITSData::loadCommon(const QString &inFilename)
158{
159 int status = 0;
160 qDeleteAll(starCenters);
161 starCenters.clear();
162
163 if (fptr != nullptr)
164 {
165 fits_flush_file(fptr, &status);
166 fits_close_file(fptr, &status);
167 free(m_PackBuffer);
168 m_PackBuffer = nullptr;
169 fptr = nullptr;
170 }
171
172 m_Filename = inFilename;
173}
174
175bool FITSData::loadFromBuffer(const QByteArray &buffer)
176{
177 loadCommon("");
178 qCDebug(KSTARS_FITS) << "Reading file buffer (" << KFormat().formatByteSize(buffer.size()) << ")";
179 return privateLoad(buffer);
180}
181
182QFuture<bool> FITSData::loadFromFile(const QString &inFilename)
183{
184 loadCommon(inFilename);
185 QFileInfo info(m_Filename);
186 m_Extension = info.completeSuffix().toLower();
187 qCDebug(KSTARS_FITS) << "Loading file " << m_Filename;
188 return QtConcurrent::run(this, &FITSData::privateLoad, QByteArray());
189}
190
191namespace
192{
193// Common code for reporting fits read errors. Always returns false.
194QString fitsErrorToString(int status)
195{
196 char error_status[512] = {0};
197 fits_report_error(stderr, status);
198 fits_get_errstatus(status, error_status);
199 QString message = error_status;
200 return message;
201}
202}
203
204bool FITSData::privateLoad(const QByteArray &buffer)
205{
206 m_isTemporary = m_Filename.startsWith(KSPaths::writableLocation(QStandardPaths::TempLocation));
207 cacheHFR = -1;
208 cacheEccentricity = -1;
209
210 if (m_Extension.contains("fit") || m_Extension.contains("fz"))
211 return loadFITSImage(buffer);
212 if (m_Extension.contains("xisf"))
213 return loadXISFImage(buffer);
214 if (QImageReader::supportedImageFormats().contains(m_Extension.toLatin1()))
215 return loadCanonicalImage(buffer);
216 else if (RAWFormats.contains(m_Extension))
217 return loadRAWImage(buffer);
218
219 return false;
220}
221
222bool FITSData::loadFITSImage(const QByteArray &buffer, const bool isCompressed)
223{
224 int status = 0, anynull = 0;
225 long naxes[3];
226
227 m_HistogramConstructed = false;
228
229 if (m_Extension.contains(".fz") || isCompressed)
230 {
231 fpstate fpvar;
232 fp_init (&fpvar);
233 bool rc = false;
234
235 if (buffer.isEmpty())
236 {
237 // Store so we don't lose.
238 m_compressedFilename = m_Filename;
239
240 QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(
241 QRegularExpression("[-{}]")));
242
243 rc = fp_unpack_file_to_fits(m_Filename.toLocal8Bit().data(), &fptr, fpvar) == 0;
244 if (rc)
245 {
246 m_Filename = uncompressedFile;
247 }
248 }
249 else
250 {
251 size_t m_PackBufferSize = 100000;
252 free(m_PackBuffer);
253 m_PackBuffer = (uint8_t *)malloc(m_PackBufferSize);
254 rc = fp_unpack_data_to_data(buffer.data(), buffer.size(), &m_PackBuffer, &m_PackBufferSize, fpvar) == 0;
255
256 if (rc)
257 {
258 void *data = reinterpret_cast<void *>(m_PackBuffer);
259 if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY, &data, &m_PackBufferSize, 0,
260 nullptr, &status))
261 {
262 m_LastError = i18n("Error reading fits buffer: %1.", fitsErrorToString(status));
263 return false;
264 }
265
266 m_Statistics.size = m_PackBufferSize;
267 }
268 //rc = fp_unpack_data_to_fits(buffer.data(), buffer.size(), &fptr, fpvar) == 0;
269 }
270
271 if (rc == false)
272 {
273 free(m_PackBuffer);
274 m_PackBuffer = nullptr;
275 m_LastError = i18n("Failed to unpack compressed fits");
276 qCCritical(KSTARS_FITS) << m_LastError;
277 return false;
278 }
279
280 m_isTemporary = true;
281 m_isCompressed = true;
282 m_Statistics.size = fptr->Fptr->logfilesize;
283
284 }
285 else if (buffer.isEmpty())
286 {
287 // Use open diskfile as it does not use extended file names which has problems opening
288 // files with [ ] or ( ) in their names.
289 if (fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status))
290 {
291 m_LastError = i18n("Error opening fits file %1 : %2", m_Filename, fitsErrorToString(status));
292 }
293
294 m_Statistics.size = QFile(m_Filename).size();
295 }
296 else
297 {
298 // Read the FITS file from a memory buffer.
299 void *temp_buffer = const_cast<void *>(reinterpret_cast<const void *>(buffer.data()));
300 size_t temp_size = buffer.size();
301 if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY,
302 &temp_buffer, &temp_size, 0, nullptr, &status))
303 {
304 m_LastError = i18n("Error reading fits buffer: %1", fitsErrorToString(status));
305 return false;
306 }
307
308 m_Statistics.size = temp_size;
309 }
310
311 if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
312 {
313
314 free(m_PackBuffer);
315 m_PackBuffer = nullptr;
316 m_LastError = i18n("Could not locate image HDU: %1", fitsErrorToString(status));
317 }
318
319 if (fits_get_img_param(fptr, 3, &m_FITSBITPIX, &(m_Statistics.ndim), naxes, &status))
320 {
321 free(m_PackBuffer);
322 m_PackBuffer = nullptr;
323 m_LastError = i18n("FITS file open error (fits_get_img_param): %1", fitsErrorToString(status));
324 return false;
325 }
326
327 // Reload if it is transparently compressed.
328 if ((fits_is_compressed_image(fptr, &status) || m_Statistics.ndim <= 0) && !isCompressed)
329 {
330 loadCommon(m_Filename);
331 qCDebug(KSTARS_FITS) << "Image is compressed. Reloading...";
332 return loadFITSImage(buffer, true);
333 }
334
335 if (m_Statistics.ndim < 2)
336 {
337 m_LastError = i18n("1D FITS images are not supported in KStars.");
338 qCCritical(KSTARS_FITS) << m_LastError;
339 free(m_PackBuffer);
340 m_PackBuffer = nullptr;
341 return false;
342 }
343
344 switch (m_FITSBITPIX)
345 {
346 case BYTE_IMG:
347 m_Statistics.dataType = TBYTE;
348 m_Statistics.bytesPerPixel = sizeof(uint8_t);
349 break;
350 case SHORT_IMG:
351 // Read SHORT image as USHORT
352 m_FITSBITPIX = USHORT_IMG;
353 m_Statistics.dataType = TUSHORT;
354 m_Statistics.bytesPerPixel = sizeof(int16_t);
355 break;
356 case USHORT_IMG:
357 m_Statistics.dataType = TUSHORT;
358 m_Statistics.bytesPerPixel = sizeof(uint16_t);
359 break;
360 case LONG_IMG:
361 // Read LONG image as ULONG
362 m_FITSBITPIX = ULONG_IMG;
363 m_Statistics.dataType = TULONG;
364 m_Statistics.bytesPerPixel = sizeof(int32_t);
365 break;
366 case ULONG_IMG:
367 m_Statistics.dataType = TULONG;
368 m_Statistics.bytesPerPixel = sizeof(uint32_t);
369 break;
370 case FLOAT_IMG:
371 m_Statistics.dataType = TFLOAT;
372 m_Statistics.bytesPerPixel = sizeof(float);
373 break;
374 case LONGLONG_IMG:
375 m_Statistics.dataType = TLONGLONG;
376 m_Statistics.bytesPerPixel = sizeof(int64_t);
377 break;
378 case DOUBLE_IMG:
379 m_Statistics.dataType = TDOUBLE;
380 m_Statistics.bytesPerPixel = sizeof(double);
381 break;
382 default:
383 m_LastError = i18n("Bit depth %1 is not supported.", m_FITSBITPIX);
384 qCCritical(KSTARS_FITS) << m_LastError;
385 return false;
386 }
387
388 if (m_Statistics.ndim < 3)
389 naxes[2] = 1;
390
391 if (naxes[0] == 0 || naxes[1] == 0)
392 {
393 m_LastError = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]);
394 qCCritical(KSTARS_FITS) << m_LastError;
395 free(m_PackBuffer);
396 m_PackBuffer = nullptr;
397 return false;
398 }
399
400 m_Statistics.width = naxes[0];
401 m_Statistics.height = naxes[1];
402 m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
403 roiCenter.setX(m_Statistics.width / 2);
404 roiCenter.setY(m_Statistics.height / 2);
405 if(m_Statistics.width % 2)
406 roiCenter.setX(roiCenter.x() + 1);
407 if(m_Statistics.height % 2)
408 roiCenter.setY(roiCenter.y() + 1);
409
410 clearImageBuffers();
411
412 m_Statistics.channels = naxes[2];
413
414 // Channels always set to #1 if we are not required to process 3D Cubes
415 // Or if mode is not FITS_NORMAL (guide, focus..etc)
416 if ( (m_Mode != FITS_NORMAL && m_Mode != FITS_CALIBRATE) || !Options::auto3DCube())
417 m_Statistics.channels = 1;
418
419 m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
420 m_ImageBuffer = new uint8_t[m_ImageBufferSize];
421 if (m_ImageBuffer == nullptr)
422 {
423 qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: "
424 << m_ImageBufferSize << " bytes.";
425 clearImageBuffers();
426 free(m_PackBuffer);
427 m_PackBuffer = nullptr;
428 return false;
429 }
430
431 rotCounter = 0;
432 flipHCounter = 0;
433 flipVCounter = 0;
434 long nelements = m_Statistics.samples_per_channel * m_Statistics.channels;
435
436 if (fits_read_img(fptr, m_Statistics.dataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status))
437 {
438 m_LastError = i18n("Error reading image: %1", fitsErrorToString(status));
439 return false;
440 }
441
442 parseHeader();
443
444 // Get UTC date time
445 QVariant value;
446 if (getRecordValue("DATE-OBS", value) && value.isValid())
447 {
448 QDateTime ts = value.toDateTime();
449 m_DateTime = KStarsDateTime(ts.date(), ts.time());
450 }
451
452 // Only check for debayed IF the original naxes[2] is 1
453 // which is for single channels.
454 if (naxes[2] == 1 && m_Statistics.channels == 1 && Options::autoDebayer() && checkDebayer())
455 {
456 // Save bayer image on disk in case we need to save it later since debayer destorys this data
457 if (m_isTemporary && m_TemporaryDataFile.open())
458 {
459 m_TemporaryDataFile.write(buffer);
460 m_TemporaryDataFile.close();
461 m_Filename = m_TemporaryDataFile.fileName();
462 }
463
464 if (debayer())
465 calculateStats(false, false);
466 }
467 else
468 calculateStats(false, false);
469
470 if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN)
471 loadWCS();
472
473 starsSearched = false;
474
475 return true;
476}
477
478bool FITSData::loadXISFImage(const QByteArray &buffer)
479{
480 m_HistogramConstructed = false;
481 clearImageBuffers();
482
483#ifdef HAVE_XISF
484 try
485 {
486 LibXISF::XISFReader xisfReader;
487 if (buffer.isEmpty())
488 {
489 xisfReader.open(m_Filename.toLocal8Bit().data());
490 }
491 else
492 {
493 LibXISF::ByteArray byteArray(buffer.constData(), buffer.size());
494 xisfReader.open(byteArray);
495 }
496
497 if (xisfReader.imagesCount() == 0)
498 {
499 m_LastError = i18n("File contain no images");
500 return false;
501 }
502
503 const LibXISF::Image &image = xisfReader.getImage(0);
504
505 switch (image.sampleFormat())
506 {
507 case LibXISF::Image::UInt8:
508 m_Statistics.dataType = TBYTE;
509 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt8);
510 m_FITSBITPIX = TBYTE;
511 break;
512 case LibXISF::Image::UInt16:
513 m_Statistics.dataType = TUSHORT;
514 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt16);
515 m_FITSBITPIX = TUSHORT;
516 break;
517 case LibXISF::Image::UInt32:
518 m_Statistics.dataType = TULONG;
519 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt32);
520 m_FITSBITPIX = TULONG;
521 break;
522 case LibXISF::Image::Float32:
523 m_Statistics.dataType = TFLOAT;
524 m_Statistics.bytesPerPixel = sizeof(LibXISF::Float32);
525 m_FITSBITPIX = TFLOAT;
526 break;
527 default:
528 m_LastError = i18n("Sample format %1 is not supported.", LibXISF::Image::sampleFormatString(image.sampleFormat()).c_str());
529 qCCritical(KSTARS_FITS) << m_LastError;
530 return false;
531 }
532
533 m_Statistics.width = image.width();
534 m_Statistics.height = image.height();
535 m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
536 m_Statistics.channels = image.channelCount();
537 m_Statistics.size = buffer.size();
538 roiCenter.setX(m_Statistics.width / 2);
539 roiCenter.setY(m_Statistics.height / 2);
540 if(m_Statistics.width % 2)
541 roiCenter.setX(roiCenter.x() + 1);
542 if(m_Statistics.height % 2)
543 roiCenter.setY(roiCenter.y() + 1);
544
545 m_HeaderRecords.clear();
546 auto &fitsKeywords = image.fitsKeywords();
547 for(auto &fitsKeyword : fitsKeywords)
548 m_HeaderRecords.push_back({QString::fromStdString(fitsKeyword.name), QString::fromStdString(fitsKeyword.value), QString::fromStdString(fitsKeyword.comment)});
549
550 QVariant value;
551 if (getRecordValue("DATE-OBS", value) && value.isValid())
552 {
553 QDateTime ts = value.toDateTime();
554 m_DateTime = KStarsDateTime(ts.date(), ts.time());
555 }
556
557 m_ImageBufferSize = image.imageDataSize();
558 m_ImageBuffer = new uint8_t[m_ImageBufferSize];
559 std::memcpy(m_ImageBuffer, image.imageData(), m_ImageBufferSize);
560
561 calculateStats(false, false);
562 loadWCS();
563 }
564 catch (LibXISF::Error &error)
565 {
566 m_LastError = i18n("XISF file open error: ") + error.what();
567 return false;
568 }
569 return true;
570#else
571 Q_UNUSED(buffer)
572 return false;
573#endif
574
575}
576
577bool FITSData::saveXISFImage(const QString &newFilename)
578{
579#ifdef HAVE_XISF
580 try
581 {
582 LibXISF::XISFWriter xisfWriter;
583 LibXISF::Image image;
584 image.setGeometry(m_Statistics.width, m_Statistics.height, m_Statistics.channels);
585 if (m_Statistics.channels > 1)
586 image.setColorSpace(LibXISF::Image::RGB);
587
588 switch (m_FITSBITPIX)
589 {
590 case BYTE_IMG:
591 image.setSampleFormat(LibXISF::Image::UInt8);
592 break;
593 case USHORT_IMG:
594 image.setSampleFormat(LibXISF::Image::UInt16);
595 break;
596 case ULONG_IMG:
597 image.setSampleFormat(LibXISF::Image::UInt32);
598 break;
599 case FLOAT_IMG:
600 image.setSampleFormat(LibXISF::Image::Float32);
601 break;
602 default:
603 m_LastError = i18n("Bit depth %1 is not supported.", m_FITSBITPIX);
604 qCCritical(KSTARS_FITS) << m_LastError;
605 return false;
606 }
607
608 std::memcpy(image.imageData(), m_ImageBuffer, m_ImageBufferSize);
609 for (auto &fitsKeyword : m_HeaderRecords)
610 image.addFITSKeyword({fitsKeyword.key.toUtf8().data(), fitsKeyword.value.toString().toUtf8().data(), fitsKeyword.comment.toUtf8().data()});
611
612 xisfWriter.writeImage(image);
613 xisfWriter.save(newFilename.toLocal8Bit().data());
614 m_Filename = newFilename;
615 }
616 catch (LibXISF::Error &err)
617 {
618 m_LastError = i18n("Error saving XISF image") + err.what();
619 return false;
620 }
621 return true;
622#else
623 Q_UNUSED(newFilename)
624 return false;
625#endif
626}
627
628bool FITSData::loadCanonicalImage(const QByteArray &buffer)
629{
630 QImage imageFromFile;
631 if (!buffer.isEmpty())
632 {
633 if(!imageFromFile.loadFromData(reinterpret_cast<const uint8_t*>(buffer.data()), buffer.size()))
634 {
635 qCCritical(KSTARS_FITS) << "Failed to open image.";
636 return false;
637 }
638
639 m_Statistics.size = buffer.size();
640 }
641 else
642 {
643 if(!imageFromFile.load(m_Filename.toLocal8Bit()))
644 {
645 qCCritical(KSTARS_FITS) << "Failed to open image.";
646 return false;
647 }
648
649 m_Statistics.size = QFileInfo(m_Filename).size();
650 }
651
652 //imageFromFile = imageFromFile.convertToFormat(QImage::Format_RGB32);
653
654 // Note: This will need to be changed. I think QT only loads 8 bpp images.
655 // Also the depth method gives the total bits per pixel in the image not just the bits per
656 // pixel in each channel.
657 m_FITSBITPIX = BYTE_IMG;
658 switch (m_FITSBITPIX)
659 {
660 case BYTE_IMG:
661 m_Statistics.dataType = TBYTE;
662 m_Statistics.bytesPerPixel = sizeof(uint8_t);
663 break;
664 case SHORT_IMG:
665 // Read SHORT image as USHORT
666 m_Statistics.dataType = TUSHORT;
667 m_Statistics.bytesPerPixel = sizeof(int16_t);
668 break;
669 case USHORT_IMG:
670 m_Statistics.dataType = TUSHORT;
671 m_Statistics.bytesPerPixel = sizeof(uint16_t);
672 break;
673 case LONG_IMG:
674 // Read LONG image as ULONG
675 m_Statistics.dataType = TULONG;
676 m_Statistics.bytesPerPixel = sizeof(int32_t);
677 break;
678 case ULONG_IMG:
679 m_Statistics.dataType = TULONG;
680 m_Statistics.bytesPerPixel = sizeof(uint32_t);
681 break;
682 case FLOAT_IMG:
683 m_Statistics.dataType = TFLOAT;
684 m_Statistics.bytesPerPixel = sizeof(float);
685 break;
686 case LONGLONG_IMG:
687 m_Statistics.dataType = TLONGLONG;
688 m_Statistics.bytesPerPixel = sizeof(int64_t);
689 break;
690 case DOUBLE_IMG:
691 m_Statistics.dataType = TDOUBLE;
692 m_Statistics.bytesPerPixel = sizeof(double);
693 break;
694 default:
695 m_LastError = QString("Bit depth %1 is not supported.").arg(m_FITSBITPIX);
696 qCCritical(KSTARS_FITS) << m_LastError;
697 return false;
698 }
699
700 m_Statistics.width = static_cast<uint16_t>(imageFromFile.width());
701 m_Statistics.height = static_cast<uint16_t>(imageFromFile.height());
702 m_Statistics.channels = imageFromFile.format() == QImage::Format_Grayscale8 ? 1 : 3;
703 m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
704 clearImageBuffers();
705 m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * static_cast<uint16_t>
706 (m_Statistics.bytesPerPixel);
707 m_ImageBuffer = new uint8_t[m_ImageBufferSize];
708 if (m_ImageBuffer == nullptr)
709 {
710 m_LastError = i18n("FITSData: Not enough memory for image_buffer channel. Requested: %1 bytes ", m_ImageBufferSize);
711 qCCritical(KSTARS_FITS) << m_LastError;
712 clearImageBuffers();
713 return false;
714 }
715
716 if (m_Statistics.channels == 1)
717 {
718 memcpy(m_ImageBuffer, imageFromFile.bits(), m_ImageBufferSize);
719 }
720 else
721 {
722
723 auto debayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
724 auto * original_bayered_buffer = reinterpret_cast<uint8_t *>(imageFromFile.bits());
725
726 // Data in RGBA, with bytes in the order of B,G,R we need to copy them into 3 layers for FITS
727 uint8_t * rBuff = debayered_buffer;
728 uint8_t * gBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height);
729 uint8_t * bBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
730
731 int imax = m_Statistics.samples_per_channel * 4 - 4;
732 for (int i = 0; i <= imax; i += 4)
733 {
734 *rBuff++ = original_bayered_buffer[i + 2];
735 *gBuff++ = original_bayered_buffer[i + 1];
736 *bBuff++ = original_bayered_buffer[i + 0];
737 }
738 }
739
740 calculateStats(false, false);
741 return true;
742}
743
744bool FITSData::loadRAWImage(const QByteArray &buffer)
745{
746#if !defined(KSTARS_LITE) && !defined(HAVE_LIBRAW)
747 m_LastError = i18n("Unable to find dcraw and cjpeg. Please install the required tools to convert CR2/NEF to JPEG.");
748 return false;
749#else
750
751 int ret = 0;
752 // Creation of image processing object
753 LibRaw RawProcessor;
754
755 // Let us open the file/buffer
756 if (buffer.isEmpty())
757 {
758 // Open file
759 if ((ret = RawProcessor.open_file(m_Filename.toLocal8Bit().constData())) != LIBRAW_SUCCESS)
760 {
761 m_LastError = i18n("Cannot open file %1: %2", m_Filename, libraw_strerror(ret));
762 RawProcessor.recycle();
763 return false;
764 }
765
766 m_Statistics.size = QFileInfo(m_Filename).size();
767 }
768 // Open Buffer
769 else
770 {
771 if ((ret = RawProcessor.open_buffer(const_cast<void *>(reinterpret_cast<const void *>(buffer.data())),
772 buffer.size())) != LIBRAW_SUCCESS)
773 {
774 m_LastError = i18n("Cannot open buffer: %1", libraw_strerror(ret));
775 RawProcessor.recycle();
776 return false;
777 }
778
779 m_Statistics.size = buffer.size();
780 }
781
782 // Let us unpack the thumbnail
783 if ((ret = RawProcessor.unpack()) != LIBRAW_SUCCESS)
784 {
785 m_LastError = i18n("Cannot unpack_thumb: %1", libraw_strerror(ret));
786 RawProcessor.recycle();
787 return false;
788 }
789
790 if ((ret = RawProcessor.dcraw_process()) != LIBRAW_SUCCESS)
791 {
792 m_LastError = i18n("Cannot dcraw_process: %1", libraw_strerror(ret));
793 RawProcessor.recycle();
794 return false;
795 }
796
797 libraw_processed_image_t *image = RawProcessor.dcraw_make_mem_image(&ret);
798 if (ret != LIBRAW_SUCCESS)
799 {
800 m_LastError = i18n("Cannot load to memory: %1", libraw_strerror(ret));
801 RawProcessor.recycle();
802 return false;
803 }
804
805 RawProcessor.recycle();
806
807 m_Statistics.bytesPerPixel = image->bits / 8;
808 // We only support two types now
809 if (m_Statistics.bytesPerPixel == 1)
810 m_Statistics.dataType = TBYTE;
811 else
812 m_Statistics.dataType = TUSHORT;
813 m_Statistics.width = image->width;
814 m_Statistics.height = image->height;
815 m_Statistics.channels = image->colors;
816 m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
817 clearImageBuffers();
818 m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
819 m_ImageBuffer = new uint8_t[m_ImageBufferSize];
820 if (m_ImageBuffer == nullptr)
821 {
822 m_LastError = i18n("FITSData: Not enough memory for image_buffer channel. Requested: %1 bytes ", m_ImageBufferSize);
823 qCCritical(KSTARS_FITS) << m_LastError;
824 libraw_dcraw_clear_mem(image);
825 clearImageBuffers();
826 return false;
827 }
828
829 auto destination_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
830 auto source_buffer = reinterpret_cast<uint8_t *>(image->data);
831
832 // For mono, we memcpy directly
833 if (image->colors == 1)
834 {
835 memcpy(destination_buffer, source_buffer, m_ImageBufferSize);
836 }
837 else
838 {
839 // Data in RGB24, with bytes in the order of R,G,B. We copy them copy them into 3 layers for FITS
840 uint8_t * rBuff = destination_buffer;
841 uint8_t * gBuff = destination_buffer + (m_Statistics.width * m_Statistics.height);
842 uint8_t * bBuff = destination_buffer + (m_Statistics.width * m_Statistics.height * 2);
843
844 int imax = m_Statistics.samples_per_channel * 3 - 3;
845 for (int i = 0; i <= imax; i += 3)
846 {
847 *rBuff++ = source_buffer[i + 0];
848 *gBuff++ = source_buffer[i + 1];
849 *bBuff++ = source_buffer[i + 2];
850 }
851 }
852 libraw_dcraw_clear_mem(image);
853
854 calculateStats(false, false);
855 return true;
856#endif
857}
858
859bool FITSData::saveImage(const QString &newFilename)
860{
861 if (newFilename == m_Filename)
862 return true;
863
864 const QString ext = QFileInfo(newFilename).suffix();
865
866 if (ext == "jpg" || ext == "png")
867 {
868 double min, max;
869 QImage fitsImage = FITSToImage(newFilename);
870 getMinMax(&min, &max);
871
872 if (min == max)
873 {
874 fitsImage.fill(Qt::white);
875 }
876 else if (channels() == 1)
877 {
878 fitsImage = QImage(width(), height(), QImage::Format_Indexed8);
879
880 fitsImage.setColorCount(256);
881 for (int i = 0; i < 256; i++)
882 fitsImage.setColor(i, qRgb(i, i, i));
883 }
884 else
885 {
886 fitsImage = QImage(width(), height(), QImage::Format_RGB32);
887 }
888
889 double dataMin = m_Statistics.mean[0] - m_Statistics.stddev[0];
890 double dataMax = m_Statistics.mean[0] + m_Statistics.stddev[0] * 3;
891
892 double bscale = 255. / (dataMax - dataMin);
893 double bzero = (-dataMin) * (255. / (dataMax - dataMin));
894
895 // Long way to do this since we do not want to use templated functions here
896 switch (m_Statistics.dataType)
897 {
898 case TBYTE:
899 convertToQImage<uint8_t>(dataMin, dataMax, bscale, bzero, fitsImage);
900 break;
901
902 case TSHORT:
903 convertToQImage<int16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
904 break;
905
906 case TUSHORT:
907 convertToQImage<uint16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
908 break;
909
910 case TLONG:
911 convertToQImage<int32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
912 break;
913
914 case TULONG:
915 convertToQImage<uint32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
916 break;
917
918 case TFLOAT:
919 convertToQImage<float>(dataMin, dataMax, bscale, bzero, fitsImage);
920 break;
921
922 case TLONGLONG:
923 convertToQImage<int64_t>(dataMin, dataMax, bscale, bzero, fitsImage);
924 break;
925
926 case TDOUBLE:
927 convertToQImage<double>(dataMin, dataMax, bscale, bzero, fitsImage);
928 break;
929
930 default:
931 break;
932 }
933
934 fitsImage.save(newFilename, ext.toLatin1().constData());
935
936 m_Filename = newFilename;
937 qCInfo(KSTARS_FITS) << "Saved image file:" << m_Filename;
938 return true;
939 }
940
941 if (m_isCompressed)
942 {
943 m_LastError = i18n("Saving compressed files is not supported.");
944 return false;
945 }
946
947 int status = 0;
948 long nelements;
949 fitsfile * new_fptr;
950
951 if (ext == "xisf")
952 {
953 if(fptr)
954 {
955 fits_close_file(fptr, &status);
956 fptr = nullptr;
957 }
958 rotCounter = flipHCounter = flipVCounter = 0;
959 return saveXISFImage(newFilename);
960 }
961
962 // if (HasDebayer && m_Filename.isEmpty() == false)
963 // {
964 // fits_flush_file(fptr, &status);
965 // /* close current file */
966 // if (fits_close_file(fptr, &status))
967 // {
968 // recordLastError(status);
969 // return status;
970 // }
971
972 // // Skip "!" in the beginning of the new file name
973 // QString finalFileName(newFilename);
974
975 // finalFileName.remove('!');
976
977 // // Remove first otherwise copy will fail below if file exists
978 // QFile::remove(finalFileName);
979
980 // if (!QFile::copy(m_Filename, finalFileName))
981 // {
982 // qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName;
983 // fptr = nullptr;
984 // return false;
985 // }
986
987 // m_Filename = finalFileName;
988
989 // // Use open diskfile as it does not use extended file names which has problems opening
990 // // files with [ ] or ( ) in their names.
991 // fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status);
992 // fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status);
993
994 // return true;
995 // }
996
997 // Read the image back into buffer in case we debyayed
998 nelements = m_Statistics.samples_per_channel * m_Statistics.channels;
999
1000 /* close current file */
1001 if (fptr && fits_close_file(fptr, &status))
1002 {
1003 m_LastError = i18n("Failed to close file: %1", fitsErrorToString(status));
1004 return false;
1005 }
1006
1007 /* Create a new File, overwriting existing*/
1008 if (fits_create_file(&new_fptr, QString("!%1").arg(newFilename).toLocal8Bit(), &status))
1009 {
1010 m_LastError = i18n("Failed to create file: %1", fitsErrorToString(status));
1011 return status;
1012 }
1013
1014 status = 0;
1015
1016 fptr = new_fptr;
1017
1018 // Create image
1019 long naxis = m_Statistics.channels == 1 ? 2 : 3;
1020 long naxes[3] = {m_Statistics.width, m_Statistics.height, naxis};
1021
1022 // JM 2020-12-28: Here we to use bitpix values
1023 if (fits_create_img(fptr, m_FITSBITPIX, naxis, naxes, &status))
1024 {
1025 m_LastError = i18n("Failed to create image: %1", fitsErrorToString(status));
1026 return false;
1027 }
1028
1029 /* Write keywords */
1030
1031 // Minimum
1032 if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(m_Statistics.min), "Minimum value", &status))
1033 {
1034 m_LastError = i18n("Failed to update key: %1", fitsErrorToString(status));
1035 return false;
1036 }
1037
1038 // Maximum
1039 if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(m_Statistics.max), "Maximum value", &status))
1040 {
1041 m_LastError = i18n("Failed to update key: %1", fitsErrorToString(status));
1042 return false;
1043 }
1044
1045 // KStars Min, for 3 channels
1046 fits_write_key(fptr, TDOUBLE, "MIN1", &m_Statistics.min[0], "Min Channel 1", &status);
1047 if (channels() > 1)
1048 {
1049 fits_write_key(fptr, TDOUBLE, "MIN2", &m_Statistics.min[1], "Min Channel 2", &status);
1050 fits_write_key(fptr, TDOUBLE, "MIN3", &m_Statistics.min[2], "Min Channel 3", &status);
1051 }
1052
1053 // KStars max, for 3 channels
1054 fits_write_key(fptr, TDOUBLE, "MAX1", &m_Statistics.max[0], "Max Channel 1", &status);
1055 if (channels() > 1)
1056 {
1057 fits_write_key(fptr, TDOUBLE, "MAX2", &m_Statistics.max[1], "Max Channel 2", &status);
1058 fits_write_key(fptr, TDOUBLE, "MAX3", &m_Statistics.max[2], "Max Channel 3", &status);
1059 }
1060
1061 // Mean
1062 if (m_Statistics.mean[0] > 0)
1063 {
1064 fits_write_key(fptr, TDOUBLE, "MEAN1", &m_Statistics.mean[0], "Mean Channel 1", &status);
1065 if (channels() > 1)
1066 {
1067 fits_write_key(fptr, TDOUBLE, "MEAN2", &m_Statistics.mean[1], "Mean Channel 2", &status);
1068 fits_write_key(fptr, TDOUBLE, "MEAN3", &m_Statistics.mean[2], "Mean Channel 3", &status);
1069 }
1070 }
1071
1072 // Median
1073 if (m_Statistics.median[0] > 0)
1074 {
1075 fits_write_key(fptr, TDOUBLE, "MEDIAN1", &m_Statistics.median[0], "Median Channel 1", &status);
1076 if (channels() > 1)
1077 {
1078 fits_write_key(fptr, TDOUBLE, "MEDIAN2", &m_Statistics.median[1], "Median Channel 2", &status);
1079 fits_write_key(fptr, TDOUBLE, "MEDIAN3", &m_Statistics.median[2], "Median Channel 3", &status);
1080 }
1081 }
1082
1083 // Standard Deviation
1084 if (m_Statistics.stddev[0] > 0)
1085 {
1086 fits_write_key(fptr, TDOUBLE, "STDDEV1", &m_Statistics.stddev[0], "Standard Deviation Channel 1", &status);
1087 if (channels() > 1)
1088 {
1089 fits_write_key(fptr, TDOUBLE, "STDDEV2", &m_Statistics.stddev[1], "Standard Deviation Channel 2", &status);
1090 fits_write_key(fptr, TDOUBLE, "STDDEV3", &m_Statistics.stddev[2], "Standard Deviation Channel 3", &status);
1091 }
1092 }
1093
1094 // Skip first 10 standard records and copy the rest.
1095 for (int i = 10; i < m_HeaderRecords.count(); i++)
1096 {
1097 QString key = m_HeaderRecords[i].key;
1098 const char *comment = m_HeaderRecords[i].comment.toLatin1().constBegin();
1099 QVariant value = m_HeaderRecords[i].value;
1100
1101 switch (value.type())
1102 {
1103 case QVariant::Int:
1104 {
1105 int number = value.toInt();
1106 fits_write_key(fptr, TINT, key.toLatin1().constData(), &number, comment, &status);
1107 }
1108 break;
1109
1110 case QVariant::Double:
1111 {
1112 double number = value.toDouble();
1113 fits_write_key(fptr, TDOUBLE, key.toLatin1().constData(), &number, comment, &status);
1114 }
1115 break;
1116
1117 case QVariant::String:
1118 default:
1119 {
1120 char valueBuffer[256] = {0};
1121 strncpy(valueBuffer, value.toString().toLatin1().constData(), 256 - 1);
1122 fits_write_key(fptr, TSTRING, key.toLatin1().constData(), valueBuffer, comment, &status);
1123 }
1124 }
1125 }
1126
1127 // ISO Date
1128 if (fits_write_date(fptr, &status))
1129 {
1130 m_LastError = i18n("Failed to update date: %1", fitsErrorToString(status));
1131 return false;
1132 }
1133
1134 QString history =
1135 QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"));
1136 // History
1137 if (fits_write_history(fptr, history.toLatin1(), &status))
1138 {
1139 m_LastError = i18n("Failed to update history: %1", fitsErrorToString(status));
1140 return false;
1141 }
1142
1143 int rot = 0, mirror = 0;
1144 if (rotCounter > 0)
1145 {
1146 rot = (90 * rotCounter) % 360;
1147 if (rot < 0)
1148 rot += 360;
1149 }
1150 if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0)
1151 mirror = 1;
1152
1153 if ((rot != 0) || (mirror != 0))
1154 rotWCSFITS(rot, mirror);
1155
1156 rotCounter = flipHCounter = flipVCounter = 0;
1157
1158 // Here we need to use the actual data type
1159 if (fits_write_img(fptr, m_Statistics.dataType, 1, nelements, m_ImageBuffer, &status))
1160 {
1161 m_LastError = i18n("Failed to write image: %1", fitsErrorToString(status));
1162 return false;
1163 }
1164
1165 m_Filename = newFilename;
1166
1167 fits_flush_file(fptr, &status);
1168
1169 qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename;
1170
1171 return true;
1172}
1173
1174void FITSData::clearImageBuffers()
1175{
1176 delete[] m_ImageBuffer;
1177 m_ImageBuffer = nullptr;
1178 if(m_ImageRoiBuffer != nullptr )
1179 {
1180 delete[] m_ImageRoiBuffer;
1181 m_ImageRoiBuffer = nullptr;
1182
1183 }
1184 //m_BayerBuffer = nullptr;
1185}
1186
1187void FITSData::makeRoiBuffer(QRect roi)
1188{
1189 if (!roi.isValid())
1190 return;
1191
1192 uint32_t channelSize = roi.height() * roi.width();
1193 if(channelSize > m_Statistics.samples_per_channel || channelSize == 1)
1194 {
1195 return;
1196 }
1197 if(m_ImageRoiBuffer != nullptr )
1198 {
1199 delete[] m_ImageRoiBuffer;
1200 m_ImageRoiBuffer = nullptr;
1201 }
1202 int xoffset = roi.topLeft().x() - 1;
1203 int yoffset = roi.topLeft().y() - 1;
1204 uint32_t bpp = m_Statistics.bytesPerPixel;
1205 m_ImageRoiBuffer = new uint8_t[roi.height()*roi.width()*m_Statistics.channels * m_Statistics.bytesPerPixel]();
1206 for(int n = 0 ; n < m_Statistics.channels ; n++)
1207 {
1208 for(int i = 0; i < roi.height(); i++)
1209 {
1210 size_t i1 = n * channelSize * bpp + i * roi.width() * bpp;
1211 size_t i2 = n * m_Statistics.samples_per_channel * bpp + (yoffset + i) * width() * bpp + xoffset * bpp;
1212 memcpy(&m_ImageRoiBuffer[i1],
1213 &m_ImageBuffer[i2],
1214 roi.width() * bpp);
1215 }
1216
1217 }
1218 memcpy(&m_ROIStatistics, &m_Statistics, sizeof(FITSImage::Statistic));
1219 m_ROIStatistics.samples_per_channel = roi.height() * roi.width();
1220 m_ROIStatistics.width = roi.width();
1221 m_ROIStatistics.height = roi.height();
1222 calculateStats(false, true);
1223}
1224void FITSData::calculateStats(bool refresh, bool roi)
1225{
1226 // Calculate min max
1227 if(roi == false)
1228 {
1229 calculateMinMax(refresh);
1230 calculateMedian(refresh);
1231
1232 // Try to read mean/median/stddev if in file
1233 if (refresh == false && fptr)
1234 {
1235 int status = 0;
1236 int nfound = 0;
1237 if (fits_read_key_dbl(fptr, "MEAN1", &m_Statistics.mean[0], nullptr, &status) == 0)
1238 nfound++;
1239 // NB. These could fail if missing, which is OK.
1240 fits_read_key_dbl(fptr, "MEAN2", & m_Statistics.mean[1], nullptr, &status);
1241 fits_read_key_dbl(fptr, "MEAN3", &m_Statistics.mean[2], nullptr, &status);
1242
1243 status = 0;
1244 if (fits_read_key_dbl(fptr, "STDDEV1", &m_Statistics.stddev[0], nullptr, &status) == 0)
1245 nfound++;
1246 // NB. These could fail if missing, which is OK.
1247 fits_read_key_dbl(fptr, "STDDEV2", &m_Statistics.stddev[1], nullptr, &status);
1248 fits_read_key_dbl(fptr, "STDDEV3", &m_Statistics.stddev[2], nullptr, &status);
1249
1250 // If all is OK, we're done
1251 if (nfound == 2)
1252 return;
1253 }
1254
1255 // Get standard deviation and mean in one run
1256 switch (m_Statistics.dataType)
1257 {
1258 case TBYTE:
1259 calculateStdDev<uint8_t>();
1260 break;
1261
1262 case TSHORT:
1263 calculateStdDev<int16_t>();
1264 break;
1265
1266 case TUSHORT:
1267 calculateStdDev<uint16_t>();
1268 break;
1269
1270 case TLONG:
1271 calculateStdDev<int32_t>();
1272 break;
1273
1274 case TULONG:
1275 calculateStdDev<uint32_t>();
1276 break;
1277
1278 case TFLOAT:
1279 calculateStdDev<float>();
1280 break;
1281
1282 case TLONGLONG:
1283 calculateStdDev<int64_t>();
1284 break;
1285
1286 case TDOUBLE:
1287 calculateStdDev<double>();
1288 break;
1289
1290 default:
1291 return;
1292 }
1293
1294 // FIXME That's not really SNR, must implement a proper solution for this value
1295 m_Statistics.SNR = m_Statistics.mean[0] / m_Statistics.stddev[0];
1296 }
1297 else
1298 {
1299 calculateMinMax(refresh, roi);
1300 calculateMedian(refresh, roi);
1301
1302 switch (m_ROIStatistics.dataType)
1303 {
1304 case TBYTE:
1305 calculateStdDev<uint8_t>(roi);
1306 break;
1307
1308 case TSHORT:
1309 calculateStdDev<int16_t>(roi);
1310 break;
1311
1312 case TUSHORT:
1313 calculateStdDev<uint16_t>(roi);
1314 break;
1315
1316 case TLONG:
1317 calculateStdDev<int32_t>(roi);
1318 break;
1319
1320 case TULONG:
1321 calculateStdDev<uint32_t>(roi);
1322 break;
1323
1324 case TFLOAT:
1325 calculateStdDev<float>(roi);
1326 break;
1327
1328 case TLONGLONG:
1329 calculateStdDev<int64_t>(roi);
1330 break;
1331
1332 case TDOUBLE:
1333 calculateStdDev<double>(roi);
1334 break;
1335
1336 default:
1337 return;
1338 }
1339 }
1340}
1341
1342void FITSData::calculateMinMax(bool refresh, bool roi)
1343{
1344 if(roi == false)
1345 {
1346 int status, nfound = 0;
1347
1348 status = 0;
1349
1350 // Only fetch from header if we have a single channel
1351 // Otherwise, calculate manually.
1352 if (fptr != nullptr && !refresh)
1353 {
1354
1355 status = 0;
1356
1357 if (fits_read_key_dbl(fptr, "DATAMIN", &(m_Statistics.min[0]), nullptr, &status) == 0)
1358 nfound++;
1359 else if (fits_read_key_dbl(fptr, "MIN1", &(m_Statistics.min[0]), nullptr, &status) == 0)
1360 nfound++;
1361
1362 // NB. These could fail if missing, which is OK.
1363 fits_read_key_dbl(fptr, "MIN2", &m_Statistics.min[1], nullptr, &status);
1364 fits_read_key_dbl(fptr, "MIN3", &m_Statistics.min[2], nullptr, &status);
1365
1366 status = 0;
1367
1368 if (fits_read_key_dbl(fptr, "DATAMAX", &(m_Statistics.max[0]), nullptr, &status) == 0)
1369 nfound++;
1370 else if (fits_read_key_dbl(fptr, "MAX1", &(m_Statistics.max[0]), nullptr, &status) == 0)
1371 nfound++;
1372
1373 // NB. These could fail if missing, which is OK.
1374 fits_read_key_dbl(fptr, "MAX2", &m_Statistics.max[1], nullptr, &status);
1375 fits_read_key_dbl(fptr, "MAX3", &m_Statistics.max[2], nullptr, &status);
1376
1377 // If we found both keywords, no need to calculate them, unless they are both zeros
1378 if (nfound == 2 && !(m_Statistics.min[0] == 0 && m_Statistics.max[0] == 0))
1379 return;
1380 }
1381
1382 m_Statistics.min[0] = 1.0E30;
1383 m_Statistics.max[0] = -1.0E30;
1384
1385 m_Statistics.min[1] = 1.0E30;
1386 m_Statistics.max[1] = -1.0E30;
1387
1388 m_Statistics.min[2] = 1.0E30;
1389 m_Statistics.max[2] = -1.0E30;
1390
1391 switch (m_Statistics.dataType)
1392 {
1393 case TBYTE:
1394 calculateMinMax<uint8_t>();
1395 break;
1396
1397 case TSHORT:
1398 calculateMinMax<int16_t>();
1399 break;
1400
1401 case TUSHORT:
1402 calculateMinMax<uint16_t>();
1403 break;
1404
1405 case TLONG:
1406 calculateMinMax<int32_t>();
1407 break;
1408
1409 case TULONG:
1410 calculateMinMax<uint32_t>();
1411 break;
1412
1413 case TFLOAT:
1414 calculateMinMax<float>();
1415 break;
1416
1417 case TLONGLONG:
1418 calculateMinMax<int64_t>();
1419 break;
1420
1421 case TDOUBLE:
1422 calculateMinMax<double>();
1423 break;
1424
1425 default:
1426 break;
1427 }
1428 }
1429 else
1430 {
1431 m_ROIStatistics.min[0] = 1.0E30;
1432 m_ROIStatistics.max[0] = -1.0E30;
1433
1434 m_ROIStatistics.min[1] = 1.0E30;
1435 m_ROIStatistics.max[1] = -1.0E30;
1436
1437 m_ROIStatistics.min[2] = 1.0E30;
1438 m_ROIStatistics.max[2] = -1.0E30;
1439
1440 switch (m_Statistics.dataType)
1441 {
1442 case TBYTE:
1443 calculateMinMax<uint8_t>(roi);
1444 break;
1445
1446 case TSHORT:
1447 calculateMinMax<int16_t>(roi);
1448 break;
1449
1450 case TUSHORT:
1451 calculateMinMax<uint16_t>(roi);
1452 break;
1453
1454 case TLONG:
1455 calculateMinMax<int32_t>(roi);
1456 break;
1457
1458 case TULONG:
1459 calculateMinMax<uint32_t>(roi);
1460 break;
1461
1462 case TFLOAT:
1463 calculateMinMax<float>(roi);
1464 break;
1465
1466 case TLONGLONG:
1467 calculateMinMax<int64_t>(roi);
1468 break;
1469
1470 case TDOUBLE:
1471 calculateMinMax<double>(roi);
1472 break;
1473
1474 default:
1475 break;
1476 }
1477
1478 }
1479}
1480
1481void FITSData::calculateMedian(bool refresh, bool roi)
1482{
1483 if(!roi)
1484 {
1485 int status, nfound = 0;
1486
1487 status = 0;
1488
1489 // Only fetch from header if we have a single channel
1490 // Otherwise, calculate manually.
1491 if (fptr != nullptr && !refresh)
1492 {
1493 status = 0;
1494 if (fits_read_key_dbl(fptr, "MEDIAN1", &m_Statistics.median[0], nullptr, &status) == 0)
1495 nfound++;
1496
1497 // NB. These could fail if missing, which is OK.
1498 fits_read_key_dbl(fptr, "MEDIAN2", &m_Statistics.median[1], nullptr, &status);
1499 fits_read_key_dbl(fptr, "MEDIAN3", &m_Statistics.median[2], nullptr, &status);
1500
1501 if (nfound == 1)
1502 return;
1503 }
1504
1505 m_Statistics.median[RED_CHANNEL] = 0;
1506 m_Statistics.median[GREEN_CHANNEL] = 0;
1507 m_Statistics.median[BLUE_CHANNEL] = 0;
1508
1509 switch (m_Statistics.dataType)
1510 {
1511 case TBYTE:
1512 calculateMedian<uint8_t>();
1513 break;
1514
1515 case TSHORT:
1516 calculateMedian<int16_t>();
1517 break;
1518
1519 case TUSHORT:
1520 calculateMedian<uint16_t>();
1521 break;
1522
1523 case TLONG:
1524 calculateMedian<int32_t>();
1525 break;
1526
1527 case TULONG:
1528 calculateMedian<uint32_t>();
1529 break;
1530
1531 case TFLOAT:
1532 calculateMedian<float>();
1533 break;
1534
1535 case TLONGLONG:
1536 calculateMedian<int64_t>();
1537 break;
1538
1539 case TDOUBLE:
1540 calculateMedian<double>();
1541 break;
1542
1543 default:
1544 break;
1545 }
1546 }
1547 else
1548 {
1549 m_ROIStatistics.median[RED_CHANNEL] = 0;
1550 m_ROIStatistics.median[GREEN_CHANNEL] = 0;
1551 m_ROIStatistics.median[BLUE_CHANNEL] = 0;
1552
1553 switch (m_ROIStatistics.dataType)
1554 {
1555 case TBYTE:
1556 calculateMedian<uint8_t>(roi);
1557 break;
1558
1559 case TSHORT:
1560 calculateMedian<int16_t>(roi);
1561 break;
1562
1563 case TUSHORT:
1564 calculateMedian<uint16_t>(roi);
1565 break;
1566
1567 case TLONG:
1568 calculateMedian<int32_t>(roi);
1569 break;
1570
1571 case TULONG:
1572 calculateMedian<uint32_t>(roi);
1573 break;
1574
1575 case TFLOAT:
1576 calculateMedian<float>(roi);
1577 break;
1578
1579 case TLONGLONG:
1580 calculateMedian<int64_t>(roi);
1581 break;
1582
1583 case TDOUBLE:
1584 calculateMedian<double>(roi);
1585 break;
1586
1587 default:
1588 break;
1589 }
1590
1591 }
1592}
1593
1594template <typename T>
1595void FITSData::calculateMedian(bool roi)
1596{
1597 auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
1598 const uint32_t maxMedianSize = 500000;
1599 uint32_t medianSize = roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel;
1600 uint8_t downsample = 1;
1601 if (medianSize > maxMedianSize)
1602 {
1603 downsample = (static_cast<double>(medianSize) / maxMedianSize) + 0.999;
1604 medianSize /= downsample;
1605 }
1606 // Ideally samples would be declared like this...
1607 //std::vector<T> samples;
1608 // Unfortunately this doesn't compile on Mac - see the comments in robuststatistics.cpp for more details
1609 // So for now declare samples like this...
1610 std::vector<int32_t> samples;
1611 samples.reserve(medianSize);
1612
1613 for (uint8_t n = 0; n < m_Statistics.channels; n++)
1614 {
1615 auto *oneChannel = buffer + n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1616 for (uint32_t upto = 0; upto < (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1617 upto += downsample)
1618 samples.push_back(oneChannel[upto]);
1619 auto median = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_MEDIAN, samples);
1620 roi ? m_ROIStatistics.median[n] = median : m_Statistics.median[n] = median;
1621 }
1622}
1623
1624template <typename T>
1625QPair<T, T> FITSData::getParitionMinMax(uint32_t start, uint32_t stride, bool roi)
1626{
1627 auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
1628 T min = std::numeric_limits<T>::max();
1629 T max = std::numeric_limits<T>::min();
1630
1631 uint32_t end = start + stride;
1632
1633 for (uint32_t i = start; i < end; i++)
1634 {
1635 min = qMin(buffer[i], min);
1636 max = qMax(buffer[i], max);
1637 // if (buffer[i] < min)
1638 // min = buffer[i];
1639 // else if (buffer[i] > max)
1640 // max = buffer[i];
1641 }
1642
1643 return qMakePair(min, max);
1644}
1645
1646template <typename T>
1647void FITSData::calculateMinMax(bool roi)
1648{
1649 T min = std::numeric_limits<T>::max();
1650 T max = std::numeric_limits<T>::min();
1651
1652
1653 // Create N threads
1654 const uint8_t nThreads = 16;
1655
1656 for (int n = 0; n < m_Statistics.channels; n++)
1657 {
1658 uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1659
1660 // Calculate how many elements we process per thread
1661 uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;
1662
1663 // Calculate the final stride since we can have some left over due to division above
1664 uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
1665 (tStride * nThreads));
1666
1667 // Start location for inspecting elements
1668 uint32_t tStart = cStart;
1669
1670 // List of futures
1671 QList<QFuture<QPair<T, T>>> futures;
1672
1673 for (int i = 0; i < nThreads; i++)
1674 {
1675 // Run threads
1676 futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax<T>, tStart, (i == (nThreads - 1)) ? fStride : tStride,
1677 roi));
1678 tStart += tStride;
1679 }
1680
1681 // Now wait for results
1682 for (int i = 0; i < nThreads; i++)
1683 {
1684 QPair<T, T> result = futures[i].result();
1685 min = qMin(result.first, min);
1686 max = qMax(result.second, max);
1687 }
1688
1689 if(!roi)
1690 {
1691 m_Statistics.min[n] = min;
1692 m_Statistics.max[n] = max;
1693 }
1694 else
1695 {
1696 m_ROIStatistics.min[n] = min;
1697 m_ROIStatistics.max[n] = max;
1698 }
1699 }
1700
1701}
1702
1703// This struct is used when returning results from the threaded getSumAndSquaredSum calculations
1704// used to compute the mean and variance of the image.
1705struct SumData
1706{
1707 double sum;
1708 double squaredSum;
1709 double numSamples;
1710 SumData(double s, double sq, int n) : sum(s), squaredSum(sq), numSamples(n) {}
1711 SumData() : sum(0), squaredSum(0), numSamples(0) {}
1712};
1713
1714template <typename T>
1715SumData getSumAndSquaredSum(uint32_t start, uint32_t stride, uint8_t *buff)
1716{
1717 auto * buffer = reinterpret_cast<T *>(buff);
1718 const uint32_t end = start + stride;
1719 double sum = 0;
1720 double squaredSum = 0;
1721 for (uint32_t i = start; i < end; i++)
1722 {
1723 double sample = buffer[i];
1724 sum += sample;
1725 squaredSum += sample * sample;
1726 }
1727 const double numSamples = end - start;
1728 return SumData(sum, squaredSum, numSamples);
1729}
1730
1731template <typename T>
1732void FITSData::calculateStdDev(bool roi )
1733{
1734 // Create N threads
1735 const uint8_t nThreads = 16;
1736
1737 for (int n = 0; n < m_Statistics.channels; n++)
1738 {
1739 uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1740
1741 // Calculate how many elements we process per thread
1742 uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;
1743
1744 // Calculate the final stride since we can have some left over due to division above
1745 uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
1746 (tStride * nThreads));
1747
1748 // Start location for inspecting elements
1749 uint32_t tStart = cStart;
1750
1751 // List of futures
1752 QList<QFuture<SumData>> futures;
1753
1754 for (int i = 0; i < nThreads; i++)
1755 {
1756 // Run threads
1757 uint8_t *buff = roi ? m_ImageRoiBuffer : m_ImageBuffer;
1758 futures.append(QtConcurrent::run(&getSumAndSquaredSum<T>, tStart,
1759 (i == (nThreads - 1)) ? fStride : tStride, buff));
1760 tStart += tStride;
1761 }
1762
1763 // Now wait for results
1764 double sum = 0, squared_sum = 0;
1765 double numSamples = 0;
1766 for (int i = 0; i < nThreads; i++)
1767 {
1768 const SumData result = futures[i].result();
1769 sum += result.sum;
1770 squared_sum += result.squaredSum;
1771 numSamples += result.numSamples;
1772
1773 }
1774 if (numSamples <= 0) continue;
1775 const double mean = sum / numSamples;
1776 const double variance = squared_sum / numSamples - mean * mean;
1777 if(!roi)
1778 {
1779 m_Statistics.mean[n] = mean;
1780 m_Statistics.stddev[n] = sqrt(variance);
1781 }
1782 else
1783 {
1784 m_ROIStatistics.mean[n] = mean;
1785 m_ROIStatistics.stddev[n] = sqrt(variance);
1786 }
1787 }
1788}
1789
1790QVector<double> FITSData::createGaussianKernel(int size, double sigma)
1791{
1792 QVector<double> kernel(size * size);
1793 kernel.fill(0.0, size * size);
1794
1795 double kernelSum = 0.0;
1796 int fOff = (size - 1) / 2;
1797 double normal = 1.0 / (2.0 * M_PI * sigma * sigma);
1798 for (int y = -fOff; y <= fOff; y++)
1799 {
1800 for (int x = -fOff; x <= fOff; x++)
1801 {
1802 double distance = ((y * y) + (x * x)) / (2.0 * sigma * sigma);
1803 int index = (y + fOff) * size + (x + fOff);
1804 kernel[index] = normal * qExp(-distance);
1805 kernelSum += kernel.at(index);
1806 }
1807 }
1808 for (int y = 0; y < size; y++)
1809 {
1810 for (int x = 0; x < size; x++)
1811 {
1812 int index = y * size + x;
1813 kernel[index] = kernel.at(index) * 1.0 / kernelSum;
1814 }
1815 }
1816
1817 return kernel;
1818}
1819
1820template <typename T>
1821void FITSData::convolutionFilter(const QVector<double> &kernel, int kernelSize)
1822{
1823 T * imagePtr = reinterpret_cast<T *>(m_ImageBuffer);
1824
1825 // Create variable for pixel data for each kernel
1826 T gt = 0;
1827
1828 // This is how much your center pixel is offset from the border of your kernel
1829 int fOff = (kernelSize - 1) / 2;
1830
1831 // Start with the pixel that is offset fOff from top and fOff from the left side
1832 // this is so entire kernel is on your image
1833 for (int offsetY = 0; offsetY < m_Statistics.height; offsetY++)
1834 {
1835 for (int offsetX = 0; offsetX < m_Statistics.width; offsetX++)
1836 {
1837 // reset gray value to 0
1838 gt = 0;
1839 // position of the kernel center pixel
1840 int byteOffset = offsetY * m_Statistics.width + offsetX;
1841
1842 // kernel calculations
1843 for (int filterY = -fOff; filterY <= fOff; filterY++)
1844 {
1845 for (int filterX = -fOff; filterX <= fOff; filterX++)
1846 {
1847 if ((offsetY + filterY) >= 0 && (offsetY + filterY) < m_Statistics.height
1848 && ((offsetX + filterX) >= 0 && (offsetX + filterX) < m_Statistics.width ))
1849 {
1850
1851 int calcOffset = byteOffset + filterX + filterY * m_Statistics.width;
1852 int index = (filterY + fOff) * kernelSize + (filterX + fOff);
1853 double kernelValue = kernel.at(index);
1854 gt += (imagePtr[calcOffset]) * kernelValue;
1855 }
1856 }
1857 }
1858
1859 // set new data in the other byte array for your image data
1860 imagePtr[byteOffset] = gt;
1861 }
1862 }
1863}
1864
1865template <typename T>
1866void FITSData::gaussianBlur(int kernelSize, double sigma)
1867{
1868 // Size must be an odd number!
1869 if (kernelSize % 2 == 0)
1870 {
1871 kernelSize--;
1872 qCInfo(KSTARS_FITS) << "Warning, size must be an odd number, correcting size to " << kernelSize;
1873 }
1874 // Edge must be a positive number!
1875 if (kernelSize < 1)
1876 {
1877 kernelSize = 1;
1878 }
1879
1880 QVector<double> gaussianKernel = createGaussianKernel(kernelSize, sigma);
1881 convolutionFilter<T>(gaussianKernel, kernelSize);
1882}
1883
1884void FITSData::setMinMax(double newMin, double newMax, uint8_t channel)
1885{
1886 m_Statistics.min[channel] = newMin;
1887 m_Statistics.max[channel] = newMax;
1888}
1889
1890bool FITSData::parseHeader()
1891{
1892 char * header = nullptr;
1893 int status = 0, nkeys = 0;
1894
1895 if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status))
1896 {
1897 fits_report_error(stderr, status);
1898 free(header);
1899 return false;
1900 }
1901
1902 m_HeaderRecords.clear();
1903 QString recordList = QString(header);
1904
1905 for (int i = 0; i < nkeys; i++)
1906 {
1907 Record oneRecord;
1908 // Quotes cause issues for simplified below so we're removing them.
1909 QString record = recordList.mid(i * 80, 80).remove("'");
1910 QStringList properties = record.split(QRegExp("[=/]"));
1911 // If it is only a comment
1912 if (properties.size() == 1)
1913 {
1914 oneRecord.key = properties[0].mid(0, 7);
1915 oneRecord.comment = properties[0].mid(8).simplified();
1916 }
1917 else
1918 {
1919 oneRecord.key = properties[0].simplified();
1920 oneRecord.value = properties[1].simplified();
1921 if (properties.size() > 2)
1922 oneRecord.comment = properties[2].simplified();
1923
1924 // Try to guess the value.
1925 // Test for integer & double. If neither, then leave it as "string".
1926 bool ok = false;
1927
1928 // Is it Integer?
1929 oneRecord.value.toInt(&ok);
1930 if (ok)
1931 oneRecord.value.convert(QMetaType::Int);
1932 else
1933 {
1934 // Is it double?
1935 oneRecord.value.toDouble(&ok);
1936 if (ok)
1937 oneRecord.value.convert(QMetaType::Double);
1938 }
1939 }
1940
1941 m_HeaderRecords.append(oneRecord);
1942 }
1943
1944 free(header);
1945
1946 return true;
1947}
1948
1949bool FITSData::getRecordValue(const QString &key, QVariant &value) const
1950{
1951 auto result = std::find_if(m_HeaderRecords.begin(), m_HeaderRecords.end(), [&key](const Record & oneRecord)
1952 {
1953 return (oneRecord.key == key && oneRecord.value.isValid());
1954 });
1955
1956 if (result != m_HeaderRecords.end())
1957 {
1958 value = (*result).value;
1959 return true;
1960 }
1961 return false;
1962}
1963
1964bool FITSData::parseSolution(FITSImage::Solution &solution) const
1965{
1966 dms angleValue;
1967 bool raOK = false, deOK = false, coordOK = false, scaleOK = false;
1968 QVariant value;
1969
1970 // Reset all
1971 solution.fieldWidth = solution.fieldHeight = solution.pixscale = solution.ra = solution.dec = -1;
1972
1973 // RA
1974 if (getRecordValue("OBJCTRA", value))
1975 {
1976 angleValue = dms::fromString(value.toString(), false);
1977 solution.ra = angleValue.Degrees();
1978 raOK = true;
1979 }
1980 else if (getRecordValue("RA", value))
1981 {
1982 solution.ra = value.toDouble(&raOK);
1983 }
1984
1985 // DE
1986 if (getRecordValue("OBJCTDEC", value))
1987 {
1988 angleValue = dms::fromString(value.toString(), true);
1989 solution.dec = angleValue.Degrees();
1990 deOK = true;
1991 }
1992 else if (getRecordValue("DEC", value))
1993 {
1994 solution.dec = value.toDouble(&deOK);
1995 }
1996
1997 coordOK = raOK && deOK;
1998
1999 // PixScale
2000 double scale = -1;
2001 if (getRecordValue("SCALE", value))
2002 {
2003 scale = value.toDouble();
2004 }
2005
2006 double focal_length = -1;
2007 if (getRecordValue("FOCALLEN", value))
2008 {
2009 focal_length = value.toDouble();
2010 }
2011
2012 double pixsize1 = -1, pixsize2 = -1;
2013 // Pixel Size 1
2014 if (getRecordValue("PIXSIZE1", value))
2015 {
2016 pixsize1 = value.toDouble();
2017 }
2018 // Pixel Size 2
2019 if (getRecordValue("PIXSIZE2", value))
2020 {
2021 pixsize2 = value.toDouble();
2022 }
2023
2024 int binx = 1, biny = 1;
2025 // Binning X
2026 if (getRecordValue("XBINNING", value))
2027 {
2028 binx = value.toDouble();
2029 }
2030 // Binning Y
2031 if (getRecordValue("YBINNING", value))
2032 {
2033 biny = value.toDouble();
2034 }
2035
2036 if (pixsize1 > 0 && pixsize2 > 0)
2037 {
2038 // If we have scale, then that's it
2039 if (scale > 0)
2040 {
2041 // Arcsecs per pixel
2042 solution.pixscale = scale;
2043 // Arcmins
2044 solution.fieldWidth = (m_Statistics.width * scale) / 60.0;
2045 // Arcmins, and account for pixel ratio if non-squared.
2046 solution.fieldHeight = (m_Statistics.height * scale * (pixsize2 / pixsize1)) / 60.0;
2047 }
2048 else if (focal_length > 0)
2049 {
2050 // Arcmins
2051 solution.fieldWidth = ((206264.8062470963552 * m_Statistics.width * (pixsize1 / 1000.0)) / (focal_length * binx)) / 60.0;
2052 // Arsmins
2053 solution.fieldHeight = ((206264.8062470963552 * m_Statistics.height * (pixsize2 / 1000.0)) / (focal_length * biny)) / 60.0;
2054 // Arcsecs per pixel
2055 solution.pixscale = (206264.8062470963552 * (pixsize1 / 1000.0)) / (focal_length * binx);
2056 }
2057
2058 scaleOK = true;
2059 }
2060
2061 return (coordOK || scaleOK);
2062}
2063
2064QFuture<bool> FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox)
2065{
2066 if (m_StarFindFuture.isRunning())
2067 m_StarFindFuture.waitForFinished();
2068
2069 starAlgorithm = algorithm;
2070 qDeleteAll(starCenters);
2071 starCenters.clear();
2072 starsSearched = true;
2073
2074 switch (algorithm)
2075 {
2076 case ALGORITHM_SEP:
2077 {
2078 m_StarDetector.reset(new FITSSEPDetector(this));
2079 m_StarDetector->setSettings(m_SourceExtractorSettings);
2080 if (m_Mode == FITS_NORMAL && trackingBox.isNull())
2081 {
2082 if (Options::quickHFR())
2083 {
2084 //Just finds stars in the center 25% of the image.
2085 const int w = getStatistics().width;
2086 const int h = getStatistics().height;
2087 QRect middle(static_cast<int>(w * 0.25), static_cast<int>(h * 0.25), w / 2, h / 2);
2088 m_StarFindFuture = m_StarDetector->findSources(middle);
2089 return m_StarFindFuture;
2090 }
2091 }
2092 m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2093 return m_StarFindFuture;
2094 }
2095
2096 case ALGORITHM_GRADIENT:
2097 default:
2098 {
2099 m_StarDetector.reset(new FITSGradientDetector(this));
2100 m_StarDetector->setSettings(m_SourceExtractorSettings);
2101 m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2102 return m_StarFindFuture;
2103 }
2104
2105 case ALGORITHM_CENTROID:
2106 {
2107#ifndef KSTARS_LITE
2108 m_StarDetector.reset(new FITSCentroidDetector(this));
2109 m_StarDetector->setSettings(m_SourceExtractorSettings);
2110 // We need JMIndex calculated from histogram
2111 if (!isHistogramConstructed())
2112 constructHistogram();
2113 m_StarDetector->configure("JMINDEX", m_JMIndex);
2114 m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2115 return m_StarFindFuture;
2116 }
2117#else
2118 {
2119 m_StarDetector.reset(new FITSCentroidDetector(this));
2120 m_StarFindFuture = starDetector->findSources(trackingBox);
2121 return m_StarFindFuture;
2122 }
2123#endif
2124
2125 case ALGORITHM_THRESHOLD:
2126 {
2127 m_StarDetector.reset(new FITSThresholdDetector(this));
2128 m_StarDetector->setSettings(m_SourceExtractorSettings);
2129 m_StarDetector->configure("THRESHOLD_PERCENTAGE", Options::focusThreshold());
2130 m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2131 return m_StarFindFuture;
2132 }
2133
2134 case ALGORITHM_BAHTINOV:
2135 {
2136 m_StarDetector.reset(new FITSBahtinovDetector(this));
2137 m_StarDetector->setSettings(m_SourceExtractorSettings);
2138 m_StarDetector->configure("NUMBER_OF_AVERAGE_ROWS", Options::focusMultiRowAverage());
2139 m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2140 return m_StarFindFuture;
2141 }
2142 }
2143}
2144
2145int FITSData::filterStars(QSharedPointer<ImageMask> mask)
2146{
2147 if (mask.isNull() == false)
2148 {
2149 starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(),
2150 [&](Edge * edge)
2151 {
2152 return (mask->isVisible(edge->x, edge->y) == false);
2153 }), starCenters.end());
2154 }
2155
2156 return starCenters.count();
2157
2158}
2159
2160double FITSData::getHFR(HFRType type)
2161{
2162 if (starCenters.empty())
2163 return -1;
2164
2165 if (cacheHFR >= 0 && cacheHFRType == type)
2166 return cacheHFR;
2167
2168 m_SelectedHFRStar.invalidate();
2169
2170 if (type == HFR_MAX)
2171 {
2172
2173 int maxVal = 0;
2174 int maxIndex = 0;
2175 for (int i = 0; i < starCenters.count(); i++)
2176 {
2177 if (starCenters[i]->HFR > maxVal)
2178 {
2179 maxIndex = i;
2180 maxVal = starCenters[i]->HFR;
2181 }
2182 }
2183
2184 m_SelectedHFRStar = *starCenters[maxIndex];
2185 cacheHFR = starCenters[maxIndex]->HFR;
2186 cacheHFRType = type;
2187 return cacheHFR;
2188 }
2189 else if (type == HFR_HIGH)
2190 {
2191 // Reject all stars within 10% of border
2192 int minX = width() / 10;
2193 int minY = height() / 10;
2194 int maxX = width() - minX;
2195 int maxY = height() - minY;
2196 starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(), [minX, minY, maxX, maxY](Edge * oneStar)
2197 {
2198 return (oneStar->x < minX || oneStar->x > maxX || oneStar->y < minY || oneStar->y > maxY);
2199 }), starCenters.end());
2200 // Top 5%
2201 if (starCenters.empty())
2202 return -1;
2203
2204 m_SelectedHFRStar = *starCenters[static_cast<int>(starCenters.size() * 0.05)];
2205 cacheHFR = m_SelectedHFRStar.HFR;
2206 cacheHFRType = type;
2207 return cacheHFR;
2208 }
2209 else if (type == HFR_MEDIAN)
2210 {
2211 std::nth_element(starCenters.begin(), starCenters.begin() + starCenters.size() / 2, starCenters.end());
2212 m_SelectedHFRStar = *starCenters[starCenters.size() / 2];
2213
2214 cacheHFR = m_SelectedHFRStar.HFR;
2215 cacheHFRType = type;
2216 return cacheHFR;
2217 }
2218
2219 // We may remove saturated stars from the HFR calculation, if we have enough stars.
2220 // No real way to tell the scale, so only remove saturated stars with range 0 -> 2**16
2221 // for non-byte types. Unsigned types and floating types, or really any pixels whose
2222 // range isn't 0-64 (or 0-255 for bytes) won't have their saturated stars removed.
2223 const int saturationValue = m_Statistics.dataType == TBYTE ? 250 : 50000;
2224 int numSaturated = 0;
2225 for (auto center : starCenters)
2226 if (center->val > saturationValue)
2227 numSaturated++;
2228 const bool removeSaturatedStars = starCenters.size() - numSaturated > 20;
2229 if (removeSaturatedStars && numSaturated > 0)
2230 qCDebug(KSTARS_FITS) << "Removing " << numSaturated << " stars from HFR calculation";
2231
2232 std::vector<double> HFRs;
2233
2234 for (auto center : starCenters)
2235 {
2236 if (removeSaturatedStars && center->val > saturationValue) continue;
2237
2238 if (type == HFR_AVERAGE)
2239 HFRs.push_back(center->HFR);
2240 else
2241 {
2242 // HFR_ADJ_AVERAGE - so adjust the HFR based on the stars brightness
2243 // HFRadj = HFR.erf(sqrt(ln(peak/background)))/(1 - background/peak)
2244 // Sanity check inputs to prevent equation blowing up
2245 if (m_SkyBackground.mean <= 0.0 || center->val < m_SkyBackground.mean)
2246 {
2247 HFRs.push_back(center->HFR);
2248 qCDebug(KSTARS_FITS) << "HFR Adj, sky background " << m_SkyBackground.mean << " star peak " << center->val <<
2249 " not adjusting";
2250 }
2251 else
2252 {
2253 const double a_div_b = center->val / m_SkyBackground.mean;
2254 const double factor = erf(sqrt(log(a_div_b))) / (1 - (1 / a_div_b));
2255 HFRs.push_back(center->HFR * factor);
2256 qCDebug(KSTARS_FITS) << "HFR Adj, brightness adjusted from " << center->HFR << " to " << center->HFR * factor;
2257 }
2258 }
2259 }
2260
2261 auto m = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING,
2262 HFRs, 2);
2263
2264 cacheHFR = m;
2265 cacheHFRType = HFR_AVERAGE;
2266 return cacheHFR;
2267}
2268
2269double FITSData::getHFR(int x, int y, double scale)
2270{
2271 if (starCenters.empty())
2272 return -1;
2273
2274 for (int i = 0; i < starCenters.count(); i++)
2275 {
2276 const int maxDist = std::max(2, static_cast<int>(0.5 + 2 * starCenters[i]->width / scale));
2277 const int dx = std::fabs(starCenters[i]->x - x);
2278 const int dy = std::fabs(starCenters[i]->y - y);
2279 if (dx <= maxDist && dy <= maxDist)
2280 {
2281 m_SelectedHFRStar = *starCenters[i];
2282 return starCenters[i]->HFR;
2283 }
2284 }
2285
2286 return -1;
2287}
2288
2289double FITSData::getEccentricity()
2290{
2291 if (starCenters.empty())
2292 return -1;
2293 if (cacheEccentricity >= 0)
2294 return cacheEccentricity;
2295 std::vector<float> eccs;
2296 for (const auto &s : starCenters)
2297 eccs.push_back(s->ellipticity);
2298 int middle = eccs.size() / 2;
2299 std::nth_element(eccs.begin(), eccs.begin() + middle, eccs.end());
2300 float medianEllipticity = eccs[middle];
2301
2302 // SEP gives us the ellipticity (flattening) directly.
2303 // To get the eccentricity, use this formula:
2304 // e = sqrt(ellipticity * (2 - ellipticity))
2305 // https://en.wikipedia.org/wiki/Eccentricity_(mathematics)#Ellipses
2306 const float eccentricity = sqrt(medianEllipticity * (2 - medianEllipticity));
2307 cacheEccentricity = eccentricity;
2308 return eccentricity;
2309}
2310
2311void FITSData::applyFilter(FITSScale type, uint8_t * image, QVector<double> * min, QVector<double> * max)
2312{
2313 if (type == FITS_NONE)
2314 return;
2315
2316 QVector<double> dataMin(3);
2317 QVector<double> dataMax(3);
2318
2319 if (min)
2320 dataMin = *min;
2321 if (max)
2322 dataMax = *max;
2323
2324 switch (type)
2325 {
2326 case FITS_AUTO_STRETCH:
2327 {
2328 for (int i = 0; i < 3; i++)
2329 {
2330 dataMin[i] = m_Statistics.mean[i] - m_Statistics.stddev[i];
2331 dataMax[i] = m_Statistics.mean[i] + m_Statistics.stddev[i] * 3;
2332 }
2333 }
2334 break;
2335
2336 case FITS_HIGH_CONTRAST:
2337 {
2338 for (int i = 0; i < 3; i++)
2339 {
2340 dataMin[i] = m_Statistics.mean[i] + m_Statistics.stddev[i];
2341 dataMax[i] = m_Statistics.mean[i] + m_Statistics.stddev[i] * 3;
2342 }
2343 }
2344 break;
2345
2346 case FITS_HIGH_PASS:
2347 {
2348 for (int i = 0; i < 3; i++)
2349 {
2350 dataMin[i] = m_Statistics.mean[i];
2351 }
2352 }
2353 break;
2354
2355 default:
2356 break;
2357 }
2358
2359 switch (m_Statistics.dataType)
2360 {
2361 case TBYTE:
2362 {
2363 for (int i = 0; i < 3; i++)
2364 {
2365 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2366 dataMax[i] = dataMax[i] > UINT8_MAX ? UINT8_MAX : dataMax[i];
2367 }
2368 applyFilter<uint8_t>(type, image, &dataMin, &dataMax);
2369 }
2370 break;
2371
2372 case TSHORT:
2373 {
2374 for (int i = 0; i < 3; i++)
2375 {
2376 dataMin[i] = dataMin[i] < INT16_MIN ? INT16_MIN : dataMin[i];
2377 dataMax[i] = dataMax[i] > INT16_MAX ? INT16_MAX : dataMax[i];
2378 }
2379 applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2380 }
2381
2382 break;
2383
2384 case TUSHORT:
2385 {
2386 for (int i = 0; i < 3; i++)
2387 {
2388 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2389 dataMax[i] = dataMax[i] > UINT16_MAX ? UINT16_MAX : dataMax[i];
2390 }
2391 applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2392 }
2393 break;
2394
2395 case TLONG:
2396 {
2397 for (int i = 0; i < 3; i++)
2398 {
2399 dataMin[i] = dataMin[i] < INT_MIN ? INT_MIN : dataMin[i];
2400 dataMax[i] = dataMax[i] > INT_MAX ? INT_MAX : dataMax[i];
2401 }
2402 applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2403 }
2404 break;
2405
2406 case TULONG:
2407 {
2408 for (int i = 0; i < 3; i++)
2409 {
2410 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2411 dataMax[i] = dataMax[i] > UINT_MAX ? UINT_MAX : dataMax[i];
2412 }
2413 applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2414 }
2415 break;
2416
2417 case TFLOAT:
2418 {
2419 for (int i = 0; i < 3; i++)
2420 {
2421 dataMin[i] = dataMin[i] < FLT_MIN ? FLT_MIN : dataMin[i];
2422 dataMax[i] = dataMax[i] > FLT_MAX ? FLT_MAX : dataMax[i];
2423 }
2424 applyFilter<float>(type, image, &dataMin, &dataMax);
2425 }
2426 break;
2427
2428 case TLONGLONG:
2429 {
2430 for (int i = 0; i < 3; i++)
2431 {
2432 dataMin[i] = dataMin[i] < LLONG_MIN ? LLONG_MIN : dataMin[i];
2433 dataMax[i] = dataMax[i] > LLONG_MAX ? LLONG_MAX : dataMax[i];
2434 }
2435
2436 applyFilter<long>(type, image, &dataMin, &dataMax);
2437 }
2438 break;
2439
2440 case TDOUBLE:
2441 {
2442 for (int i = 0; i < 3; i++)
2443 {
2444 dataMin[i] = dataMin[i] < DBL_MIN ? DBL_MIN : dataMin[i];
2445 dataMax[i] = dataMax[i] > DBL_MAX ? DBL_MAX : dataMax[i];
2446 }
2447 applyFilter<double>(type, image, &dataMin, &dataMax);
2448 }
2449
2450 break;
2451
2452 default:
2453 return;
2454 }
2455
2456 if (min != nullptr)
2457 *min = dataMin;
2458 if (max != nullptr)
2459 *max = dataMax;
2460
2461 emit dataChanged();
2462}
2463
2464template <typename T>
2465void FITSData::applyFilter(FITSScale type, uint8_t * targetImage, QVector<double> * targetMin, QVector<double> * targetMax)
2466{
2467 bool calcStats = false;
2468 T * image = nullptr;
2469
2470 if (targetImage)
2471 image = reinterpret_cast<T *>(targetImage);
2472 else
2473 {
2474 image = reinterpret_cast<T *>(m_ImageBuffer);
2475 calcStats = true;
2476 }
2477
2478 T min[3], max[3];
2479 for (int i = 0; i < 3; i++)
2480 {
2481 min[i] = (*targetMin)[i] < std::numeric_limits<T>::min() ? std::numeric_limits<T>::min() : (*targetMin)[i];
2482 max[i] = (*targetMax)[i] > std::numeric_limits<T>::max() ? std::numeric_limits<T>::max() : (*targetMax)[i];
2483 }
2484
2485
2486 // Create N threads
2487 const uint8_t nThreads = 16;
2488
2489 uint32_t width = m_Statistics.width;
2490 uint32_t height = m_Statistics.height;
2491
2492 //QTime timer;
2493 //timer.start();
2494 switch (type)
2495 {
2496 case FITS_AUTO:
2497 case FITS_LINEAR:
2498 case FITS_AUTO_STRETCH:
2499 case FITS_HIGH_CONTRAST:
2500 case FITS_LOG:
2501 case FITS_SQRT:
2502 case FITS_HIGH_PASS:
2503 {
2504 // List of futures
2505 QList<QFuture<void>> futures;
2506 QVector<double> coeff(3);
2507
2508 if (type == FITS_LOG)
2509 {
2510 for (int i = 0; i < 3; i++)
2511 coeff[i] = max[i] / std::log(1 + max[i]);
2512 }
2513 else if (type == FITS_SQRT)
2514 {
2515 for (int i = 0; i < 3; i++)
2516 coeff[i] = max[i] / sqrt(max[i]);
2517 }
2518
2519 for (int n = 0; n < m_Statistics.channels; n++)
2520 {
2521 if (type == FITS_HIGH_PASS)
2522 min[n] = m_Statistics.mean[n];
2523
2524 uint32_t cStart = n * m_Statistics.samples_per_channel;
2525
2526 // Calculate how many elements we process per thread
2527 uint32_t tStride = m_Statistics.samples_per_channel / nThreads;
2528
2529 // Calculate the final stride since we can have some left over due to division above
2530 uint32_t fStride = tStride + (m_Statistics.samples_per_channel - (tStride * nThreads));
2531
2532 T * runningBuffer = image + cStart;
2533
2534 if (type == FITS_LOG)
2535 {
2536 for (int i = 0; i < nThreads; i++)
2537 {
2538 // Run threads
2539 futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2540 coeff, n](T & a)
2541 {
2542 a = qBound(min[n], static_cast<T>(round(coeff[n] * std::log(1 + qBound(min[n], a, max[n])))), max[n]);
2543 }));
2544
2545 runningBuffer += tStride;
2546 }
2547 }
2548 else if (type == FITS_SQRT)
2549 {
2550 for (int i = 0; i < nThreads; i++)
2551 {
2552 // Run threads
2553 futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2554 coeff, n](T & a)
2555 {
2556 a = qBound(min[n], static_cast<T>(round(coeff[n] * a)), max[n]);
2557 }));
2558 }
2559
2560 runningBuffer += tStride;
2561 }
2562 else
2563 {
2564 for (int i = 0; i < nThreads; i++)
2565 {
2566 // Run threads
2567 futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2568 n](T & a)
2569 {
2570 a = qBound(min[n], a, max[n]);
2571 }));
2572 runningBuffer += tStride;
2573 }
2574 }
2575 }
2576
2577 for (int i = 0; i < nThreads * m_Statistics.channels; i++)
2578 futures[i].waitForFinished();
2579
2580 if (calcStats)
2581 {
2582 for (int i = 0; i < 3; i++)
2583 {
2584 m_Statistics.min[i] = min[i];
2585 m_Statistics.max[i] = max[i];
2586 }
2587 calculateStdDev<T>();
2588 }
2589 }
2590 break;
2591
2592 case FITS_EQUALIZE:
2593 {
2594#ifndef KSTARS_LITE
2595 if (!isHistogramConstructed())
2596 constructHistogram();
2597
2598 T bufferVal = 0;
2599
2600 double coeff = 255.0 / (height * width);
2601 uint32_t row = 0;
2602 uint32_t index = 0;
2603
2604 for (int i = 0; i < m_Statistics.channels; i++)
2605 {
2606 uint32_t offset = i * m_Statistics.samples_per_channel;
2607 for (uint32_t j = 0; j < height; j++)
2608 {
2609 row = offset + j * width;
2610 for (uint32_t k = 0; k < width; k++)
2611 {
2612 index = k + row;
2613 bufferVal = (image[index] - min[i]) / m_HistogramBinWidth[i];
2614
2615 if (bufferVal >= m_CumulativeFrequency[i].size())
2616 bufferVal = m_CumulativeFrequency[i].size() - 1;
2617
2618 image[index] = qBound(min[i], static_cast<T>(round(coeff * m_CumulativeFrequency[i][bufferVal])), max[i]);
2619 }
2620 }
2621 }
2622#endif
2623 }
2624 if (calcStats)
2625 calculateStats(true, false);
2626 break;
2627
2628 // Based on http://www.librow.com/articles/article-1
2629 case FITS_MEDIAN:
2630 {
2631 uint8_t BBP = m_Statistics.bytesPerPixel;
2632 auto * extension = new T[(width + 2) * (height + 2)];
2633 // Check memory allocation
2634 if (!extension)
2635 return;
2636 // Create image extension
2637 for (uint32_t ch = 0; ch < m_Statistics.channels; ch++)
2638 {
2639 uint32_t offset = ch * m_Statistics.samples_per_channel;
2640 uint32_t N = width, M = height;
2641
2642 for (uint32_t i = 0; i < M; ++i)
2643 {
2644 memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP);
2645 extension[(N + 2) * (i + 1)] = image[N * i + offset];
2646 extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset];
2647 }
2648 // Fill first line of image extension
2649 memcpy(extension, extension + N + 2, (N + 2) * BBP);
2650 // Fill last line of image extension
2651 memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP);
2652 // Call median filter implementation
2653
2654 N = width + 2;
2655 M = height + 2;
2656
2657 // Move window through all elements of the image
2658 for (uint32_t m = 1; m < M - 1; ++m)
2659 for (uint32_t n = 1; n < N - 1; ++n)
2660 {
2661 // Pick up window elements
2662 int k = 0;
2663 float window[9];
2664
2665 memset(&window[0], 0, 9 * sizeof(float));
2666 for (uint32_t j = m - 1; j < m + 2; ++j)
2667 for (uint32_t i = n - 1; i < n + 2; ++i)
2668 window[k++] = extension[j * N + i];
2669 // Order elements (only half of them)
2670 for (uint32_t j = 0; j < 5; ++j)
2671 {
2672 // Find position of minimum element
2673 int mine = j;
2674 for (uint32_t l = j + 1; l < 9; ++l)
2675 if (window[l] < window[mine])
2676 mine = l;
2677 // Put found minimum element in its place
2678 const float temp = window[j];
2679 window[j] = window[mine];
2680 window[mine] = temp;
2681 }
2682 // Get result - the middle element
2683 image[(m - 1) * (N - 2) + n - 1 + offset] = window[4];
2684 }
2685 }
2686
2687 // Free memory
2688 delete[] extension;
2689
2690 if (calcStats)
2691 calculateStdDev<T>();
2692 }
2693 break;
2694
2695 case FITS_GAUSSIAN:
2696 gaussianBlur<T>(Options::focusGaussianKernelSize(), Options::focusGaussianSigma());
2697 if (calcStats)
2698 calculateStats(true, false);
2699 break;
2700
2701 case FITS_ROTATE_CW:
2702 rotFITS<T>(90, 0);
2703 rotCounter++;
2704 break;
2705
2706 case FITS_ROTATE_CCW:
2707 rotFITS<T>(270, 0);
2708 rotCounter--;
2709 break;
2710
2711 case FITS_MOUNT_FLIP_H:
2712 rotFITS<T>(0, 1);
2713 flipHCounter++;
2714 break;
2715
2716 case FITS_MOUNT_FLIP_V:
2717 rotFITS<T>(0, 2);
2718 flipVCounter++;
2719 break;
2720
2721 default:
2722 break;
2723 }
2724}
2725
2726QList<Edge *> FITSData::getStarCentersInSubFrame(QRect subFrame) const
2727{
2728 QList<Edge *> starCentersInSubFrame;
2729 for (int i = 0; i < starCenters.count(); i++)
2730 {
2731 int x = static_cast<int>(starCenters[i]->x);
2732 int y = static_cast<int>(starCenters[i]->y);
2733 if(subFrame.contains(x, y))
2734 {
2735 starCentersInSubFrame.append(starCenters[i]);
2736 }
2737 }
2738 return starCentersInSubFrame;
2739}
2740
2741bool FITSData::loadWCS()
2742{
2743#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2744
2745 if (m_WCSState == Success)
2746 {
2747 qCWarning(KSTARS_FITS) << "WCS data already loaded";
2748 return true;
2749 }
2750
2751 if (m_WCSHandle != nullptr)
2752 {
2753 wcsvfree(&m_nwcs, &m_WCSHandle);
2754 m_nwcs = 0;
2755 m_WCSHandle = nullptr;
2756 }
2757
2758 qCDebug(KSTARS_FITS) << "Started WCS Data Processing...";
2759
2760 QByteArray header_str;
2761 int status = 0;
2762 int nkeyrec = 0, nreject = 0;
2763 if (fptr)
2764 {
2765 char *header = nullptr;
2766 if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status))
2767 {
2768 char errmsg[512];
2769 fits_get_errstatus(status, errmsg);
2770 m_LastError = errmsg;
2771 m_WCSState = Failure;
2772 return false;
2773 }
2774 header_str = QByteArray(header);
2775 fits_free_memory(header, &status);
2776 }
2777 else
2778 {
2779 nkeyrec = 1;
2780 for(auto &fitsKeyword : m_HeaderRecords)
2781 {
2782 QByteArray rec;
2783 rec.append(fitsKeyword.key.leftJustified(8, ' ').toLatin1());
2784 rec.append("= ");
2785 rec.append(fitsKeyword.value.toByteArray());
2786 rec.append(" / ");
2787 rec.append(fitsKeyword.comment.toLatin1());
2788 header_str.append(rec.leftJustified(80, ' ', true));
2789 nkeyrec++;
2790 }
2791 header_str.append(QByteArray("END").leftJustified(80));
2792 }
2793
2794 if ((status = wcspih(header_str.data(), nkeyrec, WCSHDR_all, 0, &nreject, &m_nwcs, &m_WCSHandle)) != 0)
2795 {
2796 wcsvfree(&m_nwcs, &m_WCSHandle);
2797 m_WCSHandle = nullptr;
2798 m_nwcs = 0;
2799 m_LastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]);
2800 m_WCSState = Failure;
2801 return false;
2802 }
2803
2804 if (m_WCSHandle == nullptr)
2805 {
2806 m_LastError = i18n("No world coordinate systems found.");
2807 m_WCSState = Failure;
2808 return false;
2809 }
2810
2811 // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now.
2812 if (m_WCSHandle->crpix[0] == 0)
2813 {
2814 wcsvfree(&m_nwcs, &m_WCSHandle);
2815 m_WCSHandle = nullptr;
2816 m_nwcs = 0;
2817 m_LastError = i18n("No world coordinate systems found.");
2818 m_WCSState = Failure;
2819 return false;
2820 }
2821
2822 cdfix(m_WCSHandle);
2823 if ((status = wcsset(m_WCSHandle)) != 0)
2824 {
2825 wcsvfree(&m_nwcs, &m_WCSHandle);
2826 m_WCSHandle = nullptr;
2827 m_nwcs = 0;
2828 m_LastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2829 m_WCSState = Failure;
2830 return false;
2831 }
2832
2833 m_ObjectsSearched = false;
2834 m_WCSState = Success;
2835 HasWCS = true;
2836
2837 qCDebug(KSTARS_FITS) << "Finished WCS Data processing...";
2838
2839 return true;
2840#else
2841 return false;
2842#endif
2843}
2844
2845bool FITSData::wcsToPixel(const SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint)
2846{
2847#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2848 int status, stat[NWCSFIX];
2849 double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, worldcrd[NWCSFIX];
2850
2851 if (m_WCSHandle == nullptr)
2852 {
2853 m_LastError = i18n("No world coordinate systems found.");
2854 return false;
2855 }
2856
2857 worldcrd[0] = wcsCoord.ra0().Degrees();
2858 worldcrd[1] = wcsCoord.dec0().Degrees();
2859
2860 if ((status = wcss2p(m_WCSHandle, 1, 2, worldcrd, &phi, &theta, imgcrd, pixcrd, stat)) != 0)
2861 {
2862 m_LastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2863 return false;
2864 }
2865
2866 wcsImagePoint.setX(imgcrd[0]);
2867 wcsImagePoint.setY(imgcrd[1]);
2868
2869 wcsPixelPoint.setX(pixcrd[0]);
2870 wcsPixelPoint.setY(pixcrd[1]);
2871
2872 return true;
2873#else
2874 Q_UNUSED(wcsCoord);
2875 Q_UNUSED(wcsPixelPoint);
2876 Q_UNUSED(wcsImagePoint);
2877 return false;
2878#endif
2879}
2880
2881bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord)
2882{
2883#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2884 int status, stat[NWCSFIX];
2885 double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, world[NWCSFIX];
2886
2887 if (m_WCSHandle == nullptr)
2888 {
2889 m_LastError = i18n("No world coordinate systems found.");
2890 return false;
2891 }
2892
2893 pixcrd[0] = wcsPixelPoint.x();
2894 pixcrd[1] = wcsPixelPoint.y();
2895
2896 if ((status = wcsp2s(m_WCSHandle, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat)) != 0)
2897 {
2898 m_LastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2899 return false;
2900 }
2901 else
2902 {
2903 wcsCoord.setRA0(world[0] / 15.0);
2904 wcsCoord.setDec0(world[1]);
2905 }
2906
2907 return true;
2908#else
2909 Q_UNUSED(wcsPixelPoint);
2910 Q_UNUSED(wcsCoord);
2911 return false;
2912#endif
2913}
2914
2915#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2916bool FITSData::searchObjects()
2917{
2918 if (m_ObjectsSearched)
2919 return true;
2920
2921 m_ObjectsSearched = true;
2922
2923 SkyPoint startPoint;
2924 SkyPoint endPoint;
2925
2926 pixelToWCS(QPointF(0, 0), startPoint);
2927 pixelToWCS(QPointF(width() - 1, height() - 1), endPoint);
2928
2929 return findObjectsInImage(startPoint, endPoint);
2930}
2931
2932bool FITSData::findWCSBounds(double &minRA, double &maxRA, double &minDec, double &maxDec)
2933{
2934 if (m_WCSHandle == nullptr)
2935 {
2936 m_LastError = i18n("No world coordinate systems found.");
2937 return false;
2938 }
2939
2940 maxRA = -1000;
2941 minRA = 1000;
2942 maxDec = -1000;
2943 minDec = 1000;
2944
2945 auto updateMinMax = [&](int x, int y)
2946 {
2947 int stat[2];
2948 double imgcrd[2], phi, pixcrd[2], theta, world[2];
2949
2950 pixcrd[0] = x;
2951 pixcrd[1] = y;
2952
2953 if (wcsp2s(m_WCSHandle, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]))
2954 return;
2955
2956 minRA = std::min(minRA, world[0]);
2957 maxRA = std::max(maxRA, world[0]);
2958 minDec = std::min(minDec, world[1]);
2959 maxDec = std::max(maxDec, world[1]);
2960 };
2961
2962 // Find min and max values from edges
2963 for (int y = 0; y < height(); y++)
2964 {
2965 updateMinMax(0, y);
2966 updateMinMax(width() - 1, y);
2967 }
2968
2969 for (int x = 1; x < width() - 1; x++)
2970 {
2971 updateMinMax(x, 0);
2972 updateMinMax(x, height() - 1);
2973 }
2974
2975 // Check if either pole is in the image
2976 SkyPoint NCP(0, 90);
2977 SkyPoint SCP(0, -90);
2978 QPointF imagePoint, pPoint;
2979 if (wcsToPixel(NCP, pPoint, imagePoint))
2980 {
2981 if (pPoint.x() > 0 && pPoint.x() < width() && pPoint.y() > 0 && pPoint.y() < height())
2982 {
2983 maxDec = 90;
2984 }
2985 }
2986 if (wcsToPixel(SCP, pPoint, imagePoint))
2987 {
2988 if (pPoint.x() > 0 && pPoint.x() < width() && pPoint.y() > 0 && pPoint.y() < height())
2989 {
2990 minDec = -90;
2991 }
2992 }
2993 return true;
2994}
2995#endif
2996
2997#if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2998bool FITSData::findObjectsInImage(SkyPoint startPoint, SkyPoint endPoint)
2999{
3000 if (KStarsData::Instance() == nullptr)
3001 return false;
3002
3003 int w = width();
3004 int h = height();
3005 QVariant date;
3006 KSNumbers * num = nullptr;
3007
3008 if (getRecordValue("DATE-OBS", date))
3009 {
3010 QString tsString(date.toString());
3011 tsString = tsString.remove('\'').trimmed();
3012 // Add Zulu time to indicate UTC
3013 tsString += "Z";
3014
3016
3017 if (ts.isValid())
3018 num = new KSNumbers(KStarsDateTime(ts).djd());
3019 }
3020
3021 //Set to current time if the above does not work.
3022 if (num == nullptr)
3023 num = new KSNumbers(KStarsData::Instance()->ut().djd());
3024
3025 startPoint.updateCoordsNow(num);
3026 endPoint.updateCoordsNow(num);
3027
3028 m_SkyObjects.clear();
3029
3030 QList<SkyObject *> list = KStarsData::Instance()->skyComposite()->findObjectsInArea(startPoint, endPoint);
3031 list.erase(std::remove_if(list.begin(), list.end(), [](SkyObject * oneObject)
3032 {
3033 int type = oneObject->type();
3034 return (type == SkyObject::STAR || type == SkyObject::PLANET || type == SkyObject::ASTEROID ||
3035 type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON ||
3036 type == SkyObject::SATELLITE);
3037 }), list.end());
3038
3039 double world[2], phi, theta, imgcrd[2], pixcrd[2];
3040 int stat[2];
3041 for (auto &object : list)
3042 {
3043 world[0] = object->ra0().Degrees();
3044 world[1] = object->dec0().Degrees();
3045
3046 if (wcss2p(m_WCSHandle, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0]) == 0)
3047 {
3048 //The X and Y are set to the found position if it does work.
3049 int x = pixcrd[0];
3050 int y = pixcrd[1];
3051 if (x > 0 && y > 0 && x < w && y < h)
3052 m_SkyObjects.append(new FITSSkyObject(object, x, y));
3053 }
3054 }
3055
3056 delete (num);
3057 return true;
3058}
3059#endif
3060
3061int FITSData::getFlipVCounter() const
3062{
3063 return flipVCounter;
3064}
3065
3066void FITSData::setFlipVCounter(int value)
3067{
3068 flipVCounter = value;
3069}
3070
3071int FITSData::getFlipHCounter() const
3072{
3073 return flipHCounter;
3074}
3075
3076void FITSData::setFlipHCounter(int value)
3077{
3078 flipHCounter = value;
3079}
3080
3081int FITSData::getRotCounter() const
3082{
3083 return rotCounter;
3084}
3085
3086void FITSData::setRotCounter(int value)
3087{
3088 rotCounter = value;
3089}
3090
3091/* Rotate an image by 90, 180, or 270 degrees, with an optional
3092 * reflection across the vertical or horizontal axis.
3093 * verbose generates extra info on stdout.
3094 * return nullptr if successful or rotated image.
3095 */
3096template <typename T>
3097bool FITSData::rotFITS(int rotate, int mirror)
3098{
3099 int ny, nx;
3100 int x1, y1, x2, y2;
3101 uint8_t * rotimage = nullptr;
3102 int offset = 0;
3103
3104 if (rotate == 1)
3105 rotate = 90;
3106 else if (rotate == 2)
3107 rotate = 180;
3108 else if (rotate == 3)
3109 rotate = 270;
3110 else if (rotate < 0)
3111 rotate = rotate + 360;
3112
3113 nx = m_Statistics.width;
3114 ny = m_Statistics.height;
3115
3116 int BBP = m_Statistics.bytesPerPixel;
3117
3118 /* Allocate buffer for rotated image */
3119 rotimage = new uint8_t[m_Statistics.samples_per_channel * m_Statistics.channels * BBP];
3120
3121 if (rotimage == nullptr)
3122 {
3123 qWarning() << "Unable to allocate memory for rotated image buffer!";
3124 return false;
3125 }
3126
3127 auto * rotBuffer = reinterpret_cast<T *>(rotimage);
3128 auto * buffer = reinterpret_cast<T *>(m_ImageBuffer);
3129
3130 /* Mirror image without rotation */
3131 if (rotate < 45 && rotate > -45)
3132 {
3133 if (mirror == 1)
3134 {
3135 for (int i = 0; i < m_Statistics.channels; i++)
3136 {
3137 offset = m_Statistics.samples_per_channel * i;
3138 for (x1 = 0; x1 < nx; x1++)
3139 {
3140 x2 = nx - x1 - 1;
3141 for (y1 = 0; y1 < ny; y1++)
3142 rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3143 }
3144 }
3145 }
3146 else if (mirror == 2)
3147 {
3148 for (int i = 0; i < m_Statistics.channels; i++)
3149 {
3150 offset = m_Statistics.samples_per_channel * i;
3151 for (y1 = 0; y1 < ny; y1++)
3152 {
3153 y2 = ny - y1 - 1;
3154 for (x1 = 0; x1 < nx; x1++)
3155 rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3156 }
3157 }
3158 }
3159 else
3160 {
3161 for (int i = 0; i < m_Statistics.channels; i++)
3162 {
3163 offset = m_Statistics.samples_per_channel * i;
3164 for (y1 = 0; y1 < ny; y1++)
3165 {
3166 for (x1 = 0; x1 < nx; x1++)
3167 rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3168 }
3169 }
3170 }
3171 }
3172
3173 /* Rotate by 90 degrees */
3174 else if (rotate >= 45 && rotate < 135)
3175 {
3176 if (mirror == 1)
3177 {
3178 for (int i = 0; i < m_Statistics.channels; i++)
3179 {
3180 offset = m_Statistics.samples_per_channel * i;
3181 for (y1 = 0; y1 < ny; y1++)
3182 {
3183 x2 = ny - y1 - 1;
3184 for (x1 = 0; x1 < nx; x1++)
3185 {
3186 y2 = nx - x1 - 1;
3187 rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3188 }
3189 }
3190 }
3191 }
3192 else if (mirror == 2)
3193 {
3194 for (int i = 0; i < m_Statistics.channels; i++)
3195 {
3196 offset = m_Statistics.samples_per_channel * i;
3197 for (y1 = 0; y1 < ny; y1++)
3198 {
3199 for (x1 = 0; x1 < nx; x1++)
3200 rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset];
3201 }
3202 }
3203 }
3204 else
3205 {
3206 for (int i = 0; i < m_Statistics.channels; i++)
3207 {
3208 offset = m_Statistics.samples_per_channel * i;
3209 for (y1 = 0; y1 < ny; y1++)
3210 {
3211 x2 = ny - y1 - 1;
3212 for (x1 = 0; x1 < nx; x1++)
3213 {
3214 y2 = x1;
3215 rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3216 }
3217 }
3218 }
3219 }
3220
3221 m_Statistics.width = ny;
3222 m_Statistics.height = nx;
3223 }
3224
3225 /* Rotate by 180 degrees */
3226 else if (rotate >= 135 && rotate < 225)
3227 {
3228 if (mirror == 1)
3229 {
3230 for (int i = 0; i < m_Statistics.channels; i++)
3231 {
3232 offset = m_Statistics.samples_per_channel * i;
3233 for (y1 = 0; y1 < ny; y1++)
3234 {
3235 y2 = ny - y1 - 1;
3236 for (x1 = 0; x1 < nx; x1++)
3237 rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3238 }
3239 }
3240 }
3241 else if (mirror == 2)
3242 {
3243 for (int i = 0; i < m_Statistics.channels; i++)
3244 {
3245 offset = m_Statistics.samples_per_channel * i;
3246 for (x1 = 0; x1 < nx; x1++)
3247 {
3248 x2 = nx - x1 - 1;
3249 for (y1 = 0; y1 < ny; y1++)
3250 rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3251 }
3252 }
3253 }
3254 else
3255 {
3256 for (int i = 0; i < m_Statistics.channels; i++)
3257 {
3258 offset = m_Statistics.samples_per_channel * i;
3259 for (y1 = 0; y1 < ny; y1++)
3260 {
3261 y2 = ny - y1 - 1;
3262 for (x1 = 0; x1 < nx; x1++)
3263 {
3264 x2 = nx - x1 - 1;
3265 rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3266 }
3267 }
3268 }
3269 }
3270 }
3271
3272 /* Rotate by 270 degrees */
3273 else if (rotate >= 225 && rotate < 315)
3274 {
3275 if (mirror == 1)
3276 {
3277 for (int i = 0; i < m_Statistics.channels; i++)
3278 {
3279 offset = m_Statistics.samples_per_channel * i;
3280 for (y1 = 0; y1 < ny; y1++)
3281 {
3282 for (x1 = 0; x1 < nx; x1++)
3283 rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset];
3284 }
3285 }
3286 }
3287 else if (mirror == 2)
3288 {
3289 for (int i = 0; i < m_Statistics.channels; i++)
3290 {
3291 offset = m_Statistics.samples_per_channel * i;
3292 for (y1 = 0; y1 < ny; y1++)
3293 {
3294 x2 = ny - y1 - 1;
3295 for (x1 = 0; x1 < nx; x1++)
3296 {
3297 y2 = nx - x1 - 1;
3298 rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3299 }
3300 }
3301 }
3302 }
3303 else
3304 {
3305 for (int i = 0; i < m_Statistics.channels; i++)
3306 {
3307 offset = m_Statistics.samples_per_channel * i;
3308 for (y1 = 0; y1 < ny; y1++)
3309 {
3310 x2 = y1;
3311 for (x1 = 0; x1 < nx; x1++)
3312 {
3313 y2 = nx - x1 - 1;
3314 rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3315 }
3316 }
3317 }
3318 }
3319
3320 m_Statistics.width = ny;
3321 m_Statistics.height = nx;
3322 }
3323
3324 /* If rotating by more than 315 degrees, assume top-bottom reflection */
3325 else if (rotate >= 315 && mirror)
3326 {
3327 for (int i = 0; i < m_Statistics.channels; i++)
3328 {
3329 offset = m_Statistics.samples_per_channel * i;
3330 for (y1 = 0; y1 < ny; y1++)
3331 {
3332 for (x1 = 0; x1 < nx; x1++)
3333 {
3334 x2 = y1;
3335 y2 = x1;
3336 rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3337 }
3338 }
3339 }
3340 }
3341
3342 delete[] m_ImageBuffer;
3343 m_ImageBuffer = rotimage;
3344
3345 return true;
3346}
3347
3348void FITSData::rotWCSFITS(int angle, int mirror)
3349{
3350 int status = 0;
3351 char comment[100];
3352 double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2;
3353 int WCS_DECIMALS = 6;
3354
3355 naxis1 = m_Statistics.width;
3356 naxis2 = m_Statistics.height;
3357
3358 if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3359 {
3360 // No WCS keywords
3361 return;
3362 }
3363
3364 /* Reset CROTAn and CD matrix if axes have been exchanged */
3365 if (angle == 90)
3366 {
3367 if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status))
3368 fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status);
3369
3370 if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status))
3371 fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status);
3372 }
3373
3374 status = 0;
3375
3376 /* Negate rotation angle if mirrored */
3377 if (mirror != 0)
3378 {
3379 if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status))
3380 fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status);
3381
3382 if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status))
3383 fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status);
3384
3385 status = 0;
3386
3387 if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status))
3388 fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3389
3390 status = 0;
3391
3392 if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3393 fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3394
3395 if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status))
3396 fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status);
3397
3398 if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status))
3399 fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status);
3400 }
3401
3402 status = 0;
3403
3404 /* Unbin CRPIX and CD matrix */
3405 if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status))
3406 {
3407 if (ctemp1 != 1.0)
3408 {
3409 if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status))
3410 if (ctemp1 == ctemp2)
3411 {
3412 double ltv1 = 0.0;
3413 double ltv2 = 0.0;
3414 status = 0;
3415 if (!fits_read_key_dbl(fptr, "LTV1", &ltv1, comment, &status))
3416 fits_delete_key(fptr, "LTV1", &status);
3417 if (!fits_read_key_dbl(fptr, "LTV2", &ltv2, comment, &status))
3418 fits_delete_key(fptr, "LTV2", &status);
3419
3420 status = 0;
3421
3422 if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status))
3423 fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status);
3424
3425 if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status))
3426 fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status);
3427
3428 status = 0;
3429
3430 if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status))
3431 fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3432
3433 if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status))
3434 fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3435
3436 if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status))
3437 fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3438
3439 if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status))
3440 fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3441
3442 status = 0;
3443
3444 fits_delete_key(fptr, "LTM1_1", &status);
3445 fits_delete_key(fptr, "LTM1_2", &status);
3446 }
3447 }
3448 }
3449
3450 status = 0;
3451
3452 /* Reset CRPIXn */
3453 if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) &&
3454 !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status))
3455 {
3456 if (mirror != 0)
3457 {
3458 if (angle == 0)
3459 fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3460 else if (angle == 90)
3461 {
3462 fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3463 fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3464 }
3465 else if (angle == 180)
3466 {
3467 fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status);
3468 fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3469 }
3470 else if (angle == 270)
3471 {
3472 fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status);
3473 fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status);
3474 }
3475 }
3476 else
3477 {
3478 if (angle == 90)
3479 {
3480 fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3481 fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status);
3482 }
3483 else if (angle == 180)
3484 {
3485 fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3486 fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3487 }
3488 else if (angle == 270)
3489 {
3490 fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status);
3491 fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3492 }
3493 }
3494 }
3495
3496 status = 0;
3497
3498 /* Reset CDELTn (degrees per pixel) */
3499 if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) &&
3500 !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status))
3501 {
3502 if (mirror != 0)
3503 {
3504 if (angle == 0)
3505 fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status);
3506 else if (angle == 90)
3507 {
3508 fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status);
3509 fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status);
3510 }
3511 else if (angle == 180)
3512 {
3513 fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status);
3514 fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status);
3515 }
3516 else if (angle == 270)
3517 {
3518 fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status);
3519 fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status);
3520 }
3521 }
3522 else
3523 {
3524 if (angle == 90)
3525 {
3526 fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status);
3527 fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status);
3528 }
3529 else if (angle == 180)
3530 {
3531 fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status);
3532 fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status);
3533 }
3534 else if (angle == 270)
3535 {
3536 fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status);
3537 fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status);
3538 }
3539 }
3540 }
3541
3542 /* Reset CD matrix, if present */
3543 ctemp1 = 0.0;
3544 ctemp2 = 0.0;
3545 ctemp3 = 0.0;
3546 ctemp4 = 0.0;
3547 status = 0;
3548 if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3549 {
3550 fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status);
3551 fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status);
3552 fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status);
3553 status = 0;
3554 if (mirror != 0)
3555 {
3556 if (angle == 0)
3557 {
3558 fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status);
3559 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3560 }
3561 else if (angle == 90)
3562 {
3563 fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status);
3564 fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status);
3565 fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status);
3566 fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status);
3567 }
3568 else if (angle == 180)
3569 {
3570 fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status);
3571 fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status);
3572 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3573 fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status);
3574 }
3575 else if (angle == 270)
3576 {
3577 fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status);
3578 fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status);
3579 fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status);
3580 fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status);
3581 }
3582 }
3583 else
3584 {
3585 if (angle == 90)
3586 {
3587 fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status);
3588 fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status);
3589 fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status);
3590 fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status);
3591 }
3592 else if (angle == 180)
3593 {
3594 fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3595 fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status);
3596 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3597 fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status);
3598 }
3599 else if (angle == 270)
3600 {
3601 fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status);
3602 fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status);
3603 fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status);
3604 fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status);
3605 }
3606 }
3607 }
3608
3609 /* Delete any polynomial solution */
3610 /* (These could maybe be switched, but I don't want to work them out yet */
3611 status = 0;
3612 if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status))
3613 {
3614 int i;
3615 char keyword[16];
3616
3617 for (i = 1; i < 13; i++)
3618 {
3619 sprintf(keyword, "CO1_%d", i);
3620 fits_delete_key(fptr, keyword, &status);
3621 }
3622 for (i = 1; i < 13; i++)
3623 {
3624 sprintf(keyword, "CO2_%d", i);
3625 fits_delete_key(fptr, keyword, &status);
3626 }
3627 }
3628
3629}
3630
3631uint8_t * FITSData::getWritableImageBuffer()
3632{
3633 return m_ImageBuffer;
3634}
3635
3636uint8_t const * FITSData::getImageBuffer() const
3637{
3638 return m_ImageBuffer;
3639}
3640
3641void FITSData::setImageBuffer(uint8_t * buffer)
3642{
3643 delete[] m_ImageBuffer;
3644 m_ImageBuffer = buffer;
3645}
3646
3647bool FITSData::checkDebayer()
3648{
3649 int status = 0;
3650 char bayerPattern[64], roworder[64];
3651
3652 // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image
3653 if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status))
3654 return false;
3655
3656 if (m_Statistics.dataType != TUSHORT && m_Statistics.dataType != TBYTE)
3657 {
3658 m_LastError = i18n("Only 8 and 16 bits bayered images supported.");
3659 return false;
3660 }
3661 QString pattern(bayerPattern);
3662 pattern = pattern.remove('\'').trimmed();
3663
3664 QString order(roworder);
3665 order = order.remove('\'').trimmed();
3666
3667 if (order == "BOTTOM-UP" && !(m_Statistics.height % 2))
3668 {
3669 if (pattern == "RGGB")
3670 pattern = "GBRG";
3671 else if (pattern == "GBRG")
3672 pattern = "RGGB";
3673 else if (pattern == "GRBG")
3674 pattern = "BGGR";
3675 else if (pattern == "BGGR")
3676 pattern = "GRBG";
3677 else return false;
3678 }
3679
3680 if (pattern == "RGGB")
3681 debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
3682 else if (pattern == "GBRG")
3683 debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
3684 else if (pattern == "GRBG")
3685 debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
3686 else if (pattern == "BGGR")
3687 debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
3688 // We return unless we find a valid pattern
3689 else
3690 {
3691 m_LastError = i18n("Unsupported bayer pattern %1.", pattern);
3692 return false;
3693 }
3694
3695 fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status);
3696 fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status);
3697
3698 if (debayerParams.offsetX == 1)
3699 {
3700 // This may leave odd values in the 0th column if the color filter is not there
3701 // in the sensor, but otherwise should process the offset correctly.
3702 // Only offsets of 0 or 1 are implemented in debayer_8bit() and debayer_16bit().
3703 switch (debayerParams.filter)
3704 {
3705 case DC1394_COLOR_FILTER_RGGB:
3706 debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
3707 break;
3708 case DC1394_COLOR_FILTER_GBRG:
3709 debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
3710 break;
3711 case DC1394_COLOR_FILTER_GRBG:
3712 debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
3713 break;
3714 case DC1394_COLOR_FILTER_BGGR:
3715 debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
3716 break;
3717 }
3718 debayerParams.offsetX = 0;
3719 }
3720 if (debayerParams.offsetX != 0 || debayerParams.offsetY > 1 || debayerParams.offsetY < 0)
3721 {
3722 m_LastError = i18n("Unsupported bayer offsets %1 %2.", debayerParams.offsetX, debayerParams.offsetY);
3723 return false;
3724 }
3725
3726 HasDebayer = true;
3727
3728 return true;
3729}
3730
3731void FITSData::getBayerParams(BayerParams * param)
3732{
3733 param->method = debayerParams.method;
3734 param->filter = debayerParams.filter;
3735 param->offsetX = debayerParams.offsetX;
3736 param->offsetY = debayerParams.offsetY;
3737}
3738
3739void FITSData::setBayerParams(BayerParams * param)
3740{
3741 debayerParams.method = param->method;
3742 debayerParams.filter = param->filter;
3743 debayerParams.offsetX = param->offsetX;
3744 debayerParams.offsetY = param->offsetY;
3745}
3746
3747bool FITSData::debayer(bool reload)
3748{
3749 if (reload)
3750 {
3751 int anynull = 0, status = 0;
3752
3753 if (fits_read_img(fptr, m_Statistics.dataType, 1, m_Statistics.samples_per_channel, nullptr, m_ImageBuffer,
3754 &anynull, &status))
3755 {
3756 // char errmsg[512];
3757 // fits_get_errstatus(status, errmsg);
3758 // KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error"));
3759 return false;
3760 }
3761 }
3762
3763 switch (m_Statistics.dataType)
3764 {
3765 case TBYTE:
3766 return debayer_8bit();
3767
3768 case TUSHORT:
3769 return debayer_16bit();
3770
3771 default:
3772 return false;
3773 }
3774}
3775
3776bool FITSData::debayer_8bit()
3777{
3778 dc1394error_t error_code;
3779
3780 uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
3781 uint8_t * destinationBuffer = nullptr;
3782
3783 try
3784 {
3785 destinationBuffer = new uint8_t[rgb_size];
3786 }
3787 catch (const std::bad_alloc &e)
3788 {
3789 logOOMError(rgb_size);
3790 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3791 return false;
3792 }
3793
3794 auto * bayer_source_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
3795 auto * bayer_destination_buffer = reinterpret_cast<uint8_t *>(destinationBuffer);
3796
3797 if (bayer_destination_buffer == nullptr)
3798 {
3799 logOOMError(rgb_size);
3800 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer.");
3801 return false;
3802 }
3803
3804 int ds1394_height = m_Statistics.height;
3805 auto dc1394_source = bayer_source_buffer;
3806
3807 if (debayerParams.offsetY == 1)
3808 {
3809 dc1394_source += m_Statistics.width;
3810 ds1394_height--;
3811 }
3812 // offsetX == 1 is handled in checkDebayer() and should be 0 here.
3813
3814 error_code = dc1394_bayer_decoding_8bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
3815 debayerParams.filter,
3816 debayerParams.method);
3817
3818 if (error_code != DC1394_SUCCESS)
3819 {
3820 m_LastError = i18n("Debayer failed (%1)", error_code);
3821 m_Statistics.channels = 1;
3822 delete[] destinationBuffer;
3823 return false;
3824 }
3825
3826 if (m_ImageBufferSize != rgb_size)
3827 {
3828 delete[] m_ImageBuffer;
3829 try
3830 {
3831 m_ImageBuffer = new uint8_t[rgb_size];
3832 }
3833 catch (const std::bad_alloc &e)
3834 {
3835 delete[] destinationBuffer;
3836 logOOMError(rgb_size);
3837 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3838 return false;
3839 }
3840
3841 m_ImageBufferSize = rgb_size;
3842 }
3843
3844 auto bayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
3845
3846 // Data in R1G1B1, we need to copy them into 3 layers for FITS
3847
3848 uint8_t * rBuff = bayered_buffer;
3849 uint8_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
3850 uint8_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
3851
3852 int imax = m_Statistics.samples_per_channel * 3 - 3;
3853 for (int i = 0; i <= imax; i += 3)
3854 {
3855 *rBuff++ = bayer_destination_buffer[i];
3856 *gBuff++ = bayer_destination_buffer[i + 1];
3857 *bBuff++ = bayer_destination_buffer[i + 2];
3858 }
3859
3860 // TODO Maybe all should be treated the same
3861 // Doing single channel saves lots of memory though for non-essential
3862 // frames
3863 m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
3864 m_Statistics.dataType = TBYTE;
3865 delete[] destinationBuffer;
3866 return true;
3867}
3868
3869bool FITSData::debayer_16bit()
3870{
3871 dc1394error_t error_code;
3872
3873 uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
3874 uint8_t *destinationBuffer = nullptr;
3875 try
3876 {
3877 destinationBuffer = new uint8_t[rgb_size];
3878 }
3879 catch (const std::bad_alloc &e)
3880 {
3881 logOOMError(rgb_size);
3882 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3883 return false;
3884 }
3885
3886 auto * bayer_source_buffer = reinterpret_cast<uint16_t *>(m_ImageBuffer);
3887 auto * bayer_destination_buffer = reinterpret_cast<uint16_t *>(destinationBuffer);
3888
3889 if (bayer_destination_buffer == nullptr)
3890 {
3891 logOOMError(rgb_size);
3892 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer.");
3893 return false;
3894 }
3895
3896 int ds1394_height = m_Statistics.height;
3897 auto dc1394_source = bayer_source_buffer;
3898
3899 if (debayerParams.offsetY == 1)
3900 {
3901 dc1394_source += m_Statistics.width;
3902 ds1394_height--;
3903 }
3904 // offsetX == 1 is handled in checkDebayer() and should be 0 here.
3905
3906 error_code = dc1394_bayer_decoding_16bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
3907 debayerParams.filter,
3908 debayerParams.method, 16);
3909
3910 if (error_code != DC1394_SUCCESS)
3911 {
3912 m_LastError = i18n("Debayer failed (%1)");
3913 m_Statistics.channels = 1;
3914 delete[] destinationBuffer;
3915 return false;
3916 }
3917
3918 if (m_ImageBufferSize != rgb_size)
3919 {
3920 delete[] m_ImageBuffer;
3921 try
3922 {
3923 m_ImageBuffer = new uint8_t[rgb_size];
3924 }
3925 catch (const std::bad_alloc &e)
3926 {
3927 logOOMError(rgb_size);
3928 delete[] destinationBuffer;
3929 m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3930 return false;
3931 }
3932
3933 m_ImageBufferSize = rgb_size;
3934 }
3935
3936 auto bayered_buffer = reinterpret_cast<uint16_t *>(m_ImageBuffer);
3937
3938 // Data in R1G1B1, we need to copy them into 3 layers for FITS
3939
3940 uint16_t * rBuff = bayered_buffer;
3941 uint16_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
3942 uint16_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
3943
3944 int imax = m_Statistics.samples_per_channel * 3 - 3;
3945 for (int i = 0; i <= imax; i += 3)
3946 {
3947 *rBuff++ = bayer_destination_buffer[i];
3948 *gBuff++ = bayer_destination_buffer[i + 1];
3949 *bBuff++ = bayer_destination_buffer[i + 2];
3950 }
3951
3952 m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
3953 m_Statistics.dataType = TUSHORT;
3954 delete[] destinationBuffer;
3955 return true;
3956}
3957
3958void FITSData::logOOMError(uint32_t requiredMemory)
3959{
3960 qCCritical(KSTARS_FITS) << "Debayed memory allocation failure. Required Memory:" << KFormat().formatByteSize(requiredMemory)
3961 << "Available system memory:" << KSUtils::getAvailableRAM();
3962}
3963
3964double FITSData::getADU() const
3965{
3966 double adu = 0;
3967 for (int i = 0; i < m_Statistics.channels; i++)
3968 adu += m_Statistics.mean[i];
3969
3970 return (adu / static_cast<double>(m_Statistics.channels));
3971}
3972
3973QString FITSData::getLastError() const
3974{
3975 return m_LastError;
3976}
3977
3978template <typename T>
3979void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image)
3980{
3981 const auto * buffer = reinterpret_cast<const T *>(getImageBuffer());
3982 const T limit = std::numeric_limits<T>::max();
3983 T bMin = dataMin < 0 ? 0 : dataMin;
3984 T bMax = dataMax > limit ? limit : dataMax;
3985 uint16_t w = width();
3986 uint16_t h = height();
3987 uint32_t size = w * h;
3988 double val;
3989
3990 if (channels() == 1)
3991 {
3992 /* Fill in pixel values using indexed map, linear scale */
3993 for (int j = 0; j < h; j++)
3994 {
3995 unsigned char * scanLine = image.scanLine(j);
3996
3997 for (int i = 0; i < w; i++)
3998 {
3999 val = qBound(bMin, buffer[j * w + i], bMax);
4000 val = val * scale + zero;
4001 scanLine[i] = qBound<unsigned char>(0, static_cast<uint8_t>(val), 255);
4002 }
4003 }
4004 }
4005 else
4006 {
4007 double rval = 0, gval = 0, bval = 0;
4008 QRgb value;
4009 /* Fill in pixel values using indexed map, linear scale */
4010 for (int j = 0; j < h; j++)
4011 {
4012 auto * scanLine = reinterpret_cast<QRgb *>((image.scanLine(j)));
4013
4014 for (int i = 0; i < w; i++)
4015 {
4016 rval = qBound(bMin, buffer[j * w + i], bMax);
4017 gval = qBound(bMin, buffer[j * w + i + size], bMax);
4018 bval = qBound(bMin, buffer[j * w + i + size * 2], bMax);
4019
4020 value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero);
4021
4022 scanLine[i] = value;
4023 }
4024 }
4025 }
4026}
4027
4028QImage FITSData::FITSToImage(const QString &filename)
4029{
4030 QImage fitsImage;
4031 double min, max;
4032
4033 FITSData data;
4034
4035 QFuture<bool> future = data.loadFromFile(filename);
4036
4037 // Wait synchronously
4038 future.waitForFinished();
4039 if (future.result() == false)
4040 return fitsImage;
4041
4042 data.getMinMax(&min, &max);
4043
4044 if (min == max)
4045 {
4046 fitsImage.fill(Qt::white);
4047 return fitsImage;
4048 }
4049
4050 if (data.channels() == 1)
4051 {
4052 fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8);
4053
4054 fitsImage.setColorCount(256);
4055 for (int i = 0; i < 256; i++)
4056 fitsImage.setColor(i, qRgb(i, i, i));
4057 }
4058 else
4059 {
4060 fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32);
4061 }
4062
4063 double dataMin = data.m_Statistics.mean[0] - data.m_Statistics.stddev[0];
4064 double dataMax = data.m_Statistics.mean[0] + data.m_Statistics.stddev[0] * 3;
4065
4066 double bscale = 255. / (dataMax - dataMin);
4067 double bzero = (-dataMin) * (255. / (dataMax - dataMin));
4068
4069 // Long way to do this since we do not want to use templated functions here
4070 switch (data.m_Statistics.dataType)
4071 {
4072 case TBYTE:
4073 data.convertToQImage<uint8_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4074 break;
4075
4076 case TSHORT:
4077 data.convertToQImage<int16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4078 break;
4079
4080 case TUSHORT:
4081 data.convertToQImage<uint16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4082 break;
4083
4084 case TLONG:
4085 data.convertToQImage<int32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4086 break;
4087
4088 case TULONG:
4089 data.convertToQImage<uint32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4090 break;
4091
4092 case TFLOAT:
4093 data.convertToQImage<float>(dataMin, dataMax, bscale, bzero, fitsImage);
4094 break;
4095
4096 case TLONGLONG:
4097 data.convertToQImage<int64_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4098 break;
4099
4100 case TDOUBLE:
4101 data.convertToQImage<double>(dataMin, dataMax, bscale, bzero, fitsImage);
4102 break;
4103
4104 default:
4105 break;
4106 }
4107
4108 return fitsImage;
4109}
4110
4111bool FITSData::ImageToFITS(const QString &filename, const QString &format, QString &output)
4112{
4113 if (QImageReader::supportedImageFormats().contains(format.toLatin1()) == false)
4114 {
4115 qCCritical(KSTARS_FITS) << "Failed to convert" << filename << "to FITS since format" << format << "is not supported in Qt";
4116 return false;
4117 }
4118
4119 QImage input;
4120
4121 if (input.load(filename, format.toLatin1()) == false)
4122 {
4123 qCCritical(KSTARS_FITS) << "Failed to open image" << filename;
4124 return false;
4125 }
4126
4127 output = QDir(getTemporaryPath()).filePath(filename + ".fits");
4128
4129 //This section sets up the FITS File
4130 fitsfile *fptr = nullptr;
4131 int status = 0;
4132 long fpixel = 1, naxis = input.allGray() ? 2 : 3, nelements, exposure;
4133 long naxes[3] = { input.width(), input.height(), naxis == 3 ? 3 : 1 };
4134 char error_status[512] = {0};
4135
4136 if (fits_create_file(&fptr, QString('!' + output).toLocal8Bit().data(), &status))
4137 {
4138 fits_get_errstatus(status, error_status);
4139 qCCritical(KSTARS_FITS) << "Failed to create FITS file. Error:" << error_status;
4140 return false;
4141 }
4142
4143 if (fits_create_img(fptr, BYTE_IMG, naxis, naxes, &status))
4144 {
4145 qCWarning(KSTARS_FITS) << "fits_create_img failed:" << error_status;
4146 status = 0;
4147 fits_flush_file(fptr, &status);
4148 fits_close_file(fptr, &status);
4149 return false;
4150 }
4151
4152 exposure = 1;
4153 fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);
4154
4155 // Gray image
4156 if (naxis == 2)
4157 {
4158 nelements = naxes[0] * naxes[1];
4159 if (fits_write_img(fptr, TBYTE, fpixel, nelements, input.bits(), &status))
4160 {
4161 fits_get_errstatus(status, error_status);
4162 qCWarning(KSTARS_FITS) << "fits_write_img GRAY failed:" << error_status;
4163 status = 0;
4164 fits_flush_file(fptr, &status);
4165 fits_close_file(fptr, &status);
4166 return false;
4167 }
4168 }
4169 // RGB image, we have to convert from ARGB format to R G B for each plane
4170 else
4171 {
4172 nelements = naxes[0] * naxes[1] * 3;
4173
4174 uint8_t *srcBuffer = input.bits();
4175 // ARGB
4176 uint32_t srcBytes = naxes[0] * naxes[1] * 4 - 4;
4177
4178 uint8_t *rgbBuffer = new uint8_t[nelements];
4179 if (rgbBuffer == nullptr)
4180 {
4181 qCWarning(KSTARS_FITS) << "Not enough memory for RGB buffer";
4182 fits_flush_file(fptr, &status);
4183 fits_close_file(fptr, &status);
4184 return false;
4185 }
4186
4187 uint8_t *subR = rgbBuffer;
4188 uint8_t *subG = rgbBuffer + naxes[0] * naxes[1];
4189 uint8_t *subB = rgbBuffer + naxes[0] * naxes[1] * 2;
4190 for (uint32_t i = 0; i < srcBytes; i += 4)
4191 {
4192 *subB++ = srcBuffer[i];
4193 *subG++ = srcBuffer[i + 1];
4194 *subR++ = srcBuffer[i + 2];
4195 }
4196
4197 if (fits_write_img(fptr, TBYTE, fpixel, nelements, rgbBuffer, &status))
4198 {
4199 fits_get_errstatus(status, error_status);
4200 qCWarning(KSTARS_FITS) << "fits_write_img RGB failed:" << error_status;
4201 status = 0;
4202 fits_flush_file(fptr, &status);
4203 fits_close_file(fptr, &status);
4204 delete [] rgbBuffer;
4205 return false;
4206 }
4207
4208 delete [] rgbBuffer;
4209 }
4210
4211 if (fits_flush_file(fptr, &status))
4212 {
4213 fits_get_errstatus(status, error_status);
4214 qCWarning(KSTARS_FITS) << "fits_flush_file failed:" << error_status;
4215 status = 0;
4216 fits_close_file(fptr, &status);
4217 return false;
4218 }
4219
4220 if (fits_close_file(fptr, &status))
4221 {
4222 fits_get_errstatus(status, error_status);
4223 qCWarning(KSTARS_FITS) << "fits_close_file failed:" << error_status;
4224 return false;
4225 }
4226
4227 return true;
4228}
4229
4230void FITSData::injectWCS(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
4231{
4232 int status = 0;
4233
4234 if (fptr == nullptr)
4235 return;
4236
4237 fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status);
4238 fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status);
4239
4240 int epoch = 2000;
4241
4242 fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status);
4243
4244 fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status);
4245 fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status);
4246
4247 char radecsys[8] = "FK5";
4248 char ctype1[16] = "RA---TAN";
4249 char ctype2[16] = "DEC--TAN";
4250
4251 fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status);
4252 fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status);
4253 fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status);
4254
4255 double crpix1 = width() / 2.0;
4256 double crpix2 = height() / 2.0;
4257
4258 fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status);
4259 fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status);
4260
4261 // Arcsecs per Pixel
4262 double secpix1 = eastToTheRight ? pixscale : -pixscale;
4263 double secpix2 = pixscale;
4264
4265 fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status);
4266 fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status);
4267
4268 double degpix1 = secpix1 / 3600.0;
4269 double degpix2 = secpix2 / 3600.0;
4270
4271 fits_update_key(fptr, TDOUBLE, "CDELT1", &degpix1, "CDELT1", &status);
4272 fits_update_key(fptr, TDOUBLE, "CDELT2", &degpix2, "CDELT2", &status);
4273
4274 // Rotation is CW, we need to convert it to CCW per CROTA1 definition
4275 double rotation = 360 - orientation;
4276 if (rotation > 360)
4277 rotation -= 360;
4278
4279 fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status);
4280 fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status);
4281
4282 m_WCSState = Idle;
4283}
4284
4285bool FITSData::contains(const QPointF &point) const
4286{
4287 return (point.x() >= 0 && point.y() >= 0 && point.x() <= m_Statistics.width && point.y() <= m_Statistics.height);
4288}
4289
4290void FITSData::saveStatistics(FITSImage::Statistic &other)
4291{
4292 other = m_Statistics;
4293}
4294
4295void FITSData::restoreStatistics(FITSImage::Statistic &other)
4296{
4297 m_Statistics = other;
4298
4299 emit dataChanged();
4300}
4301
4302void FITSData::constructHistogram()
4303{
4304 switch (m_Statistics.dataType)
4305 {
4306 case TBYTE:
4307 constructHistogramInternal<uint8_t>();
4308 break;
4309
4310 case TSHORT:
4311 constructHistogramInternal<int16_t>();
4312 break;
4313
4314 case TUSHORT:
4315 constructHistogramInternal<uint16_t>();
4316 break;
4317
4318 case TLONG:
4319 constructHistogramInternal<int32_t>();
4320 break;
4321
4322 case TULONG:
4323 constructHistogramInternal<uint32_t>();
4324 break;
4325
4326 case TFLOAT:
4327 constructHistogramInternal<float>();
4328 break;
4329
4330 case TLONGLONG:
4331 constructHistogramInternal<int64_t>();
4332 break;
4333
4334 case TDOUBLE:
4335 constructHistogramInternal<double>();
4336 break;
4337
4338 default:
4339 break;
4340 }
4341}
4342
4343template <typename T> int32_t FITSData::histogramBinInternal(T value, int channel) const
4344{
4345 return qMax(static_cast<T>(0), qMin(static_cast<T>(m_HistogramBinCount),
4346 static_cast<T>(rint((value - m_Statistics.min[channel]) / m_HistogramBinWidth[channel]))));
4347}
4348
4349template <typename T> int32_t FITSData::histogramBinInternal(int x, int y, int channel) const
4350{
4351 if (!m_ImageBuffer || !isHistogramConstructed())
4352 return 0;
4353 uint32_t samples = m_Statistics.width * m_Statistics.height;
4354 uint32_t offset = channel * samples;
4355 auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
4356 int index = y * m_Statistics.width + x;
4357 const T &sample = buffer[index + offset];
4358 return histogramBinInternal(sample, channel);
4359}
4360
4361int32_t FITSData::histogramBin(int x, int y, int channel) const
4362{
4363 switch (m_Statistics.dataType)
4364 {
4365 case TBYTE:
4366 return histogramBinInternal<uint8_t>(x, y, channel);
4367 break;
4368
4369 case TSHORT:
4370 return histogramBinInternal<int16_t>(x, y, channel);
4371 break;
4372
4373 case TUSHORT:
4374 return histogramBinInternal<uint16_t>(x, y, channel);
4375 break;
4376
4377 case TLONG:
4378 return histogramBinInternal<int32_t>(x, y, channel);
4379 break;
4380
4381 case TULONG:
4382 return histogramBinInternal<uint32_t>(x, y, channel);
4383 break;
4384
4385 case TFLOAT:
4386 return histogramBinInternal<float>(x, y, channel);
4387 break;
4388
4389 case TLONGLONG:
4390 return histogramBinInternal<int64_t>(x, y, channel);
4391 break;
4392
4393 case TDOUBLE:
4394 return histogramBinInternal<double>(x, y, channel);
4395 break;
4396
4397 default:
4398 return 0;
4399 break;
4400 }
4401}
4402
4403template <typename T> void FITSData::constructHistogramInternal()
4404{
4405 auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
4406 uint32_t samples = m_Statistics.width * m_Statistics.height;
4407 const uint32_t sampleBy = samples > 500000 ? samples / 500000 : 1;
4408
4409 m_HistogramBinCount = qMax(0., qMin(m_Statistics.max[0] - m_Statistics.min[0], 256.0));
4410 if (m_HistogramBinCount <= 0)
4411 m_HistogramBinCount = 256;
4412
4413 for (int n = 0; n < m_Statistics.channels; n++)
4414 {
4415 m_HistogramIntensity[n].fill(0, m_HistogramBinCount + 1);
4416 m_HistogramFrequency[n].fill(0, m_HistogramBinCount + 1);
4417 m_CumulativeFrequency[n].fill(0, m_HistogramBinCount + 1);
4418 // Distinguish between 0-1.0 ranges and ranges with integer values.
4419 const double minBinSize = (m_Statistics.max[n] > 1.1) ? 1.0 : .0001;
4420 m_HistogramBinWidth[n] = qMax(minBinSize, (m_Statistics.max[n] - m_Statistics.min[n]) / m_HistogramBinCount);
4421 }
4422
4423 QVector<QFuture<void>> futures;
4424
4425 for (int n = 0; n < m_Statistics.channels; n++)
4426 {
4427 futures.append(QtConcurrent::run([ = ]()
4428 {
4429 for (int i = 0; i < m_HistogramBinCount; i++)
4430 m_HistogramIntensity[n][i] = m_Statistics.min[n] + (m_HistogramBinWidth[n] * i);
4431 }));
4432 }
4433
4434 for (int n = 0; n < m_Statistics.channels; n++)
4435 {
4436 futures.append(QtConcurrent::run([ = ]()
4437 {
4438 uint32_t offset = n * samples;
4439
4440 for (uint32_t i = 0; i < samples; i += sampleBy)
4441 {
4442 int32_t id = histogramBinInternal<T>(buffer[i + offset], n);
4443 m_HistogramFrequency[n][id] += sampleBy;
4444 }
4445 }));
4446 }
4447
4448 for (QFuture<void> future : futures)
4449 future.waitForFinished();
4450
4451 futures.clear();
4452
4453 for (int n = 0; n < m_Statistics.channels; n++)
4454 {
4455 futures.append(QtConcurrent::run([ = ]()
4456 {
4457 uint32_t accumulator = 0;
4458 for (int i = 0; i < m_HistogramBinCount; i++)
4459 {
4460 accumulator += m_HistogramFrequency[n][i];
4461 m_CumulativeFrequency[n].replace(i, accumulator);
4462 }
4463 }));
4464 }
4465
4466 for (QFuture<void> future : futures)
4467 future.waitForFinished();
4468
4469 futures.clear();
4470
4471 // Custom index to indicate the overall contrast of the image
4472 if (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 4] > 0)
4473 m_JMIndex = m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 8] / static_cast<double>
4474 (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount /
4475 4]);
4476 else
4477 m_JMIndex = 1;
4478
4479 qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << m_JMIndex;
4480
4481 m_HistogramConstructed = true;
4482 emit histogramReady();
4483}
4484
4485void FITSData::recordLastError(int errorCode)
4486{
4487 char fitsErrorMessage[512] = {0};
4488 fits_get_errstatus(errorCode, fitsErrorMessage);
4489 m_LastError = fitsErrorMessage;
4490}
4491
4492double FITSData::getAverageMean(bool roi) const
4493{
4494 const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4495 if (ptr->channels == 1)
4496 return ptr->mean[0];
4497 else
4498 return (ptr->mean[0] + ptr->mean[1] + ptr->mean[2]) / 3.0;
4499}
4500
4501double FITSData::getAverageMedian(bool roi) const
4502{
4503 const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4504 if (ptr->channels == 1)
4505 return ptr->median[0];
4506 else
4507 return (ptr->median[0] + ptr->median[1] + ptr->median[2]) / 3.0;
4508}
4509
4510double FITSData::getAverageStdDev(bool roi) const
4511{
4512 const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4513 if (ptr->channels == 1)
4514 return ptr->stddev[0];
4515 else
4516 return (ptr->stddev[0] + ptr->stddev[1] + ptr->stddev[2]) / 3.0;
4517}
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
There are several time-dependent values used in position calculations, that are not specific to an ob...
Definition ksnumbers.h:43
SkyMapComposite * skyComposite()
Definition kstarsdata.h:166
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
QList< SkyObject * > findObjectsInArea(const SkyPoint &p1, const SkyPoint &p2)
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:382
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT QString number(KIO::filesize_t size)
QWidget * window(QObject *job)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem remove()
KGuiItem properties()
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
QByteArray & append(QByteArrayView data)
const char * constData() const const
char * data()
bool isEmpty() const const
QByteArray leftJustified(qsizetype width, char fill, bool truncate) const const
void push_back(QByteArrayView str)
qsizetype size() const const
QDateTime currentDateTime()
QDate date() const const
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
QTime time() const const
QString filePath(const QString &fileName) const const
QString path() const const
QString tempPath()
virtual qint64 size() const const override
virtual void close() override
qint64 size() const const
QString suffix() const const
bool isRunning() const const
T result() const const
void waitForFinished()
Format_Grayscale8
bool allGray() const const
uchar * bits()
void fill(Qt::GlobalColor color)
Format format() const const
int height() const const
bool load(QIODevice *device, const char *format)
bool loadFromData(QByteArrayView data, const char *format)
bool save(QIODevice *device, const char *format, int quality) const const
uchar * scanLine(int i)
void setColor(int index, QRgb colorValue)
void setColorCount(int colorCount)
int width() const const
QList< QByteArray > supportedImageFormats()
qint64 write(const QByteArray &data)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
void clear()
bool contains(const AT &value) const const
qsizetype count() const const
bool empty() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
qsizetype size() const const
void setObjectName(QAnyStringView name)
void setX(int x)
void setY(int y)
int x() const const
int y() const const
void setX(qreal x)
void setY(qreal y)
qreal x() const const
qreal y() const const
bool contains(const QPoint &point, bool proper) const const
int height() const const
bool isNull() const const
bool isValid() const const
QPoint topLeft() const const
int width() const const
void reset(T *other)
bool isNull() const const
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromStdString(const std::string &str)
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QByteArray toLocal8Bit() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QTextStream & center(QTextStream &stream)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< T > run(Function function,...)
virtual QString fileName() const const override
void setFileTemplate(const QString &name)
QUuid createUuid()
QString toString(StringFormat mode) const const
Type type() const const
bool isValid() const const
QDateTime toDateTime() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:51 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.