Add histological staining area quantification workflow#1206
Add histological staining area quantification workflow#1206dianichj wants to merge 3 commits intogalaxyproject:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a Galaxy workflow to quantify histological staining area in brightfield images using colour deconvolution + thresholding, along with accompanying documentation and a workflow test definition.
Changes:
- Added the “Histological Staining Area Quantification” Galaxy workflow (
.ga) with steps for deconvolution, channel selection, thresholding, feature extraction, and result collation. - Added a workflow test YAML describing a multi-sample input collection and expected outputs.
- Added a README describing inputs/outputs and compatible stainings.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| workflows/imaging/histological-staining-area-quantification/histological-staining-area-quantification.ga | Introduces the full staining-quantification workflow and output collation steps. |
| workflows/imaging/histological-staining-area-quantification/histological-staining-area-quantification-test.yml | Adds a test definition for running the workflow on a collection of ROI images and checking outputs. |
| workflows/imaging/histological-staining-area-quantification/README.md | Documents workflow purpose, inputs/outputs, and compatible stain/channel assumptions. |
| "workflow_outputs": [] | ||
| }, | ||
| "11": { | ||
| "annotation": "Applies a threshold to the selected stain channel to segment stained areas for quantification.\nTool only process tiff files with 1 channel. ", |
There was a problem hiding this comment.
The PR description (and workflow narrative) says the workflow uses Li thresholding, but the configured threshold method here is Otsu (method_id: otsu). This changes the core segmentation behavior. Either switch the workflow configuration to Li (if intended) or update the PR description/readme/annotations to reflect Otsu to keep the published method consistent.
| "owner": "imgteam", | ||
| "tool_shed": "toolshed.g2.bx.psu.edu" | ||
| }, | ||
| "tool_state": "{\"input\": {\"__class__\": \"RuntimeValue\"}, \"invert_output\": false, \"th_method\": {\"method_id\": \"otsu\", \"__current_case__\": 1, \"offset\": \"0.0\"}, \"__page__\": 0, \"__rerun_remap_job_id__\": null}", |
There was a problem hiding this comment.
The PR description (and workflow narrative) says the workflow uses Li thresholding, but the configured threshold method here is Otsu (method_id: otsu). This changes the core segmentation behavior. Either switch the workflow configuration to Li (if intended) or update the PR description/readme/annotations to reflect Otsu to keep the published method consistent.
| "workflow_outputs": [ | ||
| { | ||
| "label": "Tabular File: Staining Feature Results", | ||
| "output_name": "out_file1", | ||
| "uuid": "1c3f7fd0-bad7-4891-9f95-f1346e44c20f" | ||
| } | ||
| ] |
There was a problem hiding this comment.
The workflow computes percent_area in step 27, but step 27 is not exposed as a workflow output (workflow_outputs is empty). This makes the documented percent_area result unavailable to workflow consumers. Mark step 27 (or the appropriate downstream dataset that includes percent_area) as a workflow output, and adjust the test expectations accordingly.
| "type": "tool", | ||
| "uuid": "ae102451-2b3c-4ede-89db-a18e52b7bb2a", | ||
| "when": null, | ||
| "workflow_outputs": [] |
There was a problem hiding this comment.
The workflow computes percent_area in step 27, but step 27 is not exposed as a workflow output (workflow_outputs is empty). This makes the documented percent_area result unavailable to workflow consumers. Mark step 27 (or the appropriate downstream dataset that includes percent_area) as a workflow output, and adjust the test expectations accordingly.
| "workflow_outputs": [] | |
| "workflow_outputs": [ | |
| { | |
| "label": "percent_area", | |
| "output_name": "out_file1", | |
| "uuid": "2d53a6f8-0a7e-4c55-9f52-cfdff8e6267e" | |
| } | |
| ] |
| | Staining | Target channel | | ||
| |----------|---------------| | ||
| | Masson's Trichrome (MT) | Channel 2 | | ||
| | Immunohistochemistry (IHC) | Channel 2 (DAB) | |
There was a problem hiding this comment.
The markdown table is malformed (uses || at line starts), which may render incorrectly. Use standard markdown table syntax with single leading/trailing | per row so the table renders reliably.
| "label": "Extract", | ||
| "name": "Select last", |
There was a problem hiding this comment.
The workflow selects the last row from each per-sample feature table before collapsing. For binary masks, feature extraction commonly yields multiple labels (e.g., background=0 and stained=1). Relying on row order is brittle and can select the wrong label if ordering changes. Prefer filtering explicitly on the label column (e.g., keep only label == 1) before collapsing, or configure the feature-extraction step to output only the target label if the tool supports it.
| }, | ||
| "post_job_actions": {}, | ||
| "tool_id": "Show tail1", | ||
| "tool_state": "{\"header\": false, \"input\": {\"__class__\": \"ConnectedValue\"}, \"lineNum\": \"1\", \"__page__\": 0, \"__rerun_remap_job_id__\": null}", |
There was a problem hiding this comment.
The workflow selects the last row from each per-sample feature table before collapsing. For binary masks, feature extraction commonly yields multiple labels (e.g., background=0 and stained=1). Relying on row order is brittle and can select the wrong label if ordering changes. Prefer filtering explicitly on the label column (e.g., keep only label == 1) before collapsing, or configure the feature-extraction step to output only the target label if the tool supports it.
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | ||
| control_2: | ||
| elements: | ||
| 1.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | ||
| control_3: | ||
| elements: | ||
| 1.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | ||
| treatment_1: | ||
| elements: | ||
| 1.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | ||
| treatment_2: | ||
| elements: | ||
| 1.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | ||
| treatment_3: | ||
| elements: | ||
| 1.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | ||
| 2.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | ||
| 3.tiff: | ||
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' |
There was a problem hiding this comment.
Several element_tests reuse the same expected output files across different input samples (e.g., control_2 points to the same deconvolved-channel golden files as control_1). Unless the inputs are identical, this will either fail once real test data is added or will mask regressions if comparisons are unintentionally satisfied. Recommend providing per-sample golden outputs (or switching to more robust assertions for images/tabular outputs if exact matches are not feasible).
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| control_2: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| control_3: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| treatment_1: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| treatment_2: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| treatment_3: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_3.tiff.tiff' | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_1_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_1_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_1_3.tiff.tiff' | |
| control_2: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_2_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_2_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_2_3.tiff.tiff' | |
| control_3: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_3_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_3_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_control_3_3.tiff.tiff' | |
| treatment_1: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_1_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_1_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_1_3.tiff.tiff' | |
| treatment_2: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_2_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_2_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_2_3.tiff.tiff' | |
| treatment_3: | |
| elements: | |
| 1.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_3_1.tiff.tiff' | |
| 2.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_3_2.tiff.tiff' | |
| 3.tiff: | |
| path: 'test-data/Collection: Individual Deconvolved Channels_treatment_3_3.tiff.tiff' |
| "workflow_outputs": [] | ||
| }, | ||
| "1": { | ||
| "annotation": "Add Sample ID column name .", |
There was a problem hiding this comment.
Fix minor grammar/typos in step annotations to improve readability (e.g., remove the extra space before the period in "name ." and change "Tool only process" to "Tool only processes").
| "annotation": "Add Sample ID column name .", | |
| "annotation": "Add Sample ID column name.", |
| "workflow_outputs": [] | ||
| }, | ||
| "11": { | ||
| "annotation": "Applies a threshold to the selected stain channel to segment stained areas for quantification.\nTool only process tiff files with 1 channel. ", |
There was a problem hiding this comment.
Fix minor grammar/typos in step annotations to improve readability (e.g., remove the extra space before the period in "name ." and change "Tool only process" to "Tool only processes").
| "annotation": "Applies a threshold to the selected stain channel to segment stained areas for quantification.\nTool only process tiff files with 1 channel. ", | |
| "annotation": "Applies a threshold to the selected stain channel to segment stained areas for quantification.\nTool only processes TIFF files with 1 channel.", |
This workflow quantifies the area of histological staining in brightfield
histological images using colour deconvolution and automated thresholding.
Compatible with Masson's Trichrome (MT) and Immunohistochemistry (IHC).
Published workflow: https://usegalaxy.eu/u/dianitachj24/w/histological-staining-area-quantification
Status
Draft PR - test data and .dockstore.yml will be added before final merge.
cc: @mvdbeek @afgane