calculateQCmeasures.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. function calculateQCmeasures(functional4D_fn, structural_fn, fwhm, spm_dir, out_dir, subject)
  2. % NOTE: THIS TOOL IS UNDER CONTINUOUS DEVELOPMENT AND HAS NOT BEEN SUFFCIENTLY TESTED
  3. % Function to calculate multiple quality control measures for an fMRI time
  4. % series using SPM12 and Matlab. The goal is to create a tool, for use by
  5. % fMRI technicians/researchers/clinicians familiar with SPM12 and Matlab,
  6. % that allows the calculation of measures that can assist in diagnosing
  7. % quality issues in fMRI data. It is not intended for use as ground truth
  8. % for quality diagnosis, as these measures are known to be relative and to
  9. % vary based on scanner site, acquisition time, data format, and more.
  10. % However, it can provide insight into possible data quality issues
  11. % originating from the scanner or single subject.
  12. % Function steps include preprocessing the acquired fMRI and structural data
  13. % (coregistering structural image to first functional image, segmenting the
  14. % coregistered structural image into tissue types, and reslicing the
  15. % segments to the functional resolution image grid), and calling functions
  16. % for a standard set of QC measures based on various sources (mainly
  17. % including the Quality Assessment Protocol (QAP) from the Preprocessed
  18. % Connectomes Project (PCP) and MRIQC. This function makes use of SPM12
  19. % functions and batch routines. If SPM12 batch parameters are not
  20. % explicitly set, defaults are assumed.
  21. %
  22. % CURRENT QC MEASURES (27/06/2018):
  23. % - Temporal signal to noise ratio (tSNR) (3D image)
  24. % - Mean tSNR of brain, grey matter, white matter, and CSF
  25. % - Z-score timeseries and mean
  26. % - Standard deviation (3D image)
  27. % - Framewise displacement (FD) timeseries, total and mean (mm)
  28. % - GCOR = global correlation ....
  29. % - Non-standardized differential variance (DVARS) timeseries (a.u.)
  30. % - The Plot (GM, WM and CSF voxel intensities over time)
  31. %
  32. % INPUT:
  33. % funcional4D_fn - filename of 4D functional timeseries (.nii only)
  34. % structural_fn - filename of T1-weighted structural scan (.nii only)
  35. % fwhm - kernel size for smoothing operations (mm)
  36. % spm_dir - SPM12 directory
  37. % out_dir - output directory (for figures and html logfile)
  38. % subject - subject name/code
  39. %
  40. % OUTPUT:
  41. % Matlab figures: - timeseries plots (FD, DVARS, Zscore, The Plot)
  42. % - montage images (tSNR, tSNR brain, stddev, mean EPI)
  43. % - coregistration contour plot
  44. % HTML logfile: - HTML logfile with measures and figures and metadata
  45. %
  46. % SOURCES:
  47. % PCP-QAP: - http://preprocessed-connectomes-project.org/quality-assessment-protocol/index.html
  48. % MRIQC: - https://mriqc.readthedocs.io/en/latest/
  49. % The Plot: - https://www.sciencedirect.com/science/article/pii/S1053811916303871?via%3Dihub
  50. %__________________________________________________________________________
  51. % Copyright (C) Stephan Heunis 2018
  52. % User defined variables
  53. % -------------------------------------------------------------------------
  54. intensity_scale = [-6 6]; % scaling for plot image intensity, see what works
  55. FD_threshold = 0.2; % mm
  56. % -------------------------------------------------------------------------
  57. if ~exist(out_dir, 'dir')
  58. mkdir(out_dir)
  59. end
  60. cd(out_dir)
  61. % Get image information
  62. func_spm = spm_vol(functional4D_fn);
  63. tsize = size(func_spm);
  64. Nt = tsize(1);
  65. Ni= func_spm(1).dim(1);
  66. Nj= func_spm(1).dim(2);
  67. Nk= func_spm(1).dim(3);
  68. % Preprocess structural and functional data.
  69. [d, f, e] = fileparts(structural_fn);
  70. if exist([d filesep 'rc1' f e], 'file')
  71. % If a resliced grey matter image is present in the specified data
  72. % directory, assume that all preprocessing has been completed by
  73. % standardPreproc, and declare appropriate variables.
  74. disp('Preproc done!')
  75. preproc_data = struct;
  76. preproc_data.forward_transformation = [d filesep 'y_' f e];
  77. preproc_data.inverse_transformation = [d filesep 'iy_' f e];
  78. preproc_data.gm_fn = [d filesep 'c1' f e];
  79. preproc_data.wm_fn = [d filesep 'c2' f e];
  80. preproc_data.csf_fn = [d filesep 'c3' f e];
  81. preproc_data.bone_fn = [d filesep 'c4' f e];
  82. preproc_data.soft_fn = [d filesep 'c5' f e];
  83. preproc_data.air_fn = [d filesep 'c6' f e];
  84. preproc_data.rstructural_fn = [d filesep 'r' f e];
  85. preproc_data.rgm_fn = [d filesep 'rc1' f e];
  86. preproc_data.rwm_fn = [d filesep 'rc2' f e];
  87. preproc_data.rcsf_fn = [d filesep 'rc3' f e];
  88. preproc_data.rbone_fn = [d filesep 'rc4' f e];
  89. preproc_data.rsoft_fn = [d filesep 'rc5' f e];
  90. preproc_data.rair_fn = [d filesep 'rc6' f e];
  91. [d, f, e] = fileparts(functional4D_fn);
  92. preproc_data.rfunctional_fn = [d filesep 'r' f e];
  93. preproc_data.srfunctional_fn = [d filesep 'sr' f e];
  94. preproc_data.sfunctional_fn = [d filesep 's' f e];
  95. preproc_data.mp_fn = [d filesep 'rp_' f '.txt'];
  96. preproc_data.MP = load(preproc_data.mp_fn);
  97. else
  98. % If a resliced grey matter image is NOT present in the specified data
  99. % directory, run standardPreproc
  100. preproc_data = standardPreproc(functional4D_fn, structural_fn, fwhm, spm_dir);
  101. end
  102. % Calculate brain mask matrices for GM, WM, CSF, and all together
  103. mask_threshold = 0.5;
  104. [GM_img_bin, WM_img_bin, CSF_img_bin] = createBinarySegments(preproc_data.rgm_fn, preproc_data.rwm_fn, preproc_data.rcsf_fn, mask_threshold);
  105. I_GM = find(GM_img_bin);
  106. I_WM = find(WM_img_bin);
  107. I_CSF = find(CSF_img_bin);
  108. mask_reshaped = GM_img_bin | WM_img_bin | CSF_img_bin;
  109. I_mask = find(mask_reshaped);
  110. Nmaskvox = numel(I_mask);
  111. % Detrend 4D time series
  112. output = detrend4D(functional4D_fn);
  113. F4D_detrended = output.F4D_detrended;
  114. F2D_detrended = output.F_2D_detrended;
  115. % Statistical measures
  116. F2D_mean = mean(F2D_detrended, 2);
  117. F2D_stddev = std(F2D_detrended, 0, 2);
  118. F2D_zstat = (F2D_detrended - F2D_mean)./F2D_stddev;
  119. F2D_zstat(isnan(F2D_zstat))=0;
  120. F2D_zstat_mean = mean(abs(F2D_zstat),1);
  121. Zstat_mean = mean(F2D_zstat_mean);
  122. F2D_var = var(F2D_detrended,0,2);
  123. F2D_psc = 100*(F2D_detrended./repmat(F2D_mean, 1, Nt)) - 100;
  124. F2D_psc(isnan(F2D_psc))=0;
  125. % Framewise displacement
  126. r = 80; % mm
  127. FD_measures = calculateFD(preproc_data.MP, r, FD_threshold);
  128. % DVARS
  129. F2D_diff = [zeros(1, Ni*Nj*Nk); diff(F2D_detrended')]';
  130. DVARS = var(F2D_diff);
  131. % GCOR
  132. % Steps according to https://doi.org/10.1089/brain.2013.0156:
  133. % (1)?De-mean each voxel's time series and scale it by its Eucledian norm
  134. % (2)?Average scaled time series over the whole brain mask
  135. % (3)?GCOR is the length (L2 norm) of this averaged series
  136. F2D_demeaned = F2D_detrended - F2D_mean;
  137. F2D_norms = sqrt(sum(F2D_demeaned.^2,2));
  138. F2D_scaled = F2D_demeaned./F2D_norms;
  139. F2D_ave_timeseries = mean(F2D_scaled(I_mask,:), 1);
  140. GCOR = sum(F2D_ave_timeseries.^2,2);
  141. % The Plot (with FD, DVARS, and mean Zscore per volume)
  142. GM_img = F2D_psc(I_GM, :);
  143. WM_img = F2D_psc(I_WM, :);
  144. CSF_img = F2D_psc(I_CSF, :);
  145. all_img = [GM_img; WM_img; CSF_img];
  146. line1_pos = numel(I_GM);
  147. line2_pos = numel(I_GM) + numel(I_WM);
  148. tf = figure;
  149. fontsizeL = 14;
  150. fontsizeM = 11;
  151. ax1 = subplot(7,1,4:7);
  152. imagesc(ax1, all_img); colormap(gray); caxis(intensity_scale);
  153. title(ax1, 'thePlotSpm','fontsize',fontsizeL)
  154. ylabel(ax1, 'Voxels','fontsize',fontsizeM)
  155. xlabel(ax1, 'fMRI volumes','fontsize',fontsizeM)
  156. hold on; line([1 Nt],[line1_pos line1_pos], 'Color', 'b', 'LineWidth', 2 )
  157. line([1 Nt],[line2_pos line2_pos], 'Color', 'r', 'LineWidth', 2 )
  158. hold off;
  159. ax2 = subplot(7,1,1);
  160. plot(ax2, FD_measures.FD, 'LineWidth', 2); grid;
  161. set(ax2,'Xticklabel',[]);
  162. title(ax2, 'FD','fontsize',fontsizeL)
  163. ylabel(ax2, 'mm','fontsize',fontsizeM)
  164. ax3 = subplot(7,1,2);
  165. plot(ax3, DVARS, 'LineWidth', 2); grid;
  166. set(ax3,'Xticklabel',[]);
  167. title(ax3, 'DVARS','fontsize',fontsizeL)
  168. ylabel(ax3, 'a.u.','fontsize',fontsizeM)
  169. ax4 = subplot(7,1,3);
  170. plot(ax4, F2D_zstat_mean, 'LineWidth', 2); grid;
  171. set(ax4,'Xticklabel',[]);
  172. title(ax4, 'Z-score','fontsize',fontsizeL)
  173. ylabel(ax4, 'a.u.','fontsize',fontsizeM)
  174. print(tf, 'timeseries_summary', '-dpng')
  175. % tSNR
  176. tSNR_2D = F2D_mean./F2D_stddev;
  177. tSNR_brain = mean(tSNR_2D(I_mask));
  178. tSNR_GM = mean(tSNR_2D(I_GM));
  179. tSNR_WM = mean(tSNR_2D(I_WM));
  180. tSNR_CSF = mean(tSNR_2D(I_CSF));
  181. % Display metrics in command window
  182. disp(['Number of volumes classified as outliers based on FD>=' num2str(FD_threshold) 'mm: ' num2str(numel(FD_measures.FD_outliers_ind))])
  183. disp(['Total FD: ' num2str(FD_measures.FD_sum)])
  184. disp(['Mean FD: ' num2str(FD_measures.FD_mean)])
  185. disp(['Mean Zscore: ' num2str(Zstat_mean)])
  186. disp(['GCOR: ' num2str(GCOR)])
  187. disp(['tSNR (brain): ' num2str(tSNR_brain)])
  188. disp(['tSNR (GM): ' num2str(tSNR_GM)])
  189. disp(['tSNR (WM): ' num2str(tSNR_WM)])
  190. disp(['tSNR (CSF): ' num2str(tSNR_CSF)])
  191. % Prepare 3D and 4D images
  192. mask_3D = reshape(mask_reshaped, Ni, Nj, Nk);
  193. tSNR_3D = reshape(tSNR_2D, Ni, Nj, Nk);
  194. F3D_mean = reshape(F2D_mean, Ni, Nj, Nk);
  195. F3D_var = reshape(F2D_var, Ni, Nj, Nk);
  196. F3D_stddev = reshape(F2D_stddev, Ni, Nj, Nk);
  197. tSNR_2D_masked = zeros(Ni*Nj*Nk, 1);
  198. tSNR_2D_masked(I_mask, :) = tSNR_2D(I_mask, :);
  199. tSNR_3D_masked = reshape(tSNR_2D_masked, Ni, Nj, Nk);
  200. % Create montages of 3D images
  201. montage2 = createMontage(F3D_mean, 5, 1, 'Mean EPI (whole image)', 'gray');
  202. print(montage2.f, 'mean_epi', '-dpng')
  203. montage3 = createMontage(F3D_stddev, 5, 1, 'Standard deviation (whole image)', 'parula');
  204. print(montage3.f, 'stddev_epi', '-dpng')
  205. montage1 = createMontage(tSNR_3D, 5, 1, 'tSNR (whole image)', 'hot');
  206. print(montage1.f, 'tsnr_whole', '-dpng')
  207. montage4 = createMontage(tSNR_3D_masked, 5, 1, 'tSNR (brain)', 'hot');
  208. print(montage4.f, 'tsnr_brain', '-dpng')
  209. figmask = displayMaskContour(F3D_mean, mask_3D, 0, 3);
  210. print(figmask, 'mask_contour', '-dpng')
  211. % Create HTML log file
  212. dt = datetime('now');
  213. [Y,MO,D,H,MI,S] = datevec(dt);
  214. dt_str = [num2str(Y) num2str(MO) num2str(D) num2str(H) num2str(MI) num2str(round(S))];
  215. t = datestr(dt);
  216. log_name = [subject '_' dt_str '.html'];
  217. fid = fopen(log_name,'a');
  218. fprintf(fid, '<H2>Log</H2>');
  219. fprintf(fid, ['\n<BR>Subject: ' subject]);
  220. fprintf(fid, ['\n<BR>Date/time: ' t]);
  221. fprintf(fid, '<H2>Imaging info</H2>');
  222. fprintf(fid, ['\nVolumes: ' num2str(Nt)]);
  223. fprintf(fid, ['\n<BR>Voxels (x,y,z): ' num2str(Ni) ', ' num2str(Nj) ', ' num2str(Nk)]);
  224. fprintf(fid, '<H2>Timeseries summary</H2>');
  225. fprintf(fid, '\n<TABLE><TR><TD><img src="timeseries_summary.png" alt="no picture" width=700 height=600></TD></TR></TABLE>' );
  226. fprintf(fid, '<H2>QC Metrics</H2>');
  227. fprintf(fid, ['\nFD threshold (mm): ' num2str(FD_threshold)]);
  228. fprintf(fid, ['\n<BR>FD outliers: ' num2str(numel(FD_measures.FD_outliers_ind))]);
  229. fprintf(fid, ['\n<BR>Total FD: ' num2str(FD_measures.FD_sum)]);
  230. fprintf(fid, ['\n<BR>Mean FD: ' num2str(FD_measures.FD_mean)]);
  231. fprintf(fid, ['\n<BR>Mean Zscore: ' num2str(Zstat_mean)]);
  232. fprintf(fid, ['\n<BR>GCOR: ' num2str(GCOR)]);
  233. fprintf(fid, ['\n<BR>tSNR (brain): ' num2str(tSNR_brain)]);
  234. fprintf(fid, ['\n<BR>tSNR (GM): ' num2str(tSNR_GM)]);
  235. fprintf(fid, ['\n<BR>tSNR (WM): ' num2str(tSNR_WM)]);
  236. fprintf(fid, ['\n<BR>tSNR (CSF): ' num2str(tSNR_CSF)]);
  237. fprintf(fid, '<H2>QC brain images</H2>');
  238. fprintf(fid, '\n<TABLE><TR><TD><img src="mean_epi.png" alt="no picture" width=700 height=600></TD></TR></TABLE>' );
  239. fprintf(fid, '\n<TABLE><TR><TD><img src="stddev_epi.png" alt="no picture" width=700 height=600></TD></TR></TABLE>' );
  240. fprintf(fid, '\n<TABLE><TR><TD><img src="tsnr_whole.png" alt="no picture" width=700 height=600></TD></TR></TABLE>' );
  241. fprintf(fid, '\n<TABLE><TR><TD><img src="tsnr_brain.png" alt="no picture" width=700 height=600></TD></TR></TABLE>' );
  242. fprintf(fid, '\n<TABLE><TR><TD><img src="mask_contour.png" alt="no picture" width=700 height=700></TD></TR></TABLE>' );
  243. fclose(fid);