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

Handle unsupported geometry types #91

Merged
merged 26 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2d460a2
add test data that breaks extract_points_from_geometry
eberrigan Oct 24, 2024
12a2db9
replace images filename in slp file
eberrigan Oct 24, 2024
50144ac
update OlderMonocotPipeline notebook
eberrigan Oct 24, 2024
64a25e6
return empty list if unsupported geometry type
eberrigan Oct 25, 2024
924a545
update oldermonocot pipeline
eberrigan Oct 28, 2024
635bab8
use logging statements for convex hull calculations
eberrigan Oct 28, 2024
a76e2e5
modify tests for two plants
eberrigan Oct 28, 2024
f2f7f63
check intersection for empty list, return nans
eberrigan Oct 28, 2024
b3dd90c
add more test for unexpected inputs to extract_points_from_geometry
eberrigan Oct 28, 2024
14e32a8
include empty MultiLineString and remove duplicate test
eberrigan Oct 28, 2024
f9015b5
update OlderMonocot notebook
eberrigan Oct 28, 2024
5fc3c71
add stunted 10 day-old rice predictions as fixture
eberrigan Oct 28, 2024
239dd94
fix paths
eberrigan Oct 28, 2024
73a8490
organize and add needed imports
eberrigan Oct 28, 2024
3ec802c
update OlderMonocot pipeline notebook
eberrigan Oct 28, 2024
692abc4
black
eberrigan Oct 28, 2024
22aee1a
black
eberrigan Oct 28, 2024
e21fa97
test new fixture
eberrigan Oct 28, 2024
71bed6a
black
eberrigan Oct 28, 2024
5891f7b
black
eberrigan Oct 28, 2024
73eb4b0
add more unexpected types
eberrigan Oct 28, 2024
bba1c45
use logging
eberrigan Oct 29, 2024
b2cdb68
add test for inf values
eberrigan Oct 29, 2024
b46b625
add more tests for multilinestring
eberrigan Oct 29, 2024
db0db11
add tests for nex convex hull features using rice fixture
eberrigan Oct 29, 2024
8854f02
black
eberrigan Oct 29, 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
370 changes: 312 additions & 58 deletions notebooks/OlderMonocotPipeline.ipynb

Large diffs are not rendered by default.

