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 }