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

Add segment_ends and Update morphology tips outputs #1644

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ac60d30
Create segment_ends.py
HaleySchuhl Dec 17, 2024
a5ef615
range rather than enumerate
HaleySchuhl Dec 17, 2024
269137a
helper for _find_tips
HaleySchuhl Dec 18, 2024
cf17f11
update more instances of find_tips to use helper
HaleySchuhl Dec 18, 2024
034ebac
remove label from helper function
HaleySchuhl Dec 18, 2024
d0f49b2
Update __init__.py
HaleySchuhl Dec 18, 2024
0ea3128
add label logic into the front facing function
HaleySchuhl Dec 19, 2024
66f25fb
update segment sort stop editing tip info
HaleySchuhl Dec 19, 2024
7558e8c
add segment_ends to morphology init
HaleySchuhl Dec 19, 2024
acffd97
add segment_img as required input, add outputs
HaleySchuhl Dec 19, 2024
43cfbdb
Create test_segment_ends.py
HaleySchuhl Dec 19, 2024
81d0574
update segment_id allow optimal_assignment of IDs
HaleySchuhl Dec 19, 2024
4c48eac
Update test_segment_id.py
HaleySchuhl Dec 19, 2024
eddfab5
convert to color for debug image
HaleySchuhl Dec 20, 2024
49bdc78
segment_ends check tip or branch_pt
HaleySchuhl Dec 20, 2024
5753678
test_segment_ends_no_mask
HaleySchuhl Dec 23, 2024
fc6074d
segment_sort no need to store label
HaleySchuhl Dec 23, 2024
be3cdb7
Re-defined variable from outer scope
HaleySchuhl Dec 23, 2024
86db239
remove unused var
HaleySchuhl Dec 23, 2024
9e84adc
white space fixes
HaleySchuhl Dec 23, 2024
dcea2a9
line length fixes
HaleySchuhl Dec 23, 2024
333e353
remove trailing whitespace
HaleySchuhl Dec 23, 2024
ee3e4be
more whitespaces fixes
HaleySchuhl Dec 23, 2024
0b63217
move _iterative_prune and _find_tips to _helpers
HaleySchuhl Dec 23, 2024
554b499
whitespace fixes
HaleySchuhl Dec 23, 2024
f802504
move segment_end sorting logic into _helpers
HaleySchuhl Jan 2, 2025
d8fd5c0
add logic for returning the optimal segment assignment
HaleySchuhl Jan 2, 2025
0b296b1
updating.md add segment_ends
HaleySchuhl Jan 2, 2025
6fde9cd
Create segment_ends.md
HaleySchuhl Jan 2, 2025
a8c74a5
whitespace deepsource fixes
HaleySchuhl Jan 2, 2025
85c2c9f
add debug images for segment ends
HaleySchuhl Jan 3, 2025
869c232
return sorted objs instead of ordered labels
HaleySchuhl Jan 3, 2025
2e4149d
Update mkdocs.yml
HaleySchuhl Jan 3, 2025
fa7f689
update docs to reflect new sorting approach
HaleySchuhl Jan 3, 2025
2b8480a
update segment_ends sorting logic
HaleySchuhl Jan 22, 2025
a3d0cff
minor tweaks to docs for clarity and accuracy
HaleySchuhl Jan 30, 2025
bd17ad8
Merge branch 'main' into update_morphology_tips_outputs
HaleySchuhl Jan 30, 2025
a38e353
Merge branch 'main' into update_morphology_tips_outputs
k034b363 Mar 7, 2025
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
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.
51 changes: 51 additions & 0 deletions docs/segment_ends.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the context is incomplete, just needs the end of that sentence about grass species.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in the example usage there is a parenthesis missing, and I think the two segment_id examples are reversed? Should the "without id reassignment" example be with leaf_objs, not sorted_obs?

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Identify Segment Ends

Find segment tip and inner branch-point coordinates, and sort them by the y-coordinates of the branch points

**plantcv.morphology.segment_ends**(*skel_img, leaf_objects, mask=None, label=None*)

**returns** Re-ordered leaf segments

- **Parameters:**
- skel_img - Skeleton image (output from [plantcv.morphology.skeletonize](skeletonize.md))
- leaf_objects - Secondary segment objects (output from [plantcv.morphology.segment_sort](segment_sort.md)).
- mask - Binary mask for plotting. If provided, the debugging image will be overlaid on the mask (optional).
- label - Optional label parameter, modifies the variable name of observations recorded. (default = `pcv.params.sample_label`)
- **Context:**
- Aims to sort leaf objects by biological age. This tends to work somewhat consistently for grass species that have leav

**Reference Images**

![Screenshot](img/documentation_images/segment_ends/setaria_mask.png)

