From 3da1f29133558a6458627cd03d92eb76f5a85047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 15 Aug 2023 17:21:19 +0200 Subject: [PATCH] added support for vtp read and write --- src/napari_stl_exporter/_reader.py | 29 +++++++- .../_tests/test_writer_and_readers.py | 67 ++++++++++++++++--- src/napari_stl_exporter/_writer.py | 34 ++++++++-- src/napari_stl_exporter/napari.yaml | 17 +++++ 4 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/napari_stl_exporter/_reader.py b/src/napari_stl_exporter/_reader.py index 4a475bf..0afcce5 100644 --- a/src/napari_stl_exporter/_reader.py +++ b/src/napari_stl_exporter/_reader.py @@ -5,18 +5,23 @@ import numpy as np from typing import Optional, List -supported_formats = ['.stl', '.ply', '.obj'] +supported_surface_formats = ['.stl', '.ply', '.obj'] +suported_points_formats = ['.vtp'] + def get_reader(path: str) -> Optional[callable]: # Taken from https://napari.org/plugins/guides.html file_ext = os.path.splitext(path)[1] - if isinstance(path, str) and file_ext in supported_formats: + if isinstance(path, str) and file_ext in supported_surface_formats: return napari_import_surface + elif isinstance(path, str) and file_ext in suported_points_formats: + return napari_import_points # otherwise we return None. return None + def napari_import_surface(path: str) -> List[LayerDataTuple]: """ Read supported surface files using the vedo io functionality. @@ -33,3 +38,23 @@ def napari_import_surface(path: str) -> List[LayerDataTuple]: mesh = vedo.load(path, unpack=True, force=False) _mesh = [mesh.points(), np.asarray(mesh.faces())] return [tuple([_mesh, {}, 'surface'])] + + +def napari_import_points(path: str) -> List[LayerDataTuple]: + """ + Read supported point cloud files using the vedo io functionality. + + Parameters + ---------- + path : str + + Returns + ------- + PointsData + + """ + import pandas as pd + points = vedo.load(path, unpack=True, force=False) + features = pd.DataFrame(dict(points.pointdata)) + return [(points.points(), {'features': features, + 'face_color': features.columns[0]}, 'points')] diff --git a/src/napari_stl_exporter/_tests/test_writer_and_readers.py b/src/napari_stl_exporter/_tests/test_writer_and_readers.py index 374de3a..83de9a7 100644 --- a/src/napari_stl_exporter/_tests/test_writer_and_readers.py +++ b/src/napari_stl_exporter/_tests/test_writer_and_readers.py @@ -2,7 +2,9 @@ import os import vedo -supported_formats = ['.stl', '.ply', '.obj'] +supported_surface_formats = ['.stl', '.ply', '.obj'] +supported_points_formats = ['.vtp'] + def test_label_conversion(): import vedo @@ -14,13 +16,24 @@ def test_label_conversion(): assert isinstance(mesh, vedo.mesh.Mesh) + def test_writer(tmpdir): from skimage import measure - from napari_stl_exporter._writer import napari_write_labels, napari_write_surfaces + import pandas as pd + from napari_stl_exporter._writer import ( + napari_write_labels, + napari_write_surfaces, + napari_write_points) + label_image = np.zeros((100, 100, 100)) label_image[25:75, 25:75, 25:75] = 1 - for ext in supported_formats: + points_data_3d = np.random.rand(100, 3) + points_data_3d_features = pd.DataFrame( + np.random.rand(100, 3), + columns=['feat1', 'feat2', 'feat3']) + + for ext in supported_surface_formats: pth = os.path.join(str(tmpdir), "test_export" + ext) stl_file = napari_write_labels(pth, label_image, None) assert os.path.exists(pth) @@ -28,26 +41,45 @@ def test_writer(tmpdir): surf = measure.marching_cubes(label_image) - for ext in supported_formats: + for ext in supported_surface_formats: pth = os.path.join(str(tmpdir), "test_export" + ext) stl_file = napari_write_surfaces(pth, surf, None) assert os.path.exists(pth) assert stl_file is not None + for ext in supported_points_formats: + pth = os.path.join(str(tmpdir), "test_export" + ext) + vtp_file3d = napari_write_points( + pth, + points_data_3d, + {'features': points_data_3d_features}) + assert os.path.exists(pth) + assert vtp_file3d is not None + + def test_writer_viewer(make_napari_viewer, tmpdir): import napari + import pandas as pd # Load and binarize image label_image = np.zeros((100, 100, 100), dtype=int) + points_data_3d = np.random.rand(100, 3) + points_data_3d_features = pd.DataFrame( + np.random.rand(100, 3), + columns=['feat1', 'feat2', 'feat3']) label_image[25:75, 25:75, 25:75] = 1 # Add data to viewer viewer = make_napari_viewer() label_layer = viewer.add_labels(label_image, name='3D object') + points_layer = viewer.add_points(points_data_3d, name='3D points', + features=points_data_3d_features) # save the layer as 3D printable file to disc napari.save_layers(os.path.join(tmpdir, 'test.stl'), [label_layer]) + napari.save_layers(os.path.join(tmpdir, 'test.vtp'), [points_layer]) assert os.path.exists(os.path.join(tmpdir, 'test.stl')) + def test_sample_data(): from napari_stl_exporter._sample_data import make_pyramid_label, make_pyramid_surface label_image = make_pyramid_label() @@ -56,16 +88,23 @@ def test_sample_data(): surface_image = make_pyramid_surface() assert surface_image is not None + def test_reader(make_napari_viewer, tmpdir): import napari_stl_exporter + import pandas as pd from napari_stl_exporter._sample_data import make_pyramid_surface - from napari_stl_exporter._writer import napari_write_surfaces - from napari_stl_exporter._reader import napari_import_surface + from napari_stl_exporter._writer import napari_write_surfaces, napari_write_points + from napari_stl_exporter._reader import napari_import_surface, napari_import_points + + points_data_3d = np.random.rand(100, 3) + points_data_3d_features = pd.DataFrame( + np.random.rand(100, 3), + columns=['feat1', 'feat2', 'feat3']) pyramid = napari_stl_exporter.make_pyramid_surface()[0][0] viewer = make_napari_viewer() - for ext in supported_formats: + for ext in supported_surface_formats: path = os.path.join(tmpdir, 'test' + ext) napari_write_surfaces(path, pyramid, None) _pyramid = napari_import_surface(path)[0] @@ -76,6 +115,15 @@ def test_reader(make_napari_viewer, tmpdir): data = reader(path) assert data is not None + for ext in supported_points_formats: + path = os.path.join(tmpdir, 'test' + ext) + napari_write_points(path, points_data_3d, {'features': points_data_3d_features}) + _points = napari_import_points(path)[0] + + layer = viewer.add_points(_points[0], **_points[1]) + assert hasattr(layer, 'features') + + def test_image_surface_conversion(): from skimage import data from napari_stl_exporter._image_to_surface import image_to_surface @@ -83,6 +131,7 @@ def test_image_surface_conversion(): image = data.cell() surface = image_to_surface(image) + def test_widgets(make_napari_viewer): from napari_stl_exporter._image_to_surface import image_to_surface_widget, extrude_widget @@ -93,7 +142,3 @@ def test_widgets(make_napari_viewer): viewer.window.add_dock_widget(widget_surface) viewer.window.add_dock_widget(widget_extrude) - -if __name__ == '__main__': - import napari - test_widgets(napari.Viewer) \ No newline at end of file diff --git a/src/napari_stl_exporter/_writer.py b/src/napari_stl_exporter/_writer.py index a156330..0b450b3 100644 --- a/src/napari_stl_exporter/_writer.py +++ b/src/napari_stl_exporter/_writer.py @@ -2,28 +2,54 @@ from skimage import measure import vedo -from napari.types import LabelsData, SurfaceData +from napari.types import LabelsData, SurfaceData, PointsData from typing import Optional supported_layers = ['labels'] -supported_formats = ['.stl', '.ply', '.obj'] +supported_surface_formats = ['.stl', '.ply', '.obj'] +supported_points_formats = ['.vtp'] + def napari_write_surfaces(path: str, data: SurfaceData, meta: dict ) -> Optional[str]: file_ext = os.path.splitext(path)[1] - if file_ext in supported_formats: + if file_ext in supported_surface_formats: mesh = vedo.mesh.Mesh((data[0], data[1])) vedo.write(mesh, path) return path +def napari_write_points( + path: str, data: PointsData, meta: dict + ) -> Optional[str]: + file_ext = os.path.splitext(path)[1] + if file_ext in supported_points_formats: + points = vedo.Points(data) + + # if metadata doesn't exist + if meta is None: + vedo.write(points, path) + return path + + # add features to pointcloud + if 'properties' in meta: + features = meta['properties'] + elif 'features' in meta: + features = meta['features'] + for key in features: + points.pointdata[key] = features[key] + vedo.write(points, path) + + return path + + def napari_write_labels(path: str, data: LabelsData, meta: dict ) -> Optional[str]: file_ext = os.path.splitext(path)[1] - if file_ext in supported_formats: + if file_ext in supported_surface_formats: mesh = _labels_to_mesh(data) vedo.write(mesh, path) diff --git a/src/napari_stl_exporter/napari.yaml b/src/napari_stl_exporter/napari.yaml index 60621f5..8346401 100644 --- a/src/napari_stl_exporter/napari.yaml +++ b/src/napari_stl_exporter/napari.yaml @@ -10,6 +10,13 @@ contributions: title: Write Surface python_name: napari_stl_exporter._writer:napari_write_surfaces + - id: napari-stl-exporter.write_points + title: Write Points + python_name: napari_stl_exporter._writer:napari_write_points + - id: napari_stl_exporter.read_points + title: Read Points + python_name: napari_stl_exporter._reader:napari_import_points + - id: napari-stl-exporter.make_pyramid_label title: Create a label image of a pyramid python_name: napari_stl_exporter._sample_data:make_pyramid_label @@ -61,6 +68,12 @@ contributions: filename_extensions: [".stl", ".ply", ".obj"] display_name: surface + - command: napari-stl-exporter.write_points + layer_types: + - points + filename_extensions: [".vtp"] + display_name: points + readers: - command: napari-stl-exporter.import_surface accepts_directories: false @@ -68,3 +81,7 @@ contributions: - "*.stl" - "*.ply" - "*.obj" + - command: napari-stl-exporter.read_points + accepts_directories: false + filename_patterns: + - "*.vtp"