Skip to content

Commit 3cd0c7a

Browse files
committed
Replace point-cloud-utils with PyMeshFix for mesh hole filling
- Use PyMeshFix.repair() instead of roof concatenation + pcu.make_mesh_watertight() - PyMeshFix interpolates boundary edges to preserve mesh geometry better - Remove timepoint > 2 debug limitation to process all timepoints - Update pyproject.toml dependency: point-cloud-utils -> pymeshfix>=0.17.0
1 parent 15cbd62 commit 3cd0c7a

3 files changed

Lines changed: 24 additions & 28 deletions

File tree

EMT_data_analysis/analysis_scripts/Nuclei_localization.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# 3d meshing libraries
1111
import pyvista as pv
1212
import trimesh
13-
import point_cloud_utils as pcu
13+
import pymeshfix
1414

1515
from bioio import BioImage
1616

@@ -82,9 +82,6 @@ def nuclei_localization(
8282
print(f"Mesh for timepoint {timepoint} not found.")
8383
continue
8484

85-
if timepoint > 2:
86-
break
87-
8885
if align_segmentation:
8986
alignment_matrix = alignment.parse_rotation_matrix_from_string(df['Dual Camera Alignment Matrix Value'].values[0])
9087
else:
@@ -158,30 +155,13 @@ def localize_for_timepoint(
158155
seg = seg.transpose(2, 1, 0)
159156
scale = 2.88 / 0.271
160157

161-
# Calculate roof height to enclose all nuclei in the imaging volume
162-
# The roof must be above the maximum possible scaled Z coordinate
163-
max_z_slices = seg.shape[2] # Number of Z slices in imaging volume
164-
max_scaled_z = max_z_slices * scale # Maximum Z after scaling to isotropic
165-
166158
vert, faces = mesh.points, mesh.faces.reshape(mesh.n_faces, 4)[:,1:]
167-
vert_up = np.zeros_like(vert)
168-
np.copyto(vert_up, vert)
169-
# Place roof above the maximum scaled Z coordinate of the imaging volume
170-
# This ensures all nuclei (including those at high Z) are enclosed
171-
roof_height = max(max(vert[:,2]), max_scaled_z) * 1.05 # 5% margin above max
172-
vert_up[:, 2] = roof_height
173-
face_up = np.zeros_like(faces)
174-
np.copyto(face_up, faces)
175-
176-
mesh = trimesh.Trimesh(vertices=vert, faces=faces)
177-
roof = trimesh.Trimesh(vertices=vert_up, faces=face_up)
178-
mesh_conc = trimesh.util.concatenate(mesh, roof)
179-
180-
vert, faces = mesh_conc.vertices, mesh_conc.faces
181-
182-
vw, fw = pcu.make_mesh_watertight(vert, faces, 10000)
183159

184-
mesh = trimesh.Trimesh(vertices=vw, faces=fw)
160+
# Use PyMeshFix to fill holes and create watertight mesh
161+
# This preserves geometry better than artificial roof concatenation
162+
meshfix = pymeshfix.MeshFix(vert, faces)
163+
meshfix.repair(verbose=False)
164+
mesh = trimesh.Trimesh(vertices=meshfix.v, faces=meshfix.f)
185165

186166
# initialize ray caster (for checking if a point is inside the mesh)
187167
rayCaster = trimesh.ray.ray_triangle.RayMeshIntersector(mesh)
@@ -232,7 +212,7 @@ def run_nuclei_localization(
232212
):
233213
'''
234214
This is the main function to localize nuclei inside a 3D mesh.
235-
215+
236216
Parameters
237217
----------
238218
manifest_path: str
@@ -245,10 +225,25 @@ def run_nuclei_localization(
245225
Flag to enable alignment of the segmentation using the barcode of the movie.
246226
Default is True.
247227
'''
228+
# Filter to specific Data IDs for analysis
229+
ANALYSIS_DATA_IDS = [
230+
'3500005548_43', '3500005548_46', '3500005548_48',
231+
'3500005824_35', '3500005824_36', '3500005824_37', '3500005824_38',
232+
'3500005828_43', '3500005828_45', '3500005828_46', '3500005828_67', '3500005828_70',
233+
'3500006256_19', '3500006256_21',
234+
'3500007081_8',
235+
'3500007213_38',
236+
'3500007247_5',
237+
'3500007432_52', '3500007432_57', '3500007432_63',
238+
]
239+
248240
df_cond = df_manifest[
249241
[gene in ['HIST1H2BJ', 'EOMES|TBR2'] for gene in df_manifest['Gene'].values]
250242
].dropna(subset=['CollagenIV Segmentation Probability URL'])
251243

244+
# Filter to only the specified Data IDs
245+
df_cond = df_cond[df_cond['Data ID'].isin(ANALYSIS_DATA_IDS)]
246+
252247
print(f"Processing {len(df_cond)} movies with CollagenIV segmentations.")
253248

254249
for data_id in tqdm(pd.unique(df_cond['Data ID']), desc="Movies"):

EMT_data_analysis/figure_generation/inside-outside_classification.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def main(
4747
df_meta = df_meta[df_meta['Data ID'] == data_id]
4848
df = io.load_inside_outside_classification()
4949
df = df[df['Data ID'] == data_id]
50+
df = df[df['Z']<27]
5051

5152
tmp_dir = Path("./emt_tmp/nuclei_localization/")
5253
tmp_dir.mkdir(exist_ok=True, parents=True)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
"cgal==6.0.1.post202410241521",
2121
"trimesh>=4.9.0",
2222
"vtk==9.3.0",
23-
"point-cloud-utils==0.30.4",
23+
"pymeshfix>=0.17.0",
2424
"pyvista>=0.43.10,<0.44",
2525
"rtree>=1.4.1",
2626
"scikit_posthocs>=0.9.0",

0 commit comments

Comments
 (0)