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..f709a69 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() @@ -857,6 +845,7 @@ def asarray(self): def aspil(self): """Obtain the image data as :class:`PIL.Image.Image`. + Returns ------- If the image was loaded as a PIL image, the underlying object will be @@ -868,12 +857,34 @@ def aspil(self): """ if self._pil is not None: return self._pil - if self.mode.try_as_pil() is None: + 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 @@ -1167,9 +1178,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 +1214,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 +1248,22 @@ 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 must be used together with + 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. + Returns ------- An RGB :class:`PIL.Image.Image` representing a thumbnail of the input @@ -1249,16 +1271,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] @@ -1281,6 +1293,15 @@ def make_thumbnail_bitmap(self): try: pil_image.MAX_IMAGE_PIXELS = None 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