From 3b44c7ec7069f0953c3356304112cadbe55d36f1 Mon Sep 17 00:00:00 2001 From: daubners Date: Wed, 15 Jan 2025 16:57:26 +0100 Subject: [PATCH 1/8] refactor volume fraction computation --- taufactor/metrics.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/taufactor/metrics.py b/taufactor/metrics.py index b84667f..2a5735a 100644 --- a/taufactor/metrics.py +++ b/taufactor/metrics.py @@ -2,7 +2,7 @@ import torch import torch.nn.functional as F -def volume_fraction(img, phases={}): +def volume_fraction(img, phases={}, device=torch.device('cuda')): """ Calculates volume fractions of phases in an image :param img: segmented input image with n phases @@ -11,15 +11,17 @@ def volume_fraction(img, phases={}): """ if type(img) is not type(torch.tensor(1)): - img = torch.tensor(img) + img = torch.tensor(img, device=device) if phases=={}: - phases = torch.unique(img) - vf_out = [] - for p in phases: - vf_out.append((img==p).to(torch.float).mean().item()) - if len(vf_out)==1: - vf_out=vf_out[0] + volume = torch.numel(img) + labels, counts = torch.unique(img, return_counts=True) + labels = labels.int() + counts = counts.float() + counts /= volume + vf_out = {} + for i, label in enumerate(labels): + vf_out[str(label.item())] = counts[i].item() else: vf_out={} for p in phases: From 1469ab7bc79c18596686e76e1ef059d056e1d01f Mon Sep 17 00:00:00 2001 From: daubners Date: Wed, 15 Jan 2025 17:29:21 +0100 Subject: [PATCH 2/8] add through feature computation --- taufactor/metrics.py | 121 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/taufactor/metrics.py b/taufactor/metrics.py index 2a5735a..58e867a 100644 --- a/taufactor/metrics.py +++ b/taufactor/metrics.py @@ -2,6 +2,8 @@ import torch import torch.nn.functional as F +from scipy.ndimage import label, generate_binary_structure + def volume_fraction(img, phases={}, device=torch.device('cuda')): """ Calculates volume fractions of phases in an image @@ -144,3 +146,122 @@ def triple_phase_boundary(img): tpb += torch.sum(tpb_map) return tpb/total_edges + +def label_periodic(field, grayscale_value, neighbour_structure, periodic, debug=False): + # Initialize phi field whith enlarged dimensions in periodic directions. Boundary values of + # array are copied into ghost cells which are necessary to impose boundary conditions. + padx = int(periodic[0]) + pady = int(periodic[1]) + padz = int(periodic[2]) + mask = np.pad(field, ((padx, padx), (pady, pady), (padz, padz)), mode='wrap') + labeled_mask, num_labels = label(mask==grayscale_value, structure=neighbour_structure) + count = 1 + for k in range(100): + # Find indices where labels are different at the boundaries and create swaplist + swap_list = np.zeros((1,2)) + if periodic[0]: + # right x + indices = np.where((labeled_mask[0,:,:]!=labeled_mask[-2,:,:]) & (labeled_mask[0,:,:]!=0) & (labeled_mask[-2,:,:]!=0)) + additional_swaps = np.column_stack((labeled_mask[0,:,:][indices], labeled_mask[-2,:,:][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + # left x + indices = np.where((labeled_mask[1,:,:]!=labeled_mask[-1,:,:]) & (labeled_mask[1,:,:]!=0) & (labeled_mask[-1,:,:]!=0)) + additional_swaps = np.column_stack((labeled_mask[1,:,:][indices], labeled_mask[-1,:,:][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + if periodic[1]: + # top y + indices = np.where((labeled_mask[:,0,:]!=labeled_mask[:,-2,:]) & (labeled_mask[:,0,:]!=0) & (labeled_mask[:,-2,:]!=0)) + additional_swaps = np.column_stack((labeled_mask[:,0,:][indices], labeled_mask[:,-2,:][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + # bottom y + indices = np.where((labeled_mask[:,1,:]!=labeled_mask[:,-1,:]) & (labeled_mask[:,1,:]!=0) & (labeled_mask[:,-1,:]!=0)) + additional_swaps = np.column_stack((labeled_mask[:,1,:][indices], labeled_mask[:,-1,:][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + if periodic[2]: + # front z + indices = np.where((labeled_mask[:,:,0]!=labeled_mask[:,:,-2]) & (labeled_mask[:,:,0]!=0) & (labeled_mask[:,:,-2]!=0)) + additional_swaps = np.column_stack((labeled_mask[:,:,0][indices], labeled_mask[:,:,-2][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + # back z + indices = np.where((labeled_mask[:,:,1]!=labeled_mask[:,:,-1]) & (labeled_mask[:,:,1]!=0) & (labeled_mask[:,:,-1]!=0)) + additional_swaps = np.column_stack((labeled_mask[:,:,1][indices], labeled_mask[:,:,-1][indices])) + swap_list = np.row_stack((swap_list,additional_swaps)) + swap_list = swap_list[1:,:] + # Sort swap list columns to ensure consistent ordering + swap_list = np.sort(swap_list, axis=1) + + # Remove duplicates from swap_list + swap_list = np.unique(swap_list, axis=0) + # print(f"swap_list contains {swap_list.shape[0]} elements.") + if (swap_list.shape[0]==0): + break + for i in range(swap_list.shape[0]): + index = swap_list.shape[0] - i -1 + labeled_mask[labeled_mask == swap_list[index][1]] = swap_list[index][0] + count += 1 + if(debug): + print(f"Did {count} iterations for periodic labelling.") + dim = labeled_mask.shape + return labeled_mask[padx:dim[0]-padx,pady:dim[1]-pady,padz:dim[2]-padz], np.unique(labeled_mask).size-1 + +def find_spanning_labels(labelled_array, axis): + """ + Find labels that appear on both ends along given axis + + Returns: + set: Labels that appear on both ends of the first axis. + """ + if axis == "x": + front = np.s_[0,:,:] + end = np.s_[-1,:,:] + elif axis == "y": + front = np.s_[:,0,:] + end = np.s_[:,-1,:] + elif axis == "z": + front = np.s_[:,:,0] + end = np.s_[:,:,-1] + else: + raise ValueError("Axis should be x, y or z!") + + first_slice_labels = np.unique(labelled_array[front]) + last_slice_labels = np.unique(labelled_array[end]) + spanning_labels = set(first_slice_labels) & set(last_slice_labels) + spanning_labels.discard(0) # Remove the background label if it exists + return spanning_labels + +def extract_through_feature(array, grayscale_value, axis, periodic=[False,False,False], connectivity=1, debug=False): + if array.ndim != 3: + print(f"Expected a 3D array, but got an array with {array.ndim} dimension(s).") + return None + + # Compute volume fraction of given grayscale value + vol_phase = volume_fraction(array, phases={'1': grayscale_value})['1'] + + # Define a list of connectivities to loop over + connectivities_to_loop_over = [connectivity] if connectivity else range(1, 4) + through_feature = [] + through_feature_fraction = np.zeros(len(connectivities_to_loop_over)) + + # Compute the largest interconnected features depending on given connectivity + count = 0 + for conn in connectivities_to_loop_over: + # connectivity 1 = cells connected by sides (6 neighbours) + # connectivity 2 = cells connected by sides & edges (14 neighbours) + # connectivity 3 = cells connected by sides & edges & corners (26 neighbours) + neighbour_structure = generate_binary_structure(3,conn) + # Label connected components in the mask with given neighbour structure + if any(periodic): + labeled_mask, num_labels = label_periodic(array, grayscale_value, neighbour_structure, periodic, debug=debug) + else: + labeled_mask, num_labels = label(array == grayscale_value, structure=neighbour_structure) + if(debug): + print(f"Found {num_labels} labelled regions. For connectivity {conn} and grayscale {grayscale_value}.") + + through_labels = find_spanning_labels(labeled_mask,axis) + spanning_network = np.isin(labeled_mask, list(through_labels)) + + through_feature.append(spanning_network) + through_feature_fraction[count] = volume_fraction(spanning_network, phases={'1': 1})['1']/vol_phase + count += 1 + + return through_feature, through_feature_fraction \ No newline at end of file From c53e81846f8b9914028cc5f512d500e34291ec05 Mon Sep 17 00:00:00 2001 From: daubners Date: Wed, 15 Jan 2025 17:30:09 +0100 Subject: [PATCH 3/8] check inf for blocked pores --- taufactor/taufactor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/taufactor/taufactor.py b/taufactor/taufactor.py index a11322f..85fc192 100644 --- a/taufactor/taufactor.py +++ b/taufactor/taufactor.py @@ -7,6 +7,7 @@ except ImportError: raise ImportError("Pytorch is required to use this package. Please install pytorch and try again. More information about TauFactor's requirements can be found at https://taufactor.readthedocs.io/en/latest/") import warnings +from .metrics import extract_through_feature class BaseSolver: def __init__(self, img, bc=(-0.5, 0.5), device=torch.device('cuda')): @@ -89,7 +90,9 @@ def check_vertical_flux(self, conv_crit): fl = torch.sum(vert_flux, (0, 2, 3)) err = (fl.max() - fl.min())/(fl.max()) if fl.min() == 0: - return 'zero_flux', torch.mean(fl), err + _ , frac = extract_through_feature(self.cpu_img[0], 1, 'x') + if frac == 0: + return 'zero_flux', torch.mean(fl), err if err < conv_crit or torch.isnan(err).item(): return True, torch.mean(fl), err return False, torch.mean(fl), err From a675fe2be0a03f4ff874c65a51ce7d8e3e139d3f Mon Sep 17 00:00:00 2001 From: daubners Date: Wed, 15 Jan 2025 17:45:54 +0100 Subject: [PATCH 4/8] update requirements --- requirements_dev.txt | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 8c71d95..b0a47ee 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -13,4 +13,5 @@ matplotlib>3.6 pytest-runner==5.2 numpy>=1.24.2 tifffile==2023.2.3 -myst-parser==0.18.1 \ No newline at end of file +myst-parser==0.18.1 +scikit-image>=0.20.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 24c6814..3a78823 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open('HISTORY.md') as history_file: history = history_file.read() -requirements = ['Click>=7.0', 'numpy>=1.0', 'matplotlib>=3.4', 'tifffile>=2023.2.3'] +requirements = ['Click>=7.0', 'numpy>=1.0', 'matplotlib>=3.4', 'tifffile>=2023.2.3', 'scikit-image>=0.20.0'] setup_requirements = ['pytest-runner', ] From 1a522bd39143cfc3ad024c16e8b1efbd5af304ae Mon Sep 17 00:00:00 2001 From: daubners Date: Wed, 15 Jan 2025 17:53:26 +0100 Subject: [PATCH 5/8] add scipy requirements --- requirements_dev.txt | 3 ++- setup.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index b0a47ee..54ed682 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -14,4 +14,5 @@ pytest-runner==5.2 numpy>=1.24.2 tifffile==2023.2.3 myst-parser==0.18.1 -scikit-image>=0.20.0 \ No newline at end of file +scikit-image>=0.20.0 +scipy>=1.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 3a78823..cc136eb 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,14 @@ with open('HISTORY.md') as history_file: history = history_file.read() -requirements = ['Click>=7.0', 'numpy>=1.0', 'matplotlib>=3.4', 'tifffile>=2023.2.3', 'scikit-image>=0.20.0'] +requirements = [ + 'Click>=7.0', + 'numpy>=1.0', + 'matplotlib>=3.4', + 'tifffile>=2023.2.3', + 'scikit-image>=0.20.0', + 'scipy>=1.3' +] setup_requirements = ['pytest-runner', ] From 2040e71d112cf6f1d4ed6e35c51e4bcf19b21c2f Mon Sep 17 00:00:00 2001 From: daubners Date: Thu, 16 Jan 2025 11:51:45 +0100 Subject: [PATCH 6/8] update tests for volume fraction --- tests/test_taufactor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_taufactor.py b/tests/test_taufactor.py index b36c4d5..dae7ee2 100644 --- a/tests/test_taufactor.py +++ b/tests/test_taufactor.py @@ -125,7 +125,7 @@ def test_volume_fraction_on_uniform_block(): """Run volume fraction on uniform block""" l = 20 img = np.ones([l, l, l]).reshape(1, l, l, l) - vf = volume_fraction(img) + vf = volume_fraction(img)['1'] assert np.around(vf, decimals=5) == 1.0 @@ -134,7 +134,7 @@ def test_volume_fraction_on_empty_block(): """Run volume fraction on empty block""" l = 20 img = np.zeros([l, l, l]).reshape(1, l, l, l) - vf = volume_fraction(img) + vf = volume_fraction(img)['0'] assert np.around(vf, decimals=5) == 1.0 @@ -143,9 +143,9 @@ def test_volume_fraction_on_checkerboard(): """Run volume fraction on checkerboard block""" l = 20 img = generate_checkerboard(l) - vf = volume_fraction(img) + vf = volume_fraction(img, phases={'zeros': 0, 'ones': 1}) - assert vf == [0.5, 0.5] + assert (vf['zeros'], vf['ones']) == [0.5, 0.5] def test_volume_fraction_on_strip_of_ones(): From 278c562695ff9b666a8e5870d3eb25696fb5af41 Mon Sep 17 00:00:00 2001 From: daubners Date: Thu, 16 Jan 2025 12:56:56 +0100 Subject: [PATCH 7/8] remove cuda from volume --- taufactor/metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taufactor/metrics.py b/taufactor/metrics.py index 58e867a..2176cbe 100644 --- a/taufactor/metrics.py +++ b/taufactor/metrics.py @@ -4,7 +4,7 @@ from scipy.ndimage import label, generate_binary_structure -def volume_fraction(img, phases={}, device=torch.device('cuda')): +def volume_fraction(img, phases={}): """ Calculates volume fractions of phases in an image :param img: segmented input image with n phases @@ -13,7 +13,7 @@ def volume_fraction(img, phases={}, device=torch.device('cuda')): """ if type(img) is not type(torch.tensor(1)): - img = torch.tensor(img, device=device) + img = torch.tensor(img) if phases=={}: volume = torch.numel(img) From b9da23c934984e64cec0e58ec4d05aee61d329c8 Mon Sep 17 00:00:00 2001 From: daubners Date: Thu, 16 Jan 2025 13:11:24 +0100 Subject: [PATCH 8/8] fix tests --- taufactor/metrics.py | 2 ++ tests/test_taufactor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/taufactor/metrics.py b/taufactor/metrics.py index 2176cbe..7e37706 100644 --- a/taufactor/metrics.py +++ b/taufactor/metrics.py @@ -236,6 +236,8 @@ def extract_through_feature(array, grayscale_value, axis, periodic=[False,False, # Compute volume fraction of given grayscale value vol_phase = volume_fraction(array, phases={'1': grayscale_value})['1'] + if vol_phase == 0: + return 0, 0 # Define a list of connectivities to loop over connectivities_to_loop_over = [connectivity] if connectivity else range(1, 4) diff --git a/tests/test_taufactor.py b/tests/test_taufactor.py index dae7ee2..46dade0 100644 --- a/tests/test_taufactor.py +++ b/tests/test_taufactor.py @@ -145,7 +145,7 @@ def test_volume_fraction_on_checkerboard(): img = generate_checkerboard(l) vf = volume_fraction(img, phases={'zeros': 0, 'ones': 1}) - assert (vf['zeros'], vf['ones']) == [0.5, 0.5] + assert (vf['zeros'], vf['ones']) == (0.5, 0.5) def test_volume_fraction_on_strip_of_ones():