661 """Find streaks in a masked image.
665 maskedImage : `lsst.afw.image.maskedImage`
666 The image in which to search for streaks.
670 result : `lsst.pipe.base.Struct`
671 Results as a struct with attributes:
674 Lines identified by kernel hough transform.
676 Lines grouped into clusters in rho-theta space.
678 Final result for lines after line-profile fit.
680 2-d boolean mask where detected lines are True.
682 mask = maskedImage.mask
683 detectionMask = (mask.array & mask.getPlaneBitMask(self.config.detectedMaskPlane))
688 ignoreMask = mask.clone()
690 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
691 badMaskSpanSet = SpanSet.fromMask(mask, badPixelMask).split()
692 for sset
in badMaskSpanSet:
693 sset_dilated = sset.dilated(1)
694 sset_dilated.clippedTo(
695 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
699 if self.config.saturatedDetectionsDilation:
701 satMask = mask.getPlaneBitMask(
"SAT")
702 satMask = (mask.array & mask.getPlaneBitMask(
"SAT"))
703 satDetMask = (satMask != 0) & (detectionMask != 0)
705 satSpanSet = SpanSet.fromMask(satDetIm, 1).split()
706 for sset
in satSpanSet:
707 sset_dilated = sset.dilated(self.config.saturatedDetectionsDilation)
708 sset_dilated.clippedTo(
709 ignoreMask.getBBox()).setMask(ignoreMask, ignoreMask.getPlaneBitMask(
"BAD"))
711 dilatedBadMask = (ignoreMask.array & badPixelMask) > 0
712 self.
edges = initEdges & ~dilatedBadMask
715 if len(self.
lines) == 0:
716 lineMask = np.zeros(detectionMask.shape, dtype=bool)
721 fitLines, lineMask = self.
_fitProfile(clusters, maskedImage, detectionMask=detectionMask)
723 if self.config.onlyMaskDetected:
725 lineMask &= detectionMask.astype(bool)
727 return pipeBase.Struct(
729 lineClusters=clusters,
730 originalLines=self.
lines,
735 def run(self, maskedImage):
736 """Find and mask streaks in a masked image.
738 Finds streaks in the image and modifies maskedImage in place by adding a
739 mask plane with any identified streaks.
743 maskedImage : `lsst.afw.image.Exposure` or `lsst.afw.image.maskedImage`
744 The image in which to search for streaks. The mask detection plane
745 corresponding to `config.detectedMaskPlane` must be set with the
746 detected pixels. The mask will have a plane added with any detected
747 streaks, and with the mask plane name set by
748 self.config.streaksMaskPlane.
752 result : `lsst.pipe.base.Struct`
753 Results as a struct with attributes:
756 Lines identified by kernel hough transform.
758 Lines grouped into clusters in rho-theta space.
760 Final result for lines after line-profile fit.
762 streaks = self.
find(maskedImage)
764 if (self.config.streaksMaskPlane !=
"STREAK")
and \
765 (self.config.streaksMaskPlane
not in maskedImage.mask.getMaskPlaneDict()):
766 maskedImage.mask.addMaskPlane(self.config.streaksMaskPlane)
767 maskedImage.mask.array[streaks.mask] |= maskedImage.mask.getPlaneBitMask(self.config.streaksMaskPlane)
769 return pipeBase.Struct(
771 lineClusters=streaks.lineClusters,
772 originalLines=streaks.originalLines,
794 """Run Kernel Hough Transform on image.
799 2-d image data on which to detect lines.
803 result : `LineCollection`
804 Collection of detected lines, with their detected rho and theta
807 lines = lsst.kht.find_lines(image, self.config.clusterMinimumSize,
808 self.config.clusterMinimumDeviation, self.config.delta,
809 self.config.minimumKernelHeight, self.config.nSigma,
810 self.config.absMinimumKernelHeight)
811 self.log.info(
"The Kernel Hough Transform detected %s line(s)", len(lines))
816 """Group lines that are close in parameter space and likely describe
821 lines : `LineCollection`
822 Collection of lines to group into clusters.
826 result : `LineCollection`
827 Average `Line` for each cluster of `Line`s in the input
834 x = lines.rhos / self.config.rhoBinSize
835 y = lines.thetas / self.config.thetaBinSize
836 X = np.array([x, y]).T
845 kmeans = KMeans(n_clusters=nClusters, n_init=
'auto').fit(X)
846 clusterStandardDeviations = np.zeros((nClusters, 2))
847 for c
in range(nClusters):
848 inCluster = X[kmeans.labels_ == c]
849 clusterStandardDeviations[c] = np.std(inCluster, axis=0)
851 if (clusterStandardDeviations <= 1).all():
856 finalClusters = kmeans.cluster_centers_.T
859 finalRhos = finalClusters[0] * self.config.rhoBinSize
860 finalThetas = finalClusters[1] * self.config.thetaBinSize
862 self.log.info(
"Lines were grouped into %s potential streak(s)", len(finalRhos))
867 """Fit the profile of the streak.
869 Given the initial parameters of detected lines, fit a model for the
870 streak to the original (non-binary image). The assumed model is a
871 straight line with a Moffat profile.
875 lines : `LineCollection`
876 Collection of guesses for `Line`s detected in the image.
877 maskedImage : `lsst.afw.image.maskedImage`
878 Original image to be used to fit profile of streak.
882 lineFits : `LineCollection`
883 Collection of `Line` profiles fit to the data.
884 finalMask : `np.ndarray`
885 2d mask array with detected streaks=1.
887 data = maskedImage.image.array
888 weights = maskedImage.variance.array**-1
889 mask = maskedImage.mask
890 badPixelMask = mask.getPlaneBitMask(self.config.badMaskPlanes)
891 badMask = (mask.array & badPixelMask) > 0
893 weights[~np.isfinite(weights) | ~np.isfinite(data)] = 0
897 finalLineMasks = [np.zeros(data.shape, dtype=bool)]
901 line.sigma = self.config.invSigma**-1
902 lineModel =
LineProfile(data, weights, line=line, detectionMask=detectionMask)
904 if lineModel.modelFailure
or lineModel.lineMask.sum() == 0:
907 fit, fitFailure = lineModel.fit(dChi2Tol=self.config.dChi2Tolerance, log=self.log,
908 maxIter=self.config.maxFitIter)
912 if ((abs(fit.rho - line.rho) > 2 * self.config.rhoBinSize)
913 or (abs(fit.theta - line.theta) > 2 * self.config.thetaBinSize)):
915 self.log.debug(
"Streak fit moved too far from initial estimate. Line will be dropped.")
922 lineModel.setLineMask(fit, self.config.maxStreakWidth, self.config.nSigmaMask, logger=self.log)
923 finalModel = lineModel.makeProfile(fit)
925 finalModelMax = abs(finalModel).max()
926 finalLineMask = abs(finalModel) > self.config.footprintThreshold
929 if not finalLineMask.any():
930 self.log.debug(
"Streak model profile is below the footprintThreshold.")
932 fit.modelMaximum = finalModelMax
934 finalLineMasks.append(finalLineMask)
938 self.log.info(
"Streak profile could not be fit for %d out of %d detected lines.", nFitFailures,
940 finalMask = np.array(finalLineMasks).any(axis=0)
942 return lineFits, finalMask