Skip to content

Commit 329134e

Browse files
committed
simplify chunked IO, fix vertex/voxel reading uniqueness checks, check for multiple label dims (warn on reading, error on writing)
1 parent cd5294c commit 329134e

File tree

4 files changed

+63
-74
lines changed

4 files changed

+63
-74
lines changed

cifti_read.m

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -93,51 +93,38 @@
9393
if(fseek(fid, hdr.vox_offset, 'bof') ~= 0)
9494
error(['failed to seek to start of data in file ' filename]);
9595
end
96-
%always output as float32, maybe add a feature later
96+
%always convert to float32, maybe add a feature later
9797
%use 'cdata' to be compatible with old ciftiopen
9898
max_elems = 128 * 1024 * 1024 / 4; %when reading as float32, use only 128MiB extra memory when reading (or the size of a row, if that manages to be larger)
99-
if prod(hdr.dim(6:(hdr.dim(1) + 1))) <= max_elems
99+
if prod(dims_c) <= max_elems
100100
%file is small, use the simple code to read it all in one call
101101
%permute to match ciftiopen: cifti "rows" matching matlab rows
102102
%hack: 3:2 produces empty array, 3:3 produces [3]
103103
outstruct.cdata = permute(do_nifti_scaling(fread_excepting(fid, hdr.dim(6:(hdr.dim(1) + 1)), [intype '=>float32'], filename), hdr), [2 1 3:(hdr.dim(1) - 4)]);
104104
else
105-
outstruct.cdata = myzeros(dims_m); %avoid inconsistent 1-dimension handling, allow column vector, default to float32
106-
max_rows = max(1, min(hdr.dim(7), floor(max_elems / hdr.dim(6))));
107-
switch hdr.dim(1) %dim[0]
108-
case 6
109-
%even out the passes to use about the same memory
110-
num_passes = ceil(hdr.dim(7) / max_rows);
111-
chunk_rows = ceil(hdr.dim(7) / num_passes);
112-
for i = 1:chunk_rows:hdr.dim(7)
113-
outstruct.cdata(i:min(hdr.dim(7), i + chunk_rows - 1), :) = do_nifti_scaling(fread_excepting(fid, [hdr.dim(6), min(chunk_rows, hdr.dim(7) - i + 1)], [intype '=>float32']), hdr)';
105+
%matlab indexing modes can't handle swapping first two dims while doing "some full planes plus a partial plane" per fread()
106+
%reshape at the end would hit double the memory usage, and we want to avoid that
107+
%so to avoid slow row-at-a-time loops, two cases: less than a plane, and multiple full planes
108+
%with implicit third index and implicit flattening on final specified dimension, we can do these generically in a way that should work even for 4D+ (if it is ever supported)
109+
outstruct.cdata = myzeros(dims_m);
110+
max_rows = max(1, floor(max_elems / dims_c(1)));
111+
if max_rows < dims_c(2) %less than a plane at a time, don't read cross-plane
112+
num_passes = ceil(dims_c(2) / max_rows); %even out the passes
113+
chunk_rows = ceil(prod(dims_c(2:end)) / num_passes);
114+
total_planes = prod(dims_c(3:end));
115+
for plane = 1:total_planes
116+
for chunkstart = 1:chunk_rows:dims_c(2)
117+
outstruct.cdata(chunkstart:min(dims_c(2), chunkstart + chunk_rows - 1), :, plane) = do_nifti_scaling(fread_excepting(fid, [dims_c(1), min(chunk_rows, dims_c(2) - chunkstart + 1)], [intype '=>float32']), hdr)';
114118
end
115-
case 7
116-
%3D - this is all untested
117-
if max_rows < hdr.dim(7)
118-
%keep it simple, chunk each plane independently
119-
num_passes = ceil(hdr.dim(7) / max_rows);
120-
chunk_rows = ceil(hdr.dim(7) / num_passes);
121-
for j = 1:hdr.dim(8)
122-
for i = 1:chunk_rows:hdr.dim(7)
123-
outstruct.cdata(i:min(hdr.dim(7), i + chunk_rows - 1), :, j) = do_nifti_scaling(fread_excepting(fid, [hdr.dim(6), min(chunk_rows, hdr.dim(7) - i + 1)], [intype '=>float32']), hdr)';
124-
end
125-
end
126-
else
127-
%read multiple full planes per call
128-
plane_elems = hdr.dim(7) * hdr.dim(6);
129-
max_planes = max(1, min(hdr.dim(8), floor(max_elems / plane_elems)));
130-
num_passes = ceil(hdr.dim(8) / max_planes);
131-
chunk_planes = ceil(hdr.dim(8) / num_passes);
132-
for j = 1:chunk_planes:hdr.dim(8)
133-
outstruct.cdata(:, :, j:min(hdr.dim(8), j + chunk_planes - 1)) = permute(do_nifti_scaling(fread_excepting(fid, [hdr.dim(6:7), min(chunk_planes, hdr.dim(8) - j + 1)], [intype '=>float32']), hdr), [2 1 3]);
134-
end
135-
end
136-
otherwise
137-
%4D and beyond is not in the cifti-2 standard and is treated as an error in sanity_check_cdata
138-
%but, if it ever is supported, warn and read it the memory-intensive way anyway
139-
warning('cifti reading for 4 or more dimensions currently peaks at double the memory');
140-
outstruct.cdata = permute(do_nifti_scaling(fread_excepting(fid, hdr.dim(6:(hdr.dim(1) + 1)), [intype '=>float32'], filename), hdr), [2 1 3:(hdr.dim(1) - 4)]);
119+
end
120+
else
121+
max_planes = max(1, floor(max_rows / dims_c(2))); %just in case the division does something dumb
122+
total_planes = prod(dims_c(3:end));
123+
num_passes = ceil(total_planes / max_planes);
124+
chunk_planes = ceil(total_planes / num_passes);
125+
for chunkstart = 1:chunk_planes:total_planes
126+
outstruct.cdata(:, :, chunkstart:min(total_planes, chunkstart + chunk_planes - 1)) = permute(do_nifti_scaling(fread_excepting(fid, [dims_c(1:2), min(chunk_planes, total_planes - chunkstart + 1)], [intype '=>float32']), hdr), [2 1 3]);
127+
end
141128
end
142129
end
143130
end
@@ -158,3 +145,4 @@
158145
outzeros = zeros(dimarray(:)', 'single');
159146
end
160147
end
148+

cifti_write.m

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -70,44 +70,28 @@ function cifti_write(cifti, filename, varargin)
7070
%FIXME: if we allow setting nifti scale/intercept, that needs to be added to this code
7171
max_elems = 128 * 1024 * 1024 / 4; %assuming float32, use only 128MiB extra memory when writing (or the size of a row, if that manages to be larger)
7272
if numel(cifti.cdata) <= max_elems
73-
%file is small, use simple 'permute' writing code
73+
%file is small, use simple whole-array writing code
7474
fwrite_excepting(fid, permute(cifti.cdata, [2 1 3:length(size(cifti.cdata))]), 'float32');
7575
else
76-
max_rows = max(1, min(size(cifti.cdata, 1), floor(max_elems / size(cifti.cdata, 2))));
77-
switch length(size(cifti.cdata))
78-
case 2
79-
%even out the passes to use about the same memory
80-
num_passes = ceil(size(cifti.cdata, 1) / max_rows);
81-
chunk_rows = ceil(size(cifti.cdata, 1) / num_passes);
82-
for i = 1:chunk_rows:size(cifti.cdata, 1)
83-
fwrite_excepting(fid, cifti.cdata(i:min(size(cifti.cdata, 1), i + chunk_rows - 1), :)', 'float32');
76+
max_rows = max(1, floor(max_elems / dims_c(1)));
77+
if max_rows < dims_c(2) %less than a plane at a time, don't write cross-plane
78+
num_passes = ceil(dims_c(2) / max_rows); %even out the passes
79+
chunk_rows = ceil(prod(dims_c(2:end)) / num_passes);
80+
total_planes = prod(dims_c(3:end));
81+
for plane = 1:total_planes
82+
for chunkstart = 1:chunk_rows:dims_c(2)
83+
%since it is part of one plane, technically matlab will return a 2D matrix, and we could just use transpose...
84+
fwrite_excepting(fid, permute(cifti.cdata(chunkstart:min(dims_c(2), chunkstart + chunk_rows - 1), :, plane), [2 1 3]), 'float32');
8485
end
85-
case 3
86-
%3D - this is all untested
87-
if max_rows < size(cifti.cdata, 1)
88-
%keep it simple, chunk each plane independently
89-
num_passes = ceil(size(cifti.cdata, 1) / max_rows);
90-
chunk_rows = ceil(size(cifti.cdata, 1) / num_passes);
91-
for j = 1:size(cifti.cdata, 3)
92-
for i = 1:chunk_rows:size(cifti.cdata, 1)
93-
fwrite_excepting(fid, cifti.cdata(i:min(size(cifti.cdata, 1), i + chunk_rows - 1), :, j)', 'float32');
94-
end
95-
end
96-
else
97-
%write multiple full planes per call
98-
plane_elems = size(cifti.cdata, 1) * size(cifti.cdata, 2);
99-
max_planes = max(1, min(size(cifti.cdata, 3), floor(max_elems / plane_elems)));
100-
num_passes = ceil(size(cifti.cdata, 3) / max_planes);
101-
chunk_planes = ceil(size(cifti.cdata, 3) / num_passes);
102-
for j = 1:chunk_planes:size(cifti.cdata, 3)
103-
fwrite_excepting(fid, permute(cifti.cdata(:, :, j:min(size(cifti.cdata, 3), j + chunk_planes - 1)), [2 1 3]), 'float32');
104-
end
105-
end
106-
otherwise
107-
%4D and beyond is not in the cifti-2 standard and is treated as an error in sanity_check_cdata
108-
%but, if it ever is supported, warn and write it the memory-intensive way anyway
109-
warning('cifti writing for 4 or more dimensions currently peaks at double the memory');
110-
fwrite_excepting(fid, permute(cifti.cdata, [2 1 3:length(size(cifti.cdata))]), 'float32');
86+
end
87+
else
88+
max_planes = max(1, floor(max_rows / dims_c(2))); %just in case the division does something dumb
89+
total_planes = prod(dims_c(3:end)); %flatten all dimensions 3+
90+
num_passes = ceil(total_planes / max_planes);
91+
chunk_planes = ceil(total_planes / num_passes);
92+
for chunkstart = 1:chunk_planes:total_planes
93+
fwrite_excepting(fid, permute(cifti.cdata(:, :, chunkstart:min(total_planes, chunkstart + chunk_planes - 1)), [2 1 3]), 'float32');
94+
end
11195
end
11296
end
11397
end

private/cifti_parse_xml.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@
2424
outstruct.metadata = cifti_parse_metadata(tree, find(tree, '/CIFTI/Matrix/MetaData'), filename);
2525
map_uids = find(tree, '/CIFTI/Matrix/MatrixIndicesMap');
2626
outstruct.diminfo = {};
27+
haveLabels = false;
2728
for map_uid = map_uids
2829
[map, applies] = cifti_parse_map(tree, map_uid, filename);
30+
if strcmp(map.type, 'labels')
31+
if haveLabels || length(applies) > 1
32+
mywarn('more than one labels dimension', filename);
33+
end
34+
haveLabels = true;
35+
end
2936
appliesmod = applies;
3037
appliesmod(applies < 3) = 3 - applies(applies < 3); %NOTE: swap 1 and 2 to match ciftiopen cdata convention...
3138
outstruct.diminfo(appliesmod) = {map}; %lhs {} doesn't support multi-assignment, but () on cell does...
@@ -182,6 +189,9 @@ function mywarn(msg, filename)
182189
if length(model.vertlist) ~= model.count
183190
myerror('VertexIndices does not match IndexCount', filename);
184191
end
192+
if length(unique(model.vertlist)) ~= length(model.vertlist)
193+
myerror('brain models have repeated or overlapping voxels', filename);
194+
end
185195
case 'vox'
186196
if isfield(model, 'numvert')
187197
mywarn('SurfaceNumberOfVertices attribute present in Voxel type BrainModel', filename);
@@ -234,7 +244,7 @@ function mywarn(msg, filename)
234244
if length(unique(allstructs)) ~= length(allstructs)
235245
myerror('brain models specify a structure more than once', filename);
236246
end
237-
if size(unique(allvox, 'rows'), 1) ~= size(allvox, 1)
247+
if size(unique(allvox', 'rows'), 1) ~= size(allvox, 2)
238248
myerror('brain models have repeated or overlapping voxels', filename);
239249
end
240250
end
@@ -436,7 +446,7 @@ function mywarn(msg, filename)
436446
map.parcels(i) = thisparcel;
437447
end
438448
map.length = length(map.parcels);
439-
allvox = horzcat(map.parcels.voxlist);
449+
allvox = [map.parcels.voxlist];
440450
if size(unique(allvox', 'rows'), 1) ~= size(allvox, 2)
441451
myerror('parcels have repeated or overlapping voxels', filename);
442452
end

private/cifti_write_xml.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,15 @@
3131
end
3232

3333
function tree = cifti_write_maps(cifti, tree, matrix_uid)
34+
haveLabels = false;
3435
mapused = false(length(cifti.diminfo), 1);
3536
for i = 1:length(cifti.diminfo)
37+
if strcmp(cifti.diminfo{i}.type, 'labels')
38+
if haveLabels
39+
error('cifti files are not allowed to have more than one labels dimension, change one of them to scalar');
40+
end
41+
haveLabels = true;
42+
end
3643
if mapused(i)
3744
continue;
3845
end

0 commit comments

Comments
 (0)