37 changes: 29 additions & 8 deletions sleap_roots/convhull.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Convex hull fitting and derived trait calculation."""

import numpy as np
import logging
from scipy.spatial import ConvexHull
from scipy.spatial.distance import pdist
from typing import Tuple, Optional, Union
Expand Down Expand Up @@ -30,13 +31,13 @@

# Check for infinite values
if np.isinf(pts).any():
print("Cannot compute convex hull: input contains infinite values.")
logging.info("Cannot compute convex hull: input contains infinite values.")

Check warning on line 34 in sleap_roots/convhull.py

View check run for this annotation

Codecov / codecov/patch

sleap_roots/convhull.py#L34

Added line #L34 was not covered by tests
eberrigan marked this conversation as resolved.
Show resolved Hide resolved
return None

# Ensure there are at least 3 unique non-collinear points
unique_pts = np.unique(pts, axis=0)
if len(unique_pts) < 3:
print("Cannot compute convex hull: not enough unique points.")
logging.info("Cannot compute convex hull: not enough unique points.")
return None

try:
Expand Down Expand Up @@ -384,16 +385,25 @@

# Find the intersection between the hull perimeter and the extended line
intersection = extended_line.intersection(hull_perimeter)
logging.debug(f"Intersection: {intersection}")

# Compute the intersection points and add to lists
if not intersection.is_empty:
if intersection and not intersection.is_empty:
logging.debug("Intersection points found between the convex hull and the line.")
intersect_points = extract_points_from_geometry(intersection)
above_line.extend(intersect_points)
below_line.extend(intersect_points)
if not intersect_points: # Ensure it's not an empty list
return np.nan, np.nan
else:
logging.debug("No intersection points found between the convex hull and the line.")
return np.nan, np.nan

# Add intersection points to the lists
above_line.extend(intersect_points)
below_line.extend(intersect_points)

# Calculate areas using get_chull_area
area_above_line = get_chull_area(np.array(above_line)) if above_line else 0.0
area_below_line = get_chull_area(np.array(below_line)) if below_line else 0.0
area_above_line = get_chull_area(np.array(above_line)) if above_line else np.nan
area_below_line = get_chull_area(np.array(below_line)) if below_line else np.nan

return area_above_line, area_below_line

Expand Down Expand Up @@ -475,19 +485,22 @@

# Check for a valid or existing convex hull
if hull is None or len(unique_pts) < 3:
logging.debug("Not enough unique points to compute convex hull intersections.")
# Return two vectors of NaNs if not valid hull
return (np.array([[np.nan, np.nan]]), np.array([[np.nan, np.nan]]))

# Ensure rn_pts does not contain NaN values
rn_pts_valid = rn_pts[~np.isnan(rn_pts).any(axis=1)]
# Need at least two points to define a line
if len(rn_pts_valid) < 2:
logging.debug("Not enough valid rn points to compute convex hull intersections.")

Check warning on line 496 in sleap_roots/convhull.py

View check run for this annotation

Codecov / codecov/patch

sleap_roots/convhull.py#L496

Added line #L496 was not covered by tests
return (np.array([[np.nan, np.nan]]), np.array([[np.nan, np.nan]]))

# Ensuring r0_pts does not contain NaN values
r0_pts_valid = r0_pts[~np.isnan(r0_pts).any(axis=1)]
# Expect two vectors in the end
if len(r0_pts_valid) < 2:
logging.debug("Not enough valid r0 points to compute convex hull intersections.")
return (np.array([[np.nan, np.nan]]), np.array([[np.nan, np.nan]]))

# Get the vertices of the convex hull
Expand All @@ -509,6 +522,7 @@
leftmost_vector = np.array([[np.nan, np.nan]])
rightmost_vector = np.array([[np.nan, np.nan]])
if not is_leftmost_on_hull and not is_rightmost_on_hull:
logging.debug("Leftmost and rightmost r0 points are not on the convex hull.")

Check warning on line 525 in sleap_roots/convhull.py

View check run for this annotation

Codecov / codecov/patch

sleap_roots/convhull.py#L525

Added line #L525 was not covered by tests
# If leftmost and rightmost r0 points are not on the convex hull return NaNs
return leftmost_vector, rightmost_vector

Expand All @@ -518,6 +532,7 @@
rightmost_rn = rn_pts[np.argmax(rn_pts[:, 0])]
m, b = get_line_equation_from_points(leftmost_rn, rightmost_rn)
except Exception:
logging.debug("Could not find line equation between leftmost and rightmost rn points.")

Check warning on line 535 in sleap_roots/convhull.py

View check run for this annotation

Codecov / codecov/patch

sleap_roots/convhull.py#L535

Added line #L535 was not covered by tests
# If line equation cannot be found, return NaNs
return leftmost_vector, rightmost_vector

Expand Down Expand Up @@ -545,11 +560,17 @@

# Find the intersection between the hull perimeter and the extended line
intersection = extended_line.intersection(hull_perimeter)
logging.debug(f"Intersection: {intersection}")

# Get the intersection points
if not intersection.is_empty:
if intersection and not intersection.is_empty:
logging.debug(f"Intersection points found between the convex hull and the line: {intersection}.")
intersect_points = extract_points_from_geometry(intersection)
if not intersect_points: # Ensure it's not an empty list
logging.debug("No intersection points found after extraction.")
return leftmost_vector, rightmost_vector
eberrigan marked this conversation as resolved.
Show resolved Hide resolved
else:
logging.debug("No intersection points found between the convex hull and the line.")
# Return two vectors of NaNs if there is no intersection
return leftmost_vector, rightmost_vector

Expand Down
7 changes: 6 additions & 1 deletion sleap_roots/points.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Get traits related to the points."""

