import os
import warnings
from pandas import concat, DataFrame
import numpy as np
from nilearn import image
from itertools import combinations
from nilearn.maskers import NiftiMasker
from scipy.stats import spearmanr
from pyrelimri.tetrachoric_correlation import tetrachoric_corr as tet_corr
[docs]
def image_similarity(imgfile1: str, imgfile2: str,
mask: str = None, thresh: float = None,
similarity_type: str = 'dice') -> float:
"""
Calculate the similarity between two 3D images using a specified similarity metric.
The function computes the ratio of intersecting and union voxels based on the provided threshold and similarity type
The result is a similarity coefficient indicating the overlap between the two images.
Parameters
----------
imgfile1 : str
Path to the first NIfTI image file.
imgfile2 : str
Path to the second NIfTI image file.
mask : str, optional
Path to a binarized mask image for voxel selection. Default is None.
thresh : float, optional
Threshold value for voxel selection. Positive values retain voxels greater than the threshold,
and negative values retain voxels less than the threshold. Default is None.
similarity_type : str, optional
Similarity calculation method. Options are 'dice', 'jaccard', 'tetrachoric', or 'spearman'. Default is 'dice'.
Returns
-------
float
Similarity coefficient based on the selected method.
Example
-------
# Example usage of image_similarity
similarity = image_similarity(imgfile1='./img1.nii', imgfile2='./img2.nii',
mask='./mask.nii', thresh=0.5, similarity_type='dice')
"""
assert similarity_type.casefold() in ['dice', 'jaccard',
'tetrachoric', 'spearman'], 'similarity_type must be ' \
'"Dice", "Jaccard", "Tetrachoric" or ' \
'"Spearman". Provided: {}"'.format(similarity_type)
# load list of images
imagefiles = [imgfile1, imgfile2]
img = [image.load_img(i) for i in imagefiles]
assert img[0].shape == img[1].shape, 'images of different shape, ' \
'image 1 {} and image 2 {}'.format(img[0].shape, img[1].shape)
# mask image
masker = NiftiMasker(mask_img=mask)
imgdata = masker.fit_transform(img)
# threshold image, compatible for positive & negative values
# (i.e., some may want similarity in (de)activation)
if thresh is not None and similarity_type.casefold() != 'spearman':
if thresh > 0:
imgdata = imgdata > thresh
elif thresh < 0:
imgdata = imgdata < thresh
# Imgs must be 1/0 binary images, ensure correct format for each similarity type
if similarity_type.casefold() in ['dice', 'jaccard', 'tetrachoric']:
# Binarize input for these metrics
imgdata = imgdata.astype(bool)
if similarity_type.casefold() in ['dice', 'jaccard']:
# Intersection of images
intersect = np.logical_and(imgdata[0, :], imgdata[1, :])
if similarity_type.casefold() == 'dice':
# Dice coefficient: 2 * |A ∩ B| / (|A| + |B|)
sum_a_b = imgdata[0, :].sum() + imgdata[1, :].sum()
coeff = (2.0 * intersect.sum()) / (float(sum_a_b) + np.finfo(float).eps)
else:
# Jaccard coefficient: |A ∩ B| / |A ∪ B|
union = np.logical_or(imgdata[0, :], imgdata[1, :])
coeff = intersect.sum() / (float(union.sum()) + np.finfo(float).eps)
elif similarity_type.casefold() == 'tetrachoric':
warnings.filterwarnings('ignore')
coeff = tet_corr(vec1=imgdata[0, :], vec2=imgdata[1, :])
else:
if thresh is not None:
raise ValueError(f"Spearman rank should be for unthresholded images."
f"/n Threshold is set to: {thresh}./n Advise: 'None'.")
else:
coeff = spearmanr(a=imgdata[0, :], b=imgdata[1, :])[0]
return coeff
[docs]
def pairwise_similarity(nii_filelist: list, mask: str = None,
thresh: float = None, similarity_type: str = 'Dice') -> DataFrame:
"""
Calculate pairwise similarity between a list of NIfTI images using a specified similarity metric.
The function generates all possible combinations of the provided NIfTI images and computes the similarity
coefficient for each pair.
Parameters
----------
nii_filelist : list
List of paths to NIfTI image files.
mask : str, optional
Path to the brain mask image for voxel selection. Default is None.
thresh : float, optional
Threshold value for voxel selection. Positive values retain voxels greater than the threshold,
and negative values retain voxels less than the threshold. Default is None.
similarity_type : str, optional
Similarity calculation method. Options are 'dice', 'jaccard', 'tetrachoric', or 'spearman'. Default is 'dice'.
Returns
-------
DataFrame
A pandas DataFrame containing the similarity coefficients and corresponding image labels for each pairwise comparison.
Example
-------
# Example usage of pairwise_similarity
similarity_df = pairwise_similarity(['./img1.nii', './img2.nii', './img3.nii'],
mask='mask.nii', thresh=0.5, similarity_type='dice')
"""
# test whether function type is of 'Dice' or 'Jaccard', case insensitive
assert similarity_type.casefold() in ['dice', 'jaccard',
'tetrachoric', 'spearman'], 'similarity_type must be ' \
'"Dice", "Jaccard", "Tetrachoric" or ' \
'"Spearman". Provided: {}"'.format(similarity_type)
var_pairs = list(combinations(nii_filelist, 2))
coef_df = DataFrame(columns=['similar_coef', 'image_labels'])
for img_comb in var_pairs:
# select basename of file name(s)
path = [os.path.basename(i) for i in img_comb]
# calculate simiarlity
val = image_similarity(imgfile1=img_comb[0], imgfile2=img_comb[1], mask=mask,
thresh=thresh, similarity_type=similarity_type)
# for each pairwise come, save value + label to pandas df
similarity_data = DataFrame(np.column_stack((val, " ~ ".join([path[0], path[1]]))),
columns=['similar_coef', 'image_labels'])
coef_df = concat([coef_df, similarity_data], axis=0, ignore_index=True)
return coef_df