649 """Find streaks in a masked image.
653 maskedImage : `lsst.afw.image.maskedImage`
654 The image in which to search for streaks.
658 result : `lsst.pipe.base.Struct`
659 Results as a struct with attributes:
662 Lines identified by kernel hough transform.
664 Lines grouped into clusters in rho-theta space.
666 Final result for lines after line-profile fit.
668 2-d boolean mask where detected lines are True.
670 mask = maskedImage.mask
671 detectionMask = (mask.array & mask.getPlaneBitMask(self.config.detectedMaskPlane))
676 ignoreMask = mask.clone()
678 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
679 badMaskSpanSet = SpanSet.fromMask(mask, badPixelMask).split()
680 for sset
in badMaskSpanSet:
681 sset_dilated = sset.dilated(1)
682 sset_dilated.clippedTo(
683 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
687 if self.config.saturatedDetectionsDilation:
689 satMask = mask.getPlaneBitMask(
"SAT")
690 satMask = (mask.array & mask.getPlaneBitMask(
"SAT"))
691 satDetMask = (satMask != 0) & (detectionMask != 0)
693 satSpanSet = SpanSet.fromMask(satDetIm, 1).split()
694 for sset
in satSpanSet:
695 sset_dilated = sset.dilated(self.config.saturatedDetectionsDilation)
696 sset_dilated.clippedTo(
697 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
699 dilatedBadMask = (ignoreMask.array & badPixelMask) > 0
700 self.
edges = initEdges & ~dilatedBadMask
703 if len(self.
lines) == 0:
704 lineMask = np.zeros(detectionMask.shape, dtype=bool)
709 fitLines, lineMask = self.
_fitProfile(clusters, maskedImage, detectionMask=detectionMask)
711 if self.config.onlyMaskDetected:
713 lineMask &= detectionMask.astype(bool)
715 return pipeBase.Struct(
717 lineClusters=clusters,
718 originalLines=self.
lines,
723 def run(self, maskedImage):
724 """Find and mask streaks in a masked image.
726 Finds streaks in the image and modifies maskedImage in place by adding a
727 mask plane with any identified streaks.
731 maskedImage : `lsst.afw.image.Exposure` or `lsst.afw.image.maskedImage`
732 The image in which to search for streaks. The mask detection plane
733 corresponding to `config.detectedMaskPlane` must be set with the
734 detected pixels. The mask will have a plane added with any detected
735 streaks, and with the mask plane name set by
736 self.config.streaksMaskPlane.
740 result : `lsst.pipe.base.Struct`
741 Results as a struct with attributes:
744 Lines identified by kernel hough transform.
746 Lines grouped into clusters in rho-theta space.
748 Final result for lines after line-profile fit.
750 streaks = self.
find(maskedImage)
752 if (self.config.streaksMaskPlane !=
"STREAK")
and \
753 (self.config.streaksMaskPlane
not in maskedImage.mask.getMaskPlaneDict()):
754 maskedImage.mask.addMaskPlane(self.config.streaksMaskPlane)
755 maskedImage.mask.array[streaks.mask] |= maskedImage.mask.getPlaneBitMask(self.config.streaksMaskPlane)
757 return pipeBase.Struct(
759 lineClusters=streaks.lineClusters,
760 originalLines=streaks.originalLines,
782 """Run Kernel Hough Transform on image.
787 2-d image data on which to detect lines.
791 result : `LineCollection`
792 Collection of detected lines, with their detected rho and theta
795 lines = lsst.kht.find_lines(image, self.config.clusterMinimumSize,
796 self.config.clusterMinimumDeviation, self.config.delta,
797 self.config.minimumKernelHeight, self.config.nSigma,
798 self.config.absMinimumKernelHeight)
799 self.log.info(
"The Kernel Hough Transform detected %s line(s)", len(lines))
804 """Group lines that are close in parameter space and likely describe
809 lines : `LineCollection`
810 Collection of lines to group into clusters.
814 result : `LineCollection`
815 Average `Line` for each cluster of `Line`s in the input
822 x = lines.rhos / self.config.rhoBinSize
823 y = lines.thetas / self.config.thetaBinSize
824 X = np.array([x, y]).T
833 kmeans = KMeans(n_clusters=nClusters, n_init=
'auto').fit(X)
834 clusterStandardDeviations = np.zeros((nClusters, 2))
835 for c
in range(nClusters):
836 inCluster = X[kmeans.labels_ == c]
837 clusterStandardDeviations[c] = np.std(inCluster, axis=0)
839 if (clusterStandardDeviations <= 1).all():
844 finalClusters = kmeans.cluster_centers_.T
847 finalRhos = finalClusters[0] * self.config.rhoBinSize
848 finalThetas = finalClusters[1] * self.config.thetaBinSize
850 self.log.info(
"Lines were grouped into %s potential streak(s)", len(finalRhos))
855 """Fit the profile of the streak.
857 Given the initial parameters of detected lines, fit a model for the
858 streak to the original (non-binary image). The assumed model is a
859 straight line with a Moffat profile.
863 lines : `LineCollection`
864 Collection of guesses for `Line`s detected in the image.
865 maskedImage : `lsst.afw.image.maskedImage`
866 Original image to be used to fit profile of streak.
870 lineFits : `LineCollection`
871 Collection of `Line` profiles fit to the data.
872 finalMask : `np.ndarray`
873 2d mask array with detected streaks=1.
875 data = maskedImage.image.array
876 weights = maskedImage.variance.array**-1
877 mask = maskedImage.mask
878 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
879 badMask = (mask.array & badPixelMask) > 0
881 weights[~np.isfinite(weights) | ~np.isfinite(data)] = 0
885 finalLineMasks = [np.zeros(data.shape, dtype=bool)]
889 line.sigma = self.config.invSigma**-1
890 lineModel =
LineProfile(data, weights, line=line, detectionMask=detectionMask)
892 if lineModel.lineMask.sum() == 0:
895 fit, fitFailure = lineModel.fit(dChi2Tol=self.config.dChi2Tolerance, log=self.log,
896 maxIter=self.config.maxFitIter)
900 if ((abs(fit.rho - line.rho) > 2 * self.config.rhoBinSize)
901 or (abs(fit.theta - line.theta) > 2 * self.config.thetaBinSize)):
903 self.log.debug(
"Streak fit moved too far from initial estimate. Line will be dropped.")
910 lineModel.setLineMask(fit, self.config.maxStreakWidth, self.config.nSigmaMask, logger=self.log)
911 finalModel = lineModel.makeProfile(fit)
913 finalModelMax = abs(finalModel).max()
914 finalLineMask = abs(finalModel) > self.config.footprintThreshold
917 if not finalLineMask.any():
919 fit.modelMaximum = finalModelMax
921 finalLineMasks.append(finalLineMask)
925 self.log.info(
"Streak profile could not be fit for %d out of %d detected lines.", nFitFailures,
927 finalMask = np.array(finalLineMasks).any(axis=0)
929 return lineFits, finalMask