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 "prune by height" function to morphology subpackage #1647

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4cd2b8e
code dump, adding prune_by_height
HaleySchuhl Nov 5, 2024
c8d0b9b
update line boundary logic & add prune_by_height_partial
HaleySchuhl Nov 14, 2024
ea4212e
prune by height partial logic update
HaleySchuhl Nov 15, 2024
dddd76e
Update prune.py
HaleySchuhl Nov 20, 2024
ad14cc5
change line position default and add logic to auto find it
HaleySchuhl Nov 20, 2024
2616ace
update prune_by_height_partial imports
HaleySchuhl Nov 21, 2024
bc92207
remove duplication of skeleton in pruning functions
HaleySchuhl Nov 21, 2024
9a523de
morphology init add prune by height partial
HaleySchuhl Nov 21, 2024
9e5e7b8
add label param to prune_by_height
HaleySchuhl Nov 25, 2024
9ac44c2
add observations for segment number
HaleySchuhl Nov 25, 2024
45bb939
index out the y-position for auto line_boundary setting
HaleySchuhl Nov 25, 2024
64bcdd5
remove extra print statement
HaleySchuhl Nov 25, 2024
b034f69
simplify segment_id debug
HaleySchuhl Nov 26, 2024
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
a7f93c7
Merge branch 'update_morphology_tips_outputs' into add_prune_by_height
HaleySchuhl Jan 9, 2025
3cfd8b4
add variable definition outside of if statement
HaleySchuhl Jan 9, 2025
9213ab1
remove optimal assignment from testing
HaleySchuhl Jan 9, 2025
09ee6b8
prune by height init
HaleySchuhl Jan 9, 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
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, segmented and labeled 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 line 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_obj,
mask=plant_mask,
label="leaves")

segmented_img, leaves_labeled = pcv.morphology.segment_id(skel_img=skeleton,
objects=leaf_obj,
mask=plant_mask
# Without optimal assignment leaf tips are used by default
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
11 changes: 6 additions & 5 deletions plantcv/plantcv/morphology/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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
from plantcv.plantcv.morphology.prune import prune, prune_by_height
from plantcv.plantcv.morphology.skeletonize import skeletonize
from plantcv.plantcv.morphology.check_cycles import check_cycles
from plantcv.plantcv.morphology.segment_angle import segment_angle
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", "prune_by_height",
"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
Loading