nhdr_nrrd_read.m 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. % Read NHDR/NRRD files
  2. %
  3. % headerInfo = nhdr_nrrd_read(nrrdFileName, bReadData)
  4. %
  5. % Inputs:
  6. % * nrrdFileName (char): path to NHDR/NRRD file, either a detached header,
  7. % a detached header pointing to detached data files or a NRRD standalone
  8. % file with header and data included
  9. % * bReadData (bool): set to true to read the data and store it in
  10. % headerInfo.data, set to false to just import the header without the data
  11. %
  12. % Outputs:
  13. % * headerInfo (struct): contains all the fields specified in the file
  14. % header. If data was read it is contained in the 'data' field. That
  15. % structure can be fed to the nhdr_nrrd_write module as is and produce
  16. % valid NHDR/NRRD files.
  17. %
  18. % Format definition:
  19. % http://teem.sourceforge.net/nrrd/format.html
  20. %
  21. % A few supported NRRD features:
  22. % - detached headers with all variants of 'data file:'
  23. % - raw, txt/text/ascii, gz/gzip encodings
  24. % - definition of space and orientation
  25. % - handling of diffusion-weighted MRI data with '<key>:=<value>' lines
  26. % 'modality:=DWMRI', 'DWMRI_b-value:=' and 'DWMRI_gradient_0000:=',
  27. % 'DWMRI_gradient_0001:=', 'DWMRI_gradient_0002:=', etc.
  28. % (see https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:Nrrd_format)
  29. %
  30. %
  31. % Other features:
  32. % - exits cleanly upon error (no file should be left open)
  33. % - informative error messages whenever possible
  34. % - warnings printed to console
  35. % - NHDR/NRRD writer fills out missing fields by inspecting the data
  36. % whenever possible
  37. %
  38. %
  39. % Unsupported features:
  40. %
  41. % In general, any header field that we were unable to parse is reported
  42. % in a message printed to the console. Specific examples of
  43. % unsupported features include:
  44. %
  45. % - reading data along more than 1 dimension or along a dimension other
  46. % than the slowest (last) axis specified by the optional <subdim>, as in
  47. % 'data file: <format> <min> <max> <step> [<subdim>]' or 'data file: LIST
  48. % [<subdim>]'
  49. % - storing all the comments found in headers
  50. % - hex encoding
  51. % - bz2/bzip2 encoding
  52. % - byte skip; can only accept -1 with raw encoding, 0 for all other
  53. % encodings
  54. % - line skip: can only accept 0
  55. % - checking that field specifications of the form '<field>: <desc>'
  56. % appear no more than once in the NRRD header (unlike '<key>:=<value>'
  57. % lines)
  58. %
  59. % Date: July 2018
  60. % Author: Gaetan Rensonnet
  61. %
  62. %
  63. %
  64. % Notes :
  65. %
  66. % This is intended as a more comprehensive and accurate implementation of
  67. % the NRRD format specification than most of the Matlab scripts that have
  68. % been proposed so far. We try to fail graciously when an unsupported
  69. % feature of the NRRD format is encountered. One of our main contributions
  70. % is to propose a writer module which is compatible with the read module,
  71. % in that the output of one can be given as an argument to the other to
  72. % read or produce equivalent NHDR/NRRD files. This is still a version with
  73. % much room for improvement.
  74. %
  75. % The following contributions inspired parts of our Matlab read/write
  76. % modules:
  77. %
  78. % 1. The body of the writer module was pretty much written from scratch but
  79. % the general structure of the reader's main body is based on the Matlab
  80. % functions maReadNrrdHeader.m and maReadNrrdData.m by marc@bwh.harvard.edu
  81. % and kquintus@bwh.harvard.edu (unpublished). Many additions were made and
  82. % a few bugs fixed.
  83. %
  84. % 2. Jeff Mather's NRRD reader
  85. % (http://nl.mathworks.com/matlabcentral/fileexchange/34653-nrrd-format-file-reader)
  86. % and
  87. % http://jeffmatherphotography.com/dispatches/2012/02/writing-a-file-reader-in-matlab/)
  88. % provided the auxiliary functions:
  89. % - adjustEndian
  90. % - getDatatype, which we renamed nrrd_getMatlabDataType and now throws
  91. % a gracious error if it encounters 'block'-type data,
  92. % - readData was adapted to include a cross-platform fix to delete
  93. % temporary files when using gzip encoding. David Feng's fix used a
  94. % Windows-specific command to delete temporary files
  95. % (https://nl.mathworks.com/matlabcentral/fileexchange/50830-nrrd-format-file-reader).
  96. %
  97. % 3. mdcacio's nrrdWriter
  98. % (https://nl.mathworks.com/matlabcentral/fileexchange/48621-nrrdwriter-filename--matrix--pixelspacing--origin--encoding-)
  99. % provided the auxiliary functions:
  100. % - writeData(): we got rid of the 'unexpected end of input stream when
  101. % attempting to gunzip the file' error when using gzip encoding, which we
  102. % later found had been fixed by Quan Chen independenlty and in a similar
  103. % manner.
  104. % - setDatatype(), which we renamed get_nrrd_datatype() and is the
  105. % reciprocal of getDatatype
  106. %
  107. function headerInfo = nhdr_nrrd_read(nrrdFileName, bReadData)
  108. [mainFpath,mainFileName,mainFext] = fileparts(nrrdFileName);
  109. headerInfo = struct();
  110. % default header:
  111. headerInfo.content = mainFileName; % default value, overwritten if content field is set
  112. headerInfo.data = [];
  113. fidr = fopen(nrrdFileName, 'r');
  114. if (fidr == -1)
  115. error('ABORT: %s does not exist.\n', nrrdFileName);
  116. end
  117. try
  118. if ~(strcmpi(mainFext, '.nhdr') || strcmpi(mainFext, '.nrrd'))
  119. warning('%s looks like a %s file, not a nhdr or nrrd file.\n', nrrdFileName, mainFext );
  120. end
  121. % Magic line
  122. cs = fgetl(fidr);
  123. assert(numel(cs) >= 8 && isequal(cs(1:4), 'NRRD'),...
  124. 'Bad signature. First line should be magic line of type "NRRD000X" with X an integer between 1 and 5.'); % FIXME should throw an error if a bad integer is provided
  125. nrrd_version = sscanf(cs(5:end), '%d');
  126. if nrrd_version > 5
  127. error('This reader only supports versions of the NRRD file format up to 5. Detected %d.', nrrd_version)
  128. end
  129. % Always optional: defining orientation
  130. define_orientation = 0; % internal-use variable
  131. % Parse header
  132. while ~feof(fidr)
  133. cs = fgetl(fidr); % content string
  134. if isempty(cs)
  135. % End of header
  136. break;
  137. end
  138. if foundKeyword( 'CONTENT:', cs )
  139. headerInfo.content = strtrim( cs( length('CONTENT:')+1:end ) );
  140. elseif foundKeyword('TYPE:', cs )
  141. headerInfo.type = strtrim( cs( length('TYPE:')+1:end ) );
  142. elseif foundKeyword('ENDIAN:', cs )
  143. headerInfo.endian = strtrim( cs( length('ENDIAN:')+1:end ) );
  144. elseif foundKeyword('ENCODING:', cs )
  145. headerInfo.encoding = strtrim( cs( length('ENCODING:')+1:end ) );
  146. elseif foundKeyword('DIMENSION:', cs )
  147. headerInfo.dimension = sscanf( cs( length('DIMENSION:')+1:end ), '%i' );
  148. elseif foundKeyword('SIZES:', cs )
  149. iSizes = sscanf( cs(length('SIZES:')+1:end), '%i' );
  150. headerInfo.sizes = iSizes(:)'; % store as row vector
  151. elseif foundKeyword('KINDS:', cs )
  152. headerInfo.kinds = extractStringList( cs(length('KINDS:')+1:end) ); % bug fixed with extractStringList where 2 entries are present
  153. % FIXME: check that axis sizes match each kind according to nrrd standard
  154. elseif foundKeyword('SPACE:', cs )
  155. % Starts defining orientation (either space or space dimension,
  156. % not both)
  157. define_orientation = 1;
  158. if isfield(headerInfo, 'spacedimension')
  159. fprintf(['WARNING nhdr_nrrd_read %s:\n ''space'' field specifier will ' ...
  160. 'be checked for consistency but will be ignored afterwards ' ...
  161. 'because ''space dimension'' was specified before.\n'], fopen(fidr));
  162. end
  163. tmp_space = strtrim( cs( length('SPACE:')+1:end ) );
  164. tmp_spacedimension = nrrd_getSpaceDimensions(tmp_space);
  165. if tmp_spacedimension <= 0
  166. error('%s: unrecognized ''space'' descriptor ''%s''.', fopen(fidr), tmp_space)
  167. end
  168. if isfield(headerInfo, 'spacedimension')
  169. % internal_spacedimension already set
  170. if internal_spacedimension ~= tmp_spacedimension
  171. error(['%s: ''space'' field specifier implies a spatial ' ...
  172. '(world) dimension equal to %d, which differs from ' ...
  173. 'the ''space dimension'' field specifier set to %d.'],...
  174. fopen(fidr), tmp_spacedimension, internal_spacedimension)
  175. end
  176. % if no inconsistencies found, just ignore space field
  177. else
  178. % Set space info for the first time:
  179. headerInfo.space = tmp_space;
  180. internal_spacedimension = tmp_spacedimension; % for internal use only
  181. end
  182. elseif foundKeyword('SPACE DIMENSION:', cs)
  183. % Starts defining orientation (either space or space dimension,
  184. % not both)
  185. define_orientation = 1;
  186. if isfield(headerInfo, 'space')
  187. fprintf(['WARNING nhdr_nrrd_read %s:\n ''space dimension'' field specifier ' ...
  188. ' will be checked for consistency but will be ignored afterwards ' ...
  189. 'because ''space'' was specified before.\n'],...
  190. fopen(fidr));
  191. end
  192. tmp_spacedimension = sscanf( cs( length('SPACE DIMENSION:')+1:end), '%i' );
  193. if numel(tmp_spacedimension) ~= 1
  194. error(['%s: ''space dimension'' should be specified as one' ...
  195. ' integer number. Found %d element(s) instead.'],...
  196. fopen(fidr), numel(tmp_spacedimension))
  197. end
  198. if tmp_spacedimension <= 0
  199. error('%s: ''space dimension'' should be specified as a strictly positive integer (found %d).',...
  200. fopen(fidr), tmp_spacedimension)
  201. end
  202. if isfield(headerInfo, 'space')
  203. if tmp_spacedimension ~= internal_spacedimension
  204. error(['%s: ''space dimension'' field specifier set to %d, ' ...
  205. 'which differs from the space (world) dimension implied by the ' ...
  206. '''space'' field specifier which is equal to %d.'],...
  207. fopen(fidr), tmp_spacedimension, internal_spacedimension)
  208. end
  209. % if no inconsistencies found, just ignore space dimension
  210. % field
  211. else
  212. % Set space info for the first time:
  213. headerInfo.spacedimension = tmp_spacedimension;
  214. internal_spacedimension = tmp_spacedimension; % for internal use only
  215. end
  216. elseif foundKeyword('SPACE DIRECTIONS:', cs )
  217. % Required if orientation defined but must come after space or
  218. % space dimension
  219. % space directions: <vector[0]> <vector[1]> ... <vector[dim-1]>
  220. % The format of the <vector> is as follows. The vector is
  221. % delimited by "(" and ")", and the individual components are
  222. % comma-separated.
  223. if ~define_orientation
  224. error('%s: field specifier ''space directions'' cannot be set before ''space'' or ''space dimension''.',fopen(fidr))
  225. end
  226. space_dir_tmp = strtrim(cs(length('SPACE DIRECTIONS:')+1:end)); % remove leading and trailing spaces
  227. spacedir_vecs = strsplit(space_dir_tmp); % cell array of strings after split at {' ','\f','\n','\r','\t','\v'}
  228. SD_data = zeros(internal_spacedimension, internal_spacedimension);
  229. % Check each vector: either none or (f1,...,f_spaceDim) with fi
  230. % a floating-point number
  231. cnt_space_vectors = 0;
  232. for i = 1:length(spacedir_vecs)
  233. none_start_index = strfind(lower(spacedir_vecs{i}), 'none');
  234. if ~isempty(none_start_index)
  235. % Axis-specific entry contains substring none
  236. if ~strcmpi(spacedir_vecs{i}, 'none')
  237. fprintf(['WARNING nhdr_nrrd_read: detected %s instead of ' ...
  238. 'expected none for axis %d of the per-axis field specifications "space directions:".\n' ...
  239. ' There should be no quotation marks, parentheses or any other characters, just plain none.\n'],...
  240. spacedir_vecs{i}, i);
  241. % Clean none vector specification
  242. spacedir_vecs{i} = 'none';
  243. end
  244. else
  245. % Axis-specific entry is a numerical vector
  246. cnt_space_vectors = cnt_space_vectors + 1;
  247. if cnt_space_vectors > internal_spacedimension
  248. error(['%s:\n ''space directions'' field specifier: ' ...
  249. 'number of space vectors detected exceeds space (world)' ...
  250. ' dimension, which is equal to %d.'],...
  251. fopen(fidr), internal_spacedimension)
  252. end
  253. btw_parentheses = spacedir_vecs{i}(1) == '(' ...
  254. && spacedir_vecs{i}(end) == ')';
  255. axis_vector = regexprep(spacedir_vecs{i}, '[()]', ''); % stripped off all parens
  256. if ~btw_parentheses
  257. fprintf(['WARNING nhdr_nrrd_read: vector should be delimited ' ...
  258. 'by parentheses for axis %d of the per-axis field ' ...
  259. 'specifications "space directions:".\n' ...
  260. ' At least one missing parenthesis in ''%s''.\n'],...
  261. i, spacedir_vecs{i})
  262. end
  263. % Clean up by forcing single enclosing parentheses:
  264. spacedir_vecs{i} = ['(', axis_vector, ')'];
  265. % Check vector and extract numerical data
  266. vector_entries = strsplit(axis_vector, ',');
  267. if length(vector_entries) ~= internal_spacedimension
  268. error(['%s:\n vector for data axis %d (space axis %d) of the ' ...
  269. 'per-axis field specifications "space directions:" should ' ...
  270. 'contain %d entries corresponding to the space (or world) dimension ' ...
  271. 'specified in the "space" or "space dimension" field specification.' ...
  272. ' Found %d here.'],...
  273. fopen(fidr), i, cnt_space_vectors, internal_spacedimension, ...
  274. length(vector_entries))
  275. end
  276. for j = 1:length(vector_entries)
  277. vector_entry = sscanf(vector_entries{j}, '%f');
  278. if isempty(vector_entry)
  279. error(['%s\n in field specification "space directions:", ' ...
  280. 'vector for data axis %d (space axis %d) too short. ' ...
  281. 'Detected %d entries instead of expected %d corresponding to ' ...
  282. 'space (world) dimension.'],...
  283. fopen(fidr), i, cnt_space_vectors, j-1, internal_spacedimension)
  284. end
  285. SD_data(j, cnt_space_vectors) = vector_entry;
  286. end
  287. end
  288. end
  289. % Store array of cleaned up strings
  290. headerInfo.spacedirections = spacedir_vecs; % cell array of strings, ideally of the form {'(1,0,0)' '(0,2,0.1)' 'none' '(0.1,0.1,1.1)'} if internal_spacedimension==3
  291. % Extract numerical data more leniently:
  292. SD_data_chk = strrep(space_dir_tmp, 'none', '' ); % ignore "none" entries
  293. SD_data_chk = extractNumbersWithout(SD_data_chk, {'(',')',',', '"', ''''} ); % detects numbers up to first non numerical entry
  294. if numel(SD_data_chk) ~= (internal_spacedimension)^2
  295. error(['Expected ''space directions'' to specify a %d-by-%d matrix ' ...
  296. '(%d elements in total) in agreement with world space dimension.' ...
  297. ' Found %d element(s) instead.\n'],...
  298. internal_spacedimension, internal_spacedimension,...
  299. (internal_spacedimension)^2, numel(SD_data_chk));
  300. end
  301. % Sanity check
  302. if ~isequal(SD_data_chk(:), SD_data(:))
  303. error(['%s:\n ''space directions'' field specifier: couldn''t' ...
  304. ' read space vectors. Please refer to the NRRD format definition.'],...
  305. fopen(fidr))
  306. end
  307. headerInfo.spacedirections_matrix = SD_data;
  308. % Correctness of field specification is checked below after
  309. % whole header is parsed because we need to be sure that the
  310. % "dimension" basic field specification was set
  311. elseif foundKeyword('SPACE UNITS:', cs )
  312. % Always optional, must come after space or space dimension
  313. if define_orientation ~= 1
  314. error('Field specification ''space units'' cannot be specified before ''space'' or ''space dimension''.')
  315. end
  316. space_units_tmp = strrep( cs(length('SPACE UNITS:')+1:end), 'none', ''); % ignore none entries
  317. % FIXME: ideally, check that the sum of elements including none and
  318. % " " matches headerInfo.dimension, i.e. the total dimension, as
  319. % specified in the standard. Standard a bit unclear: should
  320. % unknown units be specified with "???", "none" or "" ? (quotes
  321. % seem to be required).
  322. headerInfo.spaceunits = extract_spaceunits_list( space_units_tmp );
  323. if length(headerInfo.spaceunits) ~= internal_spacedimension
  324. error(['Expected ''space units'' to contain %d elements enclosed in double quotes' ...
  325. ' to match the ''space'' or ''space dimension'' field but found the following ' ...
  326. '%d element(s):\n%s'],...
  327. internal_spacedimension, length(headerInfo.spaceunits), sprintf('%s\t',headerInfo.spaceunits{:}))
  328. end
  329. elseif foundKeyword('SPACE ORIGIN:', cs )
  330. % Always optional, must come after space or space dimension
  331. assert(define_orientation==1, ...
  332. sprintf(['%s: field ''space origin'' cannot be specified' ...
  333. ' before ''space'' or ''space dimension''.'], ...
  334. fopen(fidr)));
  335. iSO = extractNumbersWithout( cs(length('SPACE ORIGIN:')+1:end), {'(',')',','} );
  336. assert(numel(iSO) == internal_spacedimension,...
  337. sprintf(['%s: expected ''space origin'' to specify a ' ...
  338. '%d-element vector to match the ''space'' or ' ...
  339. '''space dimension'' field but found %d ' ...
  340. 'element(s).'],...
  341. fopen(fidr),internal_spacedimension, numel(iSO)) );
  342. headerInfo.spaceorigin = iSO(:);
  343. elseif foundKeyword('MEASUREMENT FRAME:', cs )
  344. % Always optional, must come after space or space dimension
  345. assert(define_orientation==1,...
  346. sprintf(['%s: field ''measurement frame'' cannot be ' ...
  347. 'specified before ''space'' or ''space dimension''.'],...
  348. fopen(fidr)));
  349. measframe_str = strrep( cs(length('MEASUREMENT FRAME:')+1:end), 'none', '');
  350. iMF = extractNumbersWithout( measframe_str, {'(',')',','} ); % fails if non number entries are not previously removed
  351. assert(numel(iMF) == (internal_spacedimension)^2,...
  352. sprintf(['%s: expected ''measurement frame'' to specify a ' ...
  353. '%d-by-%d matrix (%d total elements) but found %d ' ...
  354. 'element(s).'],...
  355. fopen(fidr), internal_spacedimension, ...
  356. internal_spacedimension, (internal_spacedimension)^2,...
  357. numel(iMF)));
  358. headerInfo.measurementframe = reshape(iMF(:), [internal_spacedimension, internal_spacedimension]);
  359. % FIXME: this will gladly accept '1 0 0 0 1 0 0 0 1' instead of
  360. % '(1,0,0) (0,1,0) (0,0,1)' for instance. But it should have
  361. % length spacedimension according to standard so this is not too bad.
  362. elseif foundKeyword('THICKNESSES:', cs )
  363. sThicknesses = extractStringList( cs(length('THICKNESSES:')+1:end) ); % fixed bug with extractStringList where 2 entries are present
  364. iThicknesses = [];
  365. lenThicknesses = length( sThicknesses );
  366. for iI=1:lenThicknesses
  367. iThicknesses = [iThicknesses, str2double(sThicknesses{iI}) ];
  368. end
  369. headerInfo.thicknesses = iThicknesses;
  370. elseif foundKeyword('CENTERINGS:', cs )
  371. headerInfo.centerings = extractStringList( cs(length('CENTERINGS:')+1:end ) ); % fixed bug with extractStringList where 2 entries are present
  372. elseif foundKeyword('LINE SKIP:', cs) || foundKeyword('LINESKIP', cs)
  373. if foundKeyword('LINE SKIP:', cs)
  374. headerInfo.lineskip = sscanf( cs( length('LINE SKIP:')+1:end ), '%d' );
  375. else
  376. headerInfo.lineskip = sscanf( cs( length('LINESKIP:')+1:end ), '%d' );
  377. end
  378. assert(headerInfo.lineskip >= 0,...
  379. sprintf(['Field lineskip or line skip should be greater' ...
  380. ' than or equal to zero, detected %d.'], ...
  381. headerInfo.lineskip));
  382. elseif foundKeyword('BYTE SKIP:', cs) || foundKeyword('BYTESKIP:', cs)
  383. if foundKeyword('BYTE SKIP:', cs)
  384. headerInfo.byteskip = sscanf( cs( length('BYTE SKIP:')+1:end ), '%d' );
  385. else
  386. headerInfo.byteskip = sscanf( cs( length('BYTESKIP:')+1:end ), '%d' );
  387. end
  388. assert(headerInfo.byteskip >= -1, ...
  389. sprintf(['Field byteskip or byte skip can only take ' ...
  390. 'non-negative integer values or -1, detected %d.'], ...
  391. headerInfo.byteskip));
  392. elseif foundKeyword('MODALITY', cs )
  393. headerInfo.modality = strtrim( extractKeyValueString( cs(length('MODALITY')+1:end ) ) );
  394. elseif foundKeyword('DWMRI_B-VALUE', cs )
  395. headerInfo.bvalue = str2double( extractKeyValueString( cs(length('DWMRI_B-VALUE')+1:end ) ) );
  396. elseif foundKeyword('DWMRI_GRADIENT_', cs )
  397. [iGNr, dwiGradient] = extractGradient(cs(length('DWMRI_GRADIENT_')+1:end ));
  398. headerInfo.gradients(iGNr+1,:) = dwiGradient;
  399. % FIXME: make it more general than numbering limited to 4 digits?
  400. elseif foundKeyword('DATA FILE:', cs ) || foundKeyword('DATAFILE:', cs)
  401. % This tells us it is a detached header and ends it. 3 possibilities here
  402. field_value = strtrim( cs(length('DATA FILE:')+1:end) );
  403. if foundKeyword('DATAFILE:', cs)
  404. field_value = strtrim( cs(length('DATAFILE:')+1:end) );
  405. end
  406. [filelist, LIST_mode, subdim] = extract_datafiles(field_value);
  407. headerInfo.datafiles = filelist;
  408. % In LIST mode, filelist is empty and is filled by reading the
  409. % rest of the header
  410. if LIST_mode
  411. datafile_cnt = 0;
  412. while ~feof(fidr)
  413. cs = fgetl(fidr);
  414. if isempty(cs)
  415. break;
  416. end
  417. datafile_cnt = datafile_cnt + 1;
  418. headerInfo.datafiles{datafile_cnt} = cs;
  419. end
  420. end
  421. % We could technically break the loop here bc data file/datafile is
  422. % supposed to close a header; however I think it does no harm to
  423. % keep parsing (in LIST_mode, end of file reached anyways)
  424. else
  425. % see if we are dealing with a comment
  426. csTmp = strtrim( cs );
  427. if csTmp(1)~='#' && ~strcmp(cs(1:4),'NRRD')
  428. fprintf('WARNING nhdr_nrrd_read: Could not parse input line: ''%s'' \n', cs );
  429. % REVIEW: is it better to blindly write it to the output
  430. % structure in order to be able to write the exact same file
  431. % later on ?
  432. end
  433. end
  434. end
  435. % Check for required fields
  436. % REVIEW: should this be checked only when the data is read? People
  437. % might be interested in just parsing the header no matter what is
  438. % in it...)
  439. assert(isfield(headerInfo, 'sizes'), 'Missing required ''sizes'' field in header');
  440. assert(isfield(headerInfo, 'dimension'), 'Missing required ''dimension'' field in header');
  441. assert(isfield(headerInfo, 'encoding'), 'Missing required ''encoding'' field in header');
  442. assert(isfield(headerInfo, 'type'), 'Missing required ''type'' field in header');
  443. % Check other cross-field dependencies, etc.
  444. % TODO: assert lengths of all
  445. % fields specified, check that kinds and axis sizes match, etc.
  446. if isfield(headerInfo, 'byteskip') && headerInfo.byteskip == -1
  447. assert( strcmpi(headerInfo.encoding, 'raw'), ...
  448. sprintf('byte skip value of -1 is only valid with raw encoding. See definition of NRRD File Format for more information.\n'));
  449. end
  450. matlabdatatype = nrrd_getMatlabDataType(headerInfo.type);
  451. % endian field required if data defined on 2 bytes or more
  452. if ~(any(strcmpi(headerInfo.encoding,{'txt', 'text', 'ascii'})) || any(strcmpi(matlabdatatype, {'int8', 'uint8'})))
  453. assert(isfield(headerInfo, 'endian'), 'Missing required ''endian'' field in header');
  454. else
  455. if ~isfield(headerInfo,'endian')
  456. headerInfo.endian = 'little'; % useless but harmless, just for code compatibility because endian field may be accessed later on while reading data
  457. end
  458. end
  459. % Space and orientation information
  460. if define_orientation
  461. assert(isfield(headerInfo, 'spacedirections'), ...
  462. ['Missing field ''space directions'', required if either' ...
  463. ' ''space'' or ''space dimension'' is set.']);
  464. if length(headerInfo.spacedirections) ~= headerInfo.dimension
  465. fprintf(['WARNING nhdr_nrrd_read %s:\n Unexpected format found for ''space directions'' specifier.\n',...
  466. ' Expected none entries and vectors delimited by parentheses such as (1.2,0.2,0.25), ', ...
  467. 'the number of entries being equal to the dimension field specification.\n',...
  468. ' See definition of NRRD File Format for more information.\n'], fopen(fidr));
  469. end
  470. end
  471. % TODO: add support for positive line skips
  472. if isfield(headerInfo, 'lineskip') && headerInfo.lineskip > 0
  473. assert(isfield(headerInfo, 'byteskip') && headerInfo.byteskip == -1, ...
  474. sprintf(['lineskip option is currently not supported and can ' ...
  475. 'only be set to zero unless raw encoding is used and ' ...
  476. 'byte skip is set to -1 (which cancels the effect of ' ...
  477. 'lineskip altogether).']));
  478. end
  479. % TODO: add support for positive byte skips (see line skip)
  480. if isfield(headerInfo, 'byteskip') && ~strcmpi(headerInfo.encoding,'raw')
  481. assert( headerInfo.byteskip == 0, ...
  482. sprintf('byte skip option with non raw encoding is currently not supported and can only be set to zero.\n'));
  483. end
  484. if isfield(headerInfo, 'byteskip') && strcmpi(headerInfo.encoding, 'raw')
  485. assert( headerInfo.byteskip == -1, ...
  486. sprintf('non-negative byte skip values with raw encoding are currently not supported; byte skip can only be set to -1.\n'));
  487. end
  488. catch me
  489. % Clean up before raising error
  490. fclose(fidr);
  491. rethrow(me);
  492. end
  493. % Read the data. Detect if data is in the file or in a detached data file
  494. % and check what type of detached file it is if need be.
  495. if bReadData
  496. N_data_tot = prod(headerInfo.sizes);
  497. if isfield(headerInfo, 'datafiles')
  498. % This is a detached header file
  499. % We no longer need to read the header (closing it before reading
  500. % all the detached data files is safer)
  501. fclose(fidr);
  502. % TODO: we currently only read slices along the slowest axis (i.e.,
  503. % last coordinate)
  504. if ~isempty(subdim) && subdim~=headerInfo.dimension
  505. error(['(detached header): reading data from slices along axis other than the last' ...
  506. ' (i.e. slowest) one, is currently not supported.\n' ...
  507. 'Last argument [<subdim>] in ''data file'' or ''datafile'' field ' ...
  508. 'should be removed or set equal to %d, which is the detected' ...
  509. ' ''dimension'' field value, for now.'],...
  510. headerInfo.dimension);
  511. end
  512. % Read data chunk by chunk from detached data files
  513. N_data_files = length(headerInfo.datafiles);
  514. assert(mod(N_data_tot, N_data_files)==0, ...
  515. sprintf(['Number of detected data files (%d) does not divide total' ...
  516. ' number of values contained in data %d obtained from prod(sizes=[%s]).\n'],...
  517. N_data_files, N_data_tot, sprintf('%d ',headerInfo.sizes)));
  518. N_data_per_file = N_data_tot/N_data_files;
  519. headerInfo.data = zeros(headerInfo.sizes, matlabdatatype); % specify right type of zeros, otherwise double by default
  520. for i = 1:N_data_files
  521. % Check type of detached data file
  522. [~,fname_data,ext_data] = fileparts(headerInfo.datafiles{i}); % redundant because done above
  523. data_ind = (i-1)*N_data_per_file+1:i*N_data_per_file;
  524. if strcmpi(ext_data,'.nhdr')
  525. error(['datafile %di/%d: nhdr file should not be used as ' ...
  526. 'detached data file.'], ...
  527. i, length(headerInfo.datafiles));
  528. elseif strcmpi(ext_data, '.nrrd')
  529. % Detached NRRD file
  530. bRead_Detached_Data = true;
  531. tmp_struct = nhdr_nrrd_read(fullfile(mainFpath, ...
  532. [fname_data, ext_data]), ...
  533. bRead_Detached_Data); % recursive call
  534. % TODO: check the rest of the structure for
  535. % inconsistencies with metadata from header
  536. assert(N_data_files==1 || headerInfo.dimension == tmp_struct.dimension +1, ...
  537. sprintf(['Detached header %s: the number of dimensions in detached ' ...
  538. 'nrrd data file %d/%d (%s) should be one fewer than that of' ...
  539. ' the header file.\nDetected %d instead of %d.\nDifferent ' ...
  540. 'datafile dimensions as specified by the nrrd standard are' ...
  541. ' not supported as of now.\n'],...
  542. fopen(fidr), i, N_data_files, headerInfo.datafiles{i},...
  543. tmp_struct.dimension, headerInfo.dimension-1 ));
  544. headerInfo.data(data_ind) = tmp_struct.data(:);
  545. % Store detached data header for last detached file seen,
  546. % assuming that all are the same. Do not store data though
  547. % as it would be redundant.
  548. tmp_struct = rmfield(tmp_struct, 'data');
  549. headerInfo.detached_header = tmp_struct;
  550. else
  551. % e.g., detached .raw file
  552. fid_data = fopen( fullfile(mainFpath, [fname_data, ext_data]), 'r');
  553. if( fid_data < 1 )
  554. error(['While reading detached header file %s:\ndetached ' ...
  555. 'data file number %d/%d (%s) could not be opened.'], ...
  556. nrrdFileName, i, N_data_files, headerInfo.datafiles{i});
  557. end
  558. try
  559. tmp_data = readData(fid_data, N_data_per_file, headerInfo.encoding, matlabdatatype);
  560. fclose(fid_data);
  561. catch me_detached
  562. fclose(fid_data);
  563. rethrow(me_detached);
  564. end
  565. tmp_data = adjustEndian(tmp_data, headerInfo.endian);
  566. headerInfo.data(data_ind) = tmp_data(:);
  567. end
  568. end
  569. else
  570. % This is a NRRD standalone file: read data directly from it
  571. try
  572. headerInfo.data = readData(fidr, N_data_tot, headerInfo.encoding, matlabdatatype);
  573. fclose(fidr);
  574. catch me_detached
  575. fclose(fidr);
  576. rethrow(me_detached);
  577. end
  578. headerInfo.data = adjustEndian(headerInfo.data, headerInfo.endian);
  579. headerInfo.data = reshape(headerInfo.data, headerInfo.sizes(:)'); % data into expected form. Transpose required by reshape function bc size vectors need to be row vectors
  580. end
  581. else
  582. fclose(fidr);
  583. end
  584. end
  585. % -------------------------------------------------------------------%
  586. % ====================================================================
  587. % --- Auxiliary functions -------------------------------------------%
  588. % ====================================================================
  589. function [iGNr, dwiGradient] = extractGradient( st )
  590. % first get the gradient number
  591. iGNr = str2num( st(1:4) ); % FIX numbering limited to 4 digits (synchronize with writer module)
  592. % find where the assignment is
  593. assgnLoc = strfind( st, ':=' );
  594. if ( isempty(assgnLoc) )
  595. dwiGradient = [];
  596. return;
  597. else
  598. dwiGradient = sscanf( st(assgnLoc+2:end), '%f' );
  599. end
  600. end
  601. % Return part of string after :=
  602. function kvs = extractKeyValueString( st )
  603. assgnLoc = strfind( st, ':=' );
  604. if ( isempty(assgnLoc) )
  605. kvs = [];
  606. return;
  607. else
  608. kvs = st(assgnLoc(1)+2:end);
  609. end
  610. end
  611. % Turn space-separated list into cell array of strings.
  612. function sl = extractStringList( strList )
  613. sl = strsplit(strtrim(strList)); % old Matlab file exchange version had a strange bug with lists of length 2
  614. end
  615. % Store in an array the list of numbers separated by the tokens listed in
  616. % the withoutTokens cell array
  617. function iNrs = extractNumbersWithout( inputString, withoutTokens )
  618. auxStr = inputString;
  619. for iI=1:length( withoutTokens )
  620. auxStr = strrep( auxStr, withoutTokens{iI}, ' ' );
  621. end
  622. iNrs = sscanf( auxStr, '%f' );
  623. end
  624. % Return true if the string keyWord is the beginning of the content string
  625. % cs (ignoring case)
  626. function fk = foundKeyword( keyWord, cs )
  627. lenKeyword = length( keyWord );
  628. fk = (lenKeyword <= length(cs)) && strcmpi( cs(1:lenKeyword), keyWord);
  629. end
  630. % Return cell array of strings with space units in the form {mm, km, m}
  631. % from input of the form "mm " "mm" " m" where undesired blank spaces may
  632. % have crept in. The double quotes are removed in the process but will be
  633. % added by the nhdr/nrrd writer function.
  634. function su_ca = extract_spaceunits_list( fieldValue )
  635. fv_trimmed = strtrim( fieldValue );
  636. su_ca = strsplit(fv_trimmed, '"'); % units are delimited by double quotes
  637. su_ca = su_ca(~ ( strcmp(su_ca, '') | strcmp(su_ca, ' ') ) ); % remove empty or blank space strings
  638. for i = 1:length(su_ca)
  639. su_ca{i} = strtrim( su_ca{i} );
  640. end
  641. end
  642. % Extract data file list into a cell array from the datafile or data file
  643. % field in a NHDR file, stripped off its leading and trailing spaces;
  644. % LIST_mode is set to one if a list of files closes the header;
  645. % subdim is empty by default and may be set through either of these field
  646. % specifications:
  647. % data file: <format> <min> <max> <step> [<subdim>]
  648. % data file: LIST [<subdim>]
  649. function [filelist, LIST_mode, subdim] = extract_datafiles( field_string)
  650. field_string = strtrim(field_string);
  651. filelist = {};
  652. LIST_mode = 0;
  653. subdim = [];
  654. if length(field_string)>= 4 && strcmpi(field_string(1:4), 'LIST')
  655. % LIST mode
  656. subdim = sscanf(field_string(5:end), '%d'); % assert 1<=dimensions
  657. LIST_mode = 1;
  658. return;
  659. else
  660. % single detached data file or multiple files written in concise form, non LIST mode
  661. str_lst = strsplit(field_string); % without delimiters specified, splits at any sequence in the set {' ','\f','\n','\r','\t','\v'}
  662. if length(str_lst) == 1
  663. % Single detached data file:
  664. filelist{1} = str_lst{1};
  665. % subdim does not make sense here since all of the data is
  666. % contained in the deatached data file
  667. elseif any(length(str_lst)==[0, 2, 3]) || length(str_lst)>5
  668. % Invalid cases (2, 3 or striclty more than 5 entries)
  669. error(['error in ''data list'' (or ''data list'') field, in non' ...
  670. ' ''LIST'' mode:\nexpected ''<filename>'' or ''<format> ' ...
  671. '<min> <max> <step> [<subdim>]'' but found instead ''%s'' ' ...
  672. '(%d elements instead of 1, 4 or 5).'],...
  673. sprintf('%s ',str_lst{:}), length(str_lst));
  674. else
  675. % Multiple detached data files with filenames including integral
  676. % values generated by min:step:max
  677. str_format = str_lst{1};
  678. id_min = sscanf(str_lst{2}, '%d');
  679. id_max = sscanf(str_lst{3}, '%d');
  680. step = sscanf(str_lst{4}, '%d');
  681. % check format and indexing values
  682. assert(step~=0,...
  683. sprintf(['detached data files with names specified by ' ...
  684. 'sprintf()-like format: step should be strictly positive' ...
  685. ' or negative, not zero.']));
  686. if step < 0
  687. assert(id_min >= id_max, ...
  688. sprintf(['detached data files with names specified by ' ...
  689. 'sprintf()-like format: when step is <0, min ' ...
  690. 'should be larger than or equal to max, here we ' ...
  691. 'found min=%d < max=%d.'],id_min, id_max));
  692. else
  693. assert(id_min <= id_max,...
  694. sprintf(['detached data files with names specified by ' ...
  695. 'sprintf()-like format: : when step is >0, min ' ...
  696. 'should be smaller than or equal to max, here we ' ...
  697. 'found min=%d > max=%d.'],id_min, id_max));
  698. end
  699. % populate data file list
  700. fileIDs = id_min:step:id_max;
  701. filelist = cell(length(fileIDs), 1);
  702. for i = 1:length(fileIDs) % does not necessarily incude <max>, it cannot be larger than <max>
  703. expanded_fname = sprintf(str_format, fileIDs(i));
  704. filelist{i} = expanded_fname;
  705. end
  706. if length(str_lst) == 5
  707. subdim = sscanf(str_lst{5}, '%d');
  708. end
  709. end
  710. end
  711. end
  712. % Read data in a column vector from open file with pointer fidIn at the
  713. % beginning of the data to read. The other arguments specify the number of
  714. % elements to read (determined in main body of reader function above), data
  715. % file encoding (specified in NHDR/NRRD header) and the Matlab type the
  716. % data should be converted to (determined in main body of reader function
  717. % above).
  718. function data = readData(fidIn, Nelems, meta_encoding, matlabdatatype)
  719. switch (meta_encoding)
  720. case {'raw'}
  721. % This implicitely assumes that byteskip was set to -1 in raw
  722. % encoding
  723. data_bytes = (Nelems)*sizeOf(matlabdatatype); % bytes of useful info to return in data array
  724. fseek(fidIn,0,'eof'); % set position pointer to end of file
  725. tot_bytes_file = ftell(fidIn); % current location of the position pointer (I believe as a byte offset from beginning of file)
  726. fseek(fidIn,tot_bytes_file-data_bytes,'bof'); % make sure position pointer is right before useful data starts
  727. % Just read binary file (original version of the code)
  728. data = fread(fidIn, inf, [matlabdatatype '=>' matlabdatatype]);
  729. case {'gzip', 'gz'}
  730. tmp = fread(fidIn, Inf, 'uint8=>uint8'); % byte per byte
  731. tmpBase = tempname(pwd);
  732. tmpFile = [tmpBase '.gz'];
  733. fidTmp = fopen(tmpFile, 'wb'); % this creates tmpFile
  734. assert(fidTmp > 3, 'Could not open temporary file for GZIP decompression')
  735. try
  736. fwrite(fidTmp, tmp, 'uint8');
  737. catch me
  738. fclose(fidTmp);
  739. delete(tmpFile);
  740. rethrow(me);
  741. end
  742. fclose(fidTmp);
  743. try
  744. gunzip(tmpFile); % this creates tmpBase (tmpFile without the .gz)
  745. catch me
  746. delete(tmpFile);
  747. rethrow(me);
  748. end
  749. delete(tmpFile);
  750. fidTmp = fopen(tmpBase, 'rb');
  751. % cleaner = onCleanup(@() fclose(fidTmp));
  752. try
  753. data = readData(fidTmp, Nelems, 'raw', matlabdatatype); % recursive call
  754. catch me
  755. fclose(fidTmp);
  756. delete(tmpBase);
  757. rethrow(me);
  758. end
  759. fclose(fidTmp);
  760. delete(tmpBase);
  761. case {'txt', 'text', 'ascii'}
  762. data = fscanf(fidIn, '%f');
  763. data = cast(data, matlabdatatype);
  764. otherwise
  765. assert(false, 'Unsupported encoding')
  766. end
  767. % Check number of read elements (sometimes there is an offest of about 1)
  768. assert(Nelems == numel(data),...
  769. sprintf(['Error reading binary content of %s: detected %d elements' ...
  770. ' instead of %d announced in header.'],...
  771. fopen(fidIn), numel(data), Nelems));
  772. end
  773. % Switch byte ordering according to endianness specified in nhdr/nrrd file.
  774. % The metadata should just contain a field 'endian' set to 'little' or
  775. % 'big'.
  776. function data = adjustEndian(data, meta_endian)
  777. [~,~,endian] = computer();
  778. needToSwap = (isequal(endian, 'B') && isequal(lower(meta_endian), 'little')) || ...
  779. (isequal(endian, 'L') && isequal(lower(meta_endian), 'big'));
  780. if (needToSwap)
  781. data = swapbytes(data);
  782. end
  783. end
  784. % Size of Matlab's numeric classes in bytes
  785. % See: https://stackoverflow.com/questions/16104423/size-of-a-numeric-class
  786. function numBytes = sizeOf(dataClass)
  787. % create temporary variable of data type specified
  788. eval(['var = ' dataClass '(0);']);
  789. % Use the functional form of whos, and get the number of bytes
  790. W = whos('var');
  791. numBytes = W.bytes;
  792. end