From f2b4c26f0c77587d0c0979a50cf4df5ab01f8924 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:08:34 +0000 Subject: [PATCH 01/10] Initial plan From 207164a1c6da1c3cff628d7024e0559789cb7f1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:11:41 +0000 Subject: [PATCH 02/10] Add --rgb flag to raw2ometiff when H&E is true Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/bioformats2ometiff.nf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/bioformats2ometiff.nf b/modules/bioformats2ometiff.nf index 4fc13f5..ca9c2c8 100644 --- a/modules/bioformats2ometiff.nf +++ b/modules/bioformats2ometiff.nf @@ -15,8 +15,9 @@ process bioformats2ometiff { touch "${image.simpleName}.ome.tiff" """ script: + def rgb_flag = meta.he ? '--rgb' : '' """ bioformats2raw $image 'raw_dir' - raw2ometiff 'raw_dir' "${image.simpleName}.ome.tiff" + raw2ometiff ${rgb_flag} 'raw_dir' "${image.simpleName}.ome.tiff" """ } From fed86851fec030217c9227685e475267f1c508d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:19:54 +0000 Subject: [PATCH 03/10] Replace TiffSlide with tifffile for H&E thumbnail generation TiffSlide doesn't support OME-TIFF files with SYX axes (RGB chunky format) created by raw2ometiff --rgb flag. Switch to tifffile + PIL which handles this format correctly. Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index d3742b8..c1bff98 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -17,15 +17,30 @@ process make_miniature { """ #!/usr/bin/env python - from tiffslide import TiffSlide - import matplotlib.pyplot as plt - import os + import tifffile + from PIL import Image - slide = TiffSlide('$image') - - thumb = slide.get_thumbnail((512, 512)) + # Open the OME-TIFF file + with tifffile.TiffFile('$image') as tif: + # For pyramidal images, use the smallest level for efficiency + if len(tif.series) > 0 and len(tif.series[0].levels) > 1: + # Get the smallest pyramid level + level = tif.series[0].levels[-1] + img_array = level.asarray() + else: + # No pyramid, read the full resolution + img_array = tif.asarray() + + # Convert numpy array to PIL Image + thumb = Image.fromarray(img_array) + + # Create thumbnail (maintains aspect ratio) + thumb.thumbnail((512, 512)) + + # Ensure RGB mode if thumb.mode in ("RGBA", "P"): thumb = thumb.convert("RGB") + thumb.save('miniature.jpg') """ } else { From 7f45ad24033b64a7095f6d740687b2ddde8e704a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:21:53 +0000 Subject: [PATCH 04/10] Improve thumbnail generation robustness - Add hasattr check for tif.series[0].levels to avoid AttributeError - Add data type handling to ensure uint8 for PIL compatibility - Improve RGB mode conversion to handle all image modes - Add numpy import for array operations Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index c1bff98..000286c 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -19,18 +19,27 @@ process make_miniature { import tifffile from PIL import Image + import numpy as np # Open the OME-TIFF file with tifffile.TiffFile('$image') as tif: # For pyramidal images, use the smallest level for efficiency - if len(tif.series) > 0 and len(tif.series[0].levels) > 1: + if len(tif.series) > 0 and hasattr(tif.series[0], 'levels') and len(tif.series[0].levels) > 1: # Get the smallest pyramid level level = tif.series[0].levels[-1] img_array = level.asarray() else: - # No pyramid, read the full resolution + # No pyramid or single level, read the full resolution img_array = tif.asarray() + # Ensure proper data type for PIL (uint8) + if img_array.dtype != np.uint8: + # Scale to uint8 range if needed + if img_array.max() > 255: + img_array = ((img_array - img_array.min()) / (img_array.max() - img_array.min()) * 255).astype(np.uint8) + else: + img_array = img_array.astype(np.uint8) + # Convert numpy array to PIL Image thumb = Image.fromarray(img_array) @@ -38,7 +47,7 @@ process make_miniature { thumb.thumbnail((512, 512)) # Ensure RGB mode - if thumb.mode in ("RGBA", "P"): + if thumb.mode != "RGB": thumb = thumb.convert("RGB") thumb.save('miniature.jpg') From 8dadf28d78dbc68d789fc096afd688d9af997c7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:22:57 +0000 Subject: [PATCH 05/10] Fix division by zero and optimize min/max calculations - Cache min/max values to avoid redundant calculations - Add division by zero protection when normalizing arrays Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index 000286c..47521b8 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -35,8 +35,14 @@ process make_miniature { # Ensure proper data type for PIL (uint8) if img_array.dtype != np.uint8: # Scale to uint8 range if needed - if img_array.max() > 255: - img_array = ((img_array - img_array.min()) / (img_array.max() - img_array.min()) * 255).astype(np.uint8) + img_min = img_array.min() + img_max = img_array.max() + if img_max > 255: + # Avoid division by zero + if img_max > img_min: + img_array = ((img_array - img_min) / (img_max - img_min) * 255).astype(np.uint8) + else: + img_array = np.zeros_like(img_array, dtype=np.uint8) else: img_array = img_array.astype(np.uint8) From 19d92a8f52ec04ed6833b5e19454d87e3e0f2653 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:32:29 +0000 Subject: [PATCH 06/10] Fix PIL array shape handling for RGB OME-TIFF thumbnails - Read from base series instead of smallest pyramid level to avoid tiny images - Add np.squeeze() to remove singleton dimensions - Handle 1D arrays by reshaping to 2D - Handle >3D arrays by taking first slice - Fixes TypeError: Cannot handle this data type: (1, 1, 138), |u1 Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index 47521b8..c3d1809 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -21,17 +21,35 @@ process make_miniature { from PIL import Image import numpy as np - # Open the OME-TIFF file + # Open the OME-TIFF file and read the base (full resolution) image + # Let PIL handle the resizing via thumbnail() - it's more robust with tifffile.TiffFile('$image') as tif: - # For pyramidal images, use the smallest level for efficiency - if len(tif.series) > 0 and hasattr(tif.series[0], 'levels') and len(tif.series[0].levels) > 1: - # Get the smallest pyramid level - level = tif.series[0].levels[-1] - img_array = level.asarray() + # Always read from the base series (full resolution) + # Don't try to use pyramid levels as they may be too small or have odd shapes + if len(tif.series) > 0: + img_array = tif.series[0].asarray() else: - # No pyramid or single level, read the full resolution img_array = tif.asarray() + # Handle array shape - squeeze out singleton dimensions + img_array = np.squeeze(img_array) + + # Ensure we have at least 2D array + if img_array.ndim < 2: + # After squeezing, if we end up with 1D, reshape to 2D + img_array = img_array.reshape(1, -1) + + # Handle different array shapes + # Expected shapes: (Y, X), (Y, X, 3), (Y, X, 4) for grayscale, RGB, or RGBA + if img_array.ndim > 3: + # If more than 3 dimensions after squeeze, take first slice + # This handles cases like (C, Y, X, S) -> take first channel + img_array = img_array[0] + img_array = np.squeeze(img_array) + # Check again after squeezing + if img_array.ndim < 2: + img_array = img_array.reshape(1, -1) + # Ensure proper data type for PIL (uint8) if img_array.dtype != np.uint8: # Scale to uint8 range if needed @@ -50,6 +68,7 @@ process make_miniature { thumb = Image.fromarray(img_array) # Create thumbnail (maintains aspect ratio) + # PIL's thumbnail is efficient and handles large images well thumb.thumbnail((512, 512)) # Ensure RGB mode From 9ca6e62408b33d4ef30a20ea71486d54abffde8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:35:45 +0000 Subject: [PATCH 07/10] Use pyvips for robust H&E thumbnail generation Replace custom tifffile/PIL implementation with pyvips (libvips): - Works with both OME-TIFF (convert=true) and original formats like SVS (convert=false) - Handles RGB chunky format from raw2ometiff --rgb flag - Production-ready library designed for whole slide imaging - Efficiently handles pyramidal images automatically - Eliminates complex array shape manipulation code - More maintainable and reliable Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 64 +++++---------------------------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index c3d1809..2df0009 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -17,65 +17,17 @@ process make_miniature { """ #!/usr/bin/env python - import tifffile - from PIL import Image - import numpy as np + import pyvips - # Open the OME-TIFF file and read the base (full resolution) image - # Let PIL handle the resizing via thumbnail() - it's more robust - with tifffile.TiffFile('$image') as tif: - # Always read from the base series (full resolution) - # Don't try to use pyramid levels as they may be too small or have odd shapes - if len(tif.series) > 0: - img_array = tif.series[0].asarray() - else: - img_array = tif.asarray() + # Use pyvips for robust thumbnail generation from any whole slide format + # Supports SVS, OME-TIFF, and other formats efficiently + image = pyvips.Image.new_from_file('$image', access='sequential') - # Handle array shape - squeeze out singleton dimensions - img_array = np.squeeze(img_array) + # Create thumbnail with max dimension of 512px + thumbnail = image.thumbnail_image(512) - # Ensure we have at least 2D array - if img_array.ndim < 2: - # After squeezing, if we end up with 1D, reshape to 2D - img_array = img_array.reshape(1, -1) - - # Handle different array shapes - # Expected shapes: (Y, X), (Y, X, 3), (Y, X, 4) for grayscale, RGB, or RGBA - if img_array.ndim > 3: - # If more than 3 dimensions after squeeze, take first slice - # This handles cases like (C, Y, X, S) -> take first channel - img_array = img_array[0] - img_array = np.squeeze(img_array) - # Check again after squeezing - if img_array.ndim < 2: - img_array = img_array.reshape(1, -1) - - # Ensure proper data type for PIL (uint8) - if img_array.dtype != np.uint8: - # Scale to uint8 range if needed - img_min = img_array.min() - img_max = img_array.max() - if img_max > 255: - # Avoid division by zero - if img_max > img_min: - img_array = ((img_array - img_min) / (img_max - img_min) * 255).astype(np.uint8) - else: - img_array = np.zeros_like(img_array, dtype=np.uint8) - else: - img_array = img_array.astype(np.uint8) - - # Convert numpy array to PIL Image - thumb = Image.fromarray(img_array) - - # Create thumbnail (maintains aspect ratio) - # PIL's thumbnail is efficient and handles large images well - thumb.thumbnail((512, 512)) - - # Ensure RGB mode - if thumb.mode != "RGB": - thumb = thumb.convert("RGB") - - thumb.save('miniature.jpg') + # Save as JPEG + thumbnail.write_to_file('miniature.jpg') """ } else { """ From f33399f0043d828c091c33403f596fcb3e9dbb74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:36:43 +0000 Subject: [PATCH 08/10] Rename pyvips variable to avoid shadowing input parameter Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- modules/make_miniature.nf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/make_miniature.nf b/modules/make_miniature.nf index 2df0009..0cec288 100644 --- a/modules/make_miniature.nf +++ b/modules/make_miniature.nf @@ -21,10 +21,10 @@ process make_miniature { # Use pyvips for robust thumbnail generation from any whole slide format # Supports SVS, OME-TIFF, and other formats efficiently - image = pyvips.Image.new_from_file('$image', access='sequential') + slide = pyvips.Image.new_from_file('$image', access='sequential') # Create thumbnail with max dimension of 512px - thumbnail = image.thumbnail_image(512) + thumbnail = slide.thumbnail_image(512) # Save as JPEG thumbnail.write_to_file('miniature.jpg') From 2d71f6ac5b6c44db478bdb24892b1f9aec657c21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:49:44 +0000 Subject: [PATCH 09/10] Add pyvips Python package and enable Docker in CI - Add pyvips to docker/environment.yml (Python bindings for libvips) - Update GitHub Actions workflow to use -profile test,docker - Fixes ModuleNotFoundError: No module named 'pyvips' in CI tests Co-authored-by: adamjtaylor <14945787+adamjtaylor@users.noreply.github.com> --- .github/workflows/nextflow.yml | 2 +- docker/environment.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nextflow.yml b/.github/workflows/nextflow.yml index 963b1d8..b1222e5 100644 --- a/.github/workflows/nextflow.yml +++ b/.github/workflows/nextflow.yml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: nf-core/setup-nextflow@v1 - - run: nextflow run ${GITHUB_WORKSPACE} -profile test \ No newline at end of file + - run: nextflow run ${GITHUB_WORKSPACE} -profile test,docker \ No newline at end of file diff --git a/docker/environment.yml b/docker/environment.yml index df9f387..bdc611f 100644 --- a/docker/environment.yml +++ b/docker/environment.yml @@ -31,4 +31,5 @@ dependencies: - imagemagick - tiffslide=2.1.2 - libvips + - pyvips - estimagic From f46c59940b969753df76140f53204b31b1d4182b Mon Sep 17 00:00:00 2001 From: Adam Taylor Date: Fri, 16 Jan 2026 11:25:32 +0000 Subject: [PATCH 10/10] Remove non-existant docker profile --- .github/workflows/nextflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nextflow.yml b/.github/workflows/nextflow.yml index b1222e5..963b1d8 100644 --- a/.github/workflows/nextflow.yml +++ b/.github/workflows/nextflow.yml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: nf-core/setup-nextflow@v1 - - run: nextflow run ${GITHUB_WORKSPACE} -profile test,docker \ No newline at end of file + - run: nextflow run ${GITHUB_WORKSPACE} -profile test \ No newline at end of file