diff --git a/.nf-core.yml b/.nf-core.yml index 18dd43d..497569f 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -4,6 +4,7 @@ lint: - params.input files_unchanged: - .gitignore + - docs/images/nf-core-mcmicro_logo_dark.png nf_core_version: 3.0.2 org_path: null repository_type: pipeline diff --git a/.prettierignore b/.prettierignore index 437d763..610e506 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ + email_template.html adaptivecard.json slackreport.json diff --git a/assets/markers_1_sp.csv b/assets/markers_1_sp.csv index 69e33d7..93b7c14 100644 --- a/assets/markers_1_sp.csv +++ b/assets/markers_1_sp.csv @@ -1,5 +1,5 @@ -channel_number,cycle_number,marker_name,filter,excitation_wavelength,emission_wavelength,background -21,1,DNA_6,DAPI,395,431,21 -22,1,ELA NE,FITC,485,525,21 -23,1,CD57,Sy tox,555,590,21 -24,1,CD45,Cy5,640,690,21 +channel_number,cycle_number,marker_name +21,1,DNA_6 +22,1,ELA NE +23,1,CD57 +24,1,CD45 diff --git a/assets/schema_marker.json b/assets/schema_marker.json index 656090e..857bb41 100644 --- a/assets/schema_marker.json +++ b/assets/schema_marker.json @@ -34,6 +34,15 @@ "emission_wavelength": { "type": "integer", "errorMessage": "" + }, + "exposure": { + "type": "number" + }, + "background": { + "type": "string" + }, + "remove": { + "type": "boolean" } }, "required": ["channel_number", "cycle_number", "marker_name"] diff --git a/docs/images/nf-core-mcmicro_logo_dark.png b/docs/images/nf-core-mcmicro_logo_dark.png index 399125c..a04f186 100644 Binary files a/docs/images/nf-core-mcmicro_logo_dark.png and b/docs/images/nf-core-mcmicro_logo_dark.png differ diff --git a/nextflow.config b/nextflow.config index ad12f8c..efaac0a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -21,6 +21,9 @@ params { // Illumination correction illumination = null + // Background subtraction + backsub = false + // MultiQC options multiqc_config = null multiqc_title = null diff --git a/nextflow_schema.json b/nextflow_schema.json index e577ed4..ecfcc2c 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -79,6 +79,10 @@ "type": "string", "description": "optional model file for cellpose segmentation" }, + "backsub": { + "type": "boolean", + "description": "boolean to flag whether or not to apply background subtraction" + }, "pixel_size": { "type": "number", "description": "Pixel width of input image data, in microns", diff --git a/subworkflows/local/utils_nfcore_mcmicro_pipeline/main.nf b/subworkflows/local/utils_nfcore_mcmicro_pipeline/main.nf index 44c1374..39d01f4 100644 --- a/subworkflows/local/utils_nfcore_mcmicro_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_mcmicro_pipeline/main.nf @@ -220,12 +220,35 @@ def validateInputMarkersheet( markersheet_data ) { error("Duplicate [channel, cycle] pairs: ${dups}") } + // validate backsub columns if present + def exposure_list = markersheet_data.findResults{ _1, _2, _3, _4, _5, _6, exposure, _8, _9 -> exposure ?: null } + def background_list = markersheet_data.findResults{ _1, _2, _3, _4, _5, _6, _7, background, _9 -> background ?: null } + def remove_list = markersheet_data.findResults{ _1, _2, _3, _4, _5, _6, _7, _8, remove -> remove ?: null } + + if (!background_list && (exposure_list || remove_list)) { + error("No values in background column, but values in either exposure or remove columns. Must have background column values to perform background subtraction.") + } else if (background_list) { + inter_list = marker_name_list.intersect(background_list) + if (inter_list.size() != background_list.size()) { + outliers_list = background_list - inter_list + error('background column values must exist in the marker_name column. The following background column values do not exist in the marker_name column: ' + outliers_list) + } + + if (!exposure_list) { + error('You must have at least one value in the exposure column to perform background subtraction') + } + + if (!remove_list) { + error ('You must have at least one value in the remove column to perform background subtraction') + } + } + return markersheet_data } def validateInputSamplesheetMarkersheet ( samples, markers ) { def sample_cycles = samples.collect{ meta, image_tiles, dfp, ffp -> meta.cycle_number } - def marker_cycles = markers.collect{ channel_number, cycle_number, marker_name, filter, excitation_wavelength, emission_wavelength -> cycle_number } + def marker_cycles = markers.collect{ channel_number, cycle_number, marker_name, _1, _2, _3, _4, _5, _6 -> cycle_number } if (marker_cycles.unique(false) != sample_cycles.unique(false) ) { error("cycle_number values must match between sample and marker sheets") diff --git a/tests/main.nf.test b/tests/main.nf.test index 880f7e9..a825ebf 100644 --- a/tests/main.nf.test +++ b/tests/main.nf.test @@ -24,10 +24,10 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], ], ) """ @@ -68,10 +68,10 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], ], ) """ @@ -113,10 +113,10 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], ], ) """ @@ -167,18 +167,18 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], - [5,1,'DNA_7',[],[],[]], - [6,1,'ELANE7',[],[],[]], - [7,1,'CD577',[],[],[]], - [8,1,'CD457',[],[],[]], - [9,1,'DNA_8',[],[],[]], - [10,1,'ELANE8',[],[],[]], - [11,1,'CD578',[],[],[]], - [12,1,'CD458',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], + [5,1,'DNA_7',[],[],[],[],[],[]], + [6,1,'ELANE7',[],[],[],[],[],[]], + [7,1,'CD577',[],[],[],[],[],[]], + [8,1,'CD457',[],[],[],[],[],[]], + [9,1,'DNA_8',[],[],[],[],[],[]], + [10,1,'ELANE8',[],[],[],[],[],[]], + [11,1,'CD578',[],[],[],[],[],[]], + [12,1,'CD458',[],[],[],[],[],[]], ], ) """ @@ -235,14 +235,14 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], - [5,1,'DNA_7',[],[],[]], - [6,1,'ELANE7',[],[],[]], - [7,1,'CD577',[],[],[]], - [8,1,'CD457',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], + [5,1,'DNA_7',[],[],[],[],[],[]], + [6,1,'ELANE7',[],[],[],[],[],[]], + [7,1,'CD577',[],[],[],[],[],[]], + [8,1,'CD457',[],[],[],[],[],[]], ], ) """ @@ -302,14 +302,14 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], - [5,1,'DNA_7',[],[],[]], - [6,1,'ELANE7',[],[],[]], - [7,1,'CD577',[],[],[]], - [8,1,'CD457',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], + [5,1,'DNA_7',[],[],[],[],[],[]], + [6,1,'ELANE7',[],[],[],[],[],[]], + [7,1,'CD577',[],[],[],[],[],[]], + [8,1,'CD457',[],[],[],[],[],[]], ], ) """ @@ -368,18 +368,18 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], - [5,1,'DNA_7',[],[],[]], - [6,1,'ELANE7',[],[],[]], - [7,1,'CD577',[],[],[]], - [8,1,'CD457',[],[],[]], - [9,1,'DNA_8',[],[],[]], - [10,1,'ELANE8',[],[],[]], - [11,1,'CD578',[],[],[]], - [12,1,'CD458',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], + [5,1,'DNA_7',[],[],[],[],[],[]], + [6,1,'ELANE7',[],[],[],[],[],[]], + [7,1,'CD577',[],[],[],[],[],[]], + [8,1,'CD457',[],[],[],[],[],[]], + [9,1,'DNA_8',[],[],[],[],[],[]], + [10,1,'ELANE8',[],[],[],[],[],[]], + [11,1,'CD578',[],[],[],[],[],[]], + [12,1,'CD458',[],[],[],[],[],[]], ], ) """ @@ -443,14 +443,14 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], - [5,1,'DNA_7',[],[],[]], - [6,1,'ELANE7',[],[],[]], - [7,1,'CD577',[],[],[]], - [8,1,'CD457',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], + [5,1,'DNA_7',[],[],[],[],[],[]], + [6,1,'ELANE7',[],[],[],[],[],[]], + [7,1,'CD577',[],[],[],[],[],[]], + [8,1,'CD457',[],[],[],[],[],[]], ], ) """ @@ -498,10 +498,10 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], - [2,1,'ELANE',[],[],[]], - [3,1,'CD57',[],[],[]], - [4,1,'CD45',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], + [2,1,'ELANE',[],[],[],[],[],[]], + [3,1,'CD57',[],[],[],[],[],[]], + [4,1,'CD45',[],[],[],[],[],[]], ], ) """ @@ -543,7 +543,7 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA_6',[],[],[]], + [1,1,'DNA_6',[],[],[],[],[],[]], ], ) """ @@ -564,5 +564,60 @@ nextflow_workflow { } }, + test("cycle: no illumination correction, backsub") { + + when { + params { + segmentation = "mesmer" + backsub = true + } + workflow { + """ + input[0] = Channel.of( + [ + [id:"TEST1", cycle_number:1, channel_count:4], + "https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/imaging/ome-tiff/cycif-tonsil-cycle1.ome.tif", + [], + [], + ], + [ + [id:"TEST1", cycle_number:2, channel_count:4], + "https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/imaging/ome-tiff/cycif-tonsil-cycle2.ome.tif", + [], + [], + ], + ) + input[1] = Channel.of( + [ + [1,1,'DNA 1',[],[],[],100,[],[]], + [2,1,'Na/K ATPase',[],[],[],100,[],[]], + [3,1,'CD3',[],[],[],100,[],[]], + [4,1,'CD45RO',[],[],[],100,[],[]], + [5,1,'DNA 2',[],[],[],100,[],[]], + [6,1,'Antigen Ki67',[],[],[],100,[],[]], + [7,1,'Pan-cytokeratin',[],[],[],100,[],[]], + [8,1,'Aortic smooth muscle actin',[],[],[],100,[],[]] + ], + ) + """ + } + } + + then { + assertAll ( + { + assert snapshot ( + path("$outputDir/registration/ashlar/TEST1.ome.tif"), + path("$outputDir/segmentation/deepcell_mesmer/mask_TEST1.tif"), + CsvUtils.summarizeCsv("$outputDir/quantification/mcquant/mesmer/TEST1_mask_TEST1.csv"), + path("$outputDir/backsub/TEST1.backsub.ome.tif"), + ).match() + }, + { assert workflow.success } + ) + } + + }, + ] } diff --git a/tests/main.nf.test.snap b/tests/main.nf.test.snap index 28d1faf..e67b011 100644 --- a/tests/main.nf.test.snap +++ b/tests/main.nf.test.snap @@ -113,6 +113,41 @@ }, "timestamp": "2024-08-14T13:21:48.678353201" }, + "cycle: no illumination correction, backsub": { + "content": [ + "TEST1.ome.tif:md5,fd414b610e189f3e805c8e99f4e78c09", + "mask_TEST1.tif:md5,3103759bd55b9deeb8c8a0dade798d9a", + { + "headers": [ + "CellID", + "DNA 1", + "Na/K ATPase", + "CD3", + "CD45RO", + "DNA 2", + "Antigen Ki67", + "Pan-cytokeratin", + "Aortic smooth muscle actin", + "X_centroid", + "Y_centroid", + "Area", + "MajorAxisLength", + "MinorAxisLength", + "Eccentricity", + "Solidity", + "Extent", + "Orientation" + ], + "rowCount": 2111 + }, + "TEST1.backsub.ome.tif:md5,82b15c88057ab8fd8248402a687997ea" + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-08T12:33:35.316668616" + }, "cycle: multiple file ashlar input with multiple samples no correction, multiple segmentation": { "content": [ "cycif-tonsil.ome.tif:md5,fd414b610e189f3e805c8e99f4e78c09", diff --git a/tests/workflows/mcmicro.nf.test b/tests/workflows/mcmicro.nf.test index bb92fda..dabfdee 100644 --- a/tests/workflows/mcmicro.nf.test +++ b/tests/workflows/mcmicro.nf.test @@ -22,10 +22,10 @@ nextflow_workflow { ) input[1] = Channel.of( [ - [1,1,'DNA 1',[],[],[]], - [2,1,'Na/K ATPase',[],[],[]], - [3,1,'CD3',[],[],[]], - [4,1,'CD45RO',[],[],[]], + [1,1,'DNA 1',[],[],[],[],[],[]], + [2,1,'Na/K ATPase',[],[],[],[],[],[]], + [3,1,'CD3',[],[],[],[],[],[]], + [4,1,'CD45RO',[],[],[],[],[],[]], ], ) """ diff --git a/workflows/mcmicro.nf b/workflows/mcmicro.nf index d26d291..d1167bf 100644 --- a/workflows/mcmicro.nf +++ b/workflows/mcmicro.nf @@ -73,20 +73,30 @@ workflow MCMICRO { | ASHLAR ch_versions = ch_versions.mix(ASHLAR.out.versions) - // // Run Background Correction - // BACKSUB(ASHLAR.out.tif, ch_markers) - //BACKSUB(ASHLAR.out.tif, [[id: "backsub"], params.marker_sheet]) - /* + // Run Background Correction if (params.backsub) { - BACKSUB(ASHLAR.out.tif, [[id:"$ASHLAR.out.tif[0]['id']"], params.marker_sheet]) + ch_backsub_markers = ch_markersheet + .map { ['channel_number,cycle_number,marker_name,exposure,background,remove', + it.collect{ channel_number, cycle_number, marker_name, _1, _2, _3, exposure, background, remove -> + channel_number + "," + cycle_number + "," + marker_name + "," + exposure + "," + background + "," + remove}] } + .flatten() + .map { it.replace('[]', '') } + .collectFile(name: 'markers_backsub.csv', sort: false, newLine: true) + + ASHLAR.out.tif + .combine(ch_backsub_markers) + .dump(tag: 'BACKSUB IN') + .multiMap{ meta, image, marker -> + image: [meta, image] + markers: [meta, marker] + } + | BACKSUB + ch_segmentation_input = BACKSUB.out.backsub_tif ch_versions = ch_versions.mix(BACKSUB.out.versions) } else { - */ ch_segmentation_input = ASHLAR.out.tif - /* } - */ // Run Coreograph if (params.tma_dearray) { @@ -127,8 +137,9 @@ workflow MCMICRO { ch_mcquant_markers = ch_markersheet .flatMap{ ['marker_name'] + - it.collect{ _1, _2, marker_name, _4, _5, _6 -> '"' + marker_name + '"' } + it.collect{ _1, _2, marker_name, _4, _5, _6, _7, _8, _9 -> '"' + marker_name + '"' } } + .dump(tag: "MARKERS") .collectFile(name: 'markers.csv', sort: false, newLine: true) ch_segmentation_input