@@ -1,8 +1,47 @@
Loading
1 1
import scipy as sp
2 +
import scipy.ndimage as spim
2 3
# import matplotlib.pyplot as plt
3 4
# from mpl_toolkits.mplot3d.art3d import Poly3DCollection
4 5
5 6
7 +
def show_3D(im):
8 +
    r"""
9 +
    Rotates a 3D image and creates an angled view for rough 2D visualization.
10 +
11 +
    Because it rotates the image it can be slow for large image, so is
12 +
    mostly meant for rough checking of small prototype images.
13 +
14 +
    Parameters
15 +
    ----------
16 +
    im : 3D-array
17 +
        The 3D array to be viewed from an angle
18 +
19 +
    Returns
20 +
    -------
21 +
    image : 2D-array
22 +
        A 2D veiw of the given 3D image
23 +
24 +
    Notes
25 +
    -----
26 +
    This function assumes that the image contains ``True`` for void space and
27 +
    so inverts the image to show the solid material.
28 +
29 +
    """
30 +
    im = ~sp.copy(im)
31 +
    if im.ndim < 3:
32 +
        raise Exception('show_3D only applies to 3D images')
33 +
    im = spim.rotate(input=im, angle=22.5, axes=[0, 1], order=0)
34 +
    im = spim.rotate(input=im, angle=45, axes=[2, 1], order=0)
35 +
    im = spim.rotate(input=im, angle=-17, axes=[0, 1], order=0, reshape=False)
36 +
    mask = im != 0
37 +
    view = sp.where(mask.any(axis=2), mask.argmax(axis=2), 0)
38 +
    view = view.max() - view
39 +
    f = view.max()/5
40 +
    view[view == view.max()] = -f
41 +
    view = (view + f)**2
42 +
    return view
43 +
44 +
6 45
def show_planes(im):
7 46
    r"""
8 47
    Create a quick montage showing a 3D image in all three directions

@@ -1,6 +1,7 @@
Loading
1 1
from collections import namedtuple
2 2
import scipy as sp
3 3
import numpy as np
4 +
import operator as op
4 5
import scipy.ndimage as spim
5 6
import scipy.spatial as sptl
6 7
import warnings
@@ -16,6 +17,66 @@
Loading
16 17
from porespy.tools import _create_alias_map
17 18
18 19
20 +
def trim_small_clusters(im, size=1):
21 +
    r"""
22 +
    Remove isolated voxels or clusters smaller than a given size
23 +
24 +
    Parameters
25 +
    ----------
26 +
    im : ND-array
27 +
        The binary image from which voxels are to be removed
28 +
    size : scalar
29 +
        The threshold size of clusters to trim.  As clusters with this many
30 +
        voxels or fewer will be trimmed.  The default is 1 so only single
31 +
        voxels are removed.
32 +
33 +
    Returns
34 +
    -------
35 +
    im : ND-image
36 +
        A copy of ``im`` with clusters of voxels smaller than the given
37 +
        ``size`` removed.
38 +
39 +
    """
40 +
    if im.dims == 2:
41 +
        strel = disk(1)
42 +
    elif im.ndims == 3:
43 +
        strel = ball(1)
44 +
    else:
45 +
        raise Exception('Only 2D or 3D images are accepted')
46 +
    filtered_array = sp.copy(im)
47 +
    labels, N = spim.label(filtered_array, structure=strel)
48 +
    id_sizes = sp.array(spim.sum(im, labels, range(N + 1)))
49 +
    area_mask = (id_sizes <= size)
50 +
    filtered_array[area_mask[labels]] = 0
51 +
    return filtered_array
52 +
53 +
54 +
def hold_peaks(im, axis=-1):
55 +
    r"""
56 +
    Replaces each voxel with the highest value along the given axis
57 +
58 +
    Parameters
59 +
    ----------
60 +
    im : ND-image
61 +
        A greyscale image whose peaks are to
