This repository has been archived by the owner on May 18, 2021. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* NEW: read_nii.m — read nifti image, header, and load .json when present * octave-compat: disable typechecking * octave-compat: use strcat instead of + for str addition * octave-compat: loadjson not jsondecode * octave-compat: but actually matlab-compat too * fixed jsons with .gz extension * NEW: img.get_nii_coordinates.m — draft function returning voxel positions * BUG: remove assert-3d image volume; * REF: read_nii.m: use extractBefore instead of 'if .nii.gz, else ...' * REF: +img renamed +imutils (as img is freq used as a variable name) * REF: get_nii_coordinates.m : change call to imutils.read_nii * OPT: read_nii.m — Enable option to auto-rescale Siemens raw phase images to radians * Fix newline * octave-compat: isOctave * DOC: Update comment to mention [-pi,pi) limits of the converted output phase Co-authored-by: kousu <[email protected]> Co-authored-by: Alexandre D'Astous <[email protected]>
- Loading branch information
1 parent
16dae31
commit 199b404
Showing
4 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
function [x, y, z] = get_nii_coordinates( nii ) | ||
%GET_NII_COORDINATES Return voxel position "world-coordinates" in mm | ||
% | ||
% [x, y, z] = get_nii_coordinates( niiFile ) | ||
% [x, y, z] = get_nii_coordinates( niiInfo ) | ||
% | ||
% Returns `x, y, z`: three 3-D arrays of voxel positions in mm. | ||
% | ||
% The single input can be given either as: | ||
% | ||
% 1. `niiFile`—A path string to the NIfTI file of interest; or, | ||
% | ||
% 2. `niiInfo`—A struct of the form `niiInfo = niftiinfo( niiFile )` | ||
% | ||
% __NOTE__ | ||
% The function is implemented as | ||
% ``` | ||
% [i, j, k] = ndgrid( [0:niiInfo.ImageSize(1)-1], [0:niiInfo.ImageSize(2)-1], [0:niiInfo.ImageSize(3)-1] ) ; | ||
% [x, y, z] = niiInfo.Transform.transformPointsForward( i, j, k ) ; | ||
% ``` | ||
% __TODO__ | ||
% * function should be further tested. | ||
|
||
% ----------- | ||
%% Check input | ||
narginchk(1,1); | ||
|
||
if [ isstring( nii ) | ischar( nii ) ] & isfile( nii ) | ||
|
||
[~, info] = imutils.read_nii( nii ); | ||
|
||
elseif isstruct( nii ) && isfield( nii, 'ImageSize' ) && ... | ||
isfield( nii, 'Transform' ) && isa( nii.Transform, 'affine3d' ) | ||
|
||
info = nii ; | ||
|
||
else | ||
error('Input must be a path to a .nii file, or a struct returned by `niftiinfo`') | ||
end | ||
|
||
% --------------- | ||
%% Get coordinates | ||
|
||
% voxel indices | ||
[i, j, k] = ndgrid( [0:info.ImageSize(1)-1], [0:info.ImageSize(2)-1], [0:info.ImageSize(3)-1] ) ; | ||
[x, y, z] = info.Transform.transformPointsForward( i, j, k ) ; | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
function [img,info,json] = read_nii( niiFile, options ) | ||
%READ_NII Load NIfTI image, header, and (if present) the accompanying .json sidecar | ||
% | ||
% [img, info] = read_nii( niiFile ) | ||
% [img, info, json] = read_nii( niiFile, options ) | ||
% | ||
% The input `niiFile` is the path to the NIfTI image as a string scalar or | ||
% character vector. When called with 2 output arguments, the function is | ||
% equivalent short-hand for | ||
% | ||
% info = niftiinfo( niiFile ); img = niftiread( info ) | ||
% | ||
% The function checks the `niiFile` parent-folder for the presence of a | ||
% sidecar (an identically named file, but with a .json file extension). | ||
% When such a file exists, the 3rd output is returned as a struct via | ||
% `json = jsondecode( fileread( jsonFile ) );` otherwise, `json = []`. | ||
% | ||
% __OPTIONS__ | ||
% | ||
% The function accepts an `options` struct of parameters (for now, only one) | ||
% as a 2nd argument for which the `.rescale` field can be assigned: | ||
% | ||
% | `options.rescale` | Effect | | ||
% | ------------------| -------------------------------------------------------| | ||
% | 'off' | Rescaling disabled | | ||
% | 'basic' | Rescale according to nii header info | | ||
% | 'auto' [default] | Rescale and convert to physical units when possible | | ||
% | ||
% __NOTE__ | ||
% For now, the sole effect of `'auto'` is to convert Siemens raw phase images | ||
% to physical units (radians), which requires converting from their original | ||
% integer type (between [0,4095]) to a 32-bit "single" float (between [-pi,pi)). | ||
% The json sidecar must be available to verify the Manufacturer and ImageType | ||
% entries. Otherwise, and for all other image inputs `'auto'` reverts to | ||
% `'basic'`. | ||
|
||
DEFAULTS.rescale = 'auto'; | ||
|
||
narginchk(1, 2); | ||
|
||
if nargin == 1 || ~isfield( options, 'rescale' ) || isempty( options.rescale ) | ||
options.rescale = DEFAULTS.rescale; | ||
else | ||
assert( isstruct(options), 'Optional 2nd input must be a struct' ); | ||
assert( ismember( options.rescale, ["off" "basic" "auto"] ), ... | ||
'Invalid assignment to `options.rescale`: Value must be "off", "basic", or "auto"]' ); | ||
end | ||
|
||
info = niftiinfo( niiFile ); | ||
img = niftiread( info ); | ||
|
||
% `extractBefore` should get the correct filename in both .nii and .nii.gz cases | ||
jsonFile = strcat( extractBefore( niiFile, '.nii' ), '.json' ); | ||
|
||
if isfile( jsonFile ) | ||
if isOctave() | ||
json = loadjson( jsonFile ); | ||
else % matlab | ||
json = jsondecode( fileread( jsonFile ) ); | ||
end | ||
else | ||
json = []; | ||
end | ||
|
||
% ------------------- | ||
%% Optional rescaling | ||
if strcmp(options.rescale, 'off') | ||
return | ||
end | ||
|
||
if strcmp(options.rescale, 'basic') | ||
[img, info] = rescale( img, info ); | ||
return | ||
end | ||
|
||
% NOTE: Other approaches to rescaling and/or converting from raw file values | ||
% could be added (including cases where the json sidecar is unavailable) | ||
if isfield( json, 'Manufacturer') && strcmp(json.Manufacturer, 'Siemens') ... | ||
&& strcmp( check_json_image_type(json), 'phase' ) | ||
|
||
[img, info] = convert_siemens_phase( img, info ); | ||
else | ||
[img, info] = rescale( img, info ); | ||
end | ||
|
||
end | ||
|
||
% ----------------------------------------------------------------------------- | ||
%% Local functions | ||
% ----------------------------------------------------------------------------- | ||
function [img, info] = convert_siemens_phase( img0, info0 ) | ||
%CONVERT_SIEMENS_PHASE Return phase img in rad | ||
% | ||
% [img, info] = convert_siemens_phase( img0, info0 ) | ||
% | ||
% Converts from integer-type (between [0,4095]) to a 32-bit "single" float | ||
% (between [-pi,pi)). | ||
|
||
PHASE_SCALING_SIEMENS = 4096; | ||
|
||
if (info0.AdditiveOffset == -PHASE_SCALING_SIEMENS) && (info0.MultiplicativeScaling == 2) | ||
|
||
[img, info] = rescale( img0, info0 ); | ||
|
||
img = single(img)*(pi/PHASE_SCALING_SIEMENS); | ||
|
||
%% Update header: both Matlab simplified and original `raw` portions | ||
info.Datatype = 'single'; | ||
info.BitsPerPixel = 32; | ||
% NB: 16 is the NIfTI code for float; bitpix = number of bits | ||
info.raw.datatype = 16; | ||
info.raw.bitpix = 32; | ||
else | ||
warning( 'The nii header differs from that expected of Siemens phase data.\n%s', ... | ||
'Output values (units) are effectively unknown' ); | ||
[img, info] = rescale( img0, info0 ); | ||
end | ||
|
||
end | ||
|
||
% ----------------------------------------------------------------------------- | ||
function [imgType] = check_json_image_type( json ) | ||
%CHECK_JSON_IMAGE_TYPE Check json; return 'magnitude', 'phase' or 'unknown' | ||
% | ||
% imgType = check_json_image_type( json ) | ||
% | ||
% Checks the `ImageType` field of struct `json` (derived from the json sidecar) | ||
% and returns a char vector `imgType` which may be: | ||
% | ||
% - `magnitude` for magnitude images | ||
% - `phase` for phase images | ||
% - `unknown` otherwise | ||
% | ||
% __NOTE__ | ||
% The check is hardly exhaustive and has only been tested for Siemens MRI data. | ||
assert( nargin<2 ) | ||
|
||
if isempty(json) | ||
imgType = []; | ||
return | ||
end | ||
|
||
assert( isstruct(json) ); | ||
|
||
imgType = 'unknown'; % default | ||
|
||
if myisfield( json, 'ImageType' ) | ||
|
||
isPhase = any( ismember(json.ImageType, "P") ); | ||
isMag = any( ismember(json.ImageType, "M") ); | ||
|
||
if isPhase && isMag | ||
% Both true: json file and/or DICOM issue (hopefully this doesn't occur?) | ||
warning('Ambiguous ImageType entry in json file: Indicates magnitude AND phase?'); | ||
elseif isPhase | ||
imgType = 'phase'; | ||
elseif isMag | ||
imgType = 'magnitude'; | ||
end | ||
|
||
if strcmp( imgType, 'unknown' ) && ~isfield( json, 'Manufacturer' ) || ~strcmp( json.Manufacturer, 'Siemens' ) | ||
warning('Unknown image type. Possibly due to images sourced from non-Siemens MRI') | ||
end | ||
end | ||
|
||
end | ||
% ----------------------------------------------------------------------------- | ||
function [img, info] = rescale( img0, info0 ) | ||
%RESCALE Rescale image according to NIfTI header info | ||
% | ||
% [img, info] = rescale( img, info ) | ||
|
||
img1 = info0.AdditiveOffset + info0.MultiplicativeScaling .* img0; | ||
|
||
%% Check for possible integer overflow/type issues | ||
if isequaln( img1, double(info0.AdditiveOffset) + double(info0.MultiplicativeScaling) * double(img0) ) | ||
|
||
img = img1; | ||
info = info0; | ||
|
||
%% Update header: both Matlab simplified and original `raw` portions | ||
info.MultiplicativeScaling = 1; | ||
info.AdditiveOffset = 0; | ||
info.raw.scl_slope = 1; | ||
info.raw.scl_inter = 0; | ||
|
||
else | ||
warning( ['Aborting default image rescaling to avoid integer overflow.\n' | ||
'Original NIfTI header may contain errors.\n '] ) | ||
|
||
img = img0; | ||
info = info0; | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
function [img,info,json] = read_nii( niiFile ) | ||
%READ_NII Load NIfTI image and header; and, when present, the accompanying .json sidecar | ||
% | ||
% [img, info] = read_nii( niiFile ) | ||
% [img, info, json] = read_nii( niiFile ) | ||
% | ||
% The input `niiFile` is the path to the NIfTI image as a string scalar or | ||
% character vector. When called with 2 output arguments, the function is | ||
% equivalent short-hand for | ||
% | ||
% info = niftiinfo( niiFile ); img = niftiread( info ) | ||
% | ||
% When called with the 3rd output argument, the function checks the parent | ||
% folder of `niiFile` for an identically named file but with a .json file extension. | ||
% When such a file is present, the 3rd output is returned as a struct via | ||
% `json = jsondecode( fileread( jsonFile ) );` otherwise, `json = []`. | ||
arguments | ||
niiFile(1,:) string {mustBeStringScalarOrCharVector, mustBeFile} ; | ||
end | ||
|
||
info = niftiinfo( niiFile ); | ||
img = niftiread( info ); | ||
|
||
if nargout < 3 | ||
return; | ||
end | ||
|
||
[folder, name] = fileparts( niiFile ); | ||
jsonFile = fullfile( folder, name + ".json" ); | ||
|
||
if isfile( jsonFile ) | ||
json = jsondecode( fileread( jsonFile ) ); | ||
else | ||
json = []; | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
function r = isOctave () | ||
% https://stackoverflow.com/a/9838357 | ||
persistent x; | ||
if (isempty (x)) | ||
x = exist ('OCTAVE_VERSION', 'builtin'); | ||
end | ||
r = x; | ||
end |