File indexing completed on 2024-05-05 15:55:12

0001 /*
0002     SPDX-FileCopyrightText: 2023
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #pragma once
0008 
0009 #include <QList>
0010 #include "../fitsviewer/fitsstardetector.h"
0011 #include "fitsviewer/fitsview.h"
0012 #include "fitsviewer/fitsdata.h"
0013 #include "auxiliary/imagemask.h"
0014 #include "aberrationinspectorutils.h"
0015 #include "curvefit.h"
0016 #include "../ekos.h"
0017 #include <ekos_focus_debug.h>
0018 #include <gsl/gsl_fft_complex.h>
0019 
0020 namespace Ekos
0021 {
0022 
0023 // This class performs Fourier Transform processing on the passed in FITS image.
0024 // The approach is based on this paper: https://doi.org/10.1093/mnras/stac189
0025 // (preprint, see https://arxiv.org/abs/2201.12466 )
0026 // The idea is as follows: a focus frame consists of stars and noise. As long as the
0027 // exposure is long enough (a few seconds) then the star can be treated as gaussians.
0028 // A fourier transform (FFT) of a gaussian is another gaussian. A narrow gaussian in the space
0029 // domain transforms to a wider gaussian in frequency domain. So the nearer to focus we
0030 // get, the sharper the stars (smaller HFR) become, and thereform the wider their FFTs.
0031 // So the nearer to focus we get the bigger the frequency domain's power spectrum becomes
0032 // which means the power reaches a maximum in the frequency domain.
0033 //
0034 // Random noise transforms to random noise in the frequency domain. The power spectrum of
0035 // the FFT of noise typically swamps the power spectrum of the stars' signal so it needs to
0036 // be dealt with. Tan and Schulz suggest treating noise as "white noise" which means it adds
0037 // the same power component to all frequencies. This means that be averaging the power
0038 // high frequency where the star contribution is zero, gives the noise component. This can
0039 // then to subtracted from each frequency to leave the power contribution of just the stars.
0040 //
0041 // I have found better results by background subtracting the frame before fourier transforming
0042 // which is the same thing as all thats left to FFT is an image of the stars. Currently
0043 // fitsviewer will have procvessed the image prior to this routine being called so background
0044 // information is available here.
0045 //
0046 // So we need to perform a 2D FFT on the image. GSL only performs basic 1D FFTs so this
0047 // routine performs a FFT on each row and then uses the results to perform a FFT on each
0048 // column. An optimisation would be to use a more sophisticated FFT routine. FFTW3 could,
0049 // for example, be used.
0050 //
0051 // Currently just the first channel (if there is more than 1) is used by this routine. It would
0052 // be possible to use all channels or offer the user a choice of which channel(s) to use. If
0053 // this were to be done it would make sense to synchonise this functionality with fitsviwer
0054 // functionality on HFR, FWHM, etc.
0055 
0056 class FocusFourierPower
0057 {
0058     public:
0059 
0060         FocusFourierPower(Mathematics::RobustStatistics::ScaleCalculation scaleCalc);
0061         ~FocusFourierPower();
0062 
0063         template <typename T>
0064         void processFourierPower(const T &imageBuffer, const QSharedPointer<FITSData> &imageData,
0065                                  const QSharedPointer<ImageMask> &mask, const int &tile, double *fourierPower, double *weight)
0066         {
0067             // Initialise outputs
0068             *fourierPower = INVALID_STAR_MEASURE;
0069             *weight = 1.0;
0070 
0071             // Check mask & tile inputs
0072             ImageMosaicMask *mosaicMask = nullptr;
0073             if (tile >= 0)
0074             {
0075                 if (mask)
0076                 {
0077                     if (tile >= NUM_TILES)
0078                     {
0079                         qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 Fourier Transform called with invalid mosaic tile %2").arg(__FUNCTION__)
0080                                                    .arg(tile);
0081                         return;
0082                     }
0083 
0084                     mosaicMask = dynamic_cast<ImageMosaicMask *>(mask.get());
0085                     if (!mosaicMask)
0086                     {
0087                         qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 Fourier Transform called with invalid 2 mosaic tile %2").arg(__FUNCTION__)
0088                                                    .arg(tile);
0089                         return;
0090                     }
0091                 }
0092                 else
0093                 {
0094                     qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 Fourier Transform called for mosaic tile but no mask").arg(__FUNCTION__);
0095                     return;
0096                 }
0097             }
0098 
0099             // Dimensions on area to perform Fourier Transform on; whole sensor or just tile
0100             auto stats = imageData->getStatistics();
0101             unsigned int width, height;
0102             if (tile < 0)
0103             {
0104                 width = stats.width;
0105                 height = stats.height;
0106             }
0107             else
0108             {
0109                 // Calculating for a single (square) mosaic tile
0110                 width = mosaicMask->tiles()[tile].width();
0111                 height = width;
0112             }
0113 
0114             // Allocate memory for the Fourier Transform
0115             unsigned long N = width * height;
0116             double *image = new(std::nothrow) double[2 * N];
0117             if (!image)
0118             {
0119                 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 Unable to allocate memory to perform Fourier Transforms").arg(__FUNCTION__);
0120                 return;
0121             }
0122 
0123             /* Allocate memory for GSL complex wavetables, and workspace */
0124             gsl_fft_complex_wavetable *rowWT = gsl_fft_complex_wavetable_alloc(width);
0125             gsl_fft_complex_workspace *rowWS = gsl_fft_complex_workspace_alloc(width);
0126             gsl_fft_complex_wavetable *colWT = gsl_fft_complex_wavetable_alloc(height);
0127             gsl_fft_complex_workspace *colWS = gsl_fft_complex_workspace_alloc(height);
0128             if (!rowWT || !rowWS || !colWT || !colWS)
0129             {
0130                 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 Unable to allocate memory2 to perform Fourier Transforms").arg(__FUNCTION__);
0131                 delete[] image;
0132                 return;
0133             }
0134 
0135             // Set the gsl error handler off as it aborts the program on error.
0136             auto const oldErrorHandler = gsl_set_error_handler_off();
0137 
0138             // Convert the image to complex double datatype as required by GSL FFT
0139             // The real part is just the background subtracted pixel value clipped to zero
0140             // The imaginary part is zero.
0141             auto skyBackground = imageData->getSkyBackground();
0142             auto bg = skyBackground.mean + 3.0 * skyBackground.sigma;
0143 
0144             // Load the "image" buffer from the passed in image data. As the loop is quite large I've created 3 loops
0145             // each with minimal work inside. This makes the overall code larger but avoids repeated tests within the loop
0146             if (tile < 0)
0147             {
0148                 // Setup image for whole sensor
0149                 if (mask.isNull() || mask->active() == false)
0150                 {
0151                     // No active mask
0152                     for (unsigned long i = 0; i < N; i++)
0153                     {
0154                         image[i * 2] = std::max(0.0, (double) imageBuffer[i] - bg);
0155                         image[i * 2 + 1] = 0.0;
0156                     }
0157                 }
0158                 else
0159                 {
0160                     // There is an active mask on the sensor so honour these settings
0161                     unsigned int posX = 0, posY = 0;
0162                     for (unsigned long i = 0; i < N; i++)
0163                     {
0164                         if (mask->isVisible(posX, posY))
0165                             image[i * 2] = std::max(0.0, (double) imageBuffer[i] - bg);
0166                         else
0167                             image[i * 2] = 0.0;
0168 
0169                         image[i * 2 + 1] = 0.0;
0170 
0171                         if (++posX == stats.width)
0172                         {
0173                             posX = 0;
0174                             posY++;
0175                         }
0176                     }
0177                 }
0178             }
0179             else
0180             {
0181                 // A mosaic tile has been specified so we know we are dealing with a mosaic mask
0182                 unsigned int posX = mosaicMask->tiles()[tile].topLeft().x();
0183                 unsigned int posY = mosaicMask->tiles()[tile].topLeft().y();
0184 
0185                 unsigned long offset = posY * stats.width + posX;
0186                 const unsigned int widthLimit = posX + width;
0187 
0188                 // Perform calc for a specific tile of a mosaic mask
0189                 for (unsigned long i = 0; i < N; i++)
0190                 {
0191                     image[i * 2] = std::max(0.0, (double) imageBuffer[offset + i] - bg);
0192                     image[i * 2 + 1] = 0.0;
0193 
0194                     if (++posX == widthLimit)
0195                     {
0196                         posX = mosaicMask->tiles()[tile].topLeft().x();
0197                         offset += stats.width - width;
0198                     }
0199                 }
0200             }
0201 
0202             // Perform FFT on all the rows
0203             int status = 0;
0204             for (unsigned int j = 0; j < height; j++)
0205             {
0206                 status = gsl_fft_complex_forward(&image[j * 2 * width], 1, width, rowWT, rowWS);
0207                 if (status != 0)
0208                 {
0209                     qCDebug(KSTARS_EKOS_FOCUS) << QString("Error %1 [%2] calculating FFT on row %3").arg(status).arg(gsl_strerror(status)).
0210                                                arg(j);
0211                     break;
0212                 }
0213             }
0214 
0215             if (status == 0)
0216             {
0217                 // Perform FFT on all the cols
0218                 for (unsigned int i = 0; i < width; i++)
0219                 {
0220                     status = gsl_fft_complex_forward(&image[2 * i], width, height, colWT, colWS);
0221                     if (status != 0)
0222                     {
0223                         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error %1 [%2] calculating FFT on col %3").arg(status).arg(gsl_strerror(status)).arg(
0224                                                        i);
0225                         break;
0226                     }
0227                 }
0228             }
0229 
0230             if (status == 0)
0231             {
0232                 double power = 0.0;
0233                 for (unsigned long i = 0; i < N; i++)
0234                     power += pow(image[i * 2], 2.0) + pow(image[i * 2 + 1], 2.0);
0235 
0236                 power /= pow(N, 2.0);
0237 
0238                 if (tile < 0)
0239                     qCDebug(KSTARS_EKOS_FOCUS) << QString("FFT power sensor %1x%2 = %3").arg(stats.width).arg(stats.height).arg(power);
0240                 else
0241                     qCDebug(KSTARS_EKOS_FOCUS) << QString("FFT power tile %1 %2x%3 = %4").arg(tile).arg(stats.width).arg(stats.height).arg(
0242                                                    power);
0243 
0244                 *fourierPower = power;
0245             }
0246 
0247             // free memory
0248             delete[] image;
0249             gsl_fft_complex_wavetable_free(rowWT);
0250             gsl_fft_complex_workspace_free(rowWS);
0251             gsl_fft_complex_wavetable_free(colWT);
0252             gsl_fft_complex_workspace_free(colWS);
0253 
0254             // Restore old GSL error handler
0255             gsl_set_error_handler(oldErrorHandler);
0256         }
0257 
0258         static double constexpr INVALID_STAR_MEASURE = -1.0;
0259 
0260     private:
0261 
0262         Mathematics::RobustStatistics::ScaleCalculation m_ScaleCalc;
0263 };
0264 }