Skip to content

Commit

Permalink
Implement older monocot pipeline (#54)
Browse files Browse the repository at this point in the history
* Fix docstring and `get_network_distribution_ratio` arguments

* Add `join_pts` and `get_count` functions

* Refactor `get_network_distribution`

* Generalize functions for concatenating and joining points

* Refactor scanline function

* Make `OlderMonocotPipeline`

* Refactor tests and fix arguments

* Refactor `get_network_length` to take arbitrary number of lengths

* Refactor `get_all_pts_array` to simplify

* Refactor `Series` class to take a arbitrary number of labels

* Add 10 DO rice fixture

* Test refactored functions

* Add 10 DO rice fixture

* Add test for `OlderMonocot_pipeline`

* Modify `get_grav_index` to take float and array inputs

* Fix docstring in `Series` class

* Test `OlderMonocotPipeline` and `get_grav_index`

* Lint

* Add folder of 10 do rice test data and test with `find_all_series`

* Refactor series class and related functions to include crown labels

* Refactor tests and fixtures

* Get rid of uncessary monocots flag

* Refactor tips and bases

* Refactor tips and bases tests

* Merge main with `older_monocot_pipeline` branch

* Add standalone get root angle function

* Fix typing

* Add checks to convex hull function

* Get rid of obsolete function

* Fix network distribution

* Add points related functions

* Rename test data

* Test new angle functions

* Test new convex hull functions

* Test new points functions

* Rename test data

* Fix tests

* Match shapes

* Update trait pipelines

* Test trait pipelines

* Test trait pipelines

* Update version

* Resolve conflicts

* Resolve conflicts

* resolve

* Fix tips and bases and tests

* Remove notebooks

* Reformat

* Lint
  • Loading branch information
eberrigan authored Mar 11, 2024
1 parent f40118f commit e54d7f7
Show file tree
Hide file tree
Showing 33 changed files with 2,813 additions and 1,071 deletions.
9 changes: 7 additions & 2 deletions sleap_roots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
import sleap_roots.series
import sleap_roots.summary
import sleap_roots.trait_pipelines
from sleap_roots.trait_pipelines import DicotPipeline, TraitDef, YoungerMonocotPipeline
from sleap_roots.trait_pipelines import (
DicotPipeline,
TraitDef,
YoungerMonocotPipeline,
OlderMonocotPipeline,
)
from sleap_roots.series import Series, find_all_series

# Define package version.
# This is read dynamically by setuptools in pyproject.toml to determine the release version.
__version__ = "0.0.5"
__version__ = "0.0.6"
30 changes: 29 additions & 1 deletion sleap_roots/angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ def get_root_angle(
pts = np.expand_dims(pts, axis=0)

angs_root = []
for i in range(len(node_ind)):
# Calculate the angle for each instance
for i in range(pts.shape[0]):
# if the node_ind is 0, do NOT calculate angs
if node_ind[i] == 0:
angs = np.nan
Expand All @@ -90,3 +91,30 @@ def get_root_angle(
if angs_root.shape[0] == 1:
return angs_root[0]
return angs_root


def get_vector_angles_from_gravity(vectors: np.ndarray) -> np.ndarray:
"""Calculate the angle of given vectors from the gravity vector.
Args:
vectors: An array of vectorss with shape (instances, 2), each representing a vector
from start to end in an instance.
Returns:
An array of angles in degrees with shape (instances,), representing the angle
between each vector and the downward-pointing gravity vector.
"""
gravity_vector = np.array([0, 1]) # Downwards along the positive y-axis
# Calculate the angle between the vectors and the gravity vectors
angles = np.arctan2(vectors[:, 1], vectors[:, 0]) - np.arctan2(
gravity_vector[1], gravity_vector[0]
)
angles = np.degrees(angles)
# Normalize angles to the range [0, 180] since direction doesn't matter
angles = np.abs(angles)
angles[angles > 180] = 360 - angles[angles > 180]

# If only one root, return a scalar instead of a single-element array
if angles.shape[0] == 1:
return angles[0]
return angles
96 changes: 25 additions & 71 deletions sleap_roots/bases.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Trait calculations that rely on bases (i.e., dicot-only)."""
"""Trait calculations that rely on bases."""

import numpy as np
from shapely.geometry import LineString, Point
Expand All @@ -7,20 +7,16 @@
from typing import Union, Tuple


def get_bases(pts: np.ndarray, monocots: bool = False) -> np.ndarray:
def get_bases(pts: np.ndarray) -> np.ndarray:
"""Return bases (r1) from each root.
Args:
pts: Root landmarks as array of shape `(instances, nodes, 2)` or `(nodes, 2)`.
monocots: Boolean value, where false is dicot (default), true is rice.
Returns:
Array of bases `(instances, (x, y))`. If the input is `(nodes, 2)`, an array of
shape `(2,)` will be returned.
shape `(2,)` will be returned.
"""
if monocots:
return np.nan

# If the input has shape `(nodes, 2)`, reshape it for consistency
if pts.ndim == 2:
pts = pts[np.newaxis, ...]
Expand Down Expand Up @@ -73,57 +69,33 @@ def get_base_tip_dist(
return distances


def get_lateral_count(pts: np.ndarray):
"""Get number of lateral roots.
Args:
pts: lateral root landmarks as array of shape `(instance, node, 2)`.
Return:
Scalar of number of lateral roots.
"""
lateral_count = pts.shape[0]
return lateral_count


def get_base_xs(base_pts: np.ndarray) -> np.ndarray:
"""Get x coordinates of the base of each lateral root.
Args:
base_pts: Array of bases as returned by `get_bases`, shape `(instances, 2)` or
`(2,)`.
base_pts: root bases as array of shape `(instances, 2)` or `(2)` when there is
only one root, as is the case for primary roots.
Returns:
An array of the x-coordinates of bases `(instances,)` or a single x-coordinate.
Return:
An array of base x-coordinates (instances,) or (1,) when there is only one root.
"""
# If the input is a single number (float or integer), return np.nan
if isinstance(base_pts, (np.floating, float, np.integer, int)):
return np.nan

# If the base points array has shape `(2,)`, return the first element (x)
if base_pts.ndim == 1 and base_pts.shape[0] == 2:
return base_pts[0]

# If the base points array doesn't have exactly 2 dimensions or
# the second dimension is not of size 2, raise an error
elif base_pts.ndim != 2 or base_pts.shape[1] != 2:
if base_pts.ndim not in (1, 2):
raise ValueError(
"Array of base points must be 2-dimensional with shape (instances, 2)."
"Input array must be 2-dimensional (instances, 2) or 1-dimensional (2,)."
)
if base_pts.shape[-1] != 2:
raise ValueError("Last dimension must be (x, y).")

# If everything is fine, extract and return the x-coordinates of the base points
else:
base_xs = base_pts[:, 0]
return base_xs
base_xs = base_pts[..., 0]
return base_xs


def get_base_ys(base_pts: np.ndarray, monocots: bool = False) -> np.ndarray:
def get_base_ys(base_pts: np.ndarray) -> np.ndarray:
"""Get y coordinates of the base of each root.
Args:
base_pts: root bases as array of shape `(instances, 2)` or `(2)`
when there is only one root, as is the case for primary roots.
monocots: Boolean value, where false is dicot (default), true is rice.
Return:
An array of the y-coordinates of bases (instances,).
Expand All @@ -144,25 +116,19 @@ def get_base_ys(base_pts: np.ndarray, monocots: bool = False) -> np.ndarray:
return base_ys


def get_base_length(lateral_base_ys: np.ndarray, monocots: bool = False) -> float:
def get_base_length(lateral_base_ys: np.ndarray) -> float:
"""Get the y-axis difference from the top lateral base to the bottom lateral base.
Args:
lateral_base_ys: y-coordinates of the base points of lateral roots of shape
`(instances,)`.
monocots: Boolean value, where false is dicot (default), true is rice.
Return:
The distance between the top base y-coordinate and the deepest
base y-coordinate.
"""
# If the roots are monocots, return NaN
if monocots:
return np.nan

# Compute the difference between the maximum and minimum y-coordinates
base_length = np.nanmax(lateral_base_ys) - np.nanmin(lateral_base_ys)

return base_length


Expand All @@ -179,7 +145,7 @@ def get_base_ct_density(
Return:
Scalar of base count density.
"""
# Check if the input is invalid
# Check if the input is valid for lateral_base_pts
if (
isinstance(lateral_base_pts, (np.floating, float, np.integer, int))
or np.isnan(lateral_base_pts).all()
Expand All @@ -204,22 +170,19 @@ def get_base_ct_density(
return base_ct_density


def get_base_length_ratio(
primary_length: float, base_length: float, monocots: bool = False
) -> float:
def get_base_length_ratio(primary_length: float, base_length: float) -> float:
"""Calculate the ratio of the length of the bases to the primary root length.
Args:
primary_length (float): Length of the primary root.
base_length (float): Length of the bases along the primary root.
monocots (bool): True if the roots are monocots, False if they are dicots.
Returns:
Ratio of the length of the bases along the primary root to the primary root
length.
"""
# If roots are monocots or either of the lengths are NaN, return NaN
if monocots or np.isnan(primary_length) or np.isnan(base_length):
# If either of the lengths are NaN, return NaN
if np.isnan(primary_length) or np.isnan(base_length):
return np.nan

# Handle case where primary length is zero to avoid division by zero
Expand All @@ -231,24 +194,19 @@ def get_base_length_ratio(
return base_length_ratio


def get_base_median_ratio(lateral_base_ys, primary_tip_pt_y, monocots: bool = False):
def get_base_median_ratio(lateral_base_ys, primary_tip_pt_y):
"""Get ratio of median value in all base points to tip of primary root in y axis.
Args:
lateral_base_ys: y-coordinates of the base points of lateral roots of shape
lateral_base_ys: Y-coordinates of the base points of lateral roots of shape
`(instances,)`.
primary_tip_pt_y: y-coordinate of the tip point of the primary root of shape
primary_tip_pt_y: Y-coordinate of the tip point of the primary root of shape
`(1)`.
monocots: Boolean value, where false is dicot (default), true is rice.
Return:
Scalar of base median ratio. If all y-coordinates of the lateral root bases are
NaN, the function returns NaN.
NaN, the function returns NaN.
"""
# Check if the roots are monocots, if so return NaN
if monocots:
return np.nan

# Check if all y-coordinates of lateral root bases are NaN, if so return NaN
if np.isnan(lateral_base_ys).all():
return np.nan
Expand All @@ -271,9 +229,8 @@ def get_root_widths(
primary_max_length_pts: np.ndarray,
lateral_pts: np.ndarray,
tolerance: float = 0.02,
monocots: bool = False,
return_inds: bool = False,
) -> (np.ndarray, list, np.ndarray, np.ndarray):
) -> Tuple[np.ndarray, list, np.ndarray, np.ndarray]:
"""Estimate root width using bases of lateral roots.
Args:
Expand All @@ -283,8 +240,6 @@ def get_root_widths(
shape (n, nodes, 2).
tolerance: Tolerance level for the projection difference between matched roots.
Defaults to 0.02.
monocots: Indicates the type of plant. Set to False for dicots (default) or
True for monocots like rice.
return_inds: Flag to indicate whether to return matched indices along with
distances. Defaults to False.
Expand Down Expand Up @@ -326,11 +281,10 @@ def get_root_widths(
default_left_bases = np.empty((0, 2))
default_right_bases = np.empty((0, 2))

# Check for minimum length, monocots, or all NaNs in arrays
# Check for minimum length, or all NaNs in arrays
if (
len(primary_max_length_pts) < 2
or len(lateral_pts) < 2
or monocots
or np.isnan(primary_max_length_pts).all()
or np.isnan(lateral_pts).all()
):
Expand Down
Loading

0 comments on commit e54d7f7

Please sign in to comment.