Coverage for python/lsst/scarlet/lite/models/free_form.py: 30%

38 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-01 11:54 +0000

1# This file is part of scarlet_lite. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22__all__ = ["FreeFormComponent"] 

23 

24from typing import cast 

25 

26import numpy as np 

27 

28from ..bbox import Box 

29from ..component import FactorizedComponent 

30from ..detect import footprints_to_image 

31from ..parameters import Parameter 

32 

33 

34class FreeFormComponent(FactorizedComponent): 

35 """Implements a free-form component 

36 

37 With no constraints this component is typically either a garbage collector, 

38 or part of a set of components to deconvolve an image by separating out 

39 the different spectral components. 

40 

41 See `FactorizedComponent` for a list of parameters not shown here. 

42 

43 Parameters 

44 ---------- 

45 peaks: `list` of `tuple` 

46 A set of ``(cy, cx)`` peaks for detected sources. 

47 If peak is not ``None`` then only pixels in the same "footprint" 

48 as one of the peaks are included in the morphology. 

49 If `peaks` is ``None`` then there is no constraint applied. 

50 min_area: float 

51 The minimum area for a peak. 

52 If `min_area` is not `None` then all regions of the morphology 

53 with fewer than `min_area` connected pixels are removed. 

54 """ 

55 

56 def __init__( 

57 self, 

58 bands: tuple, 

59 spectrum: np.ndarray | Parameter, 

60 morph: np.ndarray | Parameter, 

61 model_bbox: Box, 

62 bg_thresh: float | None = None, 

63 bg_rms: np.ndarray | None = None, 

64 floor: float = 1e-20, 

65 peaks: list[tuple[int, int]] | None = None, 

66 min_area: float = 0, 

67 ): 

68 super().__init__( 

69 bands=bands, 

70 spectrum=spectrum, 

71 morph=morph, 

72 bbox=model_bbox, 

73 peak=None, 

74 bg_rms=bg_rms, 

75 bg_thresh=bg_thresh, 

76 floor=floor, 

77 ) 

78 

79 self.peaks = peaks 

80 self.min_area = min_area 

81 

82 def prox_spectrum(self, spectrum: np.ndarray) -> np.ndarray: 

83 """Apply a prox-like update to the spectrum 

84 

85 This differs from `FactorizedComponent` because an 

86 `SedComponent` has the spectrum normalized to unity. 

87 """ 

88 # prevent divergent spectrum 

89 spectrum[spectrum < self.floor] = self.floor 

90 # Normalize the spectrum 

91 spectrum = spectrum / np.sum(spectrum) 

92 return spectrum 

93 

94 def prox_morph(self, morph: np.ndarray) -> np.ndarray: 

95 """Apply a prox-like update to the morphology 

96 

97 This is the main difference between an `SedComponent` and a 

98 `FactorizedComponent`, since this component has fewer constraints. 

99 """ 

100 from lsst.scarlet.lite.detect_pybind11 import get_connected_multipeak, get_footprints # type: ignore 

101 

102 if self.bg_thresh is not None and isinstance(self.bg_rms, np.ndarray): 

103 bg_thresh = self.bg_rms * self.bg_thresh 

104 # Enforce background thresholding 

105 model = self.spectrum[:, None, None] * morph[None, :, :] 

106 morph[np.all(model < bg_thresh[:, None, None], axis=0)] = 0 

107 else: 

108 # enforce positivity 

109 morph[morph < 0] = 0 

110 

111 if self.peaks is not None: 

112 morph = morph * get_connected_multipeak(morph > 0, self.peaks, 0) 

113 

114 if self.min_area > 0: 

115 footprints = get_footprints(morph > 0, 4.0, self.min_area, 0, False) 

116 footprint_image = footprints_to_image(footprints, cast(tuple[int, int], morph.shape)) 

117 morph = morph * (footprint_image > 0).data 

118 

119 if np.all(morph == 0): 

120 morph[0, 0] = self.floor 

121 

122 return morph 

123 

124 def resize(self, model_box: Box) -> bool: 

125 return False 

126 

127 def __str__(self): 

128 return ( 

129 f"FreeFormComponent(\n bands={self.bands}\n " 

130 f"spectrum={self.spectrum})\n center={self.peak}\n " 

131 f"morph_shape={self.morph.shape}" 

132 ) 

133 

134 def __repr__(self): 

135 return self.__str__()