| import gc | |
| import laspy | |
| import torch | |
| import base64 | |
| import tempfile | |
| import numpy as np | |
| import open3d as o3d | |
| import streamlit as st | |
| import plotly.graph_objs as go | |
| import pointnet2_cls_msg as pn2 | |
| from utils import calculate_dbh, calc_canopy_volume, CLASSES | |
| from SingleTreePointCloudLoader import SingleTreePointCloudLoader | |
| gc.enable() | |
| device = 'cuda' if torch.cuda.is_available() else 'cpu' | |
| with st.spinner("Loading PointNet++ model..."): | |
| checkpoint = torch.load('checkpoints/best_model.pth', map_location=torch.device(device)) | |
| classifier = pn2.get_model(num_class=4, normal_channel=False) | |
| classifier.load_state_dict(checkpoint['model_state_dict']) | |
| classifier.eval() | |
| side_bg = "static/sidebar.png" | |
| side_bg_ext = "png" | |
| st.markdown( | |
| f""" | |
| <style> | |
| [data-testid="stSidebar"] {{ | |
| background: url(data:image/{side_bg_ext};base64,{base64.b64encode(open(side_bg, "rb").read()).decode()}); | |
| color: #ffff00; | |
| }} | |
| [data-testid="stSidebarUserContent"] {{ | |
| padding-bottom: 3rem; | |
| }} | |
| .stMainBlockContainer {{ | |
| padding-top: 3rem; | |
| }} | |
| .main > div {{ | |
| padding-top: 3rem; | |
| }} | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| st.sidebar.markdown( | |
| body= | |
| "<div style='text-align: justify; color: #ffff00'>" | |
| "<h1 style='color: #ffff00; font-size: 4rem;'>About</h1>" | |
| "The species <strong>Pinus sylvestris (Scots Pine), Fagus sylvatica " | |
| "(European Beech), Picea abies (Norway Spruce), and Betula pendula " | |
| "(Silver Birch)</strong> are native to Europe and parts " | |
| "of Asia but are also found in India (Parts of Himachal Pradesh, " | |
| "Uttarakhand, Jammu and Kashmir, Sikkim and Arunachal Pradesh). " | |
| "These temperate species, typically thriving in boreal and montane ecosystems, " | |
| "are occasionally introduced in cooler Indian regions like the Himalayan " | |
| "foothills for afforestation or experimental forestry, where climatic " | |
| "conditions are favourable. However, their growth and ecological interactions " | |
| "in India may vary significantly due to the region's unique biodiversity " | |
| "and environmental factors.<br><br>" | |
| "This AI-powered application employs the PointNet++ deep learning " | |
| "architecture, optimized for processing 3D point cloud data from " | |
| "individual <code>.las</code> <code>.laz</code> <code>.pcd</code> files " | |
| "(fused aerial and terrestrial LiDAR) to classify tree species up to four classes " | |
| "(<strong>Pinus sylvestris, Fagus sylvatica, Picea abies, and Betula pendula</strong>) " | |
| "with associated confidence scores. Additionally, it calculates critical " | |
| "metrics such as Diameter at Breast Height (DBH), actual height and " | |
| "customizable canopy volume, enabling precise refinement of predictions " | |
| "and analyses. By integrating species-specific and volumetric insights, " | |
| "the tool enhances ecological research workflows, facilitating data-driven " | |
| "decision-making.<br><br>" | |
| "<div style='text-align: right; font-size: 10px;'>©Copyright: WII, " | |
| "Technology Laboratory<br>Authors: Shashank Sawan & Paras Shah</div></div>" | |
| , | |
| unsafe_allow_html=True, | |
| ) | |
| st.image("static/header.png", use_container_width=True) | |
| uploaded_file = st.file_uploader( | |
| label="Upload Point Cloud Data", | |
| type=['laz', 'las', 'pcd'], | |
| help="Please upload trees with ground points removed" | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.image("static/canopy.png", use_container_width=True) | |
| with col2: | |
| CANOPY_VOLUME = st.slider( | |
| label="Canopy Volume in % (Z)", | |
| min_value=10, | |
| max_value=90, | |
| value=70, | |
| step=1, | |
| help= | |
| "Adjust the Z-threshold value to calculate the canopy volume " | |
| "within specified limits, it uses Quickhull and DBSCAN algorithms. " | |
| ) | |
| st.markdown( | |
| body= | |
| "<div style='text-align: justify; font-size: 13px;'>" | |
| "The <b>Quickhull algorithm</b> computes the convex hull of a set of points " | |
| "by identifying extreme points to form an initial boundary and recursively " | |
| "refining it by adding the farthest points until all points lie within the " | |
| "convex boundary. It uses a divide-and-conquer approach, similar to QuickSort. " | |
| "<br>" | |
| "<b>DBSCAN (Density-Based Spatial Clustering of Applications with Noise)</b> is " | |
| "a density-based clustering algorithm that groups densely packed points within " | |
| "a specified distance 'eps' and minimum points 'minpoints', while treating " | |
| "sparse points as noise. It effectively identifies arbitrarily shaped clusters " | |
| "and handles outliers, making it suitable for spatial data and anomaly detection." | |
| "</div><br>", | |
| unsafe_allow_html=True | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.image("static/dbh.png", use_container_width=True) | |
| with col2: | |
| DBH_HEIGHT = st.slider( | |
| label="DBH (Diameter above Breast Height, in metres) (H)", | |
| min_value=1.3, | |
| max_value=1.4, | |
| value=1.4, | |
| step=0.01, | |
| help= | |
| "Adjust to calculate the DBH value within specified limits, " | |
| "it utilizes Least square circle fitting method Levenberg-Marquardt " | |
| "optimization technique." | |
| ) | |
| st.markdown( | |
| body= | |
| "<div style='text-align: justify; font-size:13px;'>" | |
| "The <b>Least Squares Circle Fitting method</b> is used to find the " | |
| "best-fitting circle to a set of 2D points by minimizing the sum of " | |
| "squared distances between each point and the circle's circumference. " | |
| "<b>Levenberg-Marquardt Optimization</b> is used to fit models " | |
| "(like circles) to point cloud data by minimizing the error between " | |
| "the model and the actual points.</div><br>", | |
| unsafe_allow_html=True | |
| ) | |
| proceed = None | |
| if uploaded_file: | |
| try: | |
| with st.spinner("Reading point cloud file..."): | |
| file_type = uploaded_file.name.split('.')[-1].lower() | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp: | |
| tmp.write(uploaded_file.read()) | |
| temp_file_path = tmp.name | |
| if file_type == 'pcd': | |
| pcd = o3d.io.read_point_cloud(temp_file_path) | |
| points = np.asarray(pcd.points) | |
| else: | |
| point_cloud = laspy.read(temp_file_path) | |
| points = np.vstack((point_cloud.x, point_cloud.y, point_cloud.z)).transpose() | |
| proceed = st.button("Run model") | |
| except Exception as e: | |
| st.error(f"An error occured: {str(e)}") | |
| if proceed: | |
| try: | |
| with st.spinner("Calculating tree inventory..."): | |
| dbh, trunk_points = calculate_dbh(points, DBH_HEIGHT) | |
| z_min = np.min(points[:, 2]) | |
| z_max = np.max(points[:, 2]) | |
| height = z_max - z_min | |
| canopy_volume, canopy_points = calc_canopy_volume(points, CANOPY_VOLUME, height, z_min) | |
| with st.spinner("Visualizing point cloud..."): | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter3d( | |
| x=points[:, 0], | |
| y=points[:, 1], | |
| z=points[:, 2], | |
| mode='markers', | |
| marker=dict( | |
| size=0.5, | |
| color=points[:, 2], | |
| colorscale='Viridis', | |
| opacity=1.0, | |
| ), | |
| name='Tree' | |
| )) | |
| fig.add_trace(go.Scatter3d( | |
| x=canopy_points[:, 0], | |
| y=canopy_points[:, 1], | |
| z=canopy_points[:, 2], | |
| mode='markers', | |
| marker=dict( | |
| size=2, | |
| color='blue', | |
| opacity=0.8, | |
| ), | |
| name='Canopy points' | |
| )) | |
| fig.add_trace(go.Scatter3d( | |
| x=trunk_points[:, 0], | |
| y=trunk_points[:, 1], | |
| z=trunk_points[:, 2], | |
| mode='markers', | |
| marker=dict( | |
| size=2, | |
| color='red', | |
| opacity=0.9, | |
| ), | |
| name='DBH' | |
| )) | |
| fig.update_layout( | |
| margin=dict(l=0, r=0, b=0, t=0), | |
| scene=dict( | |
| xaxis_title="X", | |
| yaxis_title="Y", | |
| zaxis_title="Z", | |
| aspectmode='data' | |
| ), | |
| showlegend=False | |
| ) | |
| col1, col2, col3 = st.columns([1, 3, 1]) | |
| with col2: | |
| st.markdown(""" | |
| <style> | |
| .centered-plot { | |
| text-align: center; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.plotly_chart(fig, use_container_width=True) | |
| hide_st_style = """ | |
| <style> | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| </style> | |
| """ | |
| st.markdown(hide_st_style, unsafe_allow_html=True) | |
| with st.spinner("Running inference..."): | |
| testFile = SingleTreePointCloudLoader(temp_file_path, file_type) | |
| testFileLoader = torch.utils.data.DataLoader(testFile, batch_size=8, shuffle=False, num_workers=0) | |
| point_set, _ = next(iter(testFileLoader)) | |
| point_set = point_set.transpose(2, 1) | |
| with torch.no_grad(): | |
| logits, _ = classifier(point_set) | |
| probabilities = torch.softmax(logits, dim=-1) | |
| predicted_class = torch.argmax(probabilities, dim=-1).item() | |
| confidence_score = (probabilities.numpy().tolist())[0][predicted_class] * 100 | |
| predicted_label = CLASSES[predicted_class] | |
| st.write(f"**Predicted class: {predicted_label}**") | |
| st.write(f"**Confidence score: {confidence_score:.2f}%**") | |
| st.write(f"**Height of tree: {height:.2f}m**") | |
| st.write(f"**Canopy volume: {canopy_volume:.2f}m\u00b3**") | |
| st.write(f"**DBH: {dbh:.2f}m**") | |
| except Exception as e: | |
| st.error(f"An error occured: {str(e)}") | |