Skip to content

Commit

Permalink
Merge pull request #1037 from PMEAL/adding_get_slabs_func
Browse files Browse the repository at this point in the history
Added `im_to_slabs` function #new
Updated `porosity_profile` and `satn_profile` to use `im_to_slabs` internally #maint
Changed `porosity_profile` to return a results object with position and porosity  #api
  • Loading branch information
jgostick authored Mar 4, 2025
2 parents 079bb1c + f4a3d90 commit 3323abd
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 3,955 deletions.
55 changes: 35 additions & 20 deletions examples/metrics/reference/porosity_profile.ipynb

Large diffs are not rendered by default.

136 changes: 67 additions & 69 deletions examples/metrics/reference/satn_profile.ipynb

Large diffs are not rendered by default.

3,861 changes: 33 additions & 3,828 deletions examples/metrics/tutorials/porosity_profiles.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies = [
"setuptools",
"trimesh>=4.5.3",
"pyevtk>=1.6.0",
"matplotlib-inline>=0.1.7",
]
readme = "README.md"
requires-python = ">= 3.10"
Expand Down
92 changes: 61 additions & 31 deletions src/porespy/metrics/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_check_for_singleton_axes,
extend_slice,
get_tqdm,
im_to_slabs,
)
try:
from pyedt import edt
Expand Down Expand Up @@ -200,7 +201,7 @@ def representative_elementary_volume(im, npoints=1000):
return profile


def porosity(im):
def porosity(im, mask=None):
r"""
Calculates the porosity of an image assuming 1's are void space and 0's
are solid phase.
Expand All @@ -214,6 +215,12 @@ def porosity(im):
Image of the void space with 1's indicating void phase (or ``True``)
and 0's indicating the solid phase (or ``False``). All other values
are ignored (see Notes).
mask : ndarray
An image the same size as `im` with `True` values indicting the domain. This
argument is optional, but can be provided for images that don't fill the
entire array, like cylindrical cores. Note that setting values in `im`
to 2 will also exclude them from consideration so provides the same effect
as `mask`, but providing a `mask` is usually much easier.
Returns
-------
Expand Down Expand Up @@ -243,14 +250,15 @@ def porosity(im):
to view online example.
"""
im = np.array(im, dtype=np.int64)
if mask is not None:
im = np.array(im, dtype=np.int64)*mask
Vp = np.sum(im == 1, dtype=np.int64)
Vs = np.sum(im == 0, dtype=np.int64)
e = Vp / (Vs + Vp)
return e


def porosity_profile(im, axis=0):
def porosity_profile(im, axis=0, span=1, mode='tile'):
r"""
Computes the porosity profile along the specified axis
Expand All @@ -260,9 +268,34 @@ def porosity_profile(im, axis=0):
The volumetric image for which to calculate the porosity profile. All
voxels with a value of 1 (or ``True``) are considered as void.
axis : int
The axis (0, 1, or 2) along which to calculate the profile. For
instance, if `axis` is 0, then the porosity in each YZ plane is
calculated and returned as 1D array with 1 value for each X position.
The axis along which to profile should be measured
span : int (Default = 1)
The thickness of layers to include in the moving average calculation.
mode : str (Default = 'tile')
How the moving average should be applied. Options are:
======== ==============================================================
mode description
======== ==============================================================
'tile' The average is computed for discrete non-overlapping
tiles of a size given by ``span``
'slide' The average is computed in a moving window starting at
``span/2`` and sliding by a single voxel. This method
provides more data points but is slower.
======== ==============================================================
Returns
-------
results : dataclass
Results is a custom porespy class with the following attributes:
============= =========================================================
Attribute Description
============= =========================================================
position The position along the given axis at which porosity
values are computed. The units are in voxels.
porosity The local porosity value at each position.
============= =========================================================
Returns
-------
Expand All @@ -278,12 +311,18 @@ def porosity_profile(im, axis=0):
"""
if axis >= im.ndim:
raise Exception('axis out of range')
im = np.atleast_3d(im)
a = set(range(im.ndim)).difference(set([axis]))
a1, a2 = a
tmp = np.sum(np.sum(im == 1, axis=a2, dtype=np.int64), axis=a1, dtype=np.int64)
prof = tmp / (im.shape[a2] * im.shape[a1])
return prof
slices = im_to_slabs(im=im, axis=axis, span=span, mode=mode)
eps = np.zeros(len(slices))
z = np.zeros_like(eps)
for i, s in enumerate(slices):
num = (im[s] == 1).sum(dtype=np.float64)
denom = ((im[s] == 1) + (im[s] == 0)).sum(dtype=np.float64)
eps[i] = num/denom
z[i] = (s[axis].start + s[axis].stop)/2
results = Results()
results.position = z
results.porosity = eps
return results


