Warning, file /education/kstars/kstars/ekos/align/polaralign.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "polaralign.h" 0008 #include "poleaxis.h" 0009 #include "rotations.h" 0010 0011 #include <cmath> 0012 0013 #include "fitsviewer/fitsdata.h" 0014 #include "kstarsdata.h" 0015 #include "skypoint.h" 0016 #include <ekos_align_debug.h> 0017 0018 /****************************************************************** 0019 PolarAlign is a class that supports polar alignment by determining the 0020 mount's axis of rotation when given 3 solved images taken with RA mount 0021 rotations between the images. 0022 0023 addPoint(image) is called by the polar alignment UI after it takes and 0024 solves each of its three images. The solutions are store in SkyPoints (see below) 0025 and are processed so that the sky positions correspond to "what's in the sky 0026 now" and "at this geographic localtion". 0027 0028 Addpoint() samples the location of a particular pixel in its image. 0029 When the 3 points are sampled, they should not be taken 0030 from the center of the image, as HA rotations may not move that point 0031 if the telescope and mount are well aligned. Thus, the points are sampled 0032 from the edge of the image. 0033 0034 After all 3 images are sampled, findAxis() is called, which solves for the mount's 0035 axis of rotation. It then transforms poleAxis' result into azimuth and altitude 0036 offsets from the pole. 0037 0038 After the mount's current RA axis is determined, the user then attempts to correct/improve 0039 it to match the Earth's real polar axes. Ekos has two techniques to do that. In both 0040 Ekos takes a series of "refresh images". The user looks at the images and their 0041 associated analyses and adjusts the mount's altitude and azimuth knobs. 0042 0043 In the first scheme, the user identifies a refrence star on the image. Ekos draws a triangle 0044 over the image, and user attempts to "move the star" along two sides of that triangle. 0045 0046 In the 2nd scheme, the system plate-solves the refresh images, telling the user which direction 0047 and how much to adjust the knobs. 0048 0049 findCorrectedPixel() supports the "move the star" refresh scheme. 0050 It is given an x,y position on an image and the offsets 0051 generated by findAxis(). It computes a "corrected position" for that input 0052 x,y point such that if a user adjusted the GEM mount's altitude and azimuth 0053 knobs to move a star centered in the image's original x,y position to the corrected 0054 position in the image, the mount's axis of rotation should then coincide with the pole. 0055 0056 processRefreshCoords() supports the plate-solving refresh scheme. 0057 It is given the center coordinates of a refresh image. It remembers the originally 0058 calculated mount axis, and the position of the 3rd measurement image. It computes how 0059 much the user has already adjusted the azimuth and altitude knobs from the difference 0060 in pointing between the new refresh image's center coordinates and that of the 3rd measurement 0061 image. It infers what the mounts new RA axis must be (based on that adjustment) and returns 0062 the new polar alignment error. 0063 ******************************************************************/ 0064 0065 using Rotations::V3; 0066 0067 PolarAlign::PolarAlign(const GeoLocation *geo) 0068 { 0069 if (geo == nullptr && KStarsData::Instance() != nullptr) 0070 geoLocation = KStarsData::Instance()->geo(); 0071 else 0072 geoLocation = geo; 0073 } 0074 0075 bool PolarAlign::northernHemisphere() const 0076 { 0077 if ((geoLocation == nullptr) || (geoLocation->lat() == nullptr)) 0078 return true; 0079 return geoLocation->lat()->Degrees() > 0; 0080 } 0081 0082 void PolarAlign::reset() 0083 { 0084 points.clear(); 0085 times.clear(); 0086 } 0087 0088 // Gets the pixel's j2000 RA&DEC coordinates, converts to JNow, adjust to 0089 // the local time, and sets up the azimuth and altitude coordinates. 0090 bool PolarAlign::prepareAzAlt(const QSharedPointer<FITSData> &image, const QPointF &pixel, SkyPoint *point) const 0091 { 0092 // WCS must be set up for this image. 0093 SkyPoint coords; 0094 if (image && image->pixelToWCS(pixel, coords)) 0095 { 0096 coords.apparentCoord(static_cast<long double>(J2000), image->getDateTime().djd()); 0097 *point = SkyPoint::timeTransformed(&coords, image->getDateTime(), geoLocation, 0); 0098 return true; 0099 } 0100 return false; 0101 } 0102 0103 bool PolarAlign::addPoint(const QSharedPointer<FITSData> &image) 0104 { 0105 SkyPoint coords; 0106 auto time = image->getDateTime(); 0107 // Use the HA and DEC from the center of the image. 0108 if (!prepareAzAlt(image, QPointF(image->width() / 2, image->height() / 2), &coords)) 0109 return false; 0110 0111 QString debugString = QString("PAA: addPoint ra0 %1 dec0 %2 ra %3 dec %4 az %5 alt %6") 0112 .arg(coords.ra0().Degrees()).arg(coords.dec0().Degrees()) 0113 .arg(coords.ra().Degrees()).arg(coords.dec().Degrees()) 0114 .arg(coords.az().Degrees()).arg(coords.alt().Degrees()); 0115 qCInfo(KSTARS_EKOS_ALIGN) << debugString; 0116 if (points.size() > 2) 0117 return false; 0118 points.push_back(coords); 0119 times.push_back(time); 0120 0121 return true; 0122 } 0123 0124 namespace 0125 { 0126 0127 // Returns the distance, in absolute-value degrees, of taking point "from", 0128 // rotating it around the Y axis by yAngle, then rotating around the Z axis 0129 // by zAngle and comparing that with "goal". 0130 double getResidual(const V3 &from, double yAngle, double zAngle, const V3 &goal) 0131 { 0132 V3 point1 = Rotations::rotateAroundY(from, yAngle); 0133 V3 point2 = Rotations::rotateAroundZ(point1, zAngle); 0134 return fabs(getAngle(point2, goal)); 0135 } 0136 0137 // Finds the best rotations to change from pointing to 'from' to pointing to 'goal'. 0138 // It tries 'all' possible pairs of x and y rotations (sampled by increment). 0139 // Note that you can't simply find the best Z rotation, and the go from there to find the best Y. 0140 // The space is non-linear, and that would often lead to poor solutions. 0141 double getBestRotation(const V3 &from, const V3 &goal, 0142 double zStart, double yStart, 0143 double *bestAngleZ, double *bestAngleY, 0144 double range, double increment) 0145 { 0146 0147 *bestAngleZ = 0; 0148 *bestAngleY = 0; 0149 double minDist = 1e8; 0150 range = fabs(range); 0151 for (double thetaY = yStart - range; thetaY <= yStart + range; thetaY += increment) 0152 { 0153 for (double thetaZ = zStart - range; thetaZ <= zStart + range; thetaZ += increment) 0154 { 0155 double dist = getResidual(from, thetaY, thetaZ, goal); 0156 if (dist < minDist) 0157 { 0158 minDist = dist; 0159 *bestAngleY = thetaY; 0160 *bestAngleZ = thetaZ; 0161 } 0162 } 0163 } 0164 return minDist; 0165 } 0166 0167 // Computes the rotations in Y (altitude) and Z (azimuth) that brings 'from' closest to 'goal'. 0168 // Returns the residual (error angle between where these rotations lead and "goal". 0169 double getRotationAngles(const V3 &from, const V3 &goal, double *zAngle, double *yAngle) 0170 { 0171 // All in degrees. 0172 constexpr double pass1Resolution = 1.0 / 60.0; 0173 constexpr double pass2Resolution = 5 / 3600.0; 0174 constexpr double pass2Range = 4.0 / 60.0; 0175 0176 // Compute the rotation using a great circle. This somewhat constrains our search below. 0177 const double rotationAngle = getAngle(from, goal); // degrees 0178 const double pass1Range = std::max(1.0, std::min(5.0, fabs(rotationAngle))); 0179 0180 // Grid search across all y,z angle possibilities, sampling by 2 arc-minutes. 0181 const double pass1Residual = getBestRotation(from, goal, 0, 0, zAngle, yAngle, pass1Range, pass1Resolution); 0182 Q_UNUSED(pass1Residual); 0183 0184 // Refine the search around the best solution so far 0185 return getBestRotation(from, goal, *zAngle, *yAngle, zAngle, yAngle, pass2Range, pass2Resolution); 0186 } 0187 0188 } // namespace 0189 0190 // Compute the polar-alignment azimuth and altitude error by comparing the new image's coordinates 0191 // with the coordinates from the 3rd measurement image. Use the difference to infer a rotation angle, 0192 // and rotate the originally computed polar-alignment axis by that angle to find the new axis 0193 // around which RA now rotates. 0194 bool PolarAlign::processRefreshCoords(const SkyPoint &coords, const KStarsDateTime &time, 0195 double *azError, double *altError, 0196 double *azAdjustment, double *altAdjustment) const 0197 { 0198 // Get the az and alt from this new measurement (coords), and from that derive its x,y,z coordinates. 0199 auto c = coords; // apparentCoord modifies its input. Use the temp variable c to keep coords const. 0200 c.apparentCoord(static_cast<long double>(J2000), time.djd()); 0201 SkyPoint point = SkyPoint::timeTransformed(&c, time, geoLocation, 0); 0202 const double az = point.az().Degrees(), alt = point.alt().Degrees(); 0203 const V3 newPoint = Rotations::azAlt2xyz(QPointF(az, alt)); 0204 0205 // Get the x,y,z coordinates of the original position (from the 3rd polar-align image). 0206 // We can't simply use the az/alt already computed for point3 because the mount is tracking, 0207 // and thus, even if the user made no adjustments, but simply took an image a little while 0208 // later at the same RA/DEC coordinates, the Az/Alt would have changed and we'd believe there 0209 // was a user rotation due to changes in the Az/Alt knobs. Instead we must convert point3's az/alt 0210 // values to what they would be if that image had been taken now, still pointing at point3's ra/dec. 0211 // The key is to rotate the original point around the original RA axis by the rotation given by the time 0212 // difference multiplied by the sidereal rate. 0213 0214 // Figure out what the az/alt would be if the user hadn't modified the knobs. 0215 // That is, just rotate the 3rd measurement point around the mount's original RA axis. 0216 // Time since third point in seconds 0217 const double p3secs = times[2].secsTo(time); 0218 // Angle corresponding to that interval assuming the sidereal rate. 0219 const double p3Angle = (-15.041067 * p3secs) / 3600.0; // degrees 0220 0221 // Get the xyz coordinates of the original 3rd point. 0222 const V3 p3OrigPoint = Rotations::azAlt2xyz(QPointF(points[2].az().Degrees(), points[2].alt().Degrees())); 0223 // Get the unit vector corresponding the original RA axis 0224 const V3 origAxisPt = Rotations::azAlt2xyz(QPointF(azimuthCenter, altitudeCenter)); 0225 // Rotate the original 3rd point around that axis, simulating the mount's tracking movements. 0226 const V3 point3 = Rotations::rotateAroundAxis(p3OrigPoint, origAxisPt, p3Angle); 0227 0228 // Find the adjustment the user must have made by examining the change from point3 to newPoint 0229 // (i.e. the rotation caused by the user adjusting the azimuth and altitude knobs). 0230 // We assume that this was a rotation around a level mount's y axis and z axis. 0231 double zAdjustment, yAdjustment; 0232 double residual = getRotationAngles(point3, newPoint, &zAdjustment, &yAdjustment); 0233 if (residual > 0.5) 0234 { 0235 qCInfo(KSTARS_EKOS_ALIGN) << QString("PAA refresh: failed to estimate rotation angle (residual %1'").arg(residual * 60); 0236 return false; 0237 } 0238 qCInfo(KSTARS_EKOS_ALIGN) << QString("PAA refresh: Estimated current adjustment: Az %1' Alt %2' residual %3a-s") 0239 .arg(zAdjustment * 60, 0, 'f', 1).arg(yAdjustment * 60, 0, 'f', 1).arg(residual * 3600, 0, 'f', 0); 0240 0241 // Return the estimated adjustments (used by testing). 0242 if (altAdjustment != nullptr) *altAdjustment = yAdjustment; 0243 if (azAdjustment != nullptr) *azAdjustment = zAdjustment; 0244 0245 // Rotate the original RA axis position by the above adjustments. 0246 const V3 origAxisPoint = Rotations::azAlt2xyz(QPointF(azimuthCenter, altitudeCenter)); 0247 const V3 tempPoint = Rotations::rotateAroundY(origAxisPoint, yAdjustment); 0248 const V3 newAxisPoint = Rotations::rotateAroundZ(tempPoint, zAdjustment); 0249 0250 // Convert the rotated axis point back to an az/alt coordinate, representing the new RA axis. 0251 const QPointF newAxisAzAlt = Rotations::xyz2azAlt(newAxisPoint); 0252 const double newAxisAz = newAxisAzAlt.x(); 0253 const double newAxisAlt = newAxisAzAlt.y(); 0254 0255 // Compute the polar alignment error for the new RA axis. 0256 const double latitudeDegrees = geoLocation->lat()->Degrees(); 0257 *altError = northernHemisphere() ? newAxisAlt - latitudeDegrees : newAxisAlt + latitudeDegrees; 0258 *azError = northernHemisphere() ? newAxisAz : newAxisAz + 180.0; 0259 while (*azError > 180.0) 0260 *azError -= 360; 0261 0262 QString infoString = 0263 QString("PAA refresh: ra0 %1 dec0 %2 Az/Alt: %3 %4 AXIS: %5 %6 --> %7 %8 ERR: %9' alt %10'") 0264 .arg(coords.ra0().Degrees(), 0, 'f', 3).arg(coords.dec0().Degrees(), 0, 'f', 3) 0265 .arg(az, 0, 'f', 3).arg(alt, 0, 'f', 3) 0266 .arg(azimuthCenter, 0, 'f', 3).arg(altitudeCenter, 0, 'f', 3) 0267 .arg(newAxisAz, 0, 'f', 3).arg(newAxisAlt, 0, 'f', 3) 0268 .arg(*azError * 60, 0, 'f', 1).arg(*altError * 60, 0, 'f', 1); 0269 qCInfo(KSTARS_EKOS_ALIGN) << infoString; 0270 0271 return true; 0272 } 0273 0274 // Given the telescope's current RA axis, and the its current pointing position, 0275 // compute the coordinates where it should point such that its RA axis will be at the pole. 0276 bool PolarAlign::refreshSolution(SkyPoint *solution, SkyPoint *altOnlySolution) const 0277 { 0278 if (points.size() != 3) 0279 return false; 0280 0281 double azError, altError; 0282 calculateAzAltError(&azError, &altError); 0283 0284 // The Y rotation to correct polar alignment is -altitude error, and the Z correction is -azimuth error. 0285 // Rotate the 3rd-image center coordinate by the above angles. 0286 // This is the position the telescope needs to point to (if it is taken there 0287 // by adjusting alt and az knobs) such that the new RA rotation axis is aligned with the pole. 0288 const V3 point3 = Rotations::azAlt2xyz(QPointF(points[2].az().Degrees(), points[2].alt().Degrees())); 0289 const V3 altSolutionPoint = Rotations::rotateAroundY(point3, altError); 0290 const V3 solutionPoint = Rotations::rotateAroundZ(altSolutionPoint, azError); 0291 0292 // Convert the solution xyz points back to az/alt and ra/dec. 0293 const QPointF solutionAzAlt = Rotations::xyz2azAlt(solutionPoint); 0294 solution->setAz(solutionAzAlt.x()); 0295 solution->setAlt(solutionAzAlt.y()); 0296 auto lst = geoLocation->GSTtoLST(times[2].gst()); 0297 solution->HorizontalToEquatorial(&lst, geoLocation->lat()); 0298 0299 // Not sure if this is needed 0300 solution->setRA0(solution->ra()); 0301 solution->setDec0(solution->dec()); 0302 0303 // Move the solution back to J2000 0304 KSNumbers num(times[2].djd()); 0305 *solution = solution->deprecess(&num); 0306 solution->setRA0(solution->ra()); 0307 solution->setDec0(solution->dec()); 0308 0309 // Ditto for alt-only solution 0310 const QPointF altOnlySolutionAzAlt = Rotations::xyz2azAlt(altSolutionPoint); 0311 altOnlySolution->setAz(altOnlySolutionAzAlt.x()); 0312 altOnlySolution->setAlt(altOnlySolutionAzAlt.y()); 0313 auto altOnlyLst = geoLocation->GSTtoLST(times[2].gst()); 0314 altOnlySolution->HorizontalToEquatorial(&altOnlyLst, geoLocation->lat()); 0315 altOnlySolution->setRA0(altOnlySolution->ra()); 0316 altOnlySolution->setDec0(altOnlySolution->dec()); 0317 KSNumbers altOnlyNum(times[2].djd()); 0318 *altOnlySolution = altOnlySolution->deprecess(&altOnlyNum); 0319 altOnlySolution->setRA0(altOnlySolution->ra()); 0320 altOnlySolution->setDec0(altOnlySolution->dec()); 0321 0322 return true; 0323 } 0324 0325 bool PolarAlign::findAxis() 0326 { 0327 if (points.size() != 3) 0328 return false; 0329 0330 // We have 3 points, get their xyz positions. 0331 V3 p1(Rotations::azAlt2xyz(QPointF(points[0].az().Degrees(), points[0].alt().Degrees()))); 0332 V3 p2(Rotations::azAlt2xyz(QPointF(points[1].az().Degrees(), points[1].alt().Degrees()))); 0333 V3 p3(Rotations::azAlt2xyz(QPointF(points[2].az().Degrees(), points[2].alt().Degrees()))); 0334 V3 axis = Rotations::getAxis(p1, p2, p3); 0335 0336 if (axis.length() < 0.9) 0337 { 0338 // It failed to normalize the vector, something's wrong. 0339 qCInfo(KSTARS_EKOS_ALIGN) << "Normal vector too short. findAxis failed."; 0340 return false; 0341 } 0342 0343 // Need to make sure we're pointing to the right pole. 0344 if ((northernHemisphere() && (axis.x() < 0)) || (!northernHemisphere() && axis.x() > 0)) 0345 { 0346 axis = V3(-axis.x(), -axis.y(), -axis.z()); 0347 } 0348 0349 QPointF azAlt = Rotations::xyz2azAlt(axis); 0350 azimuthCenter = azAlt.x(); 0351 altitudeCenter = azAlt.y(); 0352 0353 return true; 0354 } 0355 0356 void PolarAlign::getAxis(double *azAxis, double *altAxis) const 0357 { 0358 *azAxis = azimuthCenter; 0359 *altAxis = altitudeCenter; 0360 } 0361 0362 // Find the pixel in image corresponding to the desired azimuth & altitude. 0363 bool PolarAlign::findAzAlt(const QSharedPointer<FITSData> &image, double azimuth, double altitude, QPointF *pixel) const 0364 { 0365 SkyPoint spt; 0366 spt.setAz(azimuth); 0367 spt.setAlt(altitude); 0368 dms LST = geoLocation->GSTtoLST(image->getDateTime().gst()); 0369 spt.HorizontalToEquatorial(&LST, geoLocation->lat()); 0370 SkyPoint j2000Coord = spt.catalogueCoord(image->getDateTime().djd()); 0371 QPointF imagePoint; 0372 if (!image->wcsToPixel(j2000Coord, *pixel, imagePoint)) 0373 { 0374 QString debugString = 0375 QString("PolarAlign: Couldn't get pixel from WCS for az %1 alt %2 with j2000 RA %3 DEC %4") 0376 .arg(QString::number(azimuth), QString::number(altitude), j2000Coord.ra0().toHMSString(), j2000Coord.dec0().toDMSString()); 0377 qCInfo(KSTARS_EKOS_ALIGN) << debugString; 0378 return false; 0379 } 0380 return true; 0381 } 0382 0383 // Calculate the mount's azimuth and altitude error given the known geographic location 0384 // and the azimuth center and altitude center computed in findAxis(). 0385 void PolarAlign::calculateAzAltError(double *azError, double *altError) const 0386 { 0387 const double latitudeDegrees = geoLocation->lat()->Degrees(); 0388 *altError = northernHemisphere() ? 0389 altitudeCenter - latitudeDegrees : altitudeCenter + latitudeDegrees; 0390 *azError = northernHemisphere() ? azimuthCenter : azimuthCenter + 180.0; 0391 while (*azError > 180.0) 0392 *azError -= 360; 0393 } 0394 0395 void PolarAlign::setMaxPixelSearchRange(double degrees) 0396 { 0397 // Suggestion for how far pixelError() below searches. 0398 // Don't allow the search to be modified too much. 0399 const double d = fabs(degrees); 0400 if (d < 2) 0401 maxPixelSearchRange = 2.0; 0402 else if (d > 10) 0403 maxPixelSearchRange = 10.0; 0404 else 0405 maxPixelSearchRange = d; 0406 } 0407 0408 // Given the currently estimated RA axis polar alignment error, and given a start pixel, 0409 // find the polar-alignment error if the user moves a star (from his point of view) 0410 // from that pixel to pixel2. 0411 // 0412 // FindCorrectedPixel() determines where the user should move the star to fully correct 0413 // the alignment error. However, while the user is doing that, he/she may be at an intermediate 0414 // point (pixel2) and we want to feed back to the user what the "current" polar-alignment error is. 0415 // This searches using findCorrectedPixel() to 0416 // find the RA axis error which would be fixed by the user moving pixel to pixel2. The input 0417 // thus should be pixel = "current star position", and pixel2 = "solution star position" 0418 // from the original call to findCorrectedPixel. This calls findCorrectedPixel several hundred times 0419 // but is not too costly (about .1s on a RPi4). One could write a method that more directly estimates 0420 // the error given the current position, but it might not be applicable to our use-case as 0421 // we are constrained to move along paths detemined by a user adjusting an altitude knob and then 0422 // an azimuth adjustment. These corrections are likely not the most direct path to solve the axis error. 0423 bool PolarAlign::pixelError(const QSharedPointer<FITSData> &image, const QPointF &pixel, const QPointF &pixel2, 0424 double *azError, double *altError) 0425 { 0426 double azOffset, altOffset; 0427 calculateAzAltError(&azOffset, &altOffset); 0428 0429 QPointF pix; 0430 double azE = 0, altE = 0; 0431 0432 pixelError(image, pixel, pixel2, 0433 -maxPixelSearchRange, maxPixelSearchRange, 0.2, 0434 -maxPixelSearchRange, maxPixelSearchRange, 0.2, &azE, &altE, &pix); 0435 pixelError(image, pixel, pixel2, azE - .2, azE + .2, 0.02, 0436 altE - .2, altE + .2, 0.02, &azE, &altE, &pix); 0437 pixelError(image, pixel, pixel2, azE - .02, azE + .02, 0.002, 0438 altE - .02, altE + .02, 0.002, &azE, &altE, &pix); 0439 0440 const double pixDist = hypot(pix.x() - pixel2.x(), pix.y() - pixel2.y()); 0441 if (pixDist > 10) 0442 return false; 0443 0444 *azError = azE; 0445 *altError = altE; 0446 return true; 0447 } 0448 0449 void PolarAlign::pixelError(const QSharedPointer<FITSData> &image, const QPointF &pixel, const QPointF &pixel2, 0450 double minAz, double maxAz, double azInc, 0451 double minAlt, double maxAlt, double altInc, 0452 double *azError, double *altError, QPointF *actualPixel) 0453 { 0454 double minDistSq = 1e9; 0455 for (double eAz = minAz; eAz < maxAz; eAz += azInc) 0456 { 0457 for (double eAlt = minAlt; eAlt < maxAlt; eAlt += altInc) 0458 { 0459 QPointF pix; 0460 if (findCorrectedPixel(image, pixel, &pix, eAz, eAlt)) 0461 { 0462 // compare the distance to the pixel 0463 double distSq = ((pix.x() - pixel2.x()) * (pix.x() - pixel2.x()) + 0464 (pix.y() - pixel2.y()) * (pix.y() - pixel2.y())); 0465 if (distSq < minDistSq) 0466 { 0467 minDistSq = distSq; 0468 *actualPixel = pix; 0469 *azError = eAz; 0470 *altError = eAlt; 0471 } 0472 } 0473 } 0474 } 0475 } 0476 0477 // Given a pixel, find its RA/DEC, then its alt/az, and then solve for another pixel 0478 // where, if the star in pixel is moved to that star in the user's image (by adjusting alt and az controls) 0479 // the polar alignment error would be 0. 0480 bool PolarAlign::findCorrectedPixel(const QSharedPointer<FITSData> &image, const QPointF &pixel, QPointF *corrected, 0481 bool altOnly) 0482 { 0483 double azOffset, altOffset; 0484 calculateAzAltError(&azOffset, &altOffset); 0485 if (altOnly) 0486 azOffset = 0.0; 0487 return findCorrectedPixel(image, pixel, corrected, azOffset, altOffset); 0488 } 0489 0490 // Given a pixel, find its RA/DEC, then its alt/az, and then solve for another pixel 0491 // where, if the star in pixel is moved to that star in the user's image (by adjusting alt and az controls) 0492 // the polar alignment error would be 0. We use the fact that we can only move by adjusting and altitude 0493 // knob, then an azimuth knob--i.e. we likely don't traverse a great circle. 0494 bool PolarAlign::findCorrectedPixel(const QSharedPointer<FITSData> &image, const QPointF &pixel, QPointF *corrected, 0495 double azOffset, 0496 double altOffset) 0497 { 0498 // 1. Find the az/alt for the x,y point on the image. 0499 SkyPoint p; 0500 if (!prepareAzAlt(image, pixel, &p)) 0501 return false; 0502 double pixelAz = p.az().Degrees(), pixelAlt = p.alt().Degrees(); 0503 0504 // 2. Apply the az/alt offsets. 0505 // We know that the pole's az and alt offsets are effectively rotations 0506 // of a sphere. The offsets that apply to correct different points depend 0507 // on where (in the sphere) those points are. Points close to the pole can probably 0508 // just add the pole's offsets. This calculation is a bit more precise, and is 0509 // necessary if the points are not near the pole. 0510 double altRotation = northernHemisphere() ? altOffset : -altOffset; 0511 QPointF rotated = Rotations::rotateRaAxis(QPointF(pixelAz, pixelAlt), QPointF(azOffset, altRotation)); 0512 0513 // 3. Find a pixel with those alt/az values. 0514 if (!findAzAlt(image, rotated.x(), rotated.y(), corrected)) 0515 return false; 0516 0517 return true; 0518 }