-
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
New submodule for feret calculations #755
Conversation
New submodule `measures.feret` with functions for calculation of min and max feret and the associated co-ordinates. Utilises code from Skan developer see [gist](https://gist.github.com/VolkerH/0d07d05d5cb189b56362e8ee41882abf) and as [suggested](scikit-image/scikit-image#4817 (comment)) it adds tests. In doing so I found sorting points prior to calculation of upper and lower convex hulls missed some points. I'm also not sure about the `rotating_calipers()` function as it doesn't seem to return all pairs formed across the points from the upper and lower convex hulls and so have included but not used the `all_pairs()` function which does, although if used in place this doesn't return the minimum feret correctly, rather just the smallest distance. Currently some of the tests for the `curved_line` fail too. The intention is to use this without instantiating the `GrainStats` class to be used in profiles (#748) and could serve as a concise stand-alone set of functions outside of `GrainStats` class which currently has static methods for the calculations (#750). Benchmarking In light of #750 and re-implementing feret calculations as a new sub-module I wanted to see how this compares in terms of performance to those in `GrainStats()` class so have run some very basic benchmarking. Note this is not the full pipeline of taking labelled images and finding the outlines/edge points which are required as inputs to the calculations, its purely on the calculation of min-max feret from the edge points. ``` import timeit import numpy as np from skimage import draw from topostats.measure import feret from topostats.grainstats import GrainStats holo_circle = np.zeros((14, 14), dtype=np.uint8) rr, cc = draw.circle_perimeter(6, 6, 5) holo_circle[rr, cc] = 1 holo_circle_edge_points = np.argwhere(holo_circle == 1) %timeit feret.min_max_feret(holo_circle_edge_points) 83 µs ± 686 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each) %timeit GrainStats.get_max_min_ferets(holo_circle_edge_points) 1.06 ms ± 10.4 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) ``` So this new implementation is faster.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #755 +/- ##
==========================================
+ Coverage 84.73% 85.51% +0.77%
==========================================
Files 21 22 +1
Lines 3196 3368 +172
==========================================
+ Hits 2708 2880 +172
Misses 488 488 ☔ View full report in Codecov by Sentry. |
Reduces the size of test images making them easier to calculate/understand expected values. + `small_circle` : From 14x14 array with center at (6,6) with radius of 5 to 7x7 array with center at (3,3) and radius of 2. + `holo_ellipse_horizontal` : From 16x10 with center at (8, 5) and row radius 3, col radius 6 to 11x9 with center at (5, 4) row radius 4 col radius 3. Removed orientation option. + `holo_ellipse_vertical` : From 10x16 with center at (5, 8) and row radius 6, col radius 3 to 9x11 with center at (4, 5) row radius 3 col radius 4. Removed orientation option. + `holo_ellipse_angled` : From 12x14 with center at (6, 7) row radius 3, col radius 5 to 8x10 with center at (4, 5), row radius 1, col radius 3 + `filled_circle` : From 14x14 with center at (6, 6) and radius of 6 to 12x12 with center at (4,4) + `filled_ellipse_vertical` : From 16x10 with center at (8, 5) and row radius 4, col radius 6 to 9x7 with center at (4, 3), row radius 4, col radius 3. Removed rotation option + `filled_ellipse_horizontal` : From 10x16 with center at (5, 8) and row radius 6, column radius 3 to 7x9 with center at (3, 4) and row radius 3, column radius 4. Removed rotation option + `filled_ellipse_angled` : from 12x14 with center at (6, 7) row radius 3, column radius 5 to 9x11 with center at (4, 5) and row radius 3, column radius 5. + Updated combined integration test of overall method with multiple objects. + Adds in `tiny_triangle`, `tiny_square`, `tiny_rectangle` and `tiny_ellipse` to parametrised tests as these are super clear as to whether the min/max feret are correct or not. I think there are some problems with the Rotating Calliper algorithm as implemented as it fails for the `curved_line` and will be investigating that, fairly important as we have linear DNA molecules that will not be straight lines.
Please hold back on this, I've been working on simplifying the tests and think there is a problem with the Rotating Caliper algorithm so will be checking that next week. |
Closes #776 + Aligns command line and configuration field names with those in matplotlibrc files. + Restores the `cmap` configuration option to `default_config.yaml` and introduces `savefig_dpi` option. + Adds command line options for setting DPI (`--savefig-dpi`), Colormap (`--cmap`)and Output file format (`--savefig-format`). + Expands documentation on how to use custom configuration files or command line options to set the DPI/Colormap/Output format. + Updates the header to `topostats.mplstyle` to explain how to use it as typically users will have created a copy of the file (after the convenience function `topostats create-matplotlibrc` was introduced with #773). + To achieve this the dictionary `config["plotting"]` needed explicitly updating as the `update_config()` function doesn't update nested configurations (since this is the first PR that introduces command line options that modify any of the values in the nested dictionaries). + Updates options for `topostats toposum`` to align with `savefig_format` and adds flag to entry point so output format is consistent. + Updates and expands the configuration documentation explaining how to use these conveniences. As a consequence quite a few files are touched to ensure that validation and processing functions all have variables that align with those in the configuration. If users could test this it would be very much appreciated, if you use the Git installed version something like the following would switch branches and allow you test it. ``` conda create --name topostats-config # Create and activate a virtual env specific to this conda activate topostats-config cd ~/path/to/TopoStats git pull git checkout ns-rse/776-config-jigging pip install -e . topostats process --output-dir base topostats create-config test_config.yaml # Create test_config.yaml to try changing parameters topostats process --config test_config.yaml --output-dir test1 topostats process --output-dir test2 --savefig-dpi 10 --cmap rainbow --savefig-format svg topostats process --config test_config.yaml --output-dir test3 --savefig-dpi 80 --cmap viridis --savefig-format pdf ``` Each invocation of `topostats process` will save output to its own directory (either `base`, `test1`, `test2` and `test3`) for comparison. There should be differences between each `base` the values used in `test_config.yaml` and saved under `test1` and those under `test2` and `test3` should also differ. I would really appreciate feedback on the documentation as without clear documentation it is perhaps confusing how the components interact and work and can be modified and getting this as clear as possible will be really helpful.
Closes #776 + Aligns command line and configuration field names with those in matplotlibrc files. + Restores the `cmap` configuration option to `default_config.yaml` and introduces `savefig_dpi` option. + Adds command line options for setting DPI (`--savefig-dpi`), Colormap (`--cmap`)and Output file format (`--savefig-format`). + Expands documentation on how to use custom configuration files or command line options to set the DPI/Colormap/Output format. + Updates the header to `topostats.mplstyle` to explain how to use it as typically users will have created a copy of the file (after the convenience function `topostats create-matplotlibrc` was introduced with #773). + To achieve this the dictionary `config["plotting"]` needed explicitly updating as the `update_config()` function doesn't update nested configurations (since this is the first PR that introduces command line options that modify any of the values in the nested dictionaries). + Updates options for `topostats toposum`` to align with `savefig_format` and adds flag to entry point so output format is consistent. + Updates and expands the configuration documentation explaining how to use these conveniences. As a consequence quite a few files are touched to ensure that validation and processing functions all have variables that align with those in the configuration. If users could test this it would be very much appreciated, if you use the Git installed version something like the following would switch branches and allow you test it. ``` conda create --name topostats-config # Create and activate a virtual env specific to this conda activate topostats-config cd ~/path/to/TopoStats git pull git checkout ns-rse/776-config-jigging pip install -e . topostats process --output-dir base topostats create-config test_config.yaml # Create test_config.yaml to try changing parameters topostats process --config test_config.yaml --output-dir test1 topostats process --output-dir test2 --savefig-dpi 10 --cmap rainbow --savefig-format svg topostats process --config test_config.yaml --output-dir test3 --savefig-dpi 80 --cmap viridis --savefig-format pdf ``` Each invocation of `topostats process` will save output to its own directory (either `base`, `test1`, `test2` and `test3`) for comparison. There should be differences between each `base` the values used in `test_config.yaml` and saved under `test1` and those under `test2` and `test3` should also differ. I would really appreciate feedback on the documentation as without clear documentation it is perhaps confusing how the components interact and work and can be modified and getting this as clear as possible will be really helpful.
Co-authored-by: Max Gamill <[email protected]>
Co-authored-by: Max Gamill <[email protected]>
Co-authored-by: Max Gamill <[email protected]>
Thanks for the feedback @MaxGamil-Sheffield this commit... + Loosens validation of `cmap` so that any Matplotlib color map can be used. + Removes reporting of DPI/output format/cmap from early logging stages and output of `completion_message()`. I hadn't thought about `None` being listed in the `completion_message()` for DPI/Output Format/cmap and appreciate this would be confusing so thanks for highlighting that. The solution I've gone for (removing the additions that reported these) is different from that suggested (update the `config` dictionary with parameters from `mpl.rcParams` early in processing). My reasoning being... + Previously we didn't report these, no one has ever asked to see them in the logging output. + We write configuration options to YAML file via `write_yaml()` at the end of processing. Its a verbatim copy of that which was used (either user specified or `default_config.yaml`)and it contains the settings used. If a user didn't specify DPI/cmap/format I'm not sure we should alter this. It could be argued it is useful to provide them but then that would also require writing _all_ other configuration/plotting options to be consistent should the default values ever change in the future. Currently only a handful of parameters are read from `topostats.mplstyle` and this is conditional on whether any of these are being over-ridden or not when instantiating the `Images()` class. Currently we ``` plottingfuncs.Images() > load_mplstyle() > Set DPI/cmap/format based on arguments to Images() ``` It is certainly possible to change this process as suggested and.. ``` load_mplstyle() > Update DPI/cmap/format > plottingfuncs.Images() ``` ...but that is a larger amount of work to undertake and introduces scope drift to this PR. If it is desirable to report this information in logging and/or ensure the configuration file that is written contains the default parameters from `topostats.mplstyle` then we can address that as a separate issue.
+ Correctly details in validation the values for `figure` in plotting. + Details what the `core` set outputs. I've not added the request to add links in validation output to Matplotlib cmap as links already exist in the documentation and I would expect if someone wishes to use a particular colormap here they would already be aware of what the options are.
Temporary fix for #787
updates: - [github.com/DavidAnson/markdownlint-cli2: v0.11.0 → v0.12.1](DavidAnson/markdownlint-cli2@v0.11.0...v0.12.1) - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](astral-sh/ruff-pre-commit@v0.1.9...v0.2.0) - [github.com/psf/black-pre-commit-mirror: 23.12.1 → 24.1.1](psf/black-pre-commit-mirror@23.12.1...24.1.1) - [github.com/kynan/nbstripout: 0.6.1 → 0.7.1](kynan/nbstripout@0.6.1...0.7.1)
See #792 for further details.
Made some mistakes in the rotating calipers algorithm and spent considerable time with pen and paper working through examples. + The `curved_line` tests now pass. + Added another simple polygon to the test suite `tiny_quadrilateral`. + Found that the ordering of points makes a considerable difference when constructing the convex hulls and their halves and in so doing added a `sort_coords()` function to test this. As a consequence sorting can be done on either axis (0/rows or 1/columns). Ultimately the same min and max feret are calculated though.
Started investigating this and have worked out where the problem arises and have some thoughts on how to resolve it. Taking this small ellipse... The The next point on the adjacent hull is Extend the base and we have... The height of the triangle, which is from the apex to the perpendicular intersection with the base is outside of the original triangle... But this doesn't matter its not the minimum feretIn this example its not but as noted above this tilted ellipse has a similar error and the minimum feret (short pink line) is outside of the shape. Checking this we have... Caliper/Triangle 1Problematic A scalene triangle where the co-ordinates of the height (which we are using as the feret distance) lie outside of the convex hull. This isn't so much of a problem as caliper/triangle 4 as the height is > than the minimum feret so it would never be chosen but we should still be excluding this. Caliper/Triangle 2Problematic A scalene triangle where the co-ordinates of the height (which we are using as the feret distance) lie outside of the convex hull. This isn't so much of a problem as caliper/triangle 4 as the height is > than the minimum feret so it would never be chosen but we should still be excluding this. Caliper/Triangle 3NB This is a right-angled triangle so the height is within the bounds of the triangle and what we want. Caliper/Triangle 4Offender this is the triangle who's height is equal to the minimum feret but lies outside of the convex hull. Caliper/Triangle 5Caliper/Triangle 6NB This is a right-angled triangle so the height is within the bounds of the triangle and what we want, you can't see it because its the black side of the triangle and I only wrote a short hacky function to draw these plots. Ok, this is doing what we have asked which is find the height of the triangle formed by the calliper pairs and the next point on the convex hull, but its not what we want to know and its actually wrong if the triangle formed is scalene. Why are the minimum feret co-ordinates outside of the triangle and not the height of caliper/triangle 2?The The This list looks like this...
The third and fourth elements have the same minimum feret distance (and you can see that the What to do?The Graham Scan algorithm is the basis for this implementation (as it is in the current If algorithms don't work on small examples we can't expect them to generalise well and I would never have come across this issue if I hadn't created the various small simple shapes and tested them! I think a sensible approach here might be to have a check made before finding the minimum feret that checks if the co-ordinates of the triangle height are on the convex hull. If either of these points are outside of the shape we can drop that feret distance/co-ordinates from the list prior to finding the minimum which means we should only ever get feret distances that are within the convex hull and we therefore get not just the correct distance but the correct co-ordinates through which we can take a profile (which is what has stimulated this work in the first place!). I know you're incredibly busy @SylviaWhittle but as you tussled with this problem in the past your thoughts would be appreciated as would any thoughts from @MaxGamill-Sheffield or @llwiggins . Possible solutionsVarious solutions for finding whether points are within the convex hull are suggested in this thread along with timings, some parallel, some using |
Notes on solutions for excluding triangle heights from scalene triangles that are outside of the convex hullMatplotlib PathGave this method a whirl but as noted in the documentation.
Had a play with the value of Did like the fact you could assess a vector of co-ordinates rather than having to loop over them though.
|
The minimum feret which is calculated as the height of the triangle formed between the rotating caliper pair and the next adjacent point on the hull (with the apex being the antipodal point on the opposite hull) resulted in heights with co-ordinates that were outside of the hull if the triangles were scalene. In turn these gave incorrect minimum feret coordinates, even if the distances were correct. To avoid this a check is now made as to whether the line formed by the triangle height is `in_polygon()` using functions from Shapely.
This is ready for review. Once approved and merged I shall proceed with extracting the profiles along the min/max feret lines (the main feature from #748) and addressing #798 as I think the code implemented in Test coverage is low on this PR as I've not tested the |
Practising what I preach and adding tests for the `measure.feret.plot_feret()` function used for visualising hulls, callipers, triangle heights and feret distances. Each image is 38-43kb so not huge.
Update - Decided to try the ray tracer algorithm and as we want to include points on the convex hull have used |
After discussion with @SylivaWhittle and @llwiggins it was decided that because of the issue of minimum feret coordinates sometimes falling outside of the convex hull that we should not use it as a consistent line through the grain/image. Instead we will use the maximum feret and a line perpendicular to the mid-point of the maximum feret. As such all attempts to work out if minimum feret coordinates/lines were inside or on the edge of a polygon have now been removed and the function returns a dictionary of... + `min_feret` + `min_feret_coords` + `max_feret` + `max_feret_coords` The dictionary should be conducive to being saved in the HDF5 output (perhaps a separate issue to do so).
After discussion with @SylviaWhittle and @llwiggins it was decided that because of the issue of minimum feret coordinates sometimes falling outside of the convex hull that we should not use it as a consistent line through the grain/image. Instead we will use the maximum feret and a line perpendicular to the mid-point of the maximum feret. As such all attempts to work out if minimum feret coordinates/lines were inside or on the edge of a polygon have now been removed and the function returns a dictionary of...
The dictionary should be conducive to being saved in the HDF5 output (perhaps a separate issue to do so). @SylviaWhittle : Would it be useful to have an additional element in the returned dictionary for the triangle co-ordinates from which the minimum feret (triangle height) co-ordinates are derived? |
After discussion with @SylivaWhittle and @llwiggins it was decided that because of the issue of minimum feret coordinates sometimes falling outside of the convex hull that we should not use it as a consistent line through the grain/image. Instead we will use the maximum feret and a line perpendicular to the mid-point of the maximum feret. As such all attempts to work out if minimum feret coordinates/lines were inside or on the edge of a polygon have now been removed and the function returns a dictionary of... + `min_feret` + `min_feret_coords` + `max_feret` + `max_feret_coords` The dictionary should be conducive to being saved in the HDF5 output (perhaps a separate issue to do so).
After discussion with @SylivaWhittle and @llwiggins it was decided that because of the issue of minimum feret coordinates sometimes falling outside of the convex hull that we should not use it as a consistent line through the grain/image. Instead we will use the maximum feret and a line perpendicular to the mid-point of the maximum feret. As such all attempts to work out if minimum feret coordinates/lines were inside or on the edge of a polygon have now been removed and the function returns a dictionary of... + `min_feret` + `min_feret_coords` + `max_feret` + `max_feret_coords` The dictionary should be conducive to being saved in the HDF5 output (perhaps a separate issue to do so).
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.
I ran this locally using some test shapes that I generated and it works perfectly. Returning the coordinates of the min and max ferets is really useful, thank you.
The code reads beautifully and is very comprehendible. I also like the several options that you've added for running it.
Thank you for your patience and sorry for not reviewing it last week as requested.
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).
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).
Further to this adds the `topostats.measure.height_profiles` sub-module which for a given pair of feret coordinates... 1. Determines the orientation of of the feret co-ordinates relative to horizontal. 2. Rotates the image so that the feret is horizontal. 3. Recalculates the co-ordinates of the feret after rotation. 4. Extracts the height profile along the feret. 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`. + 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
Further to this adds the `topostats.measure.height_profiles` sub-module which for a given pair of feret coordinates... 1. Determines the orientation of of the feret co-ordinates relative to horizontal. 2. Rotates the image so that the feret is horizontal. 3. Recalculates the co-ordinates of the feret after rotation. 4. Extracts the height profile along the feret. 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`. + 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
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
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
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
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
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
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
New submodule
measures.feret
with functions for calculation of min and max feret and the associated co-ordinates.Utilises code from Skan developer see gist and as suggested it adds tests. In doing so I found sorting points prior to calculation of upper and lower convex hulls missed some points.
I'm also not sure about the
rotating_calipers()
function as it doesn't seem to return all pairs formed across the points from the upper and lower convex hulls and so have included but not used theall_pairs()
function which does, although if used in place this doesn't return the minimum feret correctly, rather just the smallest distance.Currently some of the tests for the
curved_line
fail too.The intention is to use this without instantiating the
GrainStats
class to be used in profiles (#748) and could serve as a concise stand-alone set of functions outside ofGrainStats
class which currently has static methods for the calculations (#750).Benchmarking
In light of #750 and re-implementing feret calculations as a new sub-module I wanted to see how this compares in terms of performance to those in
GrainStats()
class so have run some very basic benchmarking. Note this is not the full pipeline of taking labelled images and finding the outlines/edge points which are required as inputs to the calculations, its purely on the calculation of min-max feret from the edge points.So this new implementation appears faster.