grainmorph 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grainmorph-0.2.0/src/grainmorph.egg-info → grainmorph-0.2.1}/PKG-INFO +28 -32
- {grainmorph-0.2.0 → grainmorph-0.2.1}/README.md +27 -31
- {grainmorph-0.2.0 → grainmorph-0.2.1}/pyproject.toml +1 -1
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/__init__.py +1 -1
- {grainmorph-0.2.0 → grainmorph-0.2.1/src/grainmorph.egg-info}/PKG-INFO +28 -32
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_analyzer.py +20 -20
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_fitting.py +2 -2
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_generation.py +15 -15
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_metrics.py +3 -3
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_plot.py +3 -3
- {grainmorph-0.2.0 → grainmorph-0.2.1}/LICENSE +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/setup.cfg +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/analyzer.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/fitting.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/mask_ops.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/metrics.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/schema.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/utils/__init__.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/utils/create_mask.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/utils/geometry.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/utils/parametric_mask.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/utils/plot.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/validation.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/__init__.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/boundary.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/corner.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/discretize.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/roundness.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph/wadell/smoothing.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph.egg-info/SOURCES.txt +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph.egg-info/dependency_links.txt +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph.egg-info/requires.txt +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/src/grainmorph.egg-info/top_level.txt +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_corner.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_discretize.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_mask_ops.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_smoothing.py +0 -0
- {grainmorph-0.2.0 → grainmorph-0.2.1}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grainmorph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Particle shape analysis and visualization library.
|
|
5
5
|
Author: GrainMorph Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -53,13 +53,25 @@ python -m ruff check --isolated .
|
|
|
53
53
|
python -m ruff format --isolated --check src tests
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Releasing
|
|
57
|
+
|
|
58
|
+
Pushing a `vX.Y.Z` tag publishes that version to PyPI through
|
|
59
|
+
`.github/workflows/publish.yml`, using PyPI trusted publishing (no API token):
|
|
60
|
+
|
|
61
|
+
1. Set the same version in `pyproject.toml` and `src/grainmorph/__init__.py`.
|
|
62
|
+
2. Commit, then tag and push: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin main vX.Y.Z`.
|
|
63
|
+
|
|
64
|
+
The workflow fails if the tag and the `pyproject.toml` version differ, then runs the tests before uploading.
|
|
65
|
+
PyPI must list this repository as a trusted publisher for `grainmorph`
|
|
66
|
+
(owner `phykn`, repository `grainmorph`, workflow `publish.yml`, environment `pypi`).
|
|
67
|
+
|
|
56
68
|
## Quick Start
|
|
57
69
|
|
|
58
70
|
```python
|
|
59
|
-
import grainmorph as
|
|
71
|
+
import grainmorph as gm
|
|
60
72
|
|
|
61
|
-
mask =
|
|
62
|
-
results =
|
|
73
|
+
mask = gm.utils.create_circle_mask((256, 256), (128, 128), 60)
|
|
74
|
+
results = gm.analyze_mask(mask)
|
|
63
75
|
|
|
64
76
|
if results is None:
|
|
65
77
|
raise ValueError("mask has no foreground pixels")
|
|
@@ -73,13 +85,13 @@ print(results["aspect_ratio"]["val"])
|
|
|
73
85
|
Visualize the results:
|
|
74
86
|
|
|
75
87
|
```python
|
|
76
|
-
|
|
88
|
+
gm.utils.plot_analysis_results(mask, results)
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
## API
|
|
80
92
|
|
|
81
93
|
```python
|
|
82
|
-
|
|
94
|
+
gm.analyze_mask(
|
|
83
95
|
mask,
|
|
84
96
|
use_aspect_ratio=True,
|
|
85
97
|
use_roundness=True,
|
|
@@ -162,19 +174,19 @@ Returns `None` for an empty mask. Otherwise, returns an `AnalysisResult` dict. D
|
|
|
162
174
|
|
|
163
175
|
Mask constructors:
|
|
164
176
|
|
|
165
|
-
- `
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
169
|
-
- `
|
|
170
|
-
- `
|
|
171
|
-
- `
|
|
172
|
-
- `
|
|
177
|
+
- `gm.utils.create_circle_mask(...)`
|
|
178
|
+
- `gm.utils.create_ellipse_mask(...)`
|
|
179
|
+
- `gm.utils.create_rectangle_mask(...)`
|
|
180
|
+
- `gm.utils.create_square_mask(...)`
|
|
181
|
+
- `gm.utils.create_triangle_mask(...)`
|
|
182
|
+
- `gm.utils.create_pentagon_mask(...)`
|
|
183
|
+
- `gm.utils.create_star_mask(...)`
|
|
184
|
+
- `gm.utils.create_particle_mask(...)`
|
|
173
185
|
|
|
174
186
|
For example, a triangle in a non-square image uses `(x, y)` vertices:
|
|
175
187
|
|
|
176
188
|
```python
|
|
177
|
-
mask =
|
|
189
|
+
mask = gm.utils.create_triangle_mask((20, 60), (30, 2), (40, 2), (30, 8))
|
|
178
190
|
```
|
|
179
191
|
|
|
180
192
|
`create_particle_mask` attempts to reach the requested roundness by calibrating
|
|
@@ -185,23 +197,7 @@ clipping at the image bounds; `target_met` compares its roundness error with
|
|
|
185
197
|
|
|
186
198
|
Visualization:
|
|
187
199
|
|
|
188
|
-
- `
|
|
189
|
-
|
|
190
|
-
## Changes in 0.2.0
|
|
191
|
-
|
|
192
|
-
- Renamed the package from `partimorph` to `grainmorph`: install with `pip install grainmorph` and import with `import grainmorph`. The API is otherwise unchanged from 0.1.9.
|
|
193
|
-
|
|
194
|
-
## Changes in 0.1.9
|
|
195
|
-
|
|
196
|
-
- Wadell roundness is several times faster: corner circle fits are vectorized and boundary discretization does less per-step work. Results are unchanged.
|
|
197
|
-
- `create_particle_mask` calibrates roughness with 12 bisection steps by default instead of 20, about 1.5x faster. Generated masks can differ slightly; on a 108-case grid the roundness error changed by at most 0.012 and its mean was unchanged.
|
|
198
|
-
- Component selection, hole filling, and the Crofton perimeter now use OpenCV and NumPy, so `scipy` and `scikit-image` are no longer dependencies. Results are unchanged.
|
|
199
|
-
|
|
200
|
-
## Changes in 0.1.8
|
|
201
|
-
|
|
202
|
-
- Corrected the fitted ellipse overlay orientation for rotated particles.
|
|
203
|
-
- Unified inscribed-circle fitting used by sphericity and Wadell roundness.
|
|
204
|
-
- Simplified ellipse fitting and bounding-box coordinate transforms while preserving the public API and analysis results.
|
|
200
|
+
- `gm.utils.plot_analysis_results(mask, results)`
|
|
205
201
|
|
|
206
202
|
## License
|
|
207
203
|
|
|
@@ -33,13 +33,25 @@ python -m ruff check --isolated .
|
|
|
33
33
|
python -m ruff format --isolated --check src tests
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Releasing
|
|
37
|
+
|
|
38
|
+
Pushing a `vX.Y.Z` tag publishes that version to PyPI through
|
|
39
|
+
`.github/workflows/publish.yml`, using PyPI trusted publishing (no API token):
|
|
40
|
+
|
|
41
|
+
1. Set the same version in `pyproject.toml` and `src/grainmorph/__init__.py`.
|
|
42
|
+
2. Commit, then tag and push: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin main vX.Y.Z`.
|
|
43
|
+
|
|
44
|
+
The workflow fails if the tag and the `pyproject.toml` version differ, then runs the tests before uploading.
|
|
45
|
+
PyPI must list this repository as a trusted publisher for `grainmorph`
|
|
46
|
+
(owner `phykn`, repository `grainmorph`, workflow `publish.yml`, environment `pypi`).
|
|
47
|
+
|
|
36
48
|
## Quick Start
|
|
37
49
|
|
|
38
50
|
```python
|
|
39
|
-
import grainmorph as
|
|
51
|
+
import grainmorph as gm
|
|
40
52
|
|
|
41
|
-
mask =
|
|
42
|
-
results =
|
|
53
|
+
mask = gm.utils.create_circle_mask((256, 256), (128, 128), 60)
|
|
54
|
+
results = gm.analyze_mask(mask)
|
|
43
55
|
|
|
44
56
|
if results is None:
|
|
45
57
|
raise ValueError("mask has no foreground pixels")
|
|
@@ -53,13 +65,13 @@ print(results["aspect_ratio"]["val"])
|
|
|
53
65
|
Visualize the results:
|
|
54
66
|
|
|
55
67
|
```python
|
|
56
|
-
|
|
68
|
+
gm.utils.plot_analysis_results(mask, results)
|
|
57
69
|
```
|
|
58
70
|
|
|
59
71
|
## API
|
|
60
72
|
|
|
61
73
|
```python
|
|
62
|
-
|
|
74
|
+
gm.analyze_mask(
|
|
63
75
|
mask,
|
|
64
76
|
use_aspect_ratio=True,
|
|
65
77
|
use_roundness=True,
|
|
@@ -142,19 +154,19 @@ Returns `None` for an empty mask. Otherwise, returns an `AnalysisResult` dict. D
|
|
|
142
154
|
|
|
143
155
|
Mask constructors:
|
|
144
156
|
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
157
|
+
- `gm.utils.create_circle_mask(...)`
|
|
158
|
+
- `gm.utils.create_ellipse_mask(...)`
|
|
159
|
+
- `gm.utils.create_rectangle_mask(...)`
|
|
160
|
+
- `gm.utils.create_square_mask(...)`
|
|
161
|
+
- `gm.utils.create_triangle_mask(...)`
|
|
162
|
+
- `gm.utils.create_pentagon_mask(...)`
|
|
163
|
+
- `gm.utils.create_star_mask(...)`
|
|
164
|
+
- `gm.utils.create_particle_mask(...)`
|
|
153
165
|
|
|
154
166
|
For example, a triangle in a non-square image uses `(x, y)` vertices:
|
|
155
167
|
|
|
156
168
|
```python
|
|
157
|
-
mask =
|
|
169
|
+
mask = gm.utils.create_triangle_mask((20, 60), (30, 2), (40, 2), (30, 8))
|
|
158
170
|
```
|
|
159
171
|
|
|
160
172
|
`create_particle_mask` attempts to reach the requested roundness by calibrating
|
|
@@ -165,23 +177,7 @@ clipping at the image bounds; `target_met` compares its roundness error with
|
|
|
165
177
|
|
|
166
178
|
Visualization:
|
|
167
179
|
|
|
168
|
-
- `
|
|
169
|
-
|
|
170
|
-
## Changes in 0.2.0
|
|
171
|
-
|
|
172
|
-
- Renamed the package from `partimorph` to `grainmorph`: install with `pip install grainmorph` and import with `import grainmorph`. The API is otherwise unchanged from 0.1.9.
|
|
173
|
-
|
|
174
|
-
## Changes in 0.1.9
|
|
175
|
-
|
|
176
|
-
- Wadell roundness is several times faster: corner circle fits are vectorized and boundary discretization does less per-step work. Results are unchanged.
|
|
177
|
-
- `create_particle_mask` calibrates roughness with 12 bisection steps by default instead of 20, about 1.5x faster. Generated masks can differ slightly; on a 108-case grid the roundness error changed by at most 0.012 and its mean was unchanged.
|
|
178
|
-
- Component selection, hole filling, and the Crofton perimeter now use OpenCV and NumPy, so `scipy` and `scikit-image` are no longer dependencies. Results are unchanged.
|
|
179
|
-
|
|
180
|
-
## Changes in 0.1.8
|
|
181
|
-
|
|
182
|
-
- Corrected the fitted ellipse overlay orientation for rotated particles.
|
|
183
|
-
- Unified inscribed-circle fitting used by sphericity and Wadell roundness.
|
|
184
|
-
- Simplified ellipse fitting and bounding-box coordinate transforms while preserving the public API and analysis results.
|
|
180
|
+
- `gm.utils.plot_analysis_results(mask, results)`
|
|
185
181
|
|
|
186
182
|
## License
|
|
187
183
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grainmorph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Particle shape analysis and visualization library.
|
|
5
5
|
Author: GrainMorph Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -53,13 +53,25 @@ python -m ruff check --isolated .
|
|
|
53
53
|
python -m ruff format --isolated --check src tests
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Releasing
|
|
57
|
+
|
|
58
|
+
Pushing a `vX.Y.Z` tag publishes that version to PyPI through
|
|
59
|
+
`.github/workflows/publish.yml`, using PyPI trusted publishing (no API token):
|
|
60
|
+
|
|
61
|
+
1. Set the same version in `pyproject.toml` and `src/grainmorph/__init__.py`.
|
|
62
|
+
2. Commit, then tag and push: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin main vX.Y.Z`.
|
|
63
|
+
|
|
64
|
+
The workflow fails if the tag and the `pyproject.toml` version differ, then runs the tests before uploading.
|
|
65
|
+
PyPI must list this repository as a trusted publisher for `grainmorph`
|
|
66
|
+
(owner `phykn`, repository `grainmorph`, workflow `publish.yml`, environment `pypi`).
|
|
67
|
+
|
|
56
68
|
## Quick Start
|
|
57
69
|
|
|
58
70
|
```python
|
|
59
|
-
import grainmorph as
|
|
71
|
+
import grainmorph as gm
|
|
60
72
|
|
|
61
|
-
mask =
|
|
62
|
-
results =
|
|
73
|
+
mask = gm.utils.create_circle_mask((256, 256), (128, 128), 60)
|
|
74
|
+
results = gm.analyze_mask(mask)
|
|
63
75
|
|
|
64
76
|
if results is None:
|
|
65
77
|
raise ValueError("mask has no foreground pixels")
|
|
@@ -73,13 +85,13 @@ print(results["aspect_ratio"]["val"])
|
|
|
73
85
|
Visualize the results:
|
|
74
86
|
|
|
75
87
|
```python
|
|
76
|
-
|
|
88
|
+
gm.utils.plot_analysis_results(mask, results)
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
## API
|
|
80
92
|
|
|
81
93
|
```python
|
|
82
|
-
|
|
94
|
+
gm.analyze_mask(
|
|
83
95
|
mask,
|
|
84
96
|
use_aspect_ratio=True,
|
|
85
97
|
use_roundness=True,
|
|
@@ -162,19 +174,19 @@ Returns `None` for an empty mask. Otherwise, returns an `AnalysisResult` dict. D
|
|
|
162
174
|
|
|
163
175
|
Mask constructors:
|
|
164
176
|
|
|
165
|
-
- `
|
|
166
|
-
- `
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
169
|
-
- `
|
|
170
|
-
- `
|
|
171
|
-
- `
|
|
172
|
-
- `
|
|
177
|
+
- `gm.utils.create_circle_mask(...)`
|
|
178
|
+
- `gm.utils.create_ellipse_mask(...)`
|
|
179
|
+
- `gm.utils.create_rectangle_mask(...)`
|
|
180
|
+
- `gm.utils.create_square_mask(...)`
|
|
181
|
+
- `gm.utils.create_triangle_mask(...)`
|
|
182
|
+
- `gm.utils.create_pentagon_mask(...)`
|
|
183
|
+
- `gm.utils.create_star_mask(...)`
|
|
184
|
+
- `gm.utils.create_particle_mask(...)`
|
|
173
185
|
|
|
174
186
|
For example, a triangle in a non-square image uses `(x, y)` vertices:
|
|
175
187
|
|
|
176
188
|
```python
|
|
177
|
-
mask =
|
|
189
|
+
mask = gm.utils.create_triangle_mask((20, 60), (30, 2), (40, 2), (30, 8))
|
|
178
190
|
```
|
|
179
191
|
|
|
180
192
|
`create_particle_mask` attempts to reach the requested roundness by calibrating
|
|
@@ -185,23 +197,7 @@ clipping at the image bounds; `target_met` compares its roundness error with
|
|
|
185
197
|
|
|
186
198
|
Visualization:
|
|
187
199
|
|
|
188
|
-
- `
|
|
189
|
-
|
|
190
|
-
## Changes in 0.2.0
|
|
191
|
-
|
|
192
|
-
- Renamed the package from `partimorph` to `grainmorph`: install with `pip install grainmorph` and import with `import grainmorph`. The API is otherwise unchanged from 0.1.9.
|
|
193
|
-
|
|
194
|
-
## Changes in 0.1.9
|
|
195
|
-
|
|
196
|
-
- Wadell roundness is several times faster: corner circle fits are vectorized and boundary discretization does less per-step work. Results are unchanged.
|
|
197
|
-
- `create_particle_mask` calibrates roughness with 12 bisection steps by default instead of 20, about 1.5x faster. Generated masks can differ slightly; on a 108-case grid the roundness error changed by at most 0.012 and its mean was unchanged.
|
|
198
|
-
- Component selection, hole filling, and the Crofton perimeter now use OpenCV and NumPy, so `scipy` and `scikit-image` are no longer dependencies. Results are unchanged.
|
|
199
|
-
|
|
200
|
-
## Changes in 0.1.8
|
|
201
|
-
|
|
202
|
-
- Corrected the fitted ellipse overlay orientation for rotated particles.
|
|
203
|
-
- Unified inscribed-circle fitting used by sphericity and Wadell roundness.
|
|
204
|
-
- Simplified ellipse fitting and bounding-box coordinate transforms while preserving the public API and analysis results.
|
|
200
|
+
- `gm.utils.plot_analysis_results(mask, results)`
|
|
205
201
|
|
|
206
202
|
## License
|
|
207
203
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
|
-
import grainmorph as
|
|
4
|
+
import grainmorph as gm
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_analyze_mask_full():
|
|
8
8
|
mask = np.zeros((100, 100), dtype=bool)
|
|
9
9
|
mask[20:80, 20:80] = True
|
|
10
10
|
|
|
11
|
-
results =
|
|
11
|
+
results = gm.analyze_mask(mask)
|
|
12
12
|
|
|
13
13
|
assert results is not None
|
|
14
14
|
assert set(results) == {
|
|
@@ -25,7 +25,7 @@ def test_metrics_consistency():
|
|
|
25
25
|
y, x = np.ogrid[:100, :100]
|
|
26
26
|
mask = (x - 50) ** 2 + (y - 50) ** 2 <= 40**2
|
|
27
27
|
|
|
28
|
-
results =
|
|
28
|
+
results = gm.analyze_mask(mask)
|
|
29
29
|
|
|
30
30
|
assert results is not None
|
|
31
31
|
assert results["circularity"]["val"] > 0.9
|
|
@@ -38,7 +38,7 @@ def test_analyze_mask_rejects_invalid_eps(eps):
|
|
|
38
38
|
mask = np.ones((8, 8), dtype=bool)
|
|
39
39
|
|
|
40
40
|
with pytest.raises(ValueError, match="eps must be > 0"):
|
|
41
|
-
|
|
41
|
+
gm.analyze_mask(mask, eps=eps)
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
@pytest.mark.parametrize("target_dim", [0, -1])
|
|
@@ -46,7 +46,7 @@ def test_analyze_mask_rejects_non_positive_target_dim(target_dim):
|
|
|
46
46
|
mask = np.ones((8, 8), dtype=bool)
|
|
47
47
|
|
|
48
48
|
with pytest.raises(ValueError, match="target_dim must be > 0"):
|
|
49
|
-
|
|
49
|
+
gm.analyze_mask(mask, target_dim=target_dim)
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
@pytest.mark.parametrize("target_dim", [1.5, True])
|
|
@@ -54,7 +54,7 @@ def test_analyze_mask_rejects_non_integer_target_dim(target_dim):
|
|
|
54
54
|
mask = np.ones((8, 8), dtype=bool)
|
|
55
55
|
|
|
56
56
|
with pytest.raises(TypeError, match="target_dim must be an integer"):
|
|
57
|
-
|
|
57
|
+
gm.analyze_mask(mask, target_dim=target_dim)
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def test_analyze_mask_uses_largest_component():
|
|
@@ -62,7 +62,7 @@ def test_analyze_mask_uses_largest_component():
|
|
|
62
62
|
mask[10:30, 10:30] = True
|
|
63
63
|
mask[50:110, 50:110] = True
|
|
64
64
|
|
|
65
|
-
results =
|
|
65
|
+
results = gm.analyze_mask(mask, use_roundness=False, use_circularity=False)
|
|
66
66
|
|
|
67
67
|
assert results is not None
|
|
68
68
|
assert results["sphericity"] is not None
|
|
@@ -75,7 +75,7 @@ def test_analyze_mask_resize_path_scales_coordinates():
|
|
|
75
75
|
mask = np.zeros((1000, 1000), dtype=bool)
|
|
76
76
|
mask[100:900, 200:800] = True
|
|
77
77
|
|
|
78
|
-
results =
|
|
78
|
+
results = gm.analyze_mask(mask, target_dim=384, use_roundness=False)
|
|
79
79
|
|
|
80
80
|
assert results is not None
|
|
81
81
|
assert results["sphericity"] is not None
|
|
@@ -94,7 +94,7 @@ def test_analyze_mask_resize_path_scales_coordinates():
|
|
|
94
94
|
|
|
95
95
|
@pytest.mark.parametrize("shape", [(1, 1000), (1000, 1)])
|
|
96
96
|
def test_analyze_mask_resizes_thin_masks_without_crashing(shape):
|
|
97
|
-
results =
|
|
97
|
+
results = gm.analyze_mask(
|
|
98
98
|
np.ones(shape, dtype=bool),
|
|
99
99
|
use_roundness=False,
|
|
100
100
|
)
|
|
@@ -108,13 +108,13 @@ def test_analyze_mask_does_not_collapse_non_degenerate_axis():
|
|
|
108
108
|
mask = np.zeros((1200, 1200), dtype=bool)
|
|
109
109
|
mask[100:1100, 550:553] = True
|
|
110
110
|
|
|
111
|
-
resized =
|
|
111
|
+
resized = gm.analyze_mask(
|
|
112
112
|
mask,
|
|
113
113
|
use_roundness=False,
|
|
114
114
|
use_circularity=False,
|
|
115
115
|
use_sphericity=False,
|
|
116
116
|
)
|
|
117
|
-
reference =
|
|
117
|
+
reference = gm.analyze_mask(
|
|
118
118
|
mask,
|
|
119
119
|
target_dim=2000,
|
|
120
120
|
use_roundness=False,
|
|
@@ -133,12 +133,12 @@ def test_analyze_mask_does_not_collapse_non_degenerate_axis():
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
def test_metrics_are_invariant_to_zero_padding():
|
|
136
|
-
small =
|
|
137
|
-
large =
|
|
136
|
+
small = gm.utils.create_circle_mask((100, 100), (50, 50), 2)
|
|
137
|
+
large = gm.utils.create_circle_mask((2000, 2000), (1000, 1000), 2)
|
|
138
138
|
options = {"use_roundness": False, "use_aspect_ratio": False}
|
|
139
139
|
|
|
140
|
-
small_results =
|
|
141
|
-
large_results =
|
|
140
|
+
small_results = gm.analyze_mask(small, **options)
|
|
141
|
+
large_results = gm.analyze_mask(large, **options)
|
|
142
142
|
|
|
143
143
|
assert small_results is not None
|
|
144
144
|
assert large_results is not None
|
|
@@ -160,11 +160,11 @@ def test_metrics_are_invariant_to_zero_padding():
|
|
|
160
160
|
|
|
161
161
|
@pytest.mark.parametrize("target_dim", [32, 384])
|
|
162
162
|
def test_analysis_geometry_is_invariant_to_translation(target_dim):
|
|
163
|
-
mask =
|
|
163
|
+
mask = gm.utils.create_ellipse_mask((100, 120), (50, 60), 35, 20)
|
|
164
164
|
shifted = np.pad(mask, ((23, 0), (37, 0)))
|
|
165
165
|
|
|
166
|
-
reference =
|
|
167
|
-
actual =
|
|
166
|
+
reference = gm.analyze_mask(mask, target_dim=target_dim)
|
|
167
|
+
actual = gm.analyze_mask(shifted, target_dim=target_dim)
|
|
168
168
|
|
|
169
169
|
for key in reference:
|
|
170
170
|
assert actual[key] is not None
|
|
@@ -193,11 +193,11 @@ def test_analysis_geometry_is_invariant_to_translation(target_dim):
|
|
|
193
193
|
[(), ("aspect_ratio",), ("roundness",), ("circularity",), ("sphericity",)],
|
|
194
194
|
)
|
|
195
195
|
def test_analysis_metric_selection(enabled):
|
|
196
|
-
mask =
|
|
196
|
+
mask = gm.utils.create_circle_mask((80, 80), (40, 40), 20)
|
|
197
197
|
keys = ("aspect_ratio", "roundness", "circularity", "sphericity")
|
|
198
198
|
options = {f"use_{key}": key in enabled for key in keys}
|
|
199
199
|
|
|
200
|
-
results =
|
|
200
|
+
results = gm.analyze_mask(mask, **options)
|
|
201
201
|
|
|
202
202
|
assert set(results) == set(enabled)
|
|
203
203
|
assert all(result is not None for result in results.values())
|
|
@@ -2,7 +2,7 @@ import cv2
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
import grainmorph as
|
|
5
|
+
import grainmorph as gm
|
|
6
6
|
from grainmorph.fitting import find_inscribed_circle, fit_ellipse
|
|
7
7
|
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ def test_aspect_ratio_payload_uses_fitted_ellipse():
|
|
|
15
15
|
points = np.concatenate(contours).astype(np.float32)
|
|
16
16
|
(center_x, center_y), (width, height), angle = cv2.fitEllipse(points)
|
|
17
17
|
|
|
18
|
-
results =
|
|
18
|
+
results = gm.analyze_mask(
|
|
19
19
|
mask,
|
|
20
20
|
use_roundness=False,
|
|
21
21
|
use_circularity=False,
|
|
@@ -3,13 +3,13 @@ import json
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
import grainmorph as
|
|
6
|
+
import grainmorph as gm
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def test_triangle_vertices_use_cartesian_order():
|
|
10
10
|
vertices = ((30, 2), (40, 2), (30, 8))
|
|
11
11
|
|
|
12
|
-
mask =
|
|
12
|
+
mask = gm.utils.create_triangle_mask((20, 60), *vertices)
|
|
13
13
|
|
|
14
14
|
assert mask.dtype == bool
|
|
15
15
|
assert all(mask[y, x] for x, y in vertices)
|
|
@@ -18,7 +18,7 @@ def test_triangle_vertices_use_cartesian_order():
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def test_create_particle_mask():
|
|
21
|
-
mask =
|
|
21
|
+
mask = gm.utils.create_particle_mask(
|
|
22
22
|
shape=(256, 256),
|
|
23
23
|
center=(128, 128),
|
|
24
24
|
radius=50,
|
|
@@ -36,7 +36,7 @@ def test_create_particle_mask():
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_create_particle_mask_zero_radius():
|
|
39
|
-
mask, info =
|
|
39
|
+
mask, info = gm.utils.create_particle_mask(
|
|
40
40
|
shape=(100, 100),
|
|
41
41
|
center=(50, 50),
|
|
42
42
|
radius=0.0,
|
|
@@ -50,7 +50,7 @@ def test_create_particle_mask_zero_radius():
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def test_roundness_rough_particle_golden():
|
|
53
|
-
mask =
|
|
53
|
+
mask = gm.utils.create_particle_mask(
|
|
54
54
|
shape=(256, 256),
|
|
55
55
|
center=(128, 128),
|
|
56
56
|
radius=60,
|
|
@@ -59,7 +59,7 @@ def test_roundness_rough_particle_golden():
|
|
|
59
59
|
seed=42,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
results =
|
|
62
|
+
results = gm.analyze_mask(mask)
|
|
63
63
|
|
|
64
64
|
assert results is not None
|
|
65
65
|
assert results["roundness"] is not None
|
|
@@ -67,7 +67,7 @@ def test_roundness_rough_particle_golden():
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def test_particle_info_measures_returned_mask():
|
|
70
|
-
mask, info =
|
|
70
|
+
mask, info = gm.utils.create_particle_mask(
|
|
71
71
|
shape=(96, 96),
|
|
72
72
|
center=(0, 0),
|
|
73
73
|
radius=25,
|
|
@@ -76,7 +76,7 @@ def test_particle_info_measures_returned_mask():
|
|
|
76
76
|
seed=7,
|
|
77
77
|
return_info=True,
|
|
78
78
|
)
|
|
79
|
-
results =
|
|
79
|
+
results = gm.analyze_mask(
|
|
80
80
|
mask,
|
|
81
81
|
use_aspect_ratio=False,
|
|
82
82
|
use_circularity=False,
|
|
@@ -92,13 +92,13 @@ def test_particle_info_measures_returned_mask():
|
|
|
92
92
|
|
|
93
93
|
def test_mask_generators_reject_non_finite_radii():
|
|
94
94
|
with pytest.raises(ValueError, match="finite"):
|
|
95
|
-
|
|
95
|
+
gm.utils.create_circle_mask((16, 16), (8, 8), np.nan)
|
|
96
96
|
with pytest.raises(ValueError, match="finite"):
|
|
97
|
-
|
|
97
|
+
gm.utils.create_ellipse_mask((16, 16), (8, 8), np.inf, 2)
|
|
98
98
|
with pytest.raises(ValueError, match="finite"):
|
|
99
|
-
|
|
99
|
+
gm.utils.create_pentagon_mask((16, 16), (8, 8), np.nan)
|
|
100
100
|
with pytest.raises(ValueError, match="finite"):
|
|
101
|
-
|
|
101
|
+
gm.utils.create_star_mask((16, 16), (8, 8), 5, np.inf)
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
@pytest.mark.parametrize("field", ["sphericity", "roundness"])
|
|
@@ -107,7 +107,7 @@ def test_create_particle_mask_rejects_non_finite_targets(field):
|
|
|
107
107
|
options[field] = np.nan
|
|
108
108
|
|
|
109
109
|
with pytest.raises(ValueError, match="finite"):
|
|
110
|
-
|
|
110
|
+
gm.utils.create_particle_mask(
|
|
111
111
|
shape=(32, 32),
|
|
112
112
|
center=(16, 16),
|
|
113
113
|
radius=8,
|
|
@@ -126,8 +126,8 @@ def test_particle_report_preserves_seeded_mask(radius):
|
|
|
126
126
|
"seed": 42,
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
mask =
|
|
130
|
-
reported, info =
|
|
129
|
+
mask = gm.utils.create_particle_mask(**options)
|
|
130
|
+
reported, info = gm.utils.create_particle_mask(**options, return_info=True)
|
|
131
131
|
|
|
132
132
|
np.testing.assert_array_equal(mask, reported)
|
|
133
133
|
assert info["target_met"] == (info["abs_error"] <= info["report_tol"])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
|
-
import grainmorph as
|
|
4
|
+
import grainmorph as gm
|
|
5
5
|
from grainmorph.metrics import _crofton_perimeter
|
|
6
6
|
from grainmorph.wadell import compute_roundness
|
|
7
7
|
|
|
@@ -11,10 +11,10 @@ from grainmorph.wadell import compute_roundness
|
|
|
11
11
|
[{}, {"max_dev_thresh": 0.5, "alpha_ratio": 0.025, "beta_ratio": 0.0005}],
|
|
12
12
|
)
|
|
13
13
|
def test_analysis_roundness_uses_wadell_options(params):
|
|
14
|
-
mask =
|
|
14
|
+
mask = gm.utils.create_circle_mask((100, 100), (50, 50), 30)
|
|
15
15
|
expected = compute_roundness(mask, **params)
|
|
16
16
|
|
|
17
|
-
results =
|
|
17
|
+
results = gm.analyze_mask(
|
|
18
18
|
mask,
|
|
19
19
|
use_aspect_ratio=False,
|
|
20
20
|
use_circularity=False,
|
|
@@ -8,18 +8,18 @@ import numpy as np
|
|
|
8
8
|
import pytest
|
|
9
9
|
from matplotlib.patches import Ellipse
|
|
10
10
|
|
|
11
|
-
import grainmorph as
|
|
11
|
+
import grainmorph as gm
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@pytest.mark.parametrize("angle", [25, 60, -35])
|
|
15
15
|
def test_ellipse_overlay_aligns_with_rotated_particle(angle):
|
|
16
16
|
mask = np.zeros((160, 180), dtype=np.uint8)
|
|
17
17
|
cv2.ellipse(mask, (90, 80), (50, 18), angle, 0, 360, 1, -1)
|
|
18
|
-
results =
|
|
18
|
+
results = gm.analyze_mask(
|
|
19
19
|
mask, use_roundness=False, use_circularity=False, use_sphericity=False
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
gm.utils.plot_analysis_results(mask, results, show=False)
|
|
23
23
|
fig = plt.gcf()
|
|
24
24
|
try:
|
|
25
25
|
ax = fig.axes[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|