```python

from plantcv import plantcv as pcv

# Set global debug behavior to None (default), "print" (to file),
# or "plot" (Jupyter Notebooks or X11)
pcv.params.debug = "plot"

# Adjust point thickness with the global line_thickness parameter (default = 5)
pcv.params.line_thickness = 3

sorted_obs = pcv.morphology.segment_ends(skel_img=skeleton,
leaf_objects=leaf_objs,
mask=plant_mask,
label="leaves")

segmented_img, leaves_labeled = pcv.morphology.segment_id(skel_img=skeleton,
objects=leaf_objs,
mask=plant_mask
# Without ID re-assignment
segmented_img, leaves_labeled = pcv.morphology.segment_id(skel_img=skeleton,
objects=sorted_obs,
mask=plant_mask)

```

*Segment end points Debug*

![Screenshot](img/documentation_images/segment_ends/segment_end_pts.png)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/morphology/segment_ends.py)
5 changes: 5 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ pages for more details on the input and output variable types.
* post v3.11: labeled_img = **plantcv.morphology.segment_curvature**(*segmented_img, objects, label="default"*)
* post v4.0: labeled_img = **plantcv.morphology.segment_curvature**(*segmented_img, objects, label=None*)

#### plantcv.morphology.segment_ends

* pre v4.6: NA
* post v4.6: **plantcv.morphology.segment_ends**(*skel_img, leaf_objects, mask=None, label=None*)

#### plantcv.morphology.segment_euclidean_length

* pre v3.3: NA
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ nav:
- 'Segment Angles': segment_angle.md
- 'Combine Segments': segment_combine.md
- 'Segment Curvature': segment_curvature.md
- 'Segment Ends': segment_ends.md
- 'Segment Euclidean Length': segment_euclidean_length.md
- 'Segment ID': segment_id.md
- 'Segment Insertion Angle': segment_insertion_angle.md
Expand Down
165 changes: 165 additions & 0 deletions plantcv/plantcv/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,176 @@
import cv2
import numpy as np
from plantcv.plantcv.dilate import dilate
from plantcv.plantcv.logical_and import logical_and
from plantcv.plantcv.image_subtract import image_subtract
from plantcv.plantcv import fatal_error, warn
from plantcv.plantcv import params
import pandas as pd


def _find_segment_ends(skel_img, leaf_objects, plotting_img, size):
"""Find both segment ends and sort into tips or inner branchpoints.

Inputs:
skel_img = Skeletonized image
leaf_objects = List of leaf segments
plotting_img = Mask for debugging, might be a copy of the Skeletonized image
size = Size of inner segment ends (in pixels)

:param skel_img: numpy.ndarray
:param leaf_objects: list
:param plotting_img: numpy.ndarray
"""
labeled_img = cv2.cvtColor(plotting_img, cv2.COLOR_GRAY2RGB)
tips, _, _ = _find_tips(skel_img)
# Initialize list of tip data points
labels = []
tip_list = []
inner_list = []

# Find segment end coordinates
for i in range(len(leaf_objects)):
labels.append(i)
# Draw leaf objects
find_segment_tangents = np.zeros(labeled_img.shape[:2], np.uint8)
cv2.drawContours(find_segment_tangents, leaf_objects, i, 255, 1, lineType=8)
cv2.drawContours(labeled_img, leaf_objects, i, (150, 150, 150), params.line_thickness, lineType=8) # segments debug
# Prune back ends of leaves
pruned_segment = _iterative_prune(find_segment_tangents, size)
# Segment ends are the portions pruned off
ends = find_segment_tangents - pruned_segment
segment_end_obj, _ = _cv2_findcontours(bin_img=ends)
# Determine if a segment is segment tip or branch point
for j, obj in enumerate(segment_end_obj):
segment_plot = np.zeros(skel_img.shape[:2], np.uint8)
cv2.drawContours(segment_plot, obj, -1, 255, 1, lineType=8)
segment_plot = dilate(segment_plot, 3, 1)
overlap_img = logical_and(segment_plot, tips)
x, y = segment_end_obj[j].ravel()[:2]
coord = (int(x), int(y))
# If none of the tips are within a segment_end then it's an insertion segment
if np.sum(overlap_img) == 0:
inner_list.append(coord)
cv2.circle(labeled_img, coord, params.line_thickness, (50, 0, 255), -1) # Red auricles
else:
tip_list.append(coord)
cv2.circle(labeled_img, coord, params.line_thickness, (0, 255, 0), -1) # green tips

return labeled_img, tip_list, inner_list, labels


def _iterative_prune(skel_img, size):
"""Iteratively remove endpoints (tips) from a skeletonized image.
The pruning algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699
"Prunes" barbs off a skeleton.
Inputs:
skel_img = Skeletonized image
size = Size to get pruned off each branch
Returns:
pruned_img = Pruned image
:param skel_img: numpy.ndarray
:param size: int
:return pruned_img: numpy.ndarray
"""
pruned_img = skel_img.copy()
# Store debug
debug = params.debug
params.debug = None

# Iteratively remove endpoints (tips) from a skeleton
for _ in range(0, size):
endpoints, _, _ = _find_tips(pruned_img)
pruned_img = image_subtract(pruned_img, endpoints)

# Make debugging image
pruned_plot = np.zeros(skel_img.shape[:2], np.uint8)
pruned_plot = cv2.cvtColor(pruned_plot, cv2.COLOR_GRAY2RGB)
skel_obj, skel_hierarchy = _cv2_findcontours(bin_img=pruned_img)
pruned_obj, pruned_hierarchy = _cv2_findcontours(bin_img=pruned_img)

