File indexing completed on 2024-04-14 14:10:35

0001 /*
0002     SPDX-FileCopyrightText: 2004 Jasem Mutlaq
0003     SPDX-FileCopyrightText: 2020 Eric Dejouhanet <eric.dejouhanet@gmail.com>
0004     SPDX-FileCopyrightText: 2015 Gonzalo Exequiel Pedone <hipersayan.x@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later AND GPL-3.0-or-later
0007 
0008     Some code fragments were adapted from Peter Kirchgessner's FITS plugin
0009     SPDX-FileCopyrightText: Peter Kirchgessner <http://members.aol.com/pkirchg>
0010 */
0011 
0012 #include <math.h>
0013 #include <cmath>
0014 #include <QtConcurrent>
0015 
0016 #include "fits_debug.h"
0017 #include "fitsgradientdetector.h"
0018 #include "fitsdata.h"
0019 
0020 QFuture<bool> FITSGradientDetector::findSources(const QRect &boundary)
0021 {
0022     FITSImage::Statistic const &stats = m_ImageData->getStatistics();
0023     switch (stats.dataType)
0024     {
0025 
0026         case TBYTE:
0027         default:
0028             return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint8_t>, boundary);
0029 
0030         case TSHORT:
0031             return QtConcurrent::run(this, &FITSGradientDetector::findSources<int16_t>, boundary);
0032 
0033         case TUSHORT:
0034             return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint16_t>, boundary);
0035 
0036         case TLONG:
0037             return QtConcurrent::run(this, &FITSGradientDetector::findSources<int32_t>, boundary);
0038 
0039         case TULONG:
0040             return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint16_t>, boundary);
0041 
0042         case TFLOAT:
0043             return QtConcurrent::run(this, &FITSGradientDetector::findSources<float>, boundary);
0044 
0045         case TLONGLONG:
0046             return QtConcurrent::run(this, &FITSGradientDetector::findSources<int64_t>, boundary);
0047 
0048         case TDOUBLE:
0049             return QtConcurrent::run(this, &FITSGradientDetector::findSources<double>, boundary);
0050     }
0051 }
0052 
0053 template <typename T>
0054 bool FITSGradientDetector::findSources(const QRect &boundary)
0055 {
0056     int subX = qMax(0, boundary.isNull() ? 0 : boundary.x());
0057     int subY = qMax(0, boundary.isNull() ? 0 : boundary.y());
0058     int subW = (boundary.isNull() ? m_ImageData->width() : boundary.width());
0059     int subH = (boundary.isNull() ? m_ImageData->height() : boundary.height());
0060 
0061     int BBP = m_ImageData->getBytesPerPixel();
0062 
0063     uint16_t dataWidth = m_ImageData->width();
0064 
0065     // #1 Find offsets
0066     uint32_t size   = subW * subH;
0067     uint32_t offset = subX + subY * dataWidth;
0068 
0069     // #2 Create new buffer
0070     auto * buffer = new uint8_t[size * BBP];
0071     // If there is no offset, copy whole buffer in one go
0072     if (offset == 0)
0073         memcpy(buffer, m_ImageData->getImageBuffer(), size * BBP);
0074     else
0075     {
0076         uint8_t * dataPtr = buffer;
0077         uint8_t const * origDataPtr = m_ImageData->getImageBuffer();
0078         uint32_t lineOffset  = 0;
0079         // Copy data line by line
0080         for (int height = subY; height < (subY + subH); height++)
0081         {
0082             lineOffset = (subX + height * dataWidth) * BBP;
0083             memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP);
0084             dataPtr += (subW * BBP);
0085         }
0086     }
0087 
0088     // #3 Create new FITSData to hold it
0089     auto * boundedImage                      = new FITSData();
0090     FITSImage::Statistic stats;
0091     stats.width               = subW;
0092     stats.height              = subH;
0093     stats.dataType            = m_ImageData->getStatistics().dataType;
0094     stats.bytesPerPixel       = m_ImageData->getStatistics().bytesPerPixel;
0095     stats.samples_per_channel = size;
0096     stats.ndim                = 2;
0097     boundedImage->restoreStatistics(stats);
0098 
0099     // #4 Set image buffer and calculate stats.
0100     boundedImage->setImageBuffer(buffer);
0101 
0102     boundedImage->calculateStats(true);
0103 
0104     // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain
0105     boundedImage->applyFilter(FITS_MEDIAN);
0106     boundedImage->applyFilter(FITS_HIGH_CONTRAST);
0107 
0108     // #6 Perform Sobel to find gradients and their directions
0109     QVector<float> gradients;
0110     QVector<float> directions;
0111 
0112     // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed
0113     // and discarded whenever necessary. It won't work on noisy images unless this is done.
0114     sobel<T>(boundedImage, gradients, directions);
0115 
0116     QVector<int> ids(gradients.size());
0117 
0118     int maxID = partition(subW, subH, gradients, ids);
0119 
0120     // Not needed anymore
0121     delete boundedImage;
0122 
0123     if (maxID == 0)
0124         return 0;
0125 
0126     typedef struct massInfo
0127     {
0128         float massX     = 0;
0129         float massY     = 0;
0130         float totalMass = 0;
0131     } massInfo;
0132 
0133     QMap<int, massInfo> masses;
0134 
0135     // #7 Calculate center of mass for all detected regions
0136     for (int y = 0; y < subH; y++)
0137     {
0138         for (int x = 0; x < subW; x++)
0139         {
0140             int index = x + y * subW;
0141 
0142             int regionID = ids[index];
0143             if (regionID > 0)
0144             {
0145                 float pixel = gradients[index];
0146 
0147                 masses[regionID].totalMass += pixel;
0148                 masses[regionID].massX += x * pixel;
0149                 masses[regionID].massY += y * pixel;
0150             }
0151         }
0152     }
0153 
0154     // Compare multiple masses, and only select the highest total mass one as the desired star
0155     int maxRegionID       = 1;
0156     int maxTotalMass      = masses[1].totalMass;
0157     double totalMassRatio = 1e6;
0158     for (auto key : masses.keys())
0159     {
0160         massInfo oneMass = masses.value(key);
0161         if (oneMass.totalMass > maxTotalMass)
0162         {
0163             totalMassRatio = oneMass.totalMass / maxTotalMass;
0164             maxTotalMass   = oneMass.totalMass;
0165             maxRegionID    = key;
0166         }
0167     }
0168 
0169     // If image has many regions and there is no significant relative center of mass then it's just noise and no stars
0170     // are probably there above a useful threshold.
0171     if (maxID > 10 && totalMassRatio < 1.5)
0172         return false;
0173 
0174     auto * center  = new Edge;
0175     center->width = -1;
0176     center->x     = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5;
0177     center->y     = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5;
0178     center->HFR   = 1;
0179 
0180     // Maximum Radius
0181     int maxR = qMin(subW - 1, subH - 1) / 2;
0182 
0183     for (int r = maxR; r > 1; r--)
0184     {
0185         int pass = 0;
0186 
0187         for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0)
0188         {
0189             int testX = center->x + std::cos(theta) * r;
0190             int testY = center->y + std::sin(theta) * r;
0191 
0192             // if out of bound, break;
0193             if (testX < 0 || testX >= subW || testY < 0 || testY >= subH)
0194                 break;
0195 
0196             if (gradients[testX + testY * subW] > 0)
0197                 //if (thresholded[testX + testY * subW] > 0)
0198             {
0199                 if (++pass >= 24)
0200                 {
0201                     center->width = r * 2;
0202                     // Break of outer loop
0203                     r = 0;
0204                     break;
0205                 }
0206             }
0207         }
0208     }
0209 
0210     qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width;
0211 
0212     // If no stars were detected
0213     if (center->width == -1)
0214     {
0215         delete center;
0216         return false;
0217     }
0218 
0219     // 30% fuzzy
0220     //center->width += center->width*0.3 * (running_threshold / threshold);
0221 
0222     double FSum = 0, HF = 0, TF = 0;
0223     const double resolution = 1.0 / 20.0;
0224 
0225     int cen_y = qRound(center->y);
0226 
0227     double rightEdge = center->x + center->width / 2.0;
0228     double leftEdge  = center->x - center->width / 2.0;
0229 
0230     QVector<double> subPixels;
0231     subPixels.reserve(center->width / resolution);
0232 
0233     const T * origBuffer = reinterpret_cast<T const *>(m_ImageData->getImageBuffer()) + offset;
0234 
0235     for (double x = leftEdge; x <= rightEdge; x += resolution)
0236     {
0237         double slice = resolution * (origBuffer[static_cast<int>(floor(x)) + cen_y * dataWidth]);
0238         FSum += slice;
0239         subPixels.append(slice);
0240     }
0241 
0242     // Half flux
0243     HF = FSum / 2.0;
0244 
0245     int subPixelCenter = (center->width / resolution) / 2;
0246 
0247     // Start from center
0248     TF            = subPixels[subPixelCenter];
0249     double lastTF = TF;
0250     // Integrate flux along radius axis until we reach half flux
0251     //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution)
0252     for (int k = 1; k < subPixelCenter; k++)
0253     {
0254         TF += subPixels[subPixelCenter + k];
0255         TF += subPixels[subPixelCenter - k];
0256 
0257         if (TF >= HF)
0258         {
0259             // We overpassed HF, let's calculate from last TF how much until we reach HF
0260 
0261             // #1 Accurate calculation, but very sensitive to small variations of flux
0262             center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution;
0263 
0264             // #2 Less accurate calculation, but stable against small variations of flux
0265             //center->HFR = (k - 1) * resolution;
0266             break;
0267         }
0268 
0269         lastTF = TF;
0270     }
0271 
0272     // Correct center for subX and subY
0273     center->x += subX;
0274     center->y += subY;
0275 
0276     QList<Edge*> starCenters;
0277     starCenters.append(center);
0278     m_ImageData->setStarCenters(starCenters);
0279 
0280     qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR;
0281 
0282     return true;
0283 }
0284 
0285 /* CannyDetector, Implementation of Canny edge detector in Qt/C++.
0286  * Web-Site: https://github.com/hipersayanX/CannyDetector
0287  */
0288 
0289 template <typename T>
0290 void FITSGradientDetector::sobel(FITSData const *data, QVector<float> &gradient, QVector<float> &direction) const
0291 {
0292     if (data == nullptr)
0293         return;
0294 
0295     FITSImage::Statistic const &stats = data->getStatistics();
0296 
0297     //int size = image.width() * image.height();
0298     gradient.resize(stats.samples_per_channel);
0299     direction.resize(stats.samples_per_channel);
0300 
0301     for (int y = 0; y < stats.height; y++)
0302     {
0303         size_t yOffset    = y * stats.width;
0304         const T * grayLine = reinterpret_cast<T const *>(data->getImageBuffer()) + yOffset;
0305 
0306         const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width;
0307         const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width;
0308 
0309         float * gradientLine  = gradient.data() + yOffset;
0310         float * directionLine = direction.data() + yOffset;
0311 
0312         for (int x = 0; x < stats.width; x++)
0313         {
0314             int x_m1 = x < 1 ? x : x - 1;
0315             int x_p1 = x >= stats.width - 1 ? x : x + 1;
0316 
0317             int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] -
0318                         2 * grayLine[x_m1] - grayLine_p1[x_m1];
0319 
0320             int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] -
0321                         2 * grayLine_p1[x] - grayLine_p1[x_p1];
0322 
0323             gradientLine[x] = qAbs(gradX) + qAbs(gradY);
0324 
0325             /* Gradient directions are classified in 4 possible cases
0326              *
0327              * dir 0
0328              *
0329              * x x x
0330              * - - -
0331              * x x x
0332              *
0333              * dir 1
0334              *
0335              * x x /
0336              * x / x
0337              * / x x
0338              *
0339              * dir 2
0340              *
0341              * \ x x
0342              * x \ x
0343              * x x \
0344              *
0345              * dir 3
0346              *
0347              * x | x
0348              * x | x
0349              * x | x
0350              */
0351             if (gradX == 0 && gradY == 0)
0352                 directionLine[x] = 0;
0353             else if (gradX == 0)
0354                 directionLine[x] = 3;
0355             else
0356             {
0357                 qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI;
0358 
0359                 if (a >= -22.5 && a < 22.5)
0360                     directionLine[x] = 0;
0361                 else if (a >= 22.5 && a < 67.5)
0362                     directionLine[x] = 2;
0363                 else if (a >= -67.5 && a < -22.5)
0364                     directionLine[x] = 1;
0365                 else
0366                     directionLine[x] = 3;
0367             }
0368         }
0369     }
0370 }
0371 
0372 int FITSGradientDetector::partition(int width, int height, QVector<float> &gradient, QVector<int> &ids) const
0373 {
0374     int id = 0;
0375 
0376     for (int y = 1; y < height - 1; y++)
0377     {
0378         for (int x = 1; x < width - 1; x++)
0379         {
0380             int index = x + y * width;
0381             float val = gradient[index];
0382             if (val > 0 && ids[index] == 0)
0383             {
0384                 trace(width, height, ++id, gradient, ids, x, y);
0385             }
0386         }
0387     }
0388 
0389     // Return max id
0390     return id;
0391 }
0392 
0393 void FITSGradientDetector::trace(int width, int height, int id, QVector<float> &image, QVector<int> &ids, int x,
0394                                  int y) const
0395 {
0396     int yOffset      = y * width;
0397     float * cannyLine = image.data() + yOffset;
0398     int * idLine      = ids.data() + yOffset;
0399 
0400     if (idLine[x] != 0)
0401         return;
0402 
0403     idLine[x] = id;
0404 
0405     for (int j = -1; j < 2; j++)
0406     {
0407         int nextY = y + j;
0408 
0409         if (nextY < 0 || nextY >= height)
0410             continue;
0411 
0412         float * cannyLineNext = cannyLine + j * width;
0413 
0414         for (int i = -1; i < 2; i++)
0415         {
0416             int nextX = x + i;
0417 
0418             if (i == j || nextX < 0 || nextX >= width)
0419                 continue;
0420 
0421             if (cannyLineNext[nextX] > 0)
0422             {
0423                 // Trace neighbors.
0424                 trace(width, height, id, image, ids, nextX, nextY);
0425             }
0426         }
0427     }
0428 }