From 202fa49407a4d8952e92f57c16711717968254e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Cl=C3=A9net?= Date: Thu, 16 Nov 2023 17:33:58 +0100 Subject: [PATCH] [ENH] add voxel_dimensions [TEST] add test for voxel_dimensions --- narps_open/core/common.py | 13 ++--- narps_open/core/image.py | 25 ++++++++++ tests/core/test_common.py | 40 +++------------- tests/core/test_image.py | 48 +++++++++++++++++++ tests/test_data/core/image/test_image.nii.gz | Bin 0 -> 13326 bytes 5 files changed, 82 insertions(+), 44 deletions(-) create mode 100644 narps_open/core/image.py create mode 100644 tests/core/test_image.py create mode 100644 tests/test_data/core/image/test_image.nii.gz diff --git a/narps_open/core/common.py b/narps_open/core/common.py index 6889ed75..9a076719 100644 --- a/narps_open/core/common.py +++ b/narps_open/core/common.py @@ -3,26 +3,19 @@ """ Common functions to write pipelines """ -def remove_files(_, files): +def remove_file(_, file_name): """ Fully remove files generated by a Node, once they aren't needed anymore. This function is meant to be used in a Nipype Function Node. Parameters: - _: input only used for triggering the Node - - files: str or list, a single filename or a list of absolute filenames to remove + - file_name: str, a single absolute filename of the file to remove """ # This import must stay inside the function, as required by Nipype from os import remove - if isinstance(files, str): - files = [files] - try: - for file in files: - remove(file) + remove(file_name) except OSError as error: print(error) - else: - print('The following files were successfully deleted.') - print(files) diff --git a/narps_open/core/image.py b/narps_open/core/image.py new file mode 100644 index 00000000..35323e45 --- /dev/null +++ b/narps_open/core/image.py @@ -0,0 +1,25 @@ +#!/usr/bin/python +# coding: utf-8 + +""" Image functions to write pipelines """ + +def get_voxel_dimensions(image: str) -> list: + """ + Return the voxel dimensions of a image in millimeters. + + Arguments: + image: str, string that represent an absolute path to a Nifti image. + + Returns: + list, size of the voxels in the image in millimeters. + """ + # This import must stay inside the function, as required by Nipype + from nibabel import load + + voxel_dimensions = load(image).header.get_zooms() + + return [ + float(voxel_dimensions[0]), + float(voxel_dimensions[1]), + float(voxel_dimensions[2]) + ] diff --git a/tests/core/test_common.py b/tests/core/test_common.py index 30142fdc..a86add9f 100644 --- a/tests/core/test_common.py +++ b/tests/core/test_common.py @@ -19,7 +19,7 @@ from nipype import Node, Function from narps_open.utils.configuration import Configuration -from narps_open.core.common import remove_files +import narps_open.core.common as co TEMPORARY_DIR = join(Configuration()['directories']['test_runs'], 'test_common') @@ -38,7 +38,7 @@ class TestCoreCommon: @staticmethod @mark.unit_test def test_remove_file(remove_test_dir): - """ Test the remove_files function with only one file """ + """ Test the remove_file function """ # Create a single file test_file_path = abspath(join(TEMPORARY_DIR, 'file1.txt')) @@ -49,41 +49,13 @@ def test_remove_file(remove_test_dir): # Create a Nipype Node using remove_files test_remove_file_node = Node(Function( - function = remove_files, - input_names = ['_', 'files'], + function = co.remove_file, + input_names = ['_', 'file_name'], output_names = [] - ), 'test_remove_file_node') + ), name = 'test_remove_file_node') test_remove_file_node.inputs._ = '' - test_remove_file_node.inputs.files = test_file_path + test_remove_file_node.inputs.file_name = test_file_path test_remove_file_node.run() # Check file is removed assert not exists(test_file_path) - - @staticmethod - @mark.unit_test - def test_remove_files(remove_test_dir): - """ Test the remove_files function with a list of files """ - - # Create a list of files - test_file_paths = [ - abspath(join(TEMPORARY_DIR, 'file1.txt')), - abspath(join(TEMPORARY_DIR, 'file2.txt')) - ] - for test_file_path in test_file_paths: - Path(test_file_path).touch() - assert exists(test_file_path) - - # Create a Nipype Node using remove_files - test_remove_files_node = Node(Function( - function = remove_files, - input_names = ['_', 'files'], - output_names = [] - ), 'test_remove_files_node') - test_remove_files_node.inputs._ = '' - test_remove_files_node.inputs.files = test_file_paths - test_remove_files_node.run() - - # Check files are removed - for test_file_path in test_file_paths: - assert not exists(test_file_path) diff --git a/tests/core/test_image.py b/tests/core/test_image.py new file mode 100644 index 00000000..d3b83ac5 --- /dev/null +++ b/tests/core/test_image.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# coding: utf-8 + +""" Tests of the 'narps_open.core.image' module. + +Launch this test with PyTest + +Usage: +====== + pytest -q test_image.py + pytest -q test_image.py -k +""" + +from os.path import abspath, join +from numpy import isclose + +from pytest import mark +from nipype import Node, Function + +from narps_open.utils.configuration import Configuration +import narps_open.core.image as im + +class TestCoreImage: + """ A class that contains all the unit tests for the image module.""" + + @staticmethod + @mark.unit_test + def test_get_voxel_dimensions(): + """ Test the get_voxel_dimensions function """ + + # Path to the test image + test_file_path = abspath(join( + Configuration()['directories']['test_data'], + 'core', + 'image', + 'test_image.nii.gz')) + + # Create a Nipype Node using get_voxel_dimensions + test_get_voxel_dimensions_node = Node(Function( + function = im.get_voxel_dimensions, + input_names = ['image'], + output_names = ['voxel_dimensions'] + ), name = 'test_get_voxel_dimensions_node') + test_get_voxel_dimensions_node.inputs.image = test_file_path + outputs = test_get_voxel_dimensions_node.run().outputs + + # Check voxel sizes + assert isclose(outputs.voxel_dimensions, [8.0, 8.0, 9.6]).all() diff --git a/tests/test_data/core/image/test_image.nii.gz b/tests/test_data/core/image/test_image.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..06fb9aa32a92ef81f03316e4327804b20738c685 GIT binary patch literal 13326 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