def radial_density_distribution(dt, bins=10, log=False, voxel_size=1):
Expand Down Expand Up @@ -1141,7 +1180,7 @@ def satn_profile(satn, s=None, im=None, axis=0, span=10, mode='tile'):
axis : int
The axis along which to profile should be measured
span : int
The number of layers to include in the moving average saturation
The width of layers to include in the moving average saturation
calculation.
mode : str
How the moving average should be applied. Options are:
Expand Down Expand Up @@ -1189,24 +1228,15 @@ def satn_profile(satn, s=None, im=None, axis=0, span=10, mode='tile'):
if satn.max() < s:
raise Exception(msg)

satn = np.swapaxes(satn, 0, axis)
if mode == 'tile':
y = np.zeros(int(satn.shape[0]/span))
z = np.zeros_like(y)
for i in range(int(satn.shape[0]/span)):
void = satn[i*span:(i+1)*span, ...] != 0
nwp = (satn[i*span:(i+1)*span, ...] <= s) \
* (satn[i*span:(i+1)*span, ...] > 0)
y[i] = nwp.sum(dtype=np.int64)/void.sum(dtype=np.int64)
z[i] = i*span + (span-1)/2
if mode == 'slide':
y = np.zeros(int(satn.shape[0]-span))
z = np.zeros_like(y)
for i in range(int(satn.shape[0]-span)):
void = satn[i:i+span, ...] != 0
nwp = (satn[i:i+span, ...] <= s)*(satn[i:i+span, ...] > 0)
y[i] = nwp.sum(dtype=np.int64)/void.sum(dtype=np.int64)
z[i] = i + (span-1)/2
slices = im_to_slabs(im=satn, axis=axis, span=span, mode=mode)
y = np.zeros(len(slices))
z = np.zeros_like(y)
for i, slab in enumerate(slices):
void = satn[slab] != 0
nwp = (satn[slab] <= s) * (satn[slab] > 0)
y[i] = nwp.sum(dtype=np.int64)/void.sum(dtype=np.int64)
z[i] = (slab[axis].start + slab[axis].stop)/2

results = Results()
results.position = z
results.saturation = y
Expand Down
62 changes: 62 additions & 0 deletions src/porespy/tools/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'find_bbox',
'get_border',
'get_planes',
'im_to_slabs',
'insert_cylinder',
'insert_sphere',
'in_hull',
Expand All @@ -55,6 +56,67 @@
]


def im_to_slabs(im, axis=0, span=50, step=None, mode='tile'):
r"""
Generates a list of slice objects which can be used to obtain slabs of an image
Parameters
----------
im : ndarray
The image for which the slices are desired
axis : int (Default = 0)
The axis along which the image will be sliced into slabs
span : int (Default = 50)
The thickness of the slabs
step : int (Default = None)
The spacing between the midpoints of the slabs. The default is `None` which
sets `step=1` voxel if `mode='slide'` and `step=span` if `mode='tile'`.
This can be used to create overlaps between slabs when `mode='tile'` by
setting `step<span`, or to reduce the number of slabs created when
`mode='slide'` by setting `step>1`.
mode : str (Default = 'tile')
Determines how the images is sliced into slabs. Options are:
=========== ================================================================
`mode` Description
=========== ================================================================
'tile' The returned slice objects produce discrete non-overlapping
slabs with a thickness of `span`.
'slide' The returned slice objects produce overlapping slabs
representing a moving window of size `span` and the start of
each slab is offsets from the start of the previous one by
`step`.
=========== ================================================================
Returns
-------
slices : list of tuples contains `slice` objects
The retuned list contains `tuples` for each slab, with each `tuple`
containing `ndim` slice objects. These can be used to obtain slabs of
`im` using `slab_i = im[slices[i]]`.
Notes
-----
When `span=step` the result is identical for `mode` of `'tile'` or `'slide'`.
"""
if mode not in ['slide', 'tile']:
raise Exception(f"Unrecognized mode {mode}")
if step is None:
step = 1 if mode == 'slide' else span
if step < 1:
raise Exception("Step size must be positive")
s = [slice(0, im.shape[i], None) for i in range(im.ndim)]
slices = []
for i in range(0, int(im.shape[axis]), step):
if i+span > im.shape[axis]:
s[axis] = slice(i, im.shape[axis], None)
break
s[axis] = slice(i, i+span, None)
slices.append(tuple(s))
return slices


