Skip to content

Feature request: expose mesh-based API (get_shcoeffs_from_mesh / get_shcoeffs_from_vertices_faces) #23

Description

@keisuke-ishihara

Summary

get_shcoeffs() currently accepts only binary images. Internally, the library already converts the image to a vtkPolyData mesh before performing the actual spherical-harmonics fitting — but that mesh capability is never exposed to the user.

Users who already have surface meshes (from meshing pipelines, simulation outputs, or computational geometry tools) must voxelize them back into images as a workaround. That round-trip is lossy, slow, and unnecessary.

I would like to contribute two new public entry points that expose the existing mesh path directly:

get_shcoeffs_from_mesh(mesh, lmax, alignment_2d=True, make_unique=False)
get_shcoeffs_from_vertices_faces(vertices, faces, lmax, alignment_2d=True, make_unique=False)

I have a working implementation on my fork (branch feature/mesh-input-api) and am happy to open a PR.


Motivation

The internal flow of get_shcoeffs is already:

binary image → vtkPolyData → spherical coordinates → SH expansion

The second half of that pipeline (mesh → SH) is exactly what mesh users need. Exposing it avoids:

  • Voxelization artifacts when converting a clean mesh back to an image
  • Unnecessary dependency on image resolution
  • Code duplication for users who manage their own mesh pipeline

Proposed API

get_shcoeffs_from_mesh(mesh, lmax, ...)

Accepts a vtk.vtkPolyData object. Returns the same nested-tuple structure as get_shcoeffs for drop-in compatibility, with image_ set to None:

from aicsshparam import shparam, shtools

mesh = shtools.get_mesh_from_vertices_faces(vertices, faces)

(coeffs, grid_rec), (_, mesh_out, grid, transform) = \
    shparam.get_shcoeffs_from_mesh(mesh=mesh, lmax=4)

mse = shtools.get_reconstruction_error(grid, grid_rec)

get_shcoeffs_from_vertices_faces(vertices, faces, lmax, ...)

Convenience wrapper for users who do not work with VTK directly:

import numpy as np
from aicsshparam import shparam

# vertices: (N, 3) float array; faces: (M, 3) int array
(coeffs, grid_rec), (_, mesh_out, grid, transform) = \
    shparam.get_shcoeffs_from_vertices_faces(
        vertices=vertices, faces=faces, lmax=4
    )

Implementation outline

New public functions in shparam.py:

  • get_shcoeffs_from_mesh — centers mesh on centroid, optionally aligns via PCA on vertex (x, y), then delegates to shared core
  • get_shcoeffs_from_vertices_faces — builds a vtkPolyData from numpy arrays via a new helper and calls the above
  • _get_shcoeffs_from_mesh_coords — private shared core extracted from get_shcoeffs; both the image path and mesh path delegate here (eliminates duplication)

New helpers in shtools.py:

  • get_mesh_from_vertices_faces(vertices, faces, center=True) — builds and validates a vtkPolyData
  • check_mesh_for_parametrization(mesh) — non-raising warnings for geometric assumption violations (non-watertight surface; centroid outside mesh)
  • align_points_2d(x, y, make_unique=False) — PCA-based 2D alignment on a point cloud; refactors the alignment logic shared between image and mesh paths

Validation approach:

Situation Behaviour
Empty mesh / zero points ValueError raised
Invalid vertex array shape, NaN/Inf values, out-of-range face indices ValueError raised
Non-watertight / non-manifold surface UserWarning (computation proceeds)
Centroid outside mesh (non-star-shaped proxy) UserWarning (computation proceeds)

Important design note: alignment difference between image and mesh paths

The 2D alignment angle is not guaranteed to be bit-identical between get_shcoeffs and get_shcoeffs_from_mesh for the same object, because they run PCA on different point sets:

  • Image path: PCA on all foreground voxel (x, y) coordinates (a volume point cloud), then rotates the whole image with interpolation, then re-meshes.
  • Mesh path: PCA on surface vertex (x, y) coordinates, then rotates the vertices directly (no interpolation).

The angle formula is shared, but the input point sets differ, so the resulting angle can differ slightly. For clean, well-aligned shapes the two paths agree closely.

Reproducing the image path exactly from a mesh input would require voxelizing the mesh first, which defeats the purpose. I wanted to flag this explicitly in case it affects downstream users comparing coefficients across the two paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions