Skip to content
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

Implement older monocot pipeline #54

Merged
merged 50 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3e841ec
Fix docstring and `get_network_distribution_ratio` arguments
eberrigan Aug 17, 2023
92dae9e
Add `join_pts` and `get_count` functions
eberrigan Aug 20, 2023
9325a1b
Refactor `get_network_distribution`
eberrigan Aug 21, 2023
1621efa
Generalize functions for concatenating and joining points
eberrigan Aug 21, 2023
cb0f13b
Refactor scanline function
eberrigan Aug 21, 2023
e23af0f
Make `OlderMonocotPipeline`
eberrigan Aug 21, 2023
4bc568f
Refactor tests and fix arguments
eberrigan Aug 21, 2023
ff1fb10
Refactor `get_network_length` to take arbitrary number of lengths
eberrigan Aug 23, 2023
57b8fed
Refactor `get_all_pts_array` to simplify
eberrigan Aug 23, 2023
25391b4
Refactor `Series` class to take a arbitrary number of labels
eberrigan Aug 23, 2023
dfabf07
Add 10 DO rice fixture
eberrigan Aug 23, 2023
890df7f
Test refactored functions
eberrigan Aug 23, 2023
4843f08
Add 10 DO rice fixture
eberrigan Aug 23, 2023
0c42ecb
Add test for `OlderMonocot_pipeline`
eberrigan Aug 27, 2023
f33f0e0
Modify `get_grav_index` to take float and array inputs
eberrigan Aug 31, 2023
797561b
Fix docstring in `Series` class
eberrigan Aug 31, 2023
aa59e2b
Test `OlderMonocotPipeline` and `get_grav_index`
eberrigan Aug 31, 2023
bd2621b
Lint
eberrigan Aug 31, 2023
045a7b4
Add folder of 10 do rice test data and test with `find_all_series`
eberrigan Sep 1, 2023
2364b89
Refactor series class and related functions to include crown labels
eberrigan Feb 20, 2024
a92be32
Refactor tests and fixtures
eberrigan Feb 20, 2024
1fceb01
Get rid of uncessary monocots flag
eberrigan Feb 21, 2024
1ab5a39
Refactor tips and bases
eberrigan Feb 21, 2024
11df884
Refactor tips and bases tests
eberrigan Feb 21, 2024
606af51
Merge main with `older_monocot_pipeline` branch
eberrigan Feb 21, 2024
3d1109f
Add standalone get root angle function
eberrigan Feb 27, 2024
57cfbd1
Fix typing
eberrigan Feb 27, 2024
c063a9f
Add checks to convex hull function
eberrigan Feb 27, 2024
29cdb7a
Get rid of obsolete function
eberrigan Feb 27, 2024
113ee3e
Fix network distribution
eberrigan Feb 27, 2024
534d9e6
Add points related functions
eberrigan Feb 27, 2024
b372567
Rename test data
eberrigan Feb 27, 2024
93cfe87
Test new angle functions
eberrigan Feb 27, 2024
7464680
Test new convex hull functions
eberrigan Feb 27, 2024
dd06872
Test new points functions
eberrigan Feb 27, 2024
2279ac0
Rename test data
eberrigan Feb 27, 2024
655d9cc
Fix tests
eberrigan Feb 27, 2024
ef271da
Match shapes
eberrigan Feb 27, 2024
a9054e3
Update trait pipelines
eberrigan Feb 28, 2024
d175680
Test trait pipelines
eberrigan Mar 1, 2024
5efb4fb
Test trait pipelines
eberrigan Mar 1, 2024
2416fd8
Update version
eberrigan Mar 1, 2024
9838314
Resolve conflicts
eberrigan Mar 1, 2024
0c4b4df
Resolve conflicts
eberrigan Mar 1, 2024
1e692b5
resolve
eberrigan Mar 1, 2024
10eb050
Merge branch 'main' into elizabeth/older_monocot_pipeline
eberrigan Mar 1, 2024
264dcca
Fix tips and bases and tests
eberrigan Mar 2, 2024
e38b289
Remove notebooks
eberrigan Mar 2, 2024
880817d
Reformat
eberrigan Mar 2, 2024
83b6ddd
Lint
eberrigan Mar 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 120 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# sleap-roots

