def cut_slit(data,slit_number): slit_number -= 1 data[scipy.isnan(data)] = 0. slice = data.sum(axis=1) # Find bottom of good data bottom = 0 while slice[bottom]==0: bottom += 1 slice = scipy.trim_zeros(slice) zeros = scipy.where(slice==0) zeros = zeros[0]+bottom # Special case for images with only one slit if zeros.size==0: # Deal with upper zeros slice = data.sum(axis=1) top = bottom while top<slice.size and slice[top]!=0: top += 1 slice = data.sum(axis=0) indx = scipy.where(slice!=0) start = indx[0][0] end = indx[0][-1]+1 return data[bottom:top,start:end],start,bottom borders = scipy.array_split(zeros,zeros.size/5) if slit_number>len(borders): return scipy.asarray(0),0,0 if slit_number==0: start = 0 end = borders[0][0] elif slit_number==len(borders): start = borders[slit_number-1][4]+1 end = slice.size else: start = borders[slit_number-1][4]+1 end = borders[slit_number][0] data = data[start:end] bottom = start slice = data.sum(axis=0) indx = scipy.where(slice!=0) start = indx[0][0] end = indx[0][-1]+1 return data[:,start:end],start,bottom
def _create_alias_map(im, alias=None): r""" Creates an alias mapping between phases in original image and identifyable names. This mapping is used during network extraction to label interconnection between and properties of each phase. Parameters ---------- im : ND-array Image of porous material where each phase is represented by unique integer. Phase integer should start from 1. Boolean image will extract only one network labeled with True's only. alias : dict (Optional) A dictionary that assigns unique image label to specific phase. 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 with numerical phase labels as key, and readable phase names as valuies. If no alias is provided then default labelling is used i.e {1: 'Phase1',..} """ # ------------------------------------------------------------------------- # Get alias if provided by user phases_num = sp.unique(im * 1) phases_num = sp.trim_zeros(phases_num) al = {} for values in phases_num: al[values] = 'phase{}'.format(values) if alias is not None: alias_sort = dict(sorted(alias.items())) phase_labels = sp.array([*alias_sort]) al = alias if set(phase_labels) != set(phases_num): raise Exception('Alias labels does not match with image labels ' 'please provide correct image labels') return al
def solve(d,orders): path = os.path.split(__file__)[0] lines = {} lines['cuar'] = numpy.loadtxt(path+"/data/cuar.lines") lines['hgne'] = numpy.loadtxt(path+"/data/hgne.lines") lines['xe'] = numpy.loadtxt(path+"/data/xe.lines") #startsoln = numpy.load(path+"/data/test_wavesol.dat", # allow_pickle=True) startsoln = numpy.load(path+"/data/esi_wavesolution.dat", allow_pickle=True) #arclist = d.keys() # Under python 3 this does not produce a list # and so arclist[0] below fails arclist = list(d) arclist.sort() soln = [] if d[arclist[0]].shape[1]>3000: xvals = numpy.arange(4096.) cuslice = slice(3860,3940) fw1 = 75. fw2 = 9 else: xvals = numpy.arange(1.,4096.,2.) cuslice = slice(1930,1970) fw1 = 37. fw2 = 5 """ Do a temporary kludge. In some cases, the finding of the orders fails, and only 9 orders are found. In this case, we need to skip the first of the orders """ if len(orders) == 9: ordstart = 1 dord = 1 else: ordstart = 0 dord = 0 for i in range(ordstart, 10): solution = startsoln[i] start,end = orders[(i-dord)] peaks = {} trace = {} fitD = {} WIDTH = 4 import pylab for arc in arclist: data = numpy.nanmedian(d[arc][start:end],axis=0) data[scipy.isnan(data)] = 0. if i==0 and arc=='cuar': data[cuslice] = numpy.median(data) trace[arc] = data.copy() bak = ndimage.percentile_filter(data,50.,int(fw1)) bak = getContinuum(bak,40.) data -= bak fitD[arc] = data/d[arc][start:end].std(0) p = ndimage.maximum_filter(data,fw2) std = clip(scipy.trim_zeros(data),3.)[1] nsig = ndimage.uniform_filter((data>7.*std)*1.,3) peak = scipy.where((nsig==1)&(p>10.*std)&(p==data))[0] peaks[arc] = [] for p in peak: if p-WIDTH<0 or p+WIDTH+1>xvals.size: continue x = xvals[p-WIDTH:p+WIDTH+1].copy()#-xvals[p] f = data[p-WIDTH:p+WIDTH+1].copy() fitdata = scipy.array([x,f]).T fit = scipy.array([0.,f.max(),xvals[p],1.]) fit,chi = sf.ngaussfit(fitdata,fit,weight=1) peaks[arc].append(fit[2])#+xvals[p]) for converge in range(15): wave = 10**sf.genfunc(xvals,0.,solution) refit = [] corrA = {} err = wave[int(wave.size/2)]-wave[int(wave.size/2-1)] for arc in arclist: corr = [] p = 10.**sf.genfunc(peaks[arc],0.,solution) for k in range(p.size): cent = p[k] diff = cent-lines[arc] corr.append(diff[abs(diff).argmin()]) corr = numpy.array(corr) if corr.size<4: continue m,s = clip(corr) corr = numpy.median(corr[abs(corr-m)<5.*s]) print(corr) corrA[arc] = corr # corr = m #for arc in arclist: p = 10.**sf.genfunc(peaks[arc],0.,solution) for k in range(p.size): pos = peaks[arc][k] cent = p[k] diff = abs(cent-lines[arc]-corr) if diff.min()<2.*err: refit.append([pos,lines[arc][diff.argmin()]]) refit = scipy.asarray(refit) solution = sf.lsqfit(refit,'polynomial',3) refit = [] err = solution['coeff'][1] for arc in arclist: data = trace[arc] for pos in peaks[arc]: cent = sf.genfunc(pos,0.,solution) delta = 1e9 match = None for j in lines[arc]: diff = abs(cent-j) if diff<delta and diff<1.*err: delta = diff match = j if match is not None: refit.append([pos,match]) refit = scipy.asarray(refit) refit[:,1] = numpy.log10(refit[:,1]) solution = sf.lsqfit(refit,'chebyshev',3) #refit[:,0],refit[:,1] = refit[:,1].copy(),refit[:,0].copy() #refit = numpy.array([refit[:,1],refit[:,0]]).T #refit = refit[:,::-1] solution2 = sf.lsqfit(refit[:,::-1],'chebyshev',3) #soln.append([solution,solution2]) w = 10**sf.genfunc(xvals,0.,solution) if (w==wave).all() or converge>8: print("Order %d converged in %d iterations"%(i,converge)) soln.append([solution,solution2]) break for arc in arclist: pylab.plot(w,trace[arc]) pylab.plot(w,fitD[arc]) pp = 10**sf.genfunc(peaks[arc],0.,solution) for p in pp: pylab.axvline(p,c='k') for j in 10**refit[:,1]: if j>w[0] and j<w[-1]: pylab.axvline(j) pylab.show() break return soln
def snow_dual(im, voxel_size=1, boundary_faces=['top', 'bottom', 'left', 'right', 'front', 'back'], marching_cubes_area=False): r""" Extracts a dual pore and solid network from a binary image using a modified version of the SNOW algorithm Parameters ---------- im : ND-array Binary image in the Boolean form with True’s as void phase and False’s as solid phase. It can process the inverted configuration of the boolean image as well, but output labelling of phases will be inverted and solid phase properties will be assigned to void phase properties labels which will cause confusion while performing the simulation. 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 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. References ---------- [1] Gostick, J. "A versatile and efficient network extraction algorithm using marker-based watershed segmenation". Phys. Rev. E 96, 023307 (2017) [2] Khan, ZA et al. "Dual network extraction algorithm to investigate multiple transport processes in porous materials: Image-based modeling of pore and grain-scale processes. Computers and Chemical Engineering. 123(6), 64-77 (2019) """ # ------------------------------------------------------------------------- # SNOW void phase pore_regions = snow_partitioning(im, return_all=True) # SNOW solid phase solid_regions = snow_partitioning(~im, return_all=True) # ------------------------------------------------------------------------- # Combined Distance transform of two phases. pore_dt = pore_regions.dt solid_dt = solid_regions.dt dt = pore_dt + solid_dt pore_peaks = pore_regions.peaks solid_peaks = solid_regions.peaks peaks = pore_peaks + solid_peaks # Calculates combined void and solid regions for dual network extraction pore_regions = pore_regions.regions solid_regions = solid_regions.regions pore_region = pore_regions*im solid_region = solid_regions*~im solid_num = sp.amax(pore_regions) solid_region = solid_region + solid_num solid_region = solid_region * ~im regions = pore_region + solid_region 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') else: dt = dt # ------------------------------------------------------------------------- # Extract void,solid 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, void to solid and solid to solid throat conns loc1 = net['throat.conns'][:, 0] < solid_num loc2 = net['throat.conns'][:, 1] >= solid_num loc3 = net['throat.conns'][:, 1] < b_num pore_solid_labels = loc1 * loc2 * loc3 loc4 = net['throat.conns'][:, 0] >= solid_num loc5 = net['throat.conns'][:, 0] < b_num solid_solid_labels = loc4 * loc2 * loc5 * loc3 loc6 = net['throat.conns'][:, 1] < solid_num pore_pore_labels = loc1 * loc6 loc7 = net['throat.conns'][:, 1] >= b_num boundary_throat_labels = loc5 * loc7 solid_labels = ((net['pore.label'] > solid_num) * ~ (net['pore.label'] > b_num)) boundary_labels = net['pore.label'] > b_num b_sa = sp.zeros(len(boundary_labels[boundary_labels == 1.0])) # ------------------------------------------------------------------------- # Calculates void interfacial area that connects with solid and vice versa p_conns = net['throat.conns'][:, 0][pore_solid_labels] ps = net['throat.area'][pore_solid_labels] p_sa = sp.bincount(p_conns, ps) s_conns = net['throat.conns'][:, 1][pore_solid_labels] s_pa = sp.bincount(s_conns, ps) s_pa = sp.trim_zeros(s_pa) # remove pore surface area labels p_solid_surf = sp.concatenate((p_sa, s_pa, b_sa)) # ------------------------------------------------------------------------- # Calculates interfacial area using marching cube method if marching_cubes_area: ps_c = net['throat.area'][pore_solid_labels] p_sa_c = sp.bincount(p_conns, ps_c) s_pa_c = sp.bincount(s_conns, ps_c) s_pa_c = sp.trim_zeros(s_pa_c) # remove pore surface area labels p_solid_surf = sp.concatenate((p_sa_c, s_pa_c, b_sa)) # ------------------------------------------------------------------------- # Adding additional information of dual network net['pore.solid_void_area'] = (p_solid_surf * voxel_size**2) net['throat.void'] = pore_pore_labels net['throat.interconnect'] = pore_solid_labels net['throat.solid'] = solid_solid_labels net['throat.boundary'] = boundary_throat_labels net['pore.void'] = net['pore.label'] <= solid_num net['pore.solid'] = solid_labels net['pore.boundary'] = boundary_labels class network_dict(dict): pass net = network_dict(net) net.im = im net.dt = dt net.regions = regions net.peaks = peaks net.pore_dt = pore_dt net.pore_regions = pore_region net.pore_peaks = pore_peaks net.solid_dt = solid_dt net.solid_regions = solid_region net.solid_peaks = solid_peaks return net
def cut_slit(data, slit_number): """ cut_slit(data,slit_number) Find slit in 2d mask image. Inputs: data - 2d array of mask image slit_number - the number of the slit to cut out (with 1 as the bottom) Outputs: 2d array of slit data, left index of slit, bottom index of slit """ data = data.copy() slit_number -= 1 data[scipy.isnan(data)] = 0. slice = data.sum(axis=1) # Find bottom of good data bottom = 0 while slice[bottom] == 0: bottom += 1 slice = scipy.trim_zeros(slice) zeros = scipy.where(slice == 0) zeros = zeros[0] + bottom # Special case for images with only one slit if zeros.size == 0: # Deal with upper zeros slice = data.sum(axis=1) top = bottom while top < slice.size and slice[top] != 0: top += 1 slice = data.sum(axis=0) indx = scipy.where(slice != 0) start = indx[0][0] end = indx[0][-1] + 1 return data[bottom:top, start:end], start, bottom borders = [] start = 0 for i in range(zeros.size - 1): if zeros[i] + 1 != zeros[i + 1]: borders.append(zeros[start:i + 1].copy()) start = i + 1 borders.append(zeros[start:].copy()) if slit_number > len(borders): return scipy.asarray(0), 0, 0 if slit_number == 0: start = 0 end = borders[0][0] elif slit_number == len(borders): start = borders[slit_number - 1][4] + 1 end = slice.size else: start = borders[slit_number - 1][4] + 1 end = borders[slit_number][0] data = data[start:end] bottom = start slice = data.sum(axis=0) indx = scipy.where(slice != 0) start = indx[0][0] end = indx[0][-1] + 1 return data[:, start:end], start, bottom
def add_phase_interconnections(net, snow_partitioning_n, voxel_size=1, marching_cubes_area=False, alias=None): r""" This function connects networks of two or more phases together by interconnecting neighbouring nodes inside different phases. The resulting network can be used for the study of transport and kinetics at interphase of two phases. Parameters ---------- network : 2D or 3D network A dictoionary containing structural information of two or more phases networks. The dictonary format must be same as porespy region_to_network function. snow_partitioning_n : tuple The output generated by snow_partitioning_n function. The tuple should have phases_max_labels and original image of material. 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. marching_cubes_area : bool If ``True`` then the surface area and interfacial area between regions will be causing 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 phase. 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 network information of individual and connected networks. The dictionary names use the OpenPNM convention so it may be converted directly to an OpenPNM network object using the ``update`` command. """ # ------------------------------------------------------------------------- # Get alias if provided by user im = snow_partitioning_n.im al = _create_alias_map(im, alias=alias) # ------------------------------------------------------------------------- # Find interconnection and interfacial area between ith and jth phases conns1 = net['throat.conns'][:, 0] conns2 = net['throat.conns'][:, 1] label = net['pore.label'] - 1 num = snow_partitioning_n.phase_max_label num = [0, *num] phases_num = sp.unique(im * 1) phases_num = sp.trim_zeros(phases_num) for i in phases_num: loc1 = sp.logical_and(conns1 >= num[i - 1], conns1 < num[i]) loc2 = sp.logical_and(conns2 >= num[i - 1], conns2 < num[i]) loc3 = sp.logical_and(label >= num[i - 1], label < num[i]) net['throat.{}'.format(al[i])] = loc1 * loc2 net['pore.{}'.format(al[i])] = loc3 if i == phases_num[-1]: loc4 = sp.logical_and(conns1 < num[-1], conns2 >= num[-1]) loc5 = label >= num[-1] net['throat.boundary'] = loc4 net['pore.boundary'] = loc5 for j in phases_num: if j > i: pi_pj_sa = sp.zeros_like(label) loc6 = sp.logical_and(conns2 >= num[j - 1], conns2 < num[j]) pi_pj_conns = loc1 * loc6 net['throat.{}_{}'.format(al[i], al[j])] = pi_pj_conns if any(pi_pj_conns): # --------------------------------------------------------- # Calculates phase[i] interfacial area that connects with # phase[j] and vice versa p_conns = net['throat.conns'][:, 0][pi_pj_conns] s_conns = net['throat.conns'][:, 1][pi_pj_conns] ps = net['throat.area'][pi_pj_conns] p_sa = sp.bincount(p_conns, ps) # trim zeros at head/tail position to avoid extra bins p_sa = sp.trim_zeros(p_sa) i_index = sp.arange(min(p_conns), max(p_conns) + 1) j_index = sp.arange(min(s_conns), max(s_conns) + 1) s_pa = sp.bincount(s_conns, ps) s_pa = sp.trim_zeros(s_pa) pi_pj_sa[i_index] = p_sa pi_pj_sa[j_index] = s_pa # --------------------------------------------------------- # Calculates interfacial area using marching cube method if marching_cubes_area: ps_c = net['throat.area'][pi_pj_conns] p_sa_c = sp.bincount(p_conns, ps_c) p_sa_c = sp.trim_zeros(p_sa_c) s_pa_c = sp.bincount(s_conns, ps_c) s_pa_c = sp.trim_zeros(s_pa_c) pi_pj_sa[i_index] = p_sa_c pi_pj_sa[j_index] = s_pa_c net['pore.{}_{}_area'.format(al[i], al[j])] = (pi_pj_sa * voxel_size ** 2) 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 = sp.unique(im).astype(int) phases_num = sp.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 snow_partitioning_n(im, r_max=4, sigma=0.4, return_all=True, mask=True, randomize=False, alias=None): r""" This function partitions an imaging oontain an arbitrary number of phases into regions using a marker-based watershed segmentation. Its an extension of snow_partitioning function with all phases partitioned together. Parameters ---------- im : ND-array Image of porous material where each phase is represented by unique integer starting from 1 (0's are ignored). r_max : scalar The radius of the spherical structuring element to use in the Maximum filter stage that is used to find peaks. The default is 4. sigma : scalar The standard deviation of the Gaussian filter used. The default is 0.4. If 0 is given then the filter is not applied, which is useful if a distance transform is supplied as the ``im`` argument that has already been processed. return_all : boolean (default is False) If set to ``True`` a named tuple is returned containing the original image, the combined distance transform, list of each phase max label, and the final combined regions of all phases. mask : boolean (default is True) Apply a mask to the regions which are not under concern. randomize : boolean If ``True`` (default), then the region colors will be randomized before returning. This is helpful for visualizing otherwise neighboring regions have similar coloring and are hard to distinguish. 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 ------- An image the same shape as ``im`` with the all phases partitioned into regions using a marker based watershed with the peaks found by the SNOW algorithm [1]. If ``return_all`` is ``True`` then a **named tuple** is returned with the following attribute: * ``im`` : The actual image of the porous material * ``dt`` : The combined distance transform of the image * ``phase_max_label`` : The list of max label of each phase in order to distinguish between each other * ``regions`` : The partitioned regions of n phases using a marker based watershed with the peaks found by the SNOW algorithm References ---------- [1] Gostick, J. "A versatile and efficient network extraction algorithm using marker-based watershed segmentation". Physical Review E. (2017) [2] Khan, ZA et al. "Dual network extraction algorithm to investigate multiple transport processes in porous materials: Image-based modeling of pore and grain-scale processes". Computers in Chemical Engineering. (2019) See Also ---------- snow_partitioning Notes ----- In principle it is possible to perform a distance transform on each phase separately, merge these into a single image, then apply the watershed only once. This, however, has been found to create edge artifacts between regions arising from the way watershed handles plateaus in the distance transform. To overcome this, this function applies the watershed to each of the distance transforms separately, then merges the segmented regions back into a single image. """ # Get alias if provided by user al = _create_alias_map(im=im, alias=alias) # Perform snow on each phase and merge all segmentation and dt together phases_num = sp.unique(im * 1) phases_num = sp.trim_zeros(phases_num) combined_dt = 0 combined_region = 0 num = [0] for i in phases_num: print('_' * 60) if alias is None: print('Processing Phase {}'.format(i)) else: print('Processing Phase {}'.format(al[i])) phase_snow = snow_partitioning(im == i, dt=None, r_max=r_max, sigma=sigma, return_all=return_all, mask=mask, randomize=randomize) if len(phases_num) == 1 and phases_num == 1: combined_dt = phase_snow.dt combined_region = phase_snow.regions else: combined_dt += phase_snow.dt phase_snow.regions *= phase_snow.im phase_snow.regions += num[i - 1] phase_ws = phase_snow.regions * phase_snow.im phase_ws[phase_ws == num[i - 1]] = 0 combined_region += phase_ws num.append(sp.amax(combined_region)) if return_all: tup = namedtuple('results', field_names=['im', 'dt', 'phase_max_label', 'regions']) tup.im = im tup.dt = combined_dt tup.phase_max_label = num[1:] tup.regions = combined_region return tup else: return combined_region
def snow_dual( im, voxel_size=1, boundary_faces=['top', 'bottom', 'left', 'right', 'front', 'back']): 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. It can process the inverted configuration of the boolean image as well, but output labelling of phases will be inverted and solid phase properties will be assigned to void phase properties labels which will cause confusion while performing the simulation. 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. Returns ------- 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. """ # ------------------------------------------------------------------------- # SNOW void phase pore_regions = snow_partitioning(im, return_all=True) # SNOW solid phase solid_regions = snow_partitioning(~im, return_all=True) # ------------------------------------------------------------------------- # Combined Distance transform of two phases. pore_dt = pore_regions.dt solid_dt = solid_regions.dt dt = pore_dt + solid_dt # Calculates combined void and solid regions for dual network extraction pore_regions = pore_regions.regions solid_regions = solid_regions.regions pore_region = pore_regions * im solid_region = solid_regions * ~im solid_num = sp.amax(pore_regions) solid_region = solid_region + solid_num solid_region = solid_region * ~im regions = pore_region + solid_region 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: faces = [(int('top' in f) * 3, int('bottom' in f) * 3), (int('left' in f) * 3, int('right' in f) * 3)] if im.ndim == 3: faces = [(int('front' in f) * 3, int('back' in f) * 3)] + faces dt = sp.pad(dt, pad_width=faces, mode='edge') else: dt = dt # ------------------------------------------------------------------------- # Extract void,solid and throat information from image net = regions_to_network(im=regions, dt=dt, voxel_size=voxel_size) # ------------------------------------------------------------------------- # Find void to void, void to solid and solid to solid throat conns loc1 = net['throat.conns'][:, 0] < solid_num loc2 = net['throat.conns'][:, 1] >= solid_num loc3 = net['throat.conns'][:, 1] < b_num pore_solid_labels = loc1 * loc2 * loc3 loc4 = net['throat.conns'][:, 0] >= solid_num loc5 = net['throat.conns'][:, 0] < b_num solid_solid_labels = loc4 * loc2 * loc5 * loc3 loc6 = net['throat.conns'][:, 1] < solid_num pore_pore_labels = loc1 * loc6 loc7 = net['throat.conns'][:, 1] >= b_num boundary_throat_labels = loc5 * loc7 solid_labels = ((net['pore.label'] > solid_num) * ~(net['pore.label'] > b_num)) boundary_labels = net['pore.label'] > b_num b_sa = sp.zeros(len(boundary_labels[boundary_labels == 1.0])) # ------------------------------------------------------------------------- # Calculates void interfacial area that connects with solid and vice versa p_conns = net['throat.conns'][:, 0][pore_solid_labels] ps = net['throat.area'][pore_solid_labels] p_sa = sp.bincount(p_conns, ps) s_conns = net['throat.conns'][:, 1][pore_solid_labels] s_pa = sp.bincount(s_conns, ps) s_pa = sp.trim_zeros(s_pa) # remove pore surface area labels p_solid_surf = sp.concatenate((p_sa, s_pa, b_sa)) # ------------------------------------------------------------------------- # Calculates interfacial area using marching cube method ps_c = net['throat.area_mc'][pore_solid_labels] p_sa_c = sp.bincount(p_conns, ps_c) s_pa_c = sp.bincount(s_conns, ps_c) s_pa_c = sp.trim_zeros(s_pa_c) # remove pore surface area labels p_solid_surf_c = sp.concatenate((p_sa_c, s_pa_c, b_sa)) # ------------------------------------------------------------------------- # Adding additional information of dual network net['pore.solid_IFA'] = p_solid_surf * voxel_size**2 net['pore.solid_IFA_mc'] = (p_solid_surf_c * voxel_size**2) net['throat.void'] = pore_pore_labels net['throat.interconnect'] = pore_solid_labels net['throat.solid'] = solid_solid_labels net['throat.boundary'] = boundary_throat_labels net['pore.void'] = net['pore.label'] <= solid_num net['pore.solid'] = solid_labels net['pore.boundary'] = boundary_labels return net