Skip to content

Commit

Permalink
Circle (#20)
Browse files Browse the repository at this point in the history
* find circle radius and center

* update readme
  • Loading branch information
yuichiroaoki authored Dec 13, 2023
1 parent b313769 commit 21e2f53
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,5 @@ cython_debug/
#.idea/

output
lines
lines
.vscode
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions cnceye/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .shape import Shape # noqa: F401
70 changes: 70 additions & 0 deletions cnceye/arc.py
Original file line number Diff line number Diff line change
@@ -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
49 changes: 42 additions & 7 deletions cnceye/shape.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import trimesh
import numpy as np
from trimesh.graph import face_adjacency
from .arc import get_arc_info


class Shape:
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)):
Expand Down
44 changes: 43 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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>"]
Expand All @@ -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]
Expand Down
42 changes: 42 additions & 0 deletions tests/test_arc.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 9 additions & 1 deletion tests/test_shape.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from cnceye.shape import Shape
from cnceye import Shape


def test_are_facets_on_same_plane():
Expand Down Expand Up @@ -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

0 comments on commit 21e2f53

Please sign in to comment.