File indexing completed on 2024-05-12 15:23:44
0001 /* 0002 SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "starcorrespondence.h" 0008 0009 #include <math.h> 0010 #include "ekos_guide_debug.h" 0011 0012 // Finds the star in sortedStars that's closest to x,y and within maxDistance pixels. 0013 // Returns the index of the closest star in sortedStars, or -1 if none satisfies the criteria. 0014 // sortedStars must be sorted by x, from lower x to higher x. 0015 // Fills distance to the pixel distance to the closest star. 0016 int StarCorrespondence::findClosestStar(double x, double y, const QList<Edge> sortedStars, 0017 double maxDistance, double *distance) const 0018 { 0019 if (x < -maxDistance || y < -maxDistance || 0020 x > imageWidth + maxDistance || y > imageWidth + maxDistance) 0021 return -1; 0022 0023 const int size = sortedStars.size(); 0024 int startPoint = 0; 0025 0026 // Efficiency improvement to quickly find the start point. 0027 // Increments by size/10 to find a better start point. 0028 const int coarseIncrement = size / 10; 0029 if (coarseIncrement > 1) 0030 { 0031 for (int i = 0; i < size; i += coarseIncrement) 0032 { 0033 if (sortedStars[i].x < x - maxDistance) 0034 startPoint = i; 0035 else 0036 break; 0037 } 0038 } 0039 0040 // Iterate through the stars starting with the star with lowest x where x >= x - maxDistance 0041 // through the star with greatest x where x <= x + maxDistance. Find the closest star to x,y. 0042 int bestIndex = -1; 0043 double bestSquaredDistance = maxDistance * maxDistance; 0044 for (int i = startPoint; i < size; ++i) 0045 { 0046 auto &star = sortedStars[i]; 0047 if (star.x < x - maxDistance) continue; 0048 if (star.x > x + maxDistance) break; 0049 const double xDiff = star.x - x; 0050 const double yDiff = star.y - y; 0051 const double squaredDistance = xDiff * xDiff + yDiff * yDiff; 0052 if (squaredDistance <= bestSquaredDistance) 0053 { 0054 bestIndex = i; 0055 bestSquaredDistance = squaredDistance; 0056 } 0057 } 0058 if (distance != nullptr) *distance = sqrt(bestSquaredDistance); 0059 return bestIndex; 0060 } 0061 0062 namespace 0063 { 0064 // Sorts stars by their x values, places the sorted stars into sortedStars. 0065 // sortedToOriginal is a map whose index is the index of a star in sortedStars and whose 0066 // value is the index of a star in stars. 0067 // Therefore, sortedToOrigial[a] = b, means that sortedStars[a] corresponds to stars[b]. 0068 void sortByX(const QList<Edge> &stars, QList<Edge> *sortedStars, QVector<int> *sortedToOriginal) 0069 { 0070 sortedStars->clear(); 0071 sortedToOriginal->clear(); 0072 0073 // Sort the stars by their x-positions, lower to higher. 0074 QVector<std::pair<int, float>> rx; 0075 for (int i = 0; i < stars.size(); ++i) 0076 rx.push_back(std::pair<int, float>(i, stars[i].x)); 0077 std::sort(rx.begin(), rx.end(), [](const std::pair<int, float> &a, const std::pair<int, double> &b) 0078 { 0079 return a.second < b.second; 0080 }); 0081 for (int i = 0; i < stars.size(); ++i) 0082 { 0083 sortedStars->append(stars[ rx[i].first ]); 0084 sortedToOriginal->push_back(rx[i].first); 0085 } 0086 } 0087 0088 } // namespace 0089 0090 StarCorrespondence::StarCorrespondence(const QList<Edge> &stars, int guideStar) 0091 { 0092 initialize(stars, guideStar); 0093 } 0094 0095 StarCorrespondence::StarCorrespondence() 0096 { 0097 } 0098 0099 // Initialize with the reference stars and the guide-star index. 0100 void StarCorrespondence::initialize(const QList<Edge> &stars, int guideStar) 0101 { 0102 qCDebug(KSTARS_EKOS_GUIDE) << "StarCorrespondence initialized with " << stars.size() << guideStar; 0103 references = stars; 0104 guideStarIndex = guideStar; 0105 float guideX = references[guideStarIndex].x; 0106 float guideY = references[guideStarIndex].y; 0107 guideStarOffsets.clear(); 0108 referenceSums.clear(); 0109 referenceNumPixels.clear(); 0110 0111 // Compute the x and y offsets from the guide star to all reference stars 0112 // and store them in guideStarOffsets. 0113 const int numRefs = references.size(); 0114 for (int i = 0; i < numRefs; ++i) 0115 { 0116 const auto &ref = references[i]; 0117 guideStarOffsets.push_back(Offsets(ref.x - guideX, ref.y - guideY)); 0118 referenceSums.push_back(ref.sum); 0119 referenceNumPixels.push_back(ref.numPixels); 0120 } 0121 0122 initializeAdaptation(); 0123 0124 initialized = true; 0125 } 0126 0127 void StarCorrespondence::reset() 0128 { 0129 references.clear(); 0130 guideStarOffsets.clear(); 0131 referenceSums.clear(); 0132 referenceNumPixels.clear(); 0133 initialized = false; 0134 } 0135 0136 int StarCorrespondence::findInternal(const QList<Edge> &stars, double maxDistance, QVector<int> *starMap, 0137 int guideStarIndex, const QVector<Offsets> &offsets, 0138 int *numFound, int *numNotFound, double minFraction) const 0139 { 0140 // This is the cost of not finding one of the reference stars. 0141 constexpr double missingRefStarCost = 100; 0142 // This weight multiplies the distance between a reference star position and the closest star in stars. 0143 constexpr double distanceWeight = 1.0; 0144 0145 // Initialize all stars to not-corresponding to any reference star. 0146 *starMap = QVector<int>(stars.size(), -1); 0147 0148 // We won't accept a solution worse than bestCost. 0149 // In the default case, we need to find about half the reference stars. 0150 // Another possibility is a constraint on the input stars being mapped 0151 // E.g. that the more likely solution is one where the stars are close to the references. 0152 // This can be an issue if the number of input stars is way less than the number of reference stars 0153 // but in that case we can fail and go to the default star-finding algorithm. 0154 int bestCost = offsets.size() * missingRefStarCost * (1 - minFraction); 0155 // Note that the above implies that if stars.size() < offsets.size() * minFraction 0156 // then it is impossible to succeed. 0157 if (stars.size() < minFraction * offsets.size()) 0158 return -1; 0159 0160 // Assume the guide star corresponds to each of the stars. 0161 // Score the assignment, pick the best, and then assign the rest. 0162 const int numStars = stars.size(); 0163 int bestStarIndex = -1, bestNumFound = 0, bestNumNotFound = 0; 0164 for (int starIndex = 0; starIndex < numStars; ++starIndex) 0165 { 0166 const float starX = stars[starIndex].x; 0167 const float starY = stars[starIndex].y; 0168 0169 double cost = 0.0; 0170 QVector<int> mapping(stars.size(), -1); 0171 int numFound = 0, numNotFound = 0; 0172 for (int offsetIndex = 0; offsetIndex < offsets.size(); ++offsetIndex) 0173 { 0174 // Of course, the guide star has offsets 0 to itself. 0175 if (offsetIndex == guideStarIndex) continue; 0176 0177 // We're already worse than the best cost. No need to search any more. 0178 if (cost > bestCost) break; 0179 0180 // Look for an input star at the offset position. 0181 // Note the index value returned is the sorted index, which is converted back later. 0182 const auto &offset = offsets[offsetIndex]; 0183 double distance; 0184 const int closestIndex = findClosestStar(starX + offset.x, starY + offset.y, 0185 stars, maxDistance, &distance); 0186 if (closestIndex < 0) 0187 { 0188 // This reference star position had no corresponding input star. 0189 cost += missingRefStarCost; 0190 numNotFound++; 0191 continue; 0192 } 0193 numFound++; 0194 0195 // If starIndex is the star that corresponds to guideStarIndex, then 0196 // stars[index] corresponds to references[offsetIndex] 0197 mapping[closestIndex] = offsetIndex; 0198 cost += distance * distanceWeight; 0199 } 0200 if (cost < bestCost) 0201 { 0202 bestCost = cost; 0203 bestStarIndex = starIndex; 0204 bestNumFound = numFound; 0205 bestNumNotFound = numNotFound; 0206 0207 *starMap = QVector<int>(stars.size(), -1); 0208 for (int i = 0; i < mapping.size(); ++i) 0209 (*starMap)[i] = mapping[i]; 0210 (*starMap)[starIndex] = guideStarIndex; 0211 } 0212 } 0213 *numFound = bestNumFound; 0214 *numNotFound = bestNumNotFound; 0215 return bestStarIndex; 0216 } 0217 0218 // Converts offsets that are from the point-of-view of the guidestar, to offsets from targetStar. 0219 void StarCorrespondence::makeOffsets(const QVector<Offsets> &offsets, QVector<Offsets> *targetOffsets, 0220 int targetStar) const 0221 { 0222 targetOffsets->resize(offsets.size()); 0223 int xOffset = offsets[targetStar].x; 0224 int yOffset = offsets[targetStar].y; 0225 for (int i = 0; i < offsets.size(); ++i) 0226 { 0227 if (i == targetStar) 0228 { 0229 (*targetOffsets)[i] = Offsets(0, 0); 0230 } 0231 else 0232 { 0233 const double x = offsets[i].x - xOffset; 0234 const double y = offsets[i].y - yOffset; 0235 (*targetOffsets)[i] = Offsets(x, y); 0236 } 0237 } 0238 0239 } 0240 0241 // We create an imaginary star from the ones we did find. 0242 Edge StarCorrespondence::inventStarPosition(const QList<Edge> &stars, const QVector<int> &starMap, 0243 QVector<Offsets> offsets, Offsets offset) const 0244 { 0245 Edge inventedStar; 0246 inventedStar.invalidate(); 0247 0248 QVector<double> xPositions, yPositions; 0249 float refSum = 0, origSum = 0, refNumPixels = 0, origNumPixels = 0; 0250 for (int i = 0; i < starMap.size(); ++i) 0251 { 0252 int reference = starMap[i]; 0253 if (reference >= 0) 0254 { 0255 xPositions.push_back(stars[i].x - offsets[reference].x); 0256 yPositions.push_back(stars[i].y - offsets[reference].y); 0257 0258 // These are used to help invent the SNR. 0259 refSum += stars[i].sum; 0260 refNumPixels += stars[i].numPixels; 0261 origSum += referenceSums[reference]; 0262 origNumPixels += referenceNumPixels[reference]; 0263 } 0264 } 0265 float inventedSum = 0; 0266 float inventedNumPixels = 0; 0267 if (origSum > 0 && origNumPixels > 0) 0268 { 0269 // If possible, interpolate to estimate the invented star's flux and number of pixels. 0270 inventedSum = (refSum / origSum) * referenceSums[guideStarIndex]; 0271 inventedNumPixels = (refNumPixels / origNumPixels) * referenceNumPixels[guideStarIndex]; 0272 } 0273 0274 if (xPositions.size() == 0) 0275 return inventedStar; 0276 0277 // Compute the median x and y values. After gathering the values above, 0278 // we sort them and use the middle positions. 0279 std::sort(xPositions.begin(), xPositions.end(), [] (double d1, double d2) 0280 { 0281 return d1 < d2; 0282 }); 0283 std::sort(yPositions.begin(), yPositions.end(), [] (double d1, double d2) 0284 { 0285 return d1 < d2; 0286 }); 0287 const int num = xPositions.size(); 0288 double xVal = 0, yVal = 0; 0289 if (num % 2 == 0) 0290 { 0291 const int middle = num / 2; 0292 xVal = (xPositions[middle - 1] + xPositions[middle]) / 2.0; 0293 yVal = (yPositions[middle - 1] + yPositions[middle]) / 2.0; 0294 } 0295 else 0296 { 0297 const int middle = num / 2; // because it's 0-based 0298 xVal = xPositions[middle]; 0299 yVal = yPositions[middle]; 0300 } 0301 inventedStar.x = xVal - offset.x; 0302 inventedStar.y = yVal - offset.y; 0303 inventedStar.sum = inventedSum; 0304 inventedStar.numPixels = inventedNumPixels; 0305 return inventedStar; 0306 } 0307 0308 namespace 0309 { 0310 // This utility converts the starMap to refer to indexes in the original star QVector 0311 // instead of the sorted-by-x version. 0312 void unmapStarMap(const QVector<int> &sortedStarMap, const QVector<int> &sortedToOriginal, QVector<int> *starMap) 0313 { 0314 for (int i = 0; i < sortedStarMap.size(); ++i) 0315 (*starMap)[sortedToOriginal[i]] = sortedStarMap[i]; 0316 } 0317 } 0318 0319 Edge StarCorrespondence::find(const QList<Edge> &stars, double maxDistance, 0320 QVector<int> *starMap, bool adapt, double minFraction) 0321 { 0322 m_NumReferencesFound = 0; 0323 *starMap = QVector<int>(stars.size(), -1); 0324 Edge foundStar; 0325 foundStar.invalidate(); 0326 if (!initialized) return foundStar; 0327 int numFound, numNotFound; 0328 0329 // findClosestStar needs an input with stars sorted by their x. 0330 // Do this outside of the loops. 0331 QList<Edge> sortedStars; 0332 QVector<int> sortedToOriginal; 0333 sortByX(stars, &sortedStars, &sortedToOriginal); 0334 0335 QVector<int> sortedStarMap; 0336 int bestStarIndex = findInternal(sortedStars, maxDistance, &sortedStarMap, guideStarIndex, 0337 guideStarOffsets, &numFound, &numNotFound, minFraction); 0338 0339 if (bestStarIndex > -1) 0340 { 0341 // Convert back to the unsorted index value. 0342 bestStarIndex = sortedToOriginal[bestStarIndex]; 0343 unmapStarMap(sortedStarMap, sortedToOriginal, starMap); 0344 0345 foundStar = stars[bestStarIndex]; 0346 qCDebug(KSTARS_EKOS_GUIDE) 0347 << "StarCorrespondence found guideStar at " << bestStarIndex << "found/not" 0348 << numFound << numNotFound; 0349 m_NumReferencesFound = numFound; 0350 } 0351 else if (allowMissingGuideStar && bestStarIndex == -1 && 0352 stars.size() >= minFraction * guideStarOffsets.size()) 0353 { 0354 qCDebug(KSTARS_EKOS_GUIDE) 0355 << "Trying in invent. StarCorrespondence did NOT find guideStar. Found/not" 0356 << numFound << numNotFound << "minFraction" << minFraction << "maxDistance" << maxDistance; 0357 // If we didn't find a good solution, perhaps the guide star was not detected. 0358 // See if we can get a reasonable solution from the other stars. 0359 int bestNumFound = 0; 0360 int bestNumNotFound = 0; 0361 Edge bestInvented; 0362 bestInvented.invalidate(); 0363 for (int gStarIndex = 0; gStarIndex < guideStarOffsets.size(); gStarIndex++) 0364 { 0365 if (gStarIndex == guideStarIndex) 0366 continue; 0367 QVector<Offsets> gStarOffsets; 0368 makeOffsets(guideStarOffsets, &gStarOffsets, gStarIndex); 0369 QVector<int> newStarMap; 0370 int detectedStarIndex = findInternal(sortedStars, maxDistance, &newStarMap, 0371 gStarIndex, gStarOffsets, 0372 &numFound, &numNotFound, minFraction); 0373 if (detectedStarIndex >= 0 && numFound > bestNumFound) 0374 { 0375 Edge invented = inventStarPosition(sortedStars, newStarMap, gStarOffsets, 0376 guideStarOffsets[gStarIndex]); 0377 if (invented.x < 0 || invented.y < 0) 0378 continue; 0379 0380 bestInvented = invented; 0381 bestNumFound = numFound; 0382 bestNumNotFound = numNotFound; 0383 sortedStarMap = newStarMap; 0384 0385 if (numNotFound <= 1) 0386 // We can't do better than this. 0387 break; 0388 } 0389 } 0390 if (bestNumFound > 0) 0391 { 0392 // Convert back to the unsorted index value. 0393 unmapStarMap(sortedStarMap, sortedToOriginal, starMap); 0394 qCDebug(KSTARS_EKOS_GUIDE) 0395 << "StarCorrespondence found guideStar (invented) at " 0396 << bestInvented.x << bestInvented.y << "found/not" << bestNumFound << bestNumNotFound; 0397 m_NumReferencesFound = bestNumFound; 0398 return bestInvented; 0399 } 0400 else qCDebug(KSTARS_EKOS_GUIDE) << "StarCorrespondence could not invent guideStar."; 0401 } 0402 else qCDebug(KSTARS_EKOS_GUIDE) << "StarCorrespondence could not find guideStar."; 0403 0404 if (adapt && (bestStarIndex != -1)) 0405 adaptOffsets(stars, *starMap); 0406 return foundStar; 0407 } 0408 0409 // Compute the alpha cooeficient for a simple IIR filter used in adaptOffsets() 0410 // that causes the offsets to be, roughtly, the moving average of the last timeConstant samples. 0411 // See the discussion here: 0412 // https://dsp.stackexchange.com/questions/378/what-is-the-best-first-order-iir-ar-filter-approximation-to-a-moving-average-f 0413 void StarCorrespondence::initializeAdaptation() 0414 { 0415 // Approximately average the last 25 samples. 0416 const double timeConstant = 25.0; 0417 alpha = 1.0 / pow(timeConstant, 0.865); 0418 0419 // Don't let the adapted offsets move far from the original ones. 0420 originalGuideStarOffsets = guideStarOffsets; 0421 } 0422 0423 void StarCorrespondence::adaptOffsets(const QList<Edge> &stars, const QVector<int> &starMap) 0424 { 0425 const int numStars = stars.size(); 0426 if (starMap.size() != numStars) 0427 { 0428 qCDebug(KSTARS_EKOS_GUIDE) << "Adapt error: StarMap size != stars.size()" << starMap.size() << numStars; 0429 return; 0430 } 0431 // guideStar will be the index in stars corresponding to the reference guide star. 0432 int guideStar = -1; 0433 for (int i = 0; i < numStars; ++i) 0434 { 0435 if (starMap[i] == guideStarIndex) 0436 { 0437 guideStar = i; 0438 break; 0439 } 0440 } 0441 if (guideStar < 0) 0442 { 0443 qCDebug(KSTARS_EKOS_GUIDE) << "Adapt error: no guide star in map"; 0444 return; 0445 } 0446 0447 for (int i = 0; i < numStars; ++i) 0448 { 0449 // We don't adapt if the ith star doesn't correspond to any of the references, 0450 // or if it corresponds to the guide star (whose offsets are, of course, always 0). 0451 const int refIndex = starMap[i]; 0452 if (refIndex == -1 || refIndex == guideStarIndex) 0453 continue; 0454 0455 if ((refIndex >= references.size()) || (refIndex < 0)) 0456 { 0457 qCDebug(KSTARS_EKOS_GUIDE) << "Adapt error: bad refIndex[" << i << "] =" << refIndex; 0458 return; 0459 } 0460 // Adapt the x and y offsets using the following IIR filter: 0461 // output[t] = alpha * offset[t] + (1-alpha) * output[t-1] 0462 const double xOffset = stars[i].x - stars[guideStar].x; 0463 const double yOffset = stars[i].y - stars[guideStar].y; 0464 const double currentXOffset = guideStarOffsets[refIndex].x; 0465 const double currentYOffset = guideStarOffsets[refIndex].y; 0466 const double newXOffset = alpha * xOffset + (1 - alpha) * currentXOffset; 0467 const double newYOffset = alpha * yOffset + (1 - alpha) * currentYOffset; 0468 0469 // The adaptation is meant for small movements of at most a few pixels. 0470 // We don't want it to move enough to find a new star. 0471 constexpr double maxMovement = 2.5; // pixels 0472 if ((fabs(newXOffset - originalGuideStarOffsets[refIndex].x) < maxMovement) && 0473 (fabs(newYOffset - originalGuideStarOffsets[refIndex].y) < maxMovement)) 0474 { 0475 guideStarOffsets[refIndex].x = newXOffset; 0476 guideStarOffsets[refIndex].y = newYOffset; 0477 } 0478 } 0479 }