import numpy as np
import logging
from matplotlib import pyplot as plt
from matplotlib.lines import Line2D
from shapely.geometry import Point, MultiPoint, LineString, GeometryCollection
Expand Down Expand Up @@ -31,6 +32,8 @@ def extract_points_from_geometry(geometry) -> List[np.ndarray]:
>>> extract_points_from_geometry(geom_col)
[array([1, 2]), array([1, 2]), array([3, 4]), array([0, 0]), array([1, 1]), array([2, 2])]
"""
logging.debug(f"Geometry type: {type(geometry).__name__}")
logging.debug(f"Geometry: {geometry}")
if isinstance(geometry, Point):
return [np.array([geometry.x, geometry.y])]
elif isinstance(geometry, MultiPoint):
Expand All @@ -43,7 +46,9 @@ def extract_points_from_geometry(geometry) -> List[np.ndarray]:
points.extend(extract_points_from_geometry(geom))
return points
else:
raise TypeError(f"Unsupported geometry type: {type(geometry).__name__}")
logging.info(f"Unsupported geometry type: {type(geometry).__name__}")
return []



def get_count(pts: np.ndarray):
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Git LFS file not shown
36 changes: 34 additions & 2 deletions tests/test_points.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np
import pytest
from shapely.geometry import Point, MultiPoint, LineString, GeometryCollection
from shapely.geometry import Point, MultiPoint, LineString, GeometryCollection, MultiLineString
from sleap_roots import Series
from sleap_roots.lengths import get_max_length_pts
from sleap_roots.points import (
Expand Down Expand Up @@ -779,6 +779,13 @@ def test_extract_from_linestring():
results = extract_points_from_geometry(linestring)
assert all(np.array_equal(result, exp) for result, exp in zip(results, expected))

def test_extract_from_multilinestring():
multilinestring = MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]])
# Multilinestring is not supported, so it should return an empty list
expected = []
results = extract_points_from_geometry(multilinestring)
assert all(np.array_equal(result, exp) for result, exp in zip(results, expected))

eberrigan marked this conversation as resolved.
Show resolved Hide resolved

def test_extract_from_geometrycollection():
geom_collection = GeometryCollection([Point(1, 2), LineString([(0, 0), (1, 1)])])
Expand All @@ -803,10 +810,35 @@ def test_extract_from_unsupported_type():
with pytest.raises(NameError):
extract_points_from_geometry(
Polygon([(0, 0), (1, 1), (1, 0)])
) # Polygon is unsupported
) # Polygon is unsupported type and not imported from shapely.geometry


def test_extract_from_empty_geometrycollection():
empty_geom_collection = GeometryCollection()
expected = []
assert extract_points_from_geometry(empty_geom_collection) == expected


@pytest.mark.parametrize(
"geometry, expected",
[

(MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]), []),
(GeometryCollection([Point(1, 2), LineString([(0, 0), (1, 1)]), MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]])]), [np.array([1, 2]), np.array([0, 0]), np.array([1, 1])]), # GeometryCollection with MultiLineString
],
)
def test_extract_from_multilinestring(geometry, expected):
results = extract_points_from_geometry(geometry)
assert all(np.array_equal(result, exp) for result, exp in zip(results, expected))


@pytest.mark.parametrize(
"unexpected_input, expected_output",
[
("24", []),
(None, []),
(5, []),
],
)
def test_extract_from_unsupported_geometry(unexpected_input, expected_output):
assert extract_points_from_geometry(unexpected_input) == expected_output
19 changes: 9 additions & 10 deletions tests/test_trait_pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,23 +182,22 @@ def test_younger_monocot_pipeline(rice_pipeline_output_folder):
def test_older_monocot_pipeline(rice_10do_pipeline_output_folder):
# Find slp paths in folder
slp_paths = find_all_slp_paths(rice_10do_pipeline_output_folder)
assert len(slp_paths) == 1
assert len(slp_paths) == 2
# Load series from slps
rice_series_all = load_series_from_slps(
slp_paths=slp_paths, h5s=False, csv_path=None
)
assert len(rice_series_all) == 1
assert len(rice_series_all) == 2
# Get first series
rice_series = rice_series_all[0]
assert rice_series.series_name == "scan0K9E8BI"

pipeline = OlderMonocotPipeline()
all_traits = pipeline.compute_batch_traits(rice_series_all)
rice_traits = pipeline.compute_plant_traits(rice_series)

# Dataframe shape assertions
assert rice_traits.shape == (72, 102)
assert all_traits.shape == (1, 901)
assert all_traits.shape == (2, 901)

# Dataframe dtype assertions
expected_rice_traits_dtypes = {
Expand All @@ -223,20 +222,20 @@ def test_older_monocot_pipeline(rice_10do_pipeline_output_folder):

# Value range assertions for traits
assert (
all_traits["crown_curve_indices_mean_median"] >= 0
all_traits["crown_curve_indices_mean_median"].dropna() >= 0
).all(), "crown_curve_indices_mean_median in all_traits contains negative values"
assert (
all_traits["crown_curve_indices_mean_median"] <= 1
all_traits["crown_curve_indices_mean_median"].dropna() <= 1
).all(), (
"crown_curve_indices_mean_median in all_traits contains values greater than 1"
)
assert (
(0 <= rice_traits["crown_angles_proximal_p95"])
& (rice_traits["crown_angles_proximal_p95"] <= 180)
(0 <= rice_traits["crown_angles_proximal_p95"].dropna())
& (rice_traits["crown_angles_proximal_p95"].dropna() <= 180)
).all(), "angle_column in rice_traits contains values out of range [0, 180]"
assert (
(0 <= all_traits["crown_angles_proximal_median_p95"])
& (all_traits["crown_angles_proximal_median_p95"] <= 180)
(0 <= all_traits["crown_angles_proximal_median_p95"].dropna())
& (all_traits["crown_angles_proximal_median_p95"].dropna() <= 180)
).all(), "angle_column in all_traits contains values out of range [0, 180]"


Expand Down
Loading