-
Notifications
You must be signed in to change notification settings - Fork 17
/
dicm2nii_untouched.m
2472 lines (2291 loc) · 101 KB
/
dicm2nii_untouched.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
function varargout = dicm2nii(src, dataFolder, varargin)
% Convert dicom and more into nii or img/hdr files.
%
% DICM2NII(dcmSource, niiFolder, outFormat, MoCoOption)
%
% The input arguments are all optional:
% 1. source file or folder. It can be a zip or tgz file, a folder containing
% dicom files, or other convertible files. It can also contain wildcards
% like 'run1_*' for all files start with 'run1_'.
% 2. folder to save result files.
% 3. output file format:
% 0 or 'nii' for single nii uncompressed.
% 1 or 'nii.gz' for single nii compressed (default).
% 2 or 'hdr' for hdr/img pair uncompressed.
% 3 or 'hdr.gz' for hdr/img pair compressed.
% 4 or '3D.nii' for 3D nii uncompressed (SPM12).
% 5 or '3D.nii.gz' for 3D nii compressed.
% 6 or '3D.hdr' for 3D hdr/img pair uncompressed (SPM8).
% 7 or '3D.hdr.gz' for 3D hdr/img pair compressed.
% 4. MoCo series options:
% 0 create files for both original and MoCo series.
% 1 ignore MoCo series if both present (default).
% 2 ignore original series if both present.
% Note that if only one of the two series is present, it will be converted
% always. In the future, this option may be removed, and all files will be
% converted.
%
% The optional output is converted PatientName(s).
%
% Typical examples:
% dicm2nii; % bring up user interface if there is no input argument
% dicm2nii('D:/myProj/zip/subj1.zip', 'D:/myProj/subj1/data'); % zip file
% dicm2nii('D:/myProj/subj1/dicom/', 'D:/myProj/subj1/data'); % folder
%
% Less useful examples:
% dicm2nii('D:/myProj/dicom/', 'D:/myProj/subj2/data', 'nii'); % no gz compress
% dicm2nii('D:/myProj/dicom/run2*', 'D:/myProj/subj/data'); % convert run2 only
% dicm2nii('D:/dicom/', 'D:/data', '3D.nii'); % SPM style files
%
% If there is no input, or any of the first two input is empty, the graphic user
% interface will appear.
%
% If the first input is a zip/tgz file, such as those downloaded from dicom
% server, DICM2NII will extract files into a temp folder, create NIfTI files
% into the data folder, and then delete the temp folder. For this reason, it is
% better to keep the compressed file as backup.
%
% If a folder is the data source, DICM2NII will convert all files in the folder
% and its subfolders (there is no need to sort files for different series).
%
% Please note that, if a file in the middle of a series is missing, the series
% will normally be skipped without converting, and a warning message in red text
% will be shown in Command Window. The message will also be saved into a text
% file under the data folder.
%
% A Matlab data file, dcmHeaders.mat, is always saved into the data folder. This
% file contains dicom header from the first file for created series and some
% information from last file in field LastFile. Some extra information may also
% be saved into this file. For MoCo series, motion parameters (RBMoCoTrans and
% RBMoCoRot) are also saved.
%
% Slice timing information, if available, is stored in nii header, such as
% slice_code and slice_duration. But the simple way may be to use the field
% SliceTiming in dcmHeaders.mat. That timing is actually those numbers for FSL
% when using custom slice timing. This is the universal method to specify any
% kind of slice order, and for now, is the only way which works for multiband.
% Slice order is one of the most confusing parameters, and it is recommended to
% use this method to avoid mistake. Following shows how to convert this timing
% into slice timing in ms and slice order for SPM:
%
% load('dcmHeaders.mat'); % or drag and drop the MAT file into Matlab
% s = h.myFuncSeries; % field name is the same as nii file name
% spm_ms = (0.5 - s.SliceTiming) * s.RepetitionTime;
% [~, spm_order] = sort(-s.SliceTiming);
%
% Some information, such as TE, phase encoding direction and effective dwell
% time are stored in descrip of nii header. These are useful for fieldmap B0
% unwarp correction. Acquisition start time and date are also stored, and this
% may be useful if one wants to align the functional data to some physiological
% recording, like pulse, respiration or ECG.
%
% If there is DTI series, bval and bvec files will be generated for FSL etc.
% bval and bvec are also saved into the dcmHeaders.mat file.
%
% Starting from 20150514, the converter stores some useful information in NIfTI
% text extension (ecode=6). nii_tool can decode these information easily:
% ext = nii_tool('ext', 'myNiftiFile.nii'); % read NIfTI extension
% ext.edata_decoded contains all above mentioned information, and more. The
% inlcuded nii_viewer can show the extension by Window->Show NIfTI ext.
%
% Starting from 20151120, the converter can optionally save a .json file for
% each converted NIfTI. This can be turned on by running following line from
% Command Window for a new session:
% setpref('dicm2nii_gui_para', 'save_json', true);
% It will stay on (saving json files) until a new session with
% setpref('dicm2nii_gui_para', 'save_json', false);
% For more information about the purpose of json file, check
% http://bids.neuroimaging.io/
%
% Please note that some information, such as the slice order information, phase
% encoding direction and DTI bvec are in image reference, rather than NIfTI
% coordinate system. This is because most analysis packages require information
% in image space. For this reason, in case the image in a NIfTI file is flipped
% or re-oriented, these information may not be correct anymore.
%
% The output file names adopt SeriesDescription or ProtocolName of each series
% used on scanner console. If both original and MoCo series are converted,
% '_MoCo' will be appended for MoCo series. For phase image, such as those from
% field map, '_phase' will be appended to the name. If multiple subjects data
% are mixed (highly discouraged), subject name will be in file name. In case of
% name conflict, SeriesNumber, such as '_s005', will be appended to make file
% names unique. It is suggested to use short and descriptive SeriesDescription
% on the scanner console.
%
% For SPM 3D files, the file names will have volume index in format of '_00001'
% appended to above name.
%
% Please report any bug to xiangrui.li@gmail.com or at
% http://www.mathworks.com/matlabcentral/fileexchange/42997
%
% To cite the work and for more detail about the conversion, check the paper at
% http://authors.elsevier.com/a/1SgEObXTOhrOf
% Thanks to:
% Jimmy Shen's Tools for NIfTI and ANALYZE image,
% Chris Rorden's dcm2nii pascal source code,
% Przemyslaw Baranski for direction cosine matrix to quaternions.
% History (yymmdd):
% 130512 Publish to CCBBI users (Xiangrui Li).
% 130513 Convert img from uint16 to int16 if range allows;
% Support output file format of img/hdr/mat.
% 130515 Change creation order to acquisition order (more natural).
% If MoCo series is included, append _MoCo in file names.
% 130516 Use SpacingBetweenSlices, if exists, for SliceThickness.
% 130518 Use NumberOfImagesInMosaic in CSA header (work for some old data).
% 130604 Add scl_inter/scl_slope and special naming for fieldmap.
% 130614 Work out the way to get EffectiveEchoSpacing for B0 unwarp.
% 130616 Add needed dicom field check, so it won't err later.
% 130618 Reorient if non-mosaic or slice_dim is still 3 and no slice flip.
% 130619 Simplify DERIVED series detection. No '_mag' in fieldmap name.
% 130629 Improve the method to get phase direction;
% Permute img dim1&2 (no -90 rotation) & simplify xform accordingly.
% 130711 Make MoCoOption smarter: create nii if only 1 of 2 series exists.
% 130712 Remove 5th input (allHeader). Save memory by using partial header.
% 130712 Bug fix: dim_info with reorient. No problem since no EPI reorient.
% 130715 Use 2 slices for xform. No slice flip needed except revNum mosaic.
% 130716 Take care of lower/upper cases for output file names;
% Apply scl_slope and inter to img if range allows and no rounding;
% Save motion parameters, if any, into dcmHeader.mat.
% 130722 Ugly fix for isMos, so it works for '2004A 4VA25A' phase data;
% Store dTE instead of TE if two TE are used, such as fieldmap.
% 130724 Add two more ways for dwell time, useful for '2004A 4VA25A' dicom.
% 130801 Can't use DERIVED since MoCoSeries may be labeled as DERIVED.
% 130807 Check PixelSpacing consistency for a series;
% Prepare to publish to Matlab Central.
% 130809 Add 5th input for subjName, so one can choose a subject.
% 130813 Store ImageComments, if exists and is meaningful, into aux_file.
% 130818 Expand source to dicom file(s) and wildcards like run1*.dcm.
% Update fields in dcmHeader.mat, rather than overwriting the file.
% Include save_nii etc in the code for easy distribution.
% 130821 Bug fix for cellstr input as dicom source.
% Change file name from dcm2nii.m to reduce confusion from MRICron.
% GUI implemented into the single file.
% 130823 Remove dependency on Image Processing Toolbox.
% 130826 Bug fix for '*' src input. Minor improvement for dicm_hdr.
% 130827 Try and suggest to use pigz for compression (thanks Chris R.).
% 130905 Avoid the missing-field error for DTI data with 2 excitations.
% Protect GUI from command line plotting.
% 130912 Use lDelayInTR for slice_dur, possibly useful for old data.
% 130916 Store B_matrix for DTI image, if exists.
% 130919 Make the code work for GE and Philips dicom at Chris R website.
% 130922 Remove dependence on normc from nnet toolbox (thank Zhiwei);
% Prove no slice order info in Philips, at least for Intera 10.4.1.
% 130923 Make the code work for Philips PAR/REC pair files.
% 130926 Take care of non-mosaic DTI for Siemens (img/bval/bvec);
% 130930 Use verify_slice_dir subfun to get slice_dir even for a single file.
% 131001 dicm_hdr can deal with VR of SQ. This slows down it a little.
% 131002 Avoid fullfile for cellstr input (not supported in old ver matlab).
% 131006 Tweak dicm_hdr for multiframe dicom (some bug fixes);
% First working version for multiframe (tested with Philips dicom).
% 131009 Put dicm_hdr, dicm_img, dicm_dict outside this file;
% dicm_hdr can read implicit VR, and is faster with single fread;
% Fix problem in gzipOS when folder name contains space.
% 131020 Make TR & ProtocolName non-mandatory; Set cal_min & cal_max.
% 131021 Check SamplesPerPixel, skip run if it is 1+.
% 131021 Implement conversion for AFNI HEAD/BRIK.
% 131024 Bug fix for dealing with current folder as src folder.
% 131029 Bug fix: Siemens, 2D, non-mosaic, rev-num slices were flipped.
% 131105 DTI parameters: field names more consistent; read DTI flds in
% save_dti_para for GE/Philips (make others faster); convert Philips
% bvec from deg into vector (need to be verified).
% 131114 Treak for multiframe dicm_hdr: MUCH faster by using only 1,2,n frames;
% Bug fix for Philips multiframe DTI parameters;
% Split multiframe Philips B0 map into mag and phase nii.
% 131117 Make the order of phase/mag image in Philips B0 map irrelevant.
% 131219 Write warning message to a file in data folder (Gui's suggestion).
% 140120 Bug fix in save_dti_para due to missing Manufacturer (Thank Paul).
% 140121 Allow missing instance at beginning of a series.
% 140123 save_nii: bug fix for gzip.m detection, take care of ~ as home dir.
% 140206 bug fix: MoCo detetion bug introduced by removing empty cell earlier.
% 140223 add missing-file check for Philips data by slice locations.
% 140312 use slice timing to set slice_code for both GE and Siemens.
% Interleaved order was wrong for GE data with even number of slices.
% 140317 Use MosaicRefAcqTimes from last vol for multiband (thank Chris).
% Don't re-orient fieldmap, so make FSL happy in case of non_axial.
% Ugly fix for wrong dicom item VR 'OB': Avoid using main header
% in csa_header(), convert DTI parameters to correct type. There may
% be other wrong parameters we don't realize.
% 140319 Store SliceTiming field in dcmHeaders.mat for FSL custom slice timing.
% Re-orient even if flipping slices for 2D MRAcquisitionType.
% 140324 Not set cal_min, cal_max anymore.
% 140327 Return unconverted subject names in 2nd output.
% 140401 Always flip image so phase dir is correct.
% 140409 Store nii extension (not enabled due to nifti ext issue).
% 140501 Fix for GE: use LocationsInAcquisition to replace ImagesInAcquisition;
% isDTI=DiffusionDirection>0; Gradient already in image reference.
% 140505 Always re-orient DTI. bvec fix for GE DTI (thx Chris).
% 140506 Remove last DTI vol if it is computed ADC (as dcm2niix);
% Use SeriesDescription to replace ProtocolName for file name;
% Improved dim_info and phase direction.
% 140512 Decode GE ProtocolDataBlock for phase direction;
% strtrim SeriesDescription for nii file name.
% 140513 change stored phase direction to image space for FSL unwarp;
% Simplify code for dim_info.
% 140516 Switch back to ProtocolName for SIEMENS to take care of MOCO series;
% Detect Philips Dim3IsVolume (for multi files) during dicom check;
% Work for GE interleaved slices even if InstanceNumber is in time order;
% Do ImagePositionPatient check for all vendors;
% Simplify code for save_dti_para.
% 140517 Store img with first dim flipped, to take care of DTI bvec problems.
% 140522 Use SliceNormalVector for mosaic slice_dir, so no worry for revNumb;
% Bug fix for interleaved descending slice_code.
% 140525 xform sliceCenter to SliceLocation in verify_slice_dir.
% 140526 Take care of non-unique ixyz.
% 140608 Bug fix for GE interleaved slices;
% Take care all ixyz, put verify_slice_dir into xform_mat.
% 140610 Compute readout time for DTI, rather than dwell time.
% 140621 Support tgz file as data source.
% 140716 Bug fix due to empty src for GUI subject option.
% 140808 Simplify mosaic detection, and remove isMosaic.
% 140816 Simplify DTI detection.
% 140911 Minor fix for Siemens ProtocolName for error message.
% 141016 Remember GUI settings from last conversion;
% Make multi-subject error message friendly.
% 141021 Show percent progress for validating dicom files.
% 141023 Get LocationsInAcquisition for GE multiframe dicom.
% 141024 Use unique ImagePositionPatient to determine LocationsInAcquisition.
% 141028 Use matlabpool if available and worthy.
% 141125 Store NumberOfTemporalPositions in dicom header.
% 141128 Minor tweaks for Octave 3.8.1 command line (GUI not working).
% 141216 Use ImagePositionPatient to derive SliceThickness if possible.
% 141217 Override LocationsInAcquisition with computed nSL (thx Luigi);
% Check RescaleIntercept and RescaleSlope consistency.
% 141218 Allow 1e-4 diff for ImagePositionPatient of same slice location.
% 141223 multiFrameFields: return earlier if only single frame (thx Sander);
% No re-orient for single slice (otherwise problem for mricron to read).
% 141224 mos2vol: use nSL loop (faster unless many slices).
% 141229 Save nii ext (ecode=40) if FSL is detected & it is not 5.0.5/5.0.6.
% 141230 nojvm: no matlabpool; no dicm_hdr progress due to '\b' issue for WIN.
% 150109 dicm_img(s, 0) to follow the update for dicm_img.
% 150112 Use nii_tool.m, remove make_nii, save_nii etc from this file.
% 150115 Allow SamplesPerPixel>1, but likely not very useful.
% 150117 Store seq name in intent_name.
% 150119 Add phase img detection for Philips (still need it for GE).
% 150120 No file skip by EchoTime: keep all data by using EchoNumber.
% 150209 Add more output format for SPM style: 3D output;
% GUI includes SPM 3D, separates GZ option.
% 150211 No missing file check for all vendors, relying on ImagePosition check;
% csa_header() relies on dicm_hdr decoding (avoid error on old data);
% Deal with dim3-RGB and dim4-frames due to dicm_img.m update.
% 150222 Remove useless, mis-used TriggerTime for partial hdr; also B_matrix.
% 150302 No hardcoded sign change for DTI bvec, except for GE;
% set_nii_header: do flip only once after permute;
% 150303 Bug fix for phPos: result was right by lucky mistake;
% Progress shows nii dim, more informative than number of files.
% 150305 Replace null with cross: null gives inconsistent signs;
% Use SPM method for xform: account for shear; no qform setting if shear.
% 150306 GE: fully sort slices by loc to ease bvec sign (test data needed);
% bvec sign simplified by above sort & corrected R for Philips/Siemens.
% 150309 GUI: added the little popup for 'about/license'.
% 150323 Siemens non-mosaic: RefAcqTimes from ucMode, AcquisitionTime(disabled).
% 150324 mandatory flds reduced to 5; get info by asc_header if possible;
% 150325 Use SeriesInstanceUID to take care of multiple Study and PatientName;
% Remove 5th input (subj); GUI updated; subjName in file name if needed;
% Deal with MoCo series by output file names;
% Convert GLM and DTI junk too; no Manufacturer check in advance.
% 150405 Implement BrainVoyager dmr/fmr/vmr conversion; GUI updated accordingly.
% 150413 InstanceNumber is not mandatory (now total 4);
% Check missing files for non-DTI mosaic by InstanceNumber.
% 150418 phaseDirection: bug fix for Philips, simplify for others.
% 150420 store raw timing in RefAcqTimes, avoid confusion with SliceTiming.
% 150423 fix matlabpool for later matlab versions; no auto-close anymore;
% GUI figure handle can't be uint32 for matlab 2015;
% Turn off saveExt40: FSL 5.0.8 may read vox_offset as 352.
% 150430 xform_mat: GE, no LastScanLoc needed since sorted by ImagePosition.
% 150508 csa2pos: bug fix for revNum, iSL==1; treat dInPlaneRot specially.
% 150514 set_nii_ext: start to store txt edata (ecode=6).
% Avoid dict change in dicm_hdr due to vendor change (GE/Philips faster);
% 150517 Octave compatibility fix in multiple files.
% 150526 multiFrameFields: LocationsInAcquisition by ImagePosition if needed.
% 150531 Check slice loc for all volumes to catch missing files (thx CarloR).
% 150604 phaseDirection: typo fix for Philips 'RLAPFH'; Show converter version.
% 150606 csa_header read both CSA image/series header.
% 150609 No t_unit and SliceTiming for DTI.
% 150613 mb_slicetiming: try to fix SOME broken multiband slice timing.
% 150620 use 'bval' for nii.ext and dcmHeaders.mat, so keep original B_value.
% 150910 bug fix for scl_slope/inter: missing double for max/min(nii.img(:)).
% 150924 PAR: fix weird SliceNumber; fix mean-ADC removal if not last vol.
% 150925 Bug fix for nSL=1 (vol-dim was at slice-dim);
% 150926 multiFrameFields: add SliceNumber & simplify code;
% save_dti_para: tidy format; try to avoid genvarname.
% 150927 Repalce misused length with numel in all files.
% 150928 checkImagePostion: skip most irregular spacing.
% 150929 Take care of SL order for regular dicom: GE no longer special case.
% 150930 Remove slice_dir guess; Use NiftiName for error info.
% 151115 GUI: remove srcType; Implement drag&drop for src and dst.
% 151117 save_json proposed by ChrisG; won't flush nii_viewer para.
% 151212 Bug fix for missing pref_file.
% 151217 gui callback uses subfunc directly, also include fh as argument.
% 151221 dim_info stores phaseDir at highest 2 bits (1 pos, 2 neg, 0 unknown).
% 160110 Implement "Check update" based on findjobj; Preference method updated.
% 160112 SeriesInstanceUID & SeriesNumber only need one (thx DavidR).
% 160115 checkUpdate: fix problem to download & unzip to pwd.
% 160127 dicm_hdr & dicm_img: support big endian dicom.
% 160229 flip now makes det<0, instead negative 1st axis (cor slice affected).
% 160304 undo some changes on 140808 so it works for syngo 2004 phase masaic.
% 160309 nMosaic(): use CSAheader to detect above unlabeled mosaic.
% 160324 nMosaic(): unsecure fix for dicom without CSA header.
% End of history. Don't edit this line!
% TODO: need testing files to figure out following parameters:
% flag for MOCO series for GE/Philips
% GE non-axial slice bvec sign
% Phase image flag for GE
if nargout, varargout{1} = ''; end
if nargin==3 && ischar(varargin{1}) && strcmp(varargin{1}, 'func_handle')
if strcmp(dataFolder, 'all') % for command line test
fcns = localfunctions; % only for Matlab since 2013b
for i = 1:numel(fcns)
nam = func2str(fcns{i});
assignin('base', nam, eval(['@' nam]));
end
else
varargout{1} = eval(['@' dataFolder]);
end
return;
end
%% Deal with output format first, and error out if invalid
if nargin<3 || isempty(varargin{1}), fmt = 1; % default .nii.gz
else fmt = varargin{1};
end
if (isnumeric(fmt) && any(fmt==[0 1 4 5])) || ...
(ischar(fmt) && ~isempty(regexpi(fmt, 'nii')))
ext = '.nii';
elseif (isnumeric(fmt) && any(fmt==[2 3 6 7])) || (ischar(fmt) && ...
(~isempty(regexpi(fmt, 'hdr')) || ~isempty(regexpi(fmt, 'img'))))
ext = '.img';
else
error(' Invalid output file format (the 3rd input).');
end
if (isnumeric(fmt) && mod(fmt,2)) || (ischar(fmt) && ~isempty(regexpi(fmt, '.gz')))
ext = [ext '.gz']; % gzip file
end
rst3D = (isnumeric(fmt) && fmt>3) || (ischar(fmt) && ~isempty(regexpi(fmt, '3D')));
%% Deal with MoCo option
if nargin<4 || isempty(varargin{2})
MoCo = 1; % by default, use original series if both present
else
MoCo = varargin{2};
if ~any(MoCo==0:2)
error(' Invalid MoCoOption. The 4th input must be 0, 1 or 2.');
end
end
%% Deal with data source
if nargin<1 || isempty(src) || (nargin<2 || isempty(dataFolder))
create_gui; % show GUI if input is not enough
return;
end
tic;
unzip_cmd = '';
if isnumeric(src)
error('Invalid dicom source.');
elseif iscellstr(src) % multiple files
dcmFolder = folderFromFile(src{1});
n = numel(src);
fnames = src;
for i = 1:n
foo = dir(src{i});
if isempty(foo), error('%s does not exist.', src{i}); end
fnames{i} = fullfile(dcmFolder, foo.name);
end
elseif ~exist(src, 'file') % like input: run1*.dcm
fnames = dir(src);
if isempty(fnames), error('%s does not exist.', src); end
fnames([fnames.isdir]) = [];
dcmFolder = folderFromFile(src);
fnames = strcat(dcmFolder, filesep, {fnames.name});
elseif isdir(src) % folder
dcmFolder = src;
elseif ischar(src) % 1 dicom or zip/tgz file
dcmFolder = folderFromFile(src);
unzip_cmd = compress_func(src);
if isempty(unzip_cmd)
fnames = dir(src);
fnames = strcat(dcmFolder, filesep, {fnames.name});
end
else
error('Unknown dicom source.');
end
dcmFolder = fullfile(getfield(what(dcmFolder), 'path'));
%% Deal with dataFolder
if ~isdir(dataFolder), mkdir(dataFolder); end
dataFolder = fullfile([getfield(what(dataFolder), 'path') filesep]);
converter = ['dicm2nii.m 20' reviseDate];
if errorLog('', dataFolder) % let it remember dataFolder for later call
more off;
disp(['Xiangrui Li''s ' converter ' (feedback to xiangrui.li@gmail.com)']);
end
%% Unzip if compressed file is the source
if ~isempty(unzip_cmd)
[~, fname, ext1] = fileparts(src);
dcmFolder = sprintf('%stmpDcm%s/', dataFolder, fname);
if ~isdir(dcmFolder), mkdir(dcmFolder); end
disp(['Extracting files from ' fname ext1 ' ...']);
if strcmp(unzip_cmd, 'unzip')
cmd = sprintf('unzip -qq -o %s -d %s', src, dcmFolder);
err = system(cmd); % first try system unzip
if err, unzip(src, dcmFolder); end % Matlab's unzip is too slow
elseif strcmp(unzip_cmd, 'untar')
if isempty(which('untar')), error('No untar found in matlab path.'); end
untar(src, dcmFolder);
end
drawnow;
end
%% Get all file names including those in subfolders, if not specified
if ~exist('fnames', 'var')
dirs = genpath(dcmFolder);
dirs = textscan(dirs, '%s', 'Delimiter', pathsep);
dirs = dirs{1}; % cell str
fnames = {};
for i = 1:numel(dirs)
curFolder = [dirs{i} filesep];
foo = dir(curFolder); % all files and folders
foo([foo.isdir]) = []; % remove folders
foo = strcat(curFolder, {foo.name});
fnames = [fnames foo]; %#ok<*AGROW>
end
end
nFile = numel(fnames);
if nFile<1, error(' No files found in the data source.'); end
%% Check each file, store partial header in cell array hh
% first 3 fields are must, 4 or 5 must have one
flds = {'Columns' 'Rows' 'BitsAllocated' 'SeriesInstanceUID' 'SeriesNumber' ...
'ImageOrientationPatient' 'ImagePositionPatient' 'PixelSpacing' ...
'PixelRepresentation' 'BitsStored' 'HighBit' 'SamplesPerPixel' ...
'PlanarConfiguration' 'EchoNumber' 'RescaleIntercept' 'RescaleSlope' ...
'InstanceNumber' 'NumberOfFrames' 'B_value' 'DiffusionGradientDirection' ...
'RTIA_timer' 'RBMoCoTrans' 'RBMoCoRot' ...
'SliceThickness' 'SpacingBetweenSlices'};
dict = dicm_dict('SIEMENS', flds); % dicm_hdr will update vendor if needed
% read header for all files, use parpool if available and worthy
fprintf('Validating %g files ...\n', nFile);
hh = cell(1, nFile); errStr = cell(1, nFile);
doPar = useParTool(nFile>1000); % use it if already open or nFile>1000
for k = 1:nFile
[hh{k}, errStr{k}, dict] = dicm_hdr(fnames{k}, dict);
if doPar && ~isempty(hh{k}) % parfor wont allow updating dict
parfor i = k+1:nFile
[hh{i}, errStr{i}] = dicm_hdr(fnames{i}, dict);
end
break;
end
end
%% sort headers into cell h by SeriesInstanceUID, EchoNumber and InstanceNumber
h = {}; % in case of no dicom files at all
errInfo = '';
seriesUIDs = {};
for k = 1:nFile
s = hh{k};
if isempty(s) || any(~isfield(s, flds(1:3))) || ~any(isfield(s, flds(4:5)))
if ~isempty(errStr{k}) % && isempty(strfind(errInfo, errStr{k}))
errInfo = sprintf('%s\n%s\n', errInfo, errStr{k});
end
continue; % skip the file
end
if ~isfield(s, 'SeriesInstanceUID')
s.SeriesInstanceUID = num2str(s.SeriesNumber); % make up UID
end
m = find(strcmp(s.SeriesInstanceUID, seriesUIDs));
if isempty(m)
m = numel(seriesUIDs)+1;
seriesUIDs{m} = s.SeriesInstanceUID;
end
% EchoNumber is needed for Siemens fieldmap mag series
i = tryGetField(s, 'EchoNumber', 1); if i<1, i = 1; end
j = tryGetField(s, 'InstanceNumber');
if isempty(j) || j<1
try j = numel(h{m}{i}) + 1;
catch, j = 1;
end
end
h{m}{i}{j} = s; % store partial header
end
clear hh errStr;
%% Check headers: remove file-missing and dim-inconsistent series
nRun = numel(h);
if nRun<1
errorLog(sprintf('No valid files found:\n%s.', errInfo));
return;
end
keep = true(1, nRun); % true for useful runs
subjs = cell(1, nRun); vendor = cell(1, nRun);
sNs = ones(1, nRun); studyIDs = cell(1, nRun);
fldsCk = {'ImageOrientationPatient' 'NumberOfFrames' 'Columns' 'Rows' ...
'PixelSpacing' 'RescaleIntercept' 'RescaleSlope' 'SamplesPerPixel' ...
'SpacingBetweenSlices' 'SliceThickness'}; % last for thickness
for i = 1:nRun
h{i} = [h{i}{:}]; % concatenate different EchoNumber
ind = cellfun(@isempty, h{i});
h{i}(ind) = []; % remove all empty cell for all vendors
s = h{i}{1};
if ~isfield(s, 'LastFile') % avoid re-read for PAR/HEAD/BV file
s = dicm_hdr(s.Filename); % full header for 1st file
end
if ~isfield(s, 'Manufacturer'), s.Manufacturer = 'Unknown'; end
subjs{i} = PatientName(s);
vendor{i} = s.Manufacturer;
sNs(i) = tryGetField(s, 'SeriesNumber', 1);
studyIDs{i} = tryGetField(s, 'StudyID', '1');
series = sprintf('Subject %s, %s (Series %g)', subjs{i}, ProtocolName(s), sNs(i));
s = multiFrameFields(s); % no-op if non multi-frame
if isempty(s), keep(i) = 0; continue; end % invalid multiframe series
s.isDTI = isDTI(s);
h{i}{1} = s; % update record in case of full hdr or multiframe
if tryGetField(s, 'NumberOfFrames', 1) > 1, continue; end
% check consistency in 'fldsCk'
nFile = numel(h{i});
nFlds = numel(fldsCk);
if isfield(s, 'SpacingBetweenSlices'), nFlds = nFlds - 1; end % check 1 of 2
for k = 1:nFlds
val1 = tryGetField(s, fldsCk{k});
if isempty(val1), continue; end
for j = 2:nFile
% At least some GE ImageOrientationPatient can have diff of 1e-6
val2 = tryGetField(h{i}{j}, fldsCk{k});
if isempty(val2) || any(abs(val1 - val2) > 1e-4)
errorLog(['Inconsistent ''' fldsCk{k} ''' for ' series '. Series skipped.']);
keep(i) = 0;
break;
end
end
if ~keep(i), break; end % skip
end
nSL = nMosaic(s);
if nSL > 1
h{i}{1}.isMos = true;
h{i}{1}.LocationsInAcquisition = nSL;
if s.isDTI, continue; end % allow missing directions for DTI
a = zeros(1, nFile);
for j = 1:nFile, a(j) = tryGetField(h{i}{j}, 'InstanceNumber', 1); end
if any(diff(a) ~= 1)
errorLog(['Missing file(s) detected for ' series '. Series skipped.']);
keep(i) = 0;
end
continue; % no other check for mosaic
end
if ~keep(i) || ~isfield(s, 'ImagePositionPatient'), continue; end
ipp = zeros(nFile, 3);
for j = 1:nFile, ipp(j,:) = h{i}{j}.ImagePositionPatient; end
[err, nSL, sliceN, isTZ] = checkImagePostion(ipp);
if ~isempty(err)
errorLog([err ' for ' series '. Series skipped.']);
keep(i) = 0; continue; % skip
end
h{i}{1}.LocationsInAcquisition = uint16(nSL); % best way for nSL?
nVol = nFile / nSL;
if isTZ % Dim3IsVolume: Philips
ind = reshape(1:nFile, [nVol nSL])';
h{i} = h{i}(ind(:));
h{i}{1}.Dim3IsVolume = true; % not needed, info only
end
% re-order slices within vol. No SliceNumber since files are organized
if any(diff(sliceN, 2) > 0) % neither 1:nSL nor nSL:-1:1
if sliceN(end) == 1, sliceN = sliceN(nSL:-1:1); end % not important
inc = repmat((0:nVol-1)*nSL, nSL, 1);
ind = repmat(sliceN(:), nVol, 1) + inc(:);
h{i} = h{i}(ind); % sorted by slice locations
if sliceN(1)>1 % first file changed: update info for h{i}{1}
h{i}{1} = dicm_hdr(h{i}{1}.Filename); % read full hdr
s = h{i}{sliceN==1}; % original first file
fldsCp = {'AcquisitionDateTime' 'isDTI' 'Dim3IsVolume' ...
'LocationsInAcquisition'};
for j = 1:numel(fldsCp)
if isfield(s, fldsCp{j})
h{i}{1}.(fldsCp{j}) = s.(fldsCp{j});
end
end
if ~isfield(s, fldsCp{1}) % assumption: 1st instance is earliest
h{i}{1}.(fldsCp{1}) = [tryGetField(s, 'AcquisitionDate', '') ...
tryGetField(s, 'AcquisitionTime', '')];
end
end
end
end
h = h(keep); sNs = sNs(keep); studyIDs = studyIDs(keep);
subjs = subjs(keep); vendor = vendor(keep);
%% sort h by PatientName, then StudyID, then SeriesNumber
% Also get correct order for subjs/studyIDs/nStudy/sNs for nii file names
[subjs, ind] = sort(subjs);
subj = unique(subjs);
h = h(ind); sNs = sNs(ind); studyIDs = studyIDs(ind); % by subjs now
nStudy = ones(1, nRun); % one for each series
for i = 1:numel(subj)
iSub = find(strcmp(subj{i}, subjs));
study = studyIDs(iSub);
[study, iStudy] = sort(study); % by study for each subject
a = h(iSub); h(iSub) = a(iStudy);
a = sNs(iSub); sNs(iSub) = a(iStudy);
studyIDs(iSub) = study; % done for h/sNs/studyIDs by studyIDs for a subj
uID = unique(study);
nStudy(iSub) = numel(uID);
for k = 1:numel(uID) % now sort h/sNs by sNs for each studyID
iStudy = strcmp(uID{k}, study);
ind = iSub(iStudy);
[sNs(ind), iSN] = sort(sNs(ind));
a = h(ind); h(ind) = a(iSN);
end
end
%% Generate unique result file names
% Unique names are in format of SeriesDescription_s007. Special cases are:
% for phase image, such as field_map phase, append '_phase' to the name;
% for MoCo series, append '_MoCo' to the name if both series are created.
% for multiple subjs, it is SeriesDescription_subj_s007
% for multiple Study, it is SeriesDescription_subj_Study1_s007
nRun = numel(h); % update it, since we have removed some
if nRun<1
errorLog('No valid series found');
return;
end
rNames = cell(1, nRun);
isMoCo = false(1, nRun);
multiSubj = numel(subj)>1;
for i = 1:nRun
s = h{i}{1};
a = strtrim(ProtocolName(s));
if isType(s, '\P\') || strcmpi(tryGetField(s, 'ComplexImageComponent', ''), 'PHASE')
a = [a '_phase']; % phase image
end
isMoCo(i) = isType(s, '\MOCO\');
if MoCo==0 && isMoCo(i), a = [a '_MoCo']; end
if multiSubj, a = [a '_' subjs{i}]; end
if nStudy(i)>1, a = [a '_Study' studyIDs{i}]; end
if ~isstrprop(a(1), 'alpha'), a = ['x' a]; end % genvarname behavior
a(~isstrprop(a, 'alphanum')) = '_'; % make str valid for field name
while true % remove repeated underscore
ind = strfind(a, '__');
if isempty(ind), break; end
a(ind) = '';
end
sN = sNs(i);
if sN>100 && strncmp(s.Manufacturer, 'Philips', 7)
sN = tryGetField(s, 'AcquisitionNumber', floor(sN/100));
end
rNames{i} = sprintf('%s_s%03.0f', a, sN);
end
if any(cellfun(@numel, rNames)>namelengthmax)
rNames = genvarname(rNames); %#ok also guarantee unique names
end
% deal with MoCo series
if MoCo>0 && any(isMoCo)
keep = true(1, nRun);
for i = 2:nRun
if isMoCo(i) && sNs(i)==sNs(i-1)+1 && ...
strcmp(rNames{i}(1:end-5), rNames{i-1}(1:end-5))
if MoCo==1
keep(i) = 0; % skip MOCO
elseif MoCo==2
keep(i-1) = 0; % skip original
end
end
end
h = h(keep); rNames = rNames(keep);
vendor = vendor(keep); subj = unique(subjs(keep));
nRun = numel(h);
end
vendor = strtok(unique(vendor));
if nargout>0, varargout{1} = subj; end % return converted subject IDs
if nargout>1, varargout{2} = {}; end % will remove in the future
% After following sort, we need to compare only neighboring names. Remove
% _s007 if there is no conflict. Have to ignore letter case for Windows & MAC
fnames = rNames; % copy it, keep letter cases
[rNames, iRuns] = sort(lower(fnames));
for i = 1:nRun
a = rNames{i}(1:end-5); % remove _s003
% no conflict with both previous and next name
if nRun==1 || ... % only one run
(i==1 && ~strcmpi(a, rNames{2}(1:end-5))) || ... % first
(i==nRun && ~strcmpi(a, rNames{i-1}(1:end-5))) || ... % last
(i>1 && i<nRun && ~strcmpi(a, rNames{i-1}(1:end-5)) ...
&& ~strcmpi(a, rNames{i+1}(1:end-5))); % middle ones
fnames{iRuns(i)}(end+(-4:0)) = [];
end
end
fmtStr = sprintf(' %%-%gs %%gx%%gx%%gx%%g\n', max(cellfun(@numel, fnames))+6);
%% Now ready to convert nii series by series
subjStr = sprintf('''%s'', ', subj{:}); subjStr(end+(-1:0)) = [];
vendor = sprintf('%s, ', vendor{:}); vendor(end+(-1:0)) = [];
fprintf('Converting %g series (%s) into %g-D %s: subject %s\n', ...
nRun, vendor, 4-rst3D, ext, subjStr);
for i = 1:nRun
nFile = numel(h{i});
h{i}{1}.NiftiName = fnames{i}; % for convenience of error info
s = h{i}{1};
if nFile>1 && ~isfield(s, 'LastFile')
h{i}{1}.LastFile = h{i}{nFile}; % store partial last header into 1st
end
img = dicm_img(s, 0); % initialize data type and img size. No transpose
if ndims(img)>4 % err out, likely won't work for other series
error('Image with 5 or more dim not supported: %s', s.NiftiName);
end
img(:, :, :, 2:nFile) = 0; % pre-allocate
for j = 2:nFile, img(:,:,:,j) = dicm_img(h{i}{j}, 0); end
if size(img,3)<2, img = permute(img, [1 2 4 3]); end % put frames into dim3
if tryGetField(s, 'SamplesPerPixel', 1) > 1 % color image
img = permute(img, [1 2 4:8 3]); % put RGB into dim8 for nii_tool
elseif tryGetField(s, 'isMos', false) % SIEMENS mosaic
img = mos2vol(img, s.LocationsInAcquisition); % mosaic to volume
elseif ndims(img)==4 && tryGetField(s, 'Dim3IsVolume', false) % BV/BRIK
img = permute(img, [1 2 4 3]);
elseif ndims(img) == 3 % may need to reshape to 4D
nSL = double(tryGetField(s, 'LocationsInAcquisition'));
if ~isempty(nSL)
dim = size(img);
dim(3:4) = [nSL dim(3)/nSL]; % verified integer earlier
if nFile==1 && tryGetField(s, 'Dim3IsVolume', false)
% for PAR and single multiframe dicom
img = reshape(img, dim([1 2 4 3]));
img = permute(img, [1 2 4 3]);
else
img = reshape(img, dim);
end
end
% fix weird slice ordering for PAR (seen) and multiframe
if isfield(s, 'SliceNumber'), img(:,:,s.SliceNumber,:) = img; end
end
dim = size(img);
if numel(dim)<3, dim(3) = 1; end % single slice
fld = 'NumberOfTemporalPositions';
if ~isfield(s, fld) && numel(dim)>3 && dim(4)>1, h{i}{1}.(fld) = dim(4); end
if any(~isfield(s, {'ImageOrientationPatient' 'ImagePositionPatient'}))
h{i}{1} = csa2pos(h{i}{1}, dim(3));
end
% Store GE slice timing. No slice order info for Philips at all!
if isfield(s, 'RTIA_timer') && ~s.isDTI
t = zeros(dim(3), 1);
for j = 1:dim(3), t(j) = tryGetField(h{i}{j}, 'RTIA_timer', nan); end
if ~all(diff(t)==0), h{i}{1}.RefAcqTimes = t/10; end % in ms
% % Get slice timing for non-mosaic Siemens file. Could remove Manufacturer
% % check, but GE/Philips AcquisitionTime seems useless
% elseif numel(dim)>3 && dim(4)>2 && ~isfield(s, 'MosaicRefAcqTimes') ...
% && strncmpi(s.Manufacturer, 'SIEMENS', 7) && ~s.isDTI
% dict = dicm_dict('', {'AcquisitionDate' 'AcquisitionTime'});
% t = zeros(dim(3), 1);
% for j = 1:dim(3)
% s1 = dicm_hdr(h{i}{j}.Filename, dict);
% str = [s1.AcquisitionDate s1.AcquisitionTime];
% t(j) = datenum(str, 'yyyymmddHHMMSS.fff');
% end
% h{i}{1}.RefAcqTimes = (t - min(t)) * 24 * 3600 * 1000; % day to ms
end
% Store motion parameters for MoCo series
if all(isfield(s, {'RBMoCoTrans' 'RBMoCoRot'})) && numel(dim)>3
inc = nFile / dim(4);
trans = zeros(dim(4), 3);
rotat = zeros(dim(4), 3);
for j = 1:inc:nFile
trans(j,:) = tryGetField(h{i}{j}, 'RBMoCoTrans', [0 0 0]);
rotat(j,:) = tryGetField(h{i}{j}, 'RBMoCoRot', [0 0 0]);
end
h{i}{1}.RBMoCoTrans = trans;
h{i}{1}.RBMoCoRot = rotat;
end
if isa(img, 'uint16') && max(img(:))<32768
img = int16(img); % use int16 if lossless. seems always true
end
nii = nii_tool('init', img); % create nii struct based on img
fname = [dataFolder fnames{i}]; % name without ext
% Compute bval & bvec in image reference for DTI series
if s.isDTI, [h{i}, nii] = get_dti_para(h{i}, nii); end
[nii, h{i}{1}] = set_nii_header(nii, h{i}{1}); % set most nii header
h{i}{1}.NiftiCreator = converter;
nii.ext = set_nii_ext(h{i}{1}); % NIfTI extension
try save_json(h{i}{1}, fname); catch, end
% Save bval and bvec files after bvec perm/sign adjusted in set_nii_header
if s.isDTI, save_dti_para(h{i}{1}, fname); end
[nii, niiP] = split_philips_phase(nii, s); % split Philips mag&phase img
if ~isempty(niiP)
fprintf(fmtStr, [fnames{i} '_phase'], niiP.hdr.dim(2:5));
nii_tool('save', niiP, [fname '_phase' ext], rst3D); % save phase nii
end
fprintf(fmtStr, fnames{i}, nii.hdr.dim(2:5)); % show info and progress
nii_tool('save', nii, [fname ext], rst3D);
h{i} = h{i}{1}; % keep 1st dicm header only
if isnumeric(h{i}.PixelData), h{i} = rmfield(h{i}, 'PixelData'); end % BV
end
h = cell2struct(h, fnames, 2); % convert into struct
fname = [dataFolder 'dcmHeaders.mat'];
if exist(fname, 'file') % if file exists, we update fields only
S = load(fname);
for i = 1:numel(fnames), S.h.(fnames{i}) = h.(fnames{i}); end
h = S.h; %#ok
end
save(fname, 'h', '-v7'); % -v7 better compatibility
fprintf('Elapsed time by dicm2nii is %.1f seconds\n\n', toc);
if ~isempty(unzip_cmd), rmdir(dcmFolder, 's'); end % delete tmp dicom folder
return;
%% Subfunction: return folder name for a file name
function folder = folderFromFile(fname)
folder = fileparts(fname);
if isempty(folder), folder = pwd; end
%% Subfunction: return PatientName
function subj = PatientName(s)
subj = tryGetField(s, 'PatientName');
if isempty(subj), subj = tryGetField(s, 'PatientID', 'Anonymous'); end
%% Subfunction: return SeriesDescription
function name = ProtocolName(s)
name = tryGetField(s, 'SeriesDescription');
if isempty(name) || (strncmp(s.Manufacturer, 'SIEMENS', 7) && ...
numel(name)>9 && strcmp(name(end+(-9:0)), 'MoCoSeries'))
name = tryGetField(s, 'ProtocolName');
end
if isempty(name), [~, name] = fileparts(s.Filename); end
%% Subfunction: return true if any of keywords is in s.ImageType
function tf = isType(s, keywords)
typ = tryGetField(s, 'ImageType', '');
if ischar(keywords) % single keyword
tf = ~isempty(strfind(typ, keywords));
return;
end
for i = 1:numel(keywords)
tf = ~isempty(strfind(typ, keywords{i}));
if tf, return; end
end
%% Subfunction: return true if series is DTI
function tf = isDTI(s)
tf = isType(s, '\DIFFUSION'); % Siemens, Philips
if tf, return; end
if strncmp(s.Manufacturer, 'GE', 2) % not labeled as \DIFFISION
tf = tryGetField(s, 'DiffusionDirection', 0)>0;
elseif strncmpi(s.Manufacturer, 'Philips', 7)
tf = strcmp(tryGetField(s, 'MRSeriesDiffusion', 'N'), 'Y');
else % Some Siemens DTI are not labeled as \DIFFUSION
tf = ~isempty(csa_header(s, 'B_value'));
end
%% Subfunction: get field if exist, return default value otherwise
function val = tryGetField(s, field, dftVal)
if isfield(s, field), val = s.(field);
elseif nargin>2, val = dftVal;
else val = [];
end
%% Subfunction: Set most nii header and re-orient img
function [nii, s] = set_nii_header(nii, s)
% Transformation matrix: most important feature for nii
dim = nii.hdr.dim(2:4); % space dim, set by nii_tool according to img
[ixyz, R, pixdim, xyz_unit] = xform_mat(s, dim);
R(1:2,:) = -R(1:2,:); % dicom LPS to nifti RAS, xform matrix before reorient
% dim_info byte: freq_dim, phase_dim, slice_dim low to high, each 2 bits
[phPos, iPhase] = phaseDirection(s); % phPos relative to image in FSL feat!
if iPhase == 2, fps_bits = [1 4 16];
elseif iPhase == 1, fps_bits = [4 1 16];
else fps_bits = [0 0 16];
end
% set TR and slice timing related info before re-orient
[s, nii.hdr] = sliceTiming(s, nii.hdr);
nii.hdr.xyzt_units = xyz_unit + nii.hdr.xyzt_units; % normally: mm (2) + sec (8)
% Reorient if MRAcquisitionType==3D || isDTI && nSL>1
% If FSL etc can read dim_info for STC, we can always reorient.
[~, perm] = sort(ixyz); % may permute 3 dimensions in this order
if (strcmp(tryGetField(s, 'MRAcquisitionType', ''), '3D') || s.isDTI) && ...
dim(3)>1 && (~isequal(perm, 1:3)) % skip if already standard view
R(:, 1:3) = R(:, perm); % xform matrix after perm
fps_bits = fps_bits(perm);
ixyz = ixyz(perm); % 1:3 after perm
dim = dim(perm);
pixdim = pixdim(perm);
nii.hdr.dim(2:4) = dim;
nii.img = permute(nii.img, [perm 4:8]);
if isfield(s, 'bvec'), s.bvec = s.bvec(:, perm); end
end
iSL = find(fps_bits==16);
iPhase = find(fps_bits==4); % axis index for phase_dim in re-oriented img
nii.hdr.dim_info = (1:3) * fps_bits'; % useful for EPI only
nii.hdr.pixdim(2:4) = pixdim; % voxel zize
ind4 = ixyz + [0 4 8]; % index in 4xN matrix
flp = R(ind4)<0; % flip an axis if true
d = det(R(1:3,1:3)) * prod(1-flp*2); % det after all 3 axis positive
if d>0, flp(1) = ~flp(1); end % flip/unflip 1st axis to make det<0
rotM = diag([1-flp*2 1]); % 1 or -1 on diagnal
rotM(1:3, 4) = (dim-1) .* flp; % 0 or dim-1
R = R / rotM; % xform matrix after flip
for k = 1:3, if flp(k), nii.img = flipdim(nii.img, k); end; end %#ok
if flp(iPhase), phPos = ~phPos; end
if isfield(s, 'bvec'), s.bvec(:, flp) = -s.bvec(:, flp); end
if flp(iSL) && isfield(s, 'SliceTiming') % slices flipped
s.SliceTiming = s.SliceTiming(end:-1:1);
sc = nii.hdr.slice_code;
if sc>0, nii.hdr.slice_code = sc+mod(sc,2)*2-1; end % 1<->2, 3<->4, 5<->6
end
% sform
frmCode = all(isfield(s, {'ImageOrientationPatient' 'ImagePositionPatient'}));
frmCode = tryGetField(s, 'TemplateSpace', frmCode); % 1: SCANNER_ANAT
nii.hdr.sform_code = frmCode;
nii.hdr.srow_x = R(1,:);
nii.hdr.srow_y = R(2,:);
nii.hdr.srow_z = R(3,:);
% qform
if abs(sum(R(:,iSL).^2) - pixdim(iSL)^2) < 0.01 % no shear at slice direction
nii.hdr.qform_code = frmCode;
nii.hdr.qoffset_x = R(1,4);
nii.hdr.qoffset_y = R(2,4);
nii.hdr.qoffset_z = R(3,4);
R = R(1:3, 1:3); % for quaternion
R = bsxfun(@rdivide, R, sqrt(sum(R.^2))); % normalize
[q, nii.hdr.pixdim(1)] = dcm2quat(R); % 3x3 dir cos matrix to quaternion
nii.hdr.quatern_b = q(2);
nii.hdr.quatern_c = q(3);
nii.hdr.quatern_d = q(4);
end
% store some possibly useful info in descrip and other text hdr
str = tryGetField(s, 'ImageComments');
if isType(s, '\MOCO\'), str = ''; end % useless for MoCo
foo = tryGetField(s, 'StudyComments');
if ~isempty(foo), str = [str ';' foo]; end
str = [str ';' strtok(s.Manufacturer)];
foo = tryGetField(s, 'ProtocolName');
if ~isempty(foo), str = [str ';' foo]; end