diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fa4650f27400172b7d6c2205af0ee1174e9f2783..026bfcc8b41ecad91dc915351d54e19158ce3f74 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,14 @@ +0.12.3 (Friday 8th December 2023) +--------------------------------- + + +* Separate out the core logic of ``evalVectorImage`` so it can be called + programmatically more easily. + + 0.12.2 (Thursday 13th July 2023) -------------------------------- diff --git a/pyfeeds/__init__.py b/pyfeeds/__init__.py index 291bb2d8188a2d207d035cf716107cf0306655ab..ea50d8c9d7f9e6bb2d6b6fd5484ac0e5120dd733 100644 --- a/pyfeeds/__init__.py +++ b/pyfeeds/__init__.py @@ -5,7 +5,7 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -__version__ = '0.12.2' +__version__ = '0.12.3' """The pyfeeds version number. """ @@ -20,5 +20,7 @@ from .evaluate import ( evalImage, evalNumericalText, evalVectorImage, - evalMD5 + evalMD5, + cmpArrays, + cmpVectorArrays ) diff --git a/pyfeeds/evaluate.py b/pyfeeds/evaluate.py index 61650040a1a5bb105174db4fa7f598b5fe8314df..b40f108cbe3108c720f19fb254815721ed6d0866 100644 --- a/pyfeeds/evaluate.py +++ b/pyfeeds/evaluate.py @@ -474,54 +474,11 @@ def evalVectorImage(pyf, testfile, benchmark): both vector images, where each voxel contains a 3-dimensional vector, undirected, and centered at 0. """ - if evalImage(pyf, testfile, benchmark) == 0.0: - return 0 - pion2 = np.pi / 2 - data1 = pyf.imageCache[testfile][ 1].reshape(-1, 3).T - data2 = pyf.imageCache[benchmark][1].reshape(-1, 3).T - # Calculate the length of each vector, - # discard vectors of length 0, and - # normalise each vector to unit length - len1 = np.linalg.norm(data1, axis=0) - len2 = np.linalg.norm(data2, axis=0) - nz1 = len1 > 1e-6 - nz2 = len2 > 1e-6 - nz = nz1 & nz2 + data1 = pyf.imageCache[testfile][ 1] + data2 = pyf.imageCache[benchmark][1] - data1 = data1[:, nz] / len1[nz] - data2 = data2[:, nz] / len2[nz] - len1 = len1[nz] - len2 = len2[nz] - - # Calculate the angle between each vector. - # Vectors are undirected, and centered at - # (0, 0, 0), so the maximum possible angle - # we can have is 90 degrees. - dot = np.sum(data1 * data2, axis=0) - dot = np.clip(dot, -1, 1) - angle = np.arccos(dot) - amask = angle > pion2 - angle[amask] = np.pi - angle[amask] - - # We also compare the length of each - # vector, and the pattern of missing - # voxels (vectors of length 0) - nzcorr = np.abs(np.corrcoef(nz1, nz2)[0, 1]) - lendiff = np.abs(len1 - len2) / np.max((len1, len2), axis=0) - angle = np.abs(angle) / pion2 - - if np.isnan(nzcorr): - nzcorr = 1 - - # All errors are normalised to - # the range (0, 1). We return - # the worst error - nzError = 1 - nzcorr - angleError = angle.mean() - lenError = lendiff.mean() - - return max((nzError, lenError, angleError)) + return cmpVectorArrays(data1, data2) def evalPolarCoordinateImageGroup(pyf, testfiles, benchmarks): @@ -654,3 +611,61 @@ def cmpArrays(arr1, arr2): # The final error is the mean error across all voxels return normdiff.mean() + + +def cmpVectorArrays(arr1, arr2): + """Compare two (X, Y, Z, 3) arrays containing vector fields. Difference + between vectoer lengths and angles are calculated, and the maximum of the + mean length/angle difference is returned (normalised to lie between 0 and + 1). + """ + + if np.isclose(cmpArrays(arr1, arr2), 0): + return 0 + + pion2 = np.pi / 2 + arr1 = arr1.reshape(-1, 3).T + arr2 = arr2.reshape(-1, 3).T + + # Calculate the length of each vector, + # discard vectors of length 0, and + # normalise each vector to unit length + len1 = np.linalg.norm(arr1, axis=0) + len2 = np.linalg.norm(arr2, axis=0) + nz1 = len1 > 1e-6 + nz2 = len2 > 1e-6 + nz = nz1 & nz2 + + arr1 = arr1[:, nz] / len1[nz] + arr2 = arr2[:, nz] / len2[nz] + len1 = len1[nz] + len2 = len2[nz] + + # Calculate the angle between each vector. + # Vectors are undirected, and centered at + # (0, 0, 0), so the maximum possible angle + # we can have is 90 degrees. + dot = np.sum(arr1 * arr2, axis=0) + dot = np.clip(dot, -1, 1) + angle = np.arccos(dot) + amask = angle > pion2 + angle[amask] = np.pi - angle[amask] + + # We also compare the length of each + # vector, and the pattern of missing + # voxels (vectors of length 0) + nzcorr = np.abs(np.corrcoef(nz1, nz2)[0, 1]) + lendiff = np.abs(len1 - len2) / np.max((len1, len2), axis=0) + angle = np.abs(angle) / pion2 + + if np.isnan(nzcorr): + nzcorr = 1 + + # All errors are normalised to + # the range (0, 1). We return + # the worst error + nzError = 1 - nzcorr + angleError = angle.mean() + lenError = lendiff.mean() + + return max((nzError, lenError, angleError)) diff --git a/pyfeeds/tests/__init__.py b/pyfeeds/tests/__init__.py index db6dbb57323ea11effc3d59643923356b184c309..7feda3307a62c5d69ea0a8dbe7b06bfd32cd5bd6 100644 --- a/pyfeeds/tests/__init__.py +++ b/pyfeeds/tests/__init__.py @@ -113,6 +113,13 @@ def maketest(filename, returnCode=0, inputs=None, outputs=None, stdout=None): def makepyfeeds(**kwargs): + + # simplest way of creating a dummy pyfeeds object + if len(kwargs) == 0: + kwargs['command'] = 'compare' + kwargs['inputDir'] = os.getcwd() + kwargs['benchmarkDir'] = os.getcwd() + args = argparse.Namespace(**kwargs) cfg = argparse.Namespace() return main.Pyfeeds(args, cfg) diff --git a/pyfeeds/tests/test_evaluate.py b/pyfeeds/tests/test_evaluate.py index 96ff097c102fe24a0b236fb79fe74c29597ad5c6..c016e787f1be52fc113e7a6d095b90031be2f691 100644 --- a/pyfeeds/tests/test_evaluate.py +++ b/pyfeeds/tests/test_evaluate.py @@ -8,6 +8,9 @@ import os import os.path as op +import numpy as np +import nibabel as nib + from . import tempdir, makepaths, maketest, makepyfeeds, CaptureStdout from pyfeeds import testing, evaluate @@ -57,3 +60,41 @@ def test_evaluateTestAgainstBenchmark(): assert len(lines) == 3 for l in lines[1:]: assert 'PASS' in l + + +def test_evalVectorImage(): + + vecarr1 = -1 + 2 * np.random.random((10, 10, 10, 3)) + vecarr2 = -1 + 2 * np.random.random((10, 10, 10, 3)) + + with tempdir(): + + pyf = makepyfeeds() + fname1 = 'image1.nii.gz' + fname2 = 'image2.nii.gz' + + nib.Nifti1Image(vecarr1, np.eye(4)).to_filename(fname1) + nib.Nifti1Image(vecarr2, np.eye(4)).to_filename(fname2) + + assert evaluate.evalVectorImage(pyf, fname1, fname1) == 0 + assert evaluate.evalVectorImage(pyf, fname2, fname2) == 0 + assert evaluate.evalVectorImage(pyf, fname1, fname2) != 0 + + +def test_evalImage(): + + arr1 = -1 + 2 * np.random.random((10, 10, 10, 10)) + arr2 = -1 + 2 * np.random.random((10, 10, 10, 10)) + + with tempdir(): + + pyf = makepyfeeds() + fname1 = 'image1.nii.gz' + fname2 = 'image2.nii.gz' + + nib.Nifti1Image(arr1, np.eye(4)).to_filename(fname1) + nib.Nifti1Image(arr2, np.eye(4)).to_filename(fname2) + + assert evaluate.evalImage(pyf, fname1, fname1) == 0 + assert evaluate.evalImage(pyf, fname2, fname2) == 0 + assert evaluate.evalImage(pyf, fname1, fname2) != 0