62 +
    """
63 +
    A = im
64 +
    B = np.swapaxes(A, axis, -1)
65 +
    updown = np.empty((*B.shape[:-1], B.shape[-1]+1), B.dtype)
66 +
    updown[..., 0], updown[..., -1] = -1, -1
67 +
    np.subtract(B[..., 1:], B[..., :-1], out=updown[..., 1:-1])
68 +
    chnidx = np.where(updown)
69 +
    chng = updown[chnidx]
70 +
    pkidx, = np.where((chng[:-1] > 0) & (chng[1:] < 0) | (chnidx[-1][:-1] == 0))
71 +
    pkidx = (*map(op.itemgetter(pkidx), chnidx),)
72 +
    out = np.zeros_like(A)
73 +
    aux = out.swapaxes(axis, -1)
74 +
    aux[(*map(op.itemgetter(slice(1, None)), pkidx),)] = np.diff(B[pkidx])
75 +
    aux[..., 0] = B[..., 0]
76 +
    result = out.cumsum(axis=axis)
77 +
    return result
78 +
79 +
19 80
def distance_transform_lin(im, axis=0, mode='both'):
20 81
    r"""
21 82
    Replaces each void voxel with the linear distance to the nearest solid
@@ -985,10 +1046,11 @@
Loading
985 1046
986 1047
def local_thickness(im, sizes=25, mode='hybrid'):
987 1048
    r"""
988 -
    For each voxel, this functions calculates the radius of the largest sphere
989 -
    that both engulfs the voxel and fits entirely within the foreground. This
990 -
    is not the same as a simple distance transform, which finds the largest
991 -
    sphere that could be *centered* on each voxel.
1049 +
    For each voxel, this function calculates the radius of the largest sphere
1050 +
    that both engulfs the voxel and fits entirely within the foreground.
1051 +
1052 +
    This is not the same as a simple distance transform, which finds the
1053 +
    largest sphere that could be *centered* on each voxel.
992 1054
993 1055
    Parameters
994 1056
    ----------
@@ -1111,7 +1173,7 @@
Loading
1111 1173
1112 1174
    Notes
1113 1175
    -----
1114 -
    There are many ways to perform this filter, and PoreSpy offer 3, which
1176 +
    There are many ways to perform this filter, and PoreSpy offers 3, which
1115 1177
    users can choose between via the ``mode`` argument.  These methods all
1116 1178
    work in a similar way by finding which foreground voxels can accomodate
1117 1179
    a sphere of a given radius, then repeating for smaller radii.
@@ -1190,7 +1252,7 @@
Loading
1190 1252
    ----------
1191 1253
    im : ND-array
1192 1254
        The array to be trimmed
1193 -
    inlets : ND-array of tuple of indices
1255 +
    inlets : ND-array or tuple of indices
1194 1256
        The locations of the inlets.  Any voxels *not* connected directly to
1195 1257
        the inlets will be trimmed
1196 1258
@@ -1200,11 +1262,14 @@
Loading
1200 1262
        An array of the same shape as ``im``, but with all foreground
1201 1263
        voxels not connected to the ``inlets`` removed.
1202 1264
    """
1203 -
    temp = sp.zeros_like(im)
1204 -
    temp[inlets] = True
1205 -
    labels, N = spim.label(im + temp)
1206 -
    im = im ^ (clear_border(labels=labels) > 0)
1207 -
    return im
1265 +
    labels = spim.label(im)[0]
1266 +
    keep = sp.unique(labels[inlets])
1267 +
    keep = keep[keep > 0]
1268 +
    if len(keep) > 0:
1269 +
        im2 = sp.reshape(sp.in1d(labels, keep), newshape=im.shape)
1270 +
    else:
1271 +
        im2 = sp.zeros_like(im)
1272 +
    return im2
1208 1273
1209 1274
1210 1275
def _get_axial_shifts(ndim=2, include_diagonals=False):
@@ -1319,3 +1384,66 @@
Loading
1319 1384
        return out[1:-1, 1:-1].copy()
1320 1385
    else:
1321 1386
        return out[1:-1, 1:-1, 1:-1].copy()
1387 +
1388 +
1389 +
def prune_branches(skel, branch_points=None, iterations=1):
1390 +
    r"""
1391 +
    Removes all dangling ends or tails of a skeleton.
1392 +
1393 +
    Parameters
1394 +
    ----------
1395 +
    skel : ND-image
1396 +
        A image of a full or partial skeleton from which the tails should be
1397 +
        trimmed.
1398 +
1399 +
    branch_points : ND-image, optional
1400 +
        An image the same size ``skel`` with True values indicating the branch
1401 +
        points of the skeleton.  If this is not provided it is calculated
1402 +
        automatically.
1403 +
1404 +
    Returns
1405 +
    -------
1406 +
    An ND-image containing the skeleton with tails removed.
1407 +
1408 +
    """
1409 +
    skel = skel > 0
1410 +
    if skel.ndim == 2:
