Coverage for python/lsst/images/_color_image.py: 39%

95 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 08:38 +0000

1# This file is part of lsst-images. 

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# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ("ColorImage",) 

15 

16import functools 

17from collections.abc import Sequence 

18from types import EllipsisType 

19from typing import Any, ClassVar, Literal 

20 

21import numpy as np 

22import pydantic 

23 

24from ._generalized_image import GeneralizedImage 

25from ._geom import Box 

26from ._image import Image, ImageSerializationModel 

27from ._transforms import SkyProjection, SkyProjectionSerializationModel 

28from .describe import DescribeOptions, FieldRole, Report, ReportField 

29from .serialization import ArchiveTree, InputArchive, InvalidParameterError, MetadataValue, OutputArchive 

30from .utils import is_none 

31 

32 

33class ColorImage(GeneralizedImage): 

34 """An RGB image with an optional `SkyProjection`. 

35 

36 Parameters 

37 ---------- 

38 array 

39 Array or fill value for the image. Must have three dimensions with 

40 the shape of the third dimension equal to three. 

41 bbox 

42 Bounding box for the image. 

43 yx0 

44 Logical coordinates of the first pixel in the array, ordered ``y``, 

45 ``x`` (unless an `XY` instance is passed). Ignored if 

46 ``bbox`` is provided. Defaults to zeros. 

47 sky_projection 

48 Projection that maps the pixel grid to the sky. 

49 metadata 

50 Arbitrary flexible metadata to associate with the image. 

51 """ 

52 

53 def __init__( 

54 self, 

55 array: np.ndarray[tuple[int, int, Literal[3]], np.dtype[Any]], 

56 /, 

57 *, 

58 bbox: Box | None = None, 

59 yx0: Sequence[int] | None = None, 

60 sky_projection: SkyProjection[Any] | None = None, 

61 metadata: dict[str, MetadataValue] | None = None, 

62 ) -> None: 

63 super().__init__(metadata) 

64 if bbox is None: 

65 bbox = Box.from_shape(array.shape[:2], start=yx0) 

66 elif bbox.shape + (3,) != array.shape: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true

67 raise ValueError( 

68 f"Shape from bbox {bbox.shape + (3,)} does not match array with shape {array.shape}." 

69 ) 

70 self._array = array 

71 self._red = Image(self._array[..., 0], bbox=bbox, sky_projection=sky_projection) 

72 self._green = Image(self._array[..., 1], bbox=bbox, sky_projection=sky_projection) 

73 self._blue = Image(self._array[..., 2], bbox=bbox, sky_projection=sky_projection) 

74 

75 @staticmethod 

76 def from_channels( 

77 r: Image, 

78 g: Image, 

79 b: Image, 

80 *, 

81 sky_projection: SkyProjection[Any] | None = None, 

82 metadata: dict[str, MetadataValue] | None = None, 

83 ) -> ColorImage: 

84 """Construct from separate RGB images. 

85 

86 All channels are assumed to have the same bounding box, sky_projection, 

87 and pixel type. 

88 

89 Parameters 

90 ---------- 

91 r 

92 Red channel image. 

93 g 

94 Green channel image. 

95 b 

96 Blue channel image. 

97 sky_projection 

98 Sky projection for the image, defaulting to that of ``r``. 

99 metadata 

100 Flexible metadata to associate with the image. 

101 """ 

102 if sky_projection is None and r.sky_projection is not None: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 sky_projection = r.sky_projection 

104 return ColorImage( 

105 np.stack([r.array, g.array, b.array], axis=2), 

106 bbox=r.bbox, 

107 sky_projection=sky_projection, 

108 metadata=metadata, 

109 ) 

110 

111 @property 

112 def array(self) -> np.ndarray[tuple[int, int, Literal[3]], np.dtype[Any]]: 

113 """The 3-d array (`numpy.ndarray`).""" 

114 return self._array 

115 

116 @property 

117 def red(self) -> Image: 

118 """A 2-d view of the red channel (`Image`).""" 

119 return self._red 

120 

121 @property 

122 def green(self) -> Image: 

123 """A 2-d view of the green channel (`Image`).""" 

124 return self._green 

125 

126 @property 

127 def blue(self) -> Image: 

128 """A 2-d view of the blue channel (`Image`).""" 

129 return self._blue 

130 

131 @property 

132 def bbox(self) -> Box: 

133 """The 2-d bounding box of the image (`Box`).""" 

134 return self._red.bbox 

135 

136 @property 

137 def sky_projection(self) -> SkyProjection[Any] | None: 

138 """The projection that maps the pixel grid to the sky 

139 (`SkyProjection` | `None`). 

140 """ 

141 return self._red.sky_projection 

142 

143 def __getitem__(self, bbox: Box | EllipsisType) -> ColorImage: 

144 bbox, indices = self._handle_getitem_args(bbox) 

145 return self._transfer_metadata( 

146 ColorImage( 

147 self.array[indices + (slice(None),)], 

148 bbox=bbox, 

149 sky_projection=self.sky_projection, 

150 ), 

151 bbox=bbox, 

152 ) 

153 

154 def __setitem__(self, bbox: Box | EllipsisType, value: ColorImage) -> None: 

