From e368aedc747fd8ac78f5366de3faeb843b190b22 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 21 Mar 2024 10:42:30 -0400 Subject: [PATCH] Create reformatted BrainSwipes figures for HBCD QC (#1091) Co-authored-by: Barry Tikalsky --- .circleci/config.yml | 2 +- xcp_d/interfaces/execsummary.py | 75 +++++++++++++++++++++++++++++++++ xcp_d/interfaces/plotting.py | 15 ++++++- xcp_d/tests/test_cli.py | 1 + xcp_d/workflows/execsummary.py | 31 +++++++++++--- 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 41812221a..266df090e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -376,7 +376,7 @@ jobs: paths: - .coverage.pytests - store_artifacts: - path: /src/xcp_d/.circleci/out/ + path: /src/xcp_d/.circleci/out merge_coverage: <<: *dockersetup diff --git a/xcp_d/interfaces/execsummary.py b/xcp_d/interfaces/execsummary.py index 079e23ea9..c6ef7735a 100644 --- a/xcp_d/interfaces/execsummary.py +++ b/xcp_d/interfaces/execsummary.py @@ -10,8 +10,18 @@ from bs4 import BeautifulSoup from jinja2 import Environment, FileSystemLoader from markupsafe import Markup +from nipype.interfaces.base import ( + BaseInterfaceInputSpec, + File, + InputMultiPath, + SimpleInterface, + TraitedSpec, +) +from PIL import Image from pkg_resources import resource_filename as pkgrf +from xcp_d.utils.filemanip import fname_presuffix + class ExecutiveSummary(object): """A class to build an executive summary. @@ -327,3 +337,68 @@ def include_file(name): ) self.write_html(html, out_file) + + +class _FormatForBrainSwipesInputSpec(BaseInterfaceInputSpec): + in_files = InputMultiPath( + File(exists=True), + desc=( + "Figure files. Must be the derivative's filename, " + "not the file from the working directory." + ), + ) + + +class _FormatForBrainSwipesOutputSpec(TraitedSpec): + out_file = File(exists=True, desc="Reformatted png file.") + + +class FormatForBrainSwipes(SimpleInterface): + """Reformat figure for Brain Swipes. + + From https://github.com/DCAN-Labs/BrainSwipes/blob/cb2ce964bcae93c9a234e4421c07b88bcadf2908/\ + tools/images/ingest_brainswipes_data.py#L113 + Credit to @BarryTik. + """ + + input_spec = _FormatForBrainSwipesInputSpec + output_spec = _FormatForBrainSwipesOutputSpec + + def _run_interface(self, runtime): + input_files = self.inputs.in_files + assert len(input_files) == 9, "There must be 9 input files." + idx = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + widths, rows = [], [] + for i_row in range(3): + row_idx = idx[i_row] + row_files = [input_files[j_col] for j_col in row_idx] + row = [np.asarray(Image.open(row_file)) for row_file in row_files] + row = np.concatenate(row, axis=1) + widths.append(row.shape[1]) + rows.append(row) + + max_width = np.max(widths) + + for i_row, row in enumerate(rows): + width = widths[i_row] + if width < max_width: + pad = max_width - width + prepad = pad // 2 + postpad = pad - prepad + rows[i_row] = np.pad(row, ((0, 0), (prepad, postpad), (0, 0)), mode="constant") + + x = np.concatenate(rows, axis=0) + new_x = ((x - x.min()) * (1 / (x.max() - x.min()) * 255)).astype("uint8") + new_im = Image.fromarray(np.uint8(new_x)) + + output_file = fname_presuffix( + input_files[0], + newpath=runtime.cwd, + suffix="_reformatted.png", + use_ext=False, + ) + # all images should have the a .png extension + new_im.save(output_file) + self._results["out_file"] = output_file + + return runtime diff --git a/xcp_d/interfaces/plotting.py b/xcp_d/interfaces/plotting.py index 443c9a71d..92a43c4b9 100644 --- a/xcp_d/interfaces/plotting.py +++ b/xcp_d/interfaces/plotting.py @@ -730,7 +730,8 @@ class _SlicesDirInputSpec(FSLCommandInputSpec): class _SlicesDirOutputSpec(TraitedSpec): out_dir = Directory(exists=True, desc="Output directory.") - out_files = OutputMultiPath(File(exists=True), desc="List of generated PNG files.") + out_files = OutputMultiPath(File(exists=True), desc="Concatenated PNG files.") + slicewise_files = OutputMultiPath(File(exists=True), desc="List of generated PNG files.") class SlicesDir(FSLCommand): @@ -775,6 +776,18 @@ def _list_outputs(self): ) for f in self.inputs.in_files ] + temp_files = [ + "grota.png", + "grotb.png", + "grotc.png", + "grotd.png", + "grote.png", + "grotf.png", + "grotg.png", + "groth.png", + "groti.png", + ] + outputs["slicewise_files"] = [os.path.join(out_dir, f) for f in temp_files] return outputs def _gen_filename(self, name): diff --git a/xcp_d/tests/test_cli.py b/xcp_d/tests/test_cli.py index d4d41643e..b772f7c46 100644 --- a/xcp_d/tests/test_cli.py +++ b/xcp_d/tests/test_cli.py @@ -479,3 +479,4 @@ def _run_and_generate( check_generated_files(out_dir, output_list_file) check_affines(data_dir, out_dir, input_type=input_type) + LOGGER.warning(f"Test passed in {out_dir}") diff --git a/xcp_d/workflows/execsummary.py b/xcp_d/workflows/execsummary.py index 6cddd0f86..64b5c058e 100644 --- a/xcp_d/workflows/execsummary.py +++ b/xcp_d/workflows/execsummary.py @@ -12,6 +12,7 @@ from pkg_resources import resource_filename as pkgrf from xcp_d.interfaces.bids import DerivativesDataSink +from xcp_d.interfaces.execsummary import FormatForBrainSwipes from xcp_d.interfaces.nilearn import BinaryMath, ResampleToImage from xcp_d.interfaces.plotting import AnatomicalPlot, PNGAppend from xcp_d.interfaces.workbench import ShowScene @@ -761,14 +762,12 @@ def init_plot_overlay_wf( name="plot_overlay_figure", ) - # fmt:off workflow.connect([ (inputnode, plot_overlay_figure, [ ("underlay_file", "in_files"), ("overlay_file", "outline_image"), ]), - ]) - # fmt:on + ]) # fmt:skip ds_overlay_figure = pe.Node( DerivativesDataSink( @@ -782,11 +781,31 @@ def init_plot_overlay_wf( run_without_submitting=True, ) - # fmt:off workflow.connect([ (inputnode, ds_overlay_figure, [("name_source", "source_file")]), (plot_overlay_figure, ds_overlay_figure, [("out_files", "in_file")]), - ]) - # fmt:on + ]) # fmt:skip + + reformat_for_brain_swipes = pe.Node(FormatForBrainSwipes(), name="reformat_for_brain_swipes") + workflow.connect([ + (plot_overlay_figure, reformat_for_brain_swipes, [("slicewise_files", "in_files")]), + ]) # fmt:skip + + ds_reformatted_figure = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + dismiss_entities=["den"], + datatype="figures", + desc=f"{desc}BrainSwipes", + extension=".png", + ), + name="ds_reformatted_figure", + run_without_submitting=True, + ) + + workflow.connect([ + (inputnode, ds_reformatted_figure, [("name_source", "source_file")]), + (reformat_for_brain_swipes, ds_reformatted_figure, [("out_file", "in_file")]), + ]) # fmt:skip return workflow