@@ -1301,7 +1301,305 @@ def get_mean_rod(rod, kmeans_flag=False, auto_check = True):
13011301
13021302 return rod_mean
13031303
1304+ def gaussian (x ,mu = 0 ,std = 1 ):
1305+ """
1306+ gaussian function
1307+ """
1308+ g = 1 / (std * (2 * np .pi )** 0.5 )* np .exp (- 0.5 * (x - mu )** 2 / std ** 2 )
1309+ return g
1310+
1311+ def KAM_kernel (kernel_size = 5 , kernel_cutoff = 50 ):
1312+ """
1313+ create a gaussian kernel of KxKxK dimensions, binarized it upon thresh value (%)
1314+ TODO: non-square kernel size,
1315+ Args:
1316+ kernel_size -- int, size of the kernel
1317+ kernel_cutoff -- int, threshold value for the gaussian cutoff (in percentage)
1318+ Returns:
1319+ kernel_bin -- binarized kernel for KAM computation
1320+ """
1321+ x = np .linspace (- int (kernel_size / 2 ), int (kernel_size / 2 ), kernel_size )
1322+ #Create a 3d grid with points coordinates
1323+ kernel_grid = np .meshgrid (x ,x ,x )
1324+ #Create the final kernel shape
1325+ kernel = np .zeros (kernel_grid [0 ].shape )
1326+ for i in range (kernel .shape [0 ]):
1327+ for j in range (kernel .shape [1 ]):
1328+ for k in range (kernel .shape [2 ]):
1329+ x_coo = kernel_grid [0 ][i ,j ,k ]
1330+ y_coo = kernel_grid [1 ][i ,j ,k ]
1331+ z_coo = kernel_grid [2 ][i ,j ,k ]
1332+ dist_from_center = np .linalg .norm ((x_coo , y_coo , z_coo ))
1333+ gauss_value = gaussian (dist_from_center )
1334+ kernel [i ,j ,k ] = gauss_value
1335+ cutoff = np .percentile (kernel , kernel_cutoff )
1336+ kernel_bin = np .where (kernel > cutoff , 1 , 0 )
1337+ return kernel_bin
1338+
1339+ def find_neighbors (voxel_coo , kernel ):
1340+ """
1341+ Find all the neighbors coordinates of one pixel given a kernel
1342+ Args:
1343+ voxel_coo -- coordinates of the xovel (Z,Y,X)
1344+ kernel -- (KxKxK) matrix
1345+ Returns:
1346+ coo_neighbors -- list of coordinates
1347+ """
1348+ coo_neighbors = np .argwhere (abs (kernel ) == 1 ) + voxel_coo + np .array ([- int (kernel .shape [0 ]/ 2 ),- int (kernel .shape [1 ]/ 2 ),- int (kernel .shape [2 ]/ 2 )])
1349+ return coo_neighbors
1350+
1351+ def DS_KAM_parallel (DS , kernel_size = 5 , kernel_cutoff = 50 , crystal_system = 'cubic' ,
1352+ misorientation_threshold = (0 , 10 ), fill_value = np .nan , n_jobs = - 1 ,
1353+ Umis = True ):
1354+ """
1355+ compute Kernal Average Misorientation (KAM) on the grainmap (parallelized with joblib)
1356+ Args:
1357+ DS -- grain map
1358+ kernel_size -- int, size of the kernel
1359+ kernel_cutoff -- int, threshold value for the gaussian cutoff (in percentage)
1360+ crystal_system -- crystal system name as str
1361+ misorientation_threshold -- tuple, Reject misorientations outside specified range.
1362+ fill_value -- To put where mask is false. Defaults to np.nan.
1363+ n_jobs -- Number of parallel jobs. -1 uses all available cores.
1364+ Umis -- Use Umis instead of disori list
1365+ Returns:
1366+ KAM -- KAM map in degrees (Z,Y,X)
1367+ """
1368+ if isinstance (kernel_size , int ):
1369+ kernel = KAM_kernel (kernel_size , kernel_cutoff )
1370+ else :
1371+ raise ValueError ('{} is not supported for kernel size, must be an int.' .format (kernel_size ))
1372+ if crystal_system in ['cubic' , 'hexagonal' , 'orthorhombic' , 'tetragonal' , 'trigonal' , 'monoclinic' , 'triclinic' ]:
1373+ print ('{} is OK' .format (crystal_system ))
1374+ crystal_structure = Symmetry [crystal_system ]
1375+ else :
1376+ raise ValueError ('{} is not supported.' .format (crystal_system ))
1377+ if Umis :
1378+ Umis_list = ['triclinic' , 'monoclinic' , 'orthorombic' , 'tetragonal' ,'trigonal' , 'hexagonal' , 'cubic' ]
1379+ n_Umis = Umis_list .index (crystal_system .lower ())+ 1
1380+ try :
1381+ from xfab .symmetry import Umis
1382+ except ImportError :
1383+ raise ImportError ('Can not import Umis from xfab, defaulting to disori_list' )
1384+ Umis = False
1385+ assert 'labels' in DS .keys (), 'DS keys must contain "labels"'
1386+ mask = np .where (DS ['labels' ] > - 1 , 1 , 0 )
1387+ lower_bound = misorientation_threshold [0 ]
1388+ upper_bound = misorientation_threshold [1 ]
1389+
1390+ #### Adding some padding ####
1391+ pad_width = [(int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 0 (Z axis) --> add 1 slice before and 1 after
1392+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 1
1393+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 2
1394+ (0 , 0 ), # axis 3
1395+ (0 , 0 )] # axis 4
1396+ pad_width_mask = [(int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 0 --> add 1 slice before and 1 after
1397+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 1
1398+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 ))] # axis 2
1399+ orientation_map = np .pad (DS ['U' ], pad_width , mode = 'constant' , constant_values = 0 )
1400+ mask = np .pad (mask , pad_width_mask , mode = 'constant' , constant_values = False )
1401+ q = kernel .shape [0 ] // 2
1402+ m = kernel .shape [1 ] // 2
1403+ n = kernel .shape [2 ] // 2
1404+ min_angle = misorientation_threshold [0 ]
1405+ max_angle = misorientation_threshold [1 ]
1406+ # Using disorientation_list
1407+ if Umis == False :
1408+ # --- Collect all valid voxel coordinates ---
1409+ valid_coords = [
1410+ (k , i , j )
1411+ for k in range (q , orientation_map .shape [0 ] - q )
1412+ for i in range (m , orientation_map .shape [1 ] - m )
1413+ for j in range (n , orientation_map .shape [2 ] - n )
1414+ if mask [k , i , j ]
1415+ ]
1416+ def process_voxel (k , i , j , min_angle , max_angle , fill_value = np .nan ):
1417+ """For one voxel: gather neighbors, call disorientation_list, return mean angle."""
1418+ coo_neighbors = find_neighbors (np .array ([k , i , j ]), kernel )
1419+ center = orientation_map [k , i , j ] # (3, 3)
1420+ neighbors = np .array ([orientation_map [c [0 ], c [1 ], c [2 ]].T
1421+ for c in coo_neighbors ]) # (K, 3, 3)
1422+ centers = np .tile (center .T , (len (neighbors ), 1 , 1 )) # (K, 3, 3)
1423+ angles , _ , _ = disorientation_list (centers , neighbors , crystal_structure )
1424+ angles = np .rad2deg (np .array (angles ))
1425+
1426+ for g in range (len (angles )):
1427+ if np .allclose (neighbors [g ], 0 ) or np .allclose (centers [g ], 0 ):
1428+ angles [g ] = np .nan
1429+ thresh_angle = angles [(angles >= min_angle ) & (angles <= max_angle )];
1430+ angle_mean = np .nanmean (thresh_angle )
1431+ if angle_mean >= min_angle and angle_mean <= max_angle :
1432+ return (k , i , j , angle_mean , coo_neighbors )
1433+ else :
1434+ return (k , i , j , fill_value , coo_neighbors )
1435+ print ("Processing {} valid voxels with n_jobs={}..." .format (len (valid_coords ), n_jobs ))
1436+ results = Parallel (n_jobs = n_jobs , verbose = 5 )(
1437+ delayed (process_voxel )(k , i , j , min_angle , max_angle , fill_value ) for k , i , j in valid_coords
1438+ )
1439+ # build kammap
1440+ kam_map = np .full (orientation_map .shape [:3 ], fill_value )
1441+ for k , i , j , val , coo in results :
1442+ kam_map [k , i , j ] = val
1443+ else :
1444+ # Each valid voxel contributes K pairs (one per neighbor)
1445+ pairs = [
1446+ (k , i , j , orientation_map [k , i , j ], orientation_map [c [0 ], c [1 ], c [2 ]])
1447+ for k in range (q , orientation_map .shape [0 ] - q )
1448+ for i in range (m , orientation_map .shape [1 ] - m )
1449+ for j in range (n , orientation_map .shape [2 ] - n )
1450+ if mask [k , i , j ]
1451+ for c in find_neighbors (np .array ([k , i , j ]), kernel )
1452+ ]
1453+ print ("Processing {} valid voxels with n_jobs={}..." .format (len (pairs ), n_jobs ))
1454+ def compute_umis (k , i , j , U_center , U_neighbor , min_angle , max_angle , fill_value ):
1455+ if np .allclose (U_center , 0 ) or np .allclose (U_neighbor , 0 ):
1456+ return (k , i , j , fill_value )
1457+
1458+ angle = Umis (U_center , U_neighbor , n_Umis )[:,1 ].min ()
1459+ if angle >= min_angle and angle <= max_angle :
1460+ return (k , i , j , angle )
1461+ else :
1462+ return (k , i , j , fill_value )
1463+ results = Parallel (n_jobs = n_jobs , verbose = 5 )(
1464+ delayed (compute_umis )(k , i , j , U_c , U_n , min_angle , max_angle , fill_value ) for k , i , j , U_c , U_n in pairs
1465+ )
1466+ # --- Accumulate angles per voxel then average ---
1467+ from collections import defaultdict
1468+ voxel_angles = defaultdict (list )
1469+ #each voxel position is associated with the misroi angle corresponding
1470+ for k , i , j , angle in results :
1471+ voxel_angles [(k , i , j )].append (angle )
1472+ kam_map = np .full (orientation_map .shape [:3 ], fill_value )
1473+ for (k , i , j ), angles in voxel_angles .items ():
1474+ kam_map [k , i , j ] = np .nanmean (angles )
1475+
1476+ return kam_map [int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 , int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 , int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 ]
13041477
1478+ def get_boundary_voxels (label_map ):
1479+ """
1480+ Given a 3D label map of shape (Z, Y, X), return a binary volume
1481+ where True marks voxels on the boundary between two different labels.
1482+ Inputs:
1483+ label_map : shape (Z, Y, X) Integer label volume.
1484+ Outputs:
1485+ boundary : shape (Z, Y, X)
1486+ """
1487+ L = label_map
1488+ boundary = np .zeros (L .shape , dtype = bool )
1489+ # # Along Z
1490+ # boundary[:-1, :, :] |= (L[:-1, :, :] != L[1:, :, :])
1491+ # Along Y
1492+ boundary [:, :- 1 , :] |= (L [:, :- 1 , :] != L [:, 1 :, :])
1493+ # Along X
1494+ boundary [:, :, :- 1 ] |= (L [:, :, :- 1 ] != L [:, :, 1 : ])
1495+ return boundary
1496+
1497+ def DS_misori_parallel (DS , crystal_system = 'cubic' , kernel_size = 5 , fill_value = np .nan , n_jobs = - 1 ,Umis = True ):
1498+ """
1499+ compute Kernal Average Misorientation (KAM) on the grainmap (parallelized with joblib)
1500+ Args:
1501+ DS -- grain map
1502+ kernel_size -- int, size of the kernel
1503+ crystal_system -- crystal system name as str
1504+ fill_value -- To put where mask is false. Defaults to np.nan.
1505+ n_jobs -- Number of parallel jobs. -1 uses all available cores.
1506+ Umis -- Use Umis instead of disori list
1507+ Returns:
1508+ gb_mismap -- gb_mismap map in degrees (Z,Y,X)
1509+ """
1510+ if crystal_system in ['cubic' , 'hexagonal' , 'orthorhombic' , 'tetragonal' , 'trigonal' , 'monoclinic' , 'triclinic' ]:
1511+ print ('{} is OK' .format (crystal_system ))
1512+ crystal_structure = Symmetry [crystal_system ]
1513+ else :
1514+ raise ValueError ('{} is not supported.' .format (crystal_system ))
1515+ if Umis :
1516+ Umis_list = ['triclinic' , 'monoclinic' , 'orthorombic' , 'tetragonal' ,'trigonal' , 'hexagonal' , 'cubic' ]
1517+ n_Umis = Umis_list .index (crystal_system .lower ())+ 1
1518+ try :
1519+ from xfab .symmetry import Umis
1520+ except ImportError :
1521+ raise ImportError ('Can not import Umis from xfab, defaulting to disori_list' )
1522+ Umis = False
1523+ gb = get_boundary_voxels (DS ['labels' ])
1524+ kernel = KAM_kernel (kernel_size )
1525+ q = kernel_size // 2
1526+ m = kernel_size // 2
1527+ n = kernel_size // 2
1528+ #### Adding some padding ####
1529+ pad_width = [(int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 0 (Z axis) --> add 1 slice before and 1 after
1530+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 1
1531+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 2
1532+ (0 , 0 ), # axis 3
1533+ (0 , 0 )] # axis 4
1534+ pad_width_gb = [(int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 0 --> add 1 slice before and 1 after
1535+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 )), # axis 1
1536+ (int (kernel_size / 2 + 1 ), int (kernel_size / 2 + 1 ))] # axis 2
1537+ orientation_map = np .pad (DS ['U' ], pad_width , mode = 'constant' , constant_values = 0 )
1538+ gb = np .pad (gb , pad_width_gb , mode = 'constant' , constant_values = 0 )
1539+ if Umis == False :
1540+ # --- Collect all voxel coordinates on GB---
1541+ valid_coords = [
1542+ (k , i , j )
1543+ for k in range (q , orientation_map .shape [0 ] - q )
1544+ for i in range (m , orientation_map .shape [1 ] - m )
1545+ for j in range (n , orientation_map .shape [2 ] - n )
1546+ if gb [k , i , j ]
1547+ ]
1548+ def process_voxel (k , i , j ):
1549+ """For one voxel: gather neighbors, call disorientation_list, return mean angle."""
1550+ coo_neighbors = find_neighbors (np .array ([k , i , j ]), kernel )
1551+ center = orientation_map [k , i , j ] # (3, 3)
1552+ neighbors = np .array ([orientation_map [c [0 ], c [1 ], c [2 ]].T
1553+ for c in coo_neighbors ]) # (K, 3, 3)
1554+ centers = np .tile (center .T , (len (neighbors ), 1 , 1 )) # (K, 3, 3)
1555+
1556+ angles , _ , _ = disorientation_list (centers , neighbors , crystal_structure )
1557+ angles = np .rad2deg (np .array (angles ))
1558+
1559+ for g in range (len (angles )):
1560+ if np .allclose (neighbors [g ], 0 ) or np .allclose (centers [g ], 0 ):
1561+ angles [g ] = np .nan
1562+ # angles[(angles >= min_angle) & (angles <= max_angle)];
1563+ angle_mean = np .nanmean (angles )
1564+ return (k , i , j , angle_mean )
1565+ print ("Processing {} valid voxels with n_jobs={}..." .format (len (valid_coords ), n_jobs ))
1566+ results = Parallel (n_jobs = n_jobs , verbose = 5 )(
1567+ delayed (process_voxel )(k , i , j ) for k , i , j in valid_coords
1568+ )
1569+ # build gb_mis_map
1570+ gb_mis_map = np .full (orientation_map .shape [:3 ], fill_value )
1571+ for k , i , j , val in results :
1572+ gb_mis_map [k , i , j ] = val
1573+ else :
1574+ # Each valid voxel contributes K pairs (one per neighbor)
1575+ pairs = [
1576+ (k , i , j , orientation_map [k , i , j ], orientation_map [c [0 ], c [1 ], c [2 ]])
1577+ for k in range (q , orientation_map .shape [0 ] - q )
1578+ for i in range (m , orientation_map .shape [1 ] - m )
1579+ for j in range (n , orientation_map .shape [2 ] - n )
1580+ if gb [k , i , j ]
1581+ for c in find_neighbors (np .array ([k , i , j ]), kernel )
1582+ ]
1583+ print ("Processing {} valid voxels with n_jobs={}..." .format (len (pairs ), n_jobs ))
1584+ def compute_umis (k , i , j , U_center , U_neighbor ):
1585+ if np .allclose (U_center , 0 ) or np .allclose (U_neighbor , 0 ):
1586+ return (k , i , j , np .nan )
1587+ angle = Umis (U_center , U_neighbor , n_Umis )[:,1 ].min ()
1588+ return (k , i , j , angle )
1589+ results = Parallel (n_jobs = n_jobs , verbose = 5 )(
1590+ delayed (compute_umis )(k , i , j , U_c , U_n ) for k , i , j , U_c , U_n in pairs
1591+ )
1592+ #--- Accumulate angles per voxel then average ---
1593+ from collections import defaultdict
1594+ voxel_angles = defaultdict (list )
1595+ #each voxel position is associated with the misroi angle corresponding
1596+ for k , i , j , angle in results :
1597+ voxel_angles [(k , i , j )].append (angle )
1598+ gb_mis_map = np .full (orientation_map .shape [:3 ], fill_value )
1599+ for (k , i , j ), angles in voxel_angles .items ():
1600+ gb_mis_map [k , i , j ] = np .nanmean (angles )
1601+ return gb_mis_map [int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 , int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 , int (kernel_size / 2 )+ 1 : - int (kernel_size / 2 )- 1 ]
1602+
13051603# this class comes from pymicro/crystal/lattice.py
13061604class Symmetry (enum .Enum ):
13071605 """
0 commit comments