From e09272a97ae0edae91ba0e8855b55b470c9eebfe Mon Sep 17 00:00:00 2001 From: Jan Valosek <39456460+valosekj@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:11:11 -0500 Subject: [PATCH] Allow to specify custom metadata to be included in JSON sidecars (#80) * Add a new arg '-json-metadata' allowing to provide a custom JSON file containing metadata to be added to the JSON sidecar of all corrected labels. * Add tests for newly added '-json-metadata' flag. Update other tests to be compatible with new 'json_metadata' param. * Use 'yyyy-mm-dd hh:mm:ss' date-time format for 'Date' in JSON sidecars --- manual_correction.py | 45 ++++++++++++++++++++++++++++++++++++--- tests/test_create_json.py | 36 +++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index a004e44..f0fcbd9 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -234,6 +234,21 @@ def get_parser(): "final dataset.", action='store_true' ) + parser.add_argument( + '-json-metadata', metavar="", required=False, + help="R|A custom JSON file containing metadata to be added to the JSON sidecar of all corrected labels. " + "This flag is useful, for example, when a label was obtained automatically and you want to include this " + "information into the JSON sidecar." + "Below is an example JSON file:\n" + + dedent( + """ + { + "Name": "sct_deepseg_sc", + "Version": "SCT v6.2", + "Date": "yyyy-mm-dd hh:mm:ss" + }\n + """), + ) parser.add_argument( '-v', '--verbose', help="Full verbose (for debugging)", @@ -475,11 +490,28 @@ def correct_centerline(fname, fname_label, viewer='sct_get_centerline'): viewer_not_found(viewer) -def update_json(fname_nifti, name_rater): +def load_custom_json(fname): + """ + Load custom JSON file. + :param fname: path to the custom JSON file. + :return: dictionary with the metadata to be added to the JSON sidecar. + """ + if not os.path.isfile(fname): + sys.exit("ERROR: The file {} does not exist.".format(fname)) + try: + with open(fname, "r") as f: + json_metadata = json.load(f) + return json_metadata + except json.JSONDecodeError: + sys.exit("ERROR: The file {} is not a valid JSON file.".format(fname)) + + +def update_json(fname_nifti, name_rater, json_metadata): """ Create/update JSON sidecar with meta information :param fname_nifti: str: File name of the nifti image to associate with the JSON sidecar :param name_rater: str: Name of the expert rater + :param json_metadata: dict: Dictionary with the metadata to be added to the JSON sidecar :return: """ fname_json = fname_nifti.replace('.gz', '').replace('.nii', '.json') @@ -501,6 +533,10 @@ def update_json(fname_nifti, name_rater): # Init new json dict json_dict = {'SpatialReference': 'orig', 'GeneratedBy': []} + # NOTE: we add the custom metadata only when initializing a new JSON file. Because it does not make sense to add + # these metadata into already existing labels, which we do not know how they were generated. + if json_metadata: + json_dict['GeneratedBy'].append(json_metadata) # If the label was modified or just checked, add "Name": "Manual" to the JSON sidecar json_dict['GeneratedBy'].append({'Name': 'Manual', @@ -726,6 +762,9 @@ def main(): if not file_list: sys.exit("ERROR: No segmentation file found in {}.".format(args.path_label)) + # If a custom JSON file containing metadata was provided, load it, and verify that it is a valid JSON file + json_metadata = load_custom_json(args.json_metadata) if args.json_metadata else None + # Get name of expert rater (skip if -qc-only is true) if not args.qc_only: name_rater = input("Enter your name (Firstname Lastname). It will be used to generate a json sidecar with each " @@ -856,10 +895,10 @@ def main(): if args.add_seg_only: # We use update_json because we are adding a new segmentation, and we want to create # a JSON file - update_json(fname_out, name_rater) + update_json(fname_out, name_rater, json_metadata) # Generate QC report else: - update_json(fname_out, name_rater) + update_json(fname_out, name_rater, json_metadata) # Generate QC report generate_qc(fname, fname_out, task, fname_qc, subject, args.config, args.qc_lesion_plane, suffix_dict) diff --git a/tests/test_create_json.py b/tests/test_create_json.py index 246d7c5..0e90a74 100644 --- a/tests/test_create_json.py +++ b/tests/test_create_json.py @@ -21,7 +21,7 @@ def test_create_json(tmp_path): nifti_file.touch() # Call the function with modified=True - update_json(str(nifti_file), "Test Rater") + update_json(str(nifti_file), "Test Rater", json_metadata=None) # Check that the JSON file was created and contains the expected metadata expected_metadata = {'SpatialReference': 'orig', @@ -35,6 +35,38 @@ def test_create_json(tmp_path): assert metadata == expected_metadata +def test_create_json_custom_metadata(tmp_path): + """ + Test that the function update_json() creates a JSON file with the expected metadata and includes custom metadata + provided by the user as a JSON file + """ + # Create a temporary file for testing + fname_label = "sub-001_ses-01_T1w_seg-manual.nii.gz" + nifti_file = tmp_path / fname_label + nifti_file.touch() + + custom_metada = {'Name': 'sct_deepseg_sc', + 'Author': "SCT v6.2", + 'Date': "2024-02-21 00:00:00"} + + # Call the function with modified=True + update_json(str(nifti_file), "Test Rater", json_metadata=custom_metada) + + # Check that the JSON file was created and contains the expected metadata + expected_metadata = {'SpatialReference': 'orig', + 'GeneratedBy': [{'Name': 'sct_deepseg_sc', + 'Author': "SCT v6.2", + 'Date': "2024-02-21 00:00:00"}, + {'Name': 'Manual', + 'Author': "Test Rater", + 'Date': time.strftime('%Y-%m-%d %H:%M:%S')}]} + json_file = tmp_path / fname_label.replace(".nii.gz", ".json") + assert json_file.exists() + with open(str(json_file), "r") as f: + metadata = json.load(f) + assert metadata == expected_metadata + + def test_update_json(tmp_path): """ Test that the function update_json() updates (appends to) the JSON file with the expected metadata. @@ -52,7 +84,7 @@ def test_update_json(tmp_path): 'Date': "2023-01-01 00:00:00"}]}, f) # Call the function with modified=True - update_json(str(nifti_file), "Test Rater 2") + update_json(str(nifti_file), "Test Rater 2", json_metadata=None) # Check that the JSON file was created and contains the expected metadata expected_metadata = {'SpatialReference': 'orig',