def snow(im, voxel_size=1, boundary_faces=['top', 'bottom', 'left', 'right', 'front', 'back'], marching_cubes_area=False): r""" Analyzes an image that has been partitioned into void and solid regions and extracts the void and solid phase geometry as well as network connectivity. Parameters ---------- im : ND-array Binary image in the Boolean form with True’s as void phase and False’s as solid phase. 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. boundary_faces : list of strings Boundary faces labels are provided to assign hypothetical boundary nodes having zero resistance to transport process. For cubical geometry, the user can choose ‘left’, ‘right’, ‘top’, ‘bottom’, ‘front’ and ‘back’ face labels to assign boundary nodes. If no label is assigned then all six faces will be selected as boundary nodes automatically which can be trimmed later on based on user requirements. marching_cubes_area : bool If ``True`` then the surface area and interfacial area between regions will be using the marching cube algorithm. This is a more accurate representation of area in extracted network, but is quite slow, so it is ``False`` by default. The default method simply counts voxels so does not correctly account for the voxelated nature of the images. Returns ------- A dictionary containing the void phase 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. * ``net``: A dictionary containing all the void and solid phase 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. * ``im``: The binary image of the void space * ``dt``: The combined distance transform of the image * ``regions``: The void and solid space partitioned into pores and solids phases using a marker based watershed with the peaks found by the SNOW Algorithm. """ # ------------------------------------------------------------------------- # SNOW void phase regions = snow_partitioning(im=im, return_all=True) im = regions.im dt = regions.dt regions = regions.regions b_num = np.amax(regions) # ------------------------------------------------------------------------- # Boundary Conditions regions = add_boundary_regions(regions=regions, faces=boundary_faces) # ------------------------------------------------------------------------- # Padding distance transform and image to extract geometrical properties dt = pad_faces(im=dt, faces=boundary_faces) im = pad_faces(im=im, faces=boundary_faces) regions = regions * im regions = make_contiguous(regions) # ------------------------------------------------------------------------- # Extract void and throat information from image net = regions_to_network(im=regions, dt=dt, voxel_size=voxel_size) # ------------------------------------------------------------------------- # Extract marching cube surface area and interfacial area of regions if marching_cubes_area: areas = region_surface_areas(regions=regions) interface_area = region_interface_areas(regions=regions, areas=areas, voxel_size=voxel_size) net['pore.surface_area'] = areas * voxel_size**2 net['throat.area'] = interface_area.area # ------------------------------------------------------------------------- # Find void to void connections of boundary and internal voids boundary_labels = net['pore.label'] > b_num loc1 = net['throat.conns'][:, 0] < b_num loc2 = net['throat.conns'][:, 1] >= b_num pore_labels = net['pore.label'] <= b_num loc3 = net['throat.conns'][:, 0] < b_num loc4 = net['throat.conns'][:, 1] < b_num net['pore.boundary'] = boundary_labels net['throat.boundary'] = loc1 * loc2 net['pore.internal'] = pore_labels net['throat.internal'] = loc3 * loc4 # ------------------------------------------------------------------------- # label boundary cells net = label_boundary_cells(network=net, boundary_faces=boundary_faces) # ------------------------------------------------------------------------- # assign out values to dummy dict temp = _net_dict(net) temp.im = im.copy() temp.dt = dt temp.regions = regions return temp
def snow(im, voxel_size=1, boundary_faces=['top', 'bottom', 'left', 'right', 'front', 'back'], marching_cubes_area=False): r""" Analyzes an image that has been partitioned into void and solid regions and extracts the void and solid phase geometry as well as network connectivity. Parameters ---------- im : ND-array Binary image in the Boolean form with True’s as void phase and False’s as solid phase. 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. boundary_faces : list of strings Boundary faces labels are provided to assign hypothetical boundary nodes having zero resistance to transport process. For cubical geometry, the user can choose ‘left’, ‘right’, ‘top’, ‘bottom’, ‘front’ and ‘back’ face labels to assign boundary nodes. If no label is assigned then all six faces will be selected as boundary nodes automatically which can be trimmed later on based on user requirements. marching_cubes_area : bool If ``True`` then the surface area and interfacial area between regions will be using the marching cube algorithm. This is a more accurate representation of area in extracted network, but is quite slow, so it is ``False`` by default. The default method simply counts voxels so does not correctly account for the voxelated nature of the images. Returns ------- A dictionary containing the void phase 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. """ # ------------------------------------------------------------------------- # SNOW void phase tup = snow_partitioning(im=im, return_all=True) im = tup.im dt = tup.dt regions = tup.regions peaks = tup.peaks b_num = sp.amax(regions) # ------------------------------------------------------------------------- # Boundary Conditions regions = add_boundary_regions(regions=regions, faces=boundary_faces) # ------------------------------------------------------------------------- # Padding distance transform to extract geometrical properties f = boundary_faces if f is not None: if im.ndim == 2: faces = [(int('left' in f) * 3, int('right' in f) * 3), (int(('front') in f) * 3 or int(('bottom') in f) * 3, int(('back') in f) * 3 or int(('top') in f) * 3)] if im.ndim == 3: faces = [(int('left' in f) * 3, int('right' in f) * 3), (int('front' in f) * 3, int('back' in f) * 3), (int('top' in f) * 3, int('bottom' in f) * 3)] dt = sp.pad(dt, pad_width=faces, mode='edge') im = sp.pad(im, pad_width=faces, mode='edge') else: dt = dt regions = regions * im regions = make_contiguous(regions) # ------------------------------------------------------------------------- # Extract void and throat information from image net = regions_to_network(im=regions, dt=dt, voxel_size=voxel_size) # ------------------------------------------------------------------------- # Extract marching cube surface area and interfacial area of regions if marching_cubes_area: areas = region_surface_areas(regions=regions) interface_area = region_interface_areas(regions=regions, areas=areas, voxel_size=voxel_size) net['pore.surface_area'] = areas * voxel_size**2 net['throat.area'] = interface_area.area # ------------------------------------------------------------------------- # Find void to void connections of boundary and internal voids boundary_labels = net['pore.label'] > b_num loc1 = net['throat.conns'][:, 0] < b_num loc2 = net['throat.conns'][:, 1] >= b_num pore_labels = net['pore.label'] <= b_num loc3 = net['throat.conns'][:, 0] < b_num loc4 = net['throat.conns'][:, 1] < b_num net['pore.boundary'] = boundary_labels net['throat.boundary'] = loc1 * loc2 net['pore.internal'] = pore_labels net['throat.internal'] = loc3 * loc4 # ------------------------------------------------------------------------- # label boundary pore faces if f is not None: coords = net['pore.coords'] condition = coords[net['pore.internal']] dic = { 'left': 0, 'right': 0, 'front': 1, 'back': 1, 'top': 2, 'bottom': 2 } if all(coords[:, 2] == 0): dic['top'] = 1 dic['bottom'] = 1 for i in f: if i in ['left', 'front', 'bottom']: net['pore.{}'.format(i)] = (coords[:, dic[i]] < min( condition[:, dic[i]])) elif i in ['right', 'back', 'top']: net['pore.{}'.format(i)] = (coords[:, dic[i]] > max( condition[:, dic[i]])) class network_dict(dict): pass net = network_dict(net) net.im = im net.dt = dt net.regions = regions net.peaks = peaks return net
def snow_n(im, voxel_size=1, boundary_faces=['top', 'bottom', 'left', 'right', 'front', 'back'], marching_cubes_area=False, alias=None): r""" Analyzes an image that has been segemented into N phases and extracts all a network for each of the N phases, including geometerical information as well as network connectivity between each phase. Parameters ---------- im : ND-array Image of porous material where each phase is represented by unique integer. Phase integer should start from 1 (0 is ignored) 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 always 1 unit lenth per voxel. boundary_faces : list of strings Boundary faces labels are provided to assign hypothetical boundary nodes having zero resistance to transport process. For cubical geometry, the user can choose ‘left’, ‘right’, ‘top’, ‘bottom’, ‘front’ and ‘back’ face labels to assign boundary nodes. If no label is assigned then all six faces will be selected as boundary nodes automatically which can be trimmed later on based on user requirements. marching_cubes_area : bool If ``True`` then the surface area and interfacial area between regions will be calculated using the marching cube algorithm. This is a more accurate representation of area in extracted network, but is quite slow, so it is ``False`` by default. The default method simply counts voxels so does not correctly account for the voxelated nature of the images. alias : dict (Optional) A dictionary that assigns unique image label to specific phases. For example {1: 'Solid'} will show all structural properties associated with label 1 as Solid phase properties. If ``None`` then default labelling will be used i.e {1: 'Phase1',..}. Returns ------- A dictionary containing all N phases 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. """ # ------------------------------------------------------------------------- # Get alias if provided by user al = _create_alias_map(im, alias=alias) # ------------------------------------------------------------------------- # Perform snow on each phase and merge all segmentation and dt together snow = snow_partitioning_n(im, r_max=4, sigma=0.4, return_all=True, mask=True, randomize=False, alias=al) # ------------------------------------------------------------------------- # Add boundary regions f = boundary_faces regions = add_boundary_regions(regions=snow.regions, faces=f) # ------------------------------------------------------------------------- # Padding distance transform to extract geometrical properties dt = pad_faces(im=snow.dt, faces=f) # ------------------------------------------------------------------------- # For only one phase extraction with boundary regions phases_num = np.unique(im).astype(int) phases_num = np.trim_zeros(phases_num) if len(phases_num) == 1: if f is not None: snow.im = pad_faces(im=snow.im, faces=f) regions = regions * (snow.im.astype(bool)) regions = make_contiguous(regions) # ------------------------------------------------------------------------- # Extract N phases sites and bond information from image net = regions_to_network(im=regions, dt=dt, voxel_size=voxel_size) # ------------------------------------------------------------------------- # Extract marching cube surface area and interfacial area of regions if marching_cubes_area: areas = region_surface_areas(regions=regions) interface_area = region_interface_areas(regions=regions, areas=areas, voxel_size=voxel_size) net['pore.surface_area'] = areas * voxel_size**2 net['throat.area'] = interface_area.area # ------------------------------------------------------------------------- # Find interconnection and interfacial area between ith and jth phases net = add_phase_interconnections(net=net, snow_partitioning_n=snow, marching_cubes_area=marching_cubes_area, alias=al) # ------------------------------------------------------------------------- # label boundary cells net = label_boundary_cells(network=net, boundary_faces=f) # ------------------------------------------------------------------------- temp = _net_dict(net) temp.im = im.copy() temp.dt = dt temp.regions = regions return temp
def add_boundary_regions(regions=None, faces=[ 'front', 'back', 'left', 'right', 'top', 'bottom' ]): r""" Given an image partitioned into regions, pads specified faces with new regions Parameters ---------- regions : ND-array An image of the pore space partitioned into regions and labeled faces : list of strings The faces of ``regions`` which should have boundaries added. Options are: *'right'* - Adds boundaries to the x=0 face (``im[0, :, :]``) *'left'* - Adds boundaries to the x=X face (``im[-1, :, :]``) *'front'* - Adds boundaries to the y=0 face (``im[:, ), :]``) *'back'* - Adds boundaries to the x=0 face (``im[:, -1, :]``) *'bottom'* - Adds boundaries to the x=0 face (``im[:, :, 0]``) *'top'* - Adds boundaries to the x=0 face (``im[:, :, -1]``) The default is all faces. Returns ------- image : ND-array A copy of ``regions`` with the specified boundaries added, so will be slightly larger in each direction where boundaries were added. """ # ------------------------------------------------------------------------- # Edge pad segmentation and distance transform if faces is not None: regions = np.pad(regions, 1, 'edge') # --------------------------------------------------------------------- if regions.ndim == 3: # Remove boundary nodes interconnection regions[:, :, 0] = regions[:, :, 0] + regions.max() regions[:, :, -1] = regions[:, :, -1] + regions.max() regions[0, :, :] = regions[0, :, :] + regions.max() regions[-1, :, :] = regions[-1, :, :] + regions.max() regions[:, 0, :] = regions[:, 0, :] + regions.max() regions[:, -1, :] = regions[:, -1, :] + regions.max() regions[:, :, 0] = (~find_boundaries( regions[:, :, 0], mode='outer')) * regions[:, :, 0] regions[:, :, -1] = (~find_boundaries( regions[:, :, -1], mode='outer')) * regions[:, :, -1] regions[0, :, :] = (~find_boundaries( regions[0, :, :], mode='outer')) * regions[0, :, :] regions[-1, :, :] = (~find_boundaries( regions[-1, :, :], mode='outer')) * regions[-1, :, :] regions[:, 0, :] = (~find_boundaries( regions[:, 0, :], mode='outer')) * regions[:, 0, :] regions[:, -1, :] = (~find_boundaries( regions[:, -1, :], mode='outer')) * regions[:, -1, :] # ----------------------------------------------------------------- regions = np.pad(regions, 2, 'edge') # Remove unselected faces if 'front' not in faces: regions = regions[:, 3:, :] # y if 'back' not in faces: regions = regions[:, :-3, :] if 'left' not in faces: regions = regions[3:, :, :] # x if 'right' not in faces: regions = regions[:-3, :, :] if 'bottom' not in faces: regions = regions[:, :, 3:] # z if 'top' not in faces: regions = regions[:, :, :-3] elif regions.ndim == 2: # Remove boundary nodes interconnection regions[0, :] = regions[0, :] + regions.max() regions[-1, :] = regions[-1, :] + regions.max() regions[:, 0] = regions[:, 0] + regions.max() regions[:, -1] = regions[:, -1] + regions.max() regions[0, :] = ( ~find_boundaries(regions[0, :], mode='outer')) * regions[0, :] regions[-1, :] = (~find_boundaries(regions[-1, :], mode='outer')) * regions[-1, :] regions[:, 0] = ( ~find_boundaries(regions[:, 0], mode='outer')) * regions[:, 0] regions[:, -1] = (~find_boundaries(regions[:, -1], mode='outer')) * regions[:, -1] # ----------------------------------------------------------------- regions = np.pad(regions, 2, 'edge') # Remove unselected faces if 'left' not in faces: regions = regions[3:, :] # x if 'right' not in faces: regions = regions[:-3, :] if 'front' not in faces and 'bottom' not in faces: regions = regions[:, 3:] # y if 'back' not in faces and 'top' not in faces: regions = regions[:, :-3] else: print('add_boundary_regions works only on 2D and 3D images') # --------------------------------------------------------------------- # Make labels contiguous regions = make_contiguous(regions) else: regions = regions return regions
def add_boundary_regions(regions=None, faces=['front', 'back', 'left', 'right', 'top', 'bottom']): # ------------------------------------------------------------------------- # Edge pad segmentation and distance transform if faces is not None: regions = sp.pad(regions, 1, 'edge') # --------------------------------------------------------------------- if regions.ndim == 3: # Remove boundary nodes interconnection regions[:, :, 0] = regions[:, :, 0] + regions.max() regions[:, :, -1] = regions[:, :, -1] + regions.max() regions[0, :, :] = regions[0, :, :] + regions.max() regions[-1, :, :] = regions[-1, :, :] + regions.max() regions[:, 0, :] = regions[:, 0, :] + regions.max() regions[:, -1, :] = regions[:, -1, :] + regions.max() regions[:, :, 0] = (~find_boundaries(regions[:, :, 0], mode='outer'))*regions[:, :, 0] regions[:, :, -1] = (~find_boundaries(regions[:, :, -1], mode='outer'))*regions[:, :, -1] regions[0, :, :] = (~find_boundaries(regions[0, :, :], mode='outer'))*regions[0, :, :] regions[-1, :, :] = (~find_boundaries(regions[-1, :, :], mode='outer'))*regions[-1, :, :] regions[:, 0, :] = (~find_boundaries(regions[:, 0, :], mode='outer'))*regions[:, 0, :] regions[:, -1, :] = (~find_boundaries(regions[:, -1, :], mode='outer'))*regions[:, -1, :] # --------------------------------------------------------------------- regions = sp.pad(regions, 2, 'edge') # Remove unselected faces if 'top' not in faces: regions = regions[:, 3:, :] if 'bottom' not in faces: regions = regions[:, :-3, :] if 'front' not in faces: regions = regions[3:, :, :] if 'back' not in faces: regions = regions[:-3, :, :] if 'left' not in faces: regions = regions[:, :, 3:] if 'right' not in faces: regions = regions[:, :, :-3] elif regions.ndim == 2: # Remove boundary nodes interconnection regions[0, :] = regions[0, :] + regions.max() regions[-1, :] = regions[-1, :] + regions.max() regions[:, 0] = regions[:, 0] + regions.max() regions[:, -1] = regions[:, -1] + regions.max() regions[0, :] = (~find_boundaries(regions[0, :], mode='outer'))*regions[0, :] regions[-1, :] = (~find_boundaries(regions[-1, :], mode='outer'))*regions[-1, :] regions[:, 0] = (~find_boundaries(regions[:, 0], mode='outer'))*regions[:, 0] regions[:, -1] = (~find_boundaries(regions[:, -1], mode='outer'))*regions[:, -1] # --------------------------------------------------------------------- regions = sp.pad(regions, 2, 'edge') # Remove unselected faces if 'top' not in faces: regions = regions[3:, :] if 'bottom' not in faces: regions = regions[:-3, :] if 'left' not in faces: regions = regions[:, 3:] if 'right' not in faces: regions = regions[:, :-3] else: print('add_boundary_regions works only on 2D and 3D images') # --------------------------------------------------------------------- # Make labels contiguous regions = make_contiguous(regions) else: regions = regions return regions