Source code for openalea.phenomenal.tracking.leaf_extension
"""
Use binary images to extend the length of each phenomenal phm_leaf.
Method : A 2d skeleton is computed for the binary image at a given angle. Then,
the algorithm searches correspondences between phenomenal polylines
(reprojected in 2D) and skeleton polylines. For each match, an
extension factor e >= 1 is computed this way :
e = (skeleton 2D polyline length) / (phenomenal 2D polyline length).
This is done for each side angle. Then, results are merged : for each
phenomenal phm_leaf, the final extension factor is equal to the median of
all extension values found for this phm_leaf, or 1 if no extension value
was found.
"""
import warnings
import numpy as np
import skimage
from skimage.morphology import skeletonize
from scipy.spatial.distance import directed_hausdorff
from skan.csr import skeleton_to_csgraph, Skeleton, summarize
from openalea.phenomenal.tracking.polyline_utils import polyline_length
[docs]
def skeleton_branches(image, n_kernel=15, min_length=30):
"""
Args:
image: 2D array
binary image
n_kernel: int
parameter for image dilatation
min_length: float
minimum length of skeleton branches (px)
Returns: list
list of 2D polylines with an endpoint
"""
binary = image.copy()
if round(np.max(binary)) == 255:
binary = binary / 255.0
# dilate image
kernel = np.ones((n_kernel, n_kernel))
binary_dilated = skimage.morphology.dilation(binary, kernel)
# 2d skeleton image
skeleton = skeletonize(binary_dilated)
# skeleton analysis : get branches
skan_skeleton = Skeleton(skeleton)
branches = summarize(skan_skeleton, separator="-")
# select branches having an endpoint, and a sufficient length
branches_endpoint = branches[branches["branch-type"] == 1]
branches_endpoint = branches_endpoint[
branches_endpoint["branch-distance"] > min_length
]
# converting branches to polylines
_, coordinates = skeleton_to_csgraph(skeleton)
node_ids = list(branches["node-id-src"]) + list(branches["node-id-dst"])
polylines = []
for irow, row in branches_endpoint.iterrows():
polyline = np.array(
[[coordinates[0][i], coordinates[1][i]] for i in skan_skeleton.path(irow)]
)
polyline = polyline[:, ::-1] # same (x, y) order as phenomenal
# verify that all phm_leaf polylines are oriented the same way (phm_leaf insertion --> phm_leaf tip)
i = row["node-id-dst"]
if node_ids.count(i) > 1:
polyline = polyline[::-1]
polylines.append(polyline)
return polylines
[docs]
def compute_extension(
polylines_phm, polylines_sk, seg_length=50.0, dist_threshold=30.0
):
"""
Args:
polylines_phm: list
list of 2D projections of 3D leaf polylines.
polylines_sk: list
list of 2D skeleton polylines.
seg_length: float
length (px) of the end segment of a phenomenal phm_leaf polyline that is compared with skeleton.
dist_threshold: float
hausdorff distance threshold (px) between polylines of both types to associate them.
Returns:
"""
res = dict.fromkeys(range(len(polylines_phm)), [])
for pl_sk in polylines_sk:
b_selected = 0
dist_min = float("inf")
selected_rank = -1
for rank, pl_phm in enumerate(polylines_phm):
# end segment of phenomenal polyline
dists_to_end = np.linalg.norm(pl_phm - pl_phm[-1], axis=1)
start = np.argmin(abs(dists_to_end - seg_length))
pl_phm_segment = pl_phm[start:]
# corresponding sk segment
d_a = np.linalg.norm(pl_sk - pl_phm_segment[0], axis=1)
a = np.argmin(d_a)
d_b = np.linalg.norm(pl_sk - pl_phm_segment[-1], axis=1)
b = np.argmin(d_b)
pl_sk_segment = pl_sk[a : (b + 1)]
# distance between phm and sk segments
with warnings.catch_warnings():
warnings.simplefilter("ignore")
d1 = directed_hausdorff(pl_sk_segment, pl_phm_segment)[0]
d2 = directed_hausdorff(pl_phm_segment, pl_sk_segment)[0]
dist = max(d1, d2)
if dist < dist_threshold and b > b_selected:
b_selected = b
dist_min = dist
selected_rank = rank
if b_selected != 0:
l1 = polyline_length(polylines_phm[selected_rank])
l2 = polyline_length(pl_sk[b_selected:])
extension_factor = (l2 + l1) / l1
res[selected_rank] = res[selected_rank] + [
(round(dist_min, 4), extension_factor, pl_sk[b_selected:])
]
# verify that a phenomenal polyline has no more than 1 corresponding skeleton polyline.
# And keep extension polylines in a list for optional display
extension_polylines = []
for k, resi in res.items():
# 1 skeleton branch candidate
if len(resi) == 1:
_, extension_factor, extension_pl = resi[0]
res[k] = extension_factor
extension_polylines.append(extension_pl)
# several skeleton branch candidates
elif len(resi) > 1:
d_min = float("inf")
selected_pl = None
for d, extension_factor, extension_pl in resi:
if d < d_min:
d_min = d
res[k] = extension_factor
selected_pl = extension_pl
extension_polylines.append(selected_pl)
# no candidate
else:
res[k] = None
return res, extension_polylines
[docs]
def leaf_extension(phm_seg, binaries, projections):
"""
Args:
phm_seg: openalea.phenomenal.object.voxelSegmentation.VoxelSegmentation
3D segmentation of a maize plant
binaries: dict
{side angle : binary image}. each image pixel equals 0 (background) or 255 (plant).
projections: dict
{side angle: 3D->2D projection function}
Returns: openalea.phenomenal.object.voxelSegmentation.VoxelSegmentation
phm_seg object with a new 'pm_length_extended' key in the .info attribute of each leaf.
"""
# _____________________________________________________________________________________________________________
# compute extension for each phenomenal phm_leaf and each camera angle. Regroup results in a dictionary.
polylines_phm = [
phm_seg.get_leaf_order(k).real_longest_polyline()
for k in range(1, 1 + phm_seg.get_number_of_leaf())
]
angles = binaries.keys()
res = {}
for angle in angles:
# 2D skeleton polylines
polylines_sk = skeleton_branches(binaries[angle])
# phenomenal polylines projected in 2D
polylines_phm_2d = [projections[angle](pl) for pl in polylines_phm]
# compute phm_leaf extension factor for each phenomenal phm_leaf (if a result is found)
extension_factors, _ = compute_extension(polylines_phm_2d, polylines_sk)
res[angle] = extension_factors
# _____________________________________________________________________________________________________________
# merge results to have a single extension factor (median value) for each phenomenal phm_leaf.
# (if no skeleton segment was found for a given phenomenal phm_leaf, extension factor have a default value of 1.)
for k in range(1, 1 + phm_seg.get_number_of_leaf()):
leaf_ext = [res[a][k - 1] for a in angles if res[a][k - 1] is not None]
if phm_seg.get_leaf_order(k).info["pm_label"] == "growing_leaf":
leaf_length = phm_seg.get_leaf_order(k).info["pm_length_with_speudo_stem"]
else:
leaf_length = phm_seg.get_leaf_order(k).info["pm_length"]
if not leaf_ext:
extension_factor = 1.0
else:
extension_factor = np.median(leaf_ext)
phm_seg.get_leaf_order(k).info["pm_length_extended"] = (
leaf_length * extension_factor
)
return phm_seg