1411 +
        from skimage.morphology import square as cube
1412 +
    else:
1413 +
        from skimage.morphology import cube
1414 +
    # Create empty image to house results
1415 +
    im_result = sp.zeros_like(skel)
1416 +
    # If branch points are not supplied, attempt to find them
1417 +
    if branch_points is None:
1418 +
        branch_points = spim.convolve(skel*1.0, weights=cube(3)) > 3
1419 +
        branch_points = branch_points*skel
1420 +
    # Store original branch points before dilating
1421 +
    pts_orig = branch_points
1422 +
    # Find arcs of skeleton by deleting branch points
1423 +
    arcs = skel*(~branch_points)
1424 +
    # Label arcs
1425 +
    arc_labels = spim.label(arcs, structure=cube(3))[0]
1426 +
    # Dilate branch points so they overlap with the arcs
1427 +
    branch_points = spim.binary_dilation(branch_points, structure=cube(3))
1428 +
    pts_labels = spim.label(branch_points, structure=cube(3))[0]
1429 +
    # Now scan through each arc to see if it's connected to two branch points
1430 +
    slices = spim.find_objects(arc_labels)
1431 +
    label_num = 0
1432 +
    for s in slices:
1433 +
        label_num += 1
1434 +
        # Find branch point labels the overlap current arc
1435 +
        hits = pts_labels[s]*(arc_labels[s] == label_num)
1436 +
        # If image contains 2 branch points, then it's not a tail.
1437 +
        if len(sp.unique(hits)) == 3:
1438 +
            im_result[s] += arc_labels[s] == label_num
1439 +
    # Add missing branch points back to arc image to make complete skeleton
1440 +
    im_result += skel*pts_orig
1441 +
    if iterations > 1:
1442 +
        iterations -= 1
1443 +
        im_temp = sp.copy(im_result)
1444 +
        im_result = prune_branches(skel=im_result,
1445 +
                                   branch_points=None,
1446 +
                                   iterations=iterations)
1447 +
        if sp.all(im_temp == im_result):
1448 +
            iterations = 0
1449 +
    return im_result

@@ -234,45 +234,6 @@
Loading
234 234
    return ret
235 235
236 236
237 -
def get_slice(im, center, size, pad=0):
238 -
    r"""
239 -
    Given a ``center`` location and ``radius`` of a feature, returns the slice
240 -
    object into the ``im`` that bounds the feature but does not extend beyond
241 -
    the image boundaries.
242 -
243 -
    Parameters
244 -
    ----------
245 -
    im : ND-image
246 -
        The image of the porous media
247 -
248 -
    center : array_like
249 -
        The coordinates of the center of the feature of interest
250 -
251 -
    size : array_like or scalar
252 -
        The size of the feature in each direction.  If a scalar is supplied,
253 -
        this implies the same size in all directions.
254 -
255 -
    pad : scalar or array_like
256 -
        The amount to pad onto each side of the slice.  The default is 0.  A
257 -
        scalar value will increase the slice size equally in all directions,
258 -
        while an array the same shape as ``im.shape`` can be passed to pad
259 -
        a specified amount in each direction.
260 -
261 -
    Returns
262 -
    -------
263 -
    slices : list
264 -
        A list of slice objects, each indexing into one dimension of the image.
265 -
    """
266 -
    p = sp.ones(shape=im.ndim, dtype=int) * sp.array(pad)
267 -
    s = sp.ones(shape=im.ndim, dtype=int) * sp.array(size)
268 -
    slc = []
269 -
    for dim in range(im.ndim):
270 -
        lower_im = sp.amax((center[dim] - s[dim] - p[dim], 0))
271 -
        upper_im = sp.amin((center[dim] + s[dim] + 1 + p[dim], im.shape[dim]))
272 -
        slc.append(slice(lower_im, upper_im))
273 -
    return slc
274 -
275 -
276 237
def find_outer_region(im, r=0):
277 238
    r"""
278 239
    Finds regions of the image that are outside of the solid matrix.
@@ -648,6 +609,10 @@
Loading
648 609
        3D) indices is returned.  This tuple can be used directly to index into
649 610
        the image, such as ``im[tup] = 2``.
650 611
612 +
    asmask : Boolean
613 +
        If ``True`` (default) then an image of the specified ``shape`` is
614 +
        returned, otherwise indices of the border voxels are returned.
615 +
651 616
    Returns
652 617
    -------
653 618
    image : ND-array
@@ -677,6 +642,7 @@
Loading
677 642
    [[ True  True  True]
678 643
     [ True False  True]
679 644
     [ True  True  True]]
645 +
680 646
    """