# Reset debug mode
params.debug = debug

cv2.drawContours(pruned_plot, skel_obj, -1, (0, 0, 255), params.line_thickness,
lineType=8, hierarchy=skel_hierarchy)
cv2.drawContours(pruned_plot, pruned_obj, -1, (255, 255, 255), params.line_thickness,
lineType=8, hierarchy=pruned_hierarchy)

return pruned_img


def _find_tips(skel_img, mask=None):
"""Helper function to find tips in skeletonized image.
The endpoints algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699

Inputs:
skel_img = Skeletonized image
mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.
label = Optional label parameter, modifies the variable name of
observations recorded (default = pcv.params.sample_label).
Returns:
tip_img = Image with just tips, rest 0

:param skel_img: numpy.ndarray
:param mask: numpy.ndarray
:param label: str
:return tip_img: numpy.ndarray
"""
# In a kernel: 1 values line up with 255s, -1s line up with 0s, and 0s correspond to dont care
endpoint1 = np.array([[-1, -1, -1],
[-1, 1, -1],
[0, 1, 0]])
endpoint2 = np.array([[-1, -1, -1],
[-1, 1, 0],
[-1, 0, 1]])

endpoint3 = np.rot90(endpoint1)
endpoint4 = np.rot90(endpoint2)
endpoint5 = np.rot90(endpoint3)
endpoint6 = np.rot90(endpoint4)
endpoint7 = np.rot90(endpoint5)
endpoint8 = np.rot90(endpoint6)

endpoints = [endpoint1, endpoint2, endpoint3, endpoint4, endpoint5, endpoint6, endpoint7, endpoint8]
tip_img = np.zeros(skel_img.shape[:2], dtype=int)
for endpoint in endpoints:
tip_img = np.logical_or(cv2.morphologyEx(skel_img, op=cv2.MORPH_HITMISS, kernel=endpoint,
borderType=cv2.BORDER_CONSTANT, borderValue=0), tip_img)
tip_img = tip_img.astype(np.uint8) * 255
# Store debug
debug = params.debug
params.debug = None
tip_objects, _ = _cv2_findcontours(bin_img=tip_img)

if mask is None:
# Make debugging image
dilated_skel = dilate(skel_img, params.line_thickness, 1)
tip_plot = cv2.cvtColor(dilated_skel, cv2.COLOR_GRAY2RGB)

else:
# Make debugging image on mask
mask_copy = mask.copy()
tip_plot = cv2.cvtColor(mask_copy, cv2.COLOR_GRAY2RGB)
skel_obj, skel_hier = _cv2_findcontours(bin_img=skel_img)
cv2.drawContours(tip_plot, skel_obj, -1, (150, 150, 150), params.line_thickness,
lineType=8, hierarchy=skel_hier)

# Initialize list of tip data points
tip_list = []
tip_labels = []
for i, tip in enumerate(tip_objects):
x, y = tip.ravel()[:2]
coord = (int(x), int(y))
tip_list.append(coord)
tip_labels.append(i)
cv2.circle(tip_plot, (x, y), params.line_thickness, (0, 255, 0), -1)

# Reset debug mode
params.debug = debug

return tip_img, tip_list, tip_labels


def _hough_circle(gray_img, mindist, candec, accthresh, minradius, maxradius, maxfound=None):
"""
Hough Circle Detection
Expand Down
9 changes: 5 additions & 4 deletions plantcv/plantcv/morphology/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from plantcv.plantcv.morphology.find_branch_pts import find_branch_pts
from plantcv.plantcv.morphology.find_tips import find_tips
from plantcv.plantcv.morphology._iterative_prune import _iterative_prune
from plantcv.plantcv.morphology.segment_skeleton import segment_skeleton
from plantcv.plantcv.morphology.segment_sort import segment_sort
from plantcv.plantcv.morphology.prune import prune
Expand All @@ -16,8 +15,10 @@
from plantcv.plantcv.morphology.segment_combine import segment_combine
from plantcv.plantcv.morphology.analyze_stem import analyze_stem
from plantcv.plantcv.morphology.fill_segments import fill_segments
from plantcv.plantcv.morphology.segment_ends import segment_ends

__all__ = ["find_branch_pts", "find_tips", "prune", "skeletonize", "check_cycles", "segment_skeleton", "segment_angle",
__all__ = ["find_branch_pts", "find_tips", "prune", "skeletonize", "check_cycles",
"segment_skeleton", "segment_angle",
"segment_path_length", "segment_euclidean_length", "segment_curvature", "segment_sort", "segment_id",
"segment_tangent_angle", "segment_insertion_angle", "segment_combine", "_iterative_prune", "analyze_stem",
"fill_segments"]
"segment_tangent_angle", "segment_insertion_angle", "segment_combine", "analyze_stem",
"fill_segments", "segment_ends"]
46 changes: 0 additions & 46 deletions plantcv/plantcv/morphology/_iterative_prune.py

This file was deleted.

Loading