155 self[bbox].array[...] = value.array 

156 

157 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report: 

158 """Return a `Report` describing this color image. 

159 

160 Parameters 

161 ---------- 

162 options : `DescribeOptions`, optional 

163 Rendering options. ``"bbox"`` and ``"sky_projection"`` are 

164 recognized in `DescribeOptions.exclude`. 

165 """ 

166 children = {} 

167 sky_projection = self.sky_projection 

168 if not options.brief and "sky_projection" not in options.exclude and sky_projection is not None: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 children["sky_projection"] = sky_projection._describe(options.for_child(), bbox=self.bbox) 

170 fields = [ 

171 ReportField( 

172 label="array", 

173 value="<array>", 

174 repr_value="...", 

175 positional=True, 

176 role=FieldRole.REPR_ONLY, 

177 ), 

178 ] 

179 if "bbox" not in options.exclude: 179 ↛ 181line 179 didn't jump to line 181 because the condition on line 179 was always true

180 fields.append(ReportField(label="bbox", value=self.bbox, repr_value=repr(self.bbox))) 

181 fields.append( 

182 ReportField(label="dtype", value=str(self._array.dtype), repr_value=repr(self._array.dtype)) 

183 ) 

184 return Report( 

185 type_name="ColorImage", 

186 summary=f"ColorImage({self.bbox!s}, {self._array.dtype.type.__name__})", 

187 fields=fields, 

188 children=children, 

189 ) 

190 

191 def copy(self) -> ColorImage: 

192 """Deep-copy the image.""" 

193 return self._transfer_metadata( 

194 ColorImage(self._array.copy(), bbox=self.bbox, sky_projection=self.sky_projection), copy=True 

195 ) 

196 

197 def serialize(self, archive: OutputArchive[Any]) -> ColorImageSerializationModel: 

198 """Serialize the masked image to an output archive. 

199 

200 Parameters 

201 ---------- 

202 archive 

203 Archive to write to. 

204 """ 

205 r = archive.serialize_direct("red", functools.partial(self.red.serialize, save_projection=False)) 

206 g = archive.serialize_direct("green", functools.partial(self.green.serialize, save_projection=False)) 

207 b = archive.serialize_direct("blue", functools.partial(self.blue.serialize, save_projection=False)) 

208 serialized_projection = ( 

209 archive.serialize_direct("sky_projection", self.sky_projection.serialize) 

210 if self.sky_projection is not None 

211 else None 

212 ) 

213 return ColorImageSerializationModel( 

214 red=r, green=g, blue=b, sky_projection=serialized_projection, metadata=self.metadata 

215 ) 

216 

217 @staticmethod 

218 def _get_archive_tree_type[P: pydantic.BaseModel]( 

219 pointer_type: type[P], 

220 ) -> type[ColorImageSerializationModel[P]]: 

221 """Return the serialization model type for this object for an archive 

222 type that uses the given pointer type. 

223 """ 

224 return ColorImageSerializationModel[pointer_type] # type: ignore 

225 

226 

227class ColorImageSerializationModel[P: pydantic.BaseModel](ArchiveTree): 

228 """A Pydantic model used to represent a serialized `ColorImage`.""" 

229 

230 SCHEMA_NAME: ClassVar[str] = "color_image" 

231 SCHEMA_VERSION: ClassVar[str] = "1.0.0.dev0" 

232 MIN_READ_VERSION: ClassVar[int] = 1 

233 PUBLIC_TYPE: ClassVar[type] = ColorImage 

234 

235 red: ImageSerializationModel[P] = pydantic.Field(description="The red channel.") 

236 green: ImageSerializationModel[P] = pydantic.Field(description="The green channel.") 

237 blue: ImageSerializationModel[P] = pydantic.Field(description="The blue channel") 

238 sky_projection: SkyProjectionSerializationModel[P] | None = pydantic.Field( 

239 default=None, 

240 exclude_if=is_none, 

241 description="Projection that maps the pixel grid to the sky.", 

242 ) 

243 

244 @property 

245 def bbox(self) -> Box: 

246 """The bounding box of the image.""" 

247 return self.red.bbox 

248 

249 def deserialize( 

250 self, archive: InputArchive[Any], *, bbox: Box | None = None, **kwargs: Any 

251 ) -> ColorImage: 

252 """Deserialize a image from an input archive. 

253 

254 Parameters 

255 ---------- 

256 archive 

257 Archive to read from. 

258 bbox 

259 Bounding box of a subimage to read instead. 

260 **kwargs 

261 Unsupported keyword arguments are accepted only to provide 

262 better error messages (raising 

263 `.serialization.InvalidParameterError`). 

264 """ 

265 if kwargs: 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true

266 raise InvalidParameterError(f"Unrecognized parameters for ColoImage: {set(kwargs.keys())}.") 

267 r = self.red.deserialize(archive, bbox=bbox) 

268 g = self.green.deserialize(archive, bbox=bbox) 

269 b = self.blue.deserialize(archive, bbox=bbox) 

270 sky_projection = self.sky_projection.deserialize(archive) if self.sky_projection is not None else None 

271 return ColorImage.from_channels(r, g, b, sky_projection=sky_projection)._finish_deserialize(self)