def representative_elementary_volume(im, npoints=1000): r""" Calculates the porosity of the image as a function subdomain size. This function extracts a specified number of subdomains of random size, then finds their porosity. Parameters ---------- im : ND-array The image of the porous material npoints : int The number of randomly located and sized boxes to sample. The default is 1000. Returns ------- A tuple containing the ND-arrays: The subdomain *volume* and its *porosity*. Each of these arrays is ``npoints`` long. They can be conveniently plotted by passing the tuple to matplotlib's ``plot`` function using the \* notation: ``plt.plot(*the_tuple, 'b.')``. The resulting plot is similar to the sketch given by Bachmat and Bear [1] Notes ----- This function is frustratingly slow. Profiling indicates that all the time is spent on scipy's ``sum`` function which is needed to sum the number of void voxels (1's) in each subdomain. Also, this function is primed for parallelization since the ``npoints`` are calculated independenlty. References ---------- [1] Bachmat and Bear. On the Concept and Size of a Representative Elementary Volume (Rev), Advances in Transport Phenomena in Porous Media (1987) """ im_temp = sp.zeros_like(im) crds = sp.array(sp.rand(npoints, im.ndim) * im.shape, dtype=int) pads = sp.array(sp.rand(npoints) * sp.amin(im.shape) / 2 + 10, dtype=int) im_temp[tuple(crds.T)] = True labels, N = spim.label(input=im_temp) slices = spim.find_objects(input=labels) porosity = sp.zeros(shape=(N, ), dtype=float) volume = sp.zeros(shape=(N, ), dtype=int) for i in tqdm(sp.arange(0, N)): s = slices[i] p = pads[i] new_s = extend_slice(s, shape=im.shape, pad=p) temp = im[new_s] Vp = sp.sum(temp) Vt = sp.size(temp) porosity[i] = Vp / Vt volume[i] = Vt profile = namedtuple('profile', ('volume', 'porosity')) profile.volume = volume profile.porosity = porosity return profile
def trim_saddle_points(peaks, dt, max_iters=10): r""" Removes peaks that were mistakenly identified because they lied on a saddle or ridge in the distance transform that was not actually a true local peak. Parameters ---------- peaks : ND-array A boolean image containing True values to mark peaks in the distance transform (``dt``) dt : ND-array The distance transform of the pore space for which the true peaks are sought. max_iters : int The maximum number of iterations to run while eroding the saddle points. The default is 10, which is usually not reached; however, a warning is issued if the loop ends prior to removing all saddle points. Returns ------- image : ND-array An image with fewer peaks than the input image """ peaks = sp.copy(peaks) if dt.ndim == 2: from skimage.morphology import square as cube else: from skimage.morphology import cube labels, N = spim.label(peaks) slices = spim.find_objects(labels) for i in range(N): s = extend_slice(s=slices[i], shape=peaks.shape, pad=10) peaks_i = labels[s] == i + 1 dt_i = dt[s] im_i = dt_i > 0 iters = 0 peaks_dil = sp.copy(peaks_i) while iters < max_iters: iters += 1 peaks_dil = spim.binary_dilation(input=peaks_dil, structure=cube(3)) peaks_max = peaks_dil * sp.amax(dt_i * peaks_dil) peaks_extended = (peaks_max == dt_i) * im_i if sp.all(peaks_extended == peaks_i): break # Found a true peak elif sp.sum(peaks_extended * peaks_i) == 0: peaks_i = False break # Found a saddle point peaks[s] = peaks_i if iters >= max_iters: print('Maximum number of iterations reached, consider' + 'running again with a larger value of max_iters') return peaks
def region_surface_areas(regions, voxel_size=1, strel=None): r""" Extracts the surface area of each region in a labeled image. Optionally, it can also find the the interfacial area between all adjoining regions. Parameters ---------- regions : ND-array An image of the pore space partitioned into individual pore regions. Note that zeros in the image will not be considered for area calculation. voxel_size : scalar The resolution of the image, expressed as the length of one side of a voxel, so the volume of a voxel would be **voxel_size**-cubed. The default is 1. strel : array_like The structuring element used to blur the region. If not provided, then a spherical element (or disk) with radius 1 is used. See the docstring for ``mesh_region`` for more details, as this argument is passed to there. Returns ------- result : list A list containing the surface area of each region, offset by 1, such that the surface area of region 1 is stored in element 0 of the list. """ print('-' * 60) print('Finding surface area of each region') im = regions.copy() # Get 'slices' into im for each pore region slices = spim.find_objects(im) # Initialize arrays Ps = np.arange(1, np.amax(im) + 1) sa = np.zeros_like(Ps, dtype=float) # Start extracting marching cube area from im for i in tqdm(Ps): reg = i - 1 if slices[reg] is not None: s = extend_slice(slices[reg], im.shape) sub_im = im[s] mask_im = sub_im == i mesh = mesh_region(region=mask_im, strel=strel) sa[reg] = mesh_surface_area(mesh) result = sa * voxel_size**2 return result
def region_interface_areas(regions, areas, voxel_size=1, strel=None): r""" Calculates the interfacial area between all pairs of adjecent regions Parameters ---------- regions : ND-array An image of the pore space partitioned into individual pore regions. Note that zeros in the image will not be considered for area calculation. areas : array_like A list containing the areas of each regions, as determined by ``region_surface_area``. Note that the region number and list index are offset by 1, such that the area for region 1 is stored in ``areas[0]``. voxel_size : scalar The resolution of the image, expressed as the length of one side of a voxel, so the volume of a voxel would be **voxel_size**-cubed. The default is 1. strel : array_like The structuring element used to blur the region. If not provided, then a spherical element (or disk) with radius 1 is used. See the docstring for ``mesh_region`` for more details, as this argument is passed to there. Returns ------- result : named_tuple A named-tuple containing 2 arrays. ``conns`` holds the connectivity information and ``area`` holds the result for each pair. ``conns`` is a N-regions by 2 array with each row containing the region number of an adjacent pair of regions. For instance, if ``conns[0, 0]`` is 0 and ``conns[0, 1]`` is 5, then row 0 of ``area`` contains the interfacial area shared by regions 0 and 5. """ print('-' * 60) print('Finding interfacial areas between each region') from skimage.morphology import disk, ball im = regions.copy() if im.ndim != im.squeeze().ndim: warnings.warn('Input image conains a singleton axis:' + str(im.shape) + ' Reduce dimensionality with np.squeeze(im) to avoid' + ' unexpected behavior.') # cube_elem = square if im.ndim == 2 else cube ball_elem = disk if im.ndim == 2 else ball # Get 'slices' into im for each region slices = spim.find_objects(im) # Initialize arrays Ps = np.arange(1, np.amax(im) + 1) sa = np.zeros_like(Ps, dtype=float) sa_combined = [] # Difficult to preallocate since number of conns unknown cn = [] # Start extracting area from im for i in tqdm(Ps): reg = i - 1 if slices[reg] is not None: s = extend_slice(slices[reg], im.shape) sub_im = im[s] mask_im = sub_im == i sa[reg] = areas[reg] im_w_throats = spim.binary_dilation(input=mask_im, structure=ball_elem(1)) im_w_throats = im_w_throats * sub_im Pn = np.unique(im_w_throats)[1:] - 1 for j in Pn: if j > reg: cn.append([reg, j]) merged_region = im[( min(slices[reg][0].start, slices[j][0].start) ):max(slices[reg][0].stop, slices[j][0].stop), ( min(slices[reg][1].start, slices[j][1].start) ):max(slices[reg][1].stop, slices[j][1].stop)] merged_region = ((merged_region == reg + 1) + (merged_region == j + 1)) mesh = mesh_region(region=merged_region, strel=strel) sa_combined.append(mesh_surface_area(mesh)) # Interfacial area calculation cn = np.array(cn) ia = 0.5 * (sa[cn[:, 0]] + sa[cn[:, 1]] - sa_combined) ia[ia <= 0] = 1 result = namedtuple('interfacial_areas', ('conns', 'area')) result.conns = cn result.area = ia * voxel_size**2 return result
def regions_to_network(im, dt=None, voxel_size=1): r""" Analyzes an image that has been partitioned into pore regions and extracts the pore and throat geometry as well as network connectivity. Parameters ---------- im : ND-array An image of the pore space partitioned into individual pore regions. Note that this image must have zeros indicating the solid phase. dt : ND-array The distance transform of the pore space. If not given it will be calculated, but it can save time to provide one if available. voxel_size : scalar The resolution of the image, expressed as the length of one side of a voxel, so the volume of a voxel would be **voxel_size**-cubed. The default is 1, which is useful when overlaying the PNM on the original image since the scale of the image is alway 1 unit lenth per voxel. Returns ------- A dictionary containing all the pore and throat size data, as well as the network topological information. The dictionary names use the OpenPNM convention (i.e. 'pore.coords', 'throat.conns') so it may be converted directly to an OpenPNM network object using the ``update`` command. """ print('_' * 60) print('Extracting pore and throat information from image') from skimage.morphology import disk, square, ball, cube if im.ndim == 2: cube = square ball = disk # if ~sp.any(im == 0): # raise Exception('The received image has no solid phase (0\'s)') if dt is None: dt = spim.distance_transform_edt(im > 0) dt = spim.gaussian_filter(input=dt, sigma=0.5) # Get 'slices' into im for each pore region slices = spim.find_objects(im) # Initialize arrays Ps = sp.arange(1, sp.amax(im) + 1) Np = sp.size(Ps) p_coords = sp.zeros((Np, im.ndim), dtype=float) p_volume = sp.zeros((Np, ), dtype=float) p_dia_local = sp.zeros((Np, ), dtype=float) p_dia_global = sp.zeros((Np, ), dtype=float) p_label = sp.zeros((Np, ), dtype=int) p_area_surf = sp.zeros((Np, ), dtype=int) mc_sa = sp.zeros((Np, ), dtype=int) t_area_mc = [] t_conns = [] t_dia_inscribed = [] t_area = [] t_perimeter = [] t_coords = [] # Start extracting size information for pores and throats for i in tqdm(Ps): pore = i - 1 # if slices[pore] is None: # continue s = extend_slice(slices[pore], im.shape) sub_im = im[s] sub_dt = dt[s] pore_im = sub_im == i # --------------------------------------------------------------------- padded_mask = sp.pad(pore_im, pad_width=1, mode='constant') pore_dt = spim.distance_transform_edt(padded_mask) if padded_mask.ndim == 3: filter_mask = spim.convolve(padded_mask * 1.0, weights=ball(1)) / sp.sum(ball(1)) verts, faces, norm, val = measure.marching_cubes_lewiner( filter_mask) else: padded_mask1 = sp.reshape(pore_im, (1, ) + pore_im.shape) padded_mask1 = sp.pad(padded_mask1, pad_width=1, mode='constant') verts, faces, norm, val = measure.marching_cubes_lewiner( padded_mask1) mc_sa[pore] = measure.mesh_surface_area(verts, faces) # --------------------------------------------------------------------- s_offset = sp.array([i.start for i in s]) p_label[pore] = i p_coords[pore, :] = spim.center_of_mass(pore_im) + s_offset p_volume[pore] = sp.sum(pore_im) p_dia_local[pore] = 2 * sp.amax(pore_dt) p_dia_global[pore] = 2 * sp.amax(sub_dt) p_area_surf[pore] = sp.sum(pore_dt == 1) im_w_throats = spim.binary_dilation(input=pore_im, structure=ball(1)) im_w_throats = im_w_throats * sub_im Pn = sp.unique(im_w_throats)[1:] - 1 for j in Pn: if j > pore: t_conns.append([pore, j]) vx = sp.where(im_w_throats == (j + 1)) t_dia_inscribed.append(2 * sp.amax(sub_dt[vx])) t_perimeter.append(sp.sum(sub_dt[vx] < 2)) t_area.append(sp.size(vx[0])) # ------------------------------------------------------------- merged_region = im[( min(slices[pore][0].start, slices[j][0].start) ):max(slices[pore][0].stop, slices[j][0].stop), ( min(slices[pore][1].start, slices[j][1].start) ):max(slices[pore][1].stop, slices[j][1].stop)] merged_region = ((merged_region == pore + 1) + (merged_region == j + 1)) if im.ndim == 3: merged_region = sp.pad(merged_region, pad_width=1, mode='constant', constant_values=0) mfilter = spim.convolve(merged_region * 1.0, weights=ball(1)) / sp.sum(ball(1)) j_mask = im[slices[j]] == j + 1 j_mask = sp.pad(j_mask * 1.0, pad_width=1, mode='constant', constant_values=0) jfilter = spim.convolve(j_mask, weights=ball(1)) / sp.sum( ball(1)) else: merged_region = sp.reshape(merged_region, (1, ) + merged_region.shape) mfilter = sp.pad(merged_region, pad_width=1, mode='constant', constant_values=0) j_mask = im[slices[j]] == j + 1 j_mask = sp.reshape(j_mask, (1, ) + j_mask.shape) jfilter = sp.pad(j_mask * 1.0, pad_width=1, mode='constant', constant_values=0) verts1, face1, n1, v1 = measure.marching_cubes_lewiner(mfilter) mc_sa_combined = measure.mesh_surface_area(verts1, face1) verts2, face2, n2, v2 = measure.marching_cubes_lewiner(jfilter) mc_sa_j = measure.mesh_surface_area(verts2, face2) mc_area = 0.5 * (mc_sa_j + mc_sa[pore] - mc_sa_combined) if mc_area < 0: mc_area = 1.0 t_area_mc.append(mc_area) # ------------------------------------------------------------- t_inds = tuple([i + j for i, j in zip(vx, s_offset)]) temp = sp.where(dt[t_inds] == sp.amax(dt[t_inds]))[0][0] if im.ndim == 2: t_coords.append(tuple((t_inds[0][temp], t_inds[1][temp]))) else: t_coords.append( tuple((t_inds[0][temp], t_inds[1][temp], t_inds[2][temp]))) # Clean up values Nt = len(t_dia_inscribed) # Get number of throats if im.ndim == 2: # If 2D, add 0's in 3rd dimension p_coords = sp.vstack((p_coords.T, sp.zeros((Np, )))).T t_coords = sp.vstack((sp.array(t_coords).T, sp.zeros((Nt, )))).T net = {} net['pore.all'] = sp.ones((Np, ), dtype=bool) net['throat.all'] = sp.ones((Nt, ), dtype=bool) net['pore.coords'] = sp.copy(p_coords) * voxel_size net['pore.centroid'] = sp.copy(p_coords) * voxel_size net['throat.centroid'] = sp.array(t_coords) * voxel_size net['throat.conns'] = sp.array(t_conns) net['pore.label'] = sp.array(p_label) net['pore.volume'] = sp.copy(p_volume) * (voxel_size**3) net['throat.volume'] = sp.zeros((Nt, ), dtype=float) net['pore.diameter'] = sp.copy(p_dia_local) * voxel_size net['pore.inscribed_diameter'] = sp.copy(p_dia_local) * voxel_size net['pore.equivalent_diameter'] = 2 * ( (3 / 4 * net['pore.volume'] / sp.pi)**(1 / 3)) net['pore.extended_diameter'] = sp.copy(p_dia_global) * voxel_size net['pore.surface_area'] = sp.copy(p_area_surf) * (voxel_size)**2 net['pore.surface_area_mc'] = sp.copy(mc_sa) * (voxel_size)**2 net['throat.area_mc'] = sp.array(t_area_mc) * (voxel_size**2) net['throat.diameter'] = sp.array(t_dia_inscribed) * voxel_size net['throat.inscribed_diameter'] = sp.array(t_dia_inscribed) * voxel_size net['throat.area'] = sp.array(t_area) * (voxel_size**2) net['throat.perimeter'] = sp.array(t_perimeter) * voxel_size net['throat.equivalent_diameter'] = ((sp.array(t_area) * (voxel_size**2))**(0.5)) P12 = net['throat.conns'] PT1 = (sp.sqrt( sp.sum(((p_coords[P12[:, 0]] - t_coords) * voxel_size)**2, axis=1))) PT2 = (sp.sqrt( sp.sum(((p_coords[P12[:, 1]] - t_coords) * voxel_size)**2, axis=1))) net['throat.total_length'] = PT1 + PT2 PT1 = PT1 - p_dia_local[P12[:, 0]] / 2 * voxel_size PT2 = PT2 - p_dia_local[P12[:, 1]] / 2 * voxel_size net['throat.length'] = PT1 + PT2 dist = (p_coords[P12[:, 0]] - p_coords[P12[:, 1]]) * voxel_size net['throat.direct_length'] = sp.sqrt(sp.sum(dist**2, axis=1)) return net
def regions_to_network(im, dt=None, voxel_size=1): r""" Analyzes an image that has been partitioned into pore regions and extracts the pore and throat geometry as well as network connectivity. Parameters ---------- im : ND-array An image of the pore space partitioned into individual pore regions. Note that this image must have zeros indicating the solid phase. dt : ND-array The distance transform of the pore space. If not given it will be calculated, but it can save time to provide one if available. voxel_size : scalar The resolution of the image, expressed as the length of one side of a voxel, so the volume of a voxel would be **voxel_size**-cubed. The default is 1, which is useful when overlaying the PNM on the original image since the scale of the image is alway 1 unit lenth per voxel. Returns ------- A dictionary containing all the pore and throat size data, as well as the network topological information. The dictionary names use the OpenPNM convention (i.e. 'pore.coords', 'throat.conns') so it may be converted directly to an OpenPNM network object using the ``update`` command. """ print('-' * 60) print('Extracting pore and throat information from image') from skimage.morphology import disk, ball struc_elem = disk if im.ndim == 2 else ball # if ~np.any(im == 0): # raise Exception('The received image has no solid phase (0\'s)') if dt is None: dt = spim.distance_transform_edt(im > 0) dt = spim.gaussian_filter(input=dt, sigma=0.5) # Get 'slices' into im for each pore region slices = spim.find_objects(im) # Initialize arrays Ps = np.arange(1, np.amax(im) + 1) Np = np.size(Ps) p_coords = np.zeros((Np, im.ndim), dtype=float) p_volume = np.zeros((Np, ), dtype=float) p_dia_local = np.zeros((Np, ), dtype=float) p_dia_global = np.zeros((Np, ), dtype=float) p_label = np.zeros((Np, ), dtype=int) p_area_surf = np.zeros((Np, ), dtype=int) t_conns = [] t_dia_inscribed = [] t_area = [] t_perimeter = [] t_coords = [] # dt_shape = np.array(dt.shape) # Start extracting size information for pores and throats for i in tqdm(Ps, file=sys.stdout): pore = i - 1 if slices[pore] is None: continue s = extend_slice(slices[pore], im.shape) sub_im = im[s] sub_dt = dt[s] pore_im = sub_im == i padded_mask = np.pad(pore_im, pad_width=1, mode='constant') pore_dt = spim.distance_transform_edt(padded_mask) s_offset = np.array([i.start for i in s]) p_label[pore] = i p_coords[pore, :] = spim.center_of_mass(pore_im) + s_offset p_volume[pore] = np.sum(pore_im) p_dia_local[pore] = (2 * np.amax(pore_dt)) - np.sqrt(3) p_dia_global[pore] = 2 * np.amax(sub_dt) p_area_surf[pore] = np.sum(pore_dt == 1) im_w_throats = spim.binary_dilation(input=pore_im, structure=struc_elem(1)) im_w_throats = im_w_throats * sub_im Pn = np.unique(im_w_throats)[1:] - 1 for j in Pn: if j > pore: t_conns.append([pore, j]) vx = np.where(im_w_throats == (j + 1)) t_dia_inscribed.append(2 * np.amax(sub_dt[vx])) t_perimeter.append(np.sum(sub_dt[vx] < 2)) t_area.append(np.size(vx[0])) t_inds = tuple([i + j for i, j in zip(vx, s_offset)]) temp = np.where(dt[t_inds] == np.amax(dt[t_inds]))[0][0] if im.ndim == 2: t_coords.append(tuple((t_inds[0][temp], t_inds[1][temp]))) else: t_coords.append( tuple((t_inds[0][temp], t_inds[1][temp], t_inds[2][temp]))) # Clean up values Nt = len(t_dia_inscribed) # Get number of throats if im.ndim == 2: # If 2D, add 0's in 3rd dimension p_coords = np.vstack((p_coords.T, np.zeros((Np, )))).T t_coords = np.vstack((np.array(t_coords).T, np.zeros((Nt, )))).T net = {} net['pore.all'] = np.ones((Np, ), dtype=bool) net['throat.all'] = np.ones((Nt, ), dtype=bool) net['pore.coords'] = np.copy(p_coords) * voxel_size net['pore.centroid'] = np.copy(p_coords) * voxel_size net['throat.centroid'] = np.array(t_coords) * voxel_size net['throat.conns'] = np.array(t_conns) net['pore.label'] = np.array(p_label) net['pore.volume'] = np.copy(p_volume) * (voxel_size**3) net['throat.volume'] = np.zeros((Nt, ), dtype=float) net['pore.diameter'] = np.copy(p_dia_local) * voxel_size net['pore.inscribed_diameter'] = np.copy(p_dia_local) * voxel_size net['pore.equivalent_diameter'] = 2 * ( (3 / 4 * net['pore.volume'] / np.pi)**(1 / 3)) net['pore.extended_diameter'] = np.copy(p_dia_global) * voxel_size net['pore.surface_area'] = np.copy(p_area_surf) * (voxel_size)**2 net['throat.diameter'] = np.array(t_dia_inscribed) * voxel_size net['throat.inscribed_diameter'] = np.array(t_dia_inscribed) * voxel_size net['throat.area'] = np.array(t_area) * (voxel_size**2) net['throat.perimeter'] = np.array(t_perimeter) * voxel_size net['throat.equivalent_diameter'] = (np.array(t_area) * (voxel_size**2))**0.5 P12 = net['throat.conns'] PT1 = np.sqrt( np.sum(((p_coords[P12[:, 0]] - t_coords) * voxel_size)**2, axis=1)) PT2 = np.sqrt( np.sum(((p_coords[P12[:, 1]] - t_coords) * voxel_size)**2, axis=1)) net['throat.total_length'] = PT1 + PT2 PT1 = PT1 - p_dia_local[P12[:, 0]] / 2 * voxel_size PT2 = PT2 - p_dia_local[P12[:, 1]] / 2 * voxel_size net['throat.length'] = PT1 + PT2 dist = (p_coords[P12[:, 0]] - p_coords[P12[:, 1]]) * voxel_size net['throat.direct_length'] = np.sqrt(np.sum(dist**2, axis=1)) # Make a dummy openpnm network to get the conduit lengths pn = op.network.GenericNetwork() pn.update(net) pn.add_model(propname='throat.endpoints', model=op_gm.throat_endpoints.spherical_pores, pore_diameter='pore.inscribed_diameter', throat_diameter='throat.inscribed_diameter') pn.add_model(propname='throat.conduit_lengths', model=op_gm.throat_length.conduit_lengths) pn.add_model(propname='pore.area', model=op_gm.pore_area.sphere) net['throat.endpoints.head'] = pn['throat.endpoints.head'] net['throat.endpoints.tail'] = pn['throat.endpoints.tail'] net['throat.conduit_lengths.pore1'] = pn['throat.conduit_lengths.pore1'] net['throat.conduit_lengths.pore2'] = pn['throat.conduit_lengths.pore2'] net['throat.conduit_lengths.throat'] = pn['throat.conduit_lengths.throat'] net['pore.area'] = pn['pore.area'] prj = pn.project prj.clear() wrk = op.Workspace() wrk.close_project(prj) return net