Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BB template & vertex outliers #333

Merged
merged 17 commits into from
Dec 17, 2024
33 changes: 25 additions & 8 deletions hippunfold/config/snakebids.yml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ parse_args:
- synthseg_v0.2
- neonateT1w_v2


--enable-bids-validation:
help: |
Enable validation of BIDS dataset. BIDS validation would be performed
Expand All @@ -294,7 +295,6 @@ parse_args:
version: "1.5.2-pre.2"



# --- surface specific configuration --

autotop_labels:
Expand All @@ -308,6 +308,8 @@ surf_types:
- outer
dentate:
- midthickness
# - inner
# - outer

gifti_metric_types:
hipp:
Expand All @@ -327,7 +329,14 @@ cifti_metric_types:
- gyrification.dscalar
- curvature.dscalar


# params for detecting and removing outlier vertices
outlier_opts:
outlierSmoothDist:
unfoldiso: 15
0p5mm: 7
1mm: 3
2mm: 1
vertexOutlierThreshold: 3



Expand Down Expand Up @@ -373,6 +382,7 @@ template_based_segmentation:
- R
- L


template_files:
CITI168:
T1w: T1w_head_700um.nii.gz
Expand Down Expand Up @@ -427,12 +437,14 @@ template_files:
dseg: tpl-ABAv3_hemi-{hemi}_space-corobl_desc-tissuemanual_dseg.nii.gz
coords: tpl-ABAv3_dir-{dir}_hemi-{hemi}_space-corobl_label-{autotop}_desc-laplace_coords.nii.gz
bigbrain:
T1w: tpl-bbhist_100um_T1w.nii.gz
xfm_corobl: tpl-bbhist_from-native_to-corobl_type-itk_affine.txt
crop_ref: tpl-bbhist_hemi-{hemi}_space-corobl_desc-tissuemanual_40um_dseg.nii.gz
Mask_crop: tpl-bbhist_hemi-{hemi}_space-corobl_desc-tissuemanual_40um_dseg.nii.gz
dseg: tpl-bbhist_hemi-{hemi}_space-corobl_desc-tissuemanual_40um_dseg.nii.gz
coords: tpl-bbhist_dir-{dir}_hemi-{hemi}_space-corobl_label-{autotop}_desc-laplace_coords.nii.gz
T1w: tpl-bigbrain_1mm_merker.nii.gz
T2w: tpl-bigbrain_1mm_merker.nii.gz
xfm_corobl: tpl-bigbrain_from-native_to-corobl_type-itk_affine.txt
crop_ref: tpl-bigbrain_hemi-{hemi}_space-corobl_100um_merker.nii.gz
Mask_crop: tpl-bigbrain_hemi-{hemi}_space-corobl_desc-tissuemanual_100um_dseg.nii.gz
dseg: tpl-bigbrain_hemi-{hemi}_space-corobl_desc-tissuemanual_100um_dseg.nii.gz
coords: tpl-bigbrain_dir-{dir}_hemi-{hemi}_space-corobl_label-{autotop}_desc-laplace_coords.nii.gz


atlas_files:
multihist7:
Expand Down Expand Up @@ -591,6 +603,11 @@ unfold_vol_ref:
- '2.5'
orient: RPI

unfold_crop_epsilon_fractions:
- 0
- 0
- 1 #in unfolded space voxels


# space for uniform unfolded grid:
# currently only used for interpolating hipp subfields on surface
Expand Down
51 changes: 6 additions & 45 deletions hippunfold/workflow/rules/common.smk
Original file line number Diff line number Diff line change
Expand Up @@ -40,55 +40,17 @@ def get_modality_suffix(modality):


def get_final_spec():
if len(config["hemi"]) == 2:
specs = inputs[config["modality"]].expand(
bids(
root=root,
datatype="surf",
den="{density}",
space="{space}",
label="{autotop}",
suffix="surfaces.spec",
**inputs.subj_wildcards,
),
density=config["output_density"],
space=ref_spaces,
autotop=config["autotop_labels"],
allow_missing=True,
)
else:
specs = inputs[config["modality"]].expand(
bids(
root=root,
datatype="surf",
den="{density}",
space="{space}",
hemi="{hemi}",
label="{autotop}",
suffix="surfaces.spec",
**inputs.subj_wildcards,
),
density=config["output_density"],
space=ref_spaces,
hemi=config["hemi"],
autotop=config["autotop_labels"],
allow_missing=True,
)
return specs


