Skip to content

Commit faa83c8

Browse files
authored
Merge pull request FABLE-3DXRD#576 from CiolJegou/fix-rod2quat
KAM + gb misorientation parallel
2 parents 8617838 + ac65109 commit faa83c8

1 file changed

Lines changed: 298 additions & 0 deletions

File tree

ImageD11/forward_model/grainmaps.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
13061604
class Symmetry(enum.Enum):
13071605
"""

0 commit comments

Comments
 (0)