681 647
    ndims = len(shape)
682 648
    t = thickness
@@ -736,9 +702,8 @@
Loading
736 702
737 703
def norm_to_uniform(im, scale=None):
738 704
    r"""
739 -
    Take an image with normally distributed greyscale values and converts it to
740 -
    a uniform (i.e. flat) distribution.  It's also possible to specify the
741 -
    lower and upper limits of the uniform distribution.
705 +
    Take an image with normally distributed greyscale values and convert it to
706 +
    a uniform (i.e. flat) distribution.
742 707
743 708
    Parameters
744 709
    ----------
@@ -1117,3 +1082,45 @@
Loading
1117 1082
            raise Exception('Alias labels does not match with image labels '
1118 1083
                            'please provide correct image labels')
1119 1084
    return al
1085 +
1086 +
1087 +
def extract_regions(regions, labels: list, trim=True):
1088 +
    r"""
1089 +
    Combine given regions into a single boolean mask
1090 +
1091 +
    Parameters
1092 +
    -----------
1093 +
    regions : ND-array
1094 +
        An image containing an arbitrary number of labeled regions
1095 +
    labels : array_like or scalar
1096 +
        A list of labels indicating which region or regions to extract
1097 +
    trim : bool
1098 +
        If ``True`` then image shape will trimmed to a bounding box around the
1099 +
        given regions.
1100 +
1101 +
    Returns
1102 +
    -------
1103 +
    im : ND-array
1104 +
        A boolean mask with ``True`` values indicating where the given labels
1105 +
        exist
1106 +
1107 +
    """
1108 +
    if type(labels) is int:
1109 +
        labels = [labels]
1110 +
    s = spim.find_objects(regions)
1111 +
    im_new = sp.zeros_like(regions)
1112 +
    x_min, y_min, z_min = sp.inf, sp.inf, sp.inf
1113 +
    x_max, y_max, z_max = 0, 0, 0
1114 +
    for i in labels:
1115 +
        im_new[s[i-1]] = regions[s[i-1]] == i
1116 +
        x_min, x_max = min(s[i-1][0].start, x_min), max(s[i-1][0].stop, x_max)
1117 +
        y_min, y_max = min(s[i-1][1].start, y_min), max(s[i-1][1].stop, y_max)
1118 +
        if regions.ndim == 3:
1119 +
            z_min, z_max = min(s[i-1][2].start, z_min), max(s[i-1][2].stop, z_max)
1120 +
    if trim:
1121 +
        if regions.ndim == 3:
1122 +
            bbox = bbox_to_slices([x_min, y_min, z_min, x_max, y_max, z_max])
1123 +
        else:
1124 +
            bbox = bbox_to_slices([x_min, y_min, x_max, y_max])
1125 +
        im_new = im_new[bbox]
1126 +
    return im_new

@@ -704,7 +704,7 @@
Loading
704 704
705 705
706 706
def cylinders(shape: List[int], radius: int, ncylinders: int,
707 -
              phi_max: float = 0, theta_max: float = 90):
707 +
              phi_max: float = 0, theta_max: float = 90, length: float = None):
708 708
    r"""
709 709
    Generates a binary image of overlapping cylinders.  This is a good
710 710
    approximation of a fibrous mat.
@@ -722,14 +722,21 @@
Loading
722 722
        cylinders overlap and intersect different fractions of the domain.
723 723
    theta_max : scalar
724 724
        A value between 0 and 90 that controls the amount of rotation *in the*
725 -
        XY plane, with 0 meaning all fibers point in the X-direction, and
725 +
        XY plane, with 0 meaning all cylinders point in the X-direction, and
726 726
        90 meaning they are randomly rotated about the Z axis by as much
727 727
        as +/- 90 degrees.
728 728
    phi_max : scalar
729 -
        A value between 0 and 90 that controls the amount that the fibers
730 -
        lie *out of* the XY plane, with 0 meaning all fibers lie in the XY
731 -
        plane, and 90 meaning that fibers are randomly oriented out of the
729 +
        A value between 0 and 90 that controls the amount that the cylinders
730 +
        lie *out of* the XY plane, with 0 meaning all cylinders lie in the XY
731 +
        plane, and 90 meaning that cylinders are randomly oriented out of the
732 732
        plane by as much as +/- 90 degrees.
