File indexing completed on 2024-04-21 14:45:47

0001 /*
0002     SPDX-FileCopyrightText: 2004 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006     Some code fragments were adapted from Peter Kirchgessner's FITS plugin
0007     SPDX-FileCopyrightText: Peter Kirchgessner <http://members.aol.com/pkirchg>
0008 */
0009 
0010 #include "fitsdata.h"
0011 #include "fitsbahtinovdetector.h"
0012 #include "fitsthresholddetector.h"
0013 #include "fitsgradientdetector.h"
0014 #include "fitscentroiddetector.h"
0015 #include "fitssepdetector.h"
0016 
0017 #include "fpack.h"
0018 
0019 #include "kstarsdata.h"
0020 #include "ksutils.h"
0021 #include "kspaths.h"
0022 #include "Options.h"
0023 #include "skymapcomposite.h"
0024 #include "auxiliary/ksnotification.h"
0025 #include "auxiliary/robuststatistics.h"
0026 
0027 #include <KFormat>
0028 #include <QApplication>
0029 #include <QImage>
0030 #include <QtConcurrent>
0031 #include <QImageReader>
0032 
0033 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
0034 #include <wcshdr.h>
0035 #include <wcsfix.h>
0036 #endif
0037 
0038 #if !defined(KSTARS_LITE) && defined(HAVE_LIBRAW)
0039 #include <libraw/libraw.h>
0040 #endif
0041 
0042 #ifdef HAVE_XISF
0043 #include <libxisf.h>
0044 #endif
0045 
0046 #include <cfloat>
0047 #include <cmath>
0048 
0049 #include <fits_debug.h>
0050 
0051 #define ZOOM_DEFAULT   100.0
0052 #define ZOOM_MIN       10
0053 #define ZOOM_MAX       400
0054 #define ZOOM_LOW_INCR  10
0055 #define ZOOM_HIGH_INCR 50
0056 
0057 QString getTemporaryPath()
0058 {
0059     return QDir(KSPaths::writableLocation(QStandardPaths::TempLocation) + "/" +
0060                 qAppName()).path();
0061 }
0062 
0063 const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw", "orf" };
0064 
0065 bool FITSData::readableFilename(const QString &filename)
0066 {
0067     QFileInfo info(filename);
0068     QString extension = info.completeSuffix().toLower();
0069     return extension.contains("fit") || extension.contains("fz") ||
0070            extension.contains("xisf") ||
0071            QImageReader::supportedImageFormats().contains(extension.toLatin1()) ||
0072            RAWFormats.contains(extension);
0073 }
0074 
0075 FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode)
0076 {
0077     qRegisterMetaType<FITSMode>("FITSMode");
0078 
0079     debayerParams.method  = DC1394_BAYER_METHOD_NEAREST;
0080     debayerParams.filter  = DC1394_COLOR_FILTER_RGGB;
0081     debayerParams.offsetX = debayerParams.offsetY = 0;
0082 
0083     // Reserve 3 channels
0084     m_CumulativeFrequency.resize(3);
0085     m_HistogramBinWidth.resize(3);
0086     m_HistogramFrequency.resize(3);
0087     m_HistogramIntensity.resize(3);
0088 }
0089 
0090 FITSData::FITSData(const QSharedPointer<FITSData> &other)
0091 {
0092     qRegisterMetaType<FITSMode>("FITSMode");
0093 
0094     debayerParams.method  = DC1394_BAYER_METHOD_NEAREST;
0095     debayerParams.filter  = DC1394_COLOR_FILTER_RGGB;
0096     debayerParams.offsetX = debayerParams.offsetY = 0;
0097 
0098     m_TemporaryDataFile.setFileTemplate("fits_memory_XXXXXX");
0099 
0100     this->m_Mode = other->m_Mode;
0101     this->m_Statistics.channels = other->m_Statistics.channels;
0102     memcpy(&m_Statistics, &(other->m_Statistics), sizeof(m_Statistics));
0103     m_ImageBuffer = new uint8_t[m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel];
0104     memcpy(m_ImageBuffer, other->m_ImageBuffer,
0105            m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel);
0106 }
0107 
0108 FITSData::~FITSData()
0109 {
0110     int status = 0;
0111 
0112     if (m_StarFindFuture.isRunning())
0113         m_StarFindFuture.waitForFinished();
0114 
0115     clearImageBuffers();
0116 
0117 #ifdef HAVE_WCSLIB
0118     if (m_WCSHandle != nullptr)
0119     {
0120         wcsvfree(&m_nwcs, &m_WCSHandle);
0121         m_WCSHandle = nullptr;
0122         m_nwcs = 0;
0123     }
0124 #endif
0125 
0126     if (starCenters.count() > 0)
0127         qDeleteAll(starCenters);
0128     starCenters.clear();
0129 
0130     if (m_SkyObjects.count() > 0)
0131         qDeleteAll(m_SkyObjects);
0132     m_SkyObjects.clear();
0133 
0134     if (fptr != nullptr)
0135     {
0136         fits_flush_file(fptr, &status);
0137         fits_close_file(fptr, &status);
0138         free(m_PackBuffer);
0139         m_PackBuffer = nullptr;
0140         fptr = nullptr;
0141     }
0142 }
0143 
0144 void FITSData::loadCommon(const QString &inFilename)
0145 {
0146     int status = 0;
0147     qDeleteAll(starCenters);
0148     starCenters.clear();
0149 
0150     if (fptr != nullptr)
0151     {
0152         fits_flush_file(fptr, &status);
0153         fits_close_file(fptr, &status);
0154         free(m_PackBuffer);
0155         m_PackBuffer = nullptr;
0156         fptr = nullptr;
0157     }
0158 
0159     m_Filename = inFilename;
0160 }
0161 
0162 bool FITSData::loadFromBuffer(const QByteArray &buffer, const QString &extension, const QString &inFilename)
0163 {
0164     loadCommon(inFilename);
0165     m_Extension = extension;
0166     qCDebug(KSTARS_FITS) << "Reading file buffer (" << KFormat().formatByteSize(buffer.size()) << ")";
0167     return privateLoad(buffer);
0168 }
0169 
0170 QFuture<bool> FITSData::loadFromFile(const QString &inFilename)
0171 {
0172     loadCommon(inFilename);
0173     QFileInfo info(m_Filename);
0174     m_Extension = info.completeSuffix().toLower();
0175     qCDebug(KSTARS_FITS) << "Loading file " << m_Filename;
0176     return QtConcurrent::run(this, &FITSData::privateLoad, QByteArray());
0177 }
0178 
0179 namespace
0180 {
0181 // Common code for reporting fits read errors. Always returns false.
0182 QString fitsErrorToString(int status)
0183 {
0184     char error_status[512] = {0};
0185     fits_report_error(stderr, status);
0186     fits_get_errstatus(status, error_status);
0187     QString message = error_status;
0188     return message;
0189 }
0190 }
0191 
0192 bool FITSData::privateLoad(const QByteArray &buffer)
0193 {
0194     m_isTemporary = m_Filename.startsWith(KSPaths::writableLocation(QStandardPaths::TempLocation));
0195     cacheHFR = -1;
0196     cacheEccentricity = -1;
0197 
0198     if (m_Extension.contains("fit") || m_Extension.contains("fz"))
0199         return loadFITSImage(buffer);
0200     if (m_Extension.contains("xisf"))
0201         return loadXISFImage(buffer);
0202     if (QImageReader::supportedImageFormats().contains(m_Extension.toLatin1()))
0203         return loadCanonicalImage(buffer);
0204     else if (RAWFormats.contains(m_Extension))
0205         return loadRAWImage(buffer);
0206 
0207     return false;
0208 }
0209 
0210 bool FITSData::loadFITSImage(const QByteArray &buffer, const bool isCompressed)
0211 {
0212     int status = 0, anynull = 0;
0213     long naxes[3];
0214 
0215     m_HistogramConstructed = false;
0216 
0217     if (m_Extension.contains(".fz") || isCompressed)
0218     {
0219         fpstate fpvar;
0220         fp_init (&fpvar);
0221         bool rc = false;
0222 
0223         if (buffer.isEmpty())
0224         {
0225             // Store so we don't lose.
0226             m_compressedFilename = m_Filename;
0227 
0228             QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(
0229                                            QRegularExpression("[-{}]")));
0230 
0231             rc = fp_unpack_file_to_fits(m_Filename.toLocal8Bit().data(), &fptr, fpvar) == 0;
0232             if (rc)
0233             {
0234                 m_Filename = uncompressedFile;
0235             }
0236         }
0237         else
0238         {
0239             size_t m_PackBufferSize = 100000;
0240             free(m_PackBuffer);
0241             m_PackBuffer = (uint8_t *)malloc(m_PackBufferSize);
0242             rc = fp_unpack_data_to_data(buffer.data(), buffer.size(), &m_PackBuffer, &m_PackBufferSize, fpvar) == 0;
0243 
0244             if (rc)
0245             {
0246                 void *data = reinterpret_cast<void *>(m_PackBuffer);
0247                 if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY, &data, &m_PackBufferSize, 0,
0248                                       nullptr, &status))
0249                 {
0250                     m_LastError = i18n("Error reading fits buffer: %1.", fitsErrorToString(status));
0251                     return false;
0252                 }
0253 
0254                 m_Statistics.size = m_PackBufferSize;
0255             }
0256             //rc = fp_unpack_data_to_fits(buffer.data(), buffer.size(), &fptr, fpvar) == 0;
0257         }
0258 
0259         if (rc == false)
0260         {
0261             free(m_PackBuffer);
0262             m_PackBuffer = nullptr;
0263             m_LastError = i18n("Failed to unpack compressed fits");
0264             qCCritical(KSTARS_FITS) << m_LastError;
0265             return false;
0266         }
0267 
0268         m_isTemporary = true;
0269         m_isCompressed = true;
0270         m_Statistics.size = fptr->Fptr->logfilesize;
0271 
0272     }
0273     else if (buffer.isEmpty())
0274     {
0275         // Use open diskfile as it does not use extended file names which has problems opening
0276         // files with [ ] or ( ) in their names.
0277         if (fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status))
0278         {
0279             m_LastError = i18n("Error opening fits file %1 : %2", m_Filename, fitsErrorToString(status));
0280         }
0281 
0282         m_Statistics.size = QFile(m_Filename).size();
0283     }
0284     else
0285     {
0286         // Read the FITS file from a memory buffer.
0287         void *temp_buffer = const_cast<void *>(reinterpret_cast<const void *>(buffer.data()));
0288         size_t temp_size = buffer.size();
0289         if (fits_open_memfile(&fptr, m_Filename.toLocal8Bit().data(), READONLY,
0290                               &temp_buffer, &temp_size, 0, nullptr, &status))
0291         {
0292             m_LastError = i18n("Error reading fits buffer: %1", fitsErrorToString(status));
0293             return false;
0294         }
0295 
0296         m_Statistics.size = temp_size;
0297     }
0298 
0299     if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
0300     {
0301 
0302         free(m_PackBuffer);
0303         m_PackBuffer = nullptr;
0304         m_LastError = i18n("Could not locate image HDU: %1", fitsErrorToString(status));
0305     }
0306 
0307     if (fits_get_img_param(fptr, 3, &m_FITSBITPIX, &(m_Statistics.ndim), naxes, &status))
0308     {
0309         free(m_PackBuffer);
0310         m_PackBuffer = nullptr;
0311         m_LastError = i18n("FITS file open error (fits_get_img_param): %1", fitsErrorToString(status));
0312         return false;
0313     }
0314 
0315     // Reload if it is transparently compressed.
0316     if ((fits_is_compressed_image(fptr, &status) || m_Statistics.ndim <= 0) && !isCompressed)
0317     {
0318         loadCommon(m_Filename);
0319         qCDebug(KSTARS_FITS) << "Image is compressed. Reloading...";
0320         return loadFITSImage(buffer, true);
0321     }
0322 
0323     if (m_Statistics.ndim < 2)
0324     {
0325         m_LastError = i18n("1D FITS images are not supported in KStars.");
0326         qCCritical(KSTARS_FITS) << m_LastError;
0327         free(m_PackBuffer);
0328         m_PackBuffer = nullptr;
0329         return false;
0330     }
0331 
0332     switch (m_FITSBITPIX)
0333     {
0334         case BYTE_IMG:
0335             m_Statistics.dataType      = TBYTE;
0336             m_Statistics.bytesPerPixel = sizeof(uint8_t);
0337             break;
0338         case SHORT_IMG:
0339             // Read SHORT image as USHORT
0340             m_FITSBITPIX               = USHORT_IMG;
0341             m_Statistics.dataType      = TUSHORT;
0342             m_Statistics.bytesPerPixel = sizeof(int16_t);
0343             break;
0344         case USHORT_IMG:
0345             m_Statistics.dataType      = TUSHORT;
0346             m_Statistics.bytesPerPixel = sizeof(uint16_t);
0347             break;
0348         case LONG_IMG:
0349             // Read LONG image as ULONG
0350             m_FITSBITPIX               = ULONG_IMG;
0351             m_Statistics.dataType      = TULONG;
0352             m_Statistics.bytesPerPixel = sizeof(int32_t);
0353             break;
0354         case ULONG_IMG:
0355             m_Statistics.dataType      = TULONG;
0356             m_Statistics.bytesPerPixel = sizeof(uint32_t);
0357             break;
0358         case FLOAT_IMG:
0359             m_Statistics.dataType      = TFLOAT;
0360             m_Statistics.bytesPerPixel = sizeof(float);
0361             break;
0362         case LONGLONG_IMG:
0363             m_Statistics.dataType      = TLONGLONG;
0364             m_Statistics.bytesPerPixel = sizeof(int64_t);
0365             break;
0366         case DOUBLE_IMG:
0367             m_Statistics.dataType      = TDOUBLE;
0368             m_Statistics.bytesPerPixel = sizeof(double);
0369             break;
0370         default:
0371             m_LastError = i18n("Bit depth %1 is not supported.", m_FITSBITPIX);
0372             qCCritical(KSTARS_FITS) << m_LastError;
0373             return false;
0374     }
0375 
0376     if (m_Statistics.ndim < 3)
0377         naxes[2] = 1;
0378 
0379     if (naxes[0] == 0 || naxes[1] == 0)
0380     {
0381         m_LastError = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]);
0382         qCCritical(KSTARS_FITS) << m_LastError;
0383         free(m_PackBuffer);
0384         m_PackBuffer = nullptr;
0385         return false;
0386     }
0387 
0388     m_Statistics.width               = naxes[0];
0389     m_Statistics.height              = naxes[1];
0390     m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
0391     roiCenter.setX(m_Statistics.width / 2);
0392     roiCenter.setY(m_Statistics.height / 2);
0393     if(m_Statistics.width % 2)
0394         roiCenter.setX(roiCenter.x() + 1);
0395     if(m_Statistics.height % 2)
0396         roiCenter.setY(roiCenter.y() + 1);
0397 
0398     clearImageBuffers();
0399 
0400     m_Statistics.channels = naxes[2];
0401 
0402     // Channels always set to #1 if we are not required to process 3D Cubes
0403     // Or if mode is not FITS_NORMAL (guide, focus..etc)
0404     if ( (m_Mode != FITS_NORMAL && m_Mode != FITS_CALIBRATE) || !Options::auto3DCube())
0405         m_Statistics.channels = 1;
0406 
0407     m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
0408     m_ImageBuffer = new uint8_t[m_ImageBufferSize];
0409     if (m_ImageBuffer == nullptr)
0410     {
0411         qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: "
0412                                << m_ImageBufferSize << " bytes.";
0413         clearImageBuffers();
0414         free(m_PackBuffer);
0415         m_PackBuffer = nullptr;
0416         return false;
0417     }
0418 
0419     rotCounter     = 0;
0420     flipHCounter   = 0;
0421     flipVCounter   = 0;
0422     long nelements = m_Statistics.samples_per_channel * m_Statistics.channels;
0423 
0424     if (fits_read_img(fptr, m_Statistics.dataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status))
0425     {
0426         m_LastError = i18n("Error reading image: %1", fitsErrorToString(status));
0427         return false;
0428     }
0429 
0430     parseHeader();
0431 
0432     // Get UTC date time
0433     QVariant value;
0434     if (getRecordValue("DATE-OBS", value) && value.isValid())
0435     {
0436         QDateTime ts = value.toDateTime();
0437         m_DateTime = KStarsDateTime(ts.date(), ts.time());
0438     }
0439 
0440     // Only check for debayed IF the original naxes[2] is 1
0441     // which is for single channels.
0442     if (naxes[2] == 1 && m_Statistics.channels == 1 && Options::autoDebayer() && checkDebayer())
0443     {
0444         // Save bayer image on disk in case we need to save it later since debayer destorys this data
0445         if (m_isTemporary && m_TemporaryDataFile.open())
0446         {
0447             m_TemporaryDataFile.write(buffer);
0448             m_TemporaryDataFile.close();
0449             m_Filename = m_TemporaryDataFile.fileName();
0450         }
0451 
0452         if (debayer())
0453             calculateStats(false, false);
0454     }
0455     else
0456         calculateStats(false, false);
0457 
0458     if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN)
0459         loadWCS();
0460 
0461     starsSearched = false;
0462 
0463     return true;
0464 }
0465 
0466 bool FITSData::loadXISFImage(const QByteArray &buffer)
0467 {
0468     m_HistogramConstructed = false;
0469     clearImageBuffers();
0470 
0471 #ifdef HAVE_XISF
0472     try
0473     {
0474         LibXISF::XISFReader xisfReader;
0475         if (buffer.isEmpty())
0476         {
0477             xisfReader.open(m_Filename.toLocal8Bit().data());
0478         }
0479         else
0480         {
0481             LibXISF::ByteArray byteArray(buffer.constData(), buffer.size());
0482             xisfReader.open(byteArray);
0483         }
0484 
0485         if (xisfReader.imagesCount() == 0)
0486         {
0487             m_LastError = i18n("File contain no images");
0488             return false;
0489         }
0490 
0491         const LibXISF::Image &image = xisfReader.getImage(0);
0492 
0493         switch (image.sampleFormat())
0494         {
0495             case LibXISF::Image::UInt8:
0496                 m_Statistics.dataType = TBYTE;
0497                 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt8);
0498                 m_FITSBITPIX = TBYTE;
0499                 break;
0500             case LibXISF::Image::UInt16:
0501                 m_Statistics.dataType = TUSHORT;
0502                 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt16);
0503                 m_FITSBITPIX = TUSHORT;
0504                 break;
0505             case LibXISF::Image::UInt32:
0506                 m_Statistics.dataType = TULONG;
0507                 m_Statistics.bytesPerPixel = sizeof(LibXISF::UInt32);
0508                 m_FITSBITPIX = TULONG;
0509                 break;
0510             case LibXISF::Image::Float32:
0511                 m_Statistics.dataType = TFLOAT;
0512                 m_Statistics.bytesPerPixel = sizeof(LibXISF::Float32);
0513                 m_FITSBITPIX = TFLOAT;
0514                 break;
0515             default:
0516                 m_LastError = i18n("Sample format %1 is not supported.", LibXISF::Image::sampleFormatString(image.sampleFormat()).c_str());
0517                 qCCritical(KSTARS_FITS) << m_LastError;
0518                 return false;
0519         }
0520 
0521         m_Statistics.width = image.width();
0522         m_Statistics.height = image.height();
0523         m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
0524         m_Statistics.channels = image.channelCount();
0525         m_Statistics.size = buffer.size();
0526         roiCenter.setX(m_Statistics.width / 2);
0527         roiCenter.setY(m_Statistics.height / 2);
0528         if(m_Statistics.width % 2)
0529             roiCenter.setX(roiCenter.x() + 1);
0530         if(m_Statistics.height % 2)
0531             roiCenter.setY(roiCenter.y() + 1);
0532 
0533         m_HeaderRecords.clear();
0534         auto &fitsKeywords = image.fitsKeywords();
0535         for(auto &fitsKeyword : fitsKeywords)
0536             m_HeaderRecords.push_back({QString::fromStdString(fitsKeyword.name), QString::fromStdString(fitsKeyword.value), QString::fromStdString(fitsKeyword.comment)});
0537 
0538         QVariant value;
0539         if (getRecordValue("DATE-OBS", value) && value.isValid())
0540         {
0541             QDateTime ts = value.toDateTime();
0542             m_DateTime = KStarsDateTime(ts.date(), ts.time());
0543         }
0544 
0545         m_ImageBufferSize = image.imageDataSize();
0546         m_ImageBuffer = new uint8_t[m_ImageBufferSize];
0547         std::memcpy(m_ImageBuffer, image.imageData(), m_ImageBufferSize);
0548 
0549         calculateStats(false, false);
0550         loadWCS();
0551     }
0552     catch (LibXISF::Error &error)
0553     {
0554         m_LastError = i18n("XISF file open error: ") + error.what();
0555         return false;
0556     }
0557     return true;
0558 #else
0559     return false;
0560 #endif
0561 
0562 }
0563 
0564 bool FITSData::saveXISFImage(const QString &newFilename)
0565 {
0566 #ifdef HAVE_XISF
0567     try
0568     {
0569         LibXISF::XISFWriter xisfWriter;
0570         LibXISF::Image image;
0571         image.setGeometry(m_Statistics.width, m_Statistics.height, m_Statistics.channels);
0572         if (m_Statistics.channels > 1)
0573             image.setColorSpace(LibXISF::Image::RGB);
0574 
0575         switch (m_FITSBITPIX)
0576         {
0577             case BYTE_IMG:
0578                 image.setSampleFormat(LibXISF::Image::UInt8);
0579                 break;
0580             case USHORT_IMG:
0581                 image.setSampleFormat(LibXISF::Image::UInt16);
0582                 break;
0583             case ULONG_IMG:
0584                 image.setSampleFormat(LibXISF::Image::UInt32);
0585                 break;
0586             case FLOAT_IMG:
0587                 image.setSampleFormat(LibXISF::Image::Float32);
0588                 break;
0589             default:
0590                 m_LastError = i18n("Bit depth %1 is not supported.", m_FITSBITPIX);
0591                 qCCritical(KSTARS_FITS) << m_LastError;
0592                 return false;
0593         }
0594 
0595         std::memcpy(image.imageData(), m_ImageBuffer, m_ImageBufferSize);
0596         for (auto &fitsKeyword : m_HeaderRecords)
0597             image.addFITSKeyword({fitsKeyword.key.toUtf8().data(), fitsKeyword.value.toString().toUtf8().data(), fitsKeyword.comment.toUtf8().data()});
0598 
0599         xisfWriter.writeImage(image);
0600         xisfWriter.save(newFilename.toLocal8Bit().data());
0601         m_Filename = newFilename;
0602     }
0603     catch (LibXISF::Error &err)
0604     {
0605         m_LastError = i18n("Error saving XISF image") + err.what();
0606         return false;
0607     }
0608     return true;
0609 #else
0610     return false;
0611 #endif
0612 }
0613 
0614 bool FITSData::loadCanonicalImage(const QByteArray &buffer)
0615 {
0616     QImage imageFromFile;
0617     if (!buffer.isEmpty())
0618     {
0619         if(!imageFromFile.loadFromData(reinterpret_cast<const uint8_t*>(buffer.data()), buffer.size()))
0620         {
0621             qCCritical(KSTARS_FITS) << "Failed to open image.";
0622             return false;
0623         }
0624 
0625         m_Statistics.size = buffer.size();
0626     }
0627     else
0628     {
0629         if(!imageFromFile.load(m_Filename.toLocal8Bit()))
0630         {
0631             qCCritical(KSTARS_FITS) << "Failed to open image.";
0632             return false;
0633         }
0634 
0635         m_Statistics.size = QFileInfo(m_Filename).size();
0636     }
0637 
0638     //imageFromFile = imageFromFile.convertToFormat(QImage::Format_RGB32);
0639 
0640     // Note: This will need to be changed.  I think QT only loads 8 bpp images.
0641     // Also the depth method gives the total bits per pixel in the image not just the bits per
0642     // pixel in each channel.
0643     m_FITSBITPIX = BYTE_IMG;
0644     switch (m_FITSBITPIX)
0645     {
0646         case BYTE_IMG:
0647             m_Statistics.dataType      = TBYTE;
0648             m_Statistics.bytesPerPixel = sizeof(uint8_t);
0649             break;
0650         case SHORT_IMG:
0651             // Read SHORT image as USHORT
0652             m_Statistics.dataType      = TUSHORT;
0653             m_Statistics.bytesPerPixel = sizeof(int16_t);
0654             break;
0655         case USHORT_IMG:
0656             m_Statistics.dataType      = TUSHORT;
0657             m_Statistics.bytesPerPixel = sizeof(uint16_t);
0658             break;
0659         case LONG_IMG:
0660             // Read LONG image as ULONG
0661             m_Statistics.dataType      = TULONG;
0662             m_Statistics.bytesPerPixel = sizeof(int32_t);
0663             break;
0664         case ULONG_IMG:
0665             m_Statistics.dataType      = TULONG;
0666             m_Statistics.bytesPerPixel = sizeof(uint32_t);
0667             break;
0668         case FLOAT_IMG:
0669             m_Statistics.dataType      = TFLOAT;
0670             m_Statistics.bytesPerPixel = sizeof(float);
0671             break;
0672         case LONGLONG_IMG:
0673             m_Statistics.dataType      = TLONGLONG;
0674             m_Statistics.bytesPerPixel = sizeof(int64_t);
0675             break;
0676         case DOUBLE_IMG:
0677             m_Statistics.dataType      = TDOUBLE;
0678             m_Statistics.bytesPerPixel = sizeof(double);
0679             break;
0680         default:
0681             m_LastError = QString("Bit depth %1 is not supported.").arg(m_FITSBITPIX);
0682             qCCritical(KSTARS_FITS) << m_LastError;
0683             return false;
0684     }
0685 
0686     m_Statistics.width = static_cast<uint16_t>(imageFromFile.width());
0687     m_Statistics.height = static_cast<uint16_t>(imageFromFile.height());
0688     m_Statistics.channels = imageFromFile.format() == QImage::Format_Grayscale8 ? 1 : 3;
0689     m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
0690     clearImageBuffers();
0691     m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * static_cast<uint16_t>
0692                         (m_Statistics.bytesPerPixel);
0693     m_ImageBuffer = new uint8_t[m_ImageBufferSize];
0694     if (m_ImageBuffer == nullptr)
0695     {
0696         m_LastError = i18n("FITSData: Not enough memory for image_buffer channel. Requested: %1 bytes ", m_ImageBufferSize);
0697         qCCritical(KSTARS_FITS) << m_LastError;
0698         clearImageBuffers();
0699         return false;
0700     }
0701 
0702     if (m_Statistics.channels == 1)
0703     {
0704         memcpy(m_ImageBuffer, imageFromFile.bits(), m_ImageBufferSize);
0705     }
0706     else
0707     {
0708 
0709         auto debayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
0710         auto * original_bayered_buffer = reinterpret_cast<uint8_t *>(imageFromFile.bits());
0711 
0712         // Data in RGBA, with bytes in the order of B,G,R we need to copy them into 3 layers for FITS
0713         uint8_t * rBuff = debayered_buffer;
0714         uint8_t * gBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height);
0715         uint8_t * bBuff = debayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
0716 
0717         int imax = m_Statistics.samples_per_channel * 4 - 4;
0718         for (int i = 0; i <= imax; i += 4)
0719         {
0720             *rBuff++ = original_bayered_buffer[i + 2];
0721             *gBuff++ = original_bayered_buffer[i + 1];
0722             *bBuff++ = original_bayered_buffer[i + 0];
0723         }
0724     }
0725 
0726     calculateStats(false, false);
0727     return true;
0728 }
0729 
0730 bool FITSData::loadRAWImage(const QByteArray &buffer)
0731 {
0732 #if !defined(KSTARS_LITE) && !defined(HAVE_LIBRAW)
0733     m_LastError = i18n("Unable to find dcraw and cjpeg. Please install the required tools to convert CR2/NEF to JPEG.");
0734     return false;
0735 #else
0736 
0737     int ret = 0;
0738     // Creation of image processing object
0739     LibRaw RawProcessor;
0740 
0741     // Let us open the file/buffer
0742     if (buffer.isEmpty())
0743     {
0744         // Open file
0745         if ((ret = RawProcessor.open_file(m_Filename.toLocal8Bit().constData())) != LIBRAW_SUCCESS)
0746         {
0747             m_LastError = i18n("Cannot open file %1: %2", m_Filename, libraw_strerror(ret));
0748             RawProcessor.recycle();
0749             return false;
0750         }
0751 
0752         m_Statistics.size = QFileInfo(m_Filename).size();
0753     }
0754     // Open Buffer
0755     else
0756     {
0757         if ((ret = RawProcessor.open_buffer(const_cast<void *>(reinterpret_cast<const void *>(buffer.data())),
0758                                             buffer.size())) != LIBRAW_SUCCESS)
0759         {
0760             m_LastError = i18n("Cannot open buffer: %1", libraw_strerror(ret));
0761             RawProcessor.recycle();
0762             return false;
0763         }
0764 
0765         m_Statistics.size = buffer.size();
0766     }
0767 
0768     // Let us unpack the thumbnail
0769     if ((ret = RawProcessor.unpack()) != LIBRAW_SUCCESS)
0770     {
0771         m_LastError = i18n("Cannot unpack_thumb: %1", libraw_strerror(ret));
0772         RawProcessor.recycle();
0773         return false;
0774     }
0775 
0776     if ((ret = RawProcessor.dcraw_process()) != LIBRAW_SUCCESS)
0777     {
0778         m_LastError = i18n("Cannot dcraw_process: %1", libraw_strerror(ret));
0779         RawProcessor.recycle();
0780         return false;
0781     }
0782 
0783     libraw_processed_image_t *image = RawProcessor.dcraw_make_mem_image(&ret);
0784     if (ret != LIBRAW_SUCCESS)
0785     {
0786         m_LastError = i18n("Cannot load to memory: %1", libraw_strerror(ret));
0787         RawProcessor.recycle();
0788         return false;
0789     }
0790 
0791     RawProcessor.recycle();
0792 
0793     m_Statistics.bytesPerPixel = image->bits / 8;
0794     // We only support two types now
0795     if (m_Statistics.bytesPerPixel == 1)
0796         m_Statistics.dataType = TBYTE;
0797     else
0798         m_Statistics.dataType = TUSHORT;
0799     m_Statistics.width = image->width;
0800     m_Statistics.height = image->height;
0801     m_Statistics.channels = image->colors;
0802     m_Statistics.samples_per_channel = m_Statistics.width * m_Statistics.height;
0803     clearImageBuffers();
0804     m_ImageBufferSize = m_Statistics.samples_per_channel * m_Statistics.channels * m_Statistics.bytesPerPixel;
0805     m_ImageBuffer = new uint8_t[m_ImageBufferSize];
0806     if (m_ImageBuffer == nullptr)
0807     {
0808         m_LastError = i18n("FITSData: Not enough memory for image_buffer channel. Requested: %1 bytes ", m_ImageBufferSize);
0809         qCCritical(KSTARS_FITS) << m_LastError;
0810         libraw_dcraw_clear_mem(image);
0811         clearImageBuffers();
0812         return false;
0813     }
0814 
0815     auto destination_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
0816     auto source_buffer = reinterpret_cast<uint8_t *>(image->data);
0817 
0818     // For mono, we memcpy directly
0819     if (image->colors == 1)
0820     {
0821         memcpy(destination_buffer, source_buffer, m_ImageBufferSize);
0822     }
0823     else
0824     {
0825         // Data in RGB24, with bytes in the order of R,G,B. We copy them copy them into 3 layers for FITS
0826         uint8_t * rBuff = destination_buffer;
0827         uint8_t * gBuff = destination_buffer + (m_Statistics.width * m_Statistics.height);
0828         uint8_t * bBuff = destination_buffer + (m_Statistics.width * m_Statistics.height * 2);
0829 
0830         int imax = m_Statistics.samples_per_channel * 3 - 3;
0831         for (int i = 0; i <= imax; i += 3)
0832         {
0833             *rBuff++ = source_buffer[i + 0];
0834             *gBuff++ = source_buffer[i + 1];
0835             *bBuff++ = source_buffer[i + 2];
0836         }
0837     }
0838     libraw_dcraw_clear_mem(image);
0839 
0840     calculateStats(false, false);
0841     return true;
0842 #endif
0843 }
0844 
0845 bool FITSData::saveImage(const QString &newFilename)
0846 {
0847     if (newFilename == m_Filename)
0848         return true;
0849 
0850     const QString ext = QFileInfo(newFilename).suffix();
0851 
0852     if (ext == "jpg" || ext == "png")
0853     {
0854         double min, max;
0855         QImage fitsImage = FITSToImage(newFilename);
0856         getMinMax(&min, &max);
0857 
0858         if (min == max)
0859         {
0860             fitsImage.fill(Qt::white);
0861         }
0862         else if (channels() == 1)
0863         {
0864             fitsImage = QImage(width(), height(), QImage::Format_Indexed8);
0865 
0866             fitsImage.setColorCount(256);
0867             for (int i = 0; i < 256; i++)
0868                 fitsImage.setColor(i, qRgb(i, i, i));
0869         }
0870         else
0871         {
0872             fitsImage = QImage(width(), height(), QImage::Format_RGB32);
0873         }
0874 
0875         double dataMin = m_Statistics.mean[0] - m_Statistics.stddev[0];
0876         double dataMax = m_Statistics.mean[0] + m_Statistics.stddev[0] * 3;
0877 
0878         double bscale = 255. / (dataMax - dataMin);
0879         double bzero  = (-dataMin) * (255. / (dataMax - dataMin));
0880 
0881         // Long way to do this since we do not want to use templated functions here
0882         switch (m_Statistics.dataType)
0883         {
0884             case TBYTE:
0885                 convertToQImage<uint8_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0886                 break;
0887 
0888             case TSHORT:
0889                 convertToQImage<int16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0890                 break;
0891 
0892             case TUSHORT:
0893                 convertToQImage<uint16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0894                 break;
0895 
0896             case TLONG:
0897                 convertToQImage<int32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0898                 break;
0899 
0900             case TULONG:
0901                 convertToQImage<uint32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0902                 break;
0903 
0904             case TFLOAT:
0905                 convertToQImage<float>(dataMin, dataMax, bscale, bzero, fitsImage);
0906                 break;
0907 
0908             case TLONGLONG:
0909                 convertToQImage<int64_t>(dataMin, dataMax, bscale, bzero, fitsImage);
0910                 break;
0911 
0912             case TDOUBLE:
0913                 convertToQImage<double>(dataMin, dataMax, bscale, bzero, fitsImage);
0914                 break;
0915 
0916             default:
0917                 break;
0918         }
0919 
0920         fitsImage.save(newFilename, ext.toLatin1().constData());
0921 
0922         m_Filename = newFilename;
0923         qCInfo(KSTARS_FITS) << "Saved image file:" << m_Filename;
0924         return true;
0925     }
0926 
0927     if (m_isCompressed)
0928     {
0929         m_LastError = i18n("Saving compressed files is not supported.");
0930         return false;
0931     }
0932 
0933     int status = 0;
0934     long nelements;
0935     fitsfile * new_fptr;
0936 
0937     if (ext == "xisf")
0938     {
0939         if(fptr)
0940         {
0941             fits_close_file(fptr, &status);
0942             fptr = nullptr;
0943         }
0944         rotCounter = flipHCounter = flipVCounter = 0;
0945         return saveXISFImage(newFilename);
0946     }
0947 
0948     //    if (HasDebayer && m_Filename.isEmpty() == false)
0949     //    {
0950     //        fits_flush_file(fptr, &status);
0951     //        /* close current file */
0952     //        if (fits_close_file(fptr, &status))
0953     //        {
0954     //            recordLastError(status);
0955     //            return status;
0956     //        }
0957 
0958     //        // Skip "!" in the beginning of the new file name
0959     //        QString finalFileName(newFilename);
0960 
0961     //        finalFileName.remove('!');
0962 
0963     //        // Remove first otherwise copy will fail below if file exists
0964     //        QFile::remove(finalFileName);
0965 
0966     //        if (!QFile::copy(m_Filename, finalFileName))
0967     //        {
0968     //            qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName;
0969     //            fptr = nullptr;
0970     //            return false;
0971     //        }
0972 
0973     //        m_Filename = finalFileName;
0974 
0975     //        // Use open diskfile as it does not use extended file names which has problems opening
0976     //        // files with [ ] or ( ) in their names.
0977     //        fits_open_diskfile(&fptr, m_Filename.toLocal8Bit(), READONLY, &status);
0978     //        fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status);
0979 
0980     //        return true;
0981     //    }
0982 
0983     // Read the image back into buffer in case we debyayed
0984     nelements = m_Statistics.samples_per_channel * m_Statistics.channels;
0985 
0986     /* close current file */
0987     if (fptr && fits_close_file(fptr, &status))
0988     {
0989         m_LastError = i18n("Failed to close file: %1", fitsErrorToString(status));
0990         return false;
0991     }
0992 
0993     /* Create a new File, overwriting existing*/
0994     if (fits_create_file(&new_fptr, QString("!%1").arg(newFilename).toLocal8Bit(), &status))
0995     {
0996         m_LastError = i18n("Failed to create file: %1", fitsErrorToString(status));
0997         return status;
0998     }
0999 
1000     status = 0;
1001 
1002     fptr = new_fptr;
1003 
1004     // Create image
1005     long naxis = m_Statistics.channels == 1 ? 2 : 3;
1006     long naxes[3] = {m_Statistics.width, m_Statistics.height, naxis};
1007 
1008     // JM 2020-12-28: Here we to use bitpix values
1009     if (fits_create_img(fptr, m_FITSBITPIX, naxis, naxes, &status))
1010     {
1011         m_LastError = i18n("Failed to create image: %1", fitsErrorToString(status));
1012         return false;
1013     }
1014 
1015     /* Write keywords */
1016 
1017     // Minimum
1018     if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(m_Statistics.min), "Minimum value", &status))
1019     {
1020         m_LastError = i18n("Failed to update key: %1", fitsErrorToString(status));
1021         return false;
1022     }
1023 
1024     // Maximum
1025     if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(m_Statistics.max), "Maximum value", &status))
1026     {
1027         m_LastError = i18n("Failed to update key: %1", fitsErrorToString(status));
1028         return false;
1029     }
1030 
1031     // KStars Min, for 3 channels
1032     fits_write_key(fptr, TDOUBLE, "MIN1", &m_Statistics.min[0], "Min Channel 1", &status);
1033     if (channels() > 1)
1034     {
1035         fits_write_key(fptr, TDOUBLE, "MIN2", &m_Statistics.min[1], "Min Channel 2", &status);
1036         fits_write_key(fptr, TDOUBLE, "MIN3", &m_Statistics.min[2], "Min Channel 3", &status);
1037     }
1038 
1039     // KStars max, for 3 channels
1040     fits_write_key(fptr, TDOUBLE, "MAX1", &m_Statistics.max[0], "Max Channel 1", &status);
1041     if (channels() > 1)
1042     {
1043         fits_write_key(fptr, TDOUBLE, "MAX2", &m_Statistics.max[1], "Max Channel 2", &status);
1044         fits_write_key(fptr, TDOUBLE, "MAX3", &m_Statistics.max[2], "Max Channel 3", &status);
1045     }
1046 
1047     // Mean
1048     if (m_Statistics.mean[0] > 0)
1049     {
1050         fits_write_key(fptr, TDOUBLE, "MEAN1", &m_Statistics.mean[0], "Mean Channel 1", &status);
1051         if (channels() > 1)
1052         {
1053             fits_write_key(fptr, TDOUBLE, "MEAN2", &m_Statistics.mean[1], "Mean Channel 2", &status);
1054             fits_write_key(fptr, TDOUBLE, "MEAN3", &m_Statistics.mean[2], "Mean Channel 3", &status);
1055         }
1056     }
1057 
1058     // Median
1059     if (m_Statistics.median[0] > 0)
1060     {
1061         fits_write_key(fptr, TDOUBLE, "MEDIAN1", &m_Statistics.median[0], "Median Channel 1", &status);
1062         if (channels() > 1)
1063         {
1064             fits_write_key(fptr, TDOUBLE, "MEDIAN2", &m_Statistics.median[1], "Median Channel 2", &status);
1065             fits_write_key(fptr, TDOUBLE, "MEDIAN3", &m_Statistics.median[2], "Median Channel 3", &status);
1066         }
1067     }
1068 
1069     // Standard Deviation
1070     if (m_Statistics.stddev[0] > 0)
1071     {
1072         fits_write_key(fptr, TDOUBLE, "STDDEV1", &m_Statistics.stddev[0], "Standard Deviation Channel 1", &status);
1073         if (channels() > 1)
1074         {
1075             fits_write_key(fptr, TDOUBLE, "STDDEV2", &m_Statistics.stddev[1], "Standard Deviation Channel 2", &status);
1076             fits_write_key(fptr, TDOUBLE, "STDDEV3", &m_Statistics.stddev[2], "Standard Deviation Channel 3", &status);
1077         }
1078     }
1079 
1080     // Skip first 10 standard records and copy the rest.
1081     for (int i = 10; i < m_HeaderRecords.count(); i++)
1082     {
1083         QString key = m_HeaderRecords[i].key;
1084         const char *comment = m_HeaderRecords[i].comment.toLatin1().constBegin();
1085         QVariant value = m_HeaderRecords[i].value;
1086 
1087         switch (value.type())
1088         {
1089             case QVariant::Int:
1090             {
1091                 int number = value.toInt();
1092                 fits_write_key(fptr, TINT, key.toLatin1().constData(), &number, comment, &status);
1093             }
1094             break;
1095 
1096             case QVariant::Double:
1097             {
1098                 double number = value.toDouble();
1099                 fits_write_key(fptr, TDOUBLE, key.toLatin1().constData(), &number, comment, &status);
1100             }
1101             break;
1102 
1103             case QVariant::String:
1104             default:
1105             {
1106                 char valueBuffer[256] = {0};
1107                 strncpy(valueBuffer, value.toString().toLatin1().constData(), 256 - 1);
1108                 fits_write_key(fptr, TSTRING, key.toLatin1().constData(), valueBuffer, comment, &status);
1109             }
1110         }
1111     }
1112 
1113     // ISO Date
1114     if (fits_write_date(fptr, &status))
1115     {
1116         m_LastError = i18n("Failed to update date: %1", fitsErrorToString(status));
1117         return false;
1118     }
1119 
1120     QString history =
1121         QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"));
1122     // History
1123     if (fits_write_history(fptr, history.toLatin1(), &status))
1124     {
1125         m_LastError = i18n("Failed to update history: %1", fitsErrorToString(status));
1126         return false;
1127     }
1128 
1129     int rot = 0, mirror = 0;
1130     if (rotCounter > 0)
1131     {
1132         rot = (90 * rotCounter) % 360;
1133         if (rot < 0)
1134             rot += 360;
1135     }
1136     if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0)
1137         mirror = 1;
1138 
1139     if ((rot != 0) || (mirror != 0))
1140         rotWCSFITS(rot, mirror);
1141 
1142     rotCounter = flipHCounter = flipVCounter = 0;
1143 
1144     // Here we need to use the actual data type
1145     if (fits_write_img(fptr, m_Statistics.dataType, 1, nelements, m_ImageBuffer, &status))
1146     {
1147         m_LastError = i18n("Failed to write image: %1", fitsErrorToString(status));
1148         return false;
1149     }
1150 
1151     m_Filename = newFilename;
1152 
1153     fits_flush_file(fptr, &status);
1154 
1155     qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename;
1156 
1157     return true;
1158 }
1159 
1160 void FITSData::clearImageBuffers()
1161 {
1162     delete[] m_ImageBuffer;
1163     m_ImageBuffer = nullptr;
1164     if(m_ImageRoiBuffer != nullptr )
1165     {
1166         delete[] m_ImageRoiBuffer;
1167         m_ImageRoiBuffer = nullptr;
1168 
1169     }
1170     //m_BayerBuffer = nullptr;
1171 }
1172 
1173 void FITSData::makeRoiBuffer(QRect roi)
1174 {
1175     if (!roi.isValid())
1176         return;
1177 
1178     uint32_t channelSize = roi.height() * roi.width();
1179     if(channelSize  > m_Statistics.samples_per_channel || channelSize == 1)
1180     {
1181         return;
1182     }
1183     if(m_ImageRoiBuffer != nullptr )
1184     {
1185         delete[] m_ImageRoiBuffer;
1186         m_ImageRoiBuffer = nullptr;
1187     }
1188     int xoffset = roi.topLeft().x() - 1;
1189     int yoffset = roi.topLeft().y() - 1;
1190     uint32_t bpp = m_Statistics.bytesPerPixel;
1191     m_ImageRoiBuffer = new uint8_t[roi.height()*roi.width()*m_Statistics.channels * m_Statistics.bytesPerPixel]();
1192     for(int n = 0 ; n < m_Statistics.channels ; n++)
1193     {
1194         for(int i = 0; i < roi.height(); i++)
1195         {
1196             size_t i1 = n * channelSize * bpp +  i * roi.width() * bpp;
1197             size_t i2 = n * m_Statistics.samples_per_channel * bpp + (yoffset + i) * width() * bpp + xoffset * bpp;
1198             memcpy(&m_ImageRoiBuffer[i1],
1199                    &m_ImageBuffer[i2],
1200                    roi.width() * bpp);
1201         }
1202 
1203     }
1204     memcpy(&m_ROIStatistics, &m_Statistics, sizeof(FITSImage::Statistic));
1205     m_ROIStatistics.samples_per_channel = roi.height() * roi.width();
1206     m_ROIStatistics.width = roi.width();
1207     m_ROIStatistics.height = roi.height();
1208     calculateStats(false, true);
1209 }
1210 void FITSData::calculateStats(bool refresh, bool roi)
1211 {
1212     // Calculate min max
1213     if(roi == false)
1214     {
1215         calculateMinMax(refresh);
1216         calculateMedian(refresh);
1217 
1218         // Try to read mean/median/stddev if in file
1219         if (refresh == false && fptr)
1220         {
1221             int status = 0;
1222             int nfound = 0;
1223             if (fits_read_key_dbl(fptr, "MEAN1", &m_Statistics.mean[0], nullptr, &status) == 0)
1224                 nfound++;
1225             // NB. These could fail if missing, which is OK.
1226             fits_read_key_dbl(fptr, "MEAN2", & m_Statistics.mean[1], nullptr, &status);
1227             fits_read_key_dbl(fptr, "MEAN3", &m_Statistics.mean[2], nullptr, &status);
1228 
1229             status = 0;
1230             if (fits_read_key_dbl(fptr, "STDDEV1", &m_Statistics.stddev[0], nullptr, &status) == 0)
1231                 nfound++;
1232             // NB. These could fail if missing, which is OK.
1233             fits_read_key_dbl(fptr, "STDDEV2", &m_Statistics.stddev[1], nullptr, &status);
1234             fits_read_key_dbl(fptr, "STDDEV3", &m_Statistics.stddev[2], nullptr, &status);
1235 
1236             // If all is OK, we're done
1237             if (nfound == 2)
1238                 return;
1239         }
1240 
1241         // Get standard deviation and mean in one run
1242         switch (m_Statistics.dataType)
1243         {
1244             case TBYTE:
1245                 calculateStdDev<uint8_t>();
1246                 break;
1247 
1248             case TSHORT:
1249                 calculateStdDev<int16_t>();
1250                 break;
1251 
1252             case TUSHORT:
1253                 calculateStdDev<uint16_t>();
1254                 break;
1255 
1256             case TLONG:
1257                 calculateStdDev<int32_t>();
1258                 break;
1259 
1260             case TULONG:
1261                 calculateStdDev<uint32_t>();
1262                 break;
1263 
1264             case TFLOAT:
1265                 calculateStdDev<float>();
1266                 break;
1267 
1268             case TLONGLONG:
1269                 calculateStdDev<int64_t>();
1270                 break;
1271 
1272             case TDOUBLE:
1273                 calculateStdDev<double>();
1274                 break;
1275 
1276             default:
1277                 return;
1278         }
1279 
1280         // FIXME That's not really SNR, must implement a proper solution for this value
1281         m_Statistics.SNR = m_Statistics.mean[0] / m_Statistics.stddev[0];
1282     }
1283     else
1284     {
1285         calculateMinMax(refresh, roi);
1286         calculateMedian(refresh, roi);
1287 
1288         switch (m_ROIStatistics.dataType)
1289         {
1290             case TBYTE:
1291                 calculateStdDev<uint8_t>(roi);
1292                 break;
1293 
1294             case TSHORT:
1295                 calculateStdDev<int16_t>(roi);
1296                 break;
1297 
1298             case TUSHORT:
1299                 calculateStdDev<uint16_t>(roi);
1300                 break;
1301 
1302             case TLONG:
1303                 calculateStdDev<int32_t>(roi);
1304                 break;
1305 
1306             case TULONG:
1307                 calculateStdDev<uint32_t>(roi);
1308                 break;
1309 
1310             case TFLOAT:
1311                 calculateStdDev<float>(roi);
1312                 break;
1313 
1314             case TLONGLONG:
1315                 calculateStdDev<int64_t>(roi);
1316                 break;
1317 
1318             case TDOUBLE:
1319                 calculateStdDev<double>(roi);
1320                 break;
1321 
1322             default:
1323                 return;
1324         }
1325     }
1326 }
1327 
1328 void FITSData::calculateMinMax(bool refresh, bool roi)
1329 {
1330     if(roi == false)
1331     {
1332         int status, nfound = 0;
1333 
1334         status = 0;
1335 
1336         // Only fetch from header if we have a single channel
1337         // Otherwise, calculate manually.
1338         if (fptr != nullptr && !refresh)
1339         {
1340 
1341             status = 0;
1342 
1343             if (fits_read_key_dbl(fptr, "DATAMIN", &(m_Statistics.min[0]), nullptr, &status) == 0)
1344                 nfound++;
1345             else if (fits_read_key_dbl(fptr, "MIN1", &(m_Statistics.min[0]), nullptr, &status) == 0)
1346                 nfound++;
1347 
1348             // NB. These could fail if missing, which is OK.
1349             fits_read_key_dbl(fptr, "MIN2", &m_Statistics.min[1], nullptr, &status);
1350             fits_read_key_dbl(fptr, "MIN3", &m_Statistics.min[2], nullptr, &status);
1351 
1352             status = 0;
1353 
1354             if (fits_read_key_dbl(fptr, "DATAMAX", &(m_Statistics.max[0]), nullptr, &status) == 0)
1355                 nfound++;
1356             else if (fits_read_key_dbl(fptr, "MAX1", &(m_Statistics.max[0]), nullptr, &status) == 0)
1357                 nfound++;
1358 
1359             // NB. These could fail if missing, which is OK.
1360             fits_read_key_dbl(fptr, "MAX2", &m_Statistics.max[1], nullptr, &status);
1361             fits_read_key_dbl(fptr, "MAX3", &m_Statistics.max[2], nullptr, &status);
1362 
1363             // If we found both keywords, no need to calculate them, unless they are both zeros
1364             if (nfound == 2 && !(m_Statistics.min[0] == 0 && m_Statistics.max[0] == 0))
1365                 return;
1366         }
1367 
1368         m_Statistics.min[0] = 1.0E30;
1369         m_Statistics.max[0] = -1.0E30;
1370 
1371         m_Statistics.min[1] = 1.0E30;
1372         m_Statistics.max[1] = -1.0E30;
1373 
1374         m_Statistics.min[2] = 1.0E30;
1375         m_Statistics.max[2] = -1.0E30;
1376 
1377         switch (m_Statistics.dataType)
1378         {
1379             case TBYTE:
1380                 calculateMinMax<uint8_t>();
1381                 break;
1382 
1383             case TSHORT:
1384                 calculateMinMax<int16_t>();
1385                 break;
1386 
1387             case TUSHORT:
1388                 calculateMinMax<uint16_t>();
1389                 break;
1390 
1391             case TLONG:
1392                 calculateMinMax<int32_t>();
1393                 break;
1394 
1395             case TULONG:
1396                 calculateMinMax<uint32_t>();
1397                 break;
1398 
1399             case TFLOAT:
1400                 calculateMinMax<float>();
1401                 break;
1402 
1403             case TLONGLONG:
1404                 calculateMinMax<int64_t>();
1405                 break;
1406 
1407             case TDOUBLE:
1408                 calculateMinMax<double>();
1409                 break;
1410 
1411             default:
1412                 break;
1413         }
1414     }
1415     else
1416     {
1417         m_ROIStatistics.min[0] = 1.0E30;
1418         m_ROIStatistics.max[0] = -1.0E30;
1419 
1420         m_ROIStatistics.min[1] = 1.0E30;
1421         m_ROIStatistics.max[1] = -1.0E30;
1422 
1423         m_ROIStatistics.min[2] = 1.0E30;
1424         m_ROIStatistics.max[2] = -1.0E30;
1425 
1426         switch (m_Statistics.dataType)
1427         {
1428             case TBYTE:
1429                 calculateMinMax<uint8_t>(roi);
1430                 break;
1431 
1432             case TSHORT:
1433                 calculateMinMax<int16_t>(roi);
1434                 break;
1435 
1436             case TUSHORT:
1437                 calculateMinMax<uint16_t>(roi);
1438                 break;
1439 
1440             case TLONG:
1441                 calculateMinMax<int32_t>(roi);
1442                 break;
1443 
1444             case TULONG:
1445                 calculateMinMax<uint32_t>(roi);
1446                 break;
1447 
1448             case TFLOAT:
1449                 calculateMinMax<float>(roi);
1450                 break;
1451 
1452             case TLONGLONG:
1453                 calculateMinMax<int64_t>(roi);
1454                 break;
1455 
1456             case TDOUBLE:
1457                 calculateMinMax<double>(roi);
1458                 break;
1459 
1460             default:
1461                 break;
1462         }
1463 
1464     }
1465 }
1466 
1467 void FITSData::calculateMedian(bool refresh, bool roi)
1468 {
1469     if(!roi)
1470     {
1471         int status, nfound = 0;
1472 
1473         status = 0;
1474 
1475         // Only fetch from header if we have a single channel
1476         // Otherwise, calculate manually.
1477         if (fptr != nullptr && !refresh)
1478         {
1479             status = 0;
1480             if (fits_read_key_dbl(fptr, "MEDIAN1", &m_Statistics.median[0], nullptr, &status) == 0)
1481                 nfound++;
1482 
1483             // NB. These could fail if missing, which is OK.
1484             fits_read_key_dbl(fptr, "MEDIAN2", &m_Statistics.median[1], nullptr, &status);
1485             fits_read_key_dbl(fptr, "MEDIAN3", &m_Statistics.median[2], nullptr, &status);
1486 
1487             if (nfound == 1)
1488                 return;
1489         }
1490 
1491         m_Statistics.median[RED_CHANNEL] = 0;
1492         m_Statistics.median[GREEN_CHANNEL] = 0;
1493         m_Statistics.median[BLUE_CHANNEL] = 0;
1494 
1495         switch (m_Statistics.dataType)
1496         {
1497             case TBYTE:
1498                 calculateMedian<uint8_t>();
1499                 break;
1500 
1501             case TSHORT:
1502                 calculateMedian<int16_t>();
1503                 break;
1504 
1505             case TUSHORT:
1506                 calculateMedian<uint16_t>();
1507                 break;
1508 
1509             case TLONG:
1510                 calculateMedian<int32_t>();
1511                 break;
1512 
1513             case TULONG:
1514                 calculateMedian<uint32_t>();
1515                 break;
1516 
1517             case TFLOAT:
1518                 calculateMedian<float>();
1519                 break;
1520 
1521             case TLONGLONG:
1522                 calculateMedian<int64_t>();
1523                 break;
1524 
1525             case TDOUBLE:
1526                 calculateMedian<double>();
1527                 break;
1528 
1529             default:
1530                 break;
1531         }
1532     }
1533     else
1534     {
1535         m_ROIStatistics.median[RED_CHANNEL] = 0;
1536         m_ROIStatistics.median[GREEN_CHANNEL] = 0;
1537         m_ROIStatistics.median[BLUE_CHANNEL] = 0;
1538 
1539         switch (m_ROIStatistics.dataType)
1540         {
1541             case TBYTE:
1542                 calculateMedian<uint8_t>(roi);
1543                 break;
1544 
1545             case TSHORT:
1546                 calculateMedian<int16_t>(roi);
1547                 break;
1548 
1549             case TUSHORT:
1550                 calculateMedian<uint16_t>(roi);
1551                 break;
1552 
1553             case TLONG:
1554                 calculateMedian<int32_t>(roi);
1555                 break;
1556 
1557             case TULONG:
1558                 calculateMedian<uint32_t>(roi);
1559                 break;
1560 
1561             case TFLOAT:
1562                 calculateMedian<float>(roi);
1563                 break;
1564 
1565             case TLONGLONG:
1566                 calculateMedian<int64_t>(roi);
1567                 break;
1568 
1569             case TDOUBLE:
1570                 calculateMedian<double>(roi);
1571                 break;
1572 
1573             default:
1574                 break;
1575         }
1576 
1577     }
1578 }
1579 
1580 template <typename T>
1581 void FITSData::calculateMedian(bool roi)
1582 {
1583     auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
1584     const uint32_t maxMedianSize = 500000;
1585     uint32_t medianSize = roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel;
1586     uint8_t downsample = 1;
1587     if (medianSize > maxMedianSize)
1588     {
1589         downsample = (static_cast<double>(medianSize) / maxMedianSize) + 0.999;
1590         medianSize /= downsample;
1591     }
1592     // Ideally samples would be declared like this...
1593     //std::vector<T> samples;
1594     // Unfortunately this doesn't compile on Mac - see the comments in robuststatistics.cpp for more details
1595     // So for now declare samples like this...
1596     std::vector<int32_t> samples;
1597     samples.reserve(medianSize);
1598 
1599     for (uint8_t n = 0; n < m_Statistics.channels; n++)
1600     {
1601         auto *oneChannel = buffer + n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1602         for (uint32_t upto = 0; upto < (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1603                 upto += downsample)
1604             samples.push_back(oneChannel[upto]);
1605         auto median = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_MEDIAN, samples);
1606         roi ? m_ROIStatistics.median[n] = median : m_Statistics.median[n] = median;
1607     }
1608 }
1609 
1610 template <typename T>
1611 QPair<T, T> FITSData::getParitionMinMax(uint32_t start, uint32_t stride, bool roi)
1612 {
1613     auto * buffer = reinterpret_cast<T *>(roi ? m_ImageRoiBuffer : m_ImageBuffer);
1614     T min = std::numeric_limits<T>::max();
1615     T max = std::numeric_limits<T>::min();
1616 
1617     uint32_t end = start + stride;
1618 
1619     for (uint32_t i = start; i < end; i++)
1620     {
1621         min = qMin(buffer[i], min);
1622         max = qMax(buffer[i], max);
1623         //        if (buffer[i] < min)
1624         //            min = buffer[i];
1625         //        else if (buffer[i] > max)
1626         //            max = buffer[i];
1627     }
1628 
1629     return qMakePair(min, max);
1630 }
1631 
1632 template <typename T>
1633 void FITSData::calculateMinMax(bool roi)
1634 {
1635     T min = std::numeric_limits<T>::max();
1636     T max = std::numeric_limits<T>::min();
1637 
1638 
1639     // Create N threads
1640     const uint8_t nThreads = 16;
1641 
1642     for (int n = 0; n < m_Statistics.channels; n++)
1643     {
1644         uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1645 
1646         // Calculate how many elements we process per thread
1647         uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;
1648 
1649         // Calculate the final stride since we can have some left over due to division above
1650         uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
1651                                       (tStride * nThreads));
1652 
1653         // Start location for inspecting elements
1654         uint32_t tStart = cStart;
1655 
1656         // List of futures
1657         QList<QFuture<QPair<T, T>>> futures;
1658 
1659         for (int i = 0; i < nThreads; i++)
1660         {
1661             // Run threads
1662             futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax<T>, tStart, (i == (nThreads - 1)) ? fStride : tStride,
1663                                              roi));
1664             tStart += tStride;
1665         }
1666 
1667         // Now wait for results
1668         for (int i = 0; i < nThreads; i++)
1669         {
1670             QPair<T, T> result = futures[i].result();
1671             min = qMin(result.first, min);
1672             max = qMax(result.second, max);
1673         }
1674 
1675         if(!roi)
1676         {
1677             m_Statistics.min[n] = min;
1678             m_Statistics.max[n] = max;
1679         }
1680         else
1681         {
1682             m_ROIStatistics.min[n] = min;
1683             m_ROIStatistics.max[n] = max;
1684         }
1685     }
1686 
1687 }
1688 
1689 // This struct is used when returning results from the threaded getSumAndSquaredSum calculations
1690 // used to compute the mean and variance of the image.
1691 struct SumData
1692 {
1693     double sum;
1694     double squaredSum;
1695     double numSamples;
1696     SumData(double s, double sq, int n) : sum(s), squaredSum(sq), numSamples(n) {}
1697     SumData() : sum(0), squaredSum(0), numSamples(0) {}
1698 };
1699 
1700 template <typename T>
1701 SumData getSumAndSquaredSum(uint32_t start, uint32_t stride, uint8_t *buff)
1702 {
1703     auto * buffer = reinterpret_cast<T *>(buff);
1704     const uint32_t end = start + stride;
1705     double sum = 0;
1706     double squaredSum = 0;
1707     for (uint32_t i = start; i < end; i++)
1708     {
1709         double sample = buffer[i];
1710         sum += sample;
1711         squaredSum += sample * sample;
1712     }
1713     const double numSamples = end - start;
1714     return SumData(sum, squaredSum, numSamples);
1715 }
1716 
1717 template <typename T>
1718 void FITSData::calculateStdDev(bool roi )
1719 {
1720     // Create N threads
1721     const uint8_t nThreads = 16;
1722 
1723     for (int n = 0; n < m_Statistics.channels; n++)
1724     {
1725         uint32_t cStart = n * (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel);
1726 
1727         // Calculate how many elements we process per thread
1728         uint32_t tStride = (roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) / nThreads;
1729 
1730         // Calculate the final stride since we can have some left over due to division above
1731         uint32_t fStride = tStride + ((roi ? m_ROIStatistics.samples_per_channel : m_Statistics.samples_per_channel) -
1732                                       (tStride * nThreads));
1733 
1734         // Start location for inspecting elements
1735         uint32_t tStart = cStart;
1736 
1737         // List of futures
1738         QList<QFuture<SumData>> futures;
1739 
1740         for (int i = 0; i < nThreads; i++)
1741         {
1742             // Run threads
1743             uint8_t *buff = roi ? m_ImageRoiBuffer : m_ImageBuffer;
1744             futures.append(QtConcurrent::run(&getSumAndSquaredSum<T>, tStart,
1745                                              (i == (nThreads - 1)) ? fStride : tStride, buff));
1746             tStart += tStride;
1747         }
1748 
1749         // Now wait for results
1750         double sum = 0, squared_sum = 0;
1751         double numSamples = 0;
1752         for (int i = 0; i < nThreads; i++)
1753         {
1754             const SumData result = futures[i].result();
1755             sum += result.sum;
1756             squared_sum += result.squaredSum;
1757             numSamples += result.numSamples;
1758 
1759         }
1760         if (numSamples <= 0) continue;
1761         const double mean = sum / numSamples;
1762         const double variance = squared_sum / numSamples - mean * mean;
1763         if(!roi)
1764         {
1765             m_Statistics.mean[n]   = mean;
1766             m_Statistics.stddev[n] = sqrt(variance);
1767         }
1768         else
1769         {
1770             m_ROIStatistics.mean[n] = mean;
1771             m_ROIStatistics.stddev[n] = sqrt(variance);
1772         }
1773     }
1774 }
1775 
1776 QVector<double> FITSData::createGaussianKernel(int size, double sigma)
1777 {
1778     QVector<double> kernel(size * size);
1779     kernel.fill(0.0, size * size);
1780 
1781     double kernelSum = 0.0;
1782     int fOff = (size - 1) / 2;
1783     double normal = 1.0 / (2.0 * M_PI * sigma * sigma);
1784     for (int y = -fOff; y <= fOff; y++)
1785     {
1786         for (int x = -fOff; x <= fOff; x++)
1787         {
1788             double distance = ((y * y) + (x * x)) / (2.0 * sigma * sigma);
1789             int index = (y + fOff) * size + (x + fOff);
1790             kernel[index] = normal * qExp(-distance);
1791             kernelSum += kernel.at(index);
1792         }
1793     }
1794     for (int y = 0; y < size; y++)
1795     {
1796         for (int x = 0; x < size; x++)
1797         {
1798             int index = y * size + x;
1799             kernel[index] = kernel.at(index) * 1.0 / kernelSum;
1800         }
1801     }
1802 
1803     return kernel;
1804 }
1805 
1806 template <typename T>
1807 void FITSData::convolutionFilter(const QVector<double> &kernel, int kernelSize)
1808 {
1809     T * imagePtr = reinterpret_cast<T *>(m_ImageBuffer);
1810 
1811     // Create variable for pixel data for each kernel
1812     T gt = 0;
1813 
1814     // This is how much your center pixel is offset from the border of your kernel
1815     int fOff = (kernelSize - 1) / 2;
1816 
1817     // Start with the pixel that is offset fOff from top and fOff from the left side
1818     // this is so entire kernel is on your image
1819     for (int offsetY = 0; offsetY < m_Statistics.height; offsetY++)
1820     {
1821         for (int offsetX = 0; offsetX < m_Statistics.width; offsetX++)
1822         {
1823             // reset gray value to 0
1824             gt = 0;
1825             // position of the kernel center pixel
1826             int byteOffset = offsetY * m_Statistics.width + offsetX;
1827 
1828             // kernel calculations
1829             for (int filterY = -fOff; filterY <= fOff; filterY++)
1830             {
1831                 for (int filterX = -fOff; filterX <= fOff; filterX++)
1832                 {
1833                     if ((offsetY + filterY) >= 0 && (offsetY + filterY) < m_Statistics.height
1834                             && ((offsetX + filterX) >= 0 && (offsetX + filterX) < m_Statistics.width ))
1835                     {
1836 
1837                         int calcOffset = byteOffset + filterX + filterY * m_Statistics.width;
1838                         int index = (filterY + fOff) * kernelSize + (filterX + fOff);
1839                         double kernelValue = kernel.at(index);
1840                         gt += (imagePtr[calcOffset]) * kernelValue;
1841                     }
1842                 }
1843             }
1844 
1845             // set new data in the other byte array for your image data
1846             imagePtr[byteOffset] = gt;
1847         }
1848     }
1849 }
1850 
1851 template <typename T>
1852 void FITSData::gaussianBlur(int kernelSize, double sigma)
1853 {
1854     // Size must be an odd number!
1855     if (kernelSize % 2 == 0)
1856     {
1857         kernelSize--;
1858         qCInfo(KSTARS_FITS) << "Warning, size must be an odd number, correcting size to " << kernelSize;
1859     }
1860     // Edge must be a positive number!
1861     if (kernelSize < 1)
1862     {
1863         kernelSize = 1;
1864     }
1865 
1866     QVector<double> gaussianKernel = createGaussianKernel(kernelSize, sigma);
1867     convolutionFilter<T>(gaussianKernel, kernelSize);
1868 }
1869 
1870 void FITSData::setMinMax(double newMin, double newMax, uint8_t channel)
1871 {
1872     m_Statistics.min[channel] = newMin;
1873     m_Statistics.max[channel] = newMax;
1874 }
1875 
1876 bool FITSData::parseHeader()
1877 {
1878     char * header = nullptr;
1879     int status = 0, nkeys = 0;
1880 
1881     if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status))
1882     {
1883         fits_report_error(stderr, status);
1884         free(header);
1885         return false;
1886     }
1887 
1888     m_HeaderRecords.clear();
1889     QString recordList = QString(header);
1890 
1891     for (int i = 0; i < nkeys; i++)
1892     {
1893         Record oneRecord;
1894         // Quotes cause issues for simplified below so we're removing them.
1895         QString record = recordList.mid(i * 80, 80).remove("'");
1896         QStringList properties = record.split(QRegExp("[=/]"));
1897         // If it is only a comment
1898         if (properties.size() == 1)
1899         {
1900             oneRecord.key = properties[0].mid(0, 7);
1901             oneRecord.comment = properties[0].mid(8).simplified();
1902         }
1903         else
1904         {
1905             oneRecord.key = properties[0].simplified();
1906             oneRecord.value = properties[1].simplified();
1907             if (properties.size() > 2)
1908                 oneRecord.comment = properties[2].simplified();
1909 
1910             // Try to guess the value.
1911             // Test for integer & double. If neither, then leave it as "string".
1912             bool ok = false;
1913 
1914             // Is it Integer?
1915             oneRecord.value.toInt(&ok);
1916             if (ok)
1917                 oneRecord.value.convert(QMetaType::Int);
1918             else
1919             {
1920                 // Is it double?
1921                 oneRecord.value.toDouble(&ok);
1922                 if (ok)
1923                     oneRecord.value.convert(QMetaType::Double);
1924             }
1925         }
1926 
1927         m_HeaderRecords.append(oneRecord);
1928     }
1929 
1930     free(header);
1931 
1932     return true;
1933 }
1934 
1935 bool FITSData::getRecordValue(const QString &key, QVariant &value) const
1936 {
1937     auto result = std::find_if(m_HeaderRecords.begin(), m_HeaderRecords.end(), [&key](const Record & oneRecord)
1938     {
1939         return (oneRecord.key == key && oneRecord.value.isValid());
1940     });
1941 
1942     if (result != m_HeaderRecords.end())
1943     {
1944         value = (*result).value;
1945         return true;
1946     }
1947     return false;
1948 }
1949 
1950 bool FITSData::parseSolution(FITSImage::Solution &solution) const
1951 {
1952     dms angleValue;
1953     bool raOK = false, deOK = false, coordOK = false, scaleOK = false;
1954     QVariant value;
1955 
1956     // Reset all
1957     solution.fieldWidth = solution.fieldHeight = solution.pixscale = solution.ra = solution.dec = -1;
1958 
1959     // RA
1960     if (getRecordValue("OBJCTRA", value))
1961     {
1962         angleValue = dms::fromString(value.toString(), false);
1963         solution.ra = angleValue.Degrees();
1964         raOK = true;
1965     }
1966     else if (getRecordValue("RA", value))
1967     {
1968         solution.ra = value.toDouble(&raOK);
1969     }
1970 
1971     // DE
1972     if (getRecordValue("OBJCTDEC", value))
1973     {
1974         angleValue = dms::fromString(value.toString(), true);
1975         solution.dec = angleValue.Degrees();
1976         deOK = true;
1977     }
1978     else if (getRecordValue("DEC", value))
1979     {
1980         solution.dec = value.toDouble(&deOK);
1981     }
1982 
1983     coordOK = raOK && deOK;
1984 
1985     // PixScale
1986     double scale = -1;
1987     if (getRecordValue("SCALE", value))
1988     {
1989         scale = value.toDouble();
1990     }
1991 
1992     double focal_length = -1;
1993     if (getRecordValue("FOCALLEN", value))
1994     {
1995         focal_length = value.toDouble();
1996     }
1997 
1998     double pixsize1 = -1, pixsize2 = -1;
1999     // Pixel Size 1
2000     if (getRecordValue("PIXSIZE1", value))
2001     {
2002         pixsize1 = value.toDouble();
2003     }
2004     // Pixel Size 2
2005     if (getRecordValue("PIXSIZE2", value))
2006     {
2007         pixsize2 = value.toDouble();
2008     }
2009 
2010     int binx = 1, biny = 1;
2011     // Binning X
2012     if (getRecordValue("XBINNING", value))
2013     {
2014         binx = value.toDouble();
2015     }
2016     // Binning Y
2017     if (getRecordValue("YBINNING", value))
2018     {
2019         biny = value.toDouble();
2020     }
2021 
2022     if (pixsize1 > 0 && pixsize2 > 0)
2023     {
2024         // If we have scale, then that's it
2025         if (scale > 0)
2026         {
2027             // Arcsecs per pixel
2028             solution.pixscale = scale;
2029             // Arcmins
2030             solution.fieldWidth = (m_Statistics.width * scale) / 60.0;
2031             // Arcmins, and account for pixel ratio if non-squared.
2032             solution.fieldHeight = (m_Statistics.height * scale * (pixsize2 / pixsize1)) / 60.0;
2033         }
2034         else if (focal_length > 0)
2035         {
2036             // Arcmins
2037             solution.fieldWidth = ((206264.8062470963552 * m_Statistics.width * (pixsize1 / 1000.0)) / (focal_length * binx)) / 60.0;
2038             // Arsmins
2039             solution.fieldHeight = ((206264.8062470963552 * m_Statistics.height * (pixsize2 / 1000.0)) / (focal_length * biny)) / 60.0;
2040             // Arcsecs per pixel
2041             solution.pixscale = (206264.8062470963552 * (pixsize1 / 1000.0)) / (focal_length * binx);
2042         }
2043 
2044         scaleOK = true;
2045     }
2046 
2047     return (coordOK || scaleOK);
2048 }
2049 
2050 QFuture<bool> FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox)
2051 {
2052     if (m_StarFindFuture.isRunning())
2053         m_StarFindFuture.waitForFinished();
2054 
2055     starAlgorithm = algorithm;
2056     qDeleteAll(starCenters);
2057     starCenters.clear();
2058     starsSearched = true;
2059 
2060     switch (algorithm)
2061     {
2062         case ALGORITHM_SEP:
2063         {
2064             m_StarDetector.reset(new FITSSEPDetector(this));
2065             m_StarDetector->setSettings(m_SourceExtractorSettings);
2066             if (m_Mode == FITS_NORMAL && trackingBox.isNull())
2067             {
2068                 if (Options::quickHFR())
2069                 {
2070                     //Just finds stars in the center 25% of the image.
2071                     const int w = getStatistics().width;
2072                     const int h = getStatistics().height;
2073                     QRect middle(static_cast<int>(w * 0.25), static_cast<int>(h * 0.25), w / 2, h / 2);
2074                     m_StarFindFuture = m_StarDetector->findSources(middle);
2075                     return m_StarFindFuture;
2076                 }
2077             }
2078             m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2079             return m_StarFindFuture;
2080         }
2081 
2082         case ALGORITHM_GRADIENT:
2083         default:
2084         {
2085             m_StarDetector.reset(new FITSGradientDetector(this));
2086             m_StarDetector->setSettings(m_SourceExtractorSettings);
2087             m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2088             return m_StarFindFuture;
2089         }
2090 
2091         case ALGORITHM_CENTROID:
2092         {
2093 #ifndef KSTARS_LITE
2094             m_StarDetector.reset(new FITSCentroidDetector(this));
2095             m_StarDetector->setSettings(m_SourceExtractorSettings);
2096             // We need JMIndex calculated from histogram
2097             if (!isHistogramConstructed())
2098                 constructHistogram();
2099             m_StarDetector->configure("JMINDEX", m_JMIndex);
2100             m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2101             return m_StarFindFuture;
2102         }
2103 #else
2104             {
2105                 m_StarDetector.reset(new FITSCentroidDetector(this));
2106                 m_StarFindFuture = starDetector->findSources(trackingBox);
2107                 return m_StarFindFuture;
2108             }
2109 #endif
2110 
2111         case ALGORITHM_THRESHOLD:
2112         {
2113             m_StarDetector.reset(new FITSThresholdDetector(this));
2114             m_StarDetector->setSettings(m_SourceExtractorSettings);
2115             m_StarDetector->configure("THRESHOLD_PERCENTAGE", Options::focusThreshold());
2116             m_StarFindFuture =  m_StarDetector->findSources(trackingBox);
2117             return m_StarFindFuture;
2118         }
2119 
2120         case ALGORITHM_BAHTINOV:
2121         {
2122             m_StarDetector.reset(new FITSBahtinovDetector(this));
2123             m_StarDetector->setSettings(m_SourceExtractorSettings);
2124             m_StarDetector->configure("NUMBER_OF_AVERAGE_ROWS", Options::focusMultiRowAverage());
2125             m_StarFindFuture = m_StarDetector->findSources(trackingBox);
2126             return m_StarFindFuture;
2127         }
2128     }
2129 }
2130 
2131 int FITSData::filterStars(QSharedPointer<ImageMask> mask)
2132 {
2133     if (mask.isNull() == false)
2134     {
2135         starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(),
2136                                          [&](Edge * edge)
2137         {
2138             return (mask->isVisible(edge->x, edge->y) == false);
2139         }), starCenters.end());
2140     }
2141 
2142     return starCenters.count();
2143 
2144 }
2145 
2146 double FITSData::getHFR(HFRType type)
2147 {
2148     if (starCenters.empty())
2149         return -1;
2150 
2151     if (cacheHFR >= 0 && cacheHFRType == type)
2152         return cacheHFR;
2153 
2154     m_SelectedHFRStar.invalidate();
2155 
2156     if (type == HFR_MAX)
2157     {
2158 
2159         int maxVal   = 0;
2160         int maxIndex = 0;
2161         for (int i = 0; i < starCenters.count(); i++)
2162         {
2163             if (starCenters[i]->HFR > maxVal)
2164             {
2165                 maxIndex = i;
2166                 maxVal   = starCenters[i]->HFR;
2167             }
2168         }
2169 
2170         m_SelectedHFRStar = *starCenters[maxIndex];
2171         cacheHFR = starCenters[maxIndex]->HFR;
2172         cacheHFRType = type;
2173         return cacheHFR;
2174     }
2175     else if (type == HFR_HIGH)
2176     {
2177         // Reject all stars within 10% of border
2178         int minX = width() / 10;
2179         int minY = height() / 10;
2180         int maxX = width() - minX;
2181         int maxY = height() - minY;
2182         starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(), [minX, minY, maxX, maxY](Edge * oneStar)
2183         {
2184             return (oneStar->x < minX || oneStar->x > maxX || oneStar->y < minY || oneStar->y > maxY);
2185         }), starCenters.end());
2186         // Top 5%
2187         if (starCenters.empty())
2188             return -1;
2189 
2190         m_SelectedHFRStar = *starCenters[static_cast<int>(starCenters.size() * 0.05)];
2191         cacheHFR = m_SelectedHFRStar.HFR;
2192         cacheHFRType = type;
2193         return cacheHFR;
2194     }
2195     else if (type == HFR_MEDIAN)
2196     {
2197         std::nth_element(starCenters.begin(), starCenters.begin() + starCenters.size() / 2, starCenters.end());
2198         m_SelectedHFRStar = *starCenters[starCenters.size() / 2];
2199 
2200         cacheHFR = m_SelectedHFRStar.HFR;
2201         cacheHFRType = type;
2202         return cacheHFR;
2203     }
2204 
2205     // We may remove saturated stars from the HFR calculation, if we have enough stars.
2206     // No real way to tell the scale, so only remove saturated stars with range 0 -> 2**16
2207     // for non-byte types. Unsigned types and floating types, or really any pixels whose
2208     // range isn't 0-64 (or 0-255 for bytes) won't have their saturated stars removed.
2209     const int saturationValue = m_Statistics.dataType == TBYTE ? 250 : 50000;
2210     int numSaturated = 0;
2211     for (auto center : starCenters)
2212         if (center->val > saturationValue)
2213             numSaturated++;
2214     const bool removeSaturatedStars = starCenters.size() - numSaturated > 20;
2215     if (removeSaturatedStars && numSaturated > 0)
2216         qCDebug(KSTARS_FITS) << "Removing " << numSaturated << " stars from HFR calculation";
2217 
2218     std::vector<double> HFRs;
2219 
2220     for (auto center : starCenters)
2221     {
2222         if (removeSaturatedStars && center->val > saturationValue) continue;
2223 
2224         if (type == HFR_AVERAGE)
2225             HFRs.push_back(center->HFR);
2226         else
2227         {
2228             // HFR_ADJ_AVERAGE - so adjust the HFR based on the stars brightness
2229             // HFRadj = HFR.erf(sqrt(ln(peak/background)))/(1 - background/peak)
2230             // Sanity check inputs to prevent equation blowing up
2231             if (m_SkyBackground.mean <= 0.0 || center->val < m_SkyBackground.mean)
2232             {
2233                 HFRs.push_back(center->HFR);
2234                 qCDebug(KSTARS_FITS) << "HFR Adj, sky background " << m_SkyBackground.mean << " star peak " << center->val <<
2235                                      " not adjusting";
2236             }
2237             else
2238             {
2239                 const double a_div_b = center->val / m_SkyBackground.mean;
2240                 const double factor = erf(sqrt(log(a_div_b))) / (1 - (1 / a_div_b));
2241                 HFRs.push_back(center->HFR * factor);
2242                 qCDebug(KSTARS_FITS) << "HFR Adj, brightness adjusted from " << center->HFR << " to " << center->HFR * factor;
2243             }
2244         }
2245     }
2246 
2247     auto m = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_SIGMACLIPPING,
2248              HFRs, 2);
2249 
2250     cacheHFR = m;
2251     cacheHFRType = HFR_AVERAGE;
2252     return cacheHFR;
2253 }
2254 
2255 double FITSData::getHFR(int x, int y, double scale)
2256 {
2257     if (starCenters.empty())
2258         return -1;
2259 
2260     for (int i = 0; i < starCenters.count(); i++)
2261     {
2262         const int maxDist = std::max(2, static_cast<int>(0.5 + 2 * starCenters[i]->width / scale));
2263         const int dx = std::fabs(starCenters[i]->x - x);
2264         const int dy = std::fabs(starCenters[i]->y - y);
2265         if (dx <= maxDist && dy <= maxDist)
2266         {
2267             m_SelectedHFRStar = *starCenters[i];
2268             return starCenters[i]->HFR;
2269         }
2270     }
2271 
2272     return -1;
2273 }
2274 
2275 double FITSData::getEccentricity()
2276 {
2277     if (starCenters.empty())
2278         return -1;
2279     if (cacheEccentricity >= 0)
2280         return cacheEccentricity;
2281     std::vector<float> eccs;
2282     for (const auto &s : starCenters)
2283         eccs.push_back(s->ellipticity);
2284     int middle = eccs.size() / 2;
2285     std::nth_element(eccs.begin(), eccs.begin() + middle, eccs.end());
2286     float medianEllipticity = eccs[middle];
2287 
2288     // SEP gives us the ellipticity (flattening) directly.
2289     // To get the eccentricity, use this formula:
2290     // e = sqrt(ellipticity * (2 - ellipticity))
2291     // https://en.wikipedia.org/wiki/Eccentricity_(mathematics)#Ellipses
2292     const float eccentricity = sqrt(medianEllipticity * (2 - medianEllipticity));
2293     cacheEccentricity = eccentricity;
2294     return eccentricity;
2295 }
2296 
2297 void FITSData::applyFilter(FITSScale type, uint8_t * image, QVector<double> * min, QVector<double> * max)
2298 {
2299     if (type == FITS_NONE)
2300         return;
2301 
2302     QVector<double> dataMin(3);
2303     QVector<double> dataMax(3);
2304 
2305     if (min)
2306         dataMin = *min;
2307     if (max)
2308         dataMax = *max;
2309 
2310     switch (type)
2311     {
2312         case FITS_AUTO_STRETCH:
2313         {
2314             for (int i = 0; i < 3; i++)
2315             {
2316                 dataMin[i] = m_Statistics.mean[i] - m_Statistics.stddev[i];
2317                 dataMax[i] = m_Statistics.mean[i] + m_Statistics.stddev[i] * 3;
2318             }
2319         }
2320         break;
2321 
2322         case FITS_HIGH_CONTRAST:
2323         {
2324             for (int i = 0; i < 3; i++)
2325             {
2326                 dataMin[i] = m_Statistics.mean[i] + m_Statistics.stddev[i];
2327                 dataMax[i] = m_Statistics.mean[i] + m_Statistics.stddev[i] * 3;
2328             }
2329         }
2330         break;
2331 
2332         case FITS_HIGH_PASS:
2333         {
2334             for (int i = 0; i < 3; i++)
2335             {
2336                 dataMin[i] = m_Statistics.mean[i];
2337             }
2338         }
2339         break;
2340 
2341         default:
2342             break;
2343     }
2344 
2345     switch (m_Statistics.dataType)
2346     {
2347         case TBYTE:
2348         {
2349             for (int i = 0; i < 3; i++)
2350             {
2351                 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2352                 dataMax[i] = dataMax[i] > UINT8_MAX ? UINT8_MAX : dataMax[i];
2353             }
2354             applyFilter<uint8_t>(type, image, &dataMin, &dataMax);
2355         }
2356         break;
2357 
2358         case TSHORT:
2359         {
2360             for (int i = 0; i < 3; i++)
2361             {
2362                 dataMin[i] = dataMin[i] < INT16_MIN ? INT16_MIN : dataMin[i];
2363                 dataMax[i] = dataMax[i] > INT16_MAX ? INT16_MAX : dataMax[i];
2364             }
2365             applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2366         }
2367 
2368         break;
2369 
2370         case TUSHORT:
2371         {
2372             for (int i = 0; i < 3; i++)
2373             {
2374                 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2375                 dataMax[i] = dataMax[i] > UINT16_MAX ? UINT16_MAX : dataMax[i];
2376             }
2377             applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2378         }
2379         break;
2380 
2381         case TLONG:
2382         {
2383             for (int i = 0; i < 3; i++)
2384             {
2385                 dataMin[i] = dataMin[i] < INT_MIN ? INT_MIN : dataMin[i];
2386                 dataMax[i] = dataMax[i] > INT_MAX ? INT_MAX : dataMax[i];
2387             }
2388             applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2389         }
2390         break;
2391 
2392         case TULONG:
2393         {
2394             for (int i = 0; i < 3; i++)
2395             {
2396                 dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i];
2397                 dataMax[i] = dataMax[i] > UINT_MAX ? UINT_MAX : dataMax[i];
2398             }
2399             applyFilter<uint16_t>(type, image, &dataMin, &dataMax);
2400         }
2401         break;
2402 
2403         case TFLOAT:
2404         {
2405             for (int i = 0; i < 3; i++)
2406             {
2407                 dataMin[i] = dataMin[i] < FLT_MIN ? FLT_MIN : dataMin[i];
2408                 dataMax[i] = dataMax[i] > FLT_MAX ? FLT_MAX : dataMax[i];
2409             }
2410             applyFilter<float>(type, image, &dataMin, &dataMax);
2411         }
2412         break;
2413 
2414         case TLONGLONG:
2415         {
2416             for (int i = 0; i < 3; i++)
2417             {
2418                 dataMin[i] = dataMin[i] < LLONG_MIN ? LLONG_MIN : dataMin[i];
2419                 dataMax[i] = dataMax[i] > LLONG_MAX ? LLONG_MAX : dataMax[i];
2420             }
2421 
2422             applyFilter<long>(type, image, &dataMin, &dataMax);
2423         }
2424         break;
2425 
2426         case TDOUBLE:
2427         {
2428             for (int i = 0; i < 3; i++)
2429             {
2430                 dataMin[i] = dataMin[i] < DBL_MIN ? DBL_MIN : dataMin[i];
2431                 dataMax[i] = dataMax[i] > DBL_MAX ? DBL_MAX : dataMax[i];
2432             }
2433             applyFilter<double>(type, image, &dataMin, &dataMax);
2434         }
2435 
2436         break;
2437 
2438         default:
2439             return;
2440     }
2441 
2442     if (min != nullptr)
2443         *min = dataMin;
2444     if (max != nullptr)
2445         *max = dataMax;
2446 
2447     emit dataChanged();
2448 }
2449 
2450 template <typename T>
2451 void FITSData::applyFilter(FITSScale type, uint8_t * targetImage, QVector<double> * targetMin, QVector<double> * targetMax)
2452 {
2453     bool calcStats = false;
2454     T * image = nullptr;
2455 
2456     if (targetImage)
2457         image = reinterpret_cast<T *>(targetImage);
2458     else
2459     {
2460         image     = reinterpret_cast<T *>(m_ImageBuffer);
2461         calcStats = true;
2462     }
2463 
2464     T min[3], max[3];
2465     for (int i = 0; i < 3; i++)
2466     {
2467         min[i] = (*targetMin)[i] < std::numeric_limits<T>::min() ? std::numeric_limits<T>::min() : (*targetMin)[i];
2468         max[i] = (*targetMax)[i] > std::numeric_limits<T>::max() ? std::numeric_limits<T>::max() : (*targetMax)[i];
2469     }
2470 
2471 
2472     // Create N threads
2473     const uint8_t nThreads = 16;
2474 
2475     uint32_t width  = m_Statistics.width;
2476     uint32_t height = m_Statistics.height;
2477 
2478     //QTime timer;
2479     //timer.start();
2480     switch (type)
2481     {
2482         case FITS_AUTO:
2483         case FITS_LINEAR:
2484         case FITS_AUTO_STRETCH:
2485         case FITS_HIGH_CONTRAST:
2486         case FITS_LOG:
2487         case FITS_SQRT:
2488         case FITS_HIGH_PASS:
2489         {
2490             // List of futures
2491             QList<QFuture<void>> futures;
2492             QVector<double> coeff(3);
2493 
2494             if (type == FITS_LOG)
2495             {
2496                 for (int i = 0; i < 3; i++)
2497                     coeff[i] = max[i] / std::log(1 + max[i]);
2498             }
2499             else if (type == FITS_SQRT)
2500             {
2501                 for (int i = 0; i < 3; i++)
2502                     coeff[i] = max[i] / sqrt(max[i]);
2503             }
2504 
2505             for (int n = 0; n < m_Statistics.channels; n++)
2506             {
2507                 if (type == FITS_HIGH_PASS)
2508                     min[n] = m_Statistics.mean[n];
2509 
2510                 uint32_t cStart = n * m_Statistics.samples_per_channel;
2511 
2512                 // Calculate how many elements we process per thread
2513                 uint32_t tStride = m_Statistics.samples_per_channel / nThreads;
2514 
2515                 // Calculate the final stride since we can have some left over due to division above
2516                 uint32_t fStride = tStride + (m_Statistics.samples_per_channel - (tStride * nThreads));
2517 
2518                 T * runningBuffer = image + cStart;
2519 
2520                 if (type == FITS_LOG)
2521                 {
2522                     for (int i = 0; i < nThreads; i++)
2523                     {
2524                         // Run threads
2525                         futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2526                                                               coeff, n](T & a)
2527                         {
2528                             a = qBound(min[n], static_cast<T>(round(coeff[n] * std::log(1 + qBound(min[n], a, max[n])))), max[n]);
2529                         }));
2530 
2531                         runningBuffer += tStride;
2532                     }
2533                 }
2534                 else if (type == FITS_SQRT)
2535                 {
2536                     for (int i = 0; i < nThreads; i++)
2537                     {
2538                         // Run threads
2539                         futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2540                                                               coeff, n](T & a)
2541                         {
2542                             a = qBound(min[n], static_cast<T>(round(coeff[n] * a)), max[n]);
2543                         }));
2544                     }
2545 
2546                     runningBuffer += tStride;
2547                 }
2548                 else
2549                 {
2550                     for (int i = 0; i < nThreads; i++)
2551                     {
2552                         // Run threads
2553                         futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max,
2554                                                               n](T & a)
2555                         {
2556                             a = qBound(min[n], a, max[n]);
2557                         }));
2558                         runningBuffer += tStride;
2559                     }
2560                 }
2561             }
2562 
2563             for (int i = 0; i < nThreads * m_Statistics.channels; i++)
2564                 futures[i].waitForFinished();
2565 
2566             if (calcStats)
2567             {
2568                 for (int i = 0; i < 3; i++)
2569                 {
2570                     m_Statistics.min[i] = min[i];
2571                     m_Statistics.max[i] = max[i];
2572                 }
2573                 calculateStdDev<T>();
2574             }
2575         }
2576         break;
2577 
2578         case FITS_EQUALIZE:
2579         {
2580 #ifndef KSTARS_LITE
2581             if (!isHistogramConstructed())
2582                 constructHistogram();
2583 
2584             T bufferVal                    = 0;
2585 
2586             double coeff = 255.0 / (height * width);
2587             uint32_t row = 0;
2588             uint32_t index = 0;
2589 
2590             for (int i = 0; i < m_Statistics.channels; i++)
2591             {
2592                 uint32_t offset = i * m_Statistics.samples_per_channel;
2593                 for (uint32_t j = 0; j < height; j++)
2594                 {
2595                     row = offset + j * width;
2596                     for (uint32_t k = 0; k < width; k++)
2597                     {
2598                         index     = k + row;
2599                         bufferVal = (image[index] - min[i]) / m_HistogramBinWidth[i];
2600 
2601                         if (bufferVal >= m_CumulativeFrequency[i].size())
2602                             bufferVal = m_CumulativeFrequency[i].size() - 1;
2603 
2604                         image[index] = qBound(min[i], static_cast<T>(round(coeff * m_CumulativeFrequency[i][bufferVal])), max[i]);
2605                     }
2606                 }
2607             }
2608 #endif
2609         }
2610         if (calcStats)
2611             calculateStats(true, false);
2612         break;
2613 
2614         // Based on http://www.librow.com/articles/article-1
2615         case FITS_MEDIAN:
2616         {
2617             uint8_t BBP      = m_Statistics.bytesPerPixel;
2618             auto * extension = new T[(width + 2) * (height + 2)];
2619             //   Check memory allocation
2620             if (!extension)
2621                 return;
2622             //   Create image extension
2623             for (uint32_t ch = 0; ch < m_Statistics.channels; ch++)
2624             {
2625                 uint32_t offset = ch * m_Statistics.samples_per_channel;
2626                 uint32_t N = width, M = height;
2627 
2628                 for (uint32_t i = 0; i < M; ++i)
2629                 {
2630                     memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP);
2631                     extension[(N + 2) * (i + 1)]     = image[N * i + offset];
2632                     extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset];
2633                 }
2634                 //   Fill first line of image extension
2635                 memcpy(extension, extension + N + 2, (N + 2) * BBP);
2636                 //   Fill last line of image extension
2637                 memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP);
2638                 //   Call median filter implementation
2639 
2640                 N = width + 2;
2641                 M = height + 2;
2642 
2643                 //   Move window through all elements of the image
2644                 for (uint32_t m = 1; m < M - 1; ++m)
2645                     for (uint32_t n = 1; n < N - 1; ++n)
2646                     {
2647                         //   Pick up window elements
2648                         int k = 0;
2649                         float window[9];
2650 
2651                         memset(&window[0], 0, 9 * sizeof(float));
2652                         for (uint32_t j = m - 1; j < m + 2; ++j)
2653                             for (uint32_t i = n - 1; i < n + 2; ++i)
2654                                 window[k++] = extension[j * N + i];
2655                         //   Order elements (only half of them)
2656                         for (uint32_t j = 0; j < 5; ++j)
2657                         {
2658                             //   Find position of minimum element
2659                             int mine = j;
2660                             for (uint32_t l = j + 1; l < 9; ++l)
2661                                 if (window[l] < window[mine])
2662                                     mine = l;
2663                             //   Put found minimum element in its place
2664                             const float temp = window[j];
2665                             window[j]        = window[mine];
2666                             window[mine]     = temp;
2667                         }
2668                         //   Get result - the middle element
2669                         image[(m - 1) * (N - 2) + n - 1 + offset] = window[4];
2670                     }
2671             }
2672 
2673             //   Free memory
2674             delete[] extension;
2675 
2676             if (calcStats)
2677                 calculateStdDev<T>();
2678         }
2679         break;
2680 
2681         case FITS_GAUSSIAN:
2682             gaussianBlur<T>(Options::focusGaussianKernelSize(), Options::focusGaussianSigma());
2683             if (calcStats)
2684                 calculateStats(true, false);
2685             break;
2686 
2687         case FITS_ROTATE_CW:
2688             rotFITS<T>(90, 0);
2689             rotCounter++;
2690             break;
2691 
2692         case FITS_ROTATE_CCW:
2693             rotFITS<T>(270, 0);
2694             rotCounter--;
2695             break;
2696 
2697         case FITS_MOUNT_FLIP_H:
2698             rotFITS<T>(0, 1);
2699             flipHCounter++;
2700             break;
2701 
2702         case FITS_MOUNT_FLIP_V:
2703             rotFITS<T>(0, 2);
2704             flipVCounter++;
2705             break;
2706 
2707         default:
2708             break;
2709     }
2710 }
2711 
2712 QList<Edge *> FITSData::getStarCentersInSubFrame(QRect subFrame) const
2713 {
2714     QList<Edge *> starCentersInSubFrame;
2715     for (int i = 0; i < starCenters.count(); i++)
2716     {
2717         int x = static_cast<int>(starCenters[i]->x);
2718         int y = static_cast<int>(starCenters[i]->y);
2719         if(subFrame.contains(x, y))
2720         {
2721             starCentersInSubFrame.append(starCenters[i]);
2722         }
2723     }
2724     return starCentersInSubFrame;
2725 }
2726 
2727 bool FITSData::loadWCS()
2728 {
2729 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2730 
2731     if (m_WCSState == Success)
2732     {
2733         qCWarning(KSTARS_FITS) << "WCS data already loaded";
2734         return true;
2735     }
2736 
2737     if (m_WCSHandle != nullptr)
2738     {
2739         wcsvfree(&m_nwcs, &m_WCSHandle);
2740         m_nwcs = 0;
2741         m_WCSHandle = nullptr;
2742     }
2743 
2744     qCDebug(KSTARS_FITS) << "Started WCS Data Processing...";
2745 
2746     QByteArray header_str;
2747     int status = 0;
2748     int nkeyrec = 0, nreject = 0;
2749     if (fptr)
2750     {
2751         char *header = nullptr;
2752         if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status))
2753         {
2754             char errmsg[512];
2755             fits_get_errstatus(status, errmsg);
2756             m_LastError = errmsg;
2757             m_WCSState = Failure;
2758             return false;
2759         }
2760         header_str = QByteArray(header);
2761         fits_free_memory(header, &status);
2762     }
2763     else
2764     {
2765         nkeyrec = 1;
2766         for(auto &fitsKeyword : m_HeaderRecords)
2767         {
2768             QByteArray rec;
2769             rec.append(fitsKeyword.key.leftJustified(8, ' ').toLatin1());
2770             rec.append("= ");
2771             rec.append(fitsKeyword.value.toByteArray());
2772             rec.append(" / ");
2773             rec.append(fitsKeyword.comment.toLatin1());
2774             header_str.append(rec.leftJustified(80, ' ', true));
2775             nkeyrec++;
2776         }
2777         header_str.append(QByteArray("END").leftJustified(80));
2778     }
2779 
2780     if ((status = wcspih(header_str.data(), nkeyrec, WCSHDR_all, 0, &nreject, &m_nwcs, &m_WCSHandle)) != 0)
2781     {
2782         wcsvfree(&m_nwcs, &m_WCSHandle);
2783         m_WCSHandle = nullptr;
2784         m_nwcs = 0;
2785         m_LastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]);
2786         m_WCSState = Failure;
2787         return false;
2788     }
2789 
2790     if (m_WCSHandle == nullptr)
2791     {
2792         m_LastError = i18n("No world coordinate systems found.");
2793         m_WCSState = Failure;
2794         return false;
2795     }
2796 
2797     // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now.
2798     if (m_WCSHandle->crpix[0] == 0)
2799     {
2800         wcsvfree(&m_nwcs, &m_WCSHandle);
2801         m_WCSHandle = nullptr;
2802         m_nwcs = 0;
2803         m_LastError = i18n("No world coordinate systems found.");
2804         m_WCSState = Failure;
2805         return false;
2806     }
2807 
2808     cdfix(m_WCSHandle);
2809     if ((status = wcsset(m_WCSHandle)) != 0)
2810     {
2811         wcsvfree(&m_nwcs, &m_WCSHandle);
2812         m_WCSHandle = nullptr;
2813         m_nwcs = 0;
2814         m_LastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2815         m_WCSState = Failure;
2816         return false;
2817     }
2818 
2819     m_ObjectsSearched = false;
2820     m_WCSState = Success;
2821     HasWCS = true;
2822 
2823     qCDebug(KSTARS_FITS) << "Finished WCS Data processing...";
2824 
2825     return true;
2826 #else
2827     return false;
2828 #endif
2829 }
2830 
2831 bool FITSData::wcsToPixel(const SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint)
2832 {
2833 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2834     int status, stat[NWCSFIX];
2835     double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, worldcrd[NWCSFIX];
2836 
2837     if (m_WCSHandle == nullptr)
2838     {
2839         m_LastError = i18n("No world coordinate systems found.");
2840         return false;
2841     }
2842 
2843     worldcrd[0] = wcsCoord.ra0().Degrees();
2844     worldcrd[1] = wcsCoord.dec0().Degrees();
2845 
2846     if ((status = wcss2p(m_WCSHandle, 1, 2, worldcrd, &phi, &theta, imgcrd, pixcrd, stat)) != 0)
2847     {
2848         m_LastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2849         return false;
2850     }
2851 
2852     wcsImagePoint.setX(imgcrd[0]);
2853     wcsImagePoint.setY(imgcrd[1]);
2854 
2855     wcsPixelPoint.setX(pixcrd[0]);
2856     wcsPixelPoint.setY(pixcrd[1]);
2857 
2858     return true;
2859 #else
2860     Q_UNUSED(wcsCoord);
2861     Q_UNUSED(wcsPixelPoint);
2862     Q_UNUSED(wcsImagePoint);
2863     return false;
2864 #endif
2865 }
2866 
2867 bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord)
2868 {
2869 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2870     int status, stat[NWCSFIX];
2871     double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, world[NWCSFIX];
2872 
2873     if (m_WCSHandle == nullptr)
2874     {
2875         m_LastError = i18n("No world coordinate systems found.");
2876         return false;
2877     }
2878 
2879     pixcrd[0] = wcsPixelPoint.x();
2880     pixcrd[1] = wcsPixelPoint.y();
2881 
2882     if ((status = wcsp2s(m_WCSHandle, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat)) != 0)
2883     {
2884         m_LastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]);
2885         return false;
2886     }
2887     else
2888     {
2889         wcsCoord.setRA0(world[0] / 15.0);
2890         wcsCoord.setDec0(world[1]);
2891     }
2892 
2893     return true;
2894 #else
2895     Q_UNUSED(wcsPixelPoint);
2896     Q_UNUSED(wcsCoord);
2897     return false;
2898 #endif
2899 }
2900 
2901 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2902 bool FITSData::searchObjects()
2903 {
2904     if (m_ObjectsSearched)
2905         return true;
2906 
2907     m_ObjectsSearched = true;
2908 
2909     SkyPoint startPoint;
2910     SkyPoint endPoint;
2911 
2912     pixelToWCS(QPointF(0, 0), startPoint);
2913     pixelToWCS(QPointF(width() - 1, height() - 1), endPoint);
2914 
2915     return findObjectsInImage(startPoint, endPoint);
2916 }
2917 
2918 bool FITSData::findWCSBounds(double &minRA, double &maxRA, double &minDec, double &maxDec)
2919 {
2920     if (m_WCSHandle == nullptr)
2921     {
2922         m_LastError = i18n("No world coordinate systems found.");
2923         return false;
2924     }
2925 
2926     maxRA  = -1000;
2927     minRA  = 1000;
2928     maxDec = -1000;
2929     minDec = 1000;
2930 
2931     auto updateMinMax = [&](int x, int y)
2932     {
2933         int stat[2];
2934         double imgcrd[2], phi, pixcrd[2], theta, world[2];
2935 
2936         pixcrd[0] = x;
2937         pixcrd[1] = y;
2938 
2939         if (wcsp2s(m_WCSHandle, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]))
2940             return;
2941 
2942         minRA = std::min(minRA, world[0]);
2943         maxRA = std::max(maxRA, world[0]);
2944         minDec = std::min(minDec, world[1]);
2945         maxDec = std::max(maxDec, world[1]);
2946     };
2947 
2948     // Find min and max values from edges
2949     for (int y = 0; y < height(); y++)
2950     {
2951         updateMinMax(0, y);
2952         updateMinMax(width() - 1, y);
2953     }
2954 
2955     for (int x = 1; x < width() - 1; x++)
2956     {
2957         updateMinMax(x, 0);
2958         updateMinMax(x, height() - 1);
2959     }
2960 
2961     // Check if either pole is in the image
2962     SkyPoint NCP(0, 90);
2963     SkyPoint SCP(0, -90);
2964     QPointF pixelPoint, imagePoint, pPoint;
2965     if (wcsToPixel(NCP, pPoint, imagePoint))
2966     {
2967         if (pPoint.x() > 0 && pPoint.x() < width() && pPoint.y() > 0 && pPoint.y() < height())
2968         {
2969             maxDec = 90;
2970         }
2971     }
2972     if (wcsToPixel(SCP, pPoint, imagePoint))
2973     {
2974         if (pPoint.x() > 0 && pPoint.x() < width() && pPoint.y() > 0 && pPoint.y() < height())
2975         {
2976             minDec = -90;
2977         }
2978     }
2979     return true;
2980 }
2981 #endif
2982 
2983 #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB)
2984 bool FITSData::findObjectsInImage(SkyPoint startPoint, SkyPoint endPoint)
2985 {
2986     if (KStarsData::Instance() == nullptr)
2987         return false;
2988 
2989     int w = width();
2990     int h = height();
2991     QVariant date;
2992     KSNumbers * num = nullptr;
2993 
2994     if (getRecordValue("DATE-OBS", date))
2995     {
2996         QString tsString(date.toString());
2997         tsString = tsString.remove('\'').trimmed();
2998         // Add Zulu time to indicate UTC
2999         tsString += "Z";
3000 
3001         QDateTime ts = QDateTime::fromString(tsString, Qt::ISODate);
3002 
3003         if (ts.isValid())
3004             num = new KSNumbers(KStarsDateTime(ts).djd());
3005     }
3006 
3007     //Set to current time if the above does not work.
3008     if (num == nullptr)
3009         num = new KSNumbers(KStarsData::Instance()->ut().djd());
3010 
3011     startPoint.updateCoordsNow(num);
3012     endPoint.updateCoordsNow(num);
3013 
3014     m_SkyObjects.clear();
3015 
3016     QList<SkyObject *> list = KStarsData::Instance()->skyComposite()->findObjectsInArea(startPoint, endPoint);
3017     list.erase(std::remove_if(list.begin(), list.end(), [](SkyObject * oneObject)
3018     {
3019         int type = oneObject->type();
3020         return (type == SkyObject::STAR || type == SkyObject::PLANET || type == SkyObject::ASTEROID ||
3021                 type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON ||
3022                 type == SkyObject::SATELLITE);
3023     }), list.end());
3024 
3025     double world[2], phi, theta, imgcrd[2], pixcrd[2];
3026     int stat[2];
3027     for (auto &object : list)
3028     {
3029         world[0] = object->ra0().Degrees();
3030         world[1] = object->dec0().Degrees();
3031 
3032         if (wcss2p(m_WCSHandle, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0]) == 0)
3033         {
3034             //The X and Y are set to the found position if it does work.
3035             int x = pixcrd[0];
3036             int y = pixcrd[1];
3037             if (x > 0 && y > 0 && x < w && y < h)
3038                 m_SkyObjects.append(new FITSSkyObject(object, x, y));
3039         }
3040     }
3041 
3042     delete (num);
3043     return true;
3044 }
3045 #endif
3046 
3047 int FITSData::getFlipVCounter() const
3048 {
3049     return flipVCounter;
3050 }
3051 
3052 void FITSData::setFlipVCounter(int value)
3053 {
3054     flipVCounter = value;
3055 }
3056 
3057 int FITSData::getFlipHCounter() const
3058 {
3059     return flipHCounter;
3060 }
3061 
3062 void FITSData::setFlipHCounter(int value)
3063 {
3064     flipHCounter = value;
3065 }
3066 
3067 int FITSData::getRotCounter() const
3068 {
3069     return rotCounter;
3070 }
3071 
3072 void FITSData::setRotCounter(int value)
3073 {
3074     rotCounter = value;
3075 }
3076 
3077 /* Rotate an image by 90, 180, or 270 degrees, with an optional
3078  * reflection across the vertical or horizontal axis.
3079  * verbose generates extra info on stdout.
3080  * return nullptr if successful or rotated image.
3081  */
3082 template <typename T>
3083 bool FITSData::rotFITS(int rotate, int mirror)
3084 {
3085     int ny, nx;
3086     int x1, y1, x2, y2;
3087     uint8_t * rotimage = nullptr;
3088     int offset        = 0;
3089 
3090     if (rotate == 1)
3091         rotate = 90;
3092     else if (rotate == 2)
3093         rotate = 180;
3094     else if (rotate == 3)
3095         rotate = 270;
3096     else if (rotate < 0)
3097         rotate = rotate + 360;
3098 
3099     nx = m_Statistics.width;
3100     ny = m_Statistics.height;
3101 
3102     int BBP = m_Statistics.bytesPerPixel;
3103 
3104     /* Allocate buffer for rotated image */
3105     rotimage = new uint8_t[m_Statistics.samples_per_channel * m_Statistics.channels * BBP];
3106 
3107     if (rotimage == nullptr)
3108     {
3109         qWarning() << "Unable to allocate memory for rotated image buffer!";
3110         return false;
3111     }
3112 
3113     auto * rotBuffer = reinterpret_cast<T *>(rotimage);
3114     auto * buffer    = reinterpret_cast<T *>(m_ImageBuffer);
3115 
3116     /* Mirror image without rotation */
3117     if (rotate < 45 && rotate > -45)
3118     {
3119         if (mirror == 1)
3120         {
3121             for (int i = 0; i < m_Statistics.channels; i++)
3122             {
3123                 offset = m_Statistics.samples_per_channel * i;
3124                 for (x1 = 0; x1 < nx; x1++)
3125                 {
3126                     x2 = nx - x1 - 1;
3127                     for (y1 = 0; y1 < ny; y1++)
3128                         rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3129                 }
3130             }
3131         }
3132         else if (mirror == 2)
3133         {
3134             for (int i = 0; i < m_Statistics.channels; i++)
3135             {
3136                 offset = m_Statistics.samples_per_channel * i;
3137                 for (y1 = 0; y1 < ny; y1++)
3138                 {
3139                     y2 = ny - y1 - 1;
3140                     for (x1 = 0; x1 < nx; x1++)
3141                         rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3142                 }
3143             }
3144         }
3145         else
3146         {
3147             for (int i = 0; i < m_Statistics.channels; i++)
3148             {
3149                 offset = m_Statistics.samples_per_channel * i;
3150                 for (y1 = 0; y1 < ny; y1++)
3151                 {
3152                     for (x1 = 0; x1 < nx; x1++)
3153                         rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3154                 }
3155             }
3156         }
3157     }
3158 
3159     /* Rotate by 90 degrees */
3160     else if (rotate >= 45 && rotate < 135)
3161     {
3162         if (mirror == 1)
3163         {
3164             for (int i = 0; i < m_Statistics.channels; i++)
3165             {
3166                 offset = m_Statistics.samples_per_channel * i;
3167                 for (y1 = 0; y1 < ny; y1++)
3168                 {
3169                     x2 = ny - y1 - 1;
3170                     for (x1 = 0; x1 < nx; x1++)
3171                     {
3172                         y2                                 = nx - x1 - 1;
3173                         rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3174                     }
3175                 }
3176             }
3177         }
3178         else if (mirror == 2)
3179         {
3180             for (int i = 0; i < m_Statistics.channels; i++)
3181             {
3182                 offset = m_Statistics.samples_per_channel * i;
3183                 for (y1 = 0; y1 < ny; y1++)
3184                 {
3185                     for (x1 = 0; x1 < nx; x1++)
3186                         rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset];
3187                 }
3188             }
3189         }
3190         else
3191         {
3192             for (int i = 0; i < m_Statistics.channels; i++)
3193             {
3194                 offset = m_Statistics.samples_per_channel * i;
3195                 for (y1 = 0; y1 < ny; y1++)
3196                 {
3197                     x2 = ny - y1 - 1;
3198                     for (x1 = 0; x1 < nx; x1++)
3199                     {
3200                         y2                                 = x1;
3201                         rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3202                     }
3203                 }
3204             }
3205         }
3206 
3207         m_Statistics.width  = ny;
3208         m_Statistics.height = nx;
3209     }
3210 
3211     /* Rotate by 180 degrees */
3212     else if (rotate >= 135 && rotate < 225)
3213     {
3214         if (mirror == 1)
3215         {
3216             for (int i = 0; i < m_Statistics.channels; i++)
3217             {
3218                 offset = m_Statistics.samples_per_channel * i;
3219                 for (y1 = 0; y1 < ny; y1++)
3220                 {
3221                     y2 = ny - y1 - 1;
3222                     for (x1 = 0; x1 < nx; x1++)
3223                         rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset];
3224                 }
3225             }
3226         }
3227         else if (mirror == 2)
3228         {
3229             for (int i = 0; i < m_Statistics.channels; i++)
3230             {
3231                 offset = m_Statistics.samples_per_channel * i;
3232                 for (x1 = 0; x1 < nx; x1++)
3233                 {
3234                     x2 = nx - x1 - 1;
3235                     for (y1 = 0; y1 < ny; y1++)
3236                         rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3237                 }
3238             }
3239         }
3240         else
3241         {
3242             for (int i = 0; i < m_Statistics.channels; i++)
3243             {
3244                 offset = m_Statistics.samples_per_channel * i;
3245                 for (y1 = 0; y1 < ny; y1++)
3246                 {
3247                     y2 = ny - y1 - 1;
3248                     for (x1 = 0; x1 < nx; x1++)
3249                     {
3250                         x2                                 = nx - x1 - 1;
3251                         rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3252                     }
3253                 }
3254             }
3255         }
3256     }
3257 
3258     /* Rotate by 270 degrees */
3259     else if (rotate >= 225 && rotate < 315)
3260     {
3261         if (mirror == 1)
3262         {
3263             for (int i = 0; i < m_Statistics.channels; i++)
3264             {
3265                 offset = m_Statistics.samples_per_channel * i;
3266                 for (y1 = 0; y1 < ny; y1++)
3267                 {
3268                     for (x1 = 0; x1 < nx; x1++)
3269                         rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset];
3270                 }
3271             }
3272         }
3273         else if (mirror == 2)
3274         {
3275             for (int i = 0; i < m_Statistics.channels; i++)
3276             {
3277                 offset = m_Statistics.samples_per_channel * i;
3278                 for (y1 = 0; y1 < ny; y1++)
3279                 {
3280                     x2 = ny - y1 - 1;
3281                     for (x1 = 0; x1 < nx; x1++)
3282                     {
3283                         y2                                 = nx - x1 - 1;
3284                         rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3285                     }
3286                 }
3287             }
3288         }
3289         else
3290         {
3291             for (int i = 0; i < m_Statistics.channels; i++)
3292             {
3293                 offset = m_Statistics.samples_per_channel * i;
3294                 for (y1 = 0; y1 < ny; y1++)
3295                 {
3296                     x2 = y1;
3297                     for (x1 = 0; x1 < nx; x1++)
3298                     {
3299                         y2                                 = nx - x1 - 1;
3300                         rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3301                     }
3302                 }
3303             }
3304         }
3305 
3306         m_Statistics.width  = ny;
3307         m_Statistics.height = nx;
3308     }
3309 
3310     /* If rotating by more than 315 degrees, assume top-bottom reflection */
3311     else if (rotate >= 315 && mirror)
3312     {
3313         for (int i = 0; i < m_Statistics.channels; i++)
3314         {
3315             offset = m_Statistics.samples_per_channel * i;
3316             for (y1 = 0; y1 < ny; y1++)
3317             {
3318                 for (x1 = 0; x1 < nx; x1++)
3319                 {
3320                     x2                                 = y1;
3321                     y2                                 = x1;
3322                     rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset];
3323                 }
3324             }
3325         }
3326     }
3327 
3328     delete[] m_ImageBuffer;
3329     m_ImageBuffer = rotimage;
3330 
3331     return true;
3332 }
3333 
3334 void FITSData::rotWCSFITS(int angle, int mirror)
3335 {
3336     int status = 0;
3337     char comment[100];
3338     double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2;
3339     int WCS_DECIMALS = 6;
3340 
3341     naxis1 = m_Statistics.width;
3342     naxis2 = m_Statistics.height;
3343 
3344     if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3345     {
3346         // No WCS keywords
3347         return;
3348     }
3349 
3350     /* Reset CROTAn and CD matrix if axes have been exchanged */
3351     if (angle == 90)
3352     {
3353         if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status))
3354             fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status);
3355 
3356         if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status))
3357             fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status);
3358     }
3359 
3360     status = 0;
3361 
3362     /* Negate rotation angle if mirrored */
3363     if (mirror != 0)
3364     {
3365         if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status))
3366             fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status);
3367 
3368         if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status))
3369             fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status);
3370 
3371         status = 0;
3372 
3373         if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status))
3374             fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3375 
3376         status = 0;
3377 
3378         if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3379             fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3380 
3381         if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status))
3382             fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status);
3383 
3384         if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status))
3385             fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status);
3386     }
3387 
3388     status = 0;
3389 
3390     /* Unbin CRPIX and CD matrix */
3391     if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status))
3392     {
3393         if (ctemp1 != 1.0)
3394         {
3395             if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status))
3396                 if (ctemp1 == ctemp2)
3397                 {
3398                     double ltv1 = 0.0;
3399                     double ltv2 = 0.0;
3400                     status      = 0;
3401                     if (!fits_read_key_dbl(fptr, "LTV1", &ltv1, comment, &status))
3402                         fits_delete_key(fptr, "LTV1", &status);
3403                     if (!fits_read_key_dbl(fptr, "LTV2", &ltv2, comment, &status))
3404                         fits_delete_key(fptr, "LTV2", &status);
3405 
3406                     status = 0;
3407 
3408                     if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status))
3409                         fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status);
3410 
3411                     if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status))
3412                         fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status);
3413 
3414                     status = 0;
3415 
3416                     if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status))
3417                         fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3418 
3419                     if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status))
3420                         fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3421 
3422                     if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status))
3423                         fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3424 
3425                     if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status))
3426                         fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status);
3427 
3428                     status = 0;
3429 
3430                     fits_delete_key(fptr, "LTM1_1", &status);
3431                     fits_delete_key(fptr, "LTM1_2", &status);
3432                 }
3433         }
3434     }
3435 
3436     status = 0;
3437 
3438     /* Reset CRPIXn */
3439     if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) &&
3440             !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status))
3441     {
3442         if (mirror != 0)
3443         {
3444             if (angle == 0)
3445                 fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3446             else if (angle == 90)
3447             {
3448                 fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3449                 fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3450             }
3451             else if (angle == 180)
3452             {
3453                 fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status);
3454                 fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3455             }
3456             else if (angle == 270)
3457             {
3458                 fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status);
3459                 fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status);
3460             }
3461         }
3462         else
3463         {
3464             if (angle == 90)
3465             {
3466                 fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3467                 fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status);
3468             }
3469             else if (angle == 180)
3470             {
3471                 fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3472                 fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status);
3473             }
3474             else if (angle == 270)
3475             {
3476                 fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status);
3477                 fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status);
3478             }
3479         }
3480     }
3481 
3482     status = 0;
3483 
3484     /* Reset CDELTn (degrees per pixel) */
3485     if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) &&
3486             !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status))
3487     {
3488         if (mirror != 0)
3489         {
3490             if (angle == 0)
3491                 fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status);
3492             else if (angle == 90)
3493             {
3494                 fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status);
3495                 fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status);
3496             }
3497             else if (angle == 180)
3498             {
3499                 fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status);
3500                 fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status);
3501             }
3502             else if (angle == 270)
3503             {
3504                 fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status);
3505                 fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status);
3506             }
3507         }
3508         else
3509         {
3510             if (angle == 90)
3511             {
3512                 fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status);
3513                 fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status);
3514             }
3515             else if (angle == 180)
3516             {
3517                 fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status);
3518                 fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status);
3519             }
3520             else if (angle == 270)
3521             {
3522                 fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status);
3523                 fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status);
3524             }
3525         }
3526     }
3527 
3528     /* Reset CD matrix, if present */
3529     ctemp1 = 0.0;
3530     ctemp2 = 0.0;
3531     ctemp3 = 0.0;
3532     ctemp4 = 0.0;
3533     status = 0;
3534     if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status))
3535     {
3536         fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status);
3537         fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status);
3538         fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status);
3539         status = 0;
3540         if (mirror != 0)
3541         {
3542             if (angle == 0)
3543             {
3544                 fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status);
3545                 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3546             }
3547             else if (angle == 90)
3548             {
3549                 fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status);
3550                 fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status);
3551                 fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status);
3552                 fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status);
3553             }
3554             else if (angle == 180)
3555             {
3556                 fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status);
3557                 fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status);
3558                 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3559                 fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status);
3560             }
3561             else if (angle == 270)
3562             {
3563                 fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status);
3564                 fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status);
3565                 fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status);
3566                 fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status);
3567             }
3568         }
3569         else
3570         {
3571             if (angle == 90)
3572             {
3573                 fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status);
3574                 fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status);
3575                 fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status);
3576                 fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status);
3577             }
3578             else if (angle == 180)
3579             {
3580                 fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status);
3581                 fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status);
3582                 fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status);
3583                 fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status);
3584             }
3585             else if (angle == 270)
3586             {
3587                 fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status);
3588                 fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status);
3589                 fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status);
3590                 fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status);
3591             }
3592         }
3593     }
3594 
3595     /* Delete any polynomial solution */
3596     /* (These could maybe be switched, but I don't want to work them out yet */
3597     status = 0;
3598     if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status))
3599     {
3600         int i;
3601         char keyword[16];
3602 
3603         for (i = 1; i < 13; i++)
3604         {
3605             sprintf(keyword, "CO1_%d", i);
3606             fits_delete_key(fptr, keyword, &status);
3607         }
3608         for (i = 1; i < 13; i++)
3609         {
3610             sprintf(keyword, "CO2_%d", i);
3611             fits_delete_key(fptr, keyword, &status);
3612         }
3613     }
3614 
3615 }
3616 
3617 uint8_t * FITSData::getWritableImageBuffer()
3618 {
3619     return m_ImageBuffer;
3620 }
3621 
3622 uint8_t const * FITSData::getImageBuffer() const
3623 {
3624     return m_ImageBuffer;
3625 }
3626 
3627 void FITSData::setImageBuffer(uint8_t * buffer)
3628 {
3629     delete[] m_ImageBuffer;
3630     m_ImageBuffer = buffer;
3631 }
3632 
3633 bool FITSData::checkDebayer()
3634 {
3635     int status = 0;
3636     char bayerPattern[64], roworder[64];
3637 
3638     // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image
3639     if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status))
3640         return false;
3641 
3642     if (m_Statistics.dataType != TUSHORT && m_Statistics.dataType != TBYTE)
3643     {
3644         m_LastError = i18n("Only 8 and 16 bits bayered images supported.");
3645         return false;
3646     }
3647     QString pattern(bayerPattern);
3648     pattern = pattern.remove('\'').trimmed();
3649 
3650     QString order(roworder);
3651     order = order.remove('\'').trimmed();
3652 
3653     if (order == "BOTTOM-UP" && !(m_Statistics.height % 2))
3654     {
3655         if (pattern == "RGGB")
3656             pattern = "GBRG";
3657         else if (pattern == "GBRG")
3658             pattern = "RGGB";
3659         else if (pattern == "GRBG")
3660             pattern = "BGGR";
3661         else if (pattern == "BGGR")
3662             pattern = "GRBG";
3663         else return false;
3664     }
3665 
3666     if (pattern == "RGGB")
3667         debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
3668     else if (pattern == "GBRG")
3669         debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
3670     else if (pattern == "GRBG")
3671         debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
3672     else if (pattern == "BGGR")
3673         debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
3674     // We return unless we find a valid pattern
3675     else
3676     {
3677         m_LastError = i18n("Unsupported bayer pattern %1.", pattern);
3678         return false;
3679     }
3680 
3681     fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status);
3682     fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status);
3683 
3684     if (debayerParams.offsetX == 1)
3685     {
3686         // This may leave odd values in the 0th column if the color filter is not there
3687         // in the sensor, but otherwise should process the offset correctly.
3688         // Only offsets of 0 or 1 are implemented in debayer_8bit() and debayer_16bit().
3689         switch (debayerParams.filter)
3690         {
3691             case DC1394_COLOR_FILTER_RGGB:
3692                 debayerParams.filter = DC1394_COLOR_FILTER_GRBG;
3693                 break;
3694             case DC1394_COLOR_FILTER_GBRG:
3695                 debayerParams.filter = DC1394_COLOR_FILTER_BGGR;
3696                 break;
3697             case DC1394_COLOR_FILTER_GRBG:
3698                 debayerParams.filter = DC1394_COLOR_FILTER_RGGB;
3699                 break;
3700             case DC1394_COLOR_FILTER_BGGR:
3701                 debayerParams.filter = DC1394_COLOR_FILTER_GBRG;
3702                 break;
3703         }
3704         debayerParams.offsetX = 0;
3705     }
3706     if (debayerParams.offsetX != 0 || debayerParams.offsetY > 1 || debayerParams.offsetY < 0)
3707     {
3708         m_LastError = i18n("Unsupported bayer offsets %1 %2.", debayerParams.offsetX, debayerParams.offsetY);
3709         return false;
3710     }
3711 
3712     HasDebayer = true;
3713 
3714     return true;
3715 }
3716 
3717 void FITSData::getBayerParams(BayerParams * param)
3718 {
3719     param->method  = debayerParams.method;
3720     param->filter  = debayerParams.filter;
3721     param->offsetX = debayerParams.offsetX;
3722     param->offsetY = debayerParams.offsetY;
3723 }
3724 
3725 void FITSData::setBayerParams(BayerParams * param)
3726 {
3727     debayerParams.method  = param->method;
3728     debayerParams.filter  = param->filter;
3729     debayerParams.offsetX = param->offsetX;
3730     debayerParams.offsetY = param->offsetY;
3731 }
3732 
3733 bool FITSData::debayer(bool reload)
3734 {
3735     if (reload)
3736     {
3737         int anynull = 0, status = 0;
3738 
3739         if (fits_read_img(fptr, m_Statistics.dataType, 1, m_Statistics.samples_per_channel, nullptr, m_ImageBuffer,
3740                           &anynull, &status))
3741         {
3742             //                char errmsg[512];
3743             //                fits_get_errstatus(status, errmsg);
3744             //                KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error"));
3745             return false;
3746         }
3747     }
3748 
3749     switch (m_Statistics.dataType)
3750     {
3751         case TBYTE:
3752             return debayer_8bit();
3753 
3754         case TUSHORT:
3755             return debayer_16bit();
3756 
3757         default:
3758             return false;
3759     }
3760 }
3761 
3762 bool FITSData::debayer_8bit()
3763 {
3764     dc1394error_t error_code;
3765 
3766     uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
3767     uint8_t * destinationBuffer = nullptr;
3768 
3769     try
3770     {
3771         destinationBuffer = new uint8_t[rgb_size];
3772     }
3773     catch (const std::bad_alloc &e)
3774     {
3775         logOOMError(rgb_size);
3776         m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3777         return false;
3778     }
3779 
3780     auto * bayer_source_buffer      = reinterpret_cast<uint8_t *>(m_ImageBuffer);
3781     auto * bayer_destination_buffer = reinterpret_cast<uint8_t *>(destinationBuffer);
3782 
3783     if (bayer_destination_buffer == nullptr)
3784     {
3785         logOOMError(rgb_size);
3786         m_LastError = i18n("Unable to allocate memory for temporary bayer buffer.");
3787         return false;
3788     }
3789 
3790     int ds1394_height = m_Statistics.height;
3791     auto dc1394_source = bayer_source_buffer;
3792 
3793     if (debayerParams.offsetY == 1)
3794     {
3795         dc1394_source += m_Statistics.width;
3796         ds1394_height--;
3797     }
3798     // offsetX == 1 is handled in checkDebayer() and should be 0 here.
3799 
3800     error_code = dc1394_bayer_decoding_8bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
3801                                             debayerParams.filter,
3802                                             debayerParams.method);
3803 
3804     if (error_code != DC1394_SUCCESS)
3805     {
3806         m_LastError = i18n("Debayer failed (%1)", error_code);
3807         m_Statistics.channels = 1;
3808         delete[] destinationBuffer;
3809         return false;
3810     }
3811 
3812     if (m_ImageBufferSize != rgb_size)
3813     {
3814         delete[] m_ImageBuffer;
3815         try
3816         {
3817             m_ImageBuffer = new uint8_t[rgb_size];
3818         }
3819         catch (const std::bad_alloc &e)
3820         {
3821             delete[] destinationBuffer;
3822             logOOMError(rgb_size);
3823             m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3824             return false;
3825         }
3826 
3827         m_ImageBufferSize = rgb_size;
3828     }
3829 
3830     auto bayered_buffer = reinterpret_cast<uint8_t *>(m_ImageBuffer);
3831 
3832     // Data in R1G1B1, we need to copy them into 3 layers for FITS
3833 
3834     uint8_t * rBuff = bayered_buffer;
3835     uint8_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
3836     uint8_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
3837 
3838     int imax = m_Statistics.samples_per_channel * 3 - 3;
3839     for (int i = 0; i <= imax; i += 3)
3840     {
3841         *rBuff++ = bayer_destination_buffer[i];
3842         *gBuff++ = bayer_destination_buffer[i + 1];
3843         *bBuff++ = bayer_destination_buffer[i + 2];
3844     }
3845 
3846     // TODO Maybe all should be treated the same
3847     // Doing single channel saves lots of memory though for non-essential
3848     // frames
3849     m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
3850     m_Statistics.dataType = TBYTE;
3851     delete[] destinationBuffer;
3852     return true;
3853 }
3854 
3855 bool FITSData::debayer_16bit()
3856 {
3857     dc1394error_t error_code;
3858 
3859     uint32_t rgb_size = m_Statistics.samples_per_channel * 3 * m_Statistics.bytesPerPixel;
3860     uint8_t *destinationBuffer = nullptr;
3861     try
3862     {
3863         destinationBuffer = new uint8_t[rgb_size];
3864     }
3865     catch (const std::bad_alloc &e)
3866     {
3867         logOOMError(rgb_size);
3868         m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3869         return false;
3870     }
3871 
3872     auto * bayer_source_buffer      = reinterpret_cast<uint16_t *>(m_ImageBuffer);
3873     auto * bayer_destination_buffer = reinterpret_cast<uint16_t *>(destinationBuffer);
3874 
3875     if (bayer_destination_buffer == nullptr)
3876     {
3877         logOOMError(rgb_size);
3878         m_LastError = i18n("Unable to allocate memory for temporary bayer buffer.");
3879         return false;
3880     }
3881 
3882     int ds1394_height = m_Statistics.height;
3883     auto dc1394_source = bayer_source_buffer;
3884 
3885     if (debayerParams.offsetY == 1)
3886     {
3887         dc1394_source += m_Statistics.width;
3888         ds1394_height--;
3889     }
3890     // offsetX == 1 is handled in checkDebayer() and should be 0 here.
3891 
3892     error_code = dc1394_bayer_decoding_16bit(dc1394_source, bayer_destination_buffer, m_Statistics.width, ds1394_height,
3893                  debayerParams.filter,
3894                  debayerParams.method, 16);
3895 
3896     if (error_code != DC1394_SUCCESS)
3897     {
3898         m_LastError = i18n("Debayer failed (%1)");
3899         m_Statistics.channels = 1;
3900         delete[] destinationBuffer;
3901         return false;
3902     }
3903 
3904     if (m_ImageBufferSize != rgb_size)
3905     {
3906         delete[] m_ImageBuffer;
3907         try
3908         {
3909             m_ImageBuffer = new uint8_t[rgb_size];
3910         }
3911         catch (const std::bad_alloc &e)
3912         {
3913             logOOMError(rgb_size);
3914             delete[] destinationBuffer;
3915             m_LastError = i18n("Unable to allocate memory for temporary bayer buffer: %1", e.what());
3916             return false;
3917         }
3918 
3919         m_ImageBufferSize = rgb_size;
3920     }
3921 
3922     auto bayered_buffer = reinterpret_cast<uint16_t *>(m_ImageBuffer);
3923 
3924     // Data in R1G1B1, we need to copy them into 3 layers for FITS
3925 
3926     uint16_t * rBuff = bayered_buffer;
3927     uint16_t * gBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height);
3928     uint16_t * bBuff = bayered_buffer + (m_Statistics.width * m_Statistics.height * 2);
3929 
3930     int imax = m_Statistics.samples_per_channel * 3 - 3;
3931     for (int i = 0; i <= imax; i += 3)
3932     {
3933         *rBuff++ = bayer_destination_buffer[i];
3934         *gBuff++ = bayer_destination_buffer[i + 1];
3935         *bBuff++ = bayer_destination_buffer[i + 2];
3936     }
3937 
3938     m_Statistics.channels = (m_Mode == FITS_NORMAL || m_Mode == FITS_CALIBRATE) ? 3 : 1;
3939     m_Statistics.dataType = TUSHORT;
3940     delete[] destinationBuffer;
3941     return true;
3942 }
3943 
3944 void FITSData::logOOMError(uint32_t requiredMemory)
3945 {
3946     qCCritical(KSTARS_FITS) << "Debayed memory allocation failure. Required Memory:" << KFormat().formatByteSize(requiredMemory)
3947                             << "Available system memory:" << KSUtils::getAvailableRAM();
3948 }
3949 
3950 double FITSData::getADU() const
3951 {
3952     double adu = 0;
3953     for (int i = 0; i < m_Statistics.channels; i++)
3954         adu += m_Statistics.mean[i];
3955 
3956     return (adu / static_cast<double>(m_Statistics.channels));
3957 }
3958 
3959 QString FITSData::getLastError() const
3960 {
3961     return m_LastError;
3962 }
3963 
3964 template <typename T>
3965 void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image)
3966 {
3967     const auto * buffer = reinterpret_cast<const T *>(getImageBuffer());
3968     const T limit   = std::numeric_limits<T>::max();
3969     T bMin    = dataMin < 0 ? 0 : dataMin;
3970     T bMax    = dataMax > limit ? limit : dataMax;
3971     uint16_t w    = width();
3972     uint16_t h    = height();
3973     uint32_t size = w * h;
3974     double val;
3975 
3976     if (channels() == 1)
3977     {
3978         /* Fill in pixel values using indexed map, linear scale */
3979         for (int j = 0; j < h; j++)
3980         {
3981             unsigned char * scanLine = image.scanLine(j);
3982 
3983             for (int i = 0; i < w; i++)
3984             {
3985                 val         = qBound(bMin, buffer[j * w + i], bMax);
3986                 val         = val * scale + zero;
3987                 scanLine[i] = qBound<unsigned char>(0, static_cast<uint8_t>(val), 255);
3988             }
3989         }
3990     }
3991     else
3992     {
3993         double rval = 0, gval = 0, bval = 0;
3994         QRgb value;
3995         /* Fill in pixel values using indexed map, linear scale */
3996         for (int j = 0; j < h; j++)
3997         {
3998             auto * scanLine = reinterpret_cast<QRgb *>((image.scanLine(j)));
3999 
4000             for (int i = 0; i < w; i++)
4001             {
4002                 rval = qBound(bMin, buffer[j * w + i], bMax);
4003                 gval = qBound(bMin, buffer[j * w + i + size], bMax);
4004                 bval = qBound(bMin, buffer[j * w + i + size * 2], bMax);
4005 
4006                 value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero);
4007 
4008                 scanLine[i] = value;
4009             }
4010         }
4011     }
4012 }
4013 
4014 QImage FITSData::FITSToImage(const QString &filename)
4015 {
4016     QImage fitsImage;
4017     double min, max;
4018 
4019     FITSData data;
4020 
4021     QFuture<bool> future = data.loadFromFile(filename);
4022 
4023     // Wait synchronously
4024     future.waitForFinished();
4025     if (future.result() == false)
4026         return fitsImage;
4027 
4028     data.getMinMax(&min, &max);
4029 
4030     if (min == max)
4031     {
4032         fitsImage.fill(Qt::white);
4033         return fitsImage;
4034     }
4035 
4036     if (data.channels() == 1)
4037     {
4038         fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8);
4039 
4040         fitsImage.setColorCount(256);
4041         for (int i = 0; i < 256; i++)
4042             fitsImage.setColor(i, qRgb(i, i, i));
4043     }
4044     else
4045     {
4046         fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32);
4047     }
4048 
4049     double dataMin = data.m_Statistics.mean[0] - data.m_Statistics.stddev[0];
4050     double dataMax = data.m_Statistics.mean[0] + data.m_Statistics.stddev[0] * 3;
4051 
4052     double bscale = 255. / (dataMax - dataMin);
4053     double bzero  = (-dataMin) * (255. / (dataMax - dataMin));
4054 
4055     // Long way to do this since we do not want to use templated functions here
4056     switch (data.m_Statistics.dataType)
4057     {
4058         case TBYTE:
4059             data.convertToQImage<uint8_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4060             break;
4061 
4062         case TSHORT:
4063             data.convertToQImage<int16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4064             break;
4065 
4066         case TUSHORT:
4067             data.convertToQImage<uint16_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4068             break;
4069 
4070         case TLONG:
4071             data.convertToQImage<int32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4072             break;
4073 
4074         case TULONG:
4075             data.convertToQImage<uint32_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4076             break;
4077 
4078         case TFLOAT:
4079             data.convertToQImage<float>(dataMin, dataMax, bscale, bzero, fitsImage);
4080             break;
4081 
4082         case TLONGLONG:
4083             data.convertToQImage<int64_t>(dataMin, dataMax, bscale, bzero, fitsImage);
4084             break;
4085 
4086         case TDOUBLE:
4087             data.convertToQImage<double>(dataMin, dataMax, bscale, bzero, fitsImage);
4088             break;
4089 
4090         default:
4091             break;
4092     }
4093 
4094     return fitsImage;
4095 }
4096 
4097 bool FITSData::ImageToFITS(const QString &filename, const QString &format, QString &output)
4098 {
4099     if (QImageReader::supportedImageFormats().contains(format.toLatin1()) == false)
4100     {
4101         qCCritical(KSTARS_FITS) << "Failed to convert" << filename << "to FITS since format" << format << "is not supported in Qt";
4102         return false;
4103     }
4104 
4105     QImage input;
4106 
4107     if (input.load(filename, format.toLatin1()) == false)
4108     {
4109         qCCritical(KSTARS_FITS) << "Failed to open image" << filename;
4110         return false;
4111     }
4112 
4113     output = QDir(getTemporaryPath()).filePath(filename + ".fits");
4114 
4115     //This section sets up the FITS File
4116     fitsfile *fptr = nullptr;
4117     int status = 0;
4118     long  fpixel = 1, naxis = input.allGray() ? 2 : 3, nelements, exposure;
4119     long naxes[3] = { input.width(), input.height(), naxis == 3 ? 3 : 1 };
4120     char error_status[512] = {0};
4121 
4122     if (fits_create_file(&fptr, QString('!' + output).toLocal8Bit().data(), &status))
4123     {
4124         fits_get_errstatus(status, error_status);
4125         qCCritical(KSTARS_FITS) << "Failed to create FITS file. Error:" << error_status;
4126         return false;
4127     }
4128 
4129     if (fits_create_img(fptr, BYTE_IMG, naxis, naxes, &status))
4130     {
4131         qCWarning(KSTARS_FITS) << "fits_create_img failed:" << error_status;
4132         status = 0;
4133         fits_flush_file(fptr, &status);
4134         fits_close_file(fptr, &status);
4135         return false;
4136     }
4137 
4138     exposure = 1;
4139     fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);
4140 
4141     // Gray image
4142     if (naxis == 2)
4143     {
4144         nelements = naxes[0] * naxes[1];
4145         if (fits_write_img(fptr, TBYTE, fpixel, nelements, input.bits(), &status))
4146         {
4147             fits_get_errstatus(status, error_status);
4148             qCWarning(KSTARS_FITS) << "fits_write_img GRAY failed:" << error_status;
4149             status = 0;
4150             fits_flush_file(fptr, &status);
4151             fits_close_file(fptr, &status);
4152             return false;
4153         }
4154     }
4155     // RGB image, we have to convert from ARGB format to R G B for each plane
4156     else
4157     {
4158         nelements = naxes[0] * naxes[1] * 3;
4159 
4160         uint8_t *srcBuffer = input.bits();
4161         // ARGB
4162         uint32_t srcBytes = naxes[0] * naxes[1] * 4 - 4;
4163 
4164         uint8_t *rgbBuffer = new uint8_t[nelements];
4165         if (rgbBuffer == nullptr)
4166         {
4167             qCWarning(KSTARS_FITS) << "Not enough memory for RGB buffer";
4168             fits_flush_file(fptr, &status);
4169             fits_close_file(fptr, &status);
4170             return false;
4171         }
4172 
4173         uint8_t *subR = rgbBuffer;
4174         uint8_t *subG = rgbBuffer + naxes[0] * naxes[1];
4175         uint8_t *subB = rgbBuffer + naxes[0] * naxes[1] * 2;
4176         for (uint32_t i = 0; i < srcBytes; i += 4)
4177         {
4178             *subB++ = srcBuffer[i];
4179             *subG++ = srcBuffer[i + 1];
4180             *subR++ = srcBuffer[i + 2];
4181         }
4182 
4183         if (fits_write_img(fptr, TBYTE, fpixel, nelements, rgbBuffer, &status))
4184         {
4185             fits_get_errstatus(status, error_status);
4186             qCWarning(KSTARS_FITS) << "fits_write_img RGB failed:" << error_status;
4187             status = 0;
4188             fits_flush_file(fptr, &status);
4189             fits_close_file(fptr, &status);
4190             delete [] rgbBuffer;
4191             return false;
4192         }
4193 
4194         delete [] rgbBuffer;
4195     }
4196 
4197     if (fits_flush_file(fptr, &status))
4198     {
4199         fits_get_errstatus(status, error_status);
4200         qCWarning(KSTARS_FITS) << "fits_flush_file failed:" << error_status;
4201         status = 0;
4202         fits_close_file(fptr, &status);
4203         return false;
4204     }
4205 
4206     if (fits_close_file(fptr, &status))
4207     {
4208         fits_get_errstatus(status, error_status);
4209         qCWarning(KSTARS_FITS) << "fits_close_file failed:" << error_status;
4210         return false;
4211     }
4212 
4213     return true;
4214 }
4215 
4216 void FITSData::injectWCS(double orientation, double ra, double dec, double pixscale, bool eastToTheRight)
4217 {
4218     int status = 0;
4219 
4220     if (fptr == nullptr)
4221         return;
4222 
4223     fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status);
4224     fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status);
4225 
4226     int epoch = 2000;
4227 
4228     fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status);
4229 
4230     fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status);
4231     fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status);
4232 
4233     char radecsys[8] = "FK5";
4234     char ctype1[16]  = "RA---TAN";
4235     char ctype2[16]  = "DEC--TAN";
4236 
4237     fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status);
4238     fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status);
4239     fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status);
4240 
4241     double crpix1 = width() / 2.0;
4242     double crpix2 = height() / 2.0;
4243 
4244     fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status);
4245     fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status);
4246 
4247     // Arcsecs per Pixel
4248     double secpix1 = eastToTheRight ? pixscale : -pixscale;
4249     double secpix2 = pixscale;
4250 
4251     fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status);
4252     fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status);
4253 
4254     double degpix1 = secpix1 / 3600.0;
4255     double degpix2 = secpix2 / 3600.0;
4256 
4257     fits_update_key(fptr, TDOUBLE, "CDELT1", &degpix1, "CDELT1", &status);
4258     fits_update_key(fptr, TDOUBLE, "CDELT2", &degpix2, "CDELT2", &status);
4259 
4260     // Rotation is CW, we need to convert it to CCW per CROTA1 definition
4261     double rotation = 360 - orientation;
4262     if (rotation > 360)
4263         rotation -= 360;
4264 
4265     fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status);
4266     fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status);
4267 
4268     m_WCSState = Idle;
4269 }
4270 
4271 bool FITSData::contains(const QPointF &point) const
4272 {
4273     return (point.x() >= 0 && point.y() >= 0 && point.x() <= m_Statistics.width && point.y() <= m_Statistics.height);
4274 }
4275 
4276 void FITSData::saveStatistics(FITSImage::Statistic &other)
4277 {
4278     other = m_Statistics;
4279 }
4280 
4281 void FITSData::restoreStatistics(FITSImage::Statistic &other)
4282 {
4283     m_Statistics = other;
4284 
4285     emit dataChanged();
4286 }
4287 
4288 void FITSData::constructHistogram()
4289 {
4290     switch (m_Statistics.dataType)
4291     {
4292         case TBYTE:
4293             constructHistogramInternal<uint8_t>();
4294             break;
4295 
4296         case TSHORT:
4297             constructHistogramInternal<int16_t>();
4298             break;
4299 
4300         case TUSHORT:
4301             constructHistogramInternal<uint16_t>();
4302             break;
4303 
4304         case TLONG:
4305             constructHistogramInternal<int32_t>();
4306             break;
4307 
4308         case TULONG:
4309             constructHistogramInternal<uint32_t>();
4310             break;
4311 
4312         case TFLOAT:
4313             constructHistogramInternal<float>();
4314             break;
4315 
4316         case TLONGLONG:
4317             constructHistogramInternal<int64_t>();
4318             break;
4319 
4320         case TDOUBLE:
4321             constructHistogramInternal<double>();
4322             break;
4323 
4324         default:
4325             break;
4326     }
4327 }
4328 
4329 template <typename T> int32_t FITSData::histogramBinInternal(T value, int channel) const
4330 {
4331     return qMax(static_cast<T>(0), qMin(static_cast<T>(m_HistogramBinCount),
4332                                         static_cast<T>(rint((value - m_Statistics.min[channel]) / m_HistogramBinWidth[channel]))));
4333 }
4334 
4335 template <typename T> int32_t FITSData::histogramBinInternal(int x, int y, int channel) const
4336 {
4337     if (!m_ImageBuffer || !isHistogramConstructed())
4338         return 0;
4339     uint32_t samples = m_Statistics.width * m_Statistics.height;
4340     uint32_t offset = channel * samples;
4341     auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
4342     int index = y * m_Statistics.width + x;
4343     const T &sample = buffer[index + offset];
4344     return histogramBinInternal(sample, channel);
4345 }
4346 
4347 int32_t FITSData::histogramBin(int x, int y, int channel) const
4348 {
4349     switch (m_Statistics.dataType)
4350     {
4351         case TBYTE:
4352             return histogramBinInternal<uint8_t>(x, y, channel);
4353             break;
4354 
4355         case TSHORT:
4356             return histogramBinInternal<int16_t>(x, y, channel);
4357             break;
4358 
4359         case TUSHORT:
4360             return histogramBinInternal<uint16_t>(x, y, channel);
4361             break;
4362 
4363         case TLONG:
4364             return histogramBinInternal<int32_t>(x, y, channel);
4365             break;
4366 
4367         case TULONG:
4368             return histogramBinInternal<uint32_t>(x, y, channel);
4369             break;
4370 
4371         case TFLOAT:
4372             return histogramBinInternal<float>(x, y, channel);
4373             break;
4374 
4375         case TLONGLONG:
4376             return histogramBinInternal<int64_t>(x, y, channel);
4377             break;
4378 
4379         case TDOUBLE:
4380             return histogramBinInternal<double>(x, y, channel);
4381             break;
4382 
4383         default:
4384             return 0;
4385             break;
4386     }
4387 }
4388 
4389 template <typename T> void FITSData::constructHistogramInternal()
4390 {
4391     auto * const buffer = reinterpret_cast<T const *>(m_ImageBuffer);
4392     uint32_t samples = m_Statistics.width * m_Statistics.height;
4393     const uint32_t sampleBy = samples > 500000 ? samples / 500000 : 1;
4394 
4395     m_HistogramBinCount = qMax(0., qMin(m_Statistics.max[0] - m_Statistics.min[0], 256.0));
4396     if (m_HistogramBinCount <= 0)
4397         m_HistogramBinCount = 256;
4398 
4399     for (int n = 0; n < m_Statistics.channels; n++)
4400     {
4401         m_HistogramIntensity[n].fill(0, m_HistogramBinCount + 1);
4402         m_HistogramFrequency[n].fill(0, m_HistogramBinCount + 1);
4403         m_CumulativeFrequency[n].fill(0, m_HistogramBinCount + 1);
4404         // Distinguish between 0-1.0 ranges and ranges with integer values.
4405         const double minBinSize = (m_Statistics.max[n] > 1.1) ? 1.0 : .0001;
4406         m_HistogramBinWidth[n] = qMax(minBinSize, (m_Statistics.max[n] - m_Statistics.min[n]) / m_HistogramBinCount);
4407     }
4408 
4409     QVector<QFuture<void>> futures;
4410 
4411     for (int n = 0; n < m_Statistics.channels; n++)
4412     {
4413         futures.append(QtConcurrent::run([ = ]()
4414         {
4415             for (int i = 0; i < m_HistogramBinCount; i++)
4416                 m_HistogramIntensity[n][i] = m_Statistics.min[n] + (m_HistogramBinWidth[n] * i);
4417         }));
4418     }
4419 
4420     for (int n = 0; n < m_Statistics.channels; n++)
4421     {
4422         futures.append(QtConcurrent::run([ = ]()
4423         {
4424             uint32_t offset = n * samples;
4425 
4426             for (uint32_t i = 0; i < samples; i += sampleBy)
4427             {
4428                 int32_t id = histogramBinInternal<T>(buffer[i + offset], n);
4429                 m_HistogramFrequency[n][id] += sampleBy;
4430             }
4431         }));
4432     }
4433 
4434     for (QFuture<void> future : futures)
4435         future.waitForFinished();
4436 
4437     futures.clear();
4438 
4439     for (int n = 0; n < m_Statistics.channels; n++)
4440     {
4441         futures.append(QtConcurrent::run([ = ]()
4442         {
4443             uint32_t accumulator = 0;
4444             for (int i = 0; i < m_HistogramBinCount; i++)
4445             {
4446                 accumulator += m_HistogramFrequency[n][i];
4447                 m_CumulativeFrequency[n].replace(i, accumulator);
4448             }
4449         }));
4450     }
4451 
4452     for (QFuture<void> future : futures)
4453         future.waitForFinished();
4454 
4455     futures.clear();
4456 
4457     // Custom index to indicate the overall contrast of the image
4458     if (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 4] > 0)
4459         m_JMIndex = m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount / 8] / static_cast<double>
4460                     (m_CumulativeFrequency[RED_CHANNEL][m_HistogramBinCount /
4461                             4]);
4462     else
4463         m_JMIndex = 1;
4464 
4465     qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << m_JMIndex;
4466 
4467     m_HistogramConstructed = true;
4468     emit histogramReady();
4469 }
4470 
4471 void FITSData::recordLastError(int errorCode)
4472 {
4473     char fitsErrorMessage[512] = {0};
4474     fits_get_errstatus(errorCode, fitsErrorMessage);
4475     m_LastError = fitsErrorMessage;
4476 }
4477 
4478 double FITSData::getAverageMean(bool roi) const
4479 {
4480     const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4481     if (ptr->channels == 1)
4482         return ptr->mean[0];
4483     else
4484         return (ptr->mean[0] + ptr->mean[1] + ptr->mean[2]) / 3.0;
4485 }
4486 
4487 double FITSData::getAverageMedian(bool roi) const
4488 {
4489     const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4490     if (ptr->channels == 1)
4491         return ptr->median[0];
4492     else
4493         return (ptr->median[0] + ptr->median[1] + ptr->median[2]) / 3.0;
4494 }
4495 
4496 double FITSData::getAverageStdDev(bool roi) const
4497 {
4498     const FITSImage::Statistic* ptr = (roi ? &m_ROIStatistics : &m_Statistics);
4499     if (ptr->channels == 1)
4500         return ptr->stddev[0];
4501     else
4502         return (ptr->stddev[0] + ptr->stddev[1] + ptr->stddev[2]) / 3.0;
4503 }