Kstars

fitsgradientdetector.cpp
1/*
2 SPDX-FileCopyrightText: 2004 Jasem Mutlaq
3 SPDX-FileCopyrightText: 2020 Eric Dejouhanet <eric.dejouhanet@gmail.com>
4 SPDX-FileCopyrightText: 2015 Gonzalo Exequiel Pedone <hipersayan.x@gmail.com>
5
6 SPDX-License-Identifier: GPL-2.0-or-later AND GPL-3.0-or-later
7
8 Some code fragments were adapted from Peter Kirchgessner's FITS plugin
9 SPDX-FileCopyrightText: Peter Kirchgessner <http://members.aol.com/pkirchg>
10*/
11
12#include <math.h>
13#include <cmath>
14#include <QtConcurrent>
15
16#include "fits_debug.h"
17#include "fitsgradientdetector.h"
18#include "fitsdata.h"
19
20QFuture<bool> FITSGradientDetector::findSources(const QRect &boundary)
21{
22 FITSImage::Statistic const &stats = m_ImageData->getStatistics();
23 switch (stats.dataType)
24 {
25#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
26 case TBYTE:
27 default:
28 return QtConcurrent::run(&FITSGradientDetector::findSources<uint8_t>, this, boundary);
29
30 case TSHORT:
31 return QtConcurrent::run(&FITSGradientDetector::findSources<int16_t>, this, boundary);
32
33 case TUSHORT:
34 return QtConcurrent::run(&FITSGradientDetector::findSources<uint16_t>, this, boundary);
35
36 case TLONG:
37 return QtConcurrent::run(&FITSGradientDetector::findSources<int32_t>, this, boundary);
38
39 case TULONG:
40 return QtConcurrent::run(&FITSGradientDetector::findSources<uint16_t>, this, boundary);
41
42 case TFLOAT:
43 return QtConcurrent::run(&FITSGradientDetector::findSources<float>, this, boundary);
44
45 case TLONGLONG:
46 return QtConcurrent::run(&FITSGradientDetector::findSources<int64_t>, this, boundary);
47
48 case TDOUBLE:
49 return QtConcurrent::run(&FITSGradientDetector::findSources<double>, this, boundary);
50#else
51 case TBYTE:
52 default:
53 return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint8_t>, boundary);
54
55 case TSHORT:
56 return QtConcurrent::run(this, &FITSGradientDetector::findSources<int16_t>, boundary);
57
58 case TUSHORT:
59 return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint16_t>, boundary);
60
61 case TLONG:
62 return QtConcurrent::run(this, &FITSGradientDetector::findSources<int32_t>, boundary);
63
64 case TULONG:
65 return QtConcurrent::run(this, &FITSGradientDetector::findSources<uint16_t>, boundary);
66
67 case TFLOAT:
68 return QtConcurrent::run(this, &FITSGradientDetector::findSources<float>, boundary);
69
70 case TLONGLONG:
71 return QtConcurrent::run(this, &FITSGradientDetector::findSources<int64_t>, boundary);
72
73 case TDOUBLE:
74 return QtConcurrent::run(this, &FITSGradientDetector::findSources<double>, boundary);
75#endif
76 }
77}
78
79template <typename T>
80bool FITSGradientDetector::findSources(const QRect &boundary)
81{
82 int subX = qMax(0, boundary.isNull() ? 0 : boundary.x());
83 int subY = qMax(0, boundary.isNull() ? 0 : boundary.y());
84 int subW = (boundary.isNull() ? m_ImageData->width() : boundary.width());
85 int subH = (boundary.isNull() ? m_ImageData->height() : boundary.height());
86
87 int BBP = m_ImageData->getBytesPerPixel();
88
89 uint16_t dataWidth = m_ImageData->width();
90
91 // #1 Find offsets
92 uint32_t size = subW * subH;
93 uint32_t offset = subX + subY * dataWidth;
94
95 // #2 Create new buffer
96 auto * buffer = new uint8_t[size * BBP];
97 // If there is no offset, copy whole buffer in one go
98 if (offset == 0)
99 memcpy(buffer, m_ImageData->getImageBuffer(), size * BBP);
100 else
101 {
102 uint8_t * dataPtr = buffer;
103 uint8_t const * origDataPtr = m_ImageData->getImageBuffer();
104 uint32_t lineOffset = 0;
105 // Copy data line by line
106 for (int height = subY; height < (subY + subH); height++)
107 {
108 lineOffset = (subX + height * dataWidth) * BBP;
109 memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP);
110 dataPtr += (subW * BBP);
111 }
112 }
113
114 // #3 Create new FITSData to hold it
115 auto * boundedImage = new FITSData();
116 FITSImage::Statistic stats;
117 stats.width = subW;
118 stats.height = subH;
119 stats.dataType = m_ImageData->getStatistics().dataType;
120 stats.bytesPerPixel = m_ImageData->getStatistics().bytesPerPixel;
121 stats.samples_per_channel = size;
122 stats.ndim = 2;
123 boundedImage->restoreStatistics(stats);
124
125 // #4 Set image buffer and calculate stats.
126 boundedImage->setImageBuffer(buffer);
127
128 boundedImage->calculateStats(true);
129
130 // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain
131 boundedImage->applyFilter(FITS_MEDIAN);
132 boundedImage->applyFilter(FITS_HIGH_CONTRAST);
133
134 // #6 Perform Sobel to find gradients and their directions
135 QVector<float> gradients;
136 QVector<float> directions;
137
138 // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed
139 // and discarded whenever necessary. It won't work on noisy images unless this is done.
140 sobel<T>(boundedImage, gradients, directions);
141
142 QVector<int> ids(gradients.size());
143
144 int maxID = partition(subW, subH, gradients, ids);
145
146 // Not needed anymore
147 delete boundedImage;
148
149 if (maxID == 0)
150 return 0;
151
152 typedef struct massInfo
153 {
154 float massX = 0;
155 float massY = 0;
156 float totalMass = 0;
157 } massInfo;
158
159 QMap<int, massInfo> masses;
160
161 // #7 Calculate center of mass for all detected regions
162 for (int y = 0; y < subH; y++)
163 {
164 for (int x = 0; x < subW; x++)
165 {
166 int index = x + y * subW;
167
168 int regionID = ids[index];
169 if (regionID > 0)
170 {
171 float pixel = gradients[index];
172
173 masses[regionID].totalMass += pixel;
174 masses[regionID].massX += x * pixel;
175 masses[regionID].massY += y * pixel;
176 }
177 }
178 }
179
180 // Compare multiple masses, and only select the highest total mass one as the desired star
181 int maxRegionID = 1;
182 int maxTotalMass = masses[1].totalMass;
183 double totalMassRatio = 1e6;
184 for (auto key : masses.keys())
185 {
186 massInfo oneMass = masses.value(key);
187 if (oneMass.totalMass > maxTotalMass)
188 {
189 totalMassRatio = oneMass.totalMass / maxTotalMass;
190 maxTotalMass = oneMass.totalMass;
191 maxRegionID = key;
192 }
193 }
194
195 // If image has many regions and there is no significant relative center of mass then it's just noise and no stars
196 // are probably there above a useful threshold.
197 if (maxID > 10 && totalMassRatio < 1.5)
198 return false;
199
200 auto * center = new Edge;
201 center->width = -1;
202 center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5;
203 center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5;
204 center->HFR = 1;
205
206 // Maximum Radius
207 int maxR = qMin(subW - 1, subH - 1) / 2;
208
209 for (int r = maxR; r > 1; r--)
210 {
211 int pass = 0;
212
213 for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0)
214 {
215 int testX = center->x + std::cos(theta) * r;
216 int testY = center->y + std::sin(theta) * r;
217
218 // if out of bound, break;
219 if (testX < 0 || testX >= subW || testY < 0 || testY >= subH)
220 break;
221
222 if (gradients[testX + testY * subW] > 0)
223 //if (thresholded[testX + testY * subW] > 0)
224 {
225 if (++pass >= 24)
226 {
227 center->width = r * 2;
228 // Break of outer loop
229 r = 0;
230 break;
231 }
232 }
233 }
234 }
235
236 qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width;
237
238 // If no stars were detected
239 if (center->width == -1)
240 {
241 delete center;
242 return false;
243 }
244
245 // 30% fuzzy
246 //center->width += center->width*0.3 * (running_threshold / threshold);
247
248 double FSum = 0, HF = 0, TF = 0;
249 const double resolution = 1.0 / 20.0;
250
251 int cen_y = qRound(center->y);
252
253 double rightEdge = center->x + center->width / 2.0;
254 double leftEdge = center->x - center->width / 2.0;
255
256 QVector<double> subPixels;
257 subPixels.reserve(center->width / resolution);
258
259 const T * origBuffer = reinterpret_cast<T const *>(m_ImageData->getImageBuffer()) + offset;
260
261 for (double x = leftEdge; x <= rightEdge; x += resolution)
262 {
263 double slice = resolution * (origBuffer[static_cast<int>(floor(x)) + cen_y * dataWidth]);
264 FSum += slice;
265 subPixels.append(slice);
266 }
267
268 // Half flux
269 HF = FSum / 2.0;
270
271 int subPixelCenter = (center->width / resolution) / 2;
272
273 // Start from center
274 TF = subPixels[subPixelCenter];
275 double lastTF = TF;
276 // Integrate flux along radius axis until we reach half flux
277 //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution)
278 for (int k = 1; k < subPixelCenter; k++)
279 {
280 TF += subPixels[subPixelCenter + k];
281 TF += subPixels[subPixelCenter - k];
282
283 if (TF >= HF)
284 {
285 // We overpassed HF, let's calculate from last TF how much until we reach HF
286
287 // #1 Accurate calculation, but very sensitive to small variations of flux
288 center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution;
289
290 // #2 Less accurate calculation, but stable against small variations of flux
291 //center->HFR = (k - 1) * resolution;
292 break;
293 }
294
295 lastTF = TF;
296 }
297
298 // Correct center for subX and subY
299 center->x += subX;
300 center->y += subY;
301
302 QList<Edge*> starCenters;
303 starCenters.append(center);
304 m_ImageData->setStarCenters(starCenters);
305
306 qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR;
307
308 return true;
309}
310
311/* CannyDetector, Implementation of Canny edge detector in Qt/C++.
312 * Web-Site: https://github.com/hipersayanX/CannyDetector
313 */
314
315template <typename T>
316void FITSGradientDetector::sobel(FITSData const *data, QVector<float> &gradient, QVector<float> &direction) const
317{
318 if (data == nullptr)
319 return;
320
321 FITSImage::Statistic const &stats = data->getStatistics();
322
323 //int size = image.width() * image.height();
324 gradient.resize(stats.samples_per_channel);
325 direction.resize(stats.samples_per_channel);
326
327 for (int y = 0; y < stats.height; y++)
328 {
329 size_t yOffset = y * stats.width;
330 const T * grayLine = reinterpret_cast<T const *>(data->getImageBuffer()) + yOffset;
331
332 const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width;
333 const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width;
334
335 float * gradientLine = gradient.data() + yOffset;
336 float * directionLine = direction.data() + yOffset;
337
338 for (int x = 0; x < stats.width; x++)
339 {
340 int x_m1 = x < 1 ? x : x - 1;
341 int x_p1 = x >= stats.width - 1 ? x : x + 1;
342
343 int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] -
344 2 * grayLine[x_m1] - grayLine_p1[x_m1];
345
346 int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] -
347 2 * grayLine_p1[x] - grayLine_p1[x_p1];
348
349 gradientLine[x] = qAbs(gradX) + qAbs(gradY);
350
351 /* Gradient directions are classified in 4 possible cases
352 *
353 * dir 0
354 *
355 * x x x
356 * - - -
357 * x x x
358 *
359 * dir 1
360 *
361 * x x /
362 * x / x
363 * / x x
364 *
365 * dir 2
366 *
367 * \ x x
368 * x \ x
369 * x x \
370 *
371 * dir 3
372 *
373 * x | x
374 * x | x
375 * x | x
376 */
377 if (gradX == 0 && gradY == 0)
378 directionLine[x] = 0;
379 else if (gradX == 0)
380 directionLine[x] = 3;
381 else
382 {
383 qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI;
384
385 if (a >= -22.5 && a < 22.5)
386 directionLine[x] = 0;
387 else if (a >= 22.5 && a < 67.5)
388 directionLine[x] = 2;
389 else if (a >= -67.5 && a < -22.5)
390 directionLine[x] = 1;
391 else
392 directionLine[x] = 3;
393 }
394 }
395 }
396}
397
398int FITSGradientDetector::partition(int width, int height, QVector<float> &gradient, QVector<int> &ids) const
399{
400 int id = 0;
401
402 for (int y = 1; y < height - 1; y++)
403 {
404 for (int x = 1; x < width - 1; x++)
405 {
406 int index = x + y * width;
407 float val = gradient[index];
408 if (val > 0 && ids[index] == 0)
409 {
410 trace(width, height, ++id, gradient, ids, x, y);
411 }
412 }
413 }
414
415 // Return max id
416 return id;
417}
418
419void FITSGradientDetector::trace(int width, int height, int id, QVector<float> &image, QVector<int> &ids, int x,
420 int y) const
421{
422 int yOffset = y * width;
423 float * cannyLine = image.data() + yOffset;
424 int * idLine = ids.data() + yOffset;
425
426 if (idLine[x] != 0)
427 return;
428
429 idLine[x] = id;
430
431 for (int j = -1; j < 2; j++)
432 {
433 int nextY = y + j;
434
435 if (nextY < 0 || nextY >= height)
436 continue;
437
438 float * cannyLineNext = cannyLine + j * width;
439
440 for (int i = -1; i < 2; i++)
441 {
442 int nextX = x + i;
443
444 if (i == j || nextX < 0 || nextX >= width)
445 continue;
446
447 if (cannyLineNext[nextX] > 0)
448 {
449 // Trace neighbors.
450 trace(width, height, id, image, ids, nextX, nextY);
451 }
452 }
453 }
454}
void append(QList< T > &&value)
pointer data()
void reserve(qsizetype size)
void resize(qsizetype size)
qsizetype size() const const
QList< Key > keys() const const
T value(const Key &key, const T &defaultValue) const const
int height() const const
bool isNull() const const
int width() const const
int x() const const
int y() const const
QTextStream & center(QTextStream &stream)
QFuture< T > run(Function function,...)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:15:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.