From 21e2f532b17ba8150fc317bee9061fdaccda596c Mon Sep 17 00:00:00 2001 From: Yuichiro Aoki <45054071+yuichiroaoki@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:00:44 +0900 Subject: [PATCH] Circle (#20) * find circle radius and center * update readme --- .gitignore | 3 +- README.md | 16 ++++++++++- cnceye/__init__.py | 1 + cnceye/arc.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ cnceye/shape.py | 49 ++++++++++++++++++++++++++----- poetry.lock | 44 +++++++++++++++++++++++++++- pyproject.toml | 3 +- tests/test_arc.py | 42 +++++++++++++++++++++++++++ tests/test_shape.py | 10 ++++++- 9 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 cnceye/arc.py create mode 100644 tests/test_arc.py diff --git a/.gitignore b/.gitignore index 4f3bc0d..51e5ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ cython_debug/ #.idea/ output -lines \ No newline at end of file +lines +.vscode \ No newline at end of file diff --git a/README.md b/README.md index acf52e4..976953a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,24 @@ # cnceye ![Test](https://github.com/OpenCMM/cnceye/actions/workflows/ci.yml/badge.svg) -cnceye measures the dimensions of a workpiece using the a laser triagulation sensor on a CNC machine. +cnceye analyzes 3D models from a stl file and find measuring lines and curves. ![a laser triagulation sensor](https://opencmm.xyz/assets/images/sensor-55b7cf98350f293eba2c2b9d593bdd4f.png) +## Installation +```bash +pip install cnceye +``` + +## Usage + +```python +from cnceye import Shape + +shape = Shape("tests/fixtures/stl/sample.stl") +lines, arcs = shape.get_lines_and_arcs() +``` + ## Simulation with Blender Create test data diff --git a/cnceye/__init__.py b/cnceye/__init__.py index e69de29..f3c3b03 100644 --- a/cnceye/__init__.py +++ b/cnceye/__init__.py @@ -0,0 +1 @@ +from .shape import Shape # noqa: F401 \ No newline at end of file diff --git a/cnceye/arc.py b/cnceye/arc.py new file mode 100644 index 0000000..ac91ad2 --- /dev/null +++ b/cnceye/arc.py @@ -0,0 +1,70 @@ +import numpy as np +from scipy.optimize import least_squares + + +def get_edges_for_arc(arc_points: np.ndarray, number_of_edges: int): + """ + Returns a list of edges for an arc/circle + Edges need to be distributed evenly along the arc + """ + count = len(arc_points) + if count < 4: + raise ValueError("Not enough points to define arc") + + if number_of_edges < 3: + raise ValueError("Not enough edges to define arc") + + interval = count // number_of_edges + + edges = [] + for i in range(number_of_edges): + x, y, z = arc_points[i * interval].tolist() + edges.append((round(x, 3), round(y, 3), round(z, 3))) + + return edges + + +def get_arc_info(arc_points: np.ndarray, decimal_places: int = 3): + """ + Get information about arc + + Parameters + ---------- + arc_points : list + List of arc coordinates [(x,y,z), (x,y,z)] + decimal_places : int + Number of decimal places to round to + + Returns + ------- + radius : float + Radius of arc + center : np.array + Center of arc + """ + center_x, center_y, radius = fit_circle(arc_points[:, :2]) + center = np.array([center_x, center_y, arc_points[0, 2]]) + center = np.round(center, decimal_places) + radius = round(radius, decimal_places) + return radius, center + + +def fit_circle(points): + x = points[:, 0] + y = points[:, 1] + + initial_params = ( + np.mean(x), + np.mean(y), + np.std(np.sqrt((x - np.mean(x)) ** 2 + (y - np.mean(y)) ** 2)), + ) + + result = least_squares(circle_residuals, initial_params, args=(x, y)) + cx, cy, r = result.x + + return cx, cy, r + + +def circle_residuals(params, x, y): + cx, cy, r = params + return (x - cx) ** 2 + (y - cy) ** 2 - r**2 diff --git a/cnceye/shape.py b/cnceye/shape.py index c8af46e..98839a5 100644 --- a/cnceye/shape.py +++ b/cnceye/shape.py @@ -1,6 +1,7 @@ import trimesh import numpy as np from trimesh.graph import face_adjacency +from .arc import get_arc_info class Shape: @@ -80,6 +81,27 @@ def group_by_coplanar_facets(self, facet_indices: np.ndarray): return coplanar_facets def get_lines_and_arcs(self, decimal_places: int = 3, arc_threshold: int = 1): + """ + Extract lines and arcs from an STL file \n + If the line length is less than 1, it is considered an arc. + If the line length for an arc is close to the previous arc length, + it is considered part of the previous arc. \n + Note: This is not a robust algorithm. + + Parameters + ---------- + decimal_places : int + Number of decimal places to round to + arc_threshold : int + Length threshold to determine if a line is an arc + + Returns + ------- + lines : list + List of lines + arcs : list + List of arcs + """ shapes = self.get_shapes() lines = [] arcs = [] @@ -118,13 +140,6 @@ def get_lines_and_arcs(self, decimal_places: int = 3, arc_threshold: int = 1): return lines, arcs def get_shapes(self): - """ - Extract lines and arcs from an STL file \n - If the line length is less than 1, it is considered an arc. \n - if the line length for an arc is close to the previous arc length, - it is considered part of the previous arc. \n - Note: This is not a robust algorithm. - """ visible_facet_indices = self.get_visible_facets() group_facets = self.group_by_coplanar_facets(visible_facet_indices) adjacency = face_adjacency(self.mesh.faces) @@ -152,6 +167,26 @@ def get_shapes(self): return shapes + def get_arc_info(self, arc_points: np.ndarray, decimal_places: int = 3): + """ + Get information about arc + + Parameters + ---------- + arc_points : list + List of arc coordinates [(x,y,z), (x,y,z)] + decimal_places : int + Number of decimal places to round to + + Returns + ------- + radius : float + Radius of arc + center : np.array + Center of arc + """ + return get_arc_info(arc_points, decimal_places=decimal_places) + def round_shape_values(shapes: np.ndarray, decimal_places: int = 3): for i in range(len(shapes)): diff --git a/poetry.lock b/poetry.lock index 32fb190..f6d0f9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -315,6 +315,48 @@ files = [ {file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"}, ] +[[package]] +name = "scipy" +version = "1.11.4" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710"}, + {file = "scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41"}, + {file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4"}, + {file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56"}, + {file = "scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446"}, + {file = "scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3"}, + {file = "scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be"}, + {file = "scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8"}, + {file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c"}, + {file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff"}, + {file = "scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993"}, + {file = "scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd"}, + {file = "scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6"}, + {file = "scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d"}, + {file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4"}, + {file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79"}, + {file = "scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660"}, + {file = "scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97"}, + {file = "scipy-1.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e619aba2df228a9b34718efb023966da781e89dd3d21637b27f2e54db0410d7"}, + {file = "scipy-1.11.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f3cd9e7b3c2c1ec26364856f9fbe78695fe631150f94cd1c22228456404cf1ec"}, + {file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10e45a6c50211fe256da61a11c34927c68f277e03138777bdebedd933712fea"}, + {file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91af76a68eeae0064887a48e25c4e616fa519fa0d38602eda7e0f97d65d57937"}, + {file = "scipy-1.11.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6df1468153a31cf55ed5ed39647279beb9cfb5d3f84369453b49e4b8502394fd"}, + {file = "scipy-1.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee410e6de8f88fd5cf6eadd73c135020bfbbbdfcd0f6162c36a7638a1ea8cc65"}, + {file = "scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa"}, +] + +[package.dependencies] +numpy = ">=1.21.6,<1.28.0" + +[package.extras] +dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "trimesh" version = "4.0.5" @@ -338,4 +380,4 @@ test = ["black", "coveralls", "ezdxf", "matplotlib", "mypy", "pyinstrument", "py [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "d7fa4333381913b4db438325584c855af2255316b72d2f5d3ce8b4af1d62a4cb" +content-hash = "c9d40102369a4b1ba096e8023dd7197439365653bfa6fb079ab8fc20c2e14e58" diff --git a/pyproject.toml b/pyproject.toml index ccdd867..baf17ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cnceye" -version = "0.4.3" +version = "0.5.0" description = "CMM python library" license = "MIT" authors = ["yuichiroaoki <45054071+yuichiroaoki@users.noreply.github.com>"] @@ -13,6 +13,7 @@ python = ">=3.11,<3.13" trimesh = "^4.0.5" numpy = "^1.26.2" rtree = "^1.1.0" +scipy = "^1.11.4" [tool.poetry.group.dev.dependencies] diff --git a/tests/test_arc.py b/tests/test_arc.py new file mode 100644 index 0000000..a9c1b56 --- /dev/null +++ b/tests/test_arc.py @@ -0,0 +1,42 @@ +from cnceye import Shape +from cnceye.arc import ( + fit_circle, + get_arc_info, + get_edges_for_arc, +) + + +def test_fit_circle(): + shape = Shape("tests/fixtures/stl/sample.stl") + lines, arcs = shape.get_lines_and_arcs() + for arc_points in arcs[0]: + points = arc_points[:, :2] + center_x, center_y, radius = fit_circle(points) + assert radius > 0.0 + + +def test_get_arc_info(): + shape = Shape("tests/fixtures/stl/sample.stl") + lines, arcs = shape.get_lines_and_arcs() + for arc_points in arcs[0]: + radius, center = get_arc_info(arc_points) + assert radius == 9.0 or radius == 5.0 + + +def test_get_edges_for_arc(): + shape = Shape("tests/fixtures/stl/sample.stl") + lines, arcs = shape.get_lines_and_arcs() + for arc_points in arcs[0]: + edges = get_edges_for_arc(arc_points, 3) + assert len(edges) == 3 + + +def test_get_edges_for_arc_many_edges(): + shape = Shape("tests/fixtures/stl/sample.stl") + lines, arcs = shape.get_lines_and_arcs() + for arc_points in arcs[0]: + edges = get_edges_for_arc(arc_points, 4) + assert len(edges) == 4 + + edges = get_edges_for_arc(arc_points, 6) + assert len(edges) == 6 diff --git a/tests/test_shape.py b/tests/test_shape.py index 60d08f4..8f021eb 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -1,4 +1,4 @@ -from cnceye.shape import Shape +from cnceye import Shape def test_are_facets_on_same_plane(): @@ -60,3 +60,11 @@ def test_get_lines_and_arcs_with_step_slope(): assert len(lines[0]) == 4 assert len(lines[1]) == 4 assert len(lines[2]) == 4 + + +def test_get_arc_info(): + shape = Shape("tests/fixtures/stl/sample.stl") + lines, arcs = shape.get_lines_and_arcs() + for arc_points in arcs[0]: + radius, center = shape.get_arc_info(arc_points) + assert radius == 9.0 or radius == 5.0