[![CI](https://github.com/talmolab/sleap-roots/actions/workflows/ci.yml/badge.svg)](https://github.com/talmolab/sleap-roots/actions/workflows/ci.yml)
[![Lint](https://github.com/talmolab/sleap-roots/actions/workflows/lint.yml/badge.svg)](https://github.com/talmolab/sleap-roots/actions/workflows/lint.yml)
[![codecov](https://codecov.io/gh/talmolab/sleap-roots/branch/main/graph/badge.svg)](https://codecov.io/gh/talmolab/sleap-roots)
[![Release](https://img.shields.io/github/v/release/talmolab/sleap-roots?label=Latest)](https://github.com/talmolab/sleap-roots/releases/)
[![PyPI](https://img.shields.io/pypi/v/sleap-roots?label=PyPI)](https://pypi.org/project/sleap-roots)
Expand All @@ -10,17 +9,119 @@ Analysis tools for [SLEAP](https://sleap.ai)-based plant root phenotyping.

## Installation
```
pip install git+https://github.com/talmolab/sleap-roots.git@main
pip install sleap-roots
```

If you are using conda:
If you are using conda (recommended):
```
conda create -n sleap-roots python=3.8
conda create -n sleap-roots python=3.9
conda activate sleap-roots
pip install git+https://github.com/talmolab/sleap-roots.git@main
pip install sleap-roots
```

### Development
## Usage

### `DicotPipeline`

**1. Computing traits for a single plant:**

```py
import sleap_roots as sr

plant = sr.Series.load(
"tests/data/canola_7do/919QDUH.h5",
primary_name="primary_multi_day",
lateral_name="lateral_3_nodes"
)
pipeline = sr.DicotPipeline()
traits = pipeline.compute_plant_traits(plant, write_csv=True)
```

**2. Computing traits for a batch of plants:**

```py
import sleap_roots as sr

plant_paths = sr.find_all_series("tests/data/soy_6do")
plants = [
sr.Series.load(
plant_path,
primary_name="primary_multi_day",
lateral_name="lateral__nodes",
) for plant_path in plant_paths]

pipeline = sr.DicotPipeline()
all_traits = pipeline.compute_batch_traits(plants, write_csv=True)
```

**3. Computing individual traits:**

```py
import sleap_roots as sr
import numpy as np

plant = sr.Series.load(
"tests/data/canola_7do/919QDUH.h5",
primary_name="primary_multi_day",
lateral_name="lateral_3_nodes"
)

primary, lateral = plant[10]
pts = np.concatenate([primary.numpy(), lateral.numpy()], axis=0).reshape(-1, 2)
convex_hull = sr.convhull.get_convhull(pts)
```

### `YoungerMonocotPipeline`

**1. Computing traits for a single plant:**

```py
import sleap_roots as sr

plant = sr.Series.load(
"tests/data/rice_3do/0K9E8BI.h5",
primary_name="longest_3do_6nodes",
lateral_name="main_3do_6nodes"
)
pipeline = sr.YoungerMonocotPipeline()
traits = pipeline.compute_plant_traits(plant, write_csv=True)
```

**2. Computing traits for a batch of plants:**

```py
import sleap_roots as sr

plant_paths = sr.find_all_series("tests/data/rice_3do")
plants = [
sr.Series.load(
plant_path,
primary_name="longest_3do_6nodes",
lateral_name="main_3do_6nodes"
) for plant_path in plant_paths]

pipeline = sr.YoungerMonocotPipeline()
all_traits = pipeline.compute_batch_traits(plants, write_csv=True)
```

**3. Computing individual traits:**

```py
import sleap_roots as sr
import numpy as np

plant = sr.Series.load(
"tests/data/rice_3do/0K9E8BI.h5",
primary_name="longest_3do_6nodes",
lateral_name="main_3do_6nodes"
)

primary, lateral = plant[10]
pts = np.concatenate([primary.numpy(), lateral.numpy()], axis=0).reshape(-1, 2)
convex_hull = sr.convhull.get_convhull(pts)
```

## Development
For development, first clone the repository:
```
git clone https://github.com/talmolab/sleap-roots && cd sleap-roots
Expand Down Expand Up @@ -54,13 +155,16 @@ Then run `pytest` with:
pytest tests
```

To **develop on M1 Macs**, you'll need to manually install SLEAP first like this:
```
git clone https://github.com/talmolab/sleap && cd sleap
conda env create -f environment_m1.yml -n sleap-roots
```
Then, install this package in editable mode:
```
cd .. && git clone https://github.com/talmolab/sleap-roots
pip install -e ".[dev]"
```
## Acknowledgments

This repository was created by the [Talmo Lab](https://talmolab.org) and [Busch Lab](https://busch.salk.edu) at the Salk Institute for Biological Studies as part of the [Harnessing Plants Initiative](https://www.salk.edu/harnessing-plants-initiative/).

### Contributors

- Elizabeth Berrigan
- Lin Wang
- Talmo Pereira

### Citation

*Coming soon.*
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ authors = [
{name = "Talmo Pereira", email = "[email protected]"}
]
description="Analysis tools for SLEAP-based plant root phenotyping."
requires-python = ">=3.8"
requires-python = ">=3.7"
keywords = ["sleap", "plants", "roots", "phenotyping"]
license = {text = "BSD-3-Clause"}
classifiers = [
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9"
]
Expand All @@ -24,7 +25,7 @@ dependencies = [
"pandas",
"matplotlib",
"seaborn",
"sleap-io>=0.0.6",
"sleap-io>=0.0.11",
"scikit-image",
"shapely"
]
Expand Down
7 changes: 4 additions & 3 deletions sleap_roots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
import sleap_roots.convhull
import sleap_roots.ellipse
import sleap_roots.networklength
import sleap_roots.lengths
import sleap_roots.points
import sleap_roots.scanline
import sleap_roots.series
import sleap_roots.summary
import sleap_roots.trait_pipelines
from sleap_roots.trait_pipelines import DicotPipeline, TraitDef
from sleap_roots.series import Series
from sleap_roots.trait_pipelines import DicotPipeline, TraitDef, YoungerMonocotPipeline
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.1"
__version__ = "0.0.5"
96 changes: 51 additions & 45 deletions sleap_roots/angle.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Get angle of each root."""

import numpy as np
import math


def get_node_ind(pts: np.ndarray, proximal: bool = True) -> np.ndarray:
Expand All @@ -12,68 +11,46 @@ def get_node_ind(pts: np.ndarray, proximal: bool = True) -> np.ndarray:
proximal: Boolean value, where true is proximal (default), false is distal.

Returns:
An array of shape (instances,) of proximal or distal node index.
"""
# Check if pts is a numpy array
if not isinstance(pts, np.ndarray):
raise TypeError("Input pts should be a numpy array.")
An array of shape (instances,) of proximal or distal node indices.

# Check if pts has 2 or 3 dimensions
if pts.ndim not in [2, 3]:
raise ValueError("Input pts should have 2 or 3 dimensions.")
The proximal node is the first non-NaN node in the first half of the root.

# Check if the last dimension of pts has size 2
if pts.shape[-1] != 2:
raise ValueError(
"The last dimension of the input pts should have size 2,"
"representing x and y coordinates."
)
The distal node is the last non-NaN node in the last half of the root.

If all nodes (or all nodes in the half of the root) are NaN, then zero is
returned.
"""
# Check if pts is 2D, if so, reshape to 3D
if pts.ndim == 2:
pts = pts[np.newaxis, ...]

n_instances, n_nodes, _ = pts.shape

# Identify where NaN values exist
nan_mask = np.isnan(pts).any(axis=-1)
is_nan = np.isnan(pts).any(axis=-1) # (n_instances, n_nodes)

# If only NaN values, return NaN
if nan_mask.all():
return np.nan
if is_nan.all():
return np.zeros((n_instances,))

if proximal:
# For proximal, we want the first non-NaN node in the first half root
# get the first half nan mask (exclude the base node)
node_proximal = nan_mask[:, 1 : int((nan_mask.shape[1] + 1) / 2)]
# get the nearest non-Nan node index
node_ind = np.argmax(~node_proximal, axis=-1)
# if there is no non-Nan node, set value of 99
node_ind[node_proximal.all(axis=1)] = 99
node_ind = node_ind + 1 # adjust indices by adding one (base node)
# Proximal nodes are in the first half of the root.
is_nan = is_nan[:, 1 : (n_nodes + 1) // 2]
node_ind = np.argmax(~is_nan, axis=-1) + 1
else:
# For distal, we want the last non-NaN node in the last half root
# get the last half nan mask
node_distal = nan_mask[:, int(nan_mask.shape[1] / 2) :]
# get the farest non-Nan node
node_ind = (node_distal[:, ::-1] == False).argmax(axis=1)
node_ind[node_distal.all(axis=1)] = -95 # set value if no non-Nan node
node_ind = pts.shape[1] - node_ind - 1 # adjust indices by reversing
# Distal nodes are in the last half of the root.
is_nan = is_nan[:, (n_nodes + 1) // 2 :]
node_ind = np.argmax(~is_nan[:, ::-1], axis=-1)
node_ind = n_nodes - node_ind - 1

# reset indices of 0 (base node) if no non-Nan node
node_ind[node_ind == 100] = 0

# If pts was originally 2D, return a scalar instead of a single-element array
if pts.shape[0] == 1:
return node_ind[0]

# If only one root, return a scalar instead of a single-element array
if node_ind.shape[0] == 1:
return node_ind[0]
# If the selected index is missing originally, return 0.
node_ind = np.where(is_nan.all(axis=-1), 0, node_ind)

return node_ind


def get_root_angle(
pts: np.ndarray, node_ind: np.ndarray, proximal: bool = True, base_ind=0
pts: np.ndarray, node_ind: np.ndarray, proximal: bool = True, base_ind: int = 0
) -> np.ndarray:
"""Find angles for each root.

Expand All @@ -97,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 @@ -113,3 +91,31 @@ 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, assuming the gravity
vector points downwards along the positive y-axis.

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
Loading