-
Notifications
You must be signed in to change notification settings - Fork 10
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
feature(measure): Adds the height_profiles sub-module #859
Conversation
In starting work on extracting the height profiles now the feret module is in place (see #755) I realised I would need some shapes to test rotation. These already existed as arrays defined in `tests/measure/test_feret.py` and in order to avoid duplication these have been moved to `tests/measure/conftest.py` as fixtures. Because these fixtures are then used in `@pytest.mark.parameterize()` it has necessitated using the `request` fixture and its `.getfixturevalue()` method. A note is left in place for future reference. More on this can be read in [this blog post](https://blog.nshephard.dev/posts/pytest-param/#parameterising-with-fixtures).
6fc080f
to
2d96a6e
Compare
@llwiggins has identified a scenario where behaviour is not as expected. import numpy as np
from topostats.measure import height_profiles
circle = np.array([
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
])
binary_mask = circle
height_profiles.extract_feret_profiles(circle, binary_mask) This fails the following test... import pytest
import numpy.typing as npt
@pytest.mark.parametrize(
("img", "target"),
[
pytest.param(
{
"img": np.asarray(
[
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
]
),
"skeleton": np.asarray(
[
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
]
),
},
np.asarray([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
),
],
)
def test_extract_feret_profiles2(img: npt.NDArray, target: npt.NDArray, request) -> None:
"""Test extract_feret_profiles()."""
# _img = request.getfixturevalue(img)
# Convert boolean to 0/1 and extract profiles
feret_profiles = height_profiles.extract_feret_profiles(img=img["img"], skeleton=np.where(img["skeleton"], 1, 0))
# This fails...
# feret_profiles = height_profiles.extract_feret_profiles(img=_img["img"], skeleton=_img["skeleton"])
np.testing.assert_array_almost_equal(feret_profiles, target, decimal=21)
Why does this fail?Whilst the image doesn't need rotating this fails because the maximum feret co-ordinates after rotating (which wasn't in effect performed) are a vertical line rather than the expected horizontal line...
And the def profile(img: npt.NDArray, row: int) -> npt.NDArray:
"""
...
"""
try:
return img[row]
except IndexError as e:
raise IndexError("The slice (row) is outside the image boundary.") from e And so because the row being used is |
6257874
to
716760a
Compare
@llwiggins I've revised how the profiles are obtained and ditched the rotation method instead using As a consequence the Arguments can be passed via I've found for some reason that interpolation (using the |
Further to #755 this adds the `topostats.measure.height_profiles` sub-module which interpolates the heights between the maximum feret co-ordinates. Includes a function in `topostats.plotting.plot_height_profiles()` which produces a line-plot of multiple height profiles. ToDo... Still a fair few steps to integrate this into the processing. + Add configuration options to `topostats/default_config.yaml` of whether to calculate `height_profiles`. + Add configuration option for the `scipy.interpolate.RegularGridInterpolator()` options which are passed via `**kwargs`. + Update `GrainStats()` to calculate the height profile for the image being processed if required. + Return the `height_profile` (1-D Numpy array). + Collect `height_profile` acrss grains into a dictionary (may require current code as written to be adapted to work with dictionaries, currently works with lists in `plot_height_profiles()`). + Add functionality to write the profiles to JSON for subsequent use/plotting (e.g. customising style/axis labels/etc. of plot) Related : #748 #755
716760a
to
058c546
Compare
Been reading the PR and looks good to me. Much prefer the interpolation method, so thank you. Not given it a thorough enough read & tinker with to approve yet, but I like the method and implementation! I'll test it out soon (once my report is done at the latest). Apologies for not being active much on TopoStats recently. |
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.
Thank you @ns-rse for revising the original method for obtaining profiles - I really like the use of scipy
's RegularGridInterpolator()
here. I've tested the code out with various dummy examples, plotting graphs throughout the workflow to assess different stages, and all seems to be working as expected. Happy for this work to be merged. 😄
Further work on height profiles this PR adds the
topostats.measure.height_profiles
sub-module which for a given pair of feret coordinates...Determines the orientation of of the feret co-ordinates relative to horizontal.Rotates the image so that the feret is horizontal.Recalculates the co-ordinates of the feret after rotation.Includes a function in
topostats.plotting.plot_height_profiles()
which produces a line-plot of multiple height profiles.Test shapes defined
tests/measure/test_feret.py
moved to fixtures intests/measure/conftest.py
to avoid duplication and tests for all functions are included. Because these fixtures are then used in@pytest.mark.parameterize()
it has necessitated using therequest
fixture and its.getfixturevalue()
method.More on this can be read in this blog
post.
ToDo...
Still a fair few steps to integrate this into the processing.
topostats/default_config.yaml
of whether to calculateheight_profiles
.GrainStats()
to calculate the height profile for the image being processed if required based on configuration.height_profile
(1-D Numpy array) as part of the tupleGrainStats()
returns.height_profile
across grains into a dictionary (may require current code as written to be adapted to work with dictionaries, currently works with lists inplot_height_profiles()
).Related : #748 #755