diff --git a/conf/modules.config b/conf/modules.config index f2c5bdd8..b1f5140e 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -267,6 +267,15 @@ process { ext.args = { "--diameter 9 --channel_axis 0 --save_flows" } } + // ---------------------------- scportrait ------------------------------------- + + withName: SCPORTRAIT_SEGMENT { + publishDir = [ + path: { "${params.outdir}/${params.mode}/scportrait" }, + mode: params.publish_dir_mode, + ] + } + // ---------------------------- opt ----------------------------------------- withName: OPT_FLIP { diff --git a/modules/local/scportrait/imagesegment/main.nf b/modules/local/scportrait/imagesegment/main.nf new file mode 100644 index 00000000..68925cc0 --- /dev/null +++ b/modules/local/scportrait/imagesegment/main.nf @@ -0,0 +1,36 @@ +process SCPORTRAIT_IMAGESEGMENT { + tag "$meta.id" + label 'process_high' + maxForks params.restrict_concurrency ? 1 : 0 + + container "docker.io/library/python:3.11-slim" + + input: + tuple val(meta), path(image) + + output: + tuple val(meta), path("${prefix}/nuclei_labels.tif"), emit: nuclei, optional: true + tuple val(meta), path("${prefix}/cells_labels.tif"), emit: cells, optional: true + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + prefix = task.ext.prefix ?: "${meta.id}" + def nucleusOnly = params.nucleus_segmentation_only + template('image_segmentation.py') + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + """ + mkdir -p ${prefix} + touch ${prefix}/nuclei_labels.tif + touch ${prefix}/cells_labels.tif + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scportrait: stub + END_VERSIONS + """ +} \ No newline at end of file diff --git a/modules/local/scportrait/imagesegment/meta.yml b/modules/local/scportrait/imagesegment/meta.yml new file mode 100644 index 00000000..aa6b4ced --- /dev/null +++ b/modules/local/scportrait/imagesegment/meta.yml @@ -0,0 +1,77 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +# # TODO nf-core: Add a description of the module and list keywords +name: "scportrait_imagesegment" +description: write your description here +keywords: + - sort + - example + - genomics +tools: + ## TODO nf-core: Add a description and other details for the software below + - "scportrait": + description: "" + homepage: "" + documentation: "" + tool_dev_url: "" + doi: "" + licence: null + identifier: null + +input: + ### TODO nf-core: Add a description of all of the variables used as input + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - bam: + type: file + description: Sorted BAM/CRAM/SAM file + pattern: "*.{bam,cram,sam}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_2573" # CRAM + - edam: "http://edamontology.org/format_3462" # SAM + +output: + ### TODO nf-core: Add a description of all of the variables used as output + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "*.bam": + type: file + description: Sorted BAM/CRAM/SAM file + pattern: "*.{bam,cram,sam}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_2573" # CRAM + - edam: "http://edamontology.org/format_3462" # SAM + versions_scportrait: + - - "${task.process}": + type: string + description: The name of the process + - "scportrait": + type: string + description: The name of the tool + - "scportrait --version": + type: eval + description: The expression to obtain the version of the tool + +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - scportrait: + type: string + description: The name of the tool + - scportrait --version: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@TPbiocode" +maintainers: + - "@TPbiocode" diff --git a/modules/local/scportrait/imagesegment/templates/image_segmentation.py b/modules/local/scportrait/imagesegment/templates/image_segmentation.py new file mode 100644 index 00000000..abc570c9 --- /dev/null +++ b/modules/local/scportrait/imagesegment/templates/image_segmentation.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from importlib.metadata import PackageNotFoundError, version + +import numpy as np +from tifffile import imread, imwrite + +from scportrait.pipeline.project import Project +from scportrait.pipeline.segmentation.workflows import ( + CytosolSegmentationCellpose, + DAPISegmentationCellpose, +) + + +def normalize_input_shape(image: np.ndarray) -> np.ndarray: + image = np.asarray(image) + image = np.squeeze(image) + + if image.ndim == 2: + return image[np.newaxis, ...] + + if image.ndim == 3: + if image.shape[0] <= 4: + return image + if image.shape[-1] <= 4: + return np.moveaxis(image, -1, 0) + raise ValueError(f"Unable to infer channel axis from image shape {image.shape}") + + if image.ndim == 4: + image = image[0] + if image.shape[0] <= 4: + return image + if image.shape[-1] <= 4: + return np.moveaxis(image, -1, 0) + raise ValueError(f"Unable to infer channel axis from 4D image shape {image.shape}") + + raise ValueError(f"Unsupported morphology image shape {image.shape}") + + +def extract_label(label_obj) -> np.ndarray: + if hasattr(label_obj, "scale0"): + label_obj = label_obj.scale0.image + elif hasattr(label_obj, "image"): + label_obj = label_obj.image + + data = getattr(label_obj, "data", label_obj) + if hasattr(data, "compute"): + data = data.compute() + + data = np.asarray(data) + data = np.squeeze(data) + return data.astype(np.uint32) + + +def run_segmentation( + image_path: str, + nucleus_only: bool, + nuclei_out: str, + cells_out: str, + project_dir: str, +) -> None: + image = normalize_input_shape(imread(image_path)) + + if nucleus_only: + segmentation_cls = DAPISegmentationCellpose + config = { + "DAPISegmentationCellpose": { + "input_channels": 1, + "output_masks": 1, + "cache": ".", + "chunk_size": 100, + "nucleus_segmentation": {"model": "nuclei"}, + } + } + image = image[:1, :, :] + channel_names = ["nucleus"] + else: + segmentation_cls = CytosolSegmentationCellpose + if image.shape[0] == 1: + image = np.repeat(image, 2, axis=0) + else: + image = image[:2, :, :] + + config = { + "CytosolSegmentationCellpose": { + "input_channels": 2, + "output_masks": 2, + "cache": ".", + "chunk_size": 100, + "nucleus_segmentation": {"model": "nuclei"}, + "cytosol_segmentation": {"model": "cyto2"}, + "match_masks": True, + "filter_masks_size": False, + } + } + channel_names = ["nucleus", "cytosol"] + + project = Project( + project_location=project_dir, + config_path=config, + segmentation_f=segmentation_cls, + overwrite=True, + debug=False, + ) + project.load_input_from_array(image, channel_names=channel_names) + project.segment() + + if "seg_all_nucleus" in project.sdata: + imwrite(nuclei_out, extract_label(project.sdata["seg_all_nucleus"])) + + if "seg_all_cytosol" in project.sdata: + imwrite(cells_out, extract_label(project.sdata["seg_all_cytosol"])) + + +def generate_versions_yml() -> None: + try: + scportrait_version = version("scportrait") + except PackageNotFoundError: + scportrait_version = "unknown" + + with open("versions.yml", "w", encoding="utf-8") as handle: + handle.write('"${task.process}":\n') + handle.write(f" scportrait: {scportrait_version}\n") + + +def main() -> None: + image_path = "${image}" + prefix = "${prefix}" + project_dir = "scportrait_project" + nucleus_only = "${nucleusOnly}".lower() == "true" + nuclei_out = f"{prefix}/nuclei_labels.tif" + cells_out = f"{prefix}/cells_labels.tif" + + Path(prefix).mkdir(parents=True, exist_ok=True) + Path(project_dir).mkdir(parents=True, exist_ok=True) + + run_segmentation( + image_path=image_path, + nucleus_only=nucleus_only, + nuclei_out=nuclei_out, + cells_out=cells_out, + project_dir=project_dir, + ) + generate_versions_yml() + + +if __name__ == "__main__": + main() diff --git a/modules/local/scportrait/imagesegment/tests/main.nf.test b/modules/local/scportrait/imagesegment/tests/main.nf.test new file mode 100644 index 00000000..ef653ffc --- /dev/null +++ b/modules/local/scportrait/imagesegment/tests/main.nf.test @@ -0,0 +1,79 @@ +// TODO nf-core: Once you have added the required tests, please run the following command to build this file: +// nf-core modules test scportrait/imagesegment +nextflow_process { + + name "Test Process SCPORTRAIT_IMAGESEGMENT" + script "../main.nf" + process "SCPORTRAIT_IMAGESEGMENT" + + tag "modules" + tag "modules_" + tag "scportrait" + tag "scportrait/imagesegment" + + // TODO nf-core: Change the test name preferably indicating the test-data and file-format used + test("sarscov2 - bam") { + + // TODO nf-core: If you are created a test for a chained module + // (the module requires running more than one process to generate the required output) + // add the 'setup' method here. + // You can find more information about how to use a 'setup' method in the docs (https://nf-co.re/docs/contributing/modules#steps-for-creating-nf-test-for-chained-modules). + + when { + process { + """ + // TODO nf-core: define inputs of the process here. Example: + + input[0] = [ + [ id:'test' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot( + process.out, + path(process.out.versions[0]).yaml + ).match() } + //TODO nf-core: Add all required assertions to verify the test output. + // See https://nf-co.re/docs/contributing/tutorials/nf-test_assertions for more information and examples. + ) + } + + } + + // TODO nf-core: Change the test name preferably indicating the test-data and file-format used but keep the " - stub" suffix. + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + process { + """ + // TODO nf-core: define inputs of the process here. Example: + + input[0] = [ + [ id:'test' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot( + process.out, + path(process.out.versions[0]).yaml + ).match() } + ) + } + + } + +} diff --git a/nextflow.config b/nextflow.config index 473c41b8..6c688f53 100644 --- a/nextflow.config +++ b/nextflow.config @@ -45,7 +45,7 @@ params { format = 'xenium' // preset value set as `xenium` // Segmentation methods - image_seg_methods = ["cellpose", "xeniumranger", "baysor"] + image_seg_methods = ["cellpose", "xeniumranger", "baysor", "scportrait"] transcript_seg_methods = ["proseg", "segger", "baysor"] segfree_methods = ["ficture", "baysor"] diff --git a/nextflow_schema.json b/nextflow_schema.json index e5144c04..aed90a3d 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -36,7 +36,7 @@ }, "method": { "type": "string", - "enum": ["cellpose", "xeniumranger", "baysor", "proseg", "segger", "ficture"], + "enum": ["cellpose", "xeniumranger", "baysor", "scportrait", "proseg", "segger", "ficture"], "description": "Segmentation method to run." }, "gene_panel": { @@ -174,7 +174,7 @@ "type": "array", "items": { "type": "string", - "enum": ["cellpose", "xeniumranger", "baysor"] + "enum": ["cellpose", "xeniumranger", "baysor", "scportrait"] }, "description": "List of image-based segmentation methods." }, diff --git a/subworkflows/local/scportrait_resolift_morphology_ome_tif/main.nf b/subworkflows/local/scportrait_resolift_morphology_ome_tif/main.nf new file mode 100644 index 00000000..e69de29b diff --git a/workflows/spatialxe.nf b/workflows/spatialxe.nf index f22c4f5c..974edb67 100644 --- a/workflows/spatialxe.nf +++ b/workflows/spatialxe.nf @@ -28,6 +28,7 @@ include { BAYSOR_RUN_TRANSCRIPTS_PARQUET } from '../subworkflo include { BAYSOR_RUN_PRIOR_SEGMENTATION_MASK } from '../subworkflows/local/baysor_run_prior_segmentation_mask/main' include { CELLPOSE_RESOLIFT_MORPHOLOGY_OME_TIF } from '../subworkflows/local/cellpose_resolift_morphology_ome_tif/main' include { CELLPOSE_BAYSOR_IMPORT_SEGMENTATION } from '../subworkflows/local/cellpose_baysor_import_segmentation/main' +include { SCPORTRAIT_RESOLIFT_MORPHOLOGY_OME_TIF } from '../subworkflows/local/scportrait_resolift_morphology_ome_tif/main' include { XENIUMRANGER_RESEGMENT_MORPHOLOGY_OME_TIF } from '../subworkflows/local/xeniumranger_resegment_morphology_ome_tif/main' // segmentation-free subworkflows @@ -370,6 +371,15 @@ workflow SPATIALXE { ch_redefined_bundle = CELLPOSE_RESOLIFT_MORPHOLOGY_OME_TIF.out.redefined_bundle ch_coordinate_space = CELLPOSE_RESOLIFT_MORPHOLOGY_OME_TIF.out.coordinate_space } + + if (params.method == 'scportrait') { + SCPORTRAIT_RESOLIFT_MORPHOLOGY_OME_TIF( + ch_morphology_image, + ch_bundle_path, + ) + ch_redefined_bundle = SCPORTRAIT_RESOLIFT_MORPHOLOGY_OME_TIF.out.redefined_bundle + ch_coordinate_space = SCPORTRAIT_RESOLIFT_MORPHOLOGY_OME_TIF.out.coordinate_space + } } /*