Skip to content

Commit aa9a71f

Browse files
feat(3D): export quantitative data directly from meshes
1 parent 801ffe1 commit aa9a71f

File tree

8 files changed

+131
-43
lines changed

8 files changed

+131
-43
lines changed

PyReconstruct/modules/backend/volume/export_volumes.py

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Export 3D objects."""
22

33
from pathlib import Path
4-
from typing import List
4+
from typing import List, Union
55

66
import numpy as np
77
import trimesh
@@ -24,47 +24,13 @@ def export3DObjects(series: Series, obj_names : list, output_dir : str, export_t
2424
void
2525
"""
2626

27+
## Collect 3D objects
28+
2729
obj_data = {}
2830

2931
for obj_name in obj_names:
3032

31-
mode = series.getAttr(obj_name, "3D_mode")
32-
33-
if mode == "surface":
34-
obj_data[obj_name] = Surface(obj_name, series)
35-
36-
elif mode == "spheres":
37-
obj_data[obj_name] = Spheres(obj_name, series)
38-
39-
elif mode == "contours":
40-
obj_data[obj_name] = Contours(obj_name, series)
41-
42-
for snum, section in series.enumerateSections(show_progress=False):
43-
44-
# # Assume somewhat uniform section thickness
45-
# tform = section.tform
46-
47-
for obj_name in obj_names:
48-
49-
if obj_name not in section.contours:
50-
51-
continue
52-
53-
## Get objects alignment
54-
obj_alignment = series.getAttr(obj_name, "alignment")
55-
56-
if not obj_alignment:
57-
58-
tform = section.tform
59-
60-
else:
61-
62-
tform = section.tforms[obj_alignment]
63-
64-
for trace in section.contours[obj_name]:
65-
66-
## Collect all points if generating full surface
67-
obj_data[obj_name].addTrace(trace, snum, tform)
33+
obj_data[obj_name] = get_3D_mesh(series, obj_name)
6834

6935
## Iterate through objects and export 3D meshes
7036

@@ -83,7 +49,93 @@ def export3DObjects(series: Series, obj_names : list, output_dir : str, export_t
8349

8450
if notify_user:
8551

86-
notify(f"Object(s) exported to directory:\n\n{output_directory.absolute()}\n")
52+
notify(f"Object(s) exported to directory:\n\n{Path(output_directory).absolute()}\n")
53+
54+
55+
def export3DData(series: Series, obj_names: list, output_fp: str, notify_user: bool=True) -> None:
56+
"""Export quantitative data from meshes."""
57+
58+
sep = ","
59+
csv_str = f"Series{sep}Name{sep}MeshType{sep}SurfaceArea{sep}Volume\n"
60+
series_code = series.code
61+
62+
errors = {}
63+
64+
for obj in obj_names:
65+
66+
try:
67+
68+
obj_data = get_3D_mesh(series, obj)
69+
obj_type = type(obj_data).__name__.lower()
70+
tm = obj_data.generateTrimesh()
71+
72+
surface_area = round(tm.area, 5)
73+
volume = round(tm.volume, 5)
74+
75+
csv_str += f"{series_code}{sep}{obj}{sep}{obj_type}{sep}{surface_area}{sep}{volume}\n"
76+
77+
except Exception as e:
78+
79+
errors[obj] = e
80+
81+
if not errors:
82+
83+
with open(output_fp, "w") as fp:
84+
fp.write(csv_str)
85+
86+
if notify_user:
87+
notify(f"Data exported to:\n\n{Path(output_fp).absolute()}\n")
88+
89+
if errors:
90+
91+
print(errors)
92+
notify("There were errors exporting data from some or all of the objects. See console.")
93+
94+
95+
def get_3D_mesh(series: Series, obj_name: str) -> Union[Surface, Spheres, Contours]:
96+
"""Get mesh for an object."""
97+
98+
## Create initial 3D obj
99+
100+
mode = series.getAttr(obj_name, "3D_mode")
101+
102+
if mode == "surface":
103+
obj_data = Surface(obj_name, series)
104+
105+
elif mode == "spheres":
106+
obj_data = Spheres(obj_name, series)
107+
108+
elif mode == "contours":
109+
obj_data = Contours(obj_name, series)
110+
111+
## Iterate through sections and collect data
112+
113+
for snum, section in series.enumerateSections(show_progress=False):
114+
115+
## Assume somewhat uniform section thickness
116+
# tform = section.tform
117+
118+
if obj_name not in section.contours:
119+
120+
continue
121+
122+
## Get alignment
123+
obj_alignment = series.getAttr(obj_name, "alignment")
124+
125+
if not obj_alignment:
126+
127+
tform = section.tform
128+
129+
else:
130+
131+
tform = section.tforms[obj_alignment]
132+
133+
for trace in section.contours[obj_name]:
134+
135+
## Collect all points if generating full surface
136+
obj_data.addTrace(trace, snum, tform)
137+
138+
return obj_data
87139

88140

89141
def convert_vedo_to_tm(obj) -> trimesh.Trimesh:

PyReconstruct/modules/backend/volume/objects_3D.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def addToExtremes(self, x, y, s):
5959
if s < self.extremes[4]: self.extremes[4] = s
6060
if s > self.extremes[5]: self.extremes[5] = s
6161

62+
6263
class Surface(Object3D):
6364

6465
def __init__(self, *args):
@@ -257,6 +258,7 @@ def generate3D(self):
257258

258259
return mesh_data
259260

261+
260262
class Contours(Object3D):
261263

262264
def __init__(self, *args):

PyReconstruct/modules/datatypes/series_data.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Collect data to pass to table manager."""
2+
23
from typing import Union
34

45
from PyReconstruct.modules.calc import lineDistance, area
@@ -345,9 +346,16 @@ def getVolume(self, obj_name : str) -> float:
345346

346347
for trace_data in trace_list:
347348
v += trace_data.getArea() * self.data["sections"][snum]["thickness"]
348-
349+
349350
return v
350351

352+
def getSurfaceArea(self, obj_name: str) -> float:
353+
"""Get the surface area of an object."""
354+
355+
from PyReconstruct.modules.backend.volume.export_volumes import get_3D_mesh
356+
357+
pass
358+
351359
def getConfiguration(self, obj_name: str) -> Union[str, None]:
352360
"""Get the configuration of the object.
353361

PyReconstruct/modules/gui/main/context_menu_list.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def get_context_menu_list_obj(self):
121121
("removeobj3D_act", "Remove from scene", "", self.remove3D),
122122
{
123123
"attr_name": "exportobj3D",
124-
"text": "Export",
124+
"text": "Export meshes",
125125
"opts":
126126
[
127127
("export3D_act", "Wavefront (.obj)", "", lambda : self.exportAs3D("obj")),
@@ -132,6 +132,7 @@ def get_context_menu_list_obj(self):
132132
]
133133

134134
},
135+
("exportmeshdata", "Export quantitative data", "", self.export3DData),
135136
None,
136137
("editobj3D_act", "Edit 3D settings...", "", self.edit3D)
137138
]

