Note
Go to the end to download the full example code. or to run this example in your browser via Binder
Extracting signals from a brain parcellation¶
Here we show how to extract signals from a brain parcellation and compute a correlation matrix.
We also show the importance of defining good confounds signals: the
first correlation matrix is computed after regressing out simple
confounds signals: movement regressors, white matter and CSF signals, …
The second one is without any confounds: all regions are connected to each
other. Finally we demonstrated the functionality of
nilearn.interfaces.fmriprep.load_confounds
to flexibly select confound
variables from fMRIPrep outputs while following some implementation
guideline of fMRIPrep confounds documentation
https://fmriprep.org/en/stable/outputs.html#confounds.
One reference that discusses the importance of confounds is Varoquaux and Craddock[1].
This is just a code example, see the corresponding section in the documentation for more.
Note
If you are using Nilearn with a version older than 0.9.0
,
then you should either upgrade your version or import maskers
from the input_data
module instead of the maskers
module.
That is, you should manually replace in the following example all occurrences of:
from nilearn.maskers import NiftiMasker
with:
from nilearn.input_data import NiftiMasker
Retrieve the atlas and the data¶
from nilearn import datasets
dataset = datasets.fetch_atlas_harvard_oxford("cort-maxprob-thr25-2mm")
atlas_filename = dataset.maps
labels = dataset.labels
print(f"Atlas ROIs are located in nifti image (4D) at: {atlas_filename}")
# One subject of brain development fMRI data
data = datasets.fetch_development_fmri(n_subjects=1, reduce_confounds=True)
fmri_filenames = data.func[0]
reduced_confounds = data.confounds[0] # This is a preselected set of confounds
[get_dataset_dir] Dataset found in /home/remi/nilearn_data/fsl
Atlas ROIs are located in nifti image (4D) at:
<class 'nibabel.nifti1.Nifti1Image'>
data shape (91, 109, 91)
affine:
[[ 2. 0. 0. -90.]
[ 0. 2. 0. -126.]
[ 0. 0. 2. -72.]
[ 0. 0. 0. 1.]]
metadata:
<class 'nibabel.nifti1.Nifti1Header'> object, endian='<'
sizeof_hdr : 348
data_type : b''
db_name : b''
extents : 0
session_error : 0
regular : b'r'
dim_info : 0
dim : [ 3 91 109 91 1 1 1 1]
intent_p1 : 0.0
intent_p2 : 0.0
intent_p3 : 0.0
intent_code : none
datatype : uint8
bitpix : 8
slice_start : 0
pixdim : [1. 2. 2. 2. 1. 1. 1. 1.]
vox_offset : 0.0
scl_slope : nan
scl_inter : nan
slice_end : 0
slice_code : unknown
xyzt_units : 10
cal_max : 48.0
cal_min : 0.0
slice_duration : 0.0
toffset : 0.0
glmax : 0
glmin : 0
descrip : b'FSL3.3'
aux_file : b'MGH-Cortical'
qform_code : unknown
sform_code : aligned
quatern_b : 0.0
quatern_c : 0.0
quatern_d : 0.0
qoffset_x : -90.0
qoffset_y : -126.0
qoffset_z : -72.0
srow_x : [ 2. 0. 0. -90.]
srow_y : [ 0. 2. 0. -126.]
srow_z : [ 0. 0. 2. -72.]
intent_name : b''
magic : b'n+1'
[get_dataset_dir] Dataset found in /home/remi/nilearn_data/development_fmri
[get_dataset_dir] Dataset found in /home/remi/nilearn_data/development_fmri/development_fmri
[get_dataset_dir] Dataset found in /home/remi/nilearn_data/development_fmri/development_fmri
Extract signals on a parcellation defined by labels¶
Using the NiftiLabelsMasker
from nilearn.maskers import NiftiLabelsMasker
masker = NiftiLabelsMasker(
labels_img=atlas_filename,
standardize="zscore_sample",
standardize_confounds="zscore_sample",
memory="nilearn_cache",
verbose=5,
)
# Here we go from nifti files to the signal time series in a numpy
# array. Note how we give confounds to be regressed out during signal
# extraction
time_series = masker.fit_transform(fmri_filenames, confounds=reduced_confounds)
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
[NiftiLabelsMasker.wrapped] Resampling labels
________________________________________________________________________________
[Memory] Calling nilearn.maskers.base_masker._filter_and_extract...
_filter_and_extract('/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz',
<nilearn.maskers.nifti_labels_masker._ExtractionFunctor object at 0x740cf8431520>,
{ 'background_label': 0,
'clean_kwargs': {},
'detrend': False,
'dtype': None,
'high_pass': None,
'high_variance_confounds': False,
'keep_masked_labels': True,
'labels': None,
'labels_img': <nibabel.nifti1.Nifti1Image object at 0x740cf8473d60>,
'low_pass': None,
'mask_img': None,
'reports': True,
'smoothing_fwhm': None,
'standardize': 'zscore_sample',
'standardize_confounds': 'zscore_sample',
'strategy': 'mean',
't_r': None,
'target_affine': None,
'target_shape': None}, confounds=[ '/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_desc-reducedConfounds_regressors.tsv'], sample_mask=None, dtype=None, memory=Memory(location=nilearn_cache/joblib), memory_level=1, verbose=5)
[NiftiLabelsMasker.wrapped] Loading data from
/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
[NiftiLabelsMasker.wrapped] Extracting region signals
[NiftiLabelsMasker.wrapped] Cleaning extracted signals
_______________________________________________filter_and_extract - 0.8s, 0.0min
Compute and display a correlation matrix¶
from nilearn.connectome import ConnectivityMeasure
correlation_measure = ConnectivityMeasure(
kind="correlation",
standardize="zscore_sample",
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
# Plot the correlation matrix
import numpy as np
from nilearn import plotting
# Make a large figure
# Mask the main diagonal for visualization:
np.fill_diagonal(correlation_matrix, 0)
# The labels we have start with the background (0), hence we skip the
# first label
# matrices are ordered for block-like representation
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="Confounds",
reorder=True,
)
<matplotlib.image.AxesImage object at 0x740cf8489a90>
Extract signals and compute a connectivity matrix without confounds removal¶
After covering the basic of signal extraction and functional connectivity matrix presentation, let’s look into the impact of confounds to fMRI signal and functional connectivity. Firstly let’s find out what a functional connectivity matrix looks like without confound removal.
time_series = masker.fit_transform(fmri_filenames)
# Note how we did not specify confounds above. This is bad!
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="No confounds",
reorder=True,
)
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
________________________________________________________________________________
[Memory] Calling nilearn.maskers.base_masker._filter_and_extract...
_filter_and_extract('/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz',
<nilearn.maskers.nifti_labels_masker._ExtractionFunctor object at 0x740cf84735b0>,
{ 'background_label': 0,
'clean_kwargs': {},
'detrend': False,
'dtype': None,
'high_pass': None,
'high_variance_confounds': False,
'keep_masked_labels': True,
'labels': None,
'labels_img': <nibabel.nifti1.Nifti1Image object at 0x740cf8473d60>,
'low_pass': None,
'mask_img': None,
'reports': True,
'smoothing_fwhm': None,
'standardize': 'zscore_sample',
'standardize_confounds': 'zscore_sample',
'strategy': 'mean',
't_r': None,
'target_affine': None,
'target_shape': None}, confounds=None, sample_mask=None, dtype=None, memory=Memory(location=nilearn_cache/joblib), memory_level=1, verbose=5)
[NiftiLabelsMasker.wrapped] Loading data from
/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
[NiftiLabelsMasker.wrapped] Extracting region signals
[NiftiLabelsMasker.wrapped] Cleaning extracted signals
_______________________________________________filter_and_extract - 0.9s, 0.0min
<matplotlib.image.AxesImage object at 0x740ced053ac0>
Load confounds from file using a flexible strategy with fmriprep interface¶
The nilearn.interfaces.fmriprep.load_confounds
function provides
flexible parameters to retrieve the relevant columns from the TSV file
generated by fMRIPrep.
nilearn.interfaces.fmriprep.load_confounds
ensures two things:
The correct regressors are selected with provided strategy, and
Volumes such as non-steady-state and/or high motion volumes are masked out correctly.
Let’s try a simple strategy removing motion, white matter signal, cerebrospinal fluid signal with high-pass filtering.
from nilearn.interfaces.fmriprep import load_confounds
confounds_simple, sample_mask = load_confounds(
fmri_filenames,
strategy=["high_pass", "motion", "wm_csf"],
motion="basic",
wm_csf="basic",
)
print("The shape of the confounds matrix is:", confounds_simple.shape)
print(confounds_simple.columns)
time_series = masker.fit_transform(
fmri_filenames, confounds=confounds_simple, sample_mask=sample_mask
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="Motion, WM, CSF",
reorder=True,
)
The shape of the confounds matrix is: (168, 12)
Index(['cosine00', 'cosine01', 'cosine02', 'cosine03', 'csf', 'rot_x', 'rot_y',
'rot_z', 'trans_x', 'trans_y', 'trans_z', 'white_matter'],
dtype='object')
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
________________________________________________________________________________
[Memory] Calling nilearn.maskers.base_masker._filter_and_extract...
_filter_and_extract('/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz',
<nilearn.maskers.nifti_labels_masker._ExtractionFunctor object at 0x740cf8a482e0>,
{ 'background_label': 0,
'clean_kwargs': {},
'detrend': False,
'dtype': None,
'high_pass': None,
'high_variance_confounds': False,
'keep_masked_labels': True,
'labels': None,
'labels_img': <nibabel.nifti1.Nifti1Image object at 0x740cf8473d60>,
'low_pass': None,
'mask_img': None,
'reports': True,
'smoothing_fwhm': None,
'standardize': 'zscore_sample',
'standardize_confounds': 'zscore_sample',
'strategy': 'mean',
't_r': None,
'target_affine': None,
'target_shape': None}, confounds=[ cosine00 cosine01 cosine02 cosine03 csf rot_x rot_y rot_z trans_x trans_y trans_z white_matter
0 0.109104 0.109090 0.109066 0.109033 -2.675004 0.000304 0.000583 0.000201 0.006621 -0.026078 0.055006 -0.876886
1 0.109066 0.108937 0.108723 0.108423 -2.902773 -0.000316 0.000418 0.000135 0.000668 -0.027587 0.049458 -1.418909
2 0.108990 0.108632 0.108038 0.107207 -2.629915 -0.000285 0.000595 0.000076 0.006628 -0.019085 0.075787 -1.540842
3 0.108875 0.108176 0.107012 0.105391 -1.601793 -0.000226 0.001049 0.000041 0.009347 -0.023900 0.053022 -1.922085
4 0.108723 0.107567 0.105651 0.102986 -2.258970 -0.0..., sample_mask=None, dtype=None, memory=Memory(location=nilearn_cache/joblib), memory_level=1, verbose=5)
[NiftiLabelsMasker.wrapped] Loading data from
/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
[NiftiLabelsMasker.wrapped] Extracting region signals
[NiftiLabelsMasker.wrapped] Cleaning extracted signals
_______________________________________________filter_and_extract - 0.9s, 0.0min
<matplotlib.image.AxesImage object at 0x740d1aa5ceb0>
Motion-based scrubbing¶
With a scrubbing-based strategy,
load_confounds
returns a sample_mask
that removes the index of volumes exceeding the framewise displacement and
standardised DVARS threshold, and all the continuous segment with less than
five volumes. Before applying scrubbing, it’s important to access the
percentage of volumns scrubbed. Scrubbing is not a suitable strategy for
datasets with too many high motion subjects.
On top of the simple strategy above, let’s add scrubbing to our
strategy.
confounds_scrub, sample_mask = load_confounds(
fmri_filenames,
strategy=["high_pass", "motion", "wm_csf", "scrub"],
motion="basic",
wm_csf="basic",
scrub=5,
fd_threshold=0.5,
std_dvars_threshold=1.5,
)
print(
f"After scrubbing, {sample_mask.shape[0]} "
f"out of {confounds_scrub.shape[0]} volumes remains"
)
print("The shape of the confounds matrix is:", confounds_simple.shape)
print(confounds_scrub.columns)
time_series = masker.fit_transform(
fmri_filenames, confounds=confounds_scrub, sample_mask=sample_mask
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="Motion, WM, CSF, Scrubbing",
reorder=True,
)
After scrubbing, 164 out of 168 volumes remains
The shape of the confounds matrix is: (168, 12)
Index(['cosine00', 'cosine01', 'cosine02', 'cosine03', 'csf', 'rot_x', 'rot_y',
'rot_z', 'trans_x', 'trans_y', 'trans_z', 'white_matter'],
dtype='object')
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
________________________________________________________________________________
[Memory] Calling nilearn.maskers.base_masker._filter_and_extract...
_filter_and_extract('/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz',
<nilearn.maskers.nifti_labels_masker._ExtractionFunctor object at 0x740d1d0796d0>,
{ 'background_label': 0,
'clean_kwargs': {},
'detrend': False,
'dtype': None,
'high_pass': None,
'high_variance_confounds': False,
'keep_masked_labels': True,
'labels': None,
'labels_img': <nibabel.nifti1.Nifti1Image object at 0x740cf8473d60>,
'low_pass': None,
'mask_img': None,
'reports': True,
'smoothing_fwhm': None,
'standardize': 'zscore_sample',
'standardize_confounds': 'zscore_sample',
'strategy': 'mean',
't_r': None,
'target_affine': None,
'target_shape': None}, confounds=[ cosine00 cosine01 cosine02 cosine03 csf rot_x rot_y rot_z trans_x trans_y trans_z white_matter
0 0.108440 0.106895 0.110644 0.110240 -2.669409 0.000307 0.000590 0.000217 0.006357 -0.026366 0.054759 -0.878390
1 0.108401 0.106742 0.110301 0.109631 -2.897179 -0.000313 0.000426 0.000150 0.000404 -0.027876 0.049211 -1.420413
2 0.108325 0.106438 0.109616 0.108415 -2.624321 -0.000282 0.000602 0.000092 0.006364 -0.019374 0.075540 -1.542346
3 0.108211 0.105981 0.108591 0.106599 -1.596198 -0.000223 0.001057 0.000057 0.009083 -0.024188 0.052775 -1.923589
4 0.108058 0.105373 0.107229 0.104194 -2.253376 -0.0..., sample_mask=array([ 0, ..., 167]), dtype=None, memory=Memory(location=nilearn_cache/joblib), memory_level=1, verbose=5)
[NiftiLabelsMasker.wrapped] Loading data from
/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
[NiftiLabelsMasker.wrapped] Extracting region signals
[NiftiLabelsMasker.wrapped] Cleaning extracted signals
_______________________________________________filter_and_extract - 1.0s, 0.0min
<matplotlib.image.AxesImage object at 0x740d1d730d00>
The impact of global signal removal¶
Global signal removes the grand mean from your signal. The benefit is that it can remove impacts of physiological artifacts with minimal impact on the degrees of freedom. The downside is that one cannot get insight into variance explained by certain sources of noise. Now let’s add global signal to the simple strategy and see its impact.
confounds_minimal_no_gsr, sample_mask = load_confounds(
fmri_filenames,
strategy=["high_pass", "motion", "wm_csf", "global_signal"],
motion="basic",
wm_csf="basic",
global_signal="basic",
)
print("The shape of the confounds matrix is:", confounds_minimal_no_gsr.shape)
print(confounds_minimal_no_gsr.columns)
time_series = masker.fit_transform(
fmri_filenames, confounds=confounds_minimal_no_gsr, sample_mask=sample_mask
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="Motion, WM, CSF, GSR",
reorder=True,
)
The shape of the confounds matrix is: (168, 13)
Index(['cosine00', 'cosine01', 'cosine02', 'cosine03', 'csf', 'global_signal',
'rot_x', 'rot_y', 'rot_z', 'trans_x', 'trans_y', 'trans_z',
'white_matter'],
dtype='object')
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
________________________________________________________________________________
[Memory] Calling nilearn.maskers.base_masker._filter_and_extract...
_filter_and_extract('/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz',
<nilearn.maskers.nifti_labels_masker._ExtractionFunctor object at 0x740d1d489a30>,
{ 'background_label': 0,
'clean_kwargs': {},
'detrend': False,
'dtype': None,
'high_pass': None,
'high_variance_confounds': False,
'keep_masked_labels': True,
'labels': None,
'labels_img': <nibabel.nifti1.Nifti1Image object at 0x740cf8473d60>,
'low_pass': None,
'mask_img': None,
'reports': True,
'smoothing_fwhm': None,
'standardize': 'zscore_sample',
'standardize_confounds': 'zscore_sample',
'strategy': 'mean',
't_r': None,
'target_affine': None,
'target_shape': None}, confounds=[ cosine00 cosine01 cosine02 cosine03 csf global_signal ... rot_y rot_z trans_x trans_y trans_z white_matter
0 0.109104 0.109090 0.109066 0.109033 -2.675004 -2.997517 ... 0.000583 0.000201 0.006621 -0.026078 0.055006 -0.876886
1 0.109066 0.108937 0.108723 0.108423 -2.902773 -3.229693 ... 0.000418 0.000135 0.000668 -0.027587 0.049458 -1.418909
2 0.108990 0.108632 0.108038 0.107207 -2.629915 -3.059988 ... 0.000595 0.000076 0.006628 -0.019085 0.075787 -1.540842
3 0.108875 0.108176 0.107012 0.105391 -1.601793 -2.778077 ... 0.001049 0.000041 0.009347 -0.023900 0.053022 -1.922085
4 0.1..., sample_mask=None, dtype=None, memory=Memory(location=nilearn_cache/joblib), memory_level=1, verbose=5)
[NiftiLabelsMasker.wrapped] Loading data from
/home/remi/nilearn_data/development_fmri/development_fmri/sub-pixar123_task-pixar_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
[NiftiLabelsMasker.wrapped] Extracting region signals
[NiftiLabelsMasker.wrapped] Cleaning extracted signals
_______________________________________________filter_and_extract - 0.9s, 0.0min
<matplotlib.image.AxesImage object at 0x740d1a698040>
Using predefined strategies¶
Instead of customising the strategy through
nilearn.interfaces.fmriprep.load_confounds
, one can use a predefined
strategy with nilearn.interfaces.fmriprep.load_confounds_strategy
.
Based on the confound variables generated through fMRIPrep, and past
benchmarks studies (Ciric et al.[2], Parkes et al.[3]):
simple, scrubbing, compcor, ica_aroma.
The following examples shows how to use the simple strategy and overwrite
the motion default to basic.
from nilearn.interfaces.fmriprep import load_confounds_strategy
# use default parameters
confounds, sample_mask = load_confounds_strategy(
fmri_filenames, denoise_strategy="simple", motion="basic"
)
time_series = masker.fit_transform(
fmri_filenames, confounds=confounds, sample_mask=sample_mask
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="simple",
reorder=True,
)
# add optional parameter global signal
confounds, sample_mask = load_confounds_strategy(
fmri_filenames,
denoise_strategy="simple",
motion="basic",
global_signal="basic",
)
time_series = masker.fit_transform(
fmri_filenames, confounds=confounds, sample_mask=sample_mask
)
correlation_matrix = correlation_measure.fit_transform([time_series])[0]
np.fill_diagonal(correlation_matrix, 0)
plotting.plot_matrix(
correlation_matrix,
figure=(10, 8),
labels=labels[1:],
vmax=0.8,
vmin=-0.8,
title="simple with global signal",
reorder=True,
)
plotting.show()
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
[Memory]12.4s, 0.2min : Loading _filter_and_extract...
__________________________________filter_and_extract cache loaded - 0.0s, 0.0min
[NiftiLabelsMasker.wrapped] loading data from Nifti1Image(
shape=(91, 109, 91),
affine=array([[ 2., 0., 0., -90.],
[ 0., 2., 0., -126.],
[ 0., 0., 2., -72.],
[ 0., 0., 0., 1.]])
)
[Memory]13.3s, 0.2min : Loading _filter_and_extract...
__________________________________filter_and_extract cache loaded - 0.0s, 0.0min
References¶
Total running time of the script: (0 minutes 15.995 seconds)
Estimated memory usage: 939 MB