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 }