Skip to content

Commit 3e95064

Browse files
committed
Histalign/Transform: Add RGB to grayscale transformation
1 parent 0e1d250 commit 3e95064

File tree

2 files changed

+99
-2
lines changed

2 files changed

+99
-2
lines changed

src/histalign/io/transform/__init__.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,54 @@ def _transform_series(
216216
transform_index = 0
217217

218218
transform_function = get_appropriate_transform_function(transform)
219+
220+
# TODO: Clean this up, had to add this extra transform in a hurry!
221+
if transform == "rgb2gray":
222+
order = source_file.dimension_order
223+
if "Z" in order.value:
224+
raise ValueError("Unsupported operation: RGB2gray on Z stack.")
225+
elif "C" not in order.value:
226+
raise ValueError("Image does not have a channel dimension.")
227+
228+
channel_index = order.value.index("C")
229+
transformed_image = transform_function(
230+
source_file.load(), channel_index, **kwargs
231+
)
232+
233+
if isinstance(destination_file[0], Path) or seek_first:
234+
order = source_file.dimension_order
235+
transformed_shape = transformed_image.shape
236+
237+
updated_metadata = update_metadata(
238+
source_file.metadata, transformed_shape
239+
)
240+
241+
if seek_first:
242+
# Writing the first image of a new series
243+
destination_file[0].seek_next_series(
244+
shape=transformed_shape,
245+
dtype=source_file.dtype,
246+
metadata=updated_metadata,
247+
)
248+
seek_first = False
249+
else:
250+
destination_plugin_class = get_appropriate_plugin_class(
251+
destination_file[0], mode="w"
252+
)
253+
destination_file[0] = destination_plugin_class(
254+
destination_file[0],
255+
mode="w",
256+
dimension_order=order,
257+
shape=transformed_shape,
258+
dtype=source_file.dtype,
259+
metadata=updated_metadata,
260+
)
261+
262+
destination_file[0].write_image(transformed_image, (slice(None),) * 2)
263+
264+
return
265+
266+
219267
for image in source_file.iterate_images(source_file.dimension_order):
220268
_module_logger.info(f"Transform {transform_index}/{transform_count}.")
221269

@@ -264,6 +312,9 @@ def _transform_series(
264312
def update_metadata(metadata: OmeXml, transformed_shape: list[int]) -> OmeXml:
265313
updated_metadata = metadata.model_copy(deep=True)
266314

315+
# TODO: Fix this to support Z (and T?)
316+
should_drop_c = metadata.SizeC > 1 and len(transformed_shape) < 3
317+
267318
x_position = updated_metadata.DimensionOrder.index("X")
268319
y_position = updated_metadata.DimensionOrder.index("Y")
269320

@@ -279,4 +330,11 @@ def update_metadata(metadata: OmeXml, transformed_shape: list[int]) -> OmeXml:
279330
updated_metadata.PhysicalSizeX = new_x_scaling
280331
updated_metadata.PhysicalSizeY = new_y_scaling
281332

333+
if should_drop_c:
334+
updated_metadata.SizeC = 1
335+
updated_metadata.Channel = []
336+
337+
order = updated_metadata.DimensionOrder
338+
updated_metadata.DimensionOrder = order.replace("C", "")
339+
282340
return updated_metadata

src/histalign/io/transform/transforms.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
# SPDX-License-Identifier: MIT
44

55
from collections.abc import Callable
6-
from typing import Literal
6+
from typing import Any, Literal
77

88
import numpy as np
99
from skimage.transform import rescale
1010

11-
Transform = Literal["downscale"]
11+
Transform = Literal["downscale", "rgb2gray"]
1212

1313

1414
def get_appropriate_transform_function(transform: Transform) -> Callable:
1515
match transform:
1616
case "downscale":
1717
return downscaling_transform
18+
case "rgb2gray":
19+
return rgb_to_gray_transform
1820
case _:
1921
raise ValueError(f"Unknown transform '{transform}'.")
2022

@@ -37,3 +39,40 @@ def downscaling_transform(
3739
anti_aliasing=True,
3840
)
3941
return array
42+
43+
44+
def rgb_to_gray_transform(
45+
series: np.ndarray, channel_index: int, **kwargs: Any
46+
) -> np.ndarray:
47+
"""Convert an RGB image to grayscale using Luma conversion.
48+
49+
Note this is not perfect and does not reflect the transformation done by, e.g.,
50+
ImageJ (they seem to do some fancier scaling).
51+
52+
Args:
53+
image: Series containing images with RGB channels.
54+
channel_index: Index of the channels.
55+
56+
Returns:
57+
The image as grayscale. This will have a shape similar to the input image
58+
without the channel.
59+
60+
References:
61+
Factors: https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems
62+
"""
63+
minimum = series.min()
64+
maximum = series.max()
65+
range_ = maximum - minimum
66+
67+
transformed = (
68+
series.take(0, channel_index) * 0.299
69+
+ series.take(1, channel_index) * 0.587
70+
+ series.take(2, channel_index) * 0.114
71+
)
72+
73+
transformed -= transformed.min()
74+
transformed /= transformed.max()
75+
transformed *= range_
76+
transformed += minimum
77+
78+
return transformed.astype(series.dtype)

0 commit comments

Comments
 (0)