From eb45b5bfb06c96c8a307410fb4453fdcb5b1695c Mon Sep 17 00:00:00 2001 From: Henrik Norman Date: Tue, 6 Sep 2022 09:51:50 +0200 Subject: [PATCH 1/5] Enabled thumbnail creation for FITS files --- docs/api/toasty.image.ImageMode.rst | 2 - toasty/builder.py | 4 +- toasty/fits_tiler.py | 3 ++ toasty/image.py | 81 ++++++++++++++++------------- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/docs/api/toasty.image.ImageMode.rst b/docs/api/toasty.image.ImageMode.rst index 2289fb1..22162d2 100644 --- a/docs/api/toasty.image.ImageMode.rst +++ b/docs/api/toasty.image.ImageMode.rst @@ -23,7 +23,6 @@ ImageMode ~ImageMode.from_array_info ~ImageMode.make_maskable_buffer - ~ImageMode.try_as_pil .. rubric:: Attributes Documentation @@ -38,4 +37,3 @@ ImageMode .. automethod:: from_array_info .. automethod:: make_maskable_buffer - .. automethod:: try_as_pil diff --git a/toasty/builder.py b/toasty/builder.py index 952e85e..ed17459 100644 --- a/toasty/builder.py +++ b/toasty/builder.py @@ -257,7 +257,9 @@ def cascade(self, **kwargs): return self def make_thumbnail_from_other(self, thumbnail_image): - thumb = thumbnail_image.make_thumbnail_bitmap() + thumb = thumbnail_image.make_thumbnail_bitmap( + self.imgset.pixel_cut_low, self.imgset.pixel_cut_high + ) with self.pio.open_metadata_for_write("thumb.jpg") as f: thumb.save(f, format="JPEG") self.imgset.thumbnail_url = "thumb.jpg" diff --git a/toasty/fits_tiler.py b/toasty/fits_tiler.py index 61d94b8..ab1ea72 100644 --- a/toasty/fits_tiler.py +++ b/toasty/fits_tiler.py @@ -167,6 +167,9 @@ def tile( else: self._tile_tan(cli_progress, parallel, **kwargs) + for img in self.coll.images(): + self.builder.make_thumbnail_from_other(img) + break self.builder.write_index_rel_wtml( add_place_for_toast=self.add_place_for_toast, ) diff --git a/toasty/image.py b/toasty/image.py index f1538c7..ee462f2 100644 --- a/toasty/image.py +++ b/toasty/image.py @@ -228,18 +228,6 @@ def make_maskable_buffer(self, buf_height, buf_width): return Image.from_array(arr) - def try_as_pil(self): - """ - Attempt to convert this mode into a PIL image mode string. - - Returns - ------- - A PIL image mode string, or None if there is no exact counterpart. - """ - if self == ImageMode.F16x3: - return None - return self.value - def _wcs_to_parity_sign(wcs): h = wcs.to_header() @@ -854,24 +842,44 @@ def asarray(self): self._array = np.asarray(self._pil) return self._array - def aspil(self): + def aspil(self, pixel_cut_low=None, pixel_cut_high=None): """Obtain the image data as :class:`PIL.Image.Image`. + Parameters + ---------- + pixel_cut_low : number or ``None`` (the default) + An optional value used to stretch the pixel values to the new range + as defined by pixel_cut_low and pixel_cut_high. Only used if the + image was not loaded as a PIL image, and mus be used together with + pixel_cut_high. To get resonable values for arrays with dtype other + than ``unit8`` this parameter is highly recommended. + pixel_cut_high : number or ``None`` (the default) + An optional value used to stretch the pixel values to the new range + as defined by pixel_cut_low and pixel_cut_high. Only used if the + image was not loaded as a PIL image, and mus be used together with + pixel_cut_low. To get resonable values for arrays with dtype other + than ``unit8`` this parameter is highly recommended. Returns ------- If the image was loaded as a PIL image, the underlying object will be returned. Otherwise the data array will be converted into a PIL image, which requires that the array have an RGB(A) format with a shape of - ``(height, width, planes)``, where ``planes`` is 3 or 4, and a dtype of - ``uint8``. + ``(height, width, planes)``, where ``planes`` is 3 or 4. """ if self._pil is not None: return self._pil - if self.mode.try_as_pil() is None: - raise Exception( - f"Toasty image with mode {self.mode} cannot be converted to PIL" - ) + if ( + pixel_cut_low is not None + and pixel_cut_high is not None + and pixel_cut_low != pixel_cut_high + ): + # TODO don't operate on potential alpha channel + array = (self._array - pixel_cut_low) / ( + pixel_cut_high - pixel_cut_low + ) * 255 + 0.5 + array = np.uint8(np.clip(array, 0, 255)) + return pil_image.fromarray(array) return pil_image.fromarray(self._array) @property @@ -1167,9 +1175,7 @@ def is_completely_masked(self): elif self.mode == ImageMode.RGBA: return np.all(i[..., 3] == 0) else: - raise Exception( - f"unhandled mode `{self.mode}` in is_completely_masked" - ) + raise Exception(f"unhandled mode `{self.mode}` in is_completely_masked") def save( self, path_or_stream, format=None, mode=None, min_value=None, max_value=None @@ -1205,7 +1211,7 @@ def save( if format in PIL_RGB_FORMATS and mode is None: mode = ImageMode.RGB if mode is not None: - pil_image = pil_image.convert(mode.try_as_pil()) + pil_image = pil_image.convert(mode.value) pil_image.save(path_or_stream, format=PIL_FORMATS[format]) elif format == "npy": np.save(path_or_stream, self.asarray()) @@ -1239,9 +1245,24 @@ def save( overwrite=True, ) - def make_thumbnail_bitmap(self): + def make_thumbnail_bitmap(self, pixel_cut_low=None, pixel_cut_high=None): """Create a thumbnail bitmap from the image. + Parameters + ---------- + pixel_cut_low : number or ``None`` (the default) + An optional value used to stretch the pixel values to the new range + as defined by pixel_cut_low and pixel_cut_high. Only used if the + image was not loaded as a PIL image, and mus be used together with + pixel_cut_high. To get resonable values for arrays with dtype other + than ``unit8`` this parameter is highly recommended. + pixel_cut_high : number or ``None`` (the default) + An optional value used to stretch the pixel values to the new range + as defined by pixel_cut_low and pixel_cut_high. Only used if the + image was not loaded as a PIL image, and mus be used together with + pixel_cut_low. To get resonable values for arrays with dtype other + than ``unit8`` this parameter is highly recommended. + Returns ------- An RGB :class:`PIL.Image.Image` representing a thumbnail of the input @@ -1249,16 +1270,6 @@ def make_thumbnail_bitmap(self): be saved in JPEG format. """ - if self.mode in ( - ImageMode.U8, - ImageMode.I16, - ImageMode.I32, - ImageMode.F32, - ImageMode.F64, - ImageMode.F16x3, - ): - raise Exception("cannot thumbnail-ify non-RGB Image") - THUMB_SHAPE = (96, 45) THUMB_ASPECT = THUMB_SHAPE[0] / THUMB_SHAPE[1] @@ -1280,7 +1291,7 @@ def make_thumbnail_bitmap(self): try: pil_image.MAX_IMAGE_PIXELS = None - thumb = self.aspil().crop(crop_box) + thumb = self.aspil(pixel_cut_low, pixel_cut_high).crop(crop_box) finally: pil_image.MAX_IMAGE_PIXELS = old_max From 1d72cfdf8e1f85b8f16f2b2e586ed4d17f6c7f16 Mon Sep 17 00:00:00 2001 From: Henrik Norman Date: Tue, 6 Sep 2022 09:55:08 +0200 Subject: [PATCH 2/5] Corrected spelling --- toasty/image.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/toasty/image.py b/toasty/image.py index ee462f2..bfca129 100644 --- a/toasty/image.py +++ b/toasty/image.py @@ -850,14 +850,14 @@ def aspil(self, pixel_cut_low=None, pixel_cut_high=None): pixel_cut_low : number or ``None`` (the default) An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and mus be used together with - pixel_cut_high. To get resonable values for arrays with dtype other + image was not loaded as a PIL image, and must be used together with + pixel_cut_high. To get reasonable values for arrays with dtype other than ``unit8`` this parameter is highly recommended. pixel_cut_high : number or ``None`` (the default) An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and mus be used together with - pixel_cut_low. To get resonable values for arrays with dtype other + image was not loaded as a PIL image, and must be used together with + pixel_cut_low. To get reasonable values for arrays with dtype other than ``unit8`` this parameter is highly recommended. Returns ------- @@ -1253,14 +1253,14 @@ def make_thumbnail_bitmap(self, pixel_cut_low=None, pixel_cut_high=None): pixel_cut_low : number or ``None`` (the default) An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and mus be used together with - pixel_cut_high. To get resonable values for arrays with dtype other + image was not loaded as a PIL image, and must be used together with + pixel_cut_high. To get reasonable values for arrays with dtype other than ``unit8`` this parameter is highly recommended. pixel_cut_high : number or ``None`` (the default) An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and mus be used together with - pixel_cut_low. To get resonable values for arrays with dtype other + image was not loaded as a PIL image, and must be used together with + pixel_cut_low. To get reasonable values for arrays with dtype other than ``unit8`` this parameter is highly recommended. Returns From 21e5275b7e5adee5a61922a4cc43934abae87542 Mon Sep 17 00:00:00 2001 From: Henrik Norman Date: Thu, 8 Sep 2022 15:00:42 +0200 Subject: [PATCH 3/5] Extracted the coercion of data into PIL format from aspil. Also let the alpha channel be intact after a pixel value stretch operation --- toasty/image.py | 76 ++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/toasty/image.py b/toasty/image.py index bfca129..d1b4376 100644 --- a/toasty/image.py +++ b/toasty/image.py @@ -842,46 +842,51 @@ def asarray(self): self._array = np.asarray(self._pil) return self._array - def aspil(self, pixel_cut_low=None, pixel_cut_high=None): + def aspil(self): """Obtain the image data as :class:`PIL.Image.Image`. - Parameters - ---------- - pixel_cut_low : number or ``None`` (the default) - An optional value used to stretch the pixel values to the new range - as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and must be used together with - pixel_cut_high. To get reasonable values for arrays with dtype other - than ``unit8`` this parameter is highly recommended. - pixel_cut_high : number or ``None`` (the default) - An optional value used to stretch the pixel values to the new range - as defined by pixel_cut_low and pixel_cut_high. Only used if the - image was not loaded as a PIL image, and must be used together with - pixel_cut_low. To get reasonable values for arrays with dtype other - than ``unit8`` this parameter is highly recommended. + Returns ------- If the image was loaded as a PIL image, the underlying object will be returned. Otherwise the data array will be converted into a PIL image, which requires that the array have an RGB(A) format with a shape of - ``(height, width, planes)``, where ``planes`` is 3 or 4. + ``(height, width, planes)``, where ``planes`` is 3 or 4, and a dtype of + ``uint8``. """ if self._pil is not None: return self._pil - if ( - pixel_cut_low is not None - and pixel_cut_high is not None - and pixel_cut_low != pixel_cut_high - ): - # TODO don't operate on potential alpha channel - array = (self._array - pixel_cut_low) / ( - pixel_cut_high - pixel_cut_low - ) * 255 + 0.5 - array = np.uint8(np.clip(array, 0, 255)) - return pil_image.fromarray(array) + if self.mode not in (ImageMode.RGB, ImageMode.RGBA): + raise Exception( + f"Toasty image with mode {self.mode} cannot be converted to PIL" + ) + return pil_image.fromarray(self._array) + def coerce_into_pil(self, pixel_cut_low, pixel_cut_high): + """Coerce the image data into a :class:`PIL.Image.Image` by converting + the data into an ``uint8`` RGB(A) array. + + Parameters + ---------- + pixel_cut_low : number + An value used to stretch the pixel values to the new ``uint8`` + range (0 - 255). + pixel_cut_high : number + An value used to stretch the pixel values to the new ``uint8`` + range (0 - 255). + """ + array = np.copy(self._array) + ( + array[..., :3](array[..., :3] - pixel_cut_low) + / (pixel_cut_high - pixel_cut_low) + * 255 + + 0.5 + ) + array = np.uint8(np.clip(array, 0, 255)) + return pil_image.fromarray(array) + @property def mode(self): return self._mode @@ -1254,14 +1259,12 @@ def make_thumbnail_bitmap(self, pixel_cut_low=None, pixel_cut_high=None): An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the image was not loaded as a PIL image, and must be used together with - pixel_cut_high. To get reasonable values for arrays with dtype other - than ``unit8`` this parameter is highly recommended. + pixel_cut_high. pixel_cut_high : number or ``None`` (the default) An optional value used to stretch the pixel values to the new range as defined by pixel_cut_low and pixel_cut_high. Only used if the image was not loaded as a PIL image, and must be used together with - pixel_cut_low. To get reasonable values for arrays with dtype other - than ``unit8`` this parameter is highly recommended. + pixel_cut_low. Returns ------- @@ -1291,7 +1294,16 @@ def make_thumbnail_bitmap(self, pixel_cut_low=None, pixel_cut_high=None): try: pil_image.MAX_IMAGE_PIXELS = None - thumb = self.aspil(pixel_cut_low, pixel_cut_high).crop(crop_box) + thumb = self.aspil().crop(crop_box) + except: + if pixel_cut_low is None or pixel_cut_high is None: + raise Exception( + ( + "Need both pixel_cut_low and pixel_cut_high parameters" + "to be able to coerce Toasty image into PIL format." + ) + ) + thumb = self.coerce_into_pil(pixel_cut_low, pixel_cut_high).crop(crop_box) finally: pil_image.MAX_IMAGE_PIXELS = old_max From 0b93144c76e8231d52aff4ef8a68aef7ee8d1a33 Mon Sep 17 00:00:00 2001 From: Henrik Norman Date: Thu, 8 Sep 2022 15:18:21 +0200 Subject: [PATCH 4/5] Fixed typo --- toasty/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toasty/image.py b/toasty/image.py index d1b4376..2ba9bb9 100644 --- a/toasty/image.py +++ b/toasty/image.py @@ -879,7 +879,7 @@ def coerce_into_pil(self, pixel_cut_low, pixel_cut_high): """ array = np.copy(self._array) ( - array[..., :3](array[..., :3] - pixel_cut_low) + array[..., :3] = (array[..., :3] - pixel_cut_low) / (pixel_cut_high - pixel_cut_low) * 255 + 0.5 From 0284e8e9685ecb3da7837d177ecacf960a148754 Mon Sep 17 00:00:00 2001 From: Henrik Norman Date: Thu, 8 Sep 2022 15:36:24 +0200 Subject: [PATCH 5/5] Fixed incorrect row breaks --- toasty/image.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/toasty/image.py b/toasty/image.py index 2ba9bb9..f709a69 100644 --- a/toasty/image.py +++ b/toasty/image.py @@ -878,12 +878,10 @@ def coerce_into_pil(self, pixel_cut_low, pixel_cut_high): range (0 - 255). """ array = np.copy(self._array) - ( - array[..., :3] = (array[..., :3] - pixel_cut_low) - / (pixel_cut_high - pixel_cut_low) - * 255 - + 0.5 - ) + array[..., :3] = (array[..., :3] - pixel_cut_low) / ( + pixel_cut_high - pixel_cut_low + ) * 255 + 0.5 + array = np.uint8(np.clip(array, 0, 255)) return pil_image.fromarray(array)