PyReconstruct/modules/gui/main/field_widget_3_object.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ def exportAs3D(self, obj_names : list, export_type):
311311
"""Export 3D objects."""
312312
self.mainwindow.exportAs3D(obj_names, export_type)
313313

314+
@object_function(update_objects=False, reload_field=False)
315+
def export3DData(self, obj_names: list):
316+
"""Export quantitative data from 3D meshes."""
317+
self.mainwindow.export3DData(obj_names)
318+
314319
@object_function(update_objects=True, reload_field=False)
315320
def addToGroup(self, obj_names : list, log_event=True):
316321
"""Add objects to a group."""

PyReconstruct/modules/gui/main/main_imports.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@
108108
)
109109

110110
from PyReconstruct.modules.backend.volume import (
111-
export3DObjects
111+
export3DObjects,
112+
export3DData
112113
)
113114

114115
from PyReconstruct.modules.backend.imports import (

PyReconstruct/modules/gui/main/main_window.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,6 +2372,25 @@ def exportAs3D(self, obj_names, export_type, ztraces=False):
23722372
)
23732373
if not export_dir: return
23742374
export3DObjects(self.series, obj_names, export_dir, export_type)
2375+
2376+
def export3DData(self, obj_names):
2377+
"""Export quantitative data from meshes."""
2378+
2379+
notify(
2380+
f"3D surface area and volume measurements depend on the meshing algorithm "
2381+
f"implemented in PyReconstruct. We recommend verifying proper mesh quality by "
2382+
f"inspecting objects in the 3D scene before analyzing quantitative data.\n\n"
2383+
f"Click OK to specificy where to save this data."
2384+
)
2385+
2386+
self.saveAllData()
2387+
2388+
output_fp = FileDialog.get(
2389+
"save", self, "Save data as CSV file", "*.csv", "mesh_data.csv"
2390+
)
2391+
if not output_fp: return
2392+
2393+
export3DData(self.series, obj_names, output_fp)
23752394

23762395
def toggleCuration(self):
23772396
"""Quick shortcut to toggle curation on/off for the tables."""

PyReconstruct/modules/gui/table/object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ def exportUserColText(self):
883883
return
884884

885885
self.series.exportUserColsText(out_fp)
886-
886+
887887
def importUserColText(self):
888888
"""Import user columns from a text file."""
889889
fp = FileDialog.get(

0 commit comments

Comments
 (0)