From 2cfc9f0b53c7e1e2c00587740ebb7ee91e24151b Mon Sep 17 00:00:00 2001 From: harmening Date: Tue, 30 Sep 2025 21:39:20 +0200 Subject: [PATCH] fix landmark_picking labels, enable free choice of which landmars to pick, return landmarks as xarray --- ...hotogrammetric_optode_coregistration.ipynb | 55 +++++-------------- .../misc/finger_tapping_full_pipeline.ipynb | 17 +----- .../12_plots_example.ipynb | 14 +++-- src/cedalion/plots.py | 44 ++++++++++----- 4 files changed, 52 insertions(+), 78 deletions(-) diff --git a/examples/head_models/41_photogrammetric_optode_coregistration.ipynb b/examples/head_models/41_photogrammetric_optode_coregistration.ipynb index 074334cb..3c0233f0 100644 --- a/examples/head_models/41_photogrammetric_optode_coregistration.ipynb +++ b/examples/head_models/41_photogrammetric_optode_coregistration.ipynb @@ -345,11 +345,11 @@ "\n", "### 5.1. Pick positions in interactive plot\n", "\n", - "When using the `plot_surface` function with parameter `pick_landmarks` set to *True*, the plot becomes interactive and allows to pick the positions of 5 landmarks. These are \"Nz\", \"Iz\", \"Cz\", \"Lpa\", \"RpA\".\n", + "When using the `plot_surface` function with parameter `pick_landmarks` set to *True*, the plot becomes interactive and allows picking the positions of 5 landmarks. These are \"Nz\", \"Iz\", \"Cz\", \"Lpa\", \"RpA\". If other landmarks need to be picked, pass their labels as a list to `pick_landmarks` (instead of passing *True*).\n", "\n", "After clicking on the mesh, a green sphere marks the picked location. The sphere has a label attached. If this label is not visible, try to zoom further into the plot (mouse wheel). By clicking again with right mouse button on the sphere one can cycle through the different labels or remove a misplaced landmark.\n", "\n", - "It halps to add colored markers at the landmark positions when preparing the subject. Here green stickers where used." + "It helps to add colored markers at the landmark positions when preparing the subject. Here, green stickers were used." ] }, { @@ -385,7 +385,7 @@ "outputs": [], "source": [ "if INTERACTIVE:\n", - " landmark_coordinates, landmark_labels = get_landmarks()\n", + " landmarks = get_landmarks()\n", "else:\n", " # For documentation purposes and to enable automatically rendered example notebooks\n", " # we provide the hand-picked coordinates here, too.\n", @@ -400,45 +400,16 @@ " ]\n", " )\n", "\n", - "display(landmark_labels)\n", - "display(landmark_coordinates)\n", - "\n", - "assert len(set(landmark_labels)) == 5, \"please select 5 landmarks\"\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "27", - "metadata": {}, - "source": [ - "### 5.3 Wrap landmark positions and labels in a xarray.DataArray structure\n", - "\n", - "* insert *landmark_coordinates* and *landmark_labels*" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28", - "metadata": {}, - "outputs": [], - "source": [ - "coordinates = landmark_coordinates\n", - "labels = landmark_labels\n", - "\n", - "types = [cdc.PointType.LANDMARK] * 5\n", - "groups = [\"L\"] * 5\n", - "\n", - "landmarks = xr.DataArray(\n", - " np.vstack(coordinates),\n", - " dims=[\"label\", \"digitized\"],\n", - " coords={\n", - " \"label\": (\"label\", labels),\n", - " \"type\": (\"label\", types),\n", - " \"group\": (\"label\", groups),\n", - " },\n", - ").pint.quantify(\"mm\")\n", + " # Wrap landmark positions and labels in a xarray.DataArray structure\n", + " landmarks = xr.DataArray(\n", + " np.vstack(landmark_coordinates),\n", + " dims=[\"label\", \"digitized\"],\n", + " coords={\n", + " \"label\": (\"label\", landmark_labels),\n", + " \"type\": (\"label\", [cdc.PointType.LANDMARK] * 5),\n", + " \"group\": (\"label\", [\"L\"] * 5),\n", + " },\n", + " ).pint.quantify(\"mm\")\n", "\n", "display(landmarks)" ] diff --git a/examples/misc/finger_tapping_full_pipeline.ipynb b/examples/misc/finger_tapping_full_pipeline.ipynb index bde1f6af..bd95e8aa 100644 --- a/examples/misc/finger_tapping_full_pipeline.ipynb +++ b/examples/misc/finger_tapping_full_pipeline.ipynb @@ -299,20 +299,7 @@ "import tkinter as tk\n", "from tkinter import messagebox\n", "\n", - "landmark_coordinates, landmark_labels = get_landmarks()\n", - "\n", - "# write into Xarray\n", - "landmarks = xr.DataArray(\n", - " np.vstack(landmark_coordinates),\n", - " dims=[\"label\", \"digitized\"],\n", - " coords={\n", - " \"label\": (\"label\", landmark_labels),\n", - " \"type\": (\"label\", [cdc.PointType.LANDMARK]*5),\n", - " \"group\": (\"label\", [\"L\"]*5),\n", - " },\n", - ").pint.quantify(\"mm\")\n", - "\n", - "\n", + "landmarks = get_landmarks()\n", "display(landmarks)" ] }, @@ -1022,5 +1009,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/plots_visualization/12_plots_example.ipynb b/examples/plots_visualization/12_plots_example.ipynb index 6658fb59..8d116318 100644 --- a/examples/plots_visualization/12_plots_example.ipynb +++ b/examples/plots_visualization/12_plots_example.ipynb @@ -347,7 +347,7 @@ "metadata": {}, "source": [ "## Interactive 3D Plot to select Landmarks\n", - "using **plot_surface** with \"pick_landmarks = True\". Here we use a photogrammetric scan, and the landmarks are indicated by green dots. Right-clicking again on an existing landmark changes the label." + "using **plot_surface** with `pick_landmarks = ['Nz', 'Iz', 'Cz', 'Lpa', 'Rpa']`. Here we use a photogrammetric scan, and the landmarks are indicated by green dots. Right-clicking again on an existing landmark changes the label." ] }, { @@ -359,7 +359,7 @@ "import cedalion.plots\n", "\n", "plt = pv.Plotter()\n", - "get_landmarks = cedalion.plots.plot_surface(plt, pscan, opacity=1.0, pick_landmarks = True)\n", + "get_landmarks = cedalion.plots.plot_surface(plt, pscan, opacity=1.0, pick_landmarks = ['Nz', 'Iz', 'Cz', 'Lpa', 'Rpa'])\n", "plt.show(interactive = True)" ] }, @@ -377,9 +377,11 @@ " np.array([82.8771277, 79.79500128, 498.3338802]),\n", " np.array([15.17214095, -60.56186128, 563.29621021])]\n", "\n", - "# uncommentif you want to see your own picked results: when you are done run get_landmarks() to get the landmarks. \n", - "# landmark_coordinates, landmark_labels = get_landmarks()\n", - "display (landmark_labels)" + "# Uncomment the following lines if you want to see your own picked results. When you are done, run' get_landmarks() ' to retrieve the landmarks. \n", + "#landmarks = get_landmarks()\n", + "#landmark_labels = landmarks.label.values\n", + "#landmark_coordinates = landmarks.values\n", + "display(landmark_labels)" ] }, { @@ -663,5 +665,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/src/cedalion/plots.py b/src/cedalion/plots.py index aaf1daf2..f10fdb99 100644 --- a/src/cedalion/plots.py +++ b/src/cedalion/plots.py @@ -156,7 +156,7 @@ def plot_surface( surface: cdc.Surface, color: pv.ColorLike | None = None, opacity : float =1.0, - pick_landmarks : bool = False, + pick_landmarks : list[str] | bool = False, **kwargs, ): #used for picking landmarks in photogrammetry example @@ -168,8 +168,9 @@ def plot_surface( color: Color of the mesh. opacity: Opacity of the mesh, ranging from 0 (transparent) to 1 (opaque). Default is 1.0. - pick_landmarks: If True, enables interactive picking of landmarks on the - surface. Default is False. + pick_landmarks: If True, enables interactive picking of landmarks + ('Nz', 'Iz', 'Cz', 'Lpa', 'Rpa') on the surface. If a list of strings is + provided, these are used as the landmark labels instead. Default is False. **kwargs: Additional keyword arguments are passed to pv.add_mesh. Returns: @@ -181,7 +182,6 @@ def plot_surface( - Eike Middell | middell@tu-berlin.de | 2024 - Masha Iudina | mashayudi@gmail.com | 2024 """ - if isinstance(surface, cdc.VTKSurface): mesh = surface.mesh elif isinstance(surface, cdc.TrimeshSurface): @@ -208,7 +208,11 @@ def plot_surface( # Define landmark labels - landmark_labels = ['Nz', 'Iz', 'Cz', 'Lpa', 'Rpa'] + if isinstance(pick_landmarks, bool): + landmark_labels = ['Nz', 'Iz', 'Cz', 'Lpa', 'Rpa'] + else: + landmark_labels = pick_landmarks + pick_landmarks = True picked_points = [] labels = [] point_actors = [] @@ -247,10 +251,10 @@ def place_landmark(point): # If no point is close enough, create a new point and assign a label # Check if there are already 5 points placed - if len(picked_points) >= 5: + if len(picked_points) >= len(landmark_labels): return - landmark_label = landmark_labels[0] + landmark_label = landmark_labels[(len(picked_points) % len(landmark_labels))] # Add new point and label actors point_actor = plotter.add_mesh(pv.Sphere(radius=3, center=new_point), color='green', smooth_shading=True) @@ -262,25 +266,35 @@ def place_landmark(point): picked_points.append(new_point) labels.append(landmark_label) - # Initialize the labels list - # labels = [None] * 5 # Initialize with None for unassigned labels if pick_landmarks is True: def get_points_and_labels(): - - if len(labels) < 5: + if len(labels) < len(landmark_labels): print("Warning: Some labels are missing") - elif len(set(labels)) != 5: + elif len(set(labels)) != len(landmark_labels): print("Warning: Some labels are repeated!") - return picked_points, labels + + landmarks = xr.DataArray( + np.vstack(picked_points), + dims=["label", "digitized"], + coords={ + "label": ("label", labels), + "type": ("label", [cdc.PointType.LANDMARK]*len(labels)), + "group": ("label", ["L"]*len(labels)), + }, + ).pint.quantify("mm") + return landmarks + plotter.enable_surface_point_picking( callback=place_landmark, - show_message="Right click to place or change the landmark label", + show_message="Right click to place or change the landmark label.\n"\ + "Expected labels: "+str(landmark_labels)+"\n"\ + "Close window when done.", show_point=False, tolerance=0.005, ) - + return get_points_and_labels def plot_labeled_points(