655 """Find streaks in a masked image.
659 maskedImage : `lsst.afw.image.maskedImage`
660 The image in which to search for streaks.
664 result : `lsst.pipe.base.Struct`
665 Results as a struct with attributes:
668 Lines identified by kernel hough transform.
670 Lines grouped into clusters in rho-theta space.
672 Final result for lines after line-profile fit.
674 2-d boolean mask where detected lines are True.
676 mask = maskedImage.mask
677 detectionMask = (mask.array & mask.getPlaneBitMask(self.config.detectedMaskPlane))
682 ignoreMask = mask.clone()
684 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
685 badMaskSpanSet = SpanSet.fromMask(mask, badPixelMask).split()
686 for sset
in badMaskSpanSet:
687 sset_dilated = sset.dilated(1)
688 sset_dilated.clippedTo(
689 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
693 if self.config.saturatedDetectionsDilation:
695 satMask = mask.getPlaneBitMask(
"SAT")
696 satMask = (mask.array & mask.getPlaneBitMask(
"SAT"))
697 satDetMask = (satMask != 0) & (detectionMask != 0)
699 satSpanSet = SpanSet.fromMask(satDetIm, 1).split()
700 for sset
in satSpanSet:
701 sset_dilated = sset.dilated(self.config.saturatedDetectionsDilation)
702 sset_dilated.clippedTo(
703 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
705 dilatedBadMask = (ignoreMask.array & badPixelMask) > 0
706 self.
edges = initEdges & ~dilatedBadMask
709 if len(self.
lines) == 0:
710 lineMask = np.zeros(detectionMask.shape, dtype=bool)
715 fitLines, lineMask = self.
_fitProfile(clusters, maskedImage, detectionMask=detectionMask)
717 if self.config.onlyMaskDetected:
719 lineMask &= detectionMask.astype(bool)
721 return pipeBase.Struct(
723 lineClusters=clusters,
724 originalLines=self.
lines,
729 def run(self, maskedImage):
730 """Find and mask streaks in a masked image.
732 Finds streaks in the image and modifies maskedImage in place by adding a
733 mask plane with any identified streaks.
737 maskedImage : `lsst.afw.image.Exposure` or `lsst.afw.image.maskedImage`
738 The image in which to search for streaks. The mask detection plane
739 corresponding to `config.detectedMaskPlane` must be set with the
740 detected pixels. The mask will have a plane added with any detected
741 streaks, and with the mask plane name set by
742 self.config.streaksMaskPlane.
746 result : `lsst.pipe.base.Struct`
747 Results as a struct with attributes:
750 Lines identified by kernel hough transform.
752 Lines grouped into clusters in rho-theta space.
754 Final result for lines after line-profile fit.
756 streaks = self.
find(maskedImage)
758 if (self.config.streaksMaskPlane !=
"STREAK")
and \
759 (self.config.streaksMaskPlane
not in maskedImage.mask.getMaskPlaneDict()):
760 maskedImage.mask.addMaskPlane(self.config.streaksMaskPlane)
761 maskedImage.mask.array[streaks.mask] |= maskedImage.mask.getPlaneBitMask(self.config.streaksMaskPlane)
763 return pipeBase.Struct(
765 lineClusters=streaks.lineClusters,
766 originalLines=streaks.originalLines,
788 """Run Kernel Hough Transform on image.
793 2-d image data on which to detect lines.
797 result : `LineCollection`
798 Collection of detected lines, with their detected rho and theta
801 lines = lsst.kht.find_lines(image, self.config.clusterMinimumSize,
802 self.config.clusterMinimumDeviation, self.config.delta,
803 self.config.minimumKernelHeight, self.config.nSigma,
804 self.config.absMinimumKernelHeight)
805 self.log.info(
"The Kernel Hough Transform detected %s line(s)", len(lines))
810 """Group lines that are close in parameter space and likely describe
815 lines : `LineCollection`
816 Collection of lines to group into clusters.
820 result : `LineCollection`
821 Average `Line` for each cluster of `Line`s in the input
828 x = lines.rhos / self.config.rhoBinSize
829 y = lines.thetas / self.config.thetaBinSize
830 X = np.array([x, y]).T
839 kmeans = KMeans(n_clusters=nClusters, n_init=
'auto').fit(X)
840 clusterStandardDeviations = np.zeros((nClusters, 2))
841 for c
in range(nClusters):
842 inCluster = X[kmeans.labels_ == c]
843 clusterStandardDeviations[c] = np.std(inCluster, axis=0)
845 if (clusterStandardDeviations <= 1).all():
850 finalClusters = kmeans.cluster_centers_.T
853 finalRhos = finalClusters[0] * self.config.rhoBinSize
854 finalThetas = finalClusters[1] * self.config.thetaBinSize
856 self.log.info(
"Lines were grouped into %s potential streak(s)", len(finalRhos))
861 """Fit the profile of the streak.
863 Given the initial parameters of detected lines, fit a model for the
864 streak to the original (non-binary image). The assumed model is a
865 straight line with a Moffat profile.
869 lines : `LineCollection`
870 Collection of guesses for `Line`s detected in the image.
871 maskedImage : `lsst.afw.image.maskedImage`
872 Original image to be used to fit profile of streak.
876 lineFits : `LineCollection`
877 Collection of `Line` profiles fit to the data.
878 finalMask : `np.ndarray`
879 2d mask array with detected streaks=1.
881 data = maskedImage.image.array
882 weights = maskedImage.variance.array**-1
883 mask = maskedImage.mask
884 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
885 badMask = (mask.array & badPixelMask) > 0
887 weights[~np.isfinite(weights) | ~np.isfinite(data)] = 0
891 finalLineMasks = [np.zeros(data.shape, dtype=bool)]
895 line.sigma = self.config.invSigma**-1
896 lineModel =
LineProfile(data, weights, line=line, detectionMask=detectionMask)
898 if lineModel.lineMask.sum() == 0:
901 fit, fitFailure = lineModel.fit(dChi2Tol=self.config.dChi2Tolerance, log=self.log,
902 maxIter=self.config.maxFitIter)
906 if ((abs(fit.rho - line.rho) > 2 * self.config.rhoBinSize)
907 or (abs(fit.theta - line.theta) > 2 * self.config.thetaBinSize)):
909 self.log.debug(
"Streak fit moved too far from initial estimate. Line will be dropped.")
916 lineModel.setLineMask(fit, self.config.maxStreakWidth, self.config.nSigmaMask, logger=self.log)
917 finalModel = lineModel.makeProfile(fit)
919 finalModelMax = abs(finalModel).max()
920 finalLineMask = abs(finalModel) > self.config.footprintThreshold
923 if not finalLineMask.any():
924 self.log.debug(
"Streak model profile is below the footprintThreshold.")
926 fit.modelMaximum = finalModelMax
928 finalLineMasks.append(finalLineMask)
932 self.log.info(
"Streak profile could not be fit for %d out of %d detected lines.", nFitFailures,
934 finalMask = np.array(finalLineMasks).any(axis=0)
936 return lineFits, finalMask