| | 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 |
| |
|