copick-utils 0.6.1__py3-none-any.whl → 1.0.1__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.
- copick_utils/__init__.py +1 -1
- copick_utils/cli/__init__.py +33 -0
- copick_utils/cli/clipmesh.py +161 -0
- copick_utils/cli/clippicks.py +154 -0
- copick_utils/cli/clipseg.py +163 -0
- copick_utils/cli/conversion_commands.py +32 -0
- copick_utils/cli/enclosed.py +191 -0
- copick_utils/cli/filter_components.py +166 -0
- copick_utils/cli/fit_spline.py +191 -0
- copick_utils/cli/hull.py +138 -0
- copick_utils/cli/input_output_selection.py +76 -0
- copick_utils/cli/logical_commands.py +29 -0
- copick_utils/cli/mesh2picks.py +170 -0
- copick_utils/cli/mesh2seg.py +167 -0
- copick_utils/cli/meshop.py +262 -0
- copick_utils/cli/picks2ellipsoid.py +171 -0
- copick_utils/cli/picks2mesh.py +181 -0
- copick_utils/cli/picks2plane.py +156 -0
- copick_utils/cli/picks2seg.py +134 -0
- copick_utils/cli/picks2sphere.py +170 -0
- copick_utils/cli/picks2surface.py +164 -0
- copick_utils/cli/picksin.py +146 -0
- copick_utils/cli/picksout.py +148 -0
- copick_utils/cli/processing_commands.py +18 -0
- copick_utils/cli/seg2mesh.py +135 -0
- copick_utils/cli/seg2picks.py +128 -0
- copick_utils/cli/segop.py +248 -0
- copick_utils/cli/separate_components.py +155 -0
- copick_utils/cli/skeletonize.py +164 -0
- copick_utils/cli/util.py +580 -0
- copick_utils/cli/validbox.py +155 -0
- copick_utils/converters/__init__.py +35 -0
- copick_utils/converters/converter_common.py +543 -0
- copick_utils/converters/ellipsoid_from_picks.py +335 -0
- copick_utils/converters/lazy_converter.py +576 -0
- copick_utils/converters/mesh_from_picks.py +209 -0
- copick_utils/converters/mesh_from_segmentation.py +119 -0
- copick_utils/converters/picks_from_mesh.py +542 -0
- copick_utils/converters/picks_from_segmentation.py +168 -0
- copick_utils/converters/plane_from_picks.py +251 -0
- copick_utils/converters/segmentation_from_mesh.py +291 -0
- copick_utils/{segmentation → converters}/segmentation_from_picks.py +123 -13
- copick_utils/converters/sphere_from_picks.py +306 -0
- copick_utils/converters/surface_from_picks.py +337 -0
- copick_utils/logical/__init__.py +43 -0
- copick_utils/logical/distance_operations.py +604 -0
- copick_utils/logical/enclosed_operations.py +222 -0
- copick_utils/logical/mesh_operations.py +443 -0
- copick_utils/logical/point_operations.py +303 -0
- copick_utils/logical/segmentation_operations.py +399 -0
- copick_utils/process/__init__.py +47 -0
- copick_utils/process/connected_components.py +360 -0
- copick_utils/process/filter_components.py +306 -0
- copick_utils/process/hull.py +106 -0
- copick_utils/process/skeletonize.py +326 -0
- copick_utils/process/spline_fitting.py +648 -0
- copick_utils/process/validbox.py +333 -0
- copick_utils/util/__init__.py +6 -0
- copick_utils/util/config_models.py +614 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/METADATA +15 -2
- copick_utils-1.0.1.dist-info/RECORD +71 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/WHEEL +1 -1
- copick_utils-1.0.1.dist-info/entry_points.txt +29 -0
- copick_utils/segmentation/picks_from_segmentation.py +0 -81
- copick_utils-0.6.1.dist-info/RECORD +0 -14
- /copick_utils/{segmentation → io}/__init__.py +0 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Generate valid area box meshes for tomographic reconstructions."""
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import trimesh as tm
|
|
6
|
+
import zarr
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from copick.models import CopickRoot, CopickRun
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def shift_3d(shift: np.ndarray) -> np.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
Create a 3D translation transformation matrix.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
shift: Translation vector [x, y, z]
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
4x4 homogeneous transformation matrix
|
|
21
|
+
"""
|
|
22
|
+
return np.array(
|
|
23
|
+
[
|
|
24
|
+
[1, 0, 0, shift[0]],
|
|
25
|
+
[0, 1, 0, shift[1]],
|
|
26
|
+
[0, 0, 1, shift[2]],
|
|
27
|
+
[0, 0, 0, 1],
|
|
28
|
+
],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rotation_3d_x(angle: float) -> np.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Create a 3D rotation transformation matrix around X-axis.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
angle: Rotation angle in degrees
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
4x4 homogeneous transformation matrix
|
|
41
|
+
"""
|
|
42
|
+
phi = np.radians(angle)
|
|
43
|
+
return np.array(
|
|
44
|
+
[
|
|
45
|
+
[1, 0, 0, 0],
|
|
46
|
+
[0, np.cos(phi), -np.sin(phi), 0],
|
|
47
|
+
[0, np.sin(phi), np.cos(phi), 0],
|
|
48
|
+
[0, 0, 0, 1],
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rotation_3d_y(angle: float) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Create a 3D rotation transformation matrix around Y-axis.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
angle: Rotation angle in degrees
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
4x4 homogeneous transformation matrix
|
|
62
|
+
"""
|
|
63
|
+
phi = np.radians(angle)
|
|
64
|
+
return np.array(
|
|
65
|
+
[
|
|
66
|
+
[np.cos(phi), 0, np.sin(phi), 0],
|
|
67
|
+
[0, 1, 0, 0],
|
|
68
|
+
[-np.sin(phi), 0, np.cos(phi), 0],
|
|
69
|
+
[0, 0, 0, 1],
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def rotation_3d_z(angle: float) -> np.ndarray:
|
|
75
|
+
"""
|
|
76
|
+
Create a 3D rotation transformation matrix around Z-axis.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
angle: Rotation angle in degrees
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
4x4 homogeneous transformation matrix
|
|
83
|
+
"""
|
|
84
|
+
phi = np.radians(angle)
|
|
85
|
+
return np.array(
|
|
86
|
+
[
|
|
87
|
+
[np.cos(phi), -np.sin(phi), 0, 0],
|
|
88
|
+
[np.sin(phi), np.cos(phi), 0, 0],
|
|
89
|
+
[0, 0, 1, 0],
|
|
90
|
+
[0, 0, 0, 1],
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rotation_center(rot_matrix: np.ndarray, center: np.ndarray) -> np.ndarray:
|
|
96
|
+
"""
|
|
97
|
+
Create a rotation transformation around a center point.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
rot_matrix: 4x4 rotation matrix
|
|
101
|
+
center: Center point [x, y, z]
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
4x4 transformation matrix for rotation around center
|
|
105
|
+
"""
|
|
106
|
+
s1 = shift_3d(-center)
|
|
107
|
+
s2 = shift_3d(center)
|
|
108
|
+
return s2 @ rot_matrix @ s1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_validbox_mesh(
|
|
112
|
+
run: "CopickRun",
|
|
113
|
+
voxel_spacing: float,
|
|
114
|
+
tomo_type: str = "wbp",
|
|
115
|
+
angle: float = 0.0,
|
|
116
|
+
) -> Optional[tm.Trimesh]:
|
|
117
|
+
"""
|
|
118
|
+
Create a box mesh representing the valid area of a reconstruction.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
run: Copick run object
|
|
122
|
+
voxel_spacing: Voxel spacing for the tomogram
|
|
123
|
+
tomo_type: Type of tomogram to use as reference
|
|
124
|
+
angle: Rotation angle around Z-axis in degrees
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Trimesh box object or None if tomogram not found
|
|
128
|
+
"""
|
|
129
|
+
# Negate angle to match coordinate system conventions
|
|
130
|
+
angle = -angle
|
|
131
|
+
|
|
132
|
+
# Get tomogram dimensions
|
|
133
|
+
vs = run.get_voxel_spacing(voxel_spacing)
|
|
134
|
+
tomo = vs.get_tomograms(tomo_type)[0]
|
|
135
|
+
|
|
136
|
+
if tomo is None:
|
|
137
|
+
print(f"Warning: Could not find tomogram of type '{tomo_type}' for run {run.name}")
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Get pixel dimensions and calculate physical dimensions
|
|
141
|
+
pixel_max_dim = zarr.open(tomo.zarr())["0"].shape[::-1]
|
|
142
|
+
pixel_center = np.floor(np.array(pixel_max_dim) / 2) + 1
|
|
143
|
+
max_dim = np.array([d * voxel_spacing for d in pixel_max_dim])
|
|
144
|
+
center = np.array([c * voxel_spacing for c in pixel_center])
|
|
145
|
+
|
|
146
|
+
# Create rotation transformation
|
|
147
|
+
r = rotation_3d_z(angle)
|
|
148
|
+
transform = rotation_center(r, center)
|
|
149
|
+
|
|
150
|
+
# Create rotated box mesh with original tomogram dimensions
|
|
151
|
+
box = tm.creation.box(
|
|
152
|
+
extents=max_dim,
|
|
153
|
+
transform=transform @ shift_3d(max_dim / 2),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Define the tomogram bounding planes
|
|
157
|
+
# The tomogram extends from (0, 0, 0) to max_dim
|
|
158
|
+
bounding_planes = [
|
|
159
|
+
# X planes: normal points inward (positive X direction for min plane, negative for max)
|
|
160
|
+
(np.array([1.0, 0.0, 0.0]), np.array([0.0, 0.0, 0.0])), # x >= 0
|
|
161
|
+
(np.array([-1.0, 0.0, 0.0]), np.array([max_dim[0], 0.0, 0.0])), # x <= max_dim[0]
|
|
162
|
+
# Y planes
|
|
163
|
+
(np.array([0.0, 1.0, 0.0]), np.array([0.0, 0.0, 0.0])), # y >= 0
|
|
164
|
+
(np.array([0.0, -1.0, 0.0]), np.array([0.0, max_dim[1], 0.0])), # y <= max_dim[1]
|
|
165
|
+
# Z planes
|
|
166
|
+
(np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, 0.0])), # z >= 0
|
|
167
|
+
(np.array([0.0, 0.0, -1.0]), np.array([0.0, 0.0, max_dim[2]])), # z <= max_dim[2]
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# Start with the rotated box mesh
|
|
171
|
+
current_mesh = box
|
|
172
|
+
|
|
173
|
+
# Slice the rotated box with each bounding plane to clip it to tomogram bounds
|
|
174
|
+
for plane_normal, plane_origin in bounding_planes:
|
|
175
|
+
# Use trimesh's slice_plane method which properly caps the mesh
|
|
176
|
+
current_mesh = current_mesh.slice_plane(
|
|
177
|
+
plane_origin=plane_origin,
|
|
178
|
+
plane_normal=plane_normal,
|
|
179
|
+
cap=True, # This caps the mesh where it was sliced
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Clean up the mesh to remove degenerate faces and unused vertices
|
|
183
|
+
current_mesh.remove_unreferenced_vertices()
|
|
184
|
+
current_mesh.fix_normals()
|
|
185
|
+
|
|
186
|
+
return current_mesh
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def generate_validbox(
|
|
190
|
+
run: "CopickRun",
|
|
191
|
+
voxel_spacing: float,
|
|
192
|
+
mesh_object_name: str,
|
|
193
|
+
mesh_user_id: str,
|
|
194
|
+
mesh_session_id: str,
|
|
195
|
+
tomo_type: str = "wbp",
|
|
196
|
+
angle: float = 0.0,
|
|
197
|
+
) -> Optional[Dict[str, Any]]:
|
|
198
|
+
"""
|
|
199
|
+
Generate a valid area box mesh for a single run.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
run: Copick run object
|
|
203
|
+
voxel_spacing: Voxel spacing for the tomogram
|
|
204
|
+
mesh_object_name: Name of the mesh object to create
|
|
205
|
+
mesh_user_id: User ID for the mesh
|
|
206
|
+
mesh_session_id: Session ID for the mesh
|
|
207
|
+
tomo_type: Type of tomogram to use as reference
|
|
208
|
+
angle: Rotation angle around Z-axis in degrees
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary with result information or None if failed
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
# Create the box mesh
|
|
215
|
+
box = create_validbox_mesh(
|
|
216
|
+
run=run,
|
|
217
|
+
voxel_spacing=voxel_spacing,
|
|
218
|
+
tomo_type=tomo_type,
|
|
219
|
+
angle=angle,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if box is None:
|
|
223
|
+
return {
|
|
224
|
+
"processed": 0,
|
|
225
|
+
"errors": [f"Could not create validbox for run {run.name}"],
|
|
226
|
+
"vertices_created": 0,
|
|
227
|
+
"faces_created": 0,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Get or create mesh object
|
|
231
|
+
existing_meshes = run.get_meshes(
|
|
232
|
+
object_name=mesh_object_name,
|
|
233
|
+
user_id=mesh_user_id,
|
|
234
|
+
session_id=mesh_session_id,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if len(existing_meshes) == 0:
|
|
238
|
+
mesh_obj = run.new_mesh(
|
|
239
|
+
object_name=mesh_object_name,
|
|
240
|
+
user_id=mesh_user_id,
|
|
241
|
+
session_id=mesh_session_id,
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
mesh_obj = existing_meshes[0]
|
|
245
|
+
|
|
246
|
+
# Store the mesh
|
|
247
|
+
mesh_obj.mesh = box
|
|
248
|
+
mesh_obj.store()
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"processed": 1,
|
|
252
|
+
"errors": [],
|
|
253
|
+
"vertices_created": len(box.vertices),
|
|
254
|
+
"faces_created": len(box.faces),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return {
|
|
259
|
+
"processed": 0,
|
|
260
|
+
"errors": [f"Error processing {run.name}: {e}"],
|
|
261
|
+
"vertices_created": 0,
|
|
262
|
+
"faces_created": 0,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validbox_worker(
|
|
267
|
+
run: "CopickRun",
|
|
268
|
+
voxel_spacing: float,
|
|
269
|
+
mesh_object_name: str,
|
|
270
|
+
mesh_user_id: str,
|
|
271
|
+
mesh_session_id: str,
|
|
272
|
+
tomo_type: str,
|
|
273
|
+
angle: float,
|
|
274
|
+
) -> Dict[str, Any]:
|
|
275
|
+
"""Worker function for batch validbox generation."""
|
|
276
|
+
return generate_validbox(
|
|
277
|
+
run=run,
|
|
278
|
+
voxel_spacing=voxel_spacing,
|
|
279
|
+
mesh_object_name=mesh_object_name,
|
|
280
|
+
mesh_user_id=mesh_user_id,
|
|
281
|
+
mesh_session_id=mesh_session_id,
|
|
282
|
+
tomo_type=tomo_type,
|
|
283
|
+
angle=angle,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def validbox_batch(
|
|
288
|
+
root: "CopickRoot",
|
|
289
|
+
voxel_spacing: float,
|
|
290
|
+
mesh_object_name: str,
|
|
291
|
+
mesh_user_id: str,
|
|
292
|
+
mesh_session_id: str,
|
|
293
|
+
tomo_type: str = "wbp",
|
|
294
|
+
angle: float = 0.0,
|
|
295
|
+
run_names: Optional[List[str]] = None,
|
|
296
|
+
workers: int = 8,
|
|
297
|
+
) -> Dict[str, Any]:
|
|
298
|
+
"""
|
|
299
|
+
Generate valid area box meshes across multiple runs.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
root: The copick root containing runs to process
|
|
303
|
+
voxel_spacing: Voxel spacing for the tomograms
|
|
304
|
+
mesh_object_name: Name of the mesh object to create
|
|
305
|
+
mesh_user_id: User ID for the meshes
|
|
306
|
+
mesh_session_id: Session ID for the meshes
|
|
307
|
+
tomo_type: Type of tomogram to use as reference. Default is 'wbp'.
|
|
308
|
+
angle: Rotation angle around Z-axis in degrees. Default is 0.0.
|
|
309
|
+
run_names: List of run names to process. If None, processes all runs.
|
|
310
|
+
workers: Number of worker processes. Default is 8.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Dictionary with processing results and statistics
|
|
314
|
+
"""
|
|
315
|
+
from copick.ops.run import map_runs
|
|
316
|
+
|
|
317
|
+
runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
|
|
318
|
+
|
|
319
|
+
results = map_runs(
|
|
320
|
+
callback=_validbox_worker,
|
|
321
|
+
root=root,
|
|
322
|
+
runs=runs_to_process,
|
|
323
|
+
workers=workers,
|
|
324
|
+
task_desc="Generating validbox meshes",
|
|
325
|
+
voxel_spacing=voxel_spacing,
|
|
326
|
+
mesh_object_name=mesh_object_name,
|
|
327
|
+
mesh_user_id=mesh_user_id,
|
|
328
|
+
mesh_session_id=mesh_session_id,
|
|
329
|
+
tomo_type=tomo_type,
|
|
330
|
+
angle=angle,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return results
|