|
29 | 29 | import shutil |
30 | 30 | import argparse |
31 | 31 | import importlib |
| 32 | +from nibabel import load |
32 | 33 |
|
33 | 34 | try: |
34 | 35 | import helper_functions |
@@ -176,6 +177,7 @@ def __init__( |
176 | 177 | silent=False, |
177 | 178 | tempdir_location=None, |
178 | 179 | ezbids=False, |
| 180 | + ignore_dcm2niix_errors=False, |
179 | 181 | ): |
180 | 182 | """ |
181 | 183 | This class is a simple wrapper for dcm2niix and contains methods to do the following in order: |
@@ -223,7 +225,7 @@ def __init__( |
223 | 225 | "dcm2niix not found, this module depends on it for conversions, exiting." |
224 | 226 | ) |
225 | 227 | sys.exit(1) |
226 | | - |
| 228 | + self.ignore_dcm2niix_errors = ignore_dcm2niix_errors |
227 | 229 | # check for the version of dcm2niix |
228 | 230 | minimum_version = "v1.0.20220720" |
229 | 231 | version_string = subprocess.run([self.dcm2niix_path, "-v"], capture_output=True) |
@@ -489,7 +491,7 @@ def run_dcm2niix(self): |
489 | 491 | if ( |
490 | 492 | convert.returncode != 0 |
491 | 493 | or "error" in convert.stderr.decode("utf-8").lower() |
492 | | - ): |
| 494 | + ) and not self.ignore_dcm2niix_errors: |
493 | 495 | print( |
494 | 496 | "Check output .nii files, dcm2iix returned these errors during conversion:" |
495 | 497 | ) |
@@ -570,12 +572,46 @@ def run_dcm2niix(self): |
570 | 572 | # often a series of dicoms is incomplete (missing files) but dcm2niix can still |
571 | 573 | # output a nifti at the end. We can compare the outputs of dcm2niix with the |
572 | 574 | # frame information in the dicom header. |
573 | | - |
574 | | - # collect the number of frames that are present in the nifti |
575 | | - |
576 | | - # collect the number of frames that are listed in the sidecar, duration, etc |
577 | | - |
578 | | - # collect any frame timing info that may be contained in additional arguments or the spreadsheet metadata |
| 575 | + for dicom, matched in matched_dicoms_and_headers.items(): |
| 576 | + matched_dicom = pydicom.dcmread( |
| 577 | + join(self.image_folder, dicom), stop_before_pixels=True |
| 578 | + ) |
| 579 | + matched_json = next( |
| 580 | + (f for f in matched if f.endswith(".json")), None |
| 581 | + ) |
| 582 | + matched_nii = next( |
| 583 | + ( |
| 584 | + load(f) |
| 585 | + for f in matched |
| 586 | + if f.endswith(".nii") or f.endswith(".nii.gz") |
| 587 | + ), |
| 588 | + None, |
| 589 | + ) |
| 590 | + if matched_nii: |
| 591 | + try: |
| 592 | + nifti_time_dim = matched_nii.shape[3] |
| 593 | + except IndexError: |
| 594 | + nifti_time_dim = 0 |
| 595 | + if ( |
| 596 | + matched_dicom.get("NumberOfTimeSlices") |
| 597 | + != nifti_time_dim |
| 598 | + and not self.ignore_dcm2niix_errors |
| 599 | + ): |
| 600 | + raise Exception( |
| 601 | + f"NifTi produced has {nifti_time_dim} timing frames, should have {matched_dicom.get('NumberOfTimeSlices')} instead." |
| 602 | + ) |
| 603 | + if matched_json: |
| 604 | + with open(matched_json, "r") as infile: |
| 605 | + matched_json = json.load(infile) |
| 606 | + json_num_frames = len(matched_json.get("FrameDuration", [])) |
| 607 | + if ( |
| 608 | + matched_dicom.get("NumberOfTimeSlices") |
| 609 | + != json_num_frames |
| 610 | + and not self.ignore_dcm2niix_errors |
| 611 | + ): |
| 612 | + raise Exception( |
| 613 | + f"Length of FrameDuration is {json_num_frames} should match {matched_dicom.filename}'s NumberOfTimeSlices value {matched_dicom.get('NumberOfTimeSlices')} instead" |
| 614 | + ) |
579 | 615 |
|
580 | 616 | # we check to see what's missing from our recommended and required jsons by gathering the |
581 | 617 | # output of check_json silently |
@@ -1193,6 +1229,13 @@ def cli(): |
1193 | 1229 | help="Add fields to json output that are useful for ezBIDS or other conversion software. This will de-anonymize" |
1194 | 1230 | " pet2bids output and add AcquisitionDate an AcquisitionTime into the output json.", |
1195 | 1231 | ) |
| 1232 | + parser.add_argument( |
| 1233 | + "--ignore-dcm2niix-errors", |
| 1234 | + action="store_true", |
| 1235 | + default=False, |
| 1236 | + help="Accept any NifTi produced by dcm2niix even if it contains errors. This flag should only be used for " |
| 1237 | + "batch processing and only if you're performing robust QC after the fact.", |
| 1238 | + ) |
1196 | 1239 | return parser |
1197 | 1240 |
|
1198 | 1241 |
|
@@ -1360,6 +1403,7 @@ def main(): |
1360 | 1403 | tempdir_location=cli_args.tempdir, |
1361 | 1404 | silent=cli_args.silent, |
1362 | 1405 | ezbids=cli_args.ezbids, |
| 1406 | + ignore_dcm2niix_errors=cli_args.ignore_dcm2niix_errors, |
1363 | 1407 | ) |
1364 | 1408 |
|
1365 | 1409 | if cli_args.trc: |
|
0 commit comments