def get_final_surf():
gii = []
gii.extend(
specs = []
specs.extend(
inputs[config["modality"]].expand(
bids(
root=root,
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
space="{space}",
hemi="{hemi}",
label="{autotop}",
suffix="surfaces.spec",
**inputs.subj_wildcards,
),
density=config["output_density"],
Expand All @@ -99,16 +61,16 @@ def get_final_surf():
allow_missing=True,
)
)
gii.extend(
specs.extend(
inputs[config["modality"]].expand(
bids(
root=root,
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
space="{space}",
hemi="{hemi}",
label="{autotop}",
suffix="surfaces.spec",
**inputs.subj_wildcards,
),
density=config["output_density"],
Expand All @@ -119,7 +81,7 @@ def get_final_surf():
allow_missing=True,
)
)
return gii
return specs


def get_final_subfields():
Expand Down Expand Up @@ -373,7 +335,6 @@ def get_final_qc():
def get_final_subj_output():
subj_output = []
subj_output.extend(get_final_spec())
subj_output.extend(get_final_surf())
subj_output.extend(get_final_subfields())
subj_output.extend(get_final_coords())
subj_output.extend(get_final_transforms())
Expand Down
30 changes: 20 additions & 10 deletions hippunfold/workflow/rules/gifti.smk
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ rule warp_gii_unfold2corobl1:
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
desc="nonancorrect",
desc="nobadcorrect",
space="corobl",
unfoldreg="none",
hemi="{hemi}",
Expand All @@ -198,21 +198,26 @@ rule warp_gii_unfold2corobl1:
" -surface-secondary-type {params.secondary_type}"


# previous rule seems to be where nan vertices emerge, so we'll correct them here immediately after
rule correct_nan_vertices1:
# previous rule seems to be where bad vertices emerge, so we'll correct them here immediately after
rule correct_bad_vertices1:
input:
gii=bids(
root=work,
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
desc="nonancorrect",
desc="nobadcorrect",
space="corobl",
unfoldreg="none",
hemi="{hemi}",
label="hipp",
**inputs.subj_wildcards
),
params:
dist=lambda wildcards: config["outlier_opts"]["outlierSmoothDist"][
wildcards.density
],
threshold=config["outlier_opts"]["vertexOutlierThreshold"],
output:
gii=bids(
root=work,
Expand All @@ -230,7 +235,7 @@ rule correct_nan_vertices1:
container:
config["singularity"]["autotop"]
script:
"../scripts/fillnanvertices.py"
"../scripts/fillbadvertices.py"


# morphological features, calculated in native space:
Expand Down Expand Up @@ -730,7 +735,7 @@ rule warp_gii_unfold2corobl2:
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
desc="nonancorrect",
desc="nobadcorrect",
space="corobl",
hemi="{hemi}",
label="{autotop}",
Expand All @@ -746,20 +751,25 @@ rule warp_gii_unfold2corobl2:
" -surface-secondary-type {params.secondary_type}"


# previous rule seems to be where nan vertices emerge, so we'll correct them here immediately after
rule correct_nan_vertices2:
# previous rule seems to be where bad vertices emerge, so we'll correct them here immediately after
rule correct_bad_vertices2:
input:
gii=bids(
root=work,
datatype="surf",
den="{density}",
suffix="{surfname}.surf.gii",
desc="nonancorrect",
desc="nobadcorrect",
space="corobl",
hemi="{hemi}",
label="{autotop}",
**inputs.subj_wildcards
),
params:
dist=lambda wildcards: config["outlier_opts"]["outlierSmoothDist"][
wildcards.density
],
threshold=config["outlier_opts"]["vertexOutlierThreshold"],
output:
gii=bids(
root=work,
Expand All @@ -776,7 +786,7 @@ rule correct_nan_vertices2:
container:
config["singularity"]["autotop"]
script:
"../scripts/fillnanvertices.py"
"../scripts/fillbadvertices.py"


# needed for if native_modality is corobl
Expand Down
41 changes: 24 additions & 17 deletions hippunfold/workflow/scripts/create_warps.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,30 @@ def summary(name, array):
# we have points defined by coord_flat_{ap,pd,io}, and corresponding value as native_coords_phys[:,i]
# and we want to interpolate on a grid in the unfolded space

# unnormalize and add a bit of noise so points don't ever perfectly overlap
coord_flat_ap_unnorm = coord_flat_ap * unfold_dims[0]
coord_flat_pd_unnorm = coord_flat_pd * unfold_dims[1]
coord_flat_io_unnorm = coord_flat_io * unfold_dims[2]

# get unfolded grid

points = np.stack(
[
coord_flat_ap * unfold_dims[0]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
coord_flat_pd * unfold_dims[1]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
coord_flat_io * unfold_dims[2]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
],
axis=1,
# add some noise to avoid perfectly overlapping datapoints!
points = (
coord_flat_ap * unfold_dims[0]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
coord_flat_pd * unfold_dims[1]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
coord_flat_io * unfold_dims[2]
+ (np.random.rand(coord_flat_ap.shape[0]) - 0.5) * 1e-6,
)

# get unfolded grid (from 0 to 1, not world coords), using meshgrid:
# note: indexing='ij' to swap the ordering of x and y
epsilon = snakemake.params.epsilon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this PR is merged already, but easier to comment here than anew -- these changes break the rule because epsilon is not a parameter in the rule. Also, unfold_gx/unfold_gy/unfold_gz get create here, but don't actually get used .. so it seems we can just revert the create_warps changes to fix the problem, unless you were trying to do something else here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, good point. Looks like we were never actually using the epsilon that we'd previously defined? I'll make a small PR to patch this & 1 other small bug i found

Also yeah points and unfold_gx somehow became redundant. We can cut the latter.

(unfold_gx, unfold_gy, unfold_gz) = np.meshgrid(
np.linspace(
0 + float(epsilon[0]), unfold_dims[0] - float(epsilon[0]), unfold_dims[0]
),
np.linspace(
0 + float(epsilon[1]), unfold_dims[1] - float(epsilon[1]), unfold_dims[1]
),
np.linspace(
0 + float(epsilon[2]), unfold_dims[2] - float(epsilon[2]), unfold_dims[2]
),
indexing="ij",
)
summary("points", points)

Expand Down
59 changes: 59 additions & 0 deletions hippunfold/workflow/scripts/fillbadvertices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import nibabel as nib
import numpy as np
from scipy.stats import zscore
import copy

SDthreshold = snakemake.params.threshold
iters = snakemake.params.dist

gii = nib.load(snakemake.input.gii)
varr = gii.get_arrays_from_intent("NIFTI_INTENT_POINTSET")[0]
V = varr.data
farr = gii.get_arrays_from_intent("NIFTI_INTENT_TRIANGLE")[0]
F = farr.data


# find local outliers by smoothing and then substracting from original
# https://github.com/MICA-MNI/hippomaps/blob/master/hippomaps/utils.py
def avg_neighbours(F, cdat, n):
frows = np.where(F == n)[0]
v = np.unique(F[frows, :])
cdat = np.reshape(cdat, (len(cdat), -1))
out = np.nanmean(cdat[v, :], 0)
return out


def surfdat_smooth(F, cdata, iters=1):
sz = cdata.shape
cdata = cdata.reshape(cdata.shape[0], -1)
cdata_smooth = copy.deepcopy(cdata)
for i in range(iters):
for n in range(len(cdata)):
cdata_smooth[n, :] = avg_neighbours(F, cdata, n)
cdata = copy.deepcopy(cdata_smooth)
return cdata_smooth.reshape(sz)


Vsmooth = surfdat_smooth(F, V, iters=iters)
Vdiffs = V - Vsmooth
Vdists = np.sqrt((Vdiffs[:, 0]) ** 2 + (Vdiffs[:, 1]) ** 2 + (Vdiffs[:, 2]) ** 2)
Vzscored = zscore(Vdists)
outliers = (Vzscored > SDthreshold) | (Vzscored < -SDthreshold)
V[outliers, :] = np.nan


# most nans should be just isolated points, but in case there is an island of nans this will erode it, replacing with decent (but not perfect) guesses of where vertices should be
while np.isnan(np.sum(V)):
# index of vertices containing nan
i = np.where(np.isnan(V))
ii = np.unique(i[0])
# replace with the nanmean of neighbouring vertices
newV = V
for n in ii:
f = np.where(F == n)
v = F[f[0]]
vv = np.unique(v)
newV[n, :] = np.nanmean(V[vv, :], 0)
V = newV

nib.save(gii, snakemake.output.gii)
Loading
Loading