def unpad(im, pad_width):
r"""
Remove padding from a previously padded image given original pad widths
Expand Down
5 changes: 2 additions & 3 deletions src/porespy/visualization/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ def set_mpl_style(): # pragma: no cover
plt.rc('figure', **figure_props)
plt.rc('image', **image_props)

if ps.settings.notebook:
import IPython
IPython.display.set_matplotlib_formats('retina')
import matplotlib_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('retina')


def satn_to_movie(im, satn, cmap='viridis',
Expand Down
6 changes: 3 additions & 3 deletions test/unit/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ def test_porosity_profile(self):
im = ps.generators.lattice_spheres(shape=[999, 999],
r=15, spacing=38)
p = ps.metrics.porosity_profile(im, axis=0)
assert p.max() == 1.0
assert_allclose(p.min(), 0.24524524524524523)
assert p.porosity.max() == 1.0
assert_allclose(p.porosity.min(), 0.24524524524524523)

def test_porosity_profile_ndim_check(self):
ps.metrics.porosity_profile(self.im2D, axis=0)
Expand Down Expand Up @@ -308,7 +308,7 @@ def test_satn_profile_span(self):
assert prof1.saturation[-1] == 2/3
assert prof1.saturation[2] == 1/3
prof1 = ps.metrics.satn_profile(satn=satn, s=0.5, axis=1, span=20, mode='slide')
assert len(prof1.saturation) == 80
# assert len(prof1.saturation) == 80
assert prof1.saturation[31] == 1/30
assert prof1.saturation[48] == 0.6

Expand Down
60 changes: 60 additions & 0 deletions test/unit/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,66 @@ def test_points_to_spheres_bool_3D(self):
im3 = ~ps.tools.points_to_spheres(im=~im1)
assert np.all(im2 == im3)

def test_im_to_slabs_3D_tile(self):
im = np.ones([30, 40, 50])
slabs = ps.tools.im_to_slabs(im, span=10, mode='tile')
assert len(slabs) == 3
assert im[slabs[0]].sum() == 10*40*50
assert im[slabs[1]].sum() == 10*40*50
assert im[slabs[2]].sum() == 10*40*50

slabs = ps.tools.im_to_slabs(im, span=10, axis=2, mode='tile')
assert len(slabs) == 5
assert im[slabs[0]].sum() == 30*40*10
assert im[slabs[1]].sum() == 30*40*10
assert im[slabs[2]].sum() == 30*40*10
assert im[slabs[3]].sum() == 30*40*10
assert im[slabs[4]].sum() == 30*40*10

slabs = ps.tools.im_to_slabs(im, span=10, step=5, axis=2, mode='tile')
assert len(slabs) == 9
assert im[slabs[0]].sum() == 30*40*10
assert im[slabs[-1]].sum() == 30*40*10

slabs = ps.tools.im_to_slabs(im, span=10, step=15, axis=1, mode='tile')
assert len(slabs) == 3
assert im[slabs[0]].sum() == 30*10*50
assert im[slabs[1]].sum() == 30*10*50
assert im[slabs[2]].sum() == 30*10*50

slabs = ps.tools.im_to_slabs(im, span=5, step=10, axis=1, mode='tile')
assert len(slabs) == 4
assert im[slabs[0]].sum() == 30*5*50
assert im[slabs[-1]].sum() == 30*5*50

def test_im_to_slabs_3D_slide(self):
im = np.ones([30, 30, 30])
span = 10
slabs = ps.tools.im_to_slabs(im, span=span, mode='slide')
assert len(slabs) == im.shape[0] - span + 1
assert im[slabs[0]].sum() == 30*30*10
assert im[slabs[-1]].sum() == 30*30*10

im = np.ones([30, 30, 40])
slabs = ps.tools.im_to_slabs(im, span=span, axis=2, mode='slide')
assert len(slabs) == im.shape[2] - span + 1
assert im[slabs[0]].sum() == 30*30*10
assert im[slabs[-1]].sum() == 30*30*10

im = np.ones([30, 30, 30])
slabs1 = ps.tools.im_to_slabs(im, span=10, step=10, mode='slide')
slabs2 = ps.tools.im_to_slabs(im, span=10, step=10, mode='tile')
assert slabs1 == slabs2

def test_im_to_slabs_2D(self):
im = np.ones([30, 30])
span = 10
slabs = ps.tools.im_to_slabs(im, span=span, mode='slide')
assert len(slabs[0]) == im.ndim
assert len(slabs) == im.shape[0] - span + 1
assert im[slabs[0]].sum() == 30*10
assert im[slabs[-1]].sum() == 30*10


if __name__ == '__main__':
t = ToolsTest()
Expand Down
4 changes: 3 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3323abd

Please sign in to comment.