From 10c5e64a7da26855a14fdf390941507b67351cd0 Mon Sep 17 00:00:00 2001 From: MihinP Date: Fri, 20 Feb 2026 14:36:39 +1100 Subject: [PATCH 1/2] Fix 16-bit MRC handling in denoiser --- partinet/process_utils/guided_denoiser.py | 17 ++++++++ tests/test_guided_denoiser.py | 51 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/test_guided_denoiser.py diff --git a/partinet/process_utils/guided_denoiser.py b/partinet/process_utils/guided_denoiser.py index b3102db..3faae6a 100644 --- a/partinet/process_utils/guided_denoiser.py +++ b/partinet/process_utils/guided_denoiser.py @@ -20,6 +20,9 @@ def transform(image: np.ndarray) -> np.ndarray: """ i_min = image.min() i_max = image.max() + if i_max == i_min: + # avoid division by zero; return a zero array when input is constant + return np.zeros_like(image, dtype=np.uint8) image = ((image - i_min) / (i_max - i_min)) * 255 return image.astype(np.uint8) @@ -28,12 +31,20 @@ def standard_scaler(image: np.ndarray) -> np.ndarray: """ Apply Gaussian blur and standardize the image to have zero mean and unit variance. + The input is cast to ``float32`` before any OpenCV operations to avoid the + ``CV_16F`` kernel-type error that occurs when processing 16‑bit micrographs + (see issue #41). After blurring and normalization we transform the result to + eight‑bit for downstream filters. + Args: image (np.ndarray): Input image array. Returns: np.ndarray: Scaled and transformed image. """ + # convert to a supported floating point type for OpenCV kernels + image = image.astype(np.float32) + kernel_size = 9 image = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0) mu = np.mean(image) @@ -150,6 +161,12 @@ def denoise(image_path: str) -> np.ndarray: """ kernel = gaussian_kernel(kernel_size=9) image = mrcfile.read(image_path) + + # some MRCs are stored as 16‑bit integers; ensure we work in float32 so that + # subsequent OpenCV calls (GaussianBlur, etc.) don't raise the ktype error + # described in https://github.com/WEHI-ResearchComputing/PartiNet/issues/41 + image = image.astype(np.float32) + image = image.T image = np.rot90(image) normalized_image = standard_scaler(np.array(image)) diff --git a/tests/test_guided_denoiser.py b/tests/test_guided_denoiser.py new file mode 100644 index 0000000..1408b97 --- /dev/null +++ b/tests/test_guided_denoiser.py @@ -0,0 +1,51 @@ +import os +import tempfile + +import numpy as np +import mrcfile +import pytest + +from partinet.process_utils.guided_denoiser import denoise, transform, standard_scaler + + +def _write_temp_mrc(array: np.ndarray, dtype: np.dtype) -> str: + """Write ``array`` to a temporary .mrc file and return its path.""" + fd, path = tempfile.mkstemp(suffix=".mrc") + os.close(fd) + with mrcfile.new(path, overwrite=True) as mrc: + mrc.set_data(array.astype(dtype)) + return path + + +@ pytest.mark.parametrize("dtype", [np.uint16, np.int16, np.float32]) +def test_denoise_handles_various_dtypes(dtype): + """The ``denoise`` pipeline should accept 16-bit and 32-bit inputs without + throwing OpenCV kernel-type errors (issue #41). + """ + data = (np.random.rand(32, 32) * 255).astype(dtype) + path = _write_temp_mrc(data, dtype) + + try: + out = denoise(path) + assert isinstance(out, np.ndarray) + # our pipeline always returns 8-bit data + assert out.dtype == np.uint8 + assert out.shape == data.shape + finally: + os.unlink(path) + + +def test_transform_stable_when_constant(): + """``transform`` should not divide by zero if image has no contrast.""" + arr = np.full((4, 4), 100, dtype=np.float32) + out = transform(arr) + assert out.dtype == np.uint8 + assert np.all(out == 0) + + +def test_standard_scaler_normalises(): + arr = np.arange(25, dtype=np.float32).reshape(5, 5) + scaled = standard_scaler(arr) + assert scaled.dtype == np.uint8 + # scaled values should not all be equal + assert scaled.std() > 0 From 1f74ab9eb7aaa533e58249a2b2d7edb9b9ca11b9 Mon Sep 17 00:00:00 2001 From: MihinP Date: Fri, 20 Feb 2026 14:37:24 +1100 Subject: [PATCH 2/2] added pytest to requirements and install --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 539f1e8..574878d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "tensorboard", "scikit-learn", "mrcfile", + "pytest", ] [project.scripts]