Coverage for python / lsst / pipe / tasks / prettyPictureMaker / _utils.py: 13%

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:39 +0000

1# This file is part of pipe_tasks. 

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 

22from __future__ import annotations 

23 

24import numpy as np 

25from typing import TYPE_CHECKING 

26 

27if TYPE_CHECKING: 

28 from numpy.typing import NDArray 

29 from lsst.geom import Box2I 

30 

31 

32class FeatheredMosaicCreator: 

33 """Create feathering masks for seamless image patch blending. 

34 

35 This class generates feathering masks used to smoothly blend image patches 

36 into a larger mosaic. The feathering gradually transitions from full opacity 

37 to zero opacity across the patch boundaries, reducing visible seams. 

38 

39 Parameters 

40 ---------- 

41 patch_grow : `int` 

42 Number of pixels to grow the patch boundaries for feathering. 

43 bin_factor : `int`, optional 

44 Binning factor for the feathering calculation. Reduces resolution 

45 of feathering masks for faster computation. Default is 1. 

46 

47 Notes 

48 ----- 

49 The feathering masks are created as linear ramps from 0 to 1 (or 1 to 0) 

50 over the `patch_grow` distance. The masks are stored in `self.featherings` 

51 as a list of arrays in the order: [top, bottom, left, right]. 

52 """ 

53 

54 def __init__(self, patch_grow: int, bin_factor: int = 1) -> None: 

55 self.patch_grow = patch_grow 

56 self.bin_factor = bin_factor 

57 self.featherings = None 

58 

59 def _make_featherings(self, dimensions: tuple[int, int]) -> None: 

60 """Create feathering masks for all edges of the patch. 

61 

62 Parameters 

63 ---------- 

64 dimensions : `tuple` of `int` 

65 Shape of the patch (height, width) for which to create feathering masks. 

66 

67 Notes 

68 ----- 

69 This method creates four feathering masks (top, bottom, left, right) using 

70 linear ramps from 0 to 1 (or 1 to 0) over the `patch_grow` distance. The 

71 featherings are stored in `self.featherings` as a list of arrays. 

72 """ 

73 extent = self.patch_grow * 2 

74 if self.bin_factor != 1: 

75 extent = int(np.floor(extent / self.bin_factor)) 

76 ramp = np.linspace(0, 1, extent) 

77 ramp[0] = 1e-17 

78 top = np.ones(dimensions) 

79 top[:extent, :] = np.repeat(np.expand_dims(ramp, 1), top.shape[1], axis=1) 

80 

81 bottom = np.ones(dimensions) 

82 bottom[-1 * extent :, :] = np.repeat( # noqa: E203 

83 np.expand_dims(1 - ramp, 1), bottom.shape[1], axis=1 

84 ) 

85 

86 left = np.ones(dimensions) 

87 left[:, :extent] = np.repeat(np.expand_dims(ramp, 0), left.shape[0], axis=0) 

88 

89 right = np.ones(dimensions) 

90 right[:, -1 * extent :] = np.repeat( # noqa: E203 

91 np.expand_dims(1 - ramp, 0), right.shape[0], axis=0 

92 ) 

93 self.featherings = [ 

94 top, 

95 bottom, 

96 left, 

97 right, 

98 ] 

99 

100 def add_to_image( 

101 self, image: NDArray, patch: NDArray, newBox: Box2I, box: Box2I, reverse: bool = True 

102 ) -> None: 

103 """Add a patch to an image with feathering at the edges. 

104 

105 Parameters 

106 ---------- 

107 image : `NDArray` 

108 Target image to which the patch will be added. Modified in-place. 

109 patch : `NDArray` 

110 Patch array to be added to the image. 

111 newBox : `Box2I` 

112 New bounding box position of the patch. 

113 box : `Box2I` 

114 Original bounding box position of the patch. 

115 reverse : `bool`, optional 

116 If True, reverse the patch along the first axis before adding. 

117 Default is True. 

118 

119 Notes 

120 ----- 

121 This method applies feathering to smoothly blend the patch into the image 

122 at the edges where the patch overlaps with existing image content. The 

123 feathering is applied based on which edges of the patch differ between 

124 `box` and `newBox`. The patch is multiplied by a mixer array that gradually 

125 transitions from 0 to 1 across the feathering region. 

126 """ 

127 base_shape = patch.shape if patch.ndim == 2 else patch.shape[:2] 

128 mixer = np.ones(base_shape) 

129 if self.featherings is None: 

130 self._make_featherings(base_shape) 

131 if box.getBeginY() != newBox.getBeginY(): 

132 mixer *= self.featherings[0] 

133 if box.getEndY() != newBox.getEndY(): 

134 mixer *= self.featherings[1] 

135 if box.getBeginX() != newBox.getBeginX(): 

136 mixer *= self.featherings[2] 

137 if box.getEndX() != newBox.getEndX(): 

138 mixer *= self.featherings[3] 

139 

140 if image.ndim > 2: 

141 mixer = np.repeat(np.expand_dims(mixer, 2), 3, axis=2) 

142 

143 patch = mixer * patch 

144 

145 image[*box.slices] += patch[::-1, :, :] if reverse else patch