Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ show_box_background=True,

```show_box_background (bool)```: Add black background with 50% transparency to the name and scalebar box. *Default: True*

```in_physical_units (bool)```: If True, scale_length is interpreted as physical length in the same units as pixel_size. If False, scale_length is a fraction of image width. *Default: False*


### 2. Process Image
Pass a single image or a list of images to be processed:
Expand Down
10 changes: 5 additions & 5 deletions examples/example-use.ipynb

Large diffs are not rendered by default.

Binary file modified examples/out.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 19 additions & 16 deletions lutipy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ class LUTiPy:
"""Main interface for processing and visualizing images using LUTs."""

def __init__(self, rgb: tuple = (255, 0, 255), channel_names: list = None, layout: str = 'grid', scale_length: float = 0.25, pixel_size: str = None, name_position="top-left", show_box_background=True,
scalebar=False, scalebar_position="bottom-left"):
scalebar=False, scalebar_position="bottom-left", in_physical_units=False):
"""Initialize the LUTIPy object.

Args:
rgb (tuple): Base RGB color for LUT generation.
channel_names (list): Names for each channel in the image.
layout (str): Layout for the panel ('grid' or 'horizontal').
scale_length (float): Scale bar length as a fraction of image width.
scale_length (float): Scale bar length. If in_physical_units is False, this is a fraction of image width.
If in_physical_units is True, this is the physical length in the same units as pixel_size.
pixel_size (str): Physical size of a pixel (e.g., "10 nm").
in_physical_units (bool): If True, scale_length is interpreted as physical length. If False, it's a fraction of image width.
"""
self.rgb = rgb
self.channel_names = channel_names
Expand All @@ -28,20 +30,22 @@ def __init__(self, rgb: tuple = (255, 0, 255), channel_names: list = None, layou
self.show_box_background = show_box_background
self.scalebar = scalebar
self.scalebar_position = scalebar_position
self.in_physical_units = in_physical_units

def _convert_pixel_size(self, image: np.ndarray, pixel_size: str) -> float:
"""Convert pixel size to a float value in meters."""
try:
value, unit = pixel_size.split()
value = float(value)
unit = unit.lower()

except ValueError as e:
raise ValueError(f"Invalid pixel size format: {pixel_size}") from e

value = float(value)*image.shape[1]

return f"{value} {unit}"
if not self.in_physical_units:
# For fraction case, multiply by image width to get total width
value = float(value) * image.shape[1]

return value



Expand All @@ -54,16 +58,13 @@ def process_image(self, image: np.ndarray) -> None:
Returns:
None
"""

"""
TODO:
1. Check the scale and modify wrt image resizing, otherwise it is incorrect.
"""
image = ImageProcessor.convert_to_channel_last(image)
image_8bit = ImageProcessor.convert_to_8bit(image)
pixel_size = self._convert_pixel_size(image_8bit, self.pixel_size)
pixel_size_value = self._convert_pixel_size(image_8bit, self.pixel_size)
aspect_ratio = image_8bit.shape[1] / image_8bit.shape[0]
original_width = image_8bit.shape[1]
image_8bit = ImageProcessor.resize_image(image_8bit, (400,int(400*aspect_ratio)))
resize_factor = image_8bit.shape[1] / original_width
num_channels = image_8bit.shape[-1]
complementary_colors = ComplementaryColors(self.rgb).find_n_complementary_colors(num_channels)
composite_image = ImageProcessor.apply_complementary_luts(image_8bit, complementary_colors)
Expand All @@ -75,10 +76,12 @@ def process_image(self, image: np.ndarray) -> None:
self._figure = PanelCreator.create_panel(
image_8bit, complementary_colors, self.channel_names,
ImageProcessor.apply_complementary_luts(image_8bit, complementary_colors),
layout=self.layout, scale_length=self.scale_length, pixel_size=pixel_size,
name_position = self.name_position,
show_box_background = self.show_box_background,
scalebar=self.scalebar, scalebar_position=self.scalebar_position
layout=self.layout, scale_length=self.scale_length, pixel_size=self.pixel_size,
name_position=self.name_position,
show_box_background=self.show_box_background,
scalebar=self.scalebar, scalebar_position=self.scalebar_position,
in_physical_units=self.in_physical_units,
resize_factor=resize_factor
)

def save_figure(self, filename: str) -> None:
Expand Down
34 changes: 25 additions & 9 deletions lutipy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,14 @@ def create_panel(
channel_names: list,
composite_image: np.ndarray,
scale_length: float = 0.25,
pixel_size: float = None, # Pixel size in physical units, required for scalebar
pixel_size: str = None, # Pixel size in physical units, required for scalebar
layout: str = 'grid',
name_position: str = 'bottom-center',
show_box_background: bool = True,
scalebar: bool = False, # Whether to add a scalebar
scalebar_position: str = 'bottom-right' # Position of the scalebar
scalebar_position: str = 'bottom-right', # Position of the scalebar
in_physical_units: bool = False, # Whether scale_length is in physical units
resize_factor: float = 1.0 # Factor by which the image was resized
) -> plt.Figure:
"""
Create a tiled panel image with user-defined name positioning and optional scalebar.
Expand All @@ -511,13 +513,15 @@ def create_panel(
luts (list): List of LUT colors (RGB tuples).
channel_names (list): Names for each channel.
composite_image (np.ndarray): Composite RGB image.
scale_length (float): Length of the scalebar as a fraction of the image width.
pixel_size (float): Physical size of a pixel (e.g., 0.1 micrometers). Required if scalebar is True.
scale_length (float): Length of the scalebar. If in_physical_units is False, this is a fraction of image width.
If in_physical_units is True, this is the physical length in the same units as pixel_size.
pixel_size (str): Physical size of a pixel (e.g., "0.31 nm"). Required if scalebar is True.
layout (str): Layout of the panel ('grid', 'horizontal', 'vertical').
name_position (str): Position of channel names ('bottom-center', 'top-left', etc.).
show_box_background (bool): Whether to show a black background behind names.
scalebar (bool): Whether to add a scalebar.
scalebar_position (str): Position of the scalebar ('bottom-right', 'top-left', etc.).
in_physical_units (bool): If True, scale_length is interpreted as physical length. If False, it's a fraction of image width.

Returns:
plt.Figure: The figure object for the created panel.
Expand Down Expand Up @@ -553,7 +557,7 @@ def create_panel(
if scalebar:
if pixel_size is None:
raise ValueError("Pixel size must be provided for scalebar.")
PanelCreator._add_scalebar(axes[num_channels], scale_length, pixel_size, scalebar_text_position, image.shape, show_box_background)
PanelCreator._add_scalebar(axes[num_channels], scale_length, pixel_size, scalebar_text_position, image.shape, show_box_background, in_physical_units, resize_factor)

# Hide any unused axes
for j in range(num_channels + 1, len(axes)):
Expand Down Expand Up @@ -625,10 +629,23 @@ def _display_channel(axis, channel: np.ndarray, lut: tuple, name: str, text_posi
)

@staticmethod
def _add_scalebar(axis, scale_length: float, pixel_size: float, text_position: dict, image_shape: tuple, show_box_background: bool):
def _add_scalebar(axis, scale_length: float, pixel_size: str, text_position: dict, image_shape: tuple, show_box_background: bool, in_physical_units: bool = False, resize_factor: float = 1.0):
"""Add a scalebar to the image."""
scale_bar_length_px = int(image_shape[1] * scale_length)
scale_bar_length_physical = scale_length * float(pixel_size.split(" ")[0]) # Physical length
# Calculate scale bar length in pixels
if in_physical_units:
# Convert physical length to pixels, accounting for image resize to 400x400
pixel_size_value = float(pixel_size.split(" ")[0])
scale_bar_length_px = int((scale_length / pixel_size_value) * resize_factor)
scale_bar_length_physical = scale_length
else:
# Use fraction of image width
scale_bar_length_px = int(image_shape[1] * scale_length)
# For fraction case, calculate physical length from pixel size and total width
# IMportant note: image_shape is already the resized image shape
pixel_size_value = float(pixel_size.split(" ")[0])
# total width in physical units for the resized image
total_width = (image_shape[1] / resize_factor) * pixel_size_value
scale_bar_length_physical = total_width * scale_length

# Calculate dynamic starting positions based on text_position
x_start = text_position['x'] - (scale_bar_length_px / (2 * image_shape[1])) if text_position['ha'] == 'center' else (
Expand All @@ -652,7 +669,6 @@ def _add_scalebar(axis, scale_length: float, pixel_size: float, text_position: d
)
)


axis.plot(
[x_start, x_end], [y_start, y_start], color='white', lw=3,
transform=axis.transAxes, clip_on=False
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
numpy
matplotlib
matplotlib
imageio