miblab-ssa 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
miblab_ssa/pdm.py ADDED
@@ -0,0 +1,177 @@
1
+ import numpy as np
2
+ import trimesh
3
+ from skimage.measure import marching_cubes
4
+ from scipy.interpolate import griddata
5
+ from scipy.ndimage import gaussian_filter, binary_dilation, binary_fill_holes
6
+
7
+ # ---------------------------------------------------------
8
+ # 1. Template & Mapping Utils
9
+ # ---------------------------------------------------------
10
+
11
+ def get_template_mesh(radius=1.0, subdivisions=3):
12
+ """
13
+ Returns the fixed topology icosphere.
14
+ """
15
+ mesh = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius)
16
+
17
+ # Pre-calculate the spherical coordinates (Theta, Phi) for this template ONCE.
18
+ # We will query these angles on the target kidney.
19
+ verts = mesh.vertices
20
+ # Normalize just in case
21
+ verts /= np.linalg.norm(verts, axis=1)[:, None]
22
+
23
+ # Convert vector -> angles
24
+ # Mapping (x, y, z) -> (x, z, y) so Z is poles is standard for many libs,
25
+ # but let's stick to standard math: Z is up.
26
+ Xc, Yc, Zc = verts[:,0], verts[:,1], verts[:,2]
27
+
28
+ # Clip z to [-1, 1] to avoid NaNs at poles
29
+ Theta = np.arccos(np.clip(Zc, -1, 1))
30
+ Phi = np.arctan2(Yc, Xc) % (2*np.pi)
31
+
32
+ return mesh, Theta, Phi
33
+
34
+ def get_inflated_mapping(verts, faces, iterations=80):
35
+ """
36
+ Iteratively smoothes the target mesh into a sphere.
37
+ This 'unwraps' the concave kidney so we can map it to the template.
38
+ """
39
+ smooth_verts = verts.copy()
40
+ n_verts = len(verts)
41
+
42
+ # 1. Build Adjacency (Edges)
43
+ edges = np.concatenate([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]])
44
+
45
+ # 2. Laplacian Smoothing (Inflation)
46
+ for _ in range(iterations):
47
+ # Calculate Face centers
48
+ face_centers = smooth_verts[faces].mean(axis=1)
49
+
50
+ # Accumulate neighbor forces
51
+ v_sums = np.zeros_like(smooth_verts)
52
+ v_counts = np.zeros(n_verts)
53
+
54
+ np.add.at(v_sums, faces, face_centers[:, None, :])
55
+ np.add.at(v_counts, faces, 1)
56
+
57
+ v_counts[v_counts==0] = 1
58
+ v_avgs = v_sums / v_counts[:, None]
59
+
60
+ # Update (Relax towards average)
61
+ smooth_verts = 0.5 * smooth_verts + 0.5 * v_avgs
62
+
63
+ # Re-project to unit sphere (The "Inflation" step)
64
+ smooth_verts -= smooth_verts.mean(axis=0)
65
+ norms = np.linalg.norm(smooth_verts, axis=1)
66
+ smooth_verts /= (norms[:, None] + 1e-9)
67
+
68
+ # 3. Get Angles of the inflated target
69
+ Xc, Yc, Zc = smooth_verts[:,0], smooth_verts[:,1], smooth_verts[:,2]
70
+ Theta = np.arccos(np.clip(Zc, -1, 1))
71
+ Phi = np.arctan2(Yc, Xc) % (2*np.pi)
72
+
73
+ return Theta, Phi
74
+
75
+ # ---------------------------------------------------------
76
+ # 2. Main API
77
+ # ---------------------------------------------------------
78
+
79
+ def features_from_mask(mask: np.ndarray, subdivisions=3):
80
+ """
81
+ Extracts PDM features by mapping the Template Mesh to the Target.
82
+ Preserves Concavities (Hollows).
83
+ """
84
+ # 1. Extract Target Mesh
85
+ # Blur slightly to get smooth normals
86
+ mask_float = gaussian_filter(mask.astype(float), sigma=1.0)
87
+ verts_target, faces_target, _, _ = marching_cubes(mask_float, level=0.5)
88
+
89
+ # 2. Center Target (Crucial for PCA)
90
+ centroid = verts_target.mean(axis=0)
91
+ verts_centered = verts_target - centroid
92
+
93
+ # 3. Inflate Target (Get its spherical address map)
94
+ # This finds the unique (Theta, Phi) for every vertex on the kidney
95
+ Theta_target, Phi_target = get_inflated_mapping(verts_centered, faces_target)
96
+
97
+ # 4. Get Template Angles
98
+ # We want to move the Template vertices to the corresponding locations
99
+ template_mesh, Theta_template, Phi_template = get_template_mesh(subdivisions=subdivisions)
100
+
101
+ # 5. Interpolate
102
+ # We use the Target's (Theta, Phi) -> (X, Y, Z) mapping
103
+ # to look up the (X, Y, Z) for the Template's (Theta, Phi).
104
+
105
+ # Fix the Seam at 2pi: Augment data
106
+ T_aug = np.concatenate([Theta_target, Theta_target, Theta_target])
107
+ P_aug = np.concatenate([Phi_target, Phi_target - 2*np.pi, Phi_target + 2*np.pi])
108
+ V_aug = np.concatenate([verts_centered, verts_centered, verts_centered])
109
+
110
+ points_in = np.column_stack((T_aug, P_aug))
111
+
112
+ # Look up X, Y, Z coordinates for the template vertices
113
+ feat_x = griddata(points_in, V_aug[:,0], (Theta_template, Phi_template), method='linear')
114
+ feat_y = griddata(points_in, V_aug[:,1], (Theta_template, Phi_template), method='linear')
115
+ feat_z = griddata(points_in, V_aug[:,2], (Theta_template, Phi_template), method='linear')
116
+
117
+ # Fill any interpolation gaps (poles or sparse areas)
118
+ mask_nan = np.isnan(feat_x)
119
+ if np.any(mask_nan):
120
+ feat_x[mask_nan] = griddata(points_in, V_aug[:,0], (Theta_template[mask_nan], Phi_template[mask_nan]), method='nearest')
121
+ feat_y[mask_nan] = griddata(points_in, V_aug[:,1], (Theta_template[mask_nan], Phi_template[mask_nan]), method='nearest')
122
+ feat_z[mask_nan] = griddata(points_in, V_aug[:,2], (Theta_template[mask_nan], Phi_template[mask_nan]), method='nearest')
123
+
124
+ # 6. Flatten to Feature Vector
125
+ # These are the vertices of the TEMPLATE, but moved to the KIDNEY surface
126
+ return np.column_stack((feat_x, feat_y, feat_z)).flatten()
127
+
128
+
129
+ def mask_from_features(features: np.ndarray, shape, subdivisions=3):
130
+ """
131
+ Reconstructs the mask. Since features are derived from a Template Mesh,
132
+ we can use the Template's faces to create a guaranteed watertight surface.
133
+ """
134
+ vol = np.zeros(shape, dtype=np.uint8)
135
+
136
+ # 1. Rebuild Mesh Object
137
+ ref_template, _, _ = get_template_mesh(subdivisions=subdivisions)
138
+ n_verts = len(ref_template.vertices)
139
+
140
+ if features.size != n_verts * 3:
141
+ raise ValueError(f"Feature mismatch! Expected {n_verts*3}, got {features.size}. Check subdivision level.")
142
+
143
+ vertices = features.reshape((n_verts, 3))
144
+
145
+ # Center back to volume
146
+ vol_center = np.array(shape) / 2.0
147
+ vertices += vol_center
148
+
149
+ # Create the mesh using Fixed Topology
150
+ mesh = trimesh.Trimesh(vertices=vertices, faces=ref_template.faces)
151
+
152
+ # 2. Robust Voxelization (Sample & Fill)
153
+ # We perform dense sampling on the mesh surface to handle any folding
154
+ # 50,000 points covers a 300^3 volume well without gaps
155
+ samples, _ = trimesh.sample.sample_surface(mesh, count=50000)
156
+
157
+ ix = np.round(samples[:, 0]).astype(int)
158
+ iy = np.round(samples[:, 1]).astype(int)
159
+ iz = np.round(samples[:, 2]).astype(int)
160
+
161
+ # Bounds check
162
+ valid = (ix >= 0) & (ix < shape[0]) & \
163
+ (iy >= 0) & (iy < shape[1]) & \
164
+ (iz >= 0) & (iz < shape[2])
165
+
166
+ # Paint surface
167
+ vol[ix[valid], iy[valid], iz[valid]] = 1
168
+
169
+ # 3. Fill
170
+ vol = binary_dilation(vol, iterations=2)
171
+ vol = binary_fill_holes(vol)
172
+
173
+ return vol.astype(np.uint8)
174
+
175
+ def smooth_mask(mask, subdivisions=3):
176
+ feats = features_from_mask(mask, subdivisions=subdivisions)
177
+ return mask_from_features(feats, mask.shape, subdivisions=subdivisions)
@@ -0,0 +1,153 @@
1
+ import numpy as np
2
+ from scipy.ndimage import distance_transform_edt, label, center_of_mass
3
+ from numpy.polynomial.chebyshev import chebvander
4
+ from sklearn.linear_model import Ridge
5
+ from itertools import product
6
+
7
+
8
+ def features_from_mask(mask, order=15, n_samples=50000, random_seed=42):
9
+ """
10
+ Extracts PCA-ready shape coefficients using a fixed coordinate system
11
+ based on the mask volume dimensions.
12
+ """
13
+ # Fix the seed so the random sampling is identical every time
14
+ np.random.seed(random_seed)
15
+
16
+ # 1. Define Fixed Coordinate System (Middle of the Cube)
17
+ # Since your data is registered, we use the volume's geometrical center.
18
+ nz, ny, nx = mask.shape
19
+ center = np.array([nz, ny, nx]) / 2.0
20
+
21
+ # Scale covers the entire volume (radius from center to edge)
22
+ # For a cube of size N, radius is N/2.
23
+ scale = np.max([nz, ny, nx]) / 2.0
24
+
25
+ # 2. Compute SDF
26
+ dist_outside = distance_transform_edt(1 - mask)
27
+ dist_inside = distance_transform_edt(mask)
28
+ sdf = dist_outside - dist_inside
29
+
30
+ # 3. Robust Sampling Strategy
31
+ boundary_mask = np.abs(sdf) < 3.0
32
+ idx_boundary = np.argwhere(boundary_mask)
33
+
34
+ # Generate random indices globally
35
+ idx_random = np.random.randint(0, [nz, ny, nx], size=(n_samples, 3))
36
+
37
+ # Combine (50% boundary, 50% random air)
38
+ n_bound = int(n_samples * 0.5)
39
+ if len(idx_boundary) > 0:
40
+ idx_b_select = idx_boundary[np.random.choice(len(idx_boundary), n_bound, replace=True)]
41
+ else:
42
+ idx_b_select = idx_boundary # Fallback
43
+
44
+ indices = np.vstack([idx_b_select, idx_random])
45
+
46
+ z_idx, y_idx, x_idx = indices[:, 0], indices[:, 1], indices[:, 2]
47
+ Values = sdf[z_idx, y_idx, x_idx]
48
+
49
+ # 4. Normalize Coords to [-1, 1] using Fixed Center/Scale
50
+ Z = (z_idx - center[0]) / scale
51
+ Y = (y_idx - center[1]) / scale
52
+ X = (x_idx - center[2]) / scale
53
+
54
+ # Filter valid cube (points outside the fitting domain must be ignored)
55
+ valid_mask = (np.abs(X) <= 1) & (np.abs(Y) <= 1) & (np.abs(Z) <= 1)
56
+
57
+ X, Y, Z = X[valid_mask], Y[valid_mask], Z[valid_mask]
58
+ Values = Values[valid_mask]
59
+
60
+ # 5. Generate Basis
61
+ Tx = chebvander(X, order)
62
+ Ty = chebvander(Y, order)
63
+ Tz = chebvander(Z, order)
64
+
65
+ basis_cols = []
66
+ # Deterministic loop order for PCA consistency
67
+ for i, j, k in product(range(order + 1), repeat=3):
68
+ if i + j + k <= order:
69
+ col = Tx[:, i] * Ty[:, j] * Tz[:, k]
70
+ basis_cols.append(col)
71
+
72
+ A = np.column_stack(basis_cols)
73
+
74
+ # 6. Solve
75
+ clf = Ridge(alpha=1e-3, fit_intercept=False)
76
+ clf.fit(A, Values)
77
+
78
+ return clf.coef_
79
+
80
+ def mask_from_features(coeffs, shape, order):
81
+ """
82
+ Reconstructs mask from coefficients using the fixed volume center/scale.
83
+ """
84
+ vol = np.zeros(shape, dtype=np.uint8)
85
+
86
+ # 1. Define Fixed Coordinate System (Same as features_from_mask)
87
+ nz, ny, nx = shape
88
+ center = np.array([nz, ny, nx]) / 2.0
89
+ scale = np.max([nz, ny, nx]) / 2.0
90
+
91
+ # 2. ROI optimization (We only loop over the area covered by 'scale')
92
+ pad = int(scale * 1.0)
93
+ z_min, z_max = max(0, int(center[0]-pad)), min(shape[0], int(center[0]+pad))
94
+ y_min, y_max = max(0, int(center[1]-pad)), min(shape[1], int(center[1]+pad))
95
+ x_min, x_max = max(0, int(center[2]-pad)), min(shape[2], int(center[2]+pad))
96
+
97
+ roi_nz, roi_ny, roi_nx = z_max - z_min, y_max - y_min, x_max - x_min
98
+ if roi_nz <= 0 or roi_ny <= 0 or roi_nx <= 0: return vol
99
+
100
+ # 3. Coords
101
+ z_coords = (np.arange(z_min, z_max) - center[0]) / scale
102
+ y_coords = (np.arange(y_min, y_max) - center[1]) / scale
103
+ x_coords = (np.arange(x_min, x_max) - center[2]) / scale
104
+
105
+ # 4. Basis
106
+ Tz = chebvander(z_coords, order).T
107
+ Ty = chebvander(y_coords, order).T
108
+ Tx = chebvander(x_coords, order).T
109
+
110
+ # 5. Tensor Assembly
111
+ poly_indices = []
112
+ for i, j, k in product(range(order + 1), repeat=3):
113
+ if i + j + k <= order:
114
+ poly_indices.append((i, j, k))
115
+
116
+ C_tensor = np.zeros((order + 1, order + 1, order + 1))
117
+ for idx, (i, j, k) in enumerate(poly_indices):
118
+ C_tensor[k, j, i] = coeffs[idx]
119
+
120
+ # 6. Contraction
121
+ recon_roi = np.einsum('kji,kz,jy,ix->zyx', C_tensor, Tz, Ty, Tx, optimize='optimal')
122
+
123
+ # 7. Hard Geometric Crop
124
+ zz, yy, xx = np.meshgrid(z_coords, y_coords, x_coords, indexing='ij')
125
+ valid_box = (np.abs(zz) <= 1.0) & (np.abs(yy) <= 1.0) & (np.abs(xx) <= 1.0)
126
+
127
+ mask_roi = (recon_roi < 0) & valid_box
128
+
129
+ # 8. Intelligent Filtering (Center Distance)
130
+ if np.any(mask_roi):
131
+ labeled, n_components = label(mask_roi)
132
+ if n_components > 1:
133
+ # We assume the kidney is near the center of the volume
134
+ roi_center = np.array([roi_nz/2, roi_ny/2, roi_nx/2])
135
+
136
+ centers = center_of_mass(mask_roi, labeled, range(1, n_components+1))
137
+ centers = np.array(centers)
138
+
139
+ if len(centers) > 0:
140
+ dists = np.linalg.norm(centers - roi_center, axis=1)
141
+ winner_idx = np.argmin(dists) + 1
142
+ mask_roi = (labeled == winner_idx)
143
+
144
+ vol[z_min:z_max, y_min:y_max, x_min:x_max] = mask_roi
145
+
146
+ return vol
147
+
148
+ def smooth_mask(mask:np.ndarray, order=20):
149
+ coeffs = features_from_mask(mask, order=order)
150
+ mask_rec = mask_from_features(coeffs, mask.shape, order)
151
+ return mask_rec
152
+
153
+ # N = (L+1) (L+2) (L+3) / 6
miblab_ssa/sdf_ft.py ADDED
@@ -0,0 +1,78 @@
1
+ import numpy as np
2
+ from scipy.ndimage import distance_transform_edt
3
+ from scipy.fft import dctn, idctn
4
+
5
+
6
+
7
+
8
+ def get_spectral_mask(shape, radius):
9
+ """
10
+ Generates a boolean mask for spherical truncation.
11
+ shape: The shape of the coefficient block (e.g., (16, 16, 16))
12
+ radius: The spectral radius to keep (e.g., 16.0)
13
+ """
14
+ # 1. strictly use the 'shape' argument to determine grid dimensions
15
+ ranges = [np.arange(n) for n in shape]
16
+
17
+ # 2. Create the grid of frequencies (i, j, k)
18
+ I, J, K = np.meshgrid(*ranges, indexing='ij')
19
+
20
+ # 3. Calculate distance from DC component (0,0,0)
21
+ # i^2 + j^2 + k^2 <= r^2
22
+ mask = (I**2 + J**2 + K**2) <= radius**2
23
+
24
+ return mask
25
+
26
+ def features_from_mask(mask: np.ndarray, order=16, norm="ortho", saturation_threshold=5.0):
27
+ # 1. Compute SDF & Saturate
28
+ mask = mask.astype(bool)
29
+ # Using float32 saves memory/speed for DCT without losing precision relevant for shape
30
+ sdf = (distance_transform_edt(~mask) - distance_transform_edt(mask)).astype(np.float32)
31
+ sdf_saturated = saturation_threshold * np.tanh(sdf / saturation_threshold)
32
+
33
+ # 2. Compute coefficients
34
+ full_coeffs = dctn(sdf_saturated, norm=norm, workers=-1)
35
+
36
+ # 3. Cubic Truncation first (Efficiency)
37
+ # We slice out the corner cube first to avoid generating a mask for the massive 300^3 volume
38
+ cube_coeffs = full_coeffs[:order, :order, :order]
39
+
40
+ # 4. Spherical Masking
41
+ # Now we generate the mask specifically for this cube shape
42
+ mask = get_spectral_mask(cube_coeffs.shape, radius=order)
43
+
44
+ # Flatten ONLY the valid spherical coefficients
45
+ coeffs_flat = cube_coeffs[mask]
46
+
47
+ return coeffs_flat
48
+
49
+ def mask_from_features(coeffs_flat: np.ndarray, shape, order, norm="ortho"):
50
+ # 1. Recreate the Mask
51
+ # We must generate the exact same mask used during encoding
52
+ # The container is the small corner cube of size (order, order, order)
53
+ cube_shape = (order, order, order)
54
+ mask = get_spectral_mask(cube_shape, radius=order)
55
+
56
+ # 2. Rebuild the Cube
57
+ # Validate that coefficient counts match
58
+ expected_size = np.sum(mask)
59
+ if coeffs_flat.size != expected_size:
60
+ raise ValueError(f"Coefficient mismatch! Expected {expected_size} coeffs for order {order}, got {coeffs_flat.size}.")
61
+
62
+ cube_coeffs = np.zeros(cube_shape, dtype=coeffs_flat.dtype)
63
+ cube_coeffs[mask] = coeffs_flat
64
+
65
+ # 3. Pad to Full Volume
66
+ full_coeffs = np.zeros(shape, dtype=coeffs_flat.dtype)
67
+ full_coeffs[:order, :order, :order] = cube_coeffs
68
+
69
+ # 4. Inverse DCT
70
+ sdf_recon = idctn(full_coeffs, norm=norm, workers=-1)
71
+
72
+ return sdf_recon < 0
73
+
74
+
75
+ def smooth_mask(mask:np.ndarray, order=16, norm="ortho"):
76
+ coeffs = features_from_mask(mask, order, norm)
77
+ mask_recon = mask_from_features(coeffs, mask.shape, order, norm)
78
+ return mask_recon
@@ -0,0 +1,47 @@
1
+ import numpy as np
2
+ from scipy.ndimage import distance_transform_edt
3
+ from scipy.fftpack import dctn, idctn
4
+
5
+
6
+ def features_from_mask(mask:np.ndarray, order=16, norm="ortho"):
7
+
8
+ # Compute sdf
9
+ mask = mask.astype(bool)
10
+ dist_out = distance_transform_edt(~mask)
11
+ dist_in = distance_transform_edt(mask)
12
+ sdf = dist_out - dist_in
13
+
14
+ # Compute coefficients, truncate and flatten
15
+ coeffs = dctn(sdf, norm=norm)
16
+ coeffs = coeffs[:order, :order, :order]
17
+ coeffs = coeffs.flatten()
18
+
19
+ return coeffs
20
+
21
+ def mask_from_features(coeffs_trunc:np.ndarray, shape, norm="ortho"):
22
+
23
+ # Rebuild coefficients at required shape
24
+ order = int(np.cbrt(coeffs_trunc.size))
25
+ coeffs_trunc = coeffs_trunc.reshape(3 * [order])
26
+ coeffs = np.zeros(shape, dtype=coeffs_trunc.dtype)
27
+ coeffs[:order, :order, :order] = coeffs_trunc
28
+
29
+ # Compute mask from sdf
30
+ sdf = idctn(coeffs, norm=norm)
31
+ mask = sdf < 0
32
+
33
+ return mask
34
+
35
+ def smooth_mask(mask:np.ndarray, order=16, norm="ortho"):
36
+ coeffs = features_from_mask(mask, order, norm)
37
+ mask_recon = mask_from_features(coeffs, mask.shape, norm)
38
+ return mask_recon
39
+
40
+ def features_from_dataset(masks, order=16, norm="ortho"):
41
+ features = [features_from_mask(mask, order, norm) for mask in masks]
42
+ return np.array(features)
43
+
44
+
45
+
46
+
47
+
miblab_ssa/sdf_mono.py ADDED
@@ -0,0 +1,214 @@
1
+ import numpy as np
2
+ from scipy.ndimage import distance_transform_edt
3
+ from itertools import product
4
+ from sklearn.linear_model import Ridge
5
+
6
+ def compute_implicit_coeffs(mask, order=6, n_samples=50000):
7
+ """
8
+ Fits an implicit surface using narrow-band sampling for high accuracy.
9
+ Lower order (4-8) is often more stable than high order (10+).
10
+ """
11
+ # 1. Compute SDF
12
+ dist_outside = distance_transform_edt(1 - mask)
13
+ dist_inside = distance_transform_edt(mask)
14
+ sdf = dist_outside - dist_inside
15
+
16
+ # 2. Define Coordinate System (Relative to Object)
17
+ coords = np.argwhere(mask)
18
+ min_c = coords.min(axis=0)
19
+ max_c = coords.max(axis=0)
20
+ center = (min_c + max_c) / 2.0
21
+ # Use max extent for uniform scaling (preserves aspect ratio)
22
+ scale = (max_c - min_c).max() / 2.0 * 1.1 # 10% padding
23
+
24
+ # 3. Intelligent Sampling (The Key Fix)
25
+ # Instead of zooming the grid, we pick specific points to learn from.
26
+
27
+ # Grid of all voxel indices
28
+ nz, ny, nx = mask.shape
29
+
30
+ # Strategy: Pick mostly points near the boundary (narrow band)
31
+ # plus some points inside and outside to define the bulk.
32
+ boundary_mask = np.abs(sdf) < 2.0 # Points within 2 pixels of surface
33
+ inside_mask = (sdf < -2.0) # Deep inside
34
+ outside_mask = (sdf > 2.0) # Far outside
35
+
36
+ # Get indices
37
+ idx_boundary = np.argwhere(boundary_mask)
38
+ idx_inside = np.argwhere(inside_mask)
39
+ idx_outside = np.argwhere(outside_mask)
40
+
41
+ # Sampling counts
42
+ n_bound = int(n_samples * 0.5) # 50% samples on boundary
43
+ n_in = int(n_samples * 0.25) # 25% inside
44
+ n_out = int(n_samples * 0.25) # 25% outside
45
+
46
+ # Helper to safely sample
47
+ def sample_indices(indices, n):
48
+ if len(indices) == 0: return indices
49
+ # If we have fewer points than requested, take them all
50
+ if len(indices) < n: return indices
51
+ choice = np.random.choice(len(indices), n, replace=False)
52
+ return indices[choice]
53
+
54
+ s_bound = sample_indices(idx_boundary, n_bound)
55
+ s_in = sample_indices(idx_inside, n_in)
56
+ s_out = sample_indices(idx_outside, n_out)
57
+
58
+ # Combine samples
59
+ all_indices = np.vstack([s_bound, s_in, s_out])
60
+
61
+ # 4. Extract Coordinates and Values
62
+ # Z, Y, X (indices)
63
+ z_idx, y_idx, x_idx = all_indices[:, 0], all_indices[:, 1], all_indices[:, 2]
64
+
65
+ # Normalize to [-1, 1]
66
+ Z_norm = (z_idx - center[0]) / scale
67
+ Y_norm = (y_idx - center[1]) / scale
68
+ X_norm = (x_idx - center[2]) / scale
69
+
70
+ Values = sdf[z_idx, y_idx, x_idx]
71
+
72
+ # 5. Build Design Matrix
73
+ # Basis: x^i * y^j * z^k
74
+ # Pre-calculating powers is faster than looping
75
+ basis_cols = []
76
+ for i, j, k in product(range(order + 1), repeat=3):
77
+ if i + j + k <= order:
78
+ col = (X_norm**i) * (Y_norm**j) * (Z_norm**k)
79
+ basis_cols.append(col)
80
+
81
+ A = np.column_stack(basis_cols)
82
+
83
+ # 6. Fit
84
+ clf = Ridge(alpha=1e-4) # Small regularization
85
+ clf.fit(A, Values)
86
+
87
+ return clf.coef_, center, scale, order
88
+
89
+ def reconstruct_implicit(coeffs, shape, center, scale, order):
90
+ """
91
+ Reconstructs the volume.
92
+ Optimization: Only evaluate within the bounding box of the object.
93
+ """
94
+ vol = np.zeros(shape, dtype=np.uint8)
95
+
96
+ # Determine Bounding Box in indices (where normalized coords are roughly [-1, 1])
97
+ # Evaluating the polynomial over the entire 301^3 volume is wasteful and slow.
98
+ pad = int(scale * 1.1)
99
+ z_min, z_max = max(0, int(center[0]-pad)), min(shape[0], int(center[0]+pad))
100
+ y_min, y_max = max(0, int(center[1]-pad)), min(shape[1], int(center[1]+pad))
101
+ x_min, x_max = max(0, int(center[2]-pad)), min(shape[2], int(center[2]+pad))
102
+
103
+ # Generate grid only for the ROI
104
+ z_idx, y_idx, x_idx = np.meshgrid(
105
+ np.arange(z_min, z_max),
106
+ np.arange(y_min, y_max),
107
+ np.arange(x_min, x_max),
108
+ indexing='ij'
109
+ )
110
+
111
+ # Normalize
112
+ Z = (z_idx - center[0]) / scale
113
+ Y = (y_idx - center[1]) / scale
114
+ X = (x_idx - center[2]) / scale
115
+
116
+ # Flatten ROI
117
+ Xf, Yf, Zf = X.flatten(), Y.flatten(), Z.flatten()
118
+
119
+ # Evaluate
120
+ recon_sdf = np.zeros_like(Xf)
121
+ col_idx = 0
122
+ for i, j, k in product(range(order + 1), repeat=3):
123
+ if i + j + k <= order:
124
+ weight = coeffs[col_idx]
125
+ recon_sdf += weight * (Xf**i) * (Yf**j) * (Zf**k)
126
+ col_idx += 1
127
+
128
+ # Threshold
129
+ # SDF < 0 is inside
130
+ mask_roi = (recon_sdf < 0)
131
+
132
+ # Place back into full volume
133
+ vol[z_min:z_max, y_min:y_max, x_min:x_max] = mask_roi.reshape(z_idx.shape)
134
+
135
+ return vol
136
+
137
+ import numpy as np
138
+ from itertools import product
139
+
140
+ def reconstruct_implicit_fast(coeffs, shape, center, scale, order):
141
+ """
142
+ Optimized reconstruction using precomputed powers and broadcasting.
143
+ """
144
+ vol = np.zeros(shape, dtype=np.uint8)
145
+
146
+ # 1. Determine ROI (Bounding Box)
147
+ pad = int(scale * 1.1)
148
+ z_min, z_max = max(0, int(center[0]-pad)), min(shape[0], int(center[0]+pad))
149
+ y_min, y_max = max(0, int(center[1]-pad)), min(shape[1], int(center[1]+pad))
150
+ x_min, x_max = max(0, int(center[2]-pad)), min(shape[2], int(center[2]+pad))
151
+
152
+ # Dimensions of the ROI
153
+ nz, ny, nx = z_max - z_min, y_max - y_min, x_max - x_min
154
+ if nz <= 0 or ny <= 0 or nx <= 0: return vol
155
+
156
+ # 2. Create Normalized Coordinate Vectors (1D)
157
+ # Instead of making a meshgrid immediately, we keep them 1D first.
158
+ z_coords = (np.arange(z_min, z_max) - center[0]) / scale
159
+ y_coords = (np.arange(y_min, y_max) - center[1]) / scale
160
+ x_coords = (np.arange(x_min, x_max) - center[2]) / scale
161
+
162
+ # 3. Precompute Powers (Vandermonde-ish)
163
+ # Shape: (order+1, N)
164
+ # P_x[p, :] contains x^p for all x in the ROI
165
+ P_z = np.array([z_coords**p for p in range(order + 1)])
166
+ P_y = np.array([y_coords**p for p in range(order + 1)])
167
+ P_x = np.array([x_coords**p for p in range(order + 1)])
168
+
169
+ # 4. Re-assemble the Polynomial via Tensor Contraction
170
+ # The polynomial is: Sum_{i,j,k} ( C_{ijk} * Z^i * Y^j * X^k )
171
+ # This is a tensor dot product.
172
+
173
+ # First, we need to unpack the flat 'coeffs' list back into a 3D tensor C[i,j,k].
174
+ # Since our flat list skipped terms where i+j+k > order, we must be careful.
175
+ C_tensor = np.zeros((order + 1, order + 1, order + 1))
176
+
177
+ col_idx = 0
178
+ # Use the exact same iteration order as the training step
179
+ for i in range(order + 1):
180
+ for j in range(order + 1):
181
+ for k in range(order + 1):
182
+ if i + j + k <= order:
183
+ # Note: Your training loop was (X^i, Y^j, Z^k),
184
+ # so map indices carefully: C[k, j, i] if Z is axis 0, etc.
185
+ # Your generic loop: (X**i) * (Y**j) * (Z**k)
186
+ # Let's align C_tensor indices to (k, j, i) -> (Z, Y, X)
187
+ C_tensor[k, j, i] = coeffs[col_idx]
188
+ col_idx += 1
189
+
190
+ # 5. Perform Contraction (Einstein Summation)
191
+ # We want result[z, y, x] = Sum_{k,j,i} C[k,j,i] * P_z[k,z] * P_y[j,y] * P_x[i,x]
192
+ # 'kji,kz,jy,ix->zyx'
193
+ # k,j,i are power indices
194
+ # z,y,x are spatial indices
195
+
196
+ # einsum is efficient but can be memory hungry for large chunks.
197
+ # If memory fails, we can do it in two steps.
198
+
199
+ # Step A: Contract X axis -> Temp[k, j, y, x]
200
+ # T1 = np.einsum('kji,ix->kjx', C_tensor, P_x)
201
+ # But contracting 3 axes at once is cleanest if it fits in RAM:
202
+
203
+ recon_roi = np.einsum('kji,kz,jy,ix->zyx', C_tensor, P_z, P_y, P_x, optimize='optimal')
204
+
205
+ # 6. Threshold and Place
206
+ mask_roi = (recon_roi < 0)
207
+ vol[z_min:z_max, y_min:y_max, x_min:x_max] = mask_roi
208
+
209
+ return vol
210
+
211
+ def reconstruct_shape(mask, order=8): # Order 8 is a good balance for kidneys
212
+ coeffs, center, scale, order = compute_implicit_coeffs(mask, order=order)
213
+ mask_rec = reconstruct_implicit_fast(coeffs, mask.shape, center, scale, order)
214
+ return mask_rec