diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index fe30132..5095070 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -19,8 +19,13 @@ jobs: runs-on: ${{ matrix.platform }} strategy: matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] + platform: [ubuntu-latest, windows-latest, macos-13, macos-14] python-version: ['3.8', '3.9', '3.10'] + exclude: + - python-version: "3.8" + platform: macos-14 + - python-version: "3.9" + platform: macos-14 steps: - uses: actions/checkout@v3 diff --git a/napari_skimage_regionprops/_load_csv.py b/napari_skimage_regionprops/_load_csv.py index 40c90de..e1d64be 100644 --- a/napari_skimage_regionprops/_load_csv.py +++ b/napari_skimage_regionprops/_load_csv.py @@ -1,5 +1,6 @@ import numpy as np from napari_tools_menu import register_function +from napari.utils.notifications import show_warning try: import napari @@ -9,7 +10,7 @@ @register_function(menu="Measurement > Load from CSV (nsr)") -def load_csv(csv_filename:"magicgui.types.PathLike", labels_layer: "napari.layers.Labels", show_table: bool = True, viewer: "napari.Viewer" = None): +def load_csv(csv_filename:"magicgui.types.PathLike", labels_layer: "napari.layers.Layer", show_table: bool = True, viewer: "napari.Viewer" = None): """Save contents of a CSV file into a given layer's properties""" import pandas as pd # load region properties from csv file @@ -24,9 +25,25 @@ def load_csv(csv_filename:"magicgui.types.PathLike", labels_layer: "napari.layer {"label": np.array(range(1, (len(edited_reg_props) + 1)))} ) edited_reg_props = pd.concat([label_column, edited_reg_props], axis=1) - + # Add 'properties' and 'features' attributes if layer is Surface + if isinstance(labels_layer, napari.layers.Surface): + # Check if features and properties are already defined + if not hasattr(labels_layer, "features"): + labels_layer.features = {} + if not hasattr(labels_layer, "properties"): + labels_layer.properties = {} + elif isinstance(labels_layer, napari.layers.Points): + # Table shape must match number of points + if edited_reg_props.shape[0] < labels_layer.data.shape[0]: + # fill in missing rows with NaNs + show_warning("Number of rows in CSV file is less than number of points in the layer. Filling in missing rows with NaNs.") + edited_reg_props = pd.concat([edited_reg_props, pd.DataFrame(np.nan, index=range(labels_layer.data.shape[0] - edited_reg_props.shape[0]), columns=edited_reg_props.columns)]).reset_index(drop=True) + elif edited_reg_props.shape[0] > labels_layer.data.shape[0]: + # truncate if necessary + show_warning("Number of rows in CSV file is greater than number of points in the layer. Truncating to match number of points.") + edited_reg_props = edited_reg_props.iloc[:labels_layer.data.shape[0],:] if hasattr(labels_layer, "properties"): - labels_layer.properties = edited_reg_props + labels_layer.properties = edited_reg_props.to_dict(orient="list") if hasattr(labels_layer, "features"): labels_layer.features = edited_reg_props diff --git a/napari_skimage_regionprops/_table.py b/napari_skimage_regionprops/_table.py index ca1011b..6ca93b8 100644 --- a/napari_skimage_regionprops/_table.py +++ b/napari_skimage_regionprops/_table.py @@ -29,6 +29,19 @@ def __init__(self, layer: "napari.layers.Layer", viewer: "napari.Viewer" = None) self._view = QTableWidget() self._view.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + # Add 'properties' and 'features' attributes if layer is Surface + if isinstance(layer, napari.layers.Surface): + # Check if features and properties are already defined + if not hasattr(layer, "features"): + layer.features = {} + else: + # Mirror properties to features + layer.properties = layer.features.to_dict(orient="list") + if not hasattr(layer, "properties"): + layer.properties = {} + else: + # Mirror features to properties + layer.features = pd.DataFrame(layer.properties) if hasattr(layer, "properties"): self.set_content(layer.properties) else: diff --git a/napari_skimage_regionprops/_tests/test_function.py b/napari_skimage_regionprops/_tests/test_function.py index 3dcc9f3..45eb186 100644 --- a/napari_skimage_regionprops/_tests/test_function.py +++ b/napari_skimage_regionprops/_tests/test_function.py @@ -1,4 +1,40 @@ # from napari_skimage_regionprops import threshold, image_arithmetic +def generate_sphere_mesh(radius, segments): + """ + Generate vertices and faces for a sphere mesh. + + Parameters: + radius (float): Radius of the sphere. + segments (int): Number of segments used to generate the sphere, higher means finer mesh. + + Returns: + tuple: vertices (N, 3 array), faces (M, 3 array) + """ + import numpy as np + theta = np.linspace(0, np.pi, segments) + phi = np.linspace(0, 2 * np.pi, segments) + theta, phi = np.meshgrid(theta, phi) + + x = radius * np.sin(theta) * np.cos(phi) + y = radius * np.sin(theta) * np.sin(phi) + z = radius * np.cos(theta) + + vertices = np.column_stack((x.ravel(), y.ravel(), z.ravel())) + + # Generate faces (indices of vertices that make up each triangle) + faces = [] + for i in range(len(theta) - 1): + for j in range(len(phi) - 1): + v0 = i * len(phi) + j + v1 = v0 + 1 + v2 = v0 + len(phi) + v3 = v2 + 1 + faces.append([v0, v1, v2]) + faces.append([v1, v3, v2]) + + faces = np.array(faces) + + return vertices, faces # add your tests here... def test_regionprops(make_napari_viewer): @@ -90,6 +126,17 @@ def test_regionprops(make_napari_viewer): load_csv("test.csv", labels_layer) load_csv("test.csv", labels_layer, viewer) + points_layer = viewer.add_points(np.array([[2, 1], [2, 5], [5, 3], [6, 6]])) + vertices, faces = generate_sphere_mesh(1, 50) + surface_layer = viewer.add_surface((vertices, faces)) + + # test loading csv to points + load_csv("test.csv", points_layer) + load_csv("test.csv", points_layer, viewer) + + # test loading csv to surface + load_csv("test.csv", surface_layer) + load_csv("test.csv", surface_layer, viewer) # empty table table_widget.set_content(None)