-
Notifications
You must be signed in to change notification settings - Fork 0
Add MeshDenoiserKit Swift package for iOS/macOS #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
1d9808c
Add MeshDenoiserKit Swift package design spec
lanxinger a361709
Add MeshDenoiserKit implementation plan
lanxinger 5d5ec1b
Vendor Eigen 3.4.0 and OpenMesh 11 Core for Swift package
lanxinger 7a7f979
Add SwiftPM package skeleton with C API stub
lanxinger c54a640
Add progress callback and cancellation hook to denoiser core
lanxinger 965fe7c
Implement C bridge: buffers -> TriMesh -> SD-filter denoise -> buffers
lanxinger 6fd2bc4
Add MeshDenoiserKit Swift API with async denoise, progress, cancellation
lanxinger c6062e6
Add contract tests for errors, cancellation, and progress
lanxinger 1b94d35
Add golden-file parity test pinning kit output to CLI numerics
lanxinger 7b3dfaf
Add Swift package test job to CI
lanxinger e31fb9f
Document Swift package usage
lanxinger fd637b8
Address review: commit OBJ fixtures, stop exposing OpenMesh headers a…
lanxinger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // swift-tools-version:5.9 | ||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "MeshDenoiserKit", | ||
| platforms: [.iOS(.v17), .macOS(.v14)], | ||
| products: [ | ||
| .library(name: "MeshDenoiserKit", targets: ["MeshDenoiserKit"]), | ||
| ], | ||
| targets: [ | ||
| // OpenMesh headers deliberately live in "Headers", not the SPM default | ||
| // "include": they must stay textual includes, not a Clang module — | ||
| // some of them #include each other inside open namespaces, which | ||
| // breaks when clang maps the #include to a module import. | ||
| .target( | ||
| name: "OpenMeshCore", | ||
| path: "Sources/OpenMeshCore", | ||
| cxxSettings: [ | ||
| .define("OM_STATIC_BUILD"), | ||
| .headerSearchPath("Headers"), | ||
| ] | ||
| ), | ||
| .target( | ||
| name: "CMeshDenoiserCore", | ||
| dependencies: ["OpenMeshCore"], | ||
| path: "Sources/CMeshDenoiserCore", | ||
| publicHeadersPath: "include", | ||
| cxxSettings: [ | ||
| .headerSearchPath("../../src"), | ||
| .headerSearchPath("../../Vendor/eigen"), | ||
| .headerSearchPath("../OpenMeshCore/Headers"), | ||
| ] | ||
| ), | ||
| .target( | ||
| name: "MeshDenoiserKit", | ||
| dependencies: ["CMeshDenoiserCore"], | ||
| path: "Sources/MeshDenoiserKit" | ||
| ), | ||
| .testTarget( | ||
| name: "MeshDenoiserKitTests", | ||
| dependencies: ["MeshDenoiserKit"], | ||
| path: "Tests/MeshDenoiserKitTests", | ||
| resources: [.copy("Fixtures")] | ||
| ), | ||
| ], | ||
| cxxLanguageStandard: .cxx17 | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| #include "CMeshDenoiserCore.h" | ||
|
|
||
| #include "MeshTypes.h" | ||
| #include "MeshNormalDenoising.h" | ||
|
|
||
| #include <Eigen/Core> | ||
| #include <cmath> | ||
| #include <vector> | ||
|
|
||
| void md_params_init_defaults(md_params *params) | ||
| { | ||
| params->lambda = 0.15; | ||
| params->eta = 2.2; | ||
| params->mu = 0.2; | ||
| params->nu = 0.25; | ||
| params->mesh_update_closeness_weight = 0.001; | ||
| params->mesh_update_iterations = 20; | ||
| params->mesh_update_displacement_eps = 0.1; | ||
| params->outer_iterations = 1; | ||
| params->linear_solver_type = 1; | ||
| params->deterministic = 0; | ||
| } | ||
|
|
||
| md_status md_denoise(const float *vertices, size_t vertex_count, | ||
| const uint32_t *indices, size_t triangle_count, | ||
| const md_params *params, | ||
| float *out_vertices, | ||
| md_progress_fn progress, void *ctx) | ||
| { | ||
| if(vertices == nullptr || indices == nullptr || params == nullptr || out_vertices == nullptr || | ||
| vertex_count == 0 || triangle_count == 0){ | ||
| return MD_ERR_INVALID_INPUT; | ||
| } | ||
|
|
||
| for(size_t i = 0; i < vertex_count * 3; ++i){ | ||
| if(!std::isfinite(vertices[i])){ | ||
| return MD_ERR_INVALID_INPUT; | ||
| } | ||
| } | ||
| for(size_t i = 0; i < triangle_count * 3; ++i){ | ||
| if(indices[i] >= vertex_count){ | ||
| return MD_ERR_INVALID_INPUT; | ||
| } | ||
| } | ||
|
|
||
| SDFilter::MeshDenoisingParameters param; | ||
| param.lambda = params->lambda; | ||
| param.eta = params->eta; | ||
| param.mu = params->mu; | ||
| param.nu = params->nu; | ||
| param.mesh_update_closeness_weight = params->mesh_update_closeness_weight; | ||
| param.mesh_update_iter = params->mesh_update_iterations; | ||
| param.mesh_update_disp_eps = params->mesh_update_displacement_eps; | ||
| param.outer_iterations = params->outer_iterations; | ||
| param.deterministic_mode = (params->deterministic != 0); | ||
| switch(params->linear_solver_type){ | ||
| case 0: param.linear_solver_type = SDFilter::Parameters::CG; break; | ||
| case 1: param.linear_solver_type = SDFilter::Parameters::LDLT; break; | ||
| default: return MD_ERR_INVALID_PARAMS; | ||
| } | ||
| if(!param.valid_parameters()){ | ||
| return MD_ERR_INVALID_PARAMS; | ||
| } | ||
|
|
||
| if(param.deterministic_mode){ | ||
| Eigen::setNbThreads(1); | ||
| } | ||
|
|
||
| TriMesh mesh; | ||
| std::vector<TriMesh::VertexHandle> vhandles; | ||
| vhandles.reserve(vertex_count); | ||
| for(size_t i = 0; i < vertex_count; ++i){ | ||
| vhandles.push_back(mesh.add_vertex(TriMesh::Point( | ||
| static_cast<double>(vertices[3 * i]), | ||
| static_cast<double>(vertices[3 * i + 1]), | ||
| static_cast<double>(vertices[3 * i + 2])))); | ||
| } | ||
| for(size_t i = 0; i < triangle_count; ++i){ | ||
| TriMesh::FaceHandle fh = mesh.add_face( | ||
| vhandles[indices[3 * i]], | ||
| vhandles[indices[3 * i + 1]], | ||
| vhandles[indices[3 * i + 2]]); | ||
| if(!fh.is_valid()){ | ||
| return MD_ERR_INVALID_INPUT; // degenerate or non-manifold face | ||
| } | ||
| } | ||
|
|
||
| // Same pipeline as the CLI: normalize -> denoise -> restore. | ||
| Eigen::Vector3d original_center; | ||
| double original_scale = 0.0; | ||
| SDFilter::normalize_mesh(mesh, original_center, original_scale); | ||
|
|
||
| SDFilter::MeshNormalDenoising denoiser(mesh); | ||
| if(progress != nullptr){ | ||
| denoiser.set_progress_callback([progress, ctx](int completed, int total) -> bool { | ||
| return progress(completed, total, ctx); | ||
| }); | ||
| } | ||
|
|
||
| TriMesh output_mesh; | ||
| if(!denoiser.denoise(param, output_mesh)){ | ||
| return denoiser.cancelled() ? MD_CANCELLED : MD_ERR_SOLVER_FAILED; | ||
| } | ||
|
|
||
| SDFilter::restore_mesh(output_mesh, original_center, original_scale); | ||
|
|
||
| for(TriMesh::ConstVertexIter cv_it = output_mesh.vertices_begin(); | ||
| cv_it != output_mesh.vertices_end(); ++cv_it){ | ||
| const TriMesh::Point &pt = output_mesh.point(*cv_it); | ||
| const int idx = cv_it->idx(); | ||
| out_vertices[3 * idx] = static_cast<float>(pt[0]); | ||
| out_vertices[3 * idx + 1] = static_cast<float>(pt[1]); | ||
| out_vertices[3 * idx + 2] = static_cast<float>(pt[2]); | ||
| } | ||
|
|
||
| return MD_OK; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // C API for the SD-filter mesh denoiser (algorithm: Zhang et al., arXiv:1712.03574). | ||
| // Buffers in / buffers out; vertex count and order are preserved. | ||
| #ifndef CMESHDENOISERCORE_H | ||
| #define CMESHDENOISERCORE_H | ||
|
|
||
| #include <stdbool.h> | ||
| #include <stddef.h> | ||
| #include <stdint.h> | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" { | ||
| #endif | ||
|
|
||
| typedef struct { | ||
| double lambda; // regularization weight (> 0) | ||
| double eta; // spatial Gaussian sigma, scaled by avg face-centroid distance (> 0) | ||
| double mu; // guidance normal difference weight (> 0) | ||
| double nu; // signal normal difference weight (> 0) | ||
| double mesh_update_closeness_weight; // vertex position fidelity (>= 0) | ||
| int mesh_update_iterations; // max vertex-update iterations per outer iteration (> 0) | ||
| double mesh_update_displacement_eps; // early-stop RMS displacement threshold (<= 0 disables) | ||
| int outer_iterations; // full filtering passes (> 0) | ||
| int linear_solver_type; // 0 = conjugate gradient, 1 = LDLT (default) | ||
| int deterministic; // nonzero forces single-threaded Eigen for reproducible output | ||
| } md_params; | ||
|
|
||
| typedef enum { | ||
| MD_OK = 0, | ||
| MD_ERR_INVALID_INPUT = 1, // NULL/empty buffers, NaN/Inf coords, bad indices, non-manifold face | ||
| MD_ERR_INVALID_PARAMS = 2, | ||
| MD_ERR_SOLVER_FAILED = 3, | ||
| MD_CANCELLED = 4 | ||
| } md_status; | ||
|
|
||
| // Called after each completed outer iteration. Return false to cancel. | ||
| typedef bool (*md_progress_fn)(int completed_outer, int total_outer, void *ctx); | ||
|
|
||
| // Fills params with the tuned defaults from MeshDenoiserDefaults.txt. | ||
| void md_params_init_defaults(md_params *params); | ||
|
|
||
| // vertices: xyz triples, vertex_count points. | ||
| // indices: 3 per triangle, triangle_count triangles, each index < vertex_count. | ||
| // out_vertices: caller-allocated, 3 * vertex_count floats. Written only on MD_OK. | ||
| // progress may be NULL; ctx is passed through to it. | ||
| md_status md_denoise(const float *vertices, size_t vertex_count, | ||
| const uint32_t *indices, size_t triangle_count, | ||
| const md_params *params, | ||
| float *out_vertices, | ||
| md_progress_fn progress, void *ctx); | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif | ||
|
|
||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /// Errors thrown by ``MeshDenoiser/denoise(positions:indices:parameters:progress:)``. | ||
| /// Cancellation is reported as `CancellationError`, not through this enum. | ||
| public enum MeshDenoiseError: Error, Equatable { | ||
| /// Empty buffers, NaN/Inf coordinates, out-of-range indices, or a degenerate/non-manifold face. | ||
| case invalidInput | ||
| /// A parameter is out of its documented range. | ||
| case invalidParameters | ||
| /// The sparse solver failed to factorize or converge. | ||
| case solverFailed | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import CMeshDenoiserCore | ||
|
|
||
| /// SD-filter denoising parameters. Defaults are the tuned values from MeshDenoiserDefaults.txt. | ||
| public struct MeshDenoiseParameters: Sendable, Equatable { | ||
|
|
||
| public enum LinearSolver: Int32, Sendable { | ||
| /// Iterative conjugate gradient — lower memory, use for very large meshes (>~500k vertices). | ||
| case conjugateGradient = 0 | ||
| /// Direct sparse Cholesky (default) — fastest for typical mesh sizes. | ||
| case ldlt = 1 | ||
| } | ||
|
|
||
| /// Regularization weight. Higher = more smoothing per iteration. Must be > 0. | ||
| public var lambda: Double = 0.15 | ||
| /// Spatial Gaussian sigma, scaled by average face-centroid distance. Must be > 0. | ||
| public var eta: Double = 2.2 | ||
| /// Guidance normal difference weight. Must be > 0. | ||
| public var mu: Double = 0.2 | ||
| /// Signal normal difference weight. Must be > 0. | ||
| public var nu: Double = 0.25 | ||
| /// Vertex position fidelity during mesh update. Must be >= 0. | ||
| public var meshUpdateClosenessWeight: Double = 0.001 | ||
| /// Max vertex-update iterations per outer iteration. Must be > 0. | ||
| public var meshUpdateIterations: Int = 20 | ||
| /// Early-stop RMS displacement threshold for mesh update; <= 0 disables. | ||
| public var meshUpdateDisplacementEps: Double = 0.1 | ||
| /// Number of full filtering passes. More = more smoothing. Must be > 0. | ||
| public var outerIterations: Int = 1 | ||
| public var linearSolver: LinearSolver = .ldlt | ||
| /// Forces single-threaded execution for bit-reproducible output. | ||
| public var deterministic: Bool = false | ||
|
|
||
| public init() {} | ||
|
|
||
| var cParams: md_params { | ||
| var c = md_params() | ||
| md_params_init_defaults(&c) | ||
| c.lambda = lambda | ||
| c.eta = eta | ||
| c.mu = mu | ||
| c.nu = nu | ||
| c.mesh_update_closeness_weight = meshUpdateClosenessWeight | ||
| c.mesh_update_iterations = Int32(meshUpdateIterations) | ||
| c.mesh_update_displacement_eps = meshUpdateDisplacementEps | ||
| c.outer_iterations = Int32(outerIterations) | ||
| c.linear_solver_type = linearSolver.rawValue | ||
| c.deterministic = deterministic ? 1 : 0 | ||
| return c | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test target declares
Fixturesas a copied resource, but the committed tree underTests/MeshDenoiserKitTestscontains only the Swift test/helper files and noFixturesdirectory;swift testalready warnsInvalid Resource 'Fixtures': File not found. Once the package compiles,GoldenParityTestsforce-loadsnoisy_icosphere.objandgolden_denoised.objfrom that resource directory, so the package test suite will fail unless those OBJ fixtures are committed or the resource/test is adjusted.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in fd637b8. Root cause: the repo's pre-existing
*.objgitignore rule (intended for MSVC object files) silently excluded the Wavefront fixtures fromgit add. Added a negation forTests/MeshDenoiserKitTests/Fixtures/*.objand committed both OBJ files;swift testpasses 8/8 from a clean checkout state.