private bool HasObjectsWithAutoAperutreCloserThan8PixelsApart() { for (int i = 0; i < m_StateMachine.MeasuringStars.Count; i++) { for (int j = i + 1; j < m_StateMachine.MeasuringStars.Count; j++) { TrackedObjectConfig obj1 = m_StateMachine.MeasuringStars[i]; TrackedObjectConfig obj2 = m_StateMachine.MeasuringStars[j]; if (obj1.IsFixedAperture || obj2.IsFixedAperture) { continue; } double dist = ImagePixel.ComputeDistance( obj1.ApertureStartingX, obj2.ApertureStartingX, obj1.ApertureStartingY, obj2.ApertureStartingY); double minDist = 8 + (obj1.ApertureInPixels + obj2.ApertureInPixels) / 2; if (dist <= minDist && !obj1.ProcessInPsfGroup && !obj2.ProcessInPsfGroup) { return(true); } } } return(false); }
private bool TryAutoLocateDoubleObject(out ImagePixel newCenter) { var pixelsFlatList = new List <Tuple <int, int, uint> >(); for (int x = 0; x < 35; x++) { for (int y = 0; y < 35; y++) { uint pixel = m_ProcessingPixels[x, y]; pixelsFlatList.Add(new Tuple <int, int, uint>(x, y, pixel)); } } // Sort by brghtness (brigher at the top) pixelsFlatList.Sort((x, y) => y.Item3.CompareTo(x.Item3)); Tuple <int, int, uint> brightProbe1 = pixelsFlatList[0]; Tuple <int, int, uint> brightProbe2 = pixelsFlatList[1]; Tuple <int, int, uint> secondPeakProbe1 = null; Tuple <int, int, uint> secondPeakProbe2 = null; for (int i = 0; i < pixelsFlatList.Count; i++) { if (secondPeakProbe1 == null && ImagePixel.ComputeDistance(brightProbe1.Item1, pixelsFlatList[i].Item1, brightProbe1.Item2, pixelsFlatList[i].Item2) > 3) { secondPeakProbe1 = pixelsFlatList[i]; } if (secondPeakProbe2 == null && ImagePixel.ComputeDistance(brightProbe2.Item1, pixelsFlatList[i].Item1, brightProbe2.Item2, pixelsFlatList[i].Item2) > 3) { secondPeakProbe2 = pixelsFlatList[i]; } if (secondPeakProbe1 != null && secondPeakProbe2 != null) { break; } } if (secondPeakProbe1 != null && IsGoodDoubleObjectFit(brightProbe1, secondPeakProbe1)) { newCenter = new ImagePixel((int)Math.Max(brightProbe1.Item3, secondPeakProbe1.Item3), (brightProbe1.Item1 + secondPeakProbe1.Item1) / 2.0, (brightProbe1.Item2 + secondPeakProbe1.Item2) / 2.0); return(true); } if (secondPeakProbe2 != null && IsGoodDoubleObjectFit(brightProbe2, secondPeakProbe2)) { newCenter = new ImagePixel((int)Math.Max(brightProbe2.Item3, secondPeakProbe2.Item3), (brightProbe2.Item1 + secondPeakProbe2.Item1) / 2.0, (brightProbe2.Item2 + secondPeakProbe2.Item2) / 2.0); return(true); } newCenter = null; return(false); }
private void EnsureComputedRefinedData() { if (!float.IsNaN(m_MinLocateSignal)) { m_LocateObjects.Clear(); for (int i = 0; i < TrackedObjects.Count; i++) { if ( (!(TrackedObjects[i] as TrackedObject).IsOccultedStar || !LightCurveReductionContext.Instance.FullDisappearance) && !TrackedObjects[i].IsOffScreen && !float.IsNaN((TrackedObjects[i] as TrackedObject).LastFrameX) ) { // Don't include targets offscreen, or targets that were not found during the last tracking (could have been off screen) m_LocateObjects.Add((TrackedObjects[i] as TrackedObject).TargetNo); } } return; } m_MinLocateSignal = TrackedObjects.Cast <TrackedObject>().Min(o => o.RefinedOrLastSignalLevel == 0 ? 255f : o.RefinedOrLastSignalLevel); // Locate all peak pixels with signal higher than (minSignal + medianNoise) / 2 m_MinLocateSignal = (m_MedianValue + m_MinLocateSignal) / 2f; double minDistance = double.MaxValue; for (int i = 0; i < TrackedObjects.Count; i++) { if ((!(TrackedObjects[i] as TrackedObject).IsOccultedStar && !TrackedObjects[i].IsOffScreen) || !LightCurveReductionContext.Instance.FullDisappearance) { m_LocateObjects.Add((TrackedObjects[i] as TrackedObject).TargetNo); } for (int j = 0; j < TrackedObjects.Count; j++) { if (i == j) { continue; } double dist = ImagePixel.ComputeDistance( TrackedObjects[i].OriginalObject.ApertureStartingX, TrackedObjects[j].OriginalObject.ApertureStartingX, TrackedObjects[i].OriginalObject.ApertureStartingY, TrackedObjects[j].OriginalObject.ApertureStartingY); if (minDistance > dist) { minDistance = dist; } } } m_MinLocateDistance = minDistance / 2.0; }
public bool IsCloseEnoughToBelongToTheGroup(TrackedObjectConfig objectToCheck) { foreach (TrackedObjectConfig existingObject in m_ObjectsInGroup) { double distance = ImagePixel.ComputeDistance(existingObject.ApertureStartingX, objectToCheck.ApertureStartingX, existingObject.ApertureStartingY, objectToCheck.ApertureStartingY); if (distance < m_FWHM * TangraConfig.Settings.Special.MinDistanceForPhotometricGroupingInFWHM) { return(true); } } return(false); }
private bool IncludePixel(int x, int y) { if (m_Star1Center != null && ImagePixel.ComputeDistance(m_Star1Center.XDouble, x, m_Star1Center.YDouble, y) <= m_Star1MinDistance) { return(false); } if (m_Star2Center != null && ImagePixel.ComputeDistance(m_Star2Center.XDouble, x, m_Star2Center.YDouble, y) <= m_Star2MinDistance) { return(false); } return(true); }
private bool IsGoodDoubleObjectFit(Tuple <int, int, uint> object1, Tuple <int, int, uint> object2) { var doubleFit = new DoublePSFFit(100, 100); if (TangraConfig.Settings.Photometry.PsfFittingMethod == TangraConfig.PsfFittingMethod.LinearFitOfAveragedModel) { doubleFit.FittingMethod = PSFFittingMethod.LinearFitOfAveragedModel; } doubleFit.Fit(m_ProcessingPixels, object1.Item1, object1.Item2, object2.Item1, object2.Item2); if (doubleFit.IsSolved && Math.Abs(doubleFit.FWHM1 - doubleFit.FWHM2) < Math.Max(1, Math.Min(doubleFit.FWHM1, doubleFit.FWHM2) * 0.25) && doubleFit.IAmplitude1 > 0 && doubleFit.IAmplitude2 > 0) { double imaxRatio = doubleFit.IAmplitude1 / doubleFit.IAmplitude2; if (imaxRatio > 1) { imaxRatio = 1 / imaxRatio; } if (imaxRatio >= 0.25) { // We require at least 1:4 ratio in the maximums for the auto-detection to accept that it has found correctly two objects double distance = ImagePixel.ComputeDistance(object1.Item1, object2.Item1, object1.Item2, object2.Item2); if (distance > doubleFit.FWHM1 && distance > doubleFit.FWHM2) { // We also require at least a FWHM distance between centers for automatic detection m_X1Start = object1.Item1; m_Y1Start = object1.Item2; m_X2Start = object2.Item1; m_Y2Start = object2.Item2; return(true); } } return(false); } return(false); }
internal static PSFFit GetPSFFitForPeakPixel( uint[,] data, PotentialStarStruct starToTest, float aboveNoiseLevelRequired, double minFWHM, double maxFWHM) { int STAR_MATRIX_FIT = TangraConfig.Settings.Special.StarFinderFitArea; double MIN_DISTANCE_OF_PEAK_PIXEL_FROM_CENTER = TangraConfig.Settings.Special.StarFinderMinDistanceOfPeakPixelFromCenter; PSFFit fit = new PSFFit(starToTest.X, starToTest.Y); int fitMatrix = (int)Math.Min(data.GetLength(0), STAR_MATRIX_FIT + 2); // Get a matrix with 1 pixel larger each way and set the border pixels to zero fit.Fit(data, fitMatrix, starToTest.X, starToTest.Y, true); if (fit.IsSolved) { double distanceFromCenter = ImagePixel.ComputeDistance(fit.X0_Matrix, fitMatrix / 2, fit.Y0_Matrix, fitMatrix / 2); if (fit.Certainty > 0 && fit.FWHM >= minFWHM && fit.FWHM <= maxFWHM && distanceFromCenter < MIN_DISTANCE_OF_PEAK_PIXEL_FROM_CENTER && fit.IMax > aboveNoiseLevelRequired) { //not good for lost tracking allow higher FWHM // This object passes all tests to be furhter considered as a star return(fit); } } return(null); }
internal bool IsBrightEnoughtForGuidingStar() { double signal; double sn; byte oneSigmaBg = 0; int from = 0; int to = m_ProcessingPixels.GetLength(0); double aboveMedianLevelRequired; if (LightCurveReductionContext.Instance.DigitalFilter != TangraConfig.PreProcessingFilter.NoFilter) { from++; to--; } switch (LightCurveReductionContext.Instance.DigitalFilter) { case TangraConfig.PreProcessingFilter.LowPassFilter: aboveMedianLevelRequired = TangraConfig.Settings.Special.AboveMedianThreasholdForGuiding * TangraConfig.Settings.Special.AboveMedianCoeffLP; break; case TangraConfig.PreProcessingFilter.LowPassDifferenceFilter: aboveMedianLevelRequired = TangraConfig.Settings.Special.AboveMedianThreasholdForGuiding * TangraConfig.Settings.Special.AboveMedianCoeffLPD; break; default: aboveMedianLevelRequired = TangraConfig.Settings.Special.AboveMedianThreasholdForGuiding; break; } List <uint> bgBytes = new List <uint>(); for (int x = from; x < to; x++) { for (int y = from; y < to; y++) { double distanceFromFit = ImagePixel.ComputeDistance(m_Gaussian.XCenter, x, m_Gaussian.YCenter, y); if (distanceFromFit > m_Gaussian.FWHM) { bgBytes.Add(m_ProcessingPixels[x, y]); } } } bgBytes.Sort(); uint median = 0; if (bgBytes.Count > 0) { median = bgBytes.Count % 2 == 1 ? bgBytes[bgBytes.Count / 2] : ((bgBytes[bgBytes.Count / 2] + bgBytes[(bgBytes.Count / 2) - 1]) / 2); } double var = 0; bgBytes.ForEach(b => var += (median - b) * (median - b)); oneSigmaBg = (byte)Math.Round(Math.Sqrt(var / Math.Max(1, bgBytes.Count - 1))); signal = m_Gaussian.IMax - median; sn = signal / oneSigmaBg; Trace.WriteLine(string.Format("Signal = {0}, S/N = {1}", signal.ToString("0.00"), sn.ToString("0.00"))); if (signal >= aboveMedianLevelRequired && sn >= TangraConfig.Settings.Special.SignalNoiseForGuiding) { return(true); } Trace.WriteLine(string.Format("Not good for a guiding star: Required Signal = {0}, Required S/N = {1}", aboveMedianLevelRequired.ToString("0.00"), TangraConfig.Settings.Special.SignalNoiseForGuiding.ToString("0.00"))); return(false); }
public static List <PSFFit> GetStarsInArea( ref uint[,] data, int bpp, uint maxSignalValue, TangraConfig.PreProcessingFilter filter, List <PotentialStarStruct> allPotentialStars, List <PSFFit> allFoundStars, uint aboveNoiseLevelRequired, double minDistanceInPixels, bool useLPDFilter, Rectangle excludeArea, FilterPotentialStars filterCallback) { double minFWHM = TangraConfig.Settings.Special.StarFinderMinFWHM; double maxFWHM = TangraConfig.Settings.Special.StarFinderMaxFWHM; int STAR_MATRIX_FIT = TangraConfig.Settings.Special.StarFinderFitArea; Stopwatch sw = new Stopwatch(); sw.Start(); uint[,] lpdData; List <PotentialStarStruct> potentialStars = GetPeakPixelsInArea( data, out lpdData, bpp, maxSignalValue, aboveNoiseLevelRequired, minDistanceInPixels, useLPDFilter, excludeArea); if (filterCallback != null) { filterCallback(potentialStars); } sw.Stop(); Trace.WriteLine(string.Format("GetPeakPixelsInArea: {0} sec", sw.Elapsed.TotalSeconds.ToString("0.00"))); if (potentialStars.Count > 3) { // Only include the 3 brightest stars. The other ones cannot be stars potentialStars.Sort((x, y) => y.Z.CompareTo(x.Z)); potentialStars = potentialStars.Take(3).ToList(); } // Debugging purposes if (allPotentialStars != null) { allPotentialStars.AddRange(potentialStars); } uint[,] lpData = data; List <PSFFit> foundStars = new List <PSFFit>(); double MIN_DISTANCE_OF_PEAK_PIXEL_FROM_CENTER = TangraConfig.Settings.Special.StarFinderMinDistanceOfPeakPixelFromCenter; sw.Reset(); sw.Start(); foreach (PotentialStarStruct starToTest in potentialStars) { PSFFit fit = new PSFFit(starToTest.X, starToTest.Y); int fitMatrix = (int)Math.Min(data.GetLength(0), STAR_MATRIX_FIT + 2); // Get a matrix with 1 pixel larger each way and set the border pixels to zero fit.Fit(lpData, fitMatrix, starToTest.X, starToTest.Y, true); if (fit.IsSolved) { double distanceFromCenter = ImagePixel.ComputeDistance(fit.X0_Matrix, fitMatrix / 2, fit.Y0_Matrix, fitMatrix / 2); if (fit.Certainty > 0 && fit.FWHM >= minFWHM && fit.FWHM <= maxFWHM && distanceFromCenter < MIN_DISTANCE_OF_PEAK_PIXEL_FROM_CENTER && fit.IMax > aboveNoiseLevelRequired) { // This object passes all tests to be furhter considered as a star foundStars.Add(fit); } } if (allFoundStars != null) { allFoundStars.Add(fit); } } foundStars.Sort((f1, f2) => f1.IMax.CompareTo(f2.IMax)); PSFFit[] testStars = foundStars.ToArray(); for (int i = 0; i < testStars.Length; i++) { PSFFit fainterStar = testStars[i]; for (int j = i + 1; j < testStars.Length; j++) { PSFFit brighterStar = testStars[j]; if (fainterStar.UniqueId == brighterStar.UniqueId) { continue; } // If a the max of a fainter star is inside the fit of a brighter star // then see if it is simply not a point of the other star double dist = Math.Sqrt((fainterStar.XCenter - brighterStar.XCenter) * (fainterStar.XCenter - brighterStar.XCenter) + (fainterStar.YCenter - brighterStar.YCenter) * (fainterStar.YCenter - brighterStar.YCenter)); if (dist <= minDistanceInPixels) { if (foundStars.Contains(fainterStar)) { foundStars.Remove(fainterStar); } } } } sw.Stop(); Trace.WriteLine(string.Format("Doing PSFFitting: {0} sec", sw.Elapsed.TotalSeconds.ToString("0.00"))); switch (filter) { case TangraConfig.PreProcessingFilter.NoFilter: break; case TangraConfig.PreProcessingFilter.LowPassFilter: data = lpData; break; case TangraConfig.PreProcessingFilter.LowPassDifferenceFilter: data = lpdData; break; } return(foundStars); }
public void NextFrame(int frameNo, IAstroImage astroImage, IStarMap starMap, LeastSquareFittedAstrometry astrometricFit) { IsTrackedSuccessfully = false; int searchRadius = (int)Math.Ceiling(Math.Max(m_LastMovementPixels, CoreAstrometrySettings.Default.PreMeasureSearchCentroidRadius)); PSFFit psfFit = null; int startingX = (int)TrackedObject.LastKnownX; int startingY = (int)TrackedObject.LastKnownY; if (m_RepeatedIntergationPositions * 4 < m_PastFrameNos.Count) { var expectedPos = GetExpectedPosition(frameNo); if (expectedPos != null) { startingX = expectedPos.X; startingY = expectedPos.Y; } } var nearbyFeatures = starMap.GetFeaturesInRadius(startingX, startingY, searchRadius).ToArray(); var nonStarNearbyFeature = new List <StarMapFeature>(); foreach (var feature in nearbyFeatures) { var center = feature.GetCenter(); var referenceStarFeatures = astrometricFit.FitInfo.AllStarPairs.Where(x => x.FitInfo.UsedInSolution).ToList(); var refStar = referenceStarFeatures.FirstOrDefault(s => Math.Sqrt((s.x - center.X) * (s.x - center.X) + (s.y - center.Y) * (s.y - center.Y)) < 2); double raf, def; astrometricFit.GetRADEFromImageCoords(center.XDouble, center.YDouble, out raf, out def); var pastKnownStar = m_LastFrameStars.FirstOrDefault(s => AngleUtility.Elongation(s.RADeg, s.DEDeg, raf, def) * 3600.0 < 2); if (refStar == null && pastKnownStar == null) { nonStarNearbyFeature.Add(feature); } } if (nonStarNearbyFeature.Count > 0) { StarMapFeature closestFeature = nonStarNearbyFeature[0]; var lastKnownCenter = new ImagePixel(TrackedObject.LastKnownX, TrackedObject.LastKnownY); var smallestDistance = lastKnownCenter.DistanceTo(closestFeature.GetCenter()); for (int i = 1; i < nonStarNearbyFeature.Count; i++) { var distance = lastKnownCenter.DistanceTo(nonStarNearbyFeature[i].GetCenter()); if (distance < smallestDistance) { smallestDistance = distance; closestFeature = nonStarNearbyFeature[i]; } } if (closestFeature != null) { var center = closestFeature.GetCenter(); AstrometryContext.Current.StarMap.GetPSFFit(center.X, center.Y, PSFFittingMethod.NonLinearFit, out psfFit); } } if (psfFit == null) { // The expected location cannot be matched with any brighter feature so it is likely a faint object // with no brighter objects around. Lets find the brightest (faint) object in the are and use it ImagePixel centroid = AstrometryContext.Current.StarMap.GetCentroid(startingX, startingY, searchRadius); if (centroid != null) { AstrometryContext.Current.StarMap.GetPSFFit(centroid.X, centroid.Y, PSFFittingMethod.NonLinearFit, out psfFit); } } if (psfFit != null) { double ra, de; astrometricFit.GetRADEFromImageCoords(psfFit.XCenter, psfFit.YCenter, out ra, out de); double maxPosDiffArcSec = astrometricFit.GetDistanceInArcSec(astrometricFit.Image.CenterXImage, astrometricFit.Image.CenterYImage, astrometricFit.Image.CenterXImage + Math.Max(m_LastMovementPixels, CoreAstrometrySettings.Default.MaxAllowedDefaultMotionInPixels), astrometricFit.Image.CenterYImage); if (!double.IsNaN(TrackedObject.RAHours)) { double posDif = 3600 * AngleUtility.Elongation(15 * TrackedObject.RAHours, TrackedObject.DEDeg, ra, de); if (posDif > maxPosDiffArcSec) { // NOTE: Not a valid measurement Trace.WriteLine(string.Format("The target position is too far from the last measured position", posDif)); return; } } TrackedObject.RAHours = ra / 15.0; TrackedObject.DEDeg = de; if (TrackedObject.PSFFit != null) { m_LastMovementPixels = 1.2 * ImagePixel.ComputeDistance(TrackedObject.LastKnownX, psfFit.XCenter, TrackedObject.LastKnownY, psfFit.YCenter); } TrackedObject.LastKnownX = psfFit.XCenter; TrackedObject.LastKnownY = psfFit.YCenter; TrackedObject.PSFFit = psfFit; var lastKnownCenter = new ImagePixel(TrackedObject.LastKnownX, TrackedObject.LastKnownY); var thisFrameStars = astrometricFit.FitInfo.AllStarPairs.Where(x => lastKnownCenter.DistanceTo(x.x, x.y) > 2 * psfFit.FWHM).ToList(); if (thisFrameStars.Count > 0) { m_LastFrameStars = thisFrameStars.Select(x => new Star(x.StarNo, x.RADeg, x.DEDeg, x.Mag) as IStar).ToList(); } IsTrackedSuccessfully = true; } if (psfFit != null && psfFit.XCenter > 0 && psfFit.YCenter > 0) { m_PastFramePosX.Add(psfFit.XCenter); m_PastFramePosY.Add(psfFit.YCenter); m_PastFrameNos.Add(frameNo); } }
public override void NextFrame(int frameNo, IAstroImage astroImage) { IsTrackedSuccessfully = false; // For each of the non manualy positioned Tracked objects do a PSF fit in the area of its previous location for (int i = 0; i < m_TrackedObjectGroups.Count; i++) { TrackedObjectGroup objectGroup = m_TrackedObjectGroups[i]; objectGroup.NextFrame(); if (objectGroup.TrackLater) { // Group position will be determined after the rest of the stars are found } else { if (objectGroup.IsSingleObject) { TrackedObjectLight trackedObject = (TrackedObjectLight)objectGroup.SingleObject; uint[,] pixels = astroImage.GetPixelsArea(objectGroup.SingleObjectLastCenter.X, objectGroup.SingleObjectLastCenter.Y, 17); var fit = new PSFFit(objectGroup.SingleObjectLastCenter.X, objectGroup.SingleObjectLastCenter.Y); fit.FittingMethod = PSFFittingMethod.NonLinearFit; fit.Fit(pixels); if (fit.IsSolved) { if (fit.Certainty < GUIDING_STAR_MIN_CERTAINTY) { trackedObject.SetIsTracked(false, NotMeasuredReasons.ObjectCertaintyTooSmall); } else if (fit.FWHM < STELLAR_OBJECT_MIN_FWHM || fit.FWHM > STELLAR_OBJECT_MAX_FWHM) { trackedObject.SetIsTracked(false, NotMeasuredReasons.FWHMOutOfRange); } else if (TangraConfig.Settings.Tracking.CheckElongation && fit.ElongationPercentage > STELLAR_OBJECT_MAX_ELONGATION) { trackedObject.SetIsTracked(false, NotMeasuredReasons.ObjectTooElongated); } else { trackedObject.SetTrackedObjectMatch(fit); trackedObject.SetIsTracked(true, NotMeasuredReasons.TrackedSuccessfully); } } } else { string dbg = ""; int areaCenterX = (objectGroup.LastCenterObject1.X + objectGroup.LastCenterObject2.X) / 2; int areaCenterY = (objectGroup.LastCenterObject1.Y + objectGroup.LastCenterObject2.Y) / 2; uint[,] pixels = astroImage.GetPixelsArea(areaCenterX, areaCenterY, 35); var doubleFit = new DoublePSFFit(areaCenterX, areaCenterY); int x1 = objectGroup.LastCenterObject1.X - areaCenterX + 17; int y1 = objectGroup.LastCenterObject1.Y - areaCenterY + 17; int x2 = objectGroup.LastCenterObject2.X - areaCenterX + 17; int y2 = objectGroup.LastCenterObject2.Y - areaCenterY + 17; if (TangraConfig.Settings.Photometry.PsfFittingMethod == TangraConfig.PsfFittingMethod.LinearFitOfAveragedModel) { doubleFit.FittingMethod = PSFFittingMethod.LinearFitOfAveragedModel; } doubleFit.Fit(pixels, x1, y1, x2, y2); if (doubleFit.IsSolved) { PSFFit fit1 = doubleFit.GetGaussian1(); PSFFit fit2 = doubleFit.GetGaussian2(); TrackedObjectLight trackedObject1; TrackedObjectLight trackedObject2; bool groupIdentified = objectGroup.IdentifyObjects(fit1, fit2, GUIDING_STAR_MIN_CERTAINTY, out trackedObject1, out trackedObject2); if (!groupIdentified) { dbg += "Ni-"; objectGroup.SetIsTracked(false, NotMeasuredReasons.PSFFittingFailed); //Bitmap bmp = new Bitmap(100, 200); //using (Graphics g = Graphics.FromImage(bmp)) //{ // doubleFit.DrawInternalPoints(g, new Rectangle(0, 0, 100, 200), 5, 5, Brushes.Lime, Brushes.Yellow, 8); // g.Save(); //} //bmp.Save(@"D:\Hristo\mutual_double_fit.bmp"); m_FailedGroupFits++; } else { PSFFit[] fits = new PSFFit[] { fit1, fit2 }; TrackedObjectLight[] trackedObjects = new TrackedObjectLight[] { trackedObject1, trackedObject2 }; int tooSmallCertainties = 0; int errors = 0; for (int j = 0; j < 2; j++) { PSFFit fit = fits[j]; TrackedObjectLight trackedObject = trackedObjects[j]; if (fit.Certainty < GUIDING_STAR_MIN_CERTAINTY) { tooSmallCertainties++; trackedObject.SetIsTracked(true, NotMeasuredReasons.TrackedSuccessfully); dbg += "TsGs-"; } else if (fit.FWHM < STELLAR_OBJECT_MIN_FWHM || fit.FWHM > STELLAR_OBJECT_MAX_FWHM) { trackedObject.SetIsTracked(false, NotMeasuredReasons.FWHMOutOfRange); dbg += "Fw-"; errors++; } else if (TangraConfig.Settings.Tracking.CheckElongation && fit.ElongationPercentage > STELLAR_OBJECT_MAX_ELONGATION) { trackedObject.SetIsTracked(false, NotMeasuredReasons.ObjectTooElongated); dbg += "Elo-"; errors++; } else { trackedObject.SetIsTracked(true, NotMeasuredReasons.TrackedSuccessfully); dbg += "Ts-"; } } if (tooSmallCertainties == 2) { trackedObjects[0].SetIsTracked(false, NotMeasuredReasons.ObjectCertaintyTooSmall); trackedObjects[1].SetIsTracked(false, NotMeasuredReasons.ObjectCertaintyTooSmall); errors++; m_FailedGroupFits++; dbg += "Uncer-"; } if (errors == 0) { if (objectGroup.CheckIdentifiedObjects(fits[0].XCenter, fits[1].XCenter, fits[0].YCenter, fits[1].YCenter)) { trackedObjects[0].SetTrackedObjectMatch(fits[0]); trackedObjects[1].SetTrackedObjectMatch(fits[1]); m_FailedGroupFits = 0; dbg += "Id-"; double dist = ImagePixel.ComputeDistance(fits[0].XCenter, fits[1].XCenter, fits[0].YCenter, fits[1].YCenter); if (dist < 2) { Trace.WriteLine("TOO CLOSE"); } if (dist > 16) { Trace.WriteLine("TOO FAR"); } } else { dbg += "NoId-"; } } } } else { dbg += "NoSlv-"; } objectGroup.ValidatePosition(); Trace.WriteLine(dbg); } } } bool atLeastOneGroupLocated = false; for (int i = 0; i < m_TrackedObjectGroups.Count; i++) { TrackedObjectGroup trackedGroup = m_TrackedObjectGroups[i]; if (!trackedGroup.IsSingleObject && trackedGroup.LastCenterObject1 != null && trackedGroup.LastCenterObject2 != null) { Trace.WriteLine(string.Format("({0}, {1}, {2}) ({3},{4},{5}) [{6},{7}]", trackedGroup.LastCenterObject1.XDouble, trackedGroup.LastCenterObject1.YDouble, trackedGroup.LastCenterObject1.Brightness, trackedGroup.LastCenterObject2.XDouble, trackedGroup.LastCenterObject2.YDouble, trackedGroup.LastCenterObject2.Brightness, trackedGroup.LastCenterObject1.XDouble - trackedGroup.LastCenterObject2.XDouble, trackedGroup.LastCenterObject1.YDouble - trackedGroup.LastCenterObject2.YDouble)); } bool containsFullyDisappearingTarget = trackedGroup.ContainsOcultedStar && m_IsFullDisappearance; if (!containsFullyDisappearingTarget && trackedGroup.IsLocated) { atLeastOneGroupLocated = true; } if (!containsFullyDisappearingTarget) { continue; } int numReferences = 0; double x_double; double y_double; if (trackedGroup.IsSingleObject && m_IsFullDisappearance) { // This is the case for single fully disappearing targets double totalX = 0; double totalY = 0; numReferences = 0; for (int j = 0; j < m_TrackedObjectGroups.Count; j++) { TrackedObjectGroup referenceGroup = (TrackedObjectGroup)m_TrackedObjectGroups[j]; if (referenceGroup.IsLocated) { totalX += (trackedGroup.BrigherOriginalObject.ApertureStartingX - referenceGroup.BrigherOriginalObject.ApertureStartingX) + referenceGroup.BrigherObjectLastCenter.XDouble; totalY += (trackedGroup.BrigherOriginalObject.ApertureStartingY - referenceGroup.BrigherOriginalObject.ApertureStartingY) + referenceGroup.BrigherObjectLastCenter.YDouble; numReferences++; atLeastOneGroupLocated = true; } } x_double = totalX / numReferences; y_double = totalY / numReferences; } else { // The fully disappearing target is in a group. The other target would have been located. We use the last known position of the other targets numReferences = 1; x_double = trackedGroup.NonOccultedObjectLastCenter.XDouble; y_double = trackedGroup.NonOccultedObjectLastCenter.YDouble; } if (numReferences == 0) { trackedGroup.SetIsTracked(false, NotMeasuredReasons.FitSuspectAsNoGuidingStarsAreLocated); } else { int x = (int)(Math.Round(x_double)); int y = (int)(Math.Round(y_double)); uint[,] pixels = astroImage.GetPixelsArea(x, y, 35); if (trackedGroup.IsSingleObject) { PSFFit fit = new PSFFit(x, y); fit.Fit(pixels); if (fit.IsSolved && fit.Certainty > STELLAR_OBJECT_MIN_CERTAINTY) { trackedGroup.SingleObject.SetIsTracked(true, NotMeasuredReasons.TrackedSuccessfully); trackedGroup.SingleObject.SetTrackedObjectMatch(fit); } else if (m_IsFullDisappearance) { trackedGroup.SingleObject.SetIsTracked(false, NotMeasuredReasons.FullyDisappearingStarMarkedTrackedWithoutBeingFound); } else { trackedGroup.SingleObject.SetIsTracked(false, NotMeasuredReasons.ObjectCertaintyTooSmall); } } else { string dbg = ""; DoublePSFFit doubleFit = new DoublePSFFit(x, y); int x1 = trackedGroup.LastCenterObject1.X - x + 17; int y1 = trackedGroup.LastCenterObject1.Y - y + 17; int x2 = trackedGroup.LastCenterObject2.X - x + 17; int y2 = trackedGroup.LastCenterObject2.Y - y + 17; if (TangraConfig.Settings.Photometry.PsfFittingMethod == TangraConfig.PsfFittingMethod.LinearFitOfAveragedModel) { doubleFit.FittingMethod = PSFFittingMethod.LinearFitOfAveragedModel; } doubleFit.Fit(pixels, x1, y1, x2, y2); if (doubleFit.IsSolved) { PSFFit fit1 = doubleFit.GetGaussian1(); PSFFit fit2 = doubleFit.GetGaussian2(); IImagePixel center1 = null; IImagePixel center2 = null; TrackedObjectLight trackedObject1; TrackedObjectLight trackedObject2; bool resortToBrightness = false; bool groupIdentified = trackedGroup.IdentifyObjects(fit1, fit2, GUIDING_STAR_MIN_CERTAINTY, out trackedObject1, out trackedObject2); if (!groupIdentified && m_IsFullDisappearance) { dbg += "ReBr::"; groupIdentified = trackedGroup.IdentifyBrightObject(fit1, fit2, STELLAR_OBJECT_MIN_CERTAINTY, out trackedObject1, out trackedObject2, out center1, out center2); resortToBrightness = true; } if (!groupIdentified) { trackedGroup.SetIsTracked(false, NotMeasuredReasons.PSFFittingFailed); dbg += "PsF::"; } else { PSFFit[] fits = new PSFFit[] { fit1, fit2 }; IImagePixel[] centers = new IImagePixel[] { center1, center2 }; TrackedObjectLight[] trackedObjects = new TrackedObjectLight[] { trackedObject1, trackedObject2 }; bool objectCheckSuccessful = resortToBrightness ? trackedGroup.CheckIdentifiedObjects(center1.XDouble, center2.XDouble, center1.YDouble, center2.YDouble) : trackedGroup.CheckIdentifiedObjects(fits[0].XCenter, fits[1].XCenter, fits[0].YCenter, fits[1].YCenter); if (objectCheckSuccessful) { dbg += "ChS::"; bool atLeastOneOK = (fit1.IsSolved && fit1.Certainty > GUIDING_STAR_MIN_CERTAINTY) || (fit2.IsSolved && fit2.Certainty > GUIDING_STAR_MIN_CERTAINTY); double dist = ImagePixel.ComputeDistance(fit1.XCenter, fit2.XCenter, fit1.YCenter, fit2.YCenter); if (!atLeastOneOK || ((dist < 2 || dist > 16) && !resortToBrightness)) { trackedGroup.SetIsTracked(false, NotMeasuredReasons.PSFFittingFailed); } int cntOk = 0; for (int j = 0; j < 2; j++) { PSFFit fit = fits[j]; IImagePixel center = centers[j]; TrackedObjectLight trackedObject = trackedObjects[j]; if (fit.IsSolved && fit.Certainty > STELLAR_OBJECT_MIN_CERTAINTY) { if (resortToBrightness && trackedObject.IsOccultedStar) { trackedObject.SetIsTracked(true, NotMeasuredReasons.FullyDisappearingStarMarkedTrackedWithoutBeingFound, center, fit.Certainty); dbg += "OccDi::"; } else { trackedObject.SetTrackedObjectMatch(fit); trackedObject.SetIsTracked(true, NotMeasuredReasons.TrackedSuccessfully); dbg += "TrSuc::"; } cntOk++; } else if (m_IsFullDisappearance && trackedObject.IsOccultedStar) { trackedObject.SetIsTracked(false, NotMeasuredReasons.FullyDisappearingStarMarkedTrackedWithoutBeingFound, resortToBrightness ? center : null, 1); dbg += "BadCerSuc::"; } else { trackedObject.SetIsTracked(false, NotMeasuredReasons.ObjectCertaintyTooSmall, resortToBrightness ? center : null, resortToBrightness ? fit.Certainty : (double?)null); dbg += "BadCerNOSuc::"; } } if (cntOk == 2) { m_FailedGroupFits = 0; } } else { trackedObjects[0].SetIsTracked(false, NotMeasuredReasons.FailedToLocateAfterDistanceCheck); trackedObjects[1].SetIsTracked(false, NotMeasuredReasons.FailedToLocateAfterDistanceCheck); m_FailedGroupFits++; dbg += "ChU::"; } } } else { m_FailedGroupFits++; dbg += "NoFi::"; } trackedGroup.ValidatePosition(); Trace.WriteLine(dbg); } } } IsTrackedSuccessfully = atLeastOneGroupLocated; if (IsTrackedSuccessfully) { RefinedAverageFWHM = m_TrackedObjects.Cast <TrackedObjectLight>().Average(x => x.RefinedFWHM); } if (m_FailedGroupFits > 10) { Trace.WriteLine("ERR~10+"); } }
internal bool IdentifyObjects(PSFFit fit1, PSFFit fit2, float minGuidingStarCertainty, out TrackedObjectLight obj1, out TrackedObjectLight obj2) { // Make sure the two are not too far away and are also not too close double centDiff = ImagePixel.ComputeDistance(fit1.XCenter, fit2.XCenter, fit1.YCenter, fit2.YCenter); double oldCentDiff = ImagePixel.ComputeDistance(LastCenterObject1.XDouble, LastCenterObject2.XDouble, LastCenterObject1.YDouble, LastCenterObject2.YDouble); double diffDistRatio = centDiff / oldCentDiff; if (diffDistRatio < 0.5 || diffDistRatio > 2 || centDiff < 1 /* Two PSFs in the group are too close (possibly the same) */) { return(NoMatch(out obj1, out obj2)); } bool fit1Brighter = fit1.Brightness > fit2.Brightness; bool fit1Certain = fit1.Certainty > minGuidingStarCertainty; bool fit2Certain = fit2.Certainty > minGuidingStarCertainty; bool brighterFitCertain = fit1Brighter ? fit1Certain : fit2Certain; if (!brighterFitCertain) { return(NoMatch(out obj1, out obj2)); } double prevX1X2 = LastCenterObject1.XDouble - LastCenterObject2.XDouble; double prevY1Y2 = LastCenterObject1.YDouble - LastCenterObject2.YDouble; double thisX1X2 = fit1.XCenter - fit2.XCenter; double thisY1Y2 = fit1.YCenter - fit2.YCenter; double delta1122 = Math.Abs(prevX1X2 - thisX1X2) + Math.Abs(prevY1Y2 - thisY1Y2); double delta1212 = Math.Abs(prevX1X2 + thisX1X2) + Math.Abs(prevY1Y2 + thisY1Y2); double averageFWHM = (fit1.FWHM + fit2.FWHM) / 2; double maxGoodMatchDist = 1 + averageFWHM / 4; double minGoodDismatchDist = 4 + averageFWHM / 4; if (delta1122 < maxGoodMatchDist && delta1212 > minGoodDismatchDist) { return(Match1122(out obj1, out obj2)); } else if (delta1212 < maxGoodMatchDist && delta1122 > minGoodDismatchDist) { return(Match1212(out obj1, out obj2)); } bool match1122Ok = (Math.Abs(prevX1X2 - thisX1X2) < Math.Abs(prevX1X2 + thisX1X2)) && (Math.Abs(prevY1Y2 - thisY1Y2) < Math.Abs(prevY1Y2 + thisY1Y2)); bool match1212Ok = (Math.Abs(prevX1X2 + thisX1X2) < Math.Abs(prevX1X2 - thisX1X2)) && (Math.Abs(prevY1Y2 + thisY1Y2) < Math.Abs(prevY1Y2 - thisY1Y2)); if ((!match1122Ok && !match1212Ok) || (match1122Ok && match1212Ok)) { return(NoMatch(out obj1, out obj2)); } double d11 = ImagePixel.ComputeDistance(fit1.XCenter, LastCenterObject1.XDouble, fit1.YCenter, LastCenterObject1.YDouble); double d22 = ImagePixel.ComputeDistance(fit2.XCenter, LastCenterObject2.XDouble, fit2.YCenter, LastCenterObject2.YDouble); double d12 = ImagePixel.ComputeDistance(fit1.XCenter, LastCenterObject2.XDouble, fit1.YCenter, LastCenterObject2.YDouble); double d21 = ImagePixel.ComputeDistance(fit2.XCenter, LastCenterObject1.XDouble, fit2.YCenter, LastCenterObject1.YDouble); if (d11 < d12 && d22 < d21 && match1122Ok) { return(Match1122(out obj1, out obj2)); } else if (d11 > d12 && d22 > d21 && match1212Ok) { return(Match1212(out obj1, out obj2)); } double bDiff = Math.Abs(fit1.Brightness - fit2.Brightness); double bRatio = bDiff / Math.Max((double)fit1.Brightness, (double)fit2.Brightness); if (bRatio > 0.5) { // More than 2 times brightness difference. We can use the brightness to determine identify double b11 = Math.Abs(fit1.Brightness - LastCenterObject1.Brightness); double b22 = Math.Abs(fit2.Brightness - LastCenterObject2.Brightness); double b12 = Math.Abs(fit1.Brightness - LastCenterObject2.Brightness); double b21 = Math.Abs(fit2.Brightness - LastCenterObject1.Brightness); if (((fit1Brighter && b12 > b11 && fit1Certain) || (!fit1Brighter && b21 > b22 && fit2Certain)) && match1122Ok) { return(Match1122(out obj1, out obj2)); } else if (((fit1Brighter && b12 < b11 && fit1Certain) || (!fit1Brighter && b21 < b22 && fit2Certain)) == match1212Ok) { return(Match1212(out obj1, out obj2)); } } return(NoMatch(out obj1, out obj2)); }
private void CalculateAndDisplayBackground(uint[,] backgroundPixels) { if (backgroundPixels != null) { int bgWidth = backgroundPixels.GetLength(0); int bgHeight = backgroundPixels.GetLength(1); var bgPixels = new List <uint>(); for (int x = 0; x < bgWidth; x++) { for (int y = 0; y < bgHeight; y++) { if (m_PSFFit == null || !m_PSFFit.IsSolved || ImagePixel.ComputeDistance(m_PSFFit.XCenter, x + bgWidth - m_PSFFit.MatrixSize, m_PSFFit.YCenter, y + bgHeight - m_PSFFit.MatrixSize) > 3 * m_PSFFit.FWHM) { bgPixels.Add(backgroundPixels[x, y]); } } } bgPixels.Sort(); double background = 0; if (bgPixels.Count > 1) { background = m_Bpp < 12 ? bgPixels[bgPixels.Count / 2] // for 8 bit videos Median background works better : bgPixels.Average(x => x); // for 12+bit videos average background works better } double residualsSquareSum = 0; foreach (uint bgPixel in bgPixels) { residualsSquareSum += (background - bgPixel) * (background - bgPixel); } double noise = Math.Sqrt(residualsSquareSum / (bgPixels.Count - 1)); lblBackground.Text = background.ToString("0.0"); lblNoise.Text = noise.ToString("0.0"); if (m_PSFFit != null) { double snr = m_PSFFit.GetSNR(); lblSNR.Text = snr.ToString("0.0"); } else { lblSNR.Text = "N/A"; } if (m_PSFFit != null) { lblFitVariance.Text = m_PSFFit.GetVariance().ToString("0.0"); lblFWHM.Text = m_PSFFit.FWHM.ToString("0.0"); } else { lblFitVariance.Text = "N/A"; lblFWHM.Text = "N/A"; } } else { lblBackground.Text = "N/A"; lblNoise.Text = "N/A"; lblSNR.Text = "N/A"; } }
public override void NextFrame(int frameNo, IAstroImage astroImage) { base.NextFrame(frameNo, astroImage); #region run the full star recognition recovery if enabled if (TangraConfig.Settings.Tracking.RecoverFromLostTracking && m_RefiningFramesLeft <= 0) { bool notAllStarsLocated = TrackedObjects.Exists(o => !o.IsLocated); bool notAllLocateFirstStarsLocated = LocateFirstObjects.Exists(o => !o.IsLocated); if (notAllLocateFirstStarsLocated && LocateFirstObjects.Count > 1) { LocateStarsWithStarRecognition(astroImage); } // TODO: Use the notAllStarsLocated to troubleshoot the pattern recognition alignment } #endregion bool allGuidingStarsLocated = true; foreach (TrackedObject trackedObject in LocateFirstObjects) { if (!trackedObject.IsLocated && !trackedObject.IsOffScreen) { allGuidingStarsLocated = false; } } if (m_RefiningFramesLeft > 0) { if (allGuidingStarsLocated) { m_RefiningFramesLeft--; } } else { m_IsTrackedSuccessfully = LocateFirstObjects.Count > 0 ? allGuidingStarsLocated : OccultedStar.IsLocated; } if (!m_Refining && (LocateFirstObjects.Count > 0 && allGuidingStarsLocated) && (LightCurveReductionContext.Instance.FieldRotation || LightCurveReductionContext.Instance.LightCurveReductionType == LightCurveReductionType.MutualEvent)) { for (int i = 0; i < TrackedObjects.Count; i++) { TrackedObject obj1 = TrackedObjects[i] as TrackedObject; for (int j = 0; j < TrackedObjects.Count; j++) { if (i == j) { continue; } TrackedObject obj2 = TrackedObjects[j] as TrackedObject; long pairId = (((long)obj1.TargetNo) << 32) + (long)obj2.TargetNo; double oldDistance = m_RefinedDistances[pairId]; double newDistance = ImagePixel.ComputeDistance( obj1.Center.XDouble, obj2.Center.XDouble, obj1.Center.YDouble, obj2.Center.YDouble); m_RefinedDistances[pairId] = (oldDistance + newDistance) / 2.0; LocationVector vector = obj1.OtherGuidingStarsLocationVectors[obj2.TargetNo]; vector.DeltaXToAdd = (vector.DeltaXToAdd + obj2.Center.XDouble - obj1.Center.XDouble) / 2; vector.DeltaYToAdd = (vector.DeltaYToAdd + obj2.Center.YDouble - obj1.Center.YDouble) / 2; } } } if (!m_IsTrackedSuccessfully) { // If the tracking has failed, then some objects may be offscreen and have NaN position // So inherit the IsOffScreen flag from the previous position foreach (TrackedObject obj in TrackedObjects) { if (m_IsOffScreenPrev[obj.TargetNo] && double.IsNaN(obj.Center.XDouble)) { obj.SetIsLocated(false, NotMeasuredReasons.ObjectExpectedPositionIsOffScreen); } } } else { foreach (TrackedObject obj in TrackedObjects) { m_IsOffScreenPrev[obj.TargetNo] = obj.IsOffScreen; } } }
private List <CandidatePair> LocateStarPairsWithStarRecognition(TrackedObject trackedObject1, TrackedObject trackedObject2, List <PotentialStarStruct> stars, uint[,] pixels) { List <CandidatePair> candidates = new List <CandidatePair>(); double minFWHM = (1 - TangraConfig.Settings.Special.LostTrackingFWHMCoeff) * m_AverageFWHM; double maxFWHM = (1 + TangraConfig.Settings.Special.LostTrackingFWHMCoeff) * m_AverageFWHM; if (trackedObject1.TargetNo != trackedObject2.TargetNo && trackedObject1.LastKnownGoodPosition != null && trackedObject2.LastKnownGoodPosition != null) { double deltaX = trackedObject1.LastKnownGoodPosition.X - trackedObject2.LastKnownGoodPosition.X; double deltaY = trackedObject1.LastKnownGoodPosition.Y - trackedObject2.LastKnownGoodPosition.Y; // Looking for two stars with the same distance and similar brighness for (int i = 0; i < stars.Count; i++) { for (int j = 0; j < stars.Count; j++) { if (i == j) { continue; } double deltaXStars = stars[i].X - stars[j].X; double deltaYStars = stars[i].Y - stars[j].Y; if (Math.Abs(deltaX - deltaXStars) < TangraConfig.Settings.Special.LostTrackingPositionToleranceCoeff * PositionTolerance && Math.Abs(deltaY - deltaYStars) < TangraConfig.Settings.Special.LostTrackingPositionToleranceCoeff * PositionTolerance) { // Now compute PSFFits from the pixels PSFFit fit1 = AstroImage.GetPSFFitForPeakPixel(pixels, stars[i], m_MinLocateSignal, minFWHM, maxFWHM); PSFFit fit2 = AstroImage.GetPSFFitForPeakPixel(pixels, stars[j], m_MinLocateSignal, minFWHM, maxFWHM); if (fit1 != null && fit2 != null) { AutoDiscoveredStars.Add(fit1); AutoDiscoveredStars.Add(fit2); deltaXStars = fit1.XCenter - fit2.XCenter; deltaYStars = fit1.YCenter - fit2.YCenter; if (Math.Abs(deltaX - deltaXStars) < PositionTolerance && Math.Abs(deltaY - deltaYStars) < PositionTolerance) { // Compute a certainty and add to the candidates dictionary CandidatePair pair = new CandidatePair(); pair.Star1 = fit1; pair.Star2 = fit2; pair.Object1 = trackedObject1; pair.Object2 = trackedObject2; int[] BRIGTHNESS_MATCH_WEIGTH = new int[] { 0, 0, 0, 0, 0, 1, 1, 1, 2, 3, 3 }; int weight = (trackedObject1.IsGuidingStar ? 2 : 0) + (trackedObject2.IsGuidingStar ? 2 : 0); double d1 = pair.Star1.IMax - pair.Star1.I0; double d2 = pair.Star2.IMax - pair.Star2.I0; d1 = pair.Object1.RefinedOrLastSignalLevel / d1; if (d1 > 1) { d1 = 1 / d1; } int d1i = Math.Min(10, (int)Math.Round((1 - d1) * 10)); weight += BRIGTHNESS_MATCH_WEIGTH[d1i]; d2 = pair.Object2.RefinedOrLastSignalLevel / d2; if (d2 > 1) { d2 = 1 / d2; } int d2i = Math.Min(10, (int)Math.Round((1 - d2) * 10)); weight += BRIGTHNESS_MATCH_WEIGTH[d2i]; double distanceFromLastKnownGoodPosition = ImagePixel.ComputeDistance( pair.Star1.XCenter, pair.Object1.LastKnownGoodPosition.XDouble, pair.Star1.YCenter, pair.Object1.LastKnownGoodPosition.YDouble); int closeToPrevCoeff = LightCurveReductionContext.Instance.WindOrShaking ? 2 : 1; int closeToPrevPosWeighting = 0; if (distanceFromLastKnownGoodPosition < 4 * closeToPrevCoeff) { closeToPrevPosWeighting = 3; } else if (distanceFromLastKnownGoodPosition < 8 * closeToPrevCoeff) { closeToPrevPosWeighting = 2; } else if (distanceFromLastKnownGoodPosition < 16 * closeToPrevCoeff) { closeToPrevPosWeighting = 1; } else if (distanceFromLastKnownGoodPosition > 32 * closeToPrevCoeff) { closeToPrevPosWeighting = -1; } weight += closeToPrevPosWeighting; pair.Weight = weight; candidates.Add(pair); } else { Trace.WriteLine("Pair with close distances has been rejected."); } } } } } } return(candidates); }