Skip to content

Commit 0d4301b

Browse files
authored
Rewrote method for calculating grain perimeters. (#242)
scikit-image's "find_contours" function has been replaced with "_find_perimeter" which will can return a grains core perimeter directly, speeding up simulation time in FMM grains.
1 parent 0561b05 commit 0d4301b

10 files changed

Lines changed: 397 additions & 15 deletions

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ __pycache__/
66
# C extensions
77
*.so
88

9+
# Cython
10+
*.c
11+
mathlib/_find_perimeter_cy.c
12+
913
# Distribution / packaging
1014
.Python
1115
build/
@@ -94,6 +98,7 @@ venv/
9498
ENV/
9599
env.bak/
96100
venv.bak/
101+
myvenv
97102

98103
# Spyder project settings
99104
.spyderproject
@@ -118,4 +123,4 @@ preferences.yaml
118123
propellants.yaml
119124

120125
# autogenerated UI files
121-
*_ui.py
126+
*_ui.py

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,19 @@ $ python setup.py build_ui
5454
```
5555
Note that if you make changes to the UI using the `.ui` forms, you must re-build using the same command.
5656

57+
#### Cython Files:
58+
to speed up some more computationally expensive parts of the codebase, openMotor uses the Cython programming language to run calculations in C.
59+
60+
because the Cyphon code must be compiled separately for each system, you must build the library by running:
61+
```
62+
$ python setup.py build_ext --inplace
63+
```
64+
Note that if you make changes to any of the `.pyx` files, you must re-compile the Cython library using the same command.
65+
66+
#### Run the Application:
67+
5768
Once everything is set up, you can start openMotor by running: `python main.py`
69+
5870
###### Note: On some systems, Python 2 and 3 are installed simultaneously, so you may have to specify which version to run when creating the venv. After the venv has been activated, the programs `python` and `pip` are aliased to the python runtime specific for your venv, so use those (instead of `pip3` and `python3`, on e.g. Debian Linux)
5971

6072
Data Files

mathlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._find_perimeter import find_perimeter

mathlib/_find_perimeter.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import numpy as np
2+
from ._find_perimeter_cy import _get_perimeter
3+
from collections import deque
4+
5+
def find_perimeter(image, level,
6+
*,
7+
including_contours=False,
8+
fully_connected='low'):
9+
"""Find the perimeter of the iso-valued contours in a 2D array for a given level value.
10+
11+
Uses the "marching squares" method to compute the iso-valued contours of
12+
the input 2D array for a particular level value. As segments are computed,
13+
their lengths are added to a total perimeter value which is eventually returned.
14+
15+
Parameters
16+
----------
17+
image : 2D ndarray of double
18+
Input image in which to find contours.
19+
level : float
20+
Value along which to find contours in the array.
21+
22+
Returns
23+
-------
24+
perimeter : float
25+
The perimeter of the computed contours, using the distance between two adjacent
26+
image array points as the base unit.
27+
contour : list of (n,2)-ndarrays
28+
Each contour is an ndarray of shape ``(n, 2)``,
29+
consisting of n ``(row, column)`` coordinates along the contour.
30+
only returned if the
31+
32+
See Also
33+
--------
34+
skimage.measure.find_contours
35+
36+
Notes
37+
-----
38+
The marching squares algorithm is a special case of the marching cubes
39+
algorithm [1]_. A simple explanation is available here:
40+
41+
http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html
42+
43+
.. warning::
44+
45+
Array coordinates/values are assumed to refer to the *center* of the
46+
array element. Take a simple example input: ``[0, 1]``. The interpolated
47+
position of 0.5 in this array is midway between the 0-element (at
48+
``x=0``) and the 1-element (at ``x=1``), and thus would fall at
49+
``x=0.5``.
50+
51+
This means that to find reasonable contours, it is best to find contours
52+
midway between the expected "light" and "dark" values. In particular,
53+
given a binarized array, *do not* choose to find contours at the low or
54+
high value of the array. This will often yield degenerate contours,
55+
especially around structures that are a single array element wide. Instead
56+
choose a middle value, as above.
57+
58+
References
59+
----------
60+
.. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
61+
Resolution 3D Surface Construction Algorithm. Computer Graphics
62+
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
63+
:DOI:`10.1145/37401.37422`
64+
65+
Examples
66+
--------
67+
>>> a = np.zeros((3, 3))
68+
>>> a[0, 0] = 1
69+
>>> a
70+
array([[1., 0., 0.],
71+
[0., 0., 0.],
72+
[0., 0., 0.]])
73+
>>> find_perimeter(a, 0.5)
74+
0.7071067811865476
75+
"""
76+
if image.shape[0] < 2 or image.shape[1] < 2:
77+
raise ValueError("Input array must be at least 2x2.")
78+
if image.ndim != 2:
79+
raise ValueError('Only 2D arrays are supported.')
80+
(perimeter,segments) = _get_perimeter(image, float(level), fully_connected == 'high', including_contours)
81+
contours = []
82+
if including_contours:
83+
contours = _assemble_contours(segments)
84+
return perimeter, contours
85+
86+
87+
88+
def _assemble_contours(segments):
89+
current_index = 0
90+
contours = {}
91+
starts = {}
92+
ends = {}
93+
for from_point, to_point in segments:
94+
# Ignore degenerate segments.
95+
# This happens when (and only when) one vertex of the square is
96+
# exactly the contour level, and the rest are above or below.
97+
# This degenerate vertex will be picked up later by neighboring
98+
# squares.
99+
if from_point == to_point:
100+
continue
101+
102+
tail, tail_num = starts.pop(to_point, (None, None))
103+
head, head_num = ends.pop(from_point, (None, None))
104+
105+
if tail is not None and head is not None:
106+
# We need to connect these two contours.
107+
if tail is head:
108+
# We need to closed a contour: add the end point
109+
head.append(to_point)
110+
else: # tail is not head
111+
# We need to join two distinct contours.
112+
# We want to keep the first contour segment created, so that
113+
# the final contours are ordered left->right, top->bottom.
114+
if tail_num > head_num:
115+
# tail was created second. Append tail to head.
116+
head.extend(tail)
117+
# Remove tail from the detected contours
118+
contours.pop(tail_num, None)
119+
# Update starts and ends
120+
starts[head[0]] = (head, head_num)
121+
ends[head[-1]] = (head, head_num)
122+
else: # tail_num <= head_num
123+
# head was created second. Prepend head to tail.
124+
tail.extendleft(reversed(head))
125+
# Remove head from the detected contours
126+
starts.pop(head[0], None) # head[0] can be == to_point!
127+
contours.pop(head_num, None)
128+
# Update starts and ends
129+
starts[tail[0]] = (tail, tail_num)
130+
ends[tail[-1]] = (tail, tail_num)
131+
elif tail is None and head is None:
132+
# We need to add a new contour
133+
new_contour = deque((from_point, to_point))
134+
contours[current_index] = new_contour
135+
starts[from_point] = (new_contour, current_index)
136+
ends[to_point] = (new_contour, current_index)
137+
current_index += 1
138+
elif head is None: # tail is not None
139+
# tail first element is to_point: the new segment should be
140+
# prepended.
141+
tail.appendleft(from_point)
142+
# Update starts
143+
starts[from_point] = (tail, tail_num)
144+
else: # tail is None and head is not None:
145+
# head last element is from_point: the new segment should be
146+
# appended
147+
head.append(to_point)
148+
# Update ends
149+
ends[to_point] = (head, head_num)
150+
151+
return [np.array(contour) for _, contour in sorted(contours.items())]

0 commit comments

Comments
 (0)