733 +
    length : scalar
734 +
        The length of the cylinders to add.  If ``None`` (default) then the
735 +
        cylinders will extend beyond the domain in both directions so no ends
736 +
        will exist. If a scalar value is given it will be interpreted as the
737 +
        Euclidean distance between the two ends of the cylinder.  Note that
738 +
        one or both of the ends *may* still lie outside the domain, depending
739 +
        on the randomly chosen center point of the cylinder.
733 740
734 741
    Returns
735 742
    -------
@@ -741,7 +748,10 @@
Loading
741 748
        shape = sp.full((3, ), int(shape))
742 749
    elif sp.size(shape) == 2:
743 750
        raise Exception("2D cylinders don't make sense")
744 -
    R = sp.sqrt(sp.sum(sp.square(shape))).astype(int)
751 +
    if length is None:
752 +
        R = sp.sqrt(sp.sum(sp.square(shape))).astype(int)
753 +
    else:
754 +
        R = length/2
745 755
    im = sp.zeros(shape)
746 756
    # Adjust max angles to be between 0 and 90
747 757
    if (phi_max > 90) or (phi_max < 0):

@@ -81,7 +81,7 @@
Loading
81 81
        p_label[pore] = i
82 82
        p_coords[pore, :] = spim.center_of_mass(pore_im) + s_offset
83 83
        p_volume[pore] = sp.sum(pore_im)
84 -
        p_dia_local[pore] = 2*sp.amax(pore_dt)
84 +
        p_dia_local[pore] = (2*sp.amax(pore_dt)) - sp.sqrt(3)
85 85
        p_dia_global[pore] = 2*sp.amax(sub_dt)
86 86
        p_area_surf[pore] = sp.sum(pore_dt == 1)
87 87
        im_w_throats = spim.binary_dilation(input=pore_im, structure=struc_elem(1))

@@ -197,12 +197,12 @@
Loading
197 197
    solid phase.
198 198
199 199
    All other values are ignored, so this can also return the relative
200 -
    fraction of a phase of interest.
200 +
    fraction of a phase of interest in trinary or multiphase images.
201 201
202 202
    Parameters
203 203
    ----------
204 204
    im : ND-array
205 -
        Image of the void space with 1's indicating void space (or True) and
205 +
        Image of the void space with 1's indicating void phase (or True) and
206 206
        0's indicating the solid phase (or False).
207 207
208 208
    Returns
@@ -387,7 +387,10 @@
Loading
387 387
        be automatically generated that span the data range.
388 388
    log : boolean
389 389
        If ``True`` (default) the size data is converted to log (base-10)
390 -
        values before processing.  This can help
390 +
        values before processing.  This can help to plot wide size
391 +
        distributions or to better visualize the in the small size region.
392 +
        Note that you can anti-log the radii values in the retunred ``tuple``,
393 +
        but the binning is performed on the logged radii values.
391 394
    voxel_size : scalar
392 395
        The size of a voxel side in preferred units.  The default is 1, so the
393 396
        user can apply the scaling to the returned results after the fact.
@@ -416,8 +419,8 @@
Loading
416 419
417 420
    Notes
418 421
    -----
419 -
    (1) To ensure the returned values represent actual sizes be sure to scale
420 -
    the distance transform by the voxel size first (``dt *= voxel_size``)
422 +
    (1) To ensure the returned values represent actual sizes you can manually
423 +
    scale the input image by the voxel size first (``im *= voxel_size``)
421 424
422 425
    plt.bar(psd.R, psd.satn, width=psd.bin_widths, edgecolor='k')
423 426
Files Coverage
porespy 85.2%
docs/conf.py 100.0%
examples.py 100.0%
Project Totals (17 files) 85.6%
1257.2
TRAVIS_PYTHON_VERSION=3.7
TRAVIS_OS_NAME=linux
1257.1
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
1260.2
TRAVIS_PYTHON_VERSION=3.7
TRAVIS_OS_NAME=linux
1260.1
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
1
codecov:
2
  branch: master
3

4
coverage:
5
  precision: 2
6
  round: down
7
  range: "50...100"
8

9
  status:
10
    project:
11
      default:
12
        target: auto
13
        threshold: null
14
        branches: null
15

16
    patch:
17
      default:
18
        target: auto
19
        branches: null
20

21
comment:
22
  layout: "header, diff, changes, sunburst, uncovered"
23
  branches: null
24
  behavior: default
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading