|
19 | 19 | save_json,
|
20 | 20 | create_file_if_missing,
|
21 | 21 | json_dumps_pretty,
|
| 22 | + add_field_to_json, |
22 | 23 | set_readonly,
|
23 | 24 | is_readonly,
|
24 | 25 | get_datetime,
|
@@ -457,3 +458,125 @@ def convert_sid_bids(subject_id):
|
457 | 458 | lgr.warning('{0} contained nonalphanumeric character(s), subject '
|
458 | 459 | 'ID was cleaned to be {1}'.format(subject_id, sid))
|
459 | 460 | return sid, subject_id
|
| 461 | + |
| 462 | + |
| 463 | +def populate_intended_for(path_to_bids_session): |
| 464 | + """ |
| 465 | + Adds the 'IntendedFor' field to the fmap .json files in a session folder. |
| 466 | + It goes through the session folders and checks what runs have the same |
| 467 | + 'ShimSetting' as the fmaps. If there is no 'ShimSetting' field in the json |
| 468 | + file, we'll use the folder name ('func', 'dwi', 'anat') and see which fmap |
| 469 | + with a matching '_acq' entity. |
| 470 | +
|
| 471 | + If several fmap runs have the same 'ShimSetting' (or '_acq'), it will use |
| 472 | + the first one. Because fmaps come in groups (with reversed PE polarity, |
| 473 | + or magnitude/phase), it adds the same runs to the 'IntendedFor' of the |
| 474 | + corresponding fmaps by checking the '_acq' and '_run' entities. |
| 475 | +
|
| 476 | + Note: the logic behind the way we decide how to populate the "IntendedFor" |
| 477 | + is: we want all images in the session (except for the fmap images |
| 478 | + themselves) to have AT MOST one fmap. (That is, a pair of SE EPI with |
| 479 | + reversed polarities, or a magnitude a phase field map). If there are more |
| 480 | + than one fmap (more than a fmap pair) with the same acquisition parameters |
| 481 | + as, say, a functional run, we will just assign that run to the FIRST pair, |
| 482 | + while leaving the other fmap pairs without any assigned images. If the |
| 483 | + user's intentions were different, he/she will have to manually edit the |
| 484 | + fmap json files. |
| 485 | +
|
| 486 | + Parameters: |
| 487 | + ---------- |
| 488 | + path_to_bids_session : str or os.path |
| 489 | + path to the session folder (or to the subject folder, if there are no |
| 490 | + sessions). |
| 491 | + """ |
| 492 | + lgr.info('') |
| 493 | + lgr.info('Adding "IntendedFor" to the fieldmaps in {}.'.format(path_to_bids_session)) |
| 494 | + |
| 495 | + shim_key = 'ShimSetting' |
| 496 | + lgr.debug('shim_key: {}'.format(shim_key)) |
| 497 | + |
| 498 | + # Resolve path (eliminate '..') |
| 499 | + path_to_bids_session = op.abspath(path_to_bids_session) |
| 500 | + |
| 501 | + # get the BIDS folder (if "data_folder" includes the session, remove it): |
| 502 | + if op.basename(path_to_bids_session).startswith('ses-'): |
| 503 | + bids_folder = op.dirname(path_to_bids_session) |
| 504 | + else: |
| 505 | + bids_folder = path_to_bids_session |
| 506 | + |
| 507 | + fmap_dir = op.join(path_to_bids_session, 'fmap') |
| 508 | + if not op.exists(fmap_dir): |
| 509 | + lgr.warning('Fmap folder not found in {}.'.format(path_to_bids_session)) |
| 510 | + lgr.warning('We cannot add the IntendedFor field') |
| 511 | + return |
| 512 | + |
| 513 | + # Get a list of all fmap json files in the session: |
| 514 | + # (we will remove elements later on, so don't just iterate) |
| 515 | + fmap_jsons = sorted([j for j in glob(op.join(path_to_bids_session, 'fmap/*.json'))]) |
| 516 | + |
| 517 | + # Get a set with all non-fmap json files in the session (set is easier): |
| 518 | + # We also exclude the SBRef files. |
| 519 | + session_jsons = set( |
| 520 | + j for j in glob(op.join(path_to_bids_session, '*/*.json')) if not ( |
| 521 | + j in fmap_jsons |
| 522 | + # j[:-5] removes the '.json' from the end |
| 523 | + or j[-5].endswith('_sbref') |
| 524 | + ) |
| 525 | + ) |
| 526 | + |
| 527 | + # Loop through all the fmap json files and, for each one, find which other |
| 528 | + # non-fmap images in the session have the same shim settings. Those that |
| 529 | + # match are added to the intended_for list and removed from the list of |
| 530 | + # non-fmap json files in the session (since they have already assigned to |
| 531 | + # a fmap). |
| 532 | + # After finishing with all the non-fmap images in the session, we go back |
| 533 | + # to the fmap json file list, and find any other fmap json files of the |
| 534 | + # same acquisition type and run number (because fmaps have several files: |
| 535 | + # normal- and reversed-polarity, or magnitude and phase, etc.) We add the |
| 536 | + # same IntendedFor list to those other corresponding fmap json files, and |
| 537 | + # remove them from the list of available fmap json files. |
| 538 | + # Once we have gone through all the fmap json files, we are done. |
| 539 | + jsons_accounted_for = set() |
| 540 | + for fm_json in fmap_jsons: |
| 541 | + lgr.debug('Looking for runs for {}'.format(fm_json)) |
| 542 | + data = load_json(fm_json) |
| 543 | + if shim_key in data.keys(): |
| 544 | + fm_shims = data[shim_key] |
| 545 | + else: |
| 546 | + fm_shims = fm_json.name.split('_acq-')[1].split('_')[0] |
| 547 | + if fm_shims.lower() == 'fmri': |
| 548 | + fm_shims = 'func' |
| 549 | + |
| 550 | + intended_for = [] |
| 551 | + for image_json in session_jsons: |
| 552 | + data = load_json(image_json) |
| 553 | + if shim_key in data.keys(): |
| 554 | + image_shims = data[shim_key] |
| 555 | + else: |
| 556 | + # we just use the acquisition type (the name of the folder: |
| 557 | + # 'anat', 'func', ...) |
| 558 | + image_shims = op.basename(op.dirname(image_json)) |
| 559 | + if image_shims == fm_shims: |
| 560 | + # BIDS specifies that the intended for are: |
| 561 | + # - **image** files |
| 562 | + # - path relative to the **subject level** |
| 563 | + image_json_relative_path = op.relpath(image_json, start=bids_folder) |
| 564 | + # image_json_relative_path[:-5] removes the '.json' extension: |
| 565 | + intended_for.append( |
| 566 | + image_json_relative_path[:-5] + '.nii.gz' |
| 567 | + ) |
| 568 | + jsons_accounted_for.add(image_json) |
| 569 | + if len(intended_for) > 0: |
| 570 | + fm_json_name = op.basename(fm_json) |
| 571 | + # get from "_acq-"/"_run-" to the next "_": |
| 572 | + acq_str = '_acq-' + fm_json_name.split('_acq-')[1].split('_')[0] |
| 573 | + run_str = '_run-' + fm_json_name.split('_run-')[1].split('_')[0] |
| 574 | + # Loop through all the files that have the same "acq-" and "run-" |
| 575 | + intended_for = sorted([str(f) for f in intended_for]) |
| 576 | + for other_fm_json in glob(op.join(path_to_bids_session, 'fmap/*' + acq_str + '*' + run_str + '*.json')): |
| 577 | + # add the IntendedFor field to the json file: |
| 578 | + add_field_to_json(other_fm_json, {"IntendedFor": intended_for}) |
| 579 | + fmap_jsons.remove(other_fm_json) |
| 580 | + # Remove the runs accounted for from the session_jsons list, so that |
| 581 | + # we don't assign another fmap to this image: |
| 582 | + session_jsons -= jsons_accounted_for |
0 commit comments