22from __future__
import annotations
26 "PrettyPictureConnections",
27 "PrettyPictureConfig",
29 "PrettyMosaicConnections",
31 "PrettyPictureBackgroundFixerConfig",
32 "PrettyPictureBackgroundFixerTask",
33 "PrettyPictureStarFixerConfig",
34 "PrettyPictureStarFixerTask",
38from lsst.afw.image
import ExposureF
40from typing
import TYPE_CHECKING, cast, Any
43from scipy.stats
import norm
44from scipy.ndimage
import binary_dilation
45from scipy.optimize
import minimize
46from scipy.interpolate
import RBFInterpolator
47from skimage.restoration
import inpaint_biharmonic
49from lsst.daf.butler
import Butler, DeferredDatasetHandle
50from lsst.daf.butler
import DatasetRef
51from lsst.pex.config import Field, Config, ConfigDictField, ConfigField, ListField, ChoiceField
55 PipelineTaskConnections,
57 InMemoryDatasetHandle,
61from lsst.pipe.base.connectionTypes
import Input, Output
62from lsst.geom import Box2I, Point2I, Extent2I
63from lsst.afw.image
import Exposure, Mask
65from ._plugins
import plugins
66from ._colorMapper
import lsstRGB
72 from numpy.typing
import NDArray
73 from lsst.pipe.base import QuantumContext, InputQuantizedConnection, OutputQuantizedConnection
78 PipelineTaskConnections,
79 dimensions={
"tract",
"patch",
"skymap"},
80 defaultTemplates={
"coaddTypeName":
"deep"},
84 "Model of the static sky, used to find temporal artifacts. Typically a PSF-Matched, "
85 "sigma-clipped coadd. Written if and only if assembleStaticSkyModel.doWrite=True"
87 name=
"{coaddTypeName}CoaddPsfMatched",
88 storageClass=
"ExposureF",
89 dimensions=(
"tract",
"patch",
"skymap",
"band"),
94 doc=
"A RGB image created from the input data stored as a 3d array",
95 name=
"rgb_picture_array",
96 storageClass=
"NumpyArray",
97 dimensions=(
"tract",
"patch",
"skymap"),
100 outputRGBMask = Output(
101 doc=
"A Mask corresponding to the fused masks of the input channels",
102 name=
"rgb_picture_mask",
104 dimensions=(
"tract",
"patch",
"skymap"),
108class ChannelRGBConfig(
Config):
109 """This describes the rgb values of a given input channel.
111 For instance if this channel is red the values would be self.r = 1,
112 self.g = 0, self.b = 0. If the channel was cyan the values would be
113 self.r = 0, self.g = 1, self.b = 1.
116 r = Field[float](doc=
"The amount of red contained in this channel")
117 g = Field[float](doc=
"The amount of green contained in this channel")
118 b = Field[float](doc=
"The amount of blue contained in this channel")
122 """Configurations to control how luminance is mapped in the rgb code"""
124 stretch = Field[float](doc=
"The stretch of the luminance in asinh", default=400)
125 max = Field[float](doc=
"The maximum allowed luminance on a 0 to 100 scale", default=85)
126 floor = Field[float](doc=
"A scaling factor to apply to the luminance before asinh scaling", default=0.0)
127 Q = Field[float](doc=
"softening parameter", default=0.7)
128 highlight = Field[float](
129 doc=
"The value of highlights in scaling factor applied to post asinh streaching", default=1.0
131 shadow = Field[float](
132 doc=
"The value of shadows in scaling factor applied to post asinh streaching", default=0.0
134 midtone = Field[float](
135 doc=
"The value of midtone in scaling factor applied to post asinh streaching", default=0.5
139class LocalContrastConfig(
Config):
140 """Configuration to control local contrast enhancement of the luminance
143 doLocalContrast = Field[bool](
144 doc=
"Apply local contrast enhancements to the luminance channel", default=
True
146 highlights = Field[float](doc=
"Adjustment factor for the highlights", default=-0.9)
147 shadows = Field[float](doc=
"Adjustment factor for the shadows", default=0.5)
148 clarity = Field[float](doc=
"Amount of clarity to apply to contrast modification", default=0.1)
149 sigma = Field[float](
150 doc=
"The scale size of what is considered local in the contrast enhancement", default=30
152 maxLevel = Field[int](
153 doc=
"The maximum number of scales the contrast should be enhanced over, if None then all",
159class ScaleColorConfig(
Config):
160 """Controls color scaling in the RGB generation process."""
162 saturation = Field[float](
164 "The overall saturation factor with the scaled luminance between zero and one. "
165 "A value of one is not recommended as it makes bright pixels very saturated"
169 maxChroma = Field[float](
171 "The maximum chromaticity in the CIELCh color space, large "
172 "values will cause bright pixels to fall outside the RGB gamut."
178class RemapBoundsConfig(
Config):
179 """Remaps input images to a known range of values.
181 Often input images are not mapped to any defined range of values
182 (for instance if they are in count units). This controls how the units of
183 and image are mapped to a zero to one range by determining an upper
187 quant = Field[float](
189 "The maximum values of each of the three channels will be multiplied by this factor to "
190 "determine the maximum flux of the image, values larger than this quantity will be clipped."
194 absMax = Field[float](
195 doc=
"Instead of determining the maximum value from the image, use this fixed value instead",
201class PrettyPictureConfig(PipelineTaskConfig, pipelineConnections=PrettyPictureConnections):
203 doc=
"A dictionary that maps band names to their rgb channel configurations",
205 itemtype=ChannelRGBConfig,
208 imageRemappingConfig = ConfigField[RemapBoundsConfig](
209 doc=
"Configuration controlling channel normalization process"
211 luminanceConfig = ConfigField[LumConfig](
212 doc=
"Configuration for the luminance scaling when making an RGB image"
214 localContrastConfig = ConfigField[LocalContrastConfig](
215 doc=
"Configuration controlling the local contrast correction in RGB image production"
217 colorConfig = ConfigField[ScaleColorConfig](
218 doc=
"Configuration to control the color scaling process in RGB image production"
220 cieWhitePoint = ListField[float](
221 doc=
"The white point of the input arrays in ciexz coordinates", maxLength=2, default=[0.28, 0.28]
223 arrayType = ChoiceField[str](
224 doc=
"The dataset type for the output image array",
227 "uint8":
"Use 8 bit arrays, 255 max",
228 "uint16":
"Use 16 bit arrays, 65535 max",
229 "half":
"Use 16 bit float arrays, 1 max",
230 "float":
"Use 32 bit float arrays, 1 max",
233 doPSFDeconcovlve = Field[bool](
234 doc=
"Use the PSF in a richardson lucy deconvolution on the luminance channel.", default=
True
236 exposureBrackets = ListField[float](
238 "Exposure scaling factors used in creating multiple exposures with different scalings which will "
239 "then be fused into a final image"
242 default=[1.25, 1, 0.75],
244 doRemapGamut = Field[bool](
245 doc=
"Apply a color correction to unrepresentable colors, if false they will clip", default=
True
247 gamutMethod = ChoiceField[str](
248 doc=
"If doRemapGamut is True this determines the method",
251 "mapping":
"Use a mapping function",
252 "inpaint":
"Use surrounding pixels to determine likely value",
256 def setDefaults(self):
257 self.channelConfig[
"i"] = ChannelRGBConfig(r=1, g=0, b=0)
258 self.channelConfig[
"r"] = ChannelRGBConfig(r=0, g=1, b=0)
259 self.channelConfig[
"g"] = ChannelRGBConfig(r=0, g=0, b=1)
260 return super().setDefaults()
263class PrettyPictureTask(PipelineTask):
264 """Turns inputs into an RGB image."""
266 _DefaultName =
"prettyPictureTask"
267 ConfigClass = PrettyPictureConfig
271 def run(self, images: Mapping[str, Exposure]) -> Struct:
272 """Turns the input arguments in arguments into an RGB array.
276 images : `Mapping` of `str` to `Exposure`
277 A mapping of input images and the band they correspond to.
282 A struct with the corresponding RGB image, and mask used in
283 RGB image construction. The struct will have the attributes
284 outputRGBImage and outputRGBMask. Each of the outputs will
285 be a `NDarray` object.
289 Construction of input images are made easier by use of the
290 makeInputsFrom* methods.
294 jointMask:
None | NDArray =
None
295 maskDict: Mapping[str, int] = {}
296 doJointMaskInit =
False
297 if jointMask
is None:
299 doJointMaskInit =
True
300 for channel, imageExposure
in images.items():
301 imageArray = imageExposure.image.array
303 for plug
in plugins.channel():
305 imageArray, imageExposure.mask.array, imageExposure.mask.getMaskPlaneDict(), self.config
307 channels[channel] = imageArray
310 shape = imageArray.shape
311 maskDict = imageExposure.mask.getMaskPlaneDict()
313 jointMask = np.zeros(shape, dtype=imageExposure.mask.dtype)
314 doJointMaskInit =
False
316 jointMask |= imageExposure.mask.array
319 imageRArray = np.zeros(shape, dtype=np.float32)
320 imageGArray = np.zeros(shape, dtype=np.float32)
321 imageBArray = np.zeros(shape, dtype=np.float32)
323 for band, image
in channels.items():
324 mix = self.config.channelConfig[band]
326 imageRArray += mix.r * image
328 imageGArray += mix.g * image
330 imageBArray += mix.b * image
332 exposure = next(iter(images.values()))
333 box: Box2I = exposure.getBBox()
334 boxCenter = box.getCenter()
336 psf = exposure.psf.computeImage(boxCenter).array
341 assert jointMask
is not None
343 colorImage = np.zeros((*imageRArray.shape, 3))
344 colorImage[:, :, 0] = imageRArray
345 colorImage[:, :, 1] = imageGArray
346 colorImage[:, :, 2] = imageBArray
347 for plug
in plugins.partial():
348 colorImage = plug(colorImage, jointMask, maskDict, self.config)
356 scaleLumKWargs=self.config.luminanceConfig.toDict(),
357 remapBoundsKwargs=self.config.imageRemappingConfig.toDict(),
358 scaleColorKWargs=self.config.colorConfig.toDict(),
359 **(self.config.localContrastConfig.toDict()),
360 cieWhitePoint=tuple(self.config.cieWhitePoint),
361 psf=psf
if self.config.doPSFDeconcovlve
else None,
362 brackets=list(self.config.exposureBrackets)
if self.config.exposureBrackets
else None,
363 doRemapGamut=self.config.doRemapGamut,
364 gamutMethod=self.config.gamutMethod,
369 match self.config.arrayType:
383 assert True,
"This code path should be unreachable"
389 lsstMask =
Mask(width=jointMask.shape[1], height=jointMask.shape[0], planeDefs=maskDict)
390 lsstMask.array = jointMask
391 return Struct(outputRGB=colorImage.astype(dtype), outputRGBMask=lsstMask)
395 butlerQC: QuantumContext,
396 inputRefs: InputQuantizedConnection,
397 outputRefs: OutputQuantizedConnection,
399 imageRefs: list[DatasetRef] = inputRefs.inputCoadds
400 sortedImages = self.makeInputsFromRefs(imageRefs, butlerQC)
401 outputs = self.run(sortedImages)
402 butlerQC.put(outputs, outputRefs)
404 def makeInputsFromRefs(
405 self, refs: Iterable[DatasetRef], butler: Butler | QuantumContext
406 ) -> dict[str, Exposure]:
407 r"""Make valid inputs for the run method from butler references.
411 refs : `Iterable` of `DatasetRef`
412 Some `Iterable` container of `Butler` `DatasetRef`\ s
413 butler : `Butler` or `QuantumContext`
414 This is the object that fetches the input data.
418 sortedImages : `dict` of `str` to `Exposure`
419 A dictionary of `Exposure`\ s that keyed by the band they
422 sortedImages: dict[str, Exposure] = {}
424 key: str = cast(str, ref.dataId[
"band"])
425 image = butler.get(ref)
426 sortedImages[key] = image
429 def makeInputsFromArrays(self, **kwargs) -> dict[int, DeferredDatasetHandle]:
430 r"""Make valid inputs for the run method from numpy arrays.
435 This is standard python kwargs where the left side of the equals
436 is the data band, and the right side is the corresponding `NDArray`
441 sortedImages : `dict` of `str` to `Exposure`
442 A dictionary of `Exposure`\ s that keyed by the band they
447 for key, array
in kwargs.items():
449 temp[key].image.array[:] = array
451 return self.makeInputsFromExposures(**temp)
453 def makeInputsFromExposures(self, **kwargs) -> dict[int, DeferredDatasetHandle]:
454 r"""Make valid inputs for the run method from `Exposure` objects.
459 This is standard python kwargs where the left side of the equals
460 is the data band, and the right side is the corresponding
465 sortedImages : `dict` of `str` to `Exposure`
466 A dictionary of `Exposure`\ s that keyed by the band they
470 for key, value
in kwargs.items():
471 sortedImages[key] = value
475class PrettyPictureBackgroundFixerConnections(
476 PipelineTaskConnections,
477 dimensions=(
"tract",
"patch",
"skymap",
"band"),
478 defaultTemplates={
"coaddTypeName":
"deep"},
481 doc=(
"Input coadd for which the background is to be removed"),
482 name=
"{coaddTypeName}CoaddPsfMatched",
483 storageClass=
"ExposureF",
484 dimensions=(
"tract",
"patch",
"skymap",
"band"),
486 outputCoadd = Output(
487 doc=
"The coadd with the background fixed and subtracted",
488 name=
"pretty_picture_coadd_bg_subtracted",
489 storageClass=
"ExposureF",
490 dimensions=(
"tract",
"patch",
"skymap",
"band"),
494class PrettyPictureBackgroundFixerConfig(
495 PipelineTaskConfig, pipelineConnections=PrettyPictureBackgroundFixerConnections
500class PrettyPictureBackgroundFixerTask(PipelineTask):
501 """Empirically flatten an images background.
503 Many astrophysical images have backgrounds with imperfections in them.
504 This Task attempts to determine control points which are considered
505 background values, and fits a radial basis function model to those
506 points. This model is then subtracted off the image.
510 _DefaultName =
"prettyPictureBackgroundFixerTask"
511 ConfigClass = PrettyPictureBackgroundFixerConfig
515 def _neg_log_likelihood(self, params, x):
516 """Calculate the negative log-likelihood for a Gaussian distribution.
518 This function computes the negative log-likelihood of a set of data `x`
519 given a Gaussian distribution with parameters `mu` and `sigma`. It's
520 designed to be used as the objective function for a minimization routine
521 to find the best-fit Gaussian parameters.
526 A tuple containing the mean (`mu`) and standard deviation (`sigma`)
527 of the Gaussian distribution.
529 The data samples for which to calculate the log-likelihood.
534 The negative log-likelihood of the data given the Gaussian parameters.
535 Returns infinity if sigma is non-positive or if the mean is less than
536 the maximum value in x (to enforce the constraint that the Gaussian
537 only models the lower tail of the distribution).
546 term = np.log(2) - np.log(sigma) + norm.logpdf(z)
547 loglikelihood = np.sum(term)
548 return -loglikelihood
550 def _tile_slices(self, arr, R, C):
551 """Generate slices for tiling an array.
553 This function divides an array into a grid of tiles and returns a list of
554 slice objects representing each tile. It handles cases where the array
555 dimensions are not evenly divisible by the number of tiles in each
556 dimension, distributing the remainder among the tiles.
561 The input array to be tiled. Used only to determine the array's shape.
563 The number of tiles in the row dimension.
565 The number of tiles in the column dimension.
569 slices : `list` of `tuple`
570 A list of tuples, where each tuple contains two `slice` objects
571 representing the row and column slices for a single tile.
577 def get_slices(total_size, num_divisions):
578 base = total_size // num_divisions
579 remainder = total_size % num_divisions
582 for i
in range(num_divisions):
586 slices.append((start, end))
591 row_slices = get_slices(M, R)
592 col_slices = get_slices(N, C)
596 for rs
in row_slices:
598 for cs
in col_slices:
600 tile_slice = (slice(r_start, r_end), slice(c_start, c_end))
601 tiles.append(tile_slice)
605 def fixBackground(self, image):
606 """Estimate and subtract the background from an image.
608 This function estimates the background level in an image using a median-based
609 approach combined with Gaussian fitting and radial basis function interpolation.
610 It aims to provide a more accurate background estimation than a simple median
611 filter, especially in images with varying background levels.
616 The input image as a NumPy array.
621 An array representing the estimated background level across the image.
626 maxLikely = np.median(image, axis=
None)
631 mask = image < maxLikely
632 initial_std = (image[mask] - maxLikely).
std()
639 self._neg_log_likelihood,
640 (maxLikely, initial_std),
642 bounds=((maxLikely,
None), (1e-8,
None)),
644 mu_hat, sigma_hat = result.x
646 mu_hat, sigma_hat = (maxLikely, 2 * initial_std)
650 threshhold = mu_hat + sigma_hat
651 image_mask = image < threshhold
654 tiles = self._tile_slices(image, 25, 25)
662 for xslice, yslice
in tiles:
663 ypos = (yslice.stop - yslice.start) / 2 + yslice.start
664 xpos = (xslice.stop - xslice.start) / 2 + xslice.start
667 window = image[yslice, xslice][image_mask[yslice, xslice]]
669 value = np.median(window)
674 positions = np.meshgrid(np.arange(image.shape[0]), np.arange(image.shape[1]))
676 inter = RBFInterpolator(
677 np.vstack((yloc, xloc)).T, values, kernel=
"thin_plate_spline", degree=4, smoothing=0.05
679 backgrounds = inter(np.array(positions)[::-1].reshape(2, -1).T).reshape(image.shape)
683 def run(self, inputCoadd: Exposure):
684 """Estimate a background for an input Exposure and remove it.
688 inputCoadd : `Exposure`
689 The exposure the background will be removed from.
694 A `Struct` that contains the exposure with the background removed.
695 This `Struct` will have an attribute named ``outputCoadd``.
698 background = self.fixBackground(inputCoadd.image.array)
700 output = ExposureF(inputCoadd, deep=
True)
701 output.image.array -= background
702 return Struct(outputCoadd=output)
705class PrettyPictureStarFixerConnections(
706 PipelineTaskConnections,
707 dimensions=(
"tract",
"patch",
"skymap"),
710 doc=(
"Input coadd for which the background is to be removed"),
711 name=
"pretty_picture_coadd_bg_subtracted",
712 storageClass=
"ExposureF",
713 dimensions=(
"tract",
"patch",
"skymap",
"band"),
716 outputCoadd = Output(
717 doc=
"The coadd with the background fixed and subtracted",
718 name=
"pretty_picture_coadd_fixed_stars",
719 storageClass=
"ExposureF",
720 dimensions=(
"tract",
"patch",
"skymap",
"band"),
725class PrettyPictureStarFixerConfig(PipelineTaskConfig, pipelineConnections=PrettyPictureStarFixerConnections):
726 brightnessThresh = Field[float](
727 doc=
"The flux value below which pixels with SAT or NO_DATA bits will be ignored"
731class PrettyPictureStarFixerTask(PipelineTask):
732 """This class fixes up regions in an image where there is no, or bad data.
734 The fixes done by this task are overwhelmingly comprised of the cores of
735 bright stars for which there is no data.
738 _DefaultName =
"prettyPictureStarFixerTask"
739 ConfigClass = PrettyPictureStarFixerConfig
743 def run(self, inputs: Mapping[str, ExposureF]) -> Struct:
744 """Fix areas in an image where this is no data, most likely to be
745 the cores of bright stars.
747 Because we want to have consistent fixes accross bands, this method
748 relies on supplying all bands and fixing pixels that are marked
749 as having a defect in any band even if within one band there is
754 inputs : `Mapping` of `str` to `ExposureF`
755 This mapping has keys of band as a `str` and the corresponding
756 ExposureF as a value.
760 results : `Struct` of `Mapping` of `str` to `ExposureF`
761 A `Struct` that has a mapping of band to `ExposureF`. The `Struct`
762 has an attribute named ``results``.
766 doJointMaskInit =
True
767 for imageExposure
in inputs.values():
768 maskDict = imageExposure.mask.getMaskPlaneDict()
770 jointMask = np.zeros(imageExposure.mask.array.shape, dtype=imageExposure.mask.array.dtype)
771 doJointMaskInit =
False
772 jointMask |= imageExposure.mask.array
774 sat_bit = maskDict[
"SAT"]
775 no_data_bit = maskDict[
"NO_DATA"]
776 together = (jointMask & 2**sat_bit).astype(bool) | (jointMask & 2**no_data_bit).astype(bool)
779 bright_mask = imageExposure.image.array > self.config.brightnessThresh
784 both = together & bright_mask
785 struct = np.array(((0, 1, 0), (1, 1, 1), (0, 1, 0)), dtype=bool)
786 both = binary_dilation(both, struct, iterations=4).astype(bool)
790 for band, imageExposure
in inputs.items():
792 inpainted = inpaint_biharmonic(imageExposure.image.array, both, split_into_regions=
True)
793 imageExposure.image.array[both] = inpainted[both]
794 results[band] = imageExposure
795 return Struct(results=results)
799 butlerQC: QuantumContext,
800 inputRefs: InputQuantizedConnection,
801 outputRefs: OutputQuantizedConnection,
803 refs = inputRefs.inputCoadd
804 sortedImages: dict[str, Exposure] = {}
806 key: str = cast(str, ref.dataId[
"band"])
807 image = butlerQC.get(ref)
808 sortedImages[key] = image
810 outputs = self.run(sortedImages).results
812 for ref
in outputRefs.outputCoadd:
813 sortedOutputs[ref.dataId[
"band"]] = ref
815 for band, data
in outputs.items():
816 butlerQC.put(data, sortedOutputs[band])
819class PrettyMosaicConnections(PipelineTaskConnections, dimensions=(
"tract",
"skymap")):
821 doc=
"Individual RGB images that are to go into the mosaic",
822 name=
"rgb_picture_array",
823 storageClass=
"NumpyArray",
824 dimensions=(
"tract",
"patch",
"skymap"),
830 doc=
"The skymap which the data has been mapped onto",
831 storageClass=
"SkyMap",
832 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
833 dimensions=(
"skymap",),
836 inputRGBMask = Input(
837 doc=
"Individual RGB images that are to go into the mosaic",
838 name=
"rgb_picture_mask",
840 dimensions=(
"tract",
"patch",
"skymap"),
845 outputRGBMosaic = Output(
846 doc=
"A RGB mosaic created from the input data stored as a 3d array",
847 name=
"rgb_mosaic_array",
848 storageClass=
"NumpyArray",
849 dimensions=(
"tract",
"skymap"),
853class PrettyMosaicConfig(PipelineTaskConfig, pipelineConnections=PrettyMosaicConnections):
854 binFactor = Field[int](doc=
"The factor to bin by when producing the mosaic")
857class PrettyMosaicTask(PipelineTask):
858 """Combines multiple RGB arrays into one mosaic."""
860 _DefaultName =
"prettyMosaicTask"
861 ConfigClass = PrettyMosaicConfig
867 inputRGB: Iterable[DeferredDatasetHandle],
869 inputRGBMask: Iterable[DeferredDatasetHandle],
871 r"""Assemble individual `NDArrays` into a mosaic.
873 Each input is a `DeferredDatasetHandle` because they're loaded in one
874 at a time to be placed into the mosaic to save memory.
878 inputRGB : `Iterable` of `DeferredDatasetHandle`
879 `DeferredDatasetHandle`\ s pointing to RGB `NDArrays`.
880 skyMap : `BaseSkyMap`
881 The skymap that defines the relative position of each of the input
883 inputRGBMask : `Iterable` of `DeferredDatasetHandle`
884 `DeferredDatasetHandle`\ s pointing to masks for each of the
885 corresponding images.
890 The `Struct` containing the combined mosaic. The `Struct` has
891 and attribute named ``outputRGBMosaic``.
898 for handle
in inputRGB:
899 dataId = handle.dataId
900 tractInfo: TractInfo = skyMap[dataId[
"tract"]]
901 patchInfo: PatchInfo = tractInfo[dataId[
"patch"]]
902 bbox = patchInfo.getOuterBBox()
905 tractMaps.append(tractInfo)
910 origin = newBox.getBegin()
911 for iterBox
in boxes:
912 localOrigin = iterBox.getBegin() - origin
914 x=int(np.floor(localOrigin.x / self.config.binFactor)),
915 y=int(np.floor(localOrigin.y / self.config.binFactor)),
918 x=int(np.floor(iterBox.getWidth() / self.config.binFactor)),
919 y=int(np.floor(iterBox.getHeight() / self.config.binFactor)),
921 tmpBox =
Box2I(localOrigin, localExtent)
922 modifiedBoxes.append(tmpBox)
923 boxes = modifiedBoxes
928 x=int(np.floor(newBox.getWidth() / self.config.binFactor)),
929 y=int(np.floor(newBox.getHeight() / self.config.binFactor)),
931 newBox =
Box2I(newBoxOrigin, newBoxExtent)
934 self.imageHandle = tempfile.NamedTemporaryFile()
935 self.maskHandle = tempfile.NamedTemporaryFile()
936 consolidatedImage =
None
937 consolidatedMask =
None
942 for box, handle, handleMask, tractInfo
in zip(boxes, inputRGB, inputRGBMask, tractMaps):
944 rgbMask = handleMask.get()
945 maskDict = rgbMask.getMaskPlaneDict()
947 if consolidatedImage
is None:
948 consolidatedImage = np.memmap(
949 self.imageHandle.name,
951 shape=(newBox.getHeight(), newBox.getWidth(), 3),
954 if consolidatedMask
is None:
955 consolidatedMask = np.memmap(
956 self.maskHandle.name,
958 shape=(newBox.getHeight(), newBox.getWidth()),
959 dtype=rgbMask.array.dtype,
962 if self.config.binFactor > 1:
964 shape = tuple(box.getDimensions())[::-1]
969 fx=shape[0] / self.config.binFactor,
970 fy=shape[1] / self.config.binFactor,
972 rgbMask = cv2.resize(
973 rgbMask.array.astype(np.float32),
976 fx=shape[0] / self.config.binFactor,
977 fy=shape[1] / self.config.binFactor,
979 existing = ~np.all(consolidatedImage[*box.slices] == 0, axis=2)
980 if tmpImg
is None or tmpImg.shape != rgb.shape:
981 ramp = np.linspace(0, 1, tractInfo.patch_border * 2)
982 tmpImg = np.zeros(rgb.shape[:2])
983 tmpImg[: tractInfo.patch_border * 2, :] = np.repeat(
984 np.expand_dims(ramp, 1), tmpImg.shape[1], axis=1
987 tmpImg[-1 * tractInfo.patch_border * 2 :, :] = np.repeat(
988 np.expand_dims(1 - ramp, 1), tmpImg.shape[1], axis=1
990 tmpImg[:, : tractInfo.patch_border * 2] = np.repeat(
991 np.expand_dims(ramp, 0), tmpImg.shape[0], axis=0
994 tmpImg[:, -1 * tractInfo.patch_border * 2 :] = np.repeat(
995 np.expand_dims(1 - ramp, 0), tmpImg.shape[0], axis=0
997 tmpImg = np.repeat(np.expand_dims(tmpImg, 2), 3, axis=2)
999 consolidatedImage[*box.slices][~existing, :] = rgb[~existing, :]
1000 consolidatedImage[*box.slices][existing, :] = (
1001 tmpImg[existing] * rgb[existing]
1002 + (1 - tmpImg[existing]) * consolidatedImage[*box.slices][existing, :]
1005 tmpMask = np.zeros_like(rgbMask.array)
1006 tmpMask[existing] = np.bitwise_or(
1007 rgbMask.array[existing], consolidatedMask[*box.slices][existing]
1009 tmpMask[~existing] = rgbMask.array[~existing]
1010 consolidatedMask[*box.slices] = tmpMask
1012 for plugin
in plugins.full():
1013 if consolidatedImage
is not None and consolidatedMask
is not None:
1014 consolidatedImage = plugin(consolidatedImage, consolidatedMask, maskDict)
1017 if consolidatedImage
is None:
1018 consolidatedImage = np.zeros((0, 0, 0), dtype=np.uint8)
1020 return Struct(outputRGBMosaic=consolidatedImage)
1024 butlerQC: QuantumContext,
1025 inputRefs: InputQuantizedConnection,
1026 outputRefs: OutputQuantizedConnection,
1028 inputs = butlerQC.get(inputRefs)
1029 outputs = self.run(**inputs)
1030 butlerQC.put(outputs, outputRefs)
1031 if hasattr(self,
"imageHandle"):
1032 self.imageHandle.close()
1033 if hasattr(self,
"maskHandle"):
1034 self.maskHandle.close()
1036 def makeInputsFromArrays(
1037 self, inputs: Iterable[tuple[Mapping[str, Any], NDArray]]
1038 ) -> Iterable[DeferredDatasetHandle]:
1039 r"""Make valid inputs for the run method from numpy arrays.
1043 inputs : `Iterable` of `tuple` of `Mapping` and `NDArray`
1044 An iterable where each element is a tuble with the first
1045 element is a mapping that corresponds to an arrays dataId,
1046 and the second is an `NDArray`.
1050 sortedImages : `dict` of `str` to `Exposure`
1051 A dictionary of `Exposure`\ s that keyed by the band they
1054 structuredInputs = []
1055 for dataId, array
in inputs:
1056 structuredInputs.append(InMemoryDatasetHandle(inMemoryDataset=array, **dataId))
1058 return structuredInputs
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Represent a 2-dimensional array of bitmask pixels.
An integer coordinate rectangle.
NDArray lsstRGB(NDArray rArray, NDArray gArray, NDArray bArray, bool doLocalContrast=True, Callable[..., NDArray]|None scaleLum=latLum, Mapping|None scaleLumKWargs=None, Callable[..., tuple[NDArray, NDArray]]|None scaleColor=colorConstantSat, Mapping|None scaleColorKWargs=None, Callable|None remapBounds=mapUpperBounds, Mapping|None remapBoundsKwargs=None, tuple[float, float] cieWhitePoint=(0.28, 0.28), float sigma=30, float highlights=-0.9, float shadows=0.5, float clarity=0.1, int|None maxLevel=None, NDArray|None psf=None, list[float]|None brackets=None, bool doRemapGamut=True, Literal["mapping", "inpaint"] gamutMethod="inpaint")