|
import numpy as np |
|
from sklearn.cluster import DBSCAN |
|
from scipy.spatial import ConvexHull |
|
from scipy.optimize import least_squares |
|
|
|
CLASSES = [ |
|
'Betula pendula', |
|
'Fagus sylvatica', |
|
'Picea abies', |
|
'Pinus sylvestris' |
|
] |
|
|
|
def fit_circle(x, y): |
|
"""Fit a circle to given x, y points.""" |
|
def calc_radius(params): |
|
cx, cy, r = params |
|
return np.sqrt((x - cx)**2 + (y - cy)**2) - r |
|
|
|
|
|
x_m, y_m = np.mean(x), np.mean(y) |
|
r_initial = np.mean(np.sqrt((x - x_m)**2 + (y - y_m)**2)) |
|
initial_params = [x_m, y_m, r_initial] |
|
|
|
|
|
result = least_squares(calc_radius, initial_params) |
|
cx, cy, r = result.x |
|
return cx, cy, r |
|
|
|
def remove_noise(points, eps=0.05, min_samples=10): |
|
""" |
|
Remove noise from points using DBSCAN clustering. |
|
|
|
Args: |
|
points (numpy.ndarray): Array of shape (N, 3) with columns [x, y, z]. |
|
eps (float): Maximum distance between two samples to consider them as in the same neighborhood. |
|
min_samples (int): Minimum number of points to form a dense region. |
|
|
|
Returns: |
|
numpy.ndarray: Denoised points. |
|
""" |
|
clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points) |
|
labels = clustering.labels_ |
|
try: |
|
largest_cluster = labels == np.argmax(np.bincount(labels[labels >= 0])) |
|
except Exception: |
|
raise RuntimeError("Error in DBH calculation") |
|
return points[largest_cluster] |
|
|
|
def calculate_dbh(points, dbh_height=1.3, height_buffer=0.1, eps=0.05, min_samples=10): |
|
""" |
|
Calculate the Diameter at Breast Height (DBH) of a tree from point cloud data. |
|
|
|
Args: |
|
points (numpy.ndarray): Array of shape (N, 3) with columns [x, y, z]. |
|
dbh_height (float): Height at which DBH is measured (default is 1.3 meters). |
|
height_buffer (float): Range around dbh_height to include points (default is ±0.1 meters). |
|
|
|
Returns: |
|
float: DBH in meters. |
|
""" |
|
z_min, z_max = dbh_height - height_buffer, dbh_height + height_buffer |
|
trunk_points = points[(points[:, 2] >= (z_min)) & (points[:, 2] <= (z_max))] |
|
|
|
if trunk_points.shape[0] < 3: |
|
raise ValueError("Not enough points to calculate DBH.") |
|
|
|
|
|
denoised_points = remove_noise(trunk_points[:, :2], eps=eps, min_samples=min_samples) |
|
denoised_points = np.hstack((denoised_points, np.full((denoised_points.shape[0], 1), dbh_height))) |
|
|
|
if denoised_points.shape[0] < 3: |
|
raise ValueError("Not enough points left after noise removal.") |
|
|
|
|
|
x, y = denoised_points[:, 0], denoised_points[:, 1] |
|
cx, cy, radius = fit_circle(x, y) |
|
|
|
|
|
theta = np.linspace(0, 2 * np.pi, 100) |
|
circle_x = cx + radius * np.cos(theta) |
|
circle_y = cy + radius * np.sin(theta) |
|
circle_points = np.column_stack((circle_x, circle_y, np.full_like(circle_x, dbh_height))) |
|
|
|
|
|
dbh = 2 * radius |
|
return dbh, circle_points |
|
|
|
def calc_canopy_volume(points, threshold, height, z_min): |
|
''' |
|
Calculates the canopy points for a given point cloud data of a tree |
|
and calculates the volume using the Qhull algorithm |
|
|
|
Args: |
|
points: point cloud data |
|
threshold: z_threshold in percentage |
|
height, z_min |
|
|
|
Returns: |
|
canopy_volume, canopy_points |
|
''' |
|
z_threshold = z_min + (threshold / 100) * height |
|
canopy_points = points[points[:, 2] >= z_threshold] |
|
clustering = DBSCAN(eps=1.0, min_samples=10).fit(canopy_points[:, :3]) |
|
labels = clustering.labels_ |
|
canopy_points = canopy_points[labels != -1] |
|
|
|
if canopy_points.shape[0] < 4: |
|
canopy_volume = None |
|
else: |
|
''' |
|
Uses the QuickHull algorithm which uses a divide-and-conquer approach. |
|
It selects the 2 leftmost and rightmost points on a 2D plane; |
|
These are part of the ConvexHull. |
|
Then it selects the point farthest away from the line joining the |
|
above 2 points and adds it to the ConvexHull. |
|
The points enclosed within that shape cannot be part of the |
|
ConvexHull and are ignored. This process is then repeated until |
|
all points are either part of the ConvexHull or contained inside it. |
|
''' |
|
hull = ConvexHull(canopy_points) |
|
canopy_volume = hull.volume |
|
|
|
return canopy_volume, canopy_points |
|
|