Skip to content

Commit 304b1eb

Browse files
Refactor mesh smoothing function to the mesh header and module
1 parent 13566a2 commit 304b1eb

11 files changed

Lines changed: 264 additions & 242 deletions

File tree

MIGRATION_GUIDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,16 @@ Important details:
19331933
See `development/mesh/check_marching_cubes.py` for reference comparisons and
19341934
`development/mesh/benchmark_marching_cubes.py` for reproducible timings.
19351935

1936+
### Mesh Smoothing
1937+
1938+
Laplacian mesh smoothing is available as `bic.mesh.smooth_mesh`. It was moved
1939+
from `bic.utils.smooth_mesh`; the old location is no longer available. The C++
1940+
API likewise moved from `#include "bioimage_cpp/mesh_smoothing.hxx"` and
1941+
`bioimage_cpp::smooth_mesh` to `#include "bioimage_cpp/mesh/smoothing.hxx"`
1942+
and `bioimage_cpp::mesh::smooth_mesh`.
1943+
1944+
1945+
19361946
### Anti-Aliased Resampling
19371947

19381948
`affine_transform` itself never pre-smooths the input; downsampling without

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Image processing and segmentation functionality in C++ with light-weight python bindings through nanobind and minimal dependencies to enable distribution via pip.
99

1010
The package includes dependency-free triangle-mesh extraction from 3D volumes
11-
and segmentation masks under `bioimage_cpp.mesh`.
11+
and segmentation masks, plus Laplacian mesh smoothing, under `bioimage_cpp.mesh`.
1212

1313
The `bioimage_cpp` python library can be installed via pip:
1414
```bash
File renamed without changes.

development/utils/benchmark_mesh_smoothing.py renamed to development/mesh/benchmark_mesh_smoothing.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
99
Run::
1010
11-
python development/utils/benchmark_mesh_smoothing.py --n-vertices 5000 --iterations 5 --repeats 3
12-
python development/utils/benchmark_mesh_smoothing.py --n-vertices 20000 --threads 1,2,4,0
11+
python development/mesh/benchmark_mesh_smoothing.py --n-vertices 5000 --iterations 5 --repeats 3
12+
python development/mesh/benchmark_mesh_smoothing.py --n-vertices 20000 --threads 1,2,4,0
1313
1414
Notes
1515
-----
@@ -117,7 +117,7 @@ def main() -> int:
117117
label = f"bioimage_cpp[n_threads={n_threads}]"
118118

119119
def run(verts=mesh.verts, normals=mesh.normals, faces=mesh.faces, n=n_threads):
120-
bic.utils.smooth_mesh(verts, normals, faces, iterations=args.iterations, n_threads=n)
120+
bic.mesh.smooth_mesh(verts, normals, faces, iterations=args.iterations, n_threads=n)
121121

122122
times = time_call(run, args.repeats, args.warmup)
123123
rows.append(
@@ -141,7 +141,7 @@ def run_ref():
141141
# exact agreement; see module docstring).
142142
if reference is not None:
143143
ref_v, ref_n = reference(mesh.verts, mesh.normals, mesh.faces, 1)
144-
ours_v, ours_n = bic.utils.smooth_mesh(mesh.verts, mesh.normals, mesh.faces, iterations=1)
144+
ours_v, ours_n = bic.mesh.smooth_mesh(mesh.verts, mesh.normals, mesh.faces, iterations=1)
145145
v_max_diff = float(np.max(np.abs(ours_v - ref_v)))
146146
n_max_diff = float(np.max(np.abs(ours_n - ref_n)))
147147
print(f"Correctness vs reference (iterations=1): "
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
#include <utility>
1313
#include <vector>
1414

15-
namespace bioimage_cpp {
15+
namespace bioimage_cpp::mesh {
1616

17-
namespace detail::mesh_smoothing {
17+
namespace detail::smoothing {
1818

1919
template <class I>
2020
struct Adjacency {
@@ -74,7 +74,7 @@ Adjacency<I> build_adjacency(const ConstArrayView<I> &faces, std::ptrdiff_t n_ve
7474
return Adjacency<I>{std::move(offsets), std::move(neighbours)};
7575
}
7676

77-
} // namespace detail::mesh_smoothing
77+
} // namespace detail::smoothing
7878

7979
// Laplacian smoothing of a triangular mesh: each vertex (and corresponding
8080
// normal) is replaced by the mean of itself and its 1-ring neighbours,
@@ -120,7 +120,7 @@ void smooth_mesh(
120120
return;
121121
}
122122

123-
const auto adjacency = detail::mesh_smoothing::build_adjacency<I>(faces, n_verts);
123+
const auto adjacency = detail::smoothing::build_adjacency<I>(faces, n_verts);
124124

125125
std::vector<V> scratch_verts(n_total);
126126
std::vector<V> scratch_normals(n_total);
@@ -140,13 +140,13 @@ void smooth_mesh(
140140

141141
const auto &offsets = adjacency.offsets;
142142
const auto &neighbours = adjacency.neighbours;
143-
const std::size_t threads = detail::normalize_thread_count(
143+
const std::size_t threads = bioimage_cpp::detail::normalize_thread_count(
144144
n_threads < 0 ? 0 : static_cast<std::size_t>(n_threads),
145145
static_cast<std::size_t>(n_verts)
146146
);
147147

148148
auto smooth_pass = [&](const V *src_verts, const V *src_normals, V *dst_verts, V *dst_normals) {
149-
detail::parallel_for_chunks(
149+
bioimage_cpp::detail::parallel_for_chunks(
150150
threads,
151151
static_cast<std::size_t>(n_verts),
152152
[&](std::size_t, std::size_t begin, std::size_t end) {
@@ -184,4 +184,4 @@ void smooth_mesh(
184184
}
185185
}
186186

187-
} // namespace bioimage_cpp
187+
} // namespace bioimage_cpp::mesh

src/bindings/mesh.cxx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
#include "bioimage_cpp/array_view.hxx"
44
#include "bioimage_cpp/detail/profile.hxx"
55
#include "bioimage_cpp/mesh/marching_cubes.hxx"
6+
#include "bioimage_cpp/mesh/smoothing.hxx"
67

78
#include <nanobind/ndarray.h>
89
#include <nanobind/stl/optional.h>
10+
#include <nanobind/stl/pair.h>
911

1012
#include <cstddef>
1113
#include <cstdint>
@@ -26,6 +28,12 @@ using UInt8Input = nb::ndarray<nb::numpy, const std::uint8_t, nb::c_contig>;
2628
using FloatOutput = nb::ndarray<nb::numpy, float, nb::c_contig>;
2729
using Int32Output = nb::ndarray<nb::numpy, std::int32_t, nb::c_contig>;
2830

31+
template <class T>
32+
using InputArray = nb::ndarray<nb::numpy, const T, nb::c_contig>;
33+
34+
template <class T>
35+
using OutputArray = nb::ndarray<nb::numpy, T, nb::c_contig>;
36+
2937
template <class T, class Output>
3038
Output output_array(std::vector<T> &&values, const std::vector<std::size_t> &shape) {
3139
if (values.empty()) {
@@ -58,6 +66,77 @@ std::vector<std::ptrdiff_t> shape_of(const FloatInput &array) {
5866
return shape;
5967
}
6068

69+
using FacesArray = nb::ndarray<nb::numpy, const std::int64_t, nb::c_contig>;
70+
71+
template <class V>
72+
std::pair<OutputArray<V>, OutputArray<V>> smooth_mesh_t(
73+
InputArray<V> verts,
74+
InputArray<V> normals,
75+
FacesArray faces,
76+
std::size_t iterations,
77+
int n_threads
78+
) {
79+
if (verts.ndim() != 2) {
80+
throw std::invalid_argument(
81+
"verts must have ndim=2, got ndim=" + std::to_string(verts.ndim())
82+
);
83+
}
84+
if (normals.ndim() != 2 || normals.shape(0) != verts.shape(0)
85+
|| normals.shape(1) != verts.shape(1)) {
86+
throw std::invalid_argument("normals must have the same shape as verts");
87+
}
88+
if (faces.ndim() != 2 || faces.shape(1) != 3) {
89+
throw std::invalid_argument("faces must have shape (n_faces, 3)");
90+
}
91+
92+
const std::size_t n_verts_size = verts.shape(0);
93+
const std::size_t dim_size = verts.shape(1);
94+
const std::size_t n_faces_size = faces.shape(0);
95+
const std::size_t n_total = n_verts_size * dim_size;
96+
97+
std::vector<std::size_t> verts_ndarray_shape{n_verts_size, dim_size};
98+
std::vector<std::ptrdiff_t> verts_view_shape{
99+
static_cast<std::ptrdiff_t>(n_verts_size),
100+
static_cast<std::ptrdiff_t>(dim_size),
101+
};
102+
std::vector<std::ptrdiff_t> faces_view_shape{
103+
static_cast<std::ptrdiff_t>(n_faces_size),
104+
std::ptrdiff_t{3},
105+
};
106+
107+
auto *verts_data = new V[n_total]();
108+
nb::capsule verts_owner(verts_data, [](void *p) noexcept { delete[] static_cast<V *>(p); });
109+
auto *normals_data = new V[n_total]();
110+
nb::capsule normals_owner(normals_data, [](void *p) noexcept { delete[] static_cast<V *>(p); });
111+
112+
ConstArrayView<V> verts_view{verts.data(), verts_view_shape, {}};
113+
ConstArrayView<V> normals_view{normals.data(), verts_view_shape, {}};
114+
ConstArrayView<std::int64_t> faces_view{faces.data(), faces_view_shape, {}};
115+
ArrayView<V> out_verts_view{verts_data, verts_view_shape, {}};
116+
ArrayView<V> out_normals_view{normals_data, verts_view_shape, {}};
117+
118+
{
119+
nb::gil_scoped_release release;
120+
mesh::smooth_mesh<V, std::int64_t>(
121+
verts_view,
122+
normals_view,
123+
faces_view,
124+
iterations,
125+
n_threads,
126+
out_verts_view,
127+
out_normals_view
128+
);
129+
}
130+
131+
OutputArray<V> out_verts(
132+
verts_data, verts_ndarray_shape.size(), verts_ndarray_shape.data(), verts_owner
133+
);
134+
OutputArray<V> out_normals(
135+
normals_data, verts_ndarray_shape.size(), verts_ndarray_shape.data(), normals_owner
136+
);
137+
return {std::move(out_verts), std::move(out_normals)};
138+
}
139+
61140
nb::tuple marching_cubes_float32(
62141
FloatInput volume,
63142
const double level,
@@ -150,6 +229,26 @@ nb::tuple marching_cubes_float32(
150229
} // namespace
151230

152231
void bind_mesh(nb::module_ &m) {
232+
m.def(
233+
"_smooth_mesh_float32",
234+
&smooth_mesh_t<float>,
235+
nb::arg("verts"),
236+
nb::arg("normals"),
237+
nb::arg("faces"),
238+
nb::arg("iterations"),
239+
nb::arg("n_threads"),
240+
"Laplacian smoothing of a triangular mesh with float32 vertices and normals."
241+
);
242+
m.def(
243+
"_smooth_mesh_float64",
244+
&smooth_mesh_t<double>,
245+
nb::arg("verts"),
246+
nb::arg("normals"),
247+
nb::arg("faces"),
248+
nb::arg("iterations"),
249+
nb::arg("n_threads"),
250+
"Laplacian smoothing of a triangular mesh with float64 vertices and normals."
251+
);
153252
m.def(
154253
"_marching_cubes_float32",
155254
&marching_cubes_float32,

src/bindings/utils.cxx

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
#include "utils.hxx"
22

33
#include "bioimage_cpp/array_view.hxx"
4-
#include "bioimage_cpp/mesh_smoothing.hxx"
54
#include "bioimage_cpp/run_length.hxx"
65
#include "bioimage_cpp/take_dict.hxx"
76

87
#include <nanobind/ndarray.h>
9-
#include <nanobind/stl/pair.h>
108
#include <nanobind/stl/string.h>
119

1210
#include <algorithm>
@@ -82,76 +80,6 @@ OutputArray<T> take_dict_t(const nb::dict &relabeling, InputArray<T> to_relabel)
8280
return OutputArray<T>(data, ndarray_shape.size(), ndarray_shape.data(), owner);
8381
}
8482

85-
using FacesArray = nb::ndarray<nb::numpy, const std::int64_t, nb::c_contig>;
86-
87-
template <class V>
88-
std::pair<OutputArray<V>, OutputArray<V>> smooth_mesh_t(
89-
InputArray<V> verts,
90-
InputArray<V> normals,
91-
FacesArray faces,
92-
std::size_t iterations,
93-
int n_threads
94-
) {
95-
if (verts.ndim() != 2) {
96-
throw std::invalid_argument(
97-
"verts must have ndim=2, got ndim=" + std::to_string(verts.ndim())
98-
);
99-
}
100-
if (normals.ndim() != 2 || normals.shape(0) != verts.shape(0)
101-
|| normals.shape(1) != verts.shape(1)) {
102-
throw std::invalid_argument("normals must have the same shape as verts");
103-
}
104-
if (faces.ndim() != 2 || faces.shape(1) != 3) {
105-
throw std::invalid_argument("faces must have shape (n_faces, 3)");
106-
}
107-
108-
const std::size_t n_verts_size = verts.shape(0);
109-
const std::size_t dim_size = verts.shape(1);
110-
const std::size_t n_faces_size = faces.shape(0);
111-
const std::size_t n_total = n_verts_size * dim_size;
112-
113-
std::vector<std::size_t> verts_ndarray_shape{n_verts_size, dim_size};
114-
std::vector<std::ptrdiff_t> verts_view_shape{
115-
static_cast<std::ptrdiff_t>(n_verts_size),
116-
static_cast<std::ptrdiff_t>(dim_size),
117-
};
118-
std::vector<std::ptrdiff_t> faces_view_shape{
119-
static_cast<std::ptrdiff_t>(n_faces_size),
120-
std::ptrdiff_t{3},
121-
};
122-
123-
auto *verts_data = new V[n_total]();
124-
nb::capsule verts_owner(verts_data, [](void *p) noexcept { delete[] static_cast<V *>(p); });
125-
auto *normals_data = new V[n_total]();
126-
nb::capsule normals_owner(normals_data, [](void *p) noexcept { delete[] static_cast<V *>(p); });
127-
128-
ConstArrayView<V> verts_view{verts.data(), verts_view_shape, {}};
129-
ConstArrayView<V> normals_view{normals.data(), verts_view_shape, {}};
130-
ConstArrayView<std::int64_t> faces_view{faces.data(), faces_view_shape, {}};
131-
ArrayView<V> out_verts_view{verts_data, verts_view_shape, {}};
132-
ArrayView<V> out_normals_view{normals_data, verts_view_shape, {}};
133-
134-
{
135-
nb::gil_scoped_release release;
136-
smooth_mesh<V, std::int64_t>(
137-
verts_view,
138-
normals_view,
139-
faces_view,
140-
iterations,
141-
n_threads,
142-
out_verts_view,
143-
out_normals_view
144-
);
145-
}
146-
147-
OutputArray<V> out_verts(
148-
verts_data, verts_ndarray_shape.size(), verts_ndarray_shape.data(), verts_owner
149-
);
150-
OutputArray<V> out_normals(
151-
normals_data, verts_ndarray_shape.size(), verts_ndarray_shape.data(), normals_owner
152-
);
153-
return {std::move(out_verts), std::move(out_normals)};
154-
}
15583

15684
template <class T>
15785
OutputArray<std::int64_t> compute_rle_t(InputArray<T> mask) {
@@ -213,26 +141,6 @@ void bind_utils(nb::module_ &m) {
213141
nb::arg("to_relabel"),
214142
"Map a contiguous int64 array through an integer dictionary."
215143
);
216-
m.def(
217-
"_smooth_mesh_float32",
218-
&smooth_mesh_t<float>,
219-
nb::arg("verts"),
220-
nb::arg("normals"),
221-
nb::arg("faces"),
222-
nb::arg("iterations"),
223-
nb::arg("n_threads"),
224-
"Laplacian smoothing of a triangular mesh with float32 vertices and normals."
225-
);
226-
m.def(
227-
"_smooth_mesh_float64",
228-
&smooth_mesh_t<double>,
229-
nb::arg("verts"),
230-
nb::arg("normals"),
231-
nb::arg("faces"),
232-
nb::arg("iterations"),
233-
nb::arg("n_threads"),
234-
"Laplacian smoothing of a triangular mesh with float64 vertices and normals."
235-
);
236144
m.def("_compute_rle_bool", &compute_rle_t<bool>, nb::arg("mask"),
237145
"COCO-style binary run-length encoding of a contiguous bool array.");
238146
m.def("_compute_rle_uint8", &compute_rle_t<std::uint8_t>, nb::arg("mask"),

src/bioimage_cpp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
- `distance`: distance transform functionality.
5757
- `filters`: efficient implementation of convolutional image filters.
5858
- `graph`: graph creation and graph (partitioning) algorithms.
59-
- `mesh`: triangle-mesh extraction from 3D volumes and segmentation masks.
59+
- `mesh`: triangle-mesh extraction and processing.
6060
- `segmentation`: image segmentation functionality.
6161
- `transformation`: affine transformations.
6262
- `utils`: misc utility functionality.

0 commit comments

Comments
 (0)