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

KDE's Doxygen guidelines are available online.