From 0e0741d323fab9eff3096af8cdc41e65a4d8a91b Mon Sep 17 00:00:00 2001 From: Martin Laasmaa Date: Tue, 16 Dec 2025 12:37:34 +0200 Subject: [PATCH] Update on convert_grayscale_to_rgb_preserve_range, making it class method --- src/gui/tabs/training_tab.py | 4 +-- src/utils/image.py | 69 +++++++++++++++++------------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/gui/tabs/training_tab.py b/src/gui/tabs/training_tab.py index 5d86fe4..5200eb9 100644 --- a/src/gui/tabs/training_tab.py +++ b/src/gui/tabs/training_tab.py @@ -34,7 +34,7 @@ from PySide6.QtWidgets import ( from src.database.db_manager import DatabaseManager from src.model.yolo_wrapper import YOLOWrapper from src.utils.config_manager import ConfigManager -from src.utils.image import Image, convert_grayscale_to_rgb_preserve_range +from src.utils.image import Image from src.utils.logger import get_logger @@ -1368,7 +1368,7 @@ class TrainingTab(QWidget): img_obj = Image(src) pil_img = img_obj.pil_image if len(pil_img.getbands()) == 1: - rgb_img = convert_grayscale_to_rgb_preserve_range(pil_img) + rgb_img = img_obj.convert_grayscale_to_rgb_preserve_range() else: rgb_img = pil_img.convert("RGB") rgb_img.save(dst) diff --git a/src/utils/image.py b/src/utils/image.py index 69139cd..acc5f11 100644 --- a/src/utils/image.py +++ b/src/utils/image.py @@ -277,6 +277,38 @@ class Image: """ return self._channels >= 3 + def convert_grayscale_to_rgb_preserve_range( + self, + ) -> PILImage.Image: + """Convert a single-channel PIL image to RGB while preserving dynamic range. + + Returns: + PIL Image in RGB mode with intensities normalized to 0-255. + """ + if self._channels == 3: + return self.pil_image + + grayscale = self.data + if grayscale.ndim == 3: + grayscale = grayscale[:, :, 0] + + original_dtype = grayscale.dtype + grayscale = grayscale.astype(np.float32) + + if grayscale.size == 0: + return PILImage.new("RGB", self.shape, color=(0, 0, 0)) + + if np.issubdtype(original_dtype, np.integer): + denom = float(max(np.iinfo(original_dtype).max, 1)) + else: + max_val = float(grayscale.max()) + denom = max(max_val, 1.0) + + grayscale = np.clip(grayscale / denom, 0.0, 1.0) + grayscale_u8 = (grayscale * 255.0).round().astype(np.uint8) + rgb_arr = np.repeat(grayscale_u8[:, :, None], 3, axis=2) + return PILImage.fromarray(rgb_arr, mode="RGB") + def __repr__(self) -> str: """String representation of the Image object.""" return ( @@ -289,40 +321,3 @@ class Image: def __str__(self) -> str: """String representation of the Image object.""" return self.__repr__() - - -def convert_grayscale_to_rgb_preserve_range( - pil_image: PILImage.Image, -) -> PILImage.Image: - """Convert a single-channel PIL image to RGB while preserving dynamic range. - - Args: - pil_image: Single-channel PIL image (e.g., 16-bit grayscale). - - Returns: - PIL Image in RGB mode with intensities normalized to 0-255. - """ - - if pil_image.mode == "RGB": - return pil_image - - grayscale = np.array(pil_image) - if grayscale.ndim == 3: - grayscale = grayscale[:, :, 0] - - original_dtype = grayscale.dtype - grayscale = grayscale.astype(np.float32) - - if grayscale.size == 0: - return PILImage.new("RGB", pil_image.size, color=(0, 0, 0)) - - if np.issubdtype(original_dtype, np.integer): - denom = float(max(np.iinfo(original_dtype).max, 1)) - else: - max_val = float(grayscale.max()) - denom = max(max_val, 1.0) - - grayscale = np.clip(grayscale / denom, 0.0, 1.0) - grayscale_u8 = (grayscale * 255.0).round().astype(np.uint8) - rgb_arr = np.repeat(grayscale_u8[:, :, None], 3, axis=2) - return PILImage.fromarray(rgb_arr, mode="RGB")