├── .gitignore ├── LICENSE.md ├── Manual ├── TreeQSM_documentation.pdf └── fig_point_cloud_qsm.png ├── README.md └── src ├── create_input.m ├── estimate_precision.m ├── least_squares_fitting ├── form_rotation_matrices.m ├── func_grad_axis.m ├── func_grad_circle.m ├── func_grad_circle_centre.m ├── func_grad_cylinder.m ├── least_squares_axis.m ├── least_squares_circle.m ├── least_squares_circle_centre.m ├── least_squares_cylinder.m ├── nlssolver.m └── rotate_to_z_axis.m ├── main_steps ├── branches.m ├── correct_segments.m ├── cover_sets.m ├── cylinders.m ├── filtering.m ├── point_model_distance.m ├── relative_size.m ├── segments.m ├── tree_data.m └── tree_sets.m ├── make_models.m ├── make_models_parallel.m ├── plotting ├── plot2d.m ├── plot_branch_segmentation.m ├── plot_branches.m ├── plot_comparison.m ├── plot_cone_model.m ├── plot_cylinder_model.m ├── plot_cylinder_model2.m ├── plot_distribution.m ├── plot_large_point_cloud.m ├── plot_models_segmentations.m ├── plot_point_cloud.m ├── plot_scatter.m ├── plot_segments.m ├── plot_segs.m ├── plot_spreads.m ├── plot_tree_structure.m ├── plot_tree_structure2.m ├── plot_triangulation.m └── point_cloud_plotting.m ├── results └── qsm.mat ├── select_optimum.m ├── tools ├── average.m ├── change_precision.m ├── connected_components.m ├── cross_product.m ├── cubical_averaging.m ├── cubical_downsampling.m ├── cubical_partition.m ├── define_input.m ├── dimensions.m ├── display_time.m ├── distances_between_lines.m ├── distances_to_line.m ├── dot_product.m ├── expand.m ├── growth_volume_correction.m ├── intersect_elements.m ├── mat_vec_subtraction.m ├── median2.m ├── normalize.m ├── optimal_parallel_vector.m ├── orthonormal_vectors.m ├── rotation_matrix.m ├── save_model_text.m ├── sec2min.m ├── select_cylinders.m ├── set_difference.m ├── simplify_qsm.m ├── surface_coverage.m ├── surface_coverage2.m ├── surface_coverage_filtering.m ├── unique2.m ├── unique_elements.m ├── update_tree_data.m └── verticalcat.m ├── treeqsm.m └── triangulation ├── boundary_curve.m ├── boundary_curve2.m ├── check_self_intersection.m ├── curve_based_triangulation.m └── initial_boundary_curve.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Manual/TreeQSM_documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InverseTampere/TreeQSM/6630bbf516f8b53adb7d60a2cccbd21e6fe51226/Manual/TreeQSM_documentation.pdf -------------------------------------------------------------------------------- /Manual/fig_point_cloud_qsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InverseTampere/TreeQSM/6630bbf516f8b53adb7d60a2cccbd21e6fe51226/Manual/fig_point_cloud_qsm.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TreeQSM 2 | 3 | **Version 2.4.1** 4 | **Reconstruction of quantitative structure models for trees from point cloud data** 5 | 6 | [![DOI](https://zenodo.org/badge/100592530.svg)](https://zenodo.org/badge/latestdoi/100592530) 7 | 8 | ![QSM image](https://github.com/InverseTampere/TreeQSM/blob/master/Manual/fig_point_cloud_qsm.png) 9 | 10 | 11 | ### Description 12 | 13 | TreeQSM is a modelling method that reconstructs quantitative structure models (QSMs) for trees from point clouds. A QSM consists of a hierarchical collection of cylinders estimating topological, geometrical and volumetric details of the woody structure of the tree. The input point cloud, which is usually produced by a terrestrial laser scanner, must contain only one tree, which is intended to be modelled, but the point cloud may contain also some points from the ground and understory. Moreover, the point cloud should not contain significant amount of noise or points from leaves as these are interpreted as points from woody parts of the tree and can therefore lead to erroneous results. Much more details of the method and QSMs can be found from the manual that is part of the code distribution. 14 | 15 | The TreeQSM is written in Matlab. 16 | The main function is _treeqsm.m_, which takes in a point cloud and a structure array specifying the needed parameters. Refer to the manual or the help documentation of a particular function for further details. 17 | 18 | ### References 19 | 20 | Web: https://research.tuni.fi/inverse/ 21 | Some published papers about the method and applications: 22 | Raumonen et al. 2013, Remote Sensing https://www.mdpi.com/2072-4292/5/2/491 23 | Calders et al. 2015, Methods in Ecology and Evolution https://besjournals.onlinelibrary.wiley.com/doi/full/10.1111/2041-210X.12301 24 | Raumonen et al. 2015, ISPRS Annals https://www.isprs-ann-photogramm-remote-sens-spatial-inf-sci.net/II-3-W4/189/2015/ 25 | Åkerblom et al. 2015, Remote Sensing https://www.mdpi.com/2072-4292/7/4/4581 26 | Åkerblom et al. 2017, Remote Sensing of Environment https://www.sciencedirect.com/science/article/abs/pii/S0034425716304746 27 | de Tanago Menaca et al. 2017, Methods in Ecology and Evolution https://besjournals.onlinelibrary.wiley.com/doi/10.1111/2041-210X.12904 28 | Åkerblom et al. 2018, Interface Focus http://dx.doi.org/10.1098/rsfs.2017.0045 29 | Disney et al. 2018, Interface Focus http://dx.doi.org/10.1098/rsfs.2017.0048 30 | 31 | 32 | ### Quick guide 33 | 34 | Here is a quick guide for testing the code and starting its use. However, it is highly recommended that after the testing the user reads the manual for more information how to best use the code. 35 | 36 | 1) Start MATLAB and set the main path to the root folder, where _treeqsm.m_ is located.\ 37 | 2) Use _Set Path_ --> _Add with Subfolders_ --> _Open_ --> _Save_ --> _Close_ to add the subfolders, where all the codes of the software are, to the paths of MATLAB.\ 38 | 3) Import a point cloud of a tree into the workspace. Let us name it P.\ 39 | 4) Define suitable inputs:\ 40 |     >> inputs = define_input(P,1,1,1);\ 41 | 5) Reconstruct QSMs:\ 42 |     >> QSM = treeqsm(P,inputs); 43 | -------------------------------------------------------------------------------- /src/create_input.m: -------------------------------------------------------------------------------- 1 | 2 | % Creates input parameter structure array needed to run "treeqsm" function 3 | % and "filtering" function. 4 | % NOTE: use this code to define all the parameters but the PatchDiam and 5 | % BallRad parameters can be conveniently defined by "define_input" 6 | % function. 7 | % 8 | % Last update 11 May 2022 9 | 10 | clear inputs 11 | 12 | %% QSM reconstruction parameters 13 | %%% THE THREE INPUT PARAMETERS TO BE OPTIMIZED. 14 | % These CAN BE VARIED AND SHOULD BE OPTIMIZED 15 | % One possibility to define these is to use "define_input" code 16 | % (These can have multiple values given as vectors, e.g. [0.01 0.02]). 17 | % Patch size of the first uniform-size cover: 18 | inputs.PatchDiam1 = [0.08 0.12]; 19 | % Minimum patch size of the cover sets in the second cover: 20 | inputs.PatchDiam2Min = [0.02 0.03]; 21 | % Maximum cover set size in the stem's base in the second cover: 22 | inputs.PatchDiam2Max = [0.07 0.1]; 23 | 24 | %%% ADDITIONAL PATCH GENERATION PARAMETERS. 25 | % The following parameters CAN BE VARIED BUT CAN BE USUALLY KEPT AS SHOWN 26 | % (i.e. little bigger than PatchDiam parameters). 27 | % One possibility to define these is to use "define_input" code 28 | % Ball radius in the first uniform-size cover generation: 29 | inputs.BallRad1 = inputs.PatchDiam1+0.015; 30 | % Maximum ball radius in the second cover generation: 31 | inputs.BallRad2 = inputs.PatchDiam2Max+0.01; 32 | 33 | % The following parameters CAN BE USUALLY KEPT FIXED as shown. 34 | % Minimum number of points in BallRad1-balls, generally good value is 3: 35 | inputs.nmin1 = 3; 36 | % Minimum number of points in BallRad2-balls, generally good value is 1: 37 | inputs.nmin2 = 1; 38 | % Does the point cloud contain points only from the tree (if 1, then yes): 39 | inputs.OnlyTree = 1; 40 | % Produce a triangulation of the stem's bottom part up to the first main 41 | % branch (if 1, then yes): 42 | inputs.Tria = 0; 43 | % Compute the point-model distances (if 1, then yes): 44 | inputs.Dist = 1; 45 | 46 | %%% RADIUS CORRECTION OPTIONS FOR MODIFYING TOO LARGE AND TOO SMALL CYLINDERS. 47 | % These parameters CAN BE USUALLY KEPT FIXED as shown. 48 | % Traditional TreeQSM choices: 49 | % Minimum cylinder radius, used particularly in the taper corrections: 50 | inputs.MinCylRad = 0.0025; 51 | % Radius correction based on radius of the parent. If 1, radii in a branch 52 | % are always smaller than the radius of the parent in the parent branch: 53 | inputs.ParentCor = 1; 54 | % Parabola taper correction of radii inside branches. If 1, use the 55 | % correction: 56 | inputs.TaperCor = 1; 57 | 58 | % Growth volume correction approach introduced by Jan Hackenberg, 59 | % allometry: Radius = a*GrowthVol^b+c 60 | inputs.GrowthVolCor = 0; % If 1, use growth volume (GV) correction 61 | % fac-parameter of the GV-approach, defines upper and lower bound. When 62 | % using GV-approach, consider setting TaperCorr = 0, ParentCorr = 0, 63 | % MinCylinderRadius = 0. 64 | inputs.GrowthVolFac = 1.5; % Defines the allowed radius: 65 | % 1/fac*predicted_radius <= radius <= fac*predicted_radius 66 | % However, the radii of the branch tip cylinders are not increased. 67 | 68 | %% Filtering parameters 69 | % NOTE: These are all optional, but needed to run the "filtering" function. 70 | % Statistical k-nearest neighbor distance outlier filtering, applied if 71 | % filter.k > 0. The value filter.k is the number of nearest neighbors. 72 | inputs.filter.k = 10; 73 | % Statistical point density outlier filtering, applied if filter.radius > 0. 74 | % The value filter.radius is the radius of the ball neighborhood. This is 75 | % usually meant as alternative to the above knn-filtering. 76 | inputs.filter.radius = 0.00; 77 | % The value filter.nsigma is the multiplier of the standard deviation of 78 | % the kth-nearest neighbor distance/point density and points whose 79 | % kth-nearest neighbor distance/point density is larger/lower than the 80 | % average +/- filter.nsigma * std are removed: 81 | inputs.filter.nsigma = 1.5; 82 | % Small component filtering is applied if filter.ncomp > 0. This filter is 83 | % based on cover whose patches are defined by filter.PatchDiam1 and 84 | % filter.BallRad1. The points which are included in components that have 85 | % less than filter.ncomp patches are removed: 86 | inputs.filter.PatchDiam1 = 0.05; 87 | inputs.filter.BallRad1 = 0.075; 88 | inputs.filter.ncomp = 2; 89 | % Cubical downsampling is applied if filter.EdgeLength > 0. 90 | % The value filter.EdgeLength is the length of the cube edges: 91 | inputs.filter.EdgeLength = 0.004; 92 | % Plot the filtering results automatically after the filtering if 93 | % filter.plot > 0 94 | inputs.filter.plot = 1; 95 | 96 | %% Other inputs 97 | % These parameters don't affect the QSM-reconstruction but define what is 98 | % saved, plotted, and displayed and how the models are named/indexed 99 | % Name string for saving output files and naming models: 100 | inputs.name = 'tree'; 101 | % Tree index. If modelling multiple trees, then they can be indexed uniquely: 102 | inputs.tree = 1; 103 | % Model index, can separate models if multiple models with the same inputs: 104 | inputs.model = 1; 105 | % Save the output struct QSM as a matlab-file into \result folder. 106 | % If name = 'pine', tree = 2, model = 5, the name of the saved file is 107 | % 'QSM_pine_t2_m5.mat': 108 | inputs.savemat = 1; 109 | % Save the models in .txt-files (check "save_model_text.m"): 110 | inputs.savetxt = 1; 111 | % What are plotted during reconstruction process: 112 | % 2 = plots the QSM, the segmentated point cloud and distributions, 113 | % 1 = plots the QSM and the segmentated point cloud 114 | % 0 = plots nothing 115 | inputs.plot = 2; 116 | % What are displayed during the reconstruction: 2 = display all; 117 | % 1 = display name, parameters and distances; 0 = display only the name: 118 | inputs.disp = 2; 119 | -------------------------------------------------------------------------------- /src/estimate_precision.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [TreeData,OptQSMs,OptQSM] = ... 17 | estimate_precision(QSMs,NewQSMs,TreeData,OptModels,savename) 18 | 19 | % --------------------------------------------------------------------- 20 | % ESTIMATE_PRECISION.M Combines additional QSMs with optimal inputs 21 | % with previously generated QSMs to estimate the 22 | % precision (standard deviation) better. 23 | % 24 | % Version 1.1.0 25 | % Latest update 10 May 2022 26 | % 27 | % Copyright (C) 2016-2022 Pasi Raumonen 28 | % --------------------------------------------------------------------- 29 | 30 | % Uses models with the same inputs to estimate the precision (standard 31 | % deviation) of the results. Has two sets of models as its inputs: 32 | % 1) QSMs can contain models with many different input parameters for each tree 33 | % and OptModels contain the indexes of the models that are used here ("optimal 34 | % models"); 2) NewQSMs contains only models with the optimal inputs. 35 | % 36 | % Inputs: 37 | % QSMs Contain all the models, possibly from multiple trees 38 | % NewQSMs Contains the additional models with optimal inputs, for all trees 39 | % TreeData Similar structure array as the "treedata" in QSMs but now each 40 | % single-number attribute contains the mean and std computed 41 | % from the models with the optimal inputs, and the 42 | % sensitivities for PatchDiam-parameters 43 | % OptModels Indexes of the optimal models for each tree in "QSMs" 44 | % savename Optional input, name string specifying the name of the saved 45 | % file containing the outputs 46 | % Outputs: 47 | % TreeData Updated with new mean and std computed from all the QSMs 48 | % with the optimal inputs 49 | % OptQSMs Contains all the models with the optimal inputs, for all trees 50 | % OptQSM The best model (minimum point-model distance) among the models 51 | % with the optimal inputs, for all trees 52 | % --------------------------------------------------------------------- 53 | 54 | % Changes from version 1.0.2 to 1.1.0, 10 May 2022: 55 | % 1) Added "TreeData", the output of "select_optimum", as an input, and now 56 | % it is updated 57 | 58 | % Changes from version 1.0.1 to 1.0.2, 26 Nov 2019: 59 | % 1) Added the "name" of the point cloud from the inputs.name to the output 60 | % TreeData as a field. Also now displays the name together with the tree 61 | % number. 62 | 63 | % Changes from version 1.0.0 to 1.0.1, 08 Oct 2019: 64 | % 1) Small change for how the output "TreeData" is initialised 65 | 66 | 67 | %% Reconstruct the outputs 68 | OptQSMs = QSMs(vertcat(OptModels{:,1})); % Optimal models from the optimization process 69 | OptQSMs = [OptQSMs NewQSMs]; % Combine all the optimal QSMs 70 | 71 | m = max(size(OptQSMs)); % number of models 72 | IndAll = (1:1:m)'; 73 | % Find the first non-empty model 74 | i = 1; 75 | while isempty(OptQSMs(i).cylinder) 76 | i = i+1; 77 | end 78 | % Determine how many single-number attributes there are in treedata 79 | names = fieldnames(OptQSMs(i).treedata); 80 | n = 1; 81 | while numel(OptQSMs(i).treedata.(names{n})) == 1 82 | n = n+1; 83 | end 84 | n = n-1; 85 | 86 | treedata = zeros(n,m); % Collect all single-number tree attributes from all models 87 | TreeId = zeros(m,1); % Collect tree and model indexes from all models 88 | Dist = zeros(m,1); % Collect the distances 89 | Keep = true(m,1); % Non-empty models 90 | for i = 1:m 91 | if ~isempty(OptQSMs(i).cylinder) 92 | for j = 1:n 93 | treedata(j,i) = OptQSMs(i).treedata.(names{j}); 94 | end 95 | TreeId(i) = OptQSMs(i).rundata.inputs.tree; 96 | Dist(i) = OptQSMs(i).pmdistance.mean; 97 | else 98 | Keep(i) = false; 99 | end 100 | end 101 | treedata = treedata(:,Keep); 102 | TreeId = TreeId(Keep,:); 103 | Dist = Dist(Keep); 104 | IndAll = IndAll(Keep); 105 | TreeIds = unique(TreeId); 106 | nt = length(TreeIds); % number of trees 107 | 108 | % Compute the means and standard deviations 109 | OptModel = zeros(nt,1); 110 | DataM = zeros(n,nt); 111 | DataS = zeros(n,nt); 112 | for t = 1:nt 113 | I = TreeId == TreeIds(t); 114 | ind = IndAll(I); 115 | dist = vertcat(Dist(ind)); 116 | [~,J] = min(dist); 117 | OptModel(t) = ind(J); 118 | DataM(:,t) = mean(treedata(:,ind),2); 119 | DataS(:,t) = std(treedata(:,ind),[],2); 120 | end 121 | OptQSM = OptQSMs(OptModel); 122 | DataCV = DataS./DataM*100; 123 | 124 | %% Display some data about optimal models 125 | % Decrease the number of non-zero decimals 126 | for j = 1:nt 127 | DataM(:,j) = change_precision(DataM(:,j)); 128 | DataS(:,j) = change_precision(DataS(:,j)); 129 | DataCV(:,j) = change_precision(DataCV(:,j)); 130 | end 131 | 132 | % Display optimal inputs, model and attributes for each tree 133 | for t = 1:nt 134 | disp([' Tree: ',num2str(t),', ',OptQSM(t).rundata.inputs.name]) 135 | disp(' Attributes (mean, std, CV(%)):') 136 | for i = 1:n 137 | str = ([' ',names{i},': ',num2str([DataM(i,t) DataS(i,t) DataCV(i,t)])]); 138 | disp(str) 139 | end 140 | disp('------') 141 | end 142 | 143 | %% Generate TreeData structure for optimal models 144 | %TreeData = vertcat(OptQSM(:).treedata); 145 | for t = 1:nt 146 | for i = 1:n 147 | TreeData(t).(names{i})(1:2) = [DataM(i,t) DataS(i,t)]; 148 | end 149 | TreeData(t).name = OptQSM(t).rundata.inputs.name; 150 | end 151 | 152 | %% Save results 153 | if nargin == 5 154 | str = ['results/OptimalQSMs_',savename]; 155 | save(str,'TreeData','OptQSMs','OptQSM') 156 | end 157 | -------------------------------------------------------------------------------- /src/least_squares_fitting/form_rotation_matrices.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [R,dR1,dR2] = form_rotation_matrices(theta) 17 | 18 | % -------------------------------------------------------------------------- 19 | % FORM_ROTATION_MATRICES.M Forms rotation matrices R = R2*R1 and its 20 | % derivatives 21 | % 22 | % Input 23 | % theta Plane rotation angles (t1, t2) 24 | % 25 | % Output 26 | % R Rotation matrix 27 | % R1 Plane rotation [1 0 0; 0 c1 -s1; 0 s1 c1] 28 | % R2 Plane rotation [c2 0 s2; 0 1 0; -s2 0 c2] 29 | 30 | c = cos(theta); 31 | s = sin(theta); 32 | 33 | R1 = [1 0 0; 0 c(1) -s(1); 0 s(1) c(1)]; 34 | R = R1; 35 | 36 | R2 = [c(2) 0 s(2); 0 1 0; -s(2) 0 c(2)]; 37 | R = R2*R; 38 | 39 | if nargout > 1 40 | dR1 = [0 0 0; 0 -R1(3,2) -R1(2,2); 0 R1(2,2) -R1(3,2)]; 41 | end 42 | 43 | if nargout > 2 44 | dR2 = [-R2(1,3) 0 R2(1,1); 0 0 0; -R2(1,1) 0 -R2(1,3)]; 45 | end -------------------------------------------------------------------------------- /src/least_squares_fitting/func_grad_axis.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [dist,J] = func_grad_axis(P,par,weight) 17 | 18 | % --------------------------------------------------------------------- 19 | % FUNC_GRAD_CYLINDER.M Function and gradient calculation for 20 | % least-squares cylinder fit. 21 | % 22 | % Version 2.1.0 23 | % Latest update 14 July 2020 24 | % 25 | % Copyright (C) 2013-2020 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | % 28 | % Input 29 | % par Cylinder parameters [x0 y0 alpha beta r]' 30 | % P Point cloud 31 | % weight (Optional) Weights for the points 32 | % 33 | % Output 34 | % dist Signed distances of points to the cylinder surface: 35 | % dist(i) = sqrt(xh(i)^2 + yh(i)^2) - r, where 36 | % [xh yh zh]' = Ry(beta) * Rx(alpha) * ([x y z]' - [x0 y0 0]') 37 | % J Jacobian matrix d dist(i)/d par(j). 38 | 39 | % Changes from version 2.0.0 to 2.1.0, 14 July 2020: 40 | % 1) Added optional input for weights of the points 41 | 42 | 43 | % Five cylinder parameters: 44 | % Location = axis point intersects xy-plane: x0 and y0 (z0 == 0) 45 | % Rotation angles around x and y axis = alpha and beta 46 | % Radius = r 47 | % 48 | % Transformed points: 49 | % Pt = [xh yx zh] = Ry(beta) * Rx(alpha) * (P - [x0 y0 0]) 50 | % 51 | % "Plane points": 52 | % Qt = Pt * I2 = [xh yh]; 53 | % 54 | % Distance: 55 | % D(x0,y0,alpha,beta,r) = sqrt( dot(Qt,Qt) )-r = sqrt( Qt*Qt' )-r 56 | % 57 | % Least squares = minimize Sum( D^2 ) over x0, y0, alpha, beta and r 58 | % 59 | % rt = sqrt( dot(Qt,Qt) ) 60 | % N = Qt/rt 61 | % 62 | % Jacobian for D with respect to x0, y0, alpha, beta: 63 | % dD/di = dot( N,dQt/di ) = dot( Qt/rt,dQt/di ) 64 | % 65 | % R = Ry*Rx 66 | % dQt/dx0 = R*[-1 0 0]' 67 | % dQt/dy0 = R*[0 -1 0]' 68 | % dQt/dalpha = (P-[x0 y0 0])*DRx'; 69 | % dQt/dalpha = (P-[x0 y0 0])*DRy'; 70 | 71 | x0 = par(1); 72 | y0 = par(2); 73 | alpha = par(3); 74 | beta = par(4); 75 | r = par(5); 76 | 77 | % Determine the rotation matrices and their derivatives 78 | [R,DR1,DR2] = form_rotation_matrices([alpha beta]); 79 | 80 | % Calculate the distances 81 | Pt = (P-[x0 y0 0])*R'; 82 | xt = Pt(:,1); 83 | yt = Pt(:,2); 84 | rt = sqrt(xt.*xt + yt.*yt); 85 | dist = rt-r; % Distances to the cylinder surface 86 | if nargin == 3 87 | dist = weight.*dist; % Weighted distances 88 | end 89 | 90 | % form the Jacobian matrix 91 | if nargout > 1 92 | 93 | N = [xt./rt yt./rt]; 94 | m = size(P,1); 95 | J = zeros(m,2); 96 | 97 | A3 = (P-[x0 y0 0])*DR1'; 98 | J(:,1) = sum(N(:,1:2).*A3(:,1:2),2); 99 | 100 | A4 = (P-[x0 y0 0])*DR2'; 101 | J(:,2) = sum(N(:,1:2).*A4(:,1:2),2); 102 | 103 | if nargin == 3 104 | % Weighted Jacobian 105 | J = [weight.*J(:,1) weight.*J(:,2)]; 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /src/least_squares_fitting/func_grad_circle.m: -------------------------------------------------------------------------------- 1 | function [dist,J] = func_grad_circle(P,par,weight) 2 | 3 | % --------------------------------------------------------------------- 4 | % FUNC_GRAD_CIRCLE.M Function and gradient calculation for 5 | % least-squares circle fit. 6 | % 7 | % Version 1.0 8 | % Latest update 20 Oct 2017 9 | % 10 | % Copyright (C) 2017 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % Input 14 | % P Point cloud 15 | % par Circle parameters [x0 y0 r]' 16 | % weight Weights for the points. Weight the distances. 17 | % 18 | % Output 19 | % dist Signed distances of points to the circle: 20 | % dist(i) = sqrt((xi-x0)^2 + (yi-y0)^2) - r, where 21 | % 22 | % J Jacobian matrix d dist(i)/d par(j). 23 | 24 | 25 | % Calculate the distances 26 | Vx = P(:,1)-par(1); 27 | Vy = P(:,2)-par(2); 28 | rt = sqrt(Vx.*Vx + Vy.*Vy); 29 | if nargin == 3 30 | dist = weight.*(rt-par(3)); % Weighted distances to the circle 31 | else 32 | dist = rt-par(3); % Distances to the circle 33 | end 34 | 35 | % form the Jacobian matrix 36 | if nargout > 1 37 | m = size(P, 1); 38 | J = zeros(m,3); 39 | J(:,1) = -Vx./rt; 40 | J(:,2) = -Vy./rt; 41 | J(:,3) = -1*ones(m,1); 42 | % apply the weights 43 | if nargin == 3 44 | J = [weight.*J(:,1) weight.*J(:,2) weight.*J(:,3)]; 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/least_squares_fitting/func_grad_circle_centre.m: -------------------------------------------------------------------------------- 1 | function [dist,J] = func_grad_circle_centre(P,par,weight) 2 | 3 | % --------------------------------------------------------------------- 4 | % FUNC_GRAD_CIRCLE.M Function and gradient calculation for 5 | % least-squares circle fit. 6 | % 7 | % Version 1.0 8 | % Latest update 20 Oct 2017 9 | % 10 | % Copyright (C) 2017 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % Input 14 | % P Point cloud 15 | % par Circle parameters [x0 y0 r]' 16 | % weight Weights for the points. Weight the distances. 17 | % 18 | % Output 19 | % dist Signed distances of points to the circle: 20 | % dist(i) = sqrt((xi-x0)^2 + (yi-y0)^2) - r, where 21 | % 22 | % J Jacobian matrix d dist(i)/d par(j). 23 | 24 | 25 | % Calculate the distances 26 | Vx = P(:,1)-par(1); 27 | Vy = P(:,2)-par(2); 28 | rt = sqrt(Vx.*Vx+Vy.*Vy); 29 | if nargin == 3 30 | dist = weight.*(rt-par(3)); % Weighted distances to the circle 31 | else 32 | dist = rt-par(3); % Distances to the circle 33 | end 34 | 35 | % form the Jacobian matrix 36 | if nargout > 1 37 | m = size(P,1); 38 | J = zeros(m,2); 39 | J(:,1) = -Vx./rt; 40 | J(:,2) = -Vy./rt; 41 | % apply the weights 42 | if nargin == 3 43 | J = [weight.*J(:,1) weight.*J(:,2)]; 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/least_squares_fitting/func_grad_cylinder.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [dist,J] = func_grad_cylinder(par,P,weight) 17 | 18 | % --------------------------------------------------------------------- 19 | % FUNC_GRAD_CYLINDER.M Function and gradient calculation for 20 | % least-squares cylinder fit. 21 | % 22 | % Version 2.2.0 23 | % Latest update 5 Oct 2021 24 | % 25 | % Copyright (C) 2013-2021 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | % 28 | % Input 29 | % par Cylinder parameters [x0 y0 alpha beta r]' 30 | % P Point cloud 31 | % weight (Optional) Weights for the points 32 | % 33 | % Output 34 | % dist Signed distances of points to the cylinder surface: 35 | % dist(i) = sqrt(xh(i)^2 + yh(i)^2) - r, where 36 | % [xh yh zh]' = Ry(beta) * Rx(alpha) * ([x y z]' - [x0 y0 0]') 37 | % J Jacobian matrix d dist(i)/d par(j). 38 | 39 | % Five cylinder parameters: 40 | % Location = axis point intersects xy-plane: x0 and y0 (z0 == 0) 41 | % Rotation angles around x and y axis = alpha and beta 42 | % Radius = r 43 | % 44 | % Transformed points: 45 | % Pt = [xh yx zh] = Ry(beta) * Rx(alpha) * (P - [x0 y0 0]) 46 | % 47 | % "Plane points": 48 | % Qt = Pt * I2 = [xh yh]; 49 | % 50 | % Distance: 51 | % D(x0,y0,alpha,beta,r) = sqrt( dot(Qt,Qt) )-r = sqrt( Qt*Qt' )-r 52 | % 53 | % Least squares = minimize Sum( D^2 ) over x0, y0, alpha, beta and r 54 | % 55 | % rt = sqrt( dot(Qt,Qt) ) 56 | % N = Qt/rt 57 | % 58 | % Jacobian for D with respect to x0, y0, alpha, beta: 59 | % dD/di = dot( N,dQt/di ) = dot( Qt/rt,dQt/di ) 60 | % 61 | % R = Ry*Rx 62 | % dQt/dx0 = R*[-1 0 0]' 63 | % dQt/dy0 = R*[0 -1 0]' 64 | % dQt/dalpha = (P-[x0 y0 0])*DRx'; 65 | % dQt/dalpha = (P-[x0 y0 0])*DRy'; 66 | 67 | % Changes from version 2.1.0 to 2.2.0, 5 Oct 20201: 68 | % 1) Minor changes in syntax 69 | 70 | % Changes from version 2.0.0 to 2.1.0, 14 July 2020: 71 | % 1) Added optional input for weights of the points 72 | 73 | x0 = par(1); 74 | y0 = par(2); 75 | alpha = par(3); 76 | beta = par(4); 77 | r = par(5); 78 | 79 | % Determine the rotation matrices and their derivatives 80 | [R,DR1,DR2] = form_rotation_matrices([alpha beta]); 81 | 82 | % Calculate the distances 83 | Pt = (P-[x0 y0 0])*R'; 84 | xt = Pt(:,1); 85 | yt = Pt(:,2); 86 | rt = sqrt(xt.*xt + yt.*yt); 87 | dist = rt-r; % Distances to the cylinder surface 88 | if nargin == 3 89 | dist = weight.*dist; % Weighted distances 90 | end 91 | 92 | % form the Jacobian matrix 93 | if nargout > 1 94 | N = [xt./rt yt./rt]; 95 | m = size(P,1); 96 | J = zeros(m,5); 97 | 98 | A1 = (R*[-1 0 0]')'; 99 | A = eye(2); 100 | A(1,1) = A1(1); A(2,2) = A1(2); 101 | J(:,1) = sum(N(:,1:2)*A,2); 102 | 103 | A2 = (R*[0 -1 0]')'; 104 | A(1,1) = A2(1); A(2,2) = A2(2); 105 | J(:,2) = sum(N(:,1:2)*A,2); 106 | 107 | A3 = (P-[x0 y0 0])*DR1'; 108 | J(:,3) = sum(N(:,1:2).*A3(:,1:2),2); 109 | 110 | A4 = (P-[x0 y0 0])*DR2'; 111 | J(:,4) = sum(N(:,1:2).*A4(:,1:2),2); 112 | 113 | J(:,5) = -1*ones(m,1); 114 | if nargin == 3 115 | % Weighted Jacobian 116 | J = [weight.*J(:,1) weight.*J(:,2) weight.*J(:,3) ... 117 | weight.*J(:,4) weight.*J(:,5)]; 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /src/least_squares_fitting/least_squares_axis.m: -------------------------------------------------------------------------------- 1 | 2 | function cyl = least_squares_axis(P,Axis,Point0,Rad0,weight) 3 | 4 | % --------------------------------------------------------------------- 5 | % LEAST_SQUARES_AXIS.M Least-squares cylinder axis fitting using 6 | % Gauss-Newton when radius and point are given 7 | % 8 | % Version 1.0 9 | % Latest update 1 Oct 2021 10 | % 11 | % Copyright (C) 2017-2021 Pasi Raumonen 12 | % --------------------------------------------------------------------- 13 | % Input 14 | % P 3d point cloud 15 | % Axis0 Initial axis estimate (1 x 3) 16 | % Point0 Initial estimate of axis point (1 x 3) 17 | % Rad0 Initial estimate of the cylinder radius 18 | % weight (Optional) Weights for each point 19 | % 20 | % Output 21 | % cyl Structure array with the following fields 22 | % axis Cylinder axis (optimized here) 23 | % radius Radius of the cylinder (from the input) 24 | % start Axis point (from the input) 25 | % mad Mean absolute distance of the points to the cylinder surface 26 | % SurfCov Surface coverage, how much of the cylinder surface is covered 27 | % with points 28 | % conv If conv = 1, the algorithm has converged 29 | % rel If rel = 1, the algorithm has reliable answer in terms of 30 | % matrix inversion with a good enough condition number 31 | % --------------------------------------------------------------------- 32 | 33 | 34 | %% Initial estimates and other settings 35 | res = 0.03; % "Resolution level" for computing surface coverage 36 | par = [0 0]'; 37 | maxiter = 50; % maximum number of Gauss-Newton iteration 38 | iter = 0; % number of iterations so far 39 | conv = false; % converge of Gauss-Newton algorithm 40 | rel = true; % are the results reliable, system matrix not badly conditioned 41 | if nargin == 4 42 | weight = ones(size(P,1),1); 43 | end 44 | Rot0 = rotate_to_z_axis(Axis); 45 | Pt = (P-Point0)*Rot0'; 46 | 47 | Par = [0 0 0 0 Rad0]'; 48 | 49 | %% Gauss-Newton iterations 50 | while iter < maxiter && ~conv && rel 51 | 52 | % Calculate the distances and Jacobian 53 | [dist,J] = func_grad_axis(Pt,Par); 54 | 55 | % Calculate update step and gradient. 56 | SS0 = norm(dist); % Squared sum of the distances 57 | % solve for the system of equations: 58 | % par(i+1) = par(i) - (J'J)^(-1)*J'd(par(i)) 59 | A = J'*J; 60 | b = J'*dist; 61 | warning off 62 | p = -A\b; % solve for the system of equations 63 | warning on 64 | 65 | % Update 66 | par = par+p; 67 | 68 | % Check if the updated parameters lower the squared sum value 69 | Par = [0; 0; par; Rad0]; 70 | dist = func_grad_axis(Pt,Par); 71 | SS1 = norm(dist); 72 | if SS1 > SS0 73 | % Update did not decreased the squared sum, use update with much 74 | % shorter update step 75 | par = par-0.95*p; 76 | Par = [0; 0; par; Rad0]; 77 | dist = func_grad_axis(Pt,Par); 78 | SS1 = norm(dist); 79 | end 80 | 81 | % Check reliability 82 | rel = true; 83 | if rcond(A) < 10000*eps 84 | rel = false; 85 | end 86 | 87 | % Check convergence 88 | if abs(SS0-SS1) < 1e-5 89 | conv = true; 90 | end 91 | 92 | iter = iter+1; 93 | end 94 | 95 | %% Output 96 | % Inverse transformation to find axis and point on axis 97 | % corresponding to original data 98 | Rot = form_rotation_matrices(par); 99 | Axis = Rot0'*Rot'*[0 0 1]'; % axis direction 100 | 101 | % Compute the point distances to the axis 102 | [dist,~,h] = distances_to_line(P,Axis,Point0); 103 | dist = dist-Rad0; % distances without weights 104 | Len = max(h)-min(h); 105 | 106 | % Compute mad (for points with maximum weights) 107 | if nargin <= 4 108 | mad = mean(abs(dist)); % mean absolute distance to the circle 109 | else 110 | I = weight == max(weight); 111 | mad = mean(abs(dist(I))); % mean absolute distance to the circle 112 | end 113 | 114 | % Compute SurfCov, minimum 3*8 grid 115 | if ~any(isnan(par)) && rel && conv 116 | nl = ceil(Len/res); 117 | nl = max(nl,3); 118 | ns = ceil(2*pi*Rad0/res); 119 | ns = max(ns,8); 120 | ns = min(36,ns); 121 | SurfCov = single(surface_coverage(P,Axis,Point0,nl,ns,0.8*Rad0)); 122 | else 123 | SurfCov = single(0); 124 | end 125 | 126 | 127 | %% Define the output 128 | clear cir 129 | cyl.radius = Rad0; 130 | cyl.start = Point0; 131 | cyl.axis = Axis'; 132 | cyl.mad = mad; 133 | cyl.SurfCov = SurfCov; 134 | cyl.conv = conv; 135 | cyl.rel = rel; 136 | -------------------------------------------------------------------------------- /src/least_squares_fitting/least_squares_circle.m: -------------------------------------------------------------------------------- 1 | function cir = least_squares_circle(P,Point0,Rad0,weight) 2 | % --------------------------------------------------------------------- 3 | % LEAST_SQUARES_CIRCLE.M Least-squares circle fitting using Gauss-Newton. 4 | % 5 | % Version 1.1.0 6 | % Latest update 6 Oct 2021 7 | % 8 | % Copyright (C) 2017-2021 Pasi Raumonen 9 | % --------------------------------------------------------------------- 10 | % Input 11 | % P 2d point cloud 12 | % Point0 Initial estimate of centre (1 x 2) 13 | % Rad0 Initial estimate of the circle radius 14 | % weight Optional, weights for each point 15 | % 16 | % Output 17 | % Rad Radius of the cylinder 18 | % Point Centre point (1 x 2) 19 | % ArcCov Arc point coverage (%), how much of the circle arc is covered with points 20 | % conv If conv = 1, the algorithm has converged 21 | % rel If rel = 1, the algorithm has reliable answer in terms of 22 | % matrix inversion with a good enough condition number 23 | % --------------------------------------------------------------------- 24 | 25 | 26 | %% Initial estimates and other settings 27 | par = [Point0 Rad0]'; 28 | maxiter = 200; % maximum number of Gauss-Newton iteration 29 | iter = 0; % number of iterations so far 30 | conv = false; % converge of Gauss-Newton algorithm 31 | rel = true; % are the reusults reliable in the sense that system matrix was not badly conditioned 32 | if nargin == 3 33 | weight = ones(size(P,1),1); 34 | end 35 | 36 | %% Gauss-Newton iterations 37 | while iter < maxiter && ~conv && rel 38 | 39 | % Calculate the distances and Jacobian 40 | [dist,J] = func_grad_circle(P,par,weight); 41 | 42 | % Calculate update step and gradient. 43 | SS0 = norm(dist); % Squared sum of the distances 44 | % solve for the system of equations: par(i+1) = par(i) - (J'J)^(-1)*J'd(par(i)) 45 | A = J'*J; 46 | b = J'*dist; 47 | warning off 48 | p = -A\b; % solve for the system of equations 49 | warning on 50 | 51 | % Update 52 | par = par+p; 53 | 54 | % Check if the updated parameters lower the squared sum value 55 | dist = func_grad_circle(P,par,weight); 56 | SS1 = norm(dist); 57 | if SS1 > SS0 58 | % Update did not decreased the squared sum, use update with much 59 | % shorter update step 60 | par = par-0.95*p; 61 | dist = func_grad_circle(P,par,weight); 62 | SS1 = norm(dist); 63 | end 64 | 65 | % Check reliability 66 | if rcond(A) < 10000*eps 67 | rel = false; 68 | end 69 | 70 | % Check convergence 71 | if abs(SS0-SS1) < 1e-5 72 | conv = true; 73 | end 74 | 75 | iter = iter+1; 76 | end 77 | 78 | %% Output 79 | Rad = par(3); 80 | Point = par(1:2); 81 | U = P(:,1)-Point(1); 82 | V = P(:,2)-Point(2); 83 | dist = sqrt(U.*U+V.*V)-Rad; 84 | if nargin <= 3 85 | mad = mean(abs(dist)); % mean absolute distance to the circle 86 | else 87 | I = weight == max(weight); 88 | mad = mean(abs(dist(I))); % mean absolute distance to the circle 89 | end 90 | % Calculate ArcCov, how much of the circle arc is covered with points 91 | if ~any(isnan(par)) 92 | if nargin <= 3 93 | I = dist > -0.2*Rad; 94 | else 95 | I = dist > -0.2*Rad & weight == max(weight); 96 | end 97 | U = U(I,:); V = V(I,:); 98 | ang = atan2(V,U)+pi; 99 | ang = ceil(ang/2/pi*100); 100 | ang(ang <= 0) = 1; 101 | Arc = false(100,1); 102 | Arc(ang) = true; 103 | ArcCov = nnz(Arc)/100; 104 | else 105 | ArcCov = 0; 106 | end 107 | 108 | cir.radius = Rad; 109 | cir.point = Point'; 110 | cir.mad = mad; 111 | cir.ArcCov = ArcCov; 112 | cir.conv = conv; 113 | cir.rel = rel; -------------------------------------------------------------------------------- /src/least_squares_fitting/least_squares_circle_centre.m: -------------------------------------------------------------------------------- 1 | function cir = least_squares_circle_centre(P,Point0,Rad0) 2 | % --------------------------------------------------------------------- 3 | % LEAST_SQUARES_CIRCLE_CENTRE.M Least-squares circle fitting such that 4 | % radius is given (fits the centre) 5 | % 6 | % Version 1.0.0 7 | % Latest update 6 Oct 2021 8 | % 9 | % Copyright (C) 2017-2021 Pasi Raumonen 10 | % --------------------------------------------------------------------- 11 | % Input 12 | % P 2d point cloud 13 | % Point0 Initial estimate of centre (1 x 2) 14 | % Rad0 The circle radius 15 | % weight Optional, weights for each point 16 | % 17 | % Output 18 | % cir Structure array with the following fields 19 | % Rad Radius of the cylinder 20 | % Point Centre point (1 x 2) 21 | % ArcCov Arc point coverage (%), how much of the circle arc is covered 22 | % with points 23 | % conv If conv = 1, the algorithm has converged 24 | % rel If rel = 1, the algorithm has reliable answer in terms of 25 | % matrix inversion with a good enough condition number 26 | % --------------------------------------------------------------------- 27 | 28 | % Changes from version 1.0.0 to 1.1.0, 6 Oct 2021: 29 | % 1) Streamlining code and some computations 30 | 31 | %% Initial estimates and other settings 32 | par = [Point0 Rad0]'; 33 | maxiter = 200; % maximum number of Gauss-Newton iteration 34 | iter = 0; % number of iterations so far 35 | conv = false; % converge of Gauss-Newton algorithm 36 | rel = true; % the results reliable (system matrix was not badly conditioned) 37 | 38 | %% Gauss-Newton iterations 39 | while iter < maxiter && ~conv && rel 40 | 41 | % Calculate the distances and Jacobian 42 | [dist,J] = func_grad_circle_centre(P,par); 43 | 44 | % Calculate update step and gradient. 45 | SS0 = norm(dist); % Squared sum of the distances 46 | % solve for the system of equations: par(i+1) = par(i) - (J'J)^(-1)*J'd(par(i)) 47 | A = J'*J; 48 | b = J'*dist; 49 | warning off 50 | p = -A\b; % solve for the system of equations 51 | warning on 52 | 53 | % Update 54 | par(1:2,1) = par(1:2,1)+p; 55 | 56 | % Check if the updated parameters lower the squared sum value 57 | dist = func_grad_circle_centre(P,par); 58 | SS1 = norm(dist); 59 | if SS1 > SS0 60 | % Update did not decreased the squared sum, use update with much 61 | % shorter update step 62 | par(1:2,1) = par(1:2,1)-0.95*p; 63 | dist = func_grad_circle_centre(P,par); 64 | SS1 = norm(dist); 65 | end 66 | 67 | % Check reliability 68 | if rcond(A) < 10000*eps 69 | rel = false; 70 | end 71 | 72 | % Check convergence 73 | if abs(SS0-SS1) < 1e-5 74 | conv = true; 75 | end 76 | 77 | iter = iter+1; 78 | end 79 | 80 | %% Output 81 | Point = par(1:2); 82 | if conv && rel 83 | % Calculate ArcCov, how much of the circle arc is covered with points 84 | U = P(:,1)-par(1); 85 | V = P(:,2)-par(2); 86 | ang = atan2(V,U)+pi; 87 | I = false(100,1); 88 | ang = ceil(ang/2/pi*100); 89 | I(ang) = true; 90 | ArcCov = nnz(I)/100; 91 | % mean absolute distance to the circle 92 | d = sqrt(U.*U+V.*V)-Rad0; 93 | mad = mean(abs(d)); 94 | else 95 | mad = 0; 96 | ArcCov = 0; 97 | end 98 | 99 | cir.radius = Rad0; 100 | cir.point = Point'; 101 | cir.mad = mad; 102 | cir.ArcCov = ArcCov; 103 | cir.conv = conv; 104 | cir.rel = rel; -------------------------------------------------------------------------------- /src/least_squares_fitting/least_squares_cylinder.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function cyl = least_squares_cylinder(P,cyl0,weight,Q) 17 | % --------------------------------------------------------------------- 18 | % LEAST_SQUARES_CYLINDER.M Least-squares cylinder using Gauss-Newton. 19 | % 20 | % Version 2.0.0 21 | % Latest update 5 Oct 2021 22 | % 23 | % Copyright (C) 2013-2021 Pasi Raumonen 24 | % --------------------------------------------------------------------- 25 | % Input 26 | % P Point cloud 27 | % cyl0 Initial estimates of the cylinder parameters 28 | % weight (Optional) Weights of the points for fitting 29 | % Q (Optional) Subset of "P" where the cylinder is intended 30 | % 31 | % Output 32 | % cyl Structure array containing the following fields: 33 | % radius Radius of the cylinder 34 | % length Length of the cylinder 35 | % start Point on the axis at the bottom of the cylinder (1 x 3) 36 | % axis Axis direction of the cylinder (1 x 3) 37 | % mad Mean absolute distance between points and cylinder surface 38 | % SurfCov Relative cover of the cylinder's surface by the points 39 | % dist Radial distances from the points to the cylinder (m x 1) 40 | % conv If conv = 1, the algorithm has converged 41 | % rel If rel = 1, the algorithm has reliable answer in terms of 42 | % matrix inversion with a good enough condition number 43 | % --------------------------------------------------------------------- 44 | 45 | % Changes from version 1.3.0 to 2.0.0, 5 Oct 2021: 46 | % 1) Included the Gauss-Newton iterations into this function (removed the 47 | % call to nlssolver function) 48 | % 2) Changed how the updata step is solved from the Jacobian 49 | % 3) Simplified some expressions and added comments 50 | % 4) mad is computed only from the points along the cylinder length in the 51 | % case of the optional input "Q" is given. 52 | % 5) Changed the surface coverage estimation by filtering out points whose 53 | % distance to the axis is less than 80% of the radius 54 | 55 | % Changes from version 1.2.0 to 1.3.0, 14 July 2020: 56 | % 1) Changed the input parameters of the cylinder to the struct format. 57 | % 2) Added optional input for weights 58 | % 3) Added optional input "Q", a subset of "P", the cylinder is intended 59 | % to be fitted in this subset but it is fitted to "P" to get better 60 | % estimate of the axis direction and radius 61 | 62 | % Changes from version 1.1.0 to 1.2.0, 14 Jan 2020: 63 | % 1) Changed the outputs and optionally the inputs to the struct format. 64 | % 2) Added new output, "mad", which is the mean absolute distance of the 65 | % points from the surface of the cylinder. 66 | % 3) Added new output, "SurfCov", that measures how well the surface of the 67 | % cylinder is covered by the points. 68 | % 4) Added new output, "SurfCovDis", which is a matrix of mean point distances 69 | % from layer/sector-intersections to the axis. 70 | % 5) Added new output, "SurfCovVol", which is an estimate of the cylinder's 71 | % volume based on the radii in "SurfCovDis" and "cylindrical sectors". 72 | % 6) Added new optional input "res" which gives the point resolution level 73 | % for computing SurfCov: the width and length of sectors/layers. 74 | 75 | % Changes from version 1.0.0 to 1.1.0, 3 Oct 2019: 76 | % 1) Bug fix: --> "Point = Rot0'*([par(1) par(2) 0]')..." 77 | 78 | 79 | %% Initialize data and values 80 | res = 0.03; % "Resolution level" for computing surface coverage 81 | maxiter = 50; % maximum number of Gauss-Newton iterations 82 | iter = 0; 83 | conv = false; % Did the iterations converge 84 | rel = true; % Are the results reliable (condition number was not very bad) 85 | if nargin == 2 86 | NoWeights = true; % No point weight given for the fitting 87 | else 88 | NoWeights = false; 89 | end 90 | 91 | % Transform the data to close to standard position via a translation 92 | % followed by a rotation 93 | Rot0 = rotate_to_z_axis(cyl0.axis); 94 | Pt = (P-cyl0.start)*Rot0'; 95 | 96 | % Initial estimates 97 | par = [0 0 0 0 cyl0.radius]'; 98 | 99 | 100 | %% Gauss-Newton algorithm 101 | % find estimate of rotation-translation-radius parameters that transform 102 | % the data so that the best-fit cylinder is one in standard position 103 | while iter < maxiter && ~conv && rel 104 | 105 | %% Calculate the distances and Jacobian 106 | if NoWeights 107 | [d0,J] = func_grad_cylinder(par,Pt); 108 | else 109 | [d0,J] = func_grad_cylinder(par,Pt,weight); 110 | end 111 | 112 | %% Calculate update step 113 | SS0 = norm(d0); % Squared sum of the distances 114 | % solve for the system of equations: 115 | % par(i+1) = par(i) - (J'J)^(-1)*J'd0(par(i)) 116 | A = J'*J; 117 | b = J'*d0; 118 | warning off 119 | p = -A\b; % solve for the system of equations 120 | warning on 121 | par = par+p; % update the parameters 122 | 123 | %% Check reliability 124 | if rcond(-A) < 10000*eps 125 | rel = false; 126 | end 127 | 128 | %% Check convergence: 129 | % The distances with the new parameter values: 130 | if NoWeights 131 | dist = func_grad_cylinder(par,Pt); 132 | else 133 | dist = func_grad_cylinder(par,Pt,weight); 134 | end 135 | SS1 = norm(dist); % Squared sum of the distances 136 | if abs(SS0-SS1) < 1e-4 137 | conv = true; 138 | end 139 | 140 | iter = iter + 1; 141 | end 142 | 143 | %% Compute the cylinder parameters and other outputs 144 | cyl.radius = single(par(5)); % radius 145 | 146 | % Inverse transformation to find axis and point on axis 147 | % corresponding to original data 148 | Rot = form_rotation_matrices(par(3:4)); 149 | Axis = Rot0'*Rot'*[0 0 1]'; % axis direction 150 | Point = Rot0'*([par(1) par(2) 0]')+cyl0.start'; % axis point 151 | 152 | % Compute the start, length and mad, translate the axis point to the 153 | % cylinder's bottom: 154 | % If the fourth input (point cloud Q) is given, use it for the start, 155 | % length, mad, and SurfCov 156 | if nargin == 4 157 | if size(Q,1) > 5 158 | P = Q; 159 | end 160 | end 161 | H = P*Axis; % heights along the axis 162 | hmin = min(H); 163 | cyl.length = single(abs(max(H)-hmin)); 164 | hpoint = Axis'*Point; 165 | Point = Point-(hpoint-hmin)*Axis; % axis point at the cylinder's bottom 166 | cyl.start = single(Point'); 167 | cyl.axis = single(Axis'); 168 | % Compute mad for the points along the cylinder length: 169 | if nargin >= 6 170 | I = weight == max(weight); 171 | cyl.mad = single(average(abs(dist(I)))); % mean absolute distance 172 | else 173 | cyl.mad = single(average(abs(dist))); % mean absolute distance 174 | end 175 | cyl.conv = conv; 176 | cyl.rel = rel; 177 | 178 | % Compute SurfCov, minimum 3*8 grid 179 | if ~any(isnan(Axis)) && ~any(isnan(Point)) && rel && conv 180 | nl = max(3,ceil(cyl.length/res)); 181 | ns = ceil(2*pi*cyl.radius/res); 182 | ns = min(36,max(ns,8)); 183 | SurfCov = surface_coverage(P,Axis',Point',nl,ns,0.8*cyl.radius); 184 | 185 | cyl.SurfCov = single(SurfCov); 186 | else 187 | cyl.SurfCov = single(0); 188 | end 189 | -------------------------------------------------------------------------------- /src/least_squares_fitting/nlssolver.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [par,d,conv,rel] = nlssolver(par,P,weight) 17 | 18 | % --------------------------------------------------------------------- 19 | % NLSSOLVER.M Nonlinear least squares solver for cylinders. 20 | % 21 | % Version 2.1.0 22 | % Latest update 14 July 2020 23 | % 24 | % Copyright (C) 2013-2020 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | % 27 | % Input 28 | % par Intial estimates of the parameters 29 | % P Point cloud 30 | % 31 | % Output 32 | % par Optimised parameter values 33 | % d Distances of points to cylinder 34 | % conv True if fitting converged 35 | % rel True if condition number was not very bad, fit was reliable 36 | 37 | % Changes from version 2.0.0 to 2.1.0, 14 July 2020: 38 | % 1) Added optional input for weights of the points 39 | 40 | maxiter = 50; 41 | iter = 0; 42 | conv = false; 43 | rel = true; 44 | 45 | if nargin == 2 46 | NoWeights = true; 47 | else 48 | NoWeights = false; 49 | end 50 | 51 | %% Gauss-Newton iterations 52 | while iter < maxiter && ~conv && rel 53 | 54 | %% Calculate the distances and Jacobian 55 | if NoWeights 56 | [d0, J] = func_grad_cylinder(par,P); 57 | else 58 | [d0, J] = func_grad_cylinder(par,P,weight); 59 | end 60 | 61 | %% Calculate update step 62 | SS0 = norm(d0); % Squared sum of the distances 63 | % solve for the system of equations: 64 | % par(i+1) = par(i) - (J'J)^(-1)*J'd0(par(i)) 65 | A = J'*J; 66 | b = J'*d0; 67 | warning off 68 | p = -A\b; % solve for the system of equations 69 | warning on 70 | par = par+p; % update the parameters 71 | 72 | %% Check reliability 73 | if rcond(-R) < 10000*eps 74 | rel = false; 75 | end 76 | 77 | %% Check convergence: 78 | % The distances with the new parameter values: 79 | if NoWeights 80 | d = func_grad_cylinder(par,P); 81 | else 82 | d = func_grad_cylinder(par,P,weight); 83 | end 84 | SS1 = norm(d); % Squared sum of the distances 85 | if abs(SS0-SS1) < 1e-4 86 | conv = true; 87 | end 88 | 89 | iter = iter + 1; 90 | end 91 | -------------------------------------------------------------------------------- /src/least_squares_fitting/rotate_to_z_axis.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [R,D,a] = rotate_to_z_axis(Vec) 17 | 18 | % -------------------------------------------------------------------------- 19 | % ROTATE_TO_Z_AXIS.M Forms the rotation matrix to rotate the vector to 20 | % a point along the positive z-axis. 21 | % 22 | % Input 23 | % Vec Vector (3 x 1) 24 | % 25 | % Output 26 | % R Rotation matrix, with R * Vec = [0 0 z]', z > 0 27 | 28 | 29 | D = cross(Vec,[0 0 1]); 30 | if norm(D) > 0 31 | a = acos(Vec(3)); 32 | R = rotation_matrix(D,a); 33 | else 34 | R = eye(3); 35 | a = 0; 36 | D = [1 0 0]; 37 | end 38 | -------------------------------------------------------------------------------- /src/main_steps/branches.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function branch = branches(cylinder) 17 | 18 | % --------------------------------------------------------------------- 19 | % BRANCHES.M Determines the branching structure and computes branch 20 | % attributes 21 | % 22 | % Version 3.0.0 23 | % Latest update 2 May 2022 24 | % 25 | % Copyright (C) 2013-2022 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | 28 | % Determines the branches (cylinders in a segment define a branch), their order 29 | % and topological parent-child-relation. Branch number one is the trunk and 30 | % its order is zero. Notice that branch number does not tell its age in the 31 | % sense that branch number two would be the oldest branch and the number 32 | % three the second oldest. 33 | % 34 | % Inputs: 35 | % cylinder Cylinders, structure array 36 | % 37 | % Outputs: 38 | % branch Branch structure array, contains fields: 39 | % Branch order, parent, volume, length, angle, height, azimuth 40 | % and diameter 41 | % --------------------------------------------------------------------- 42 | 43 | % Changes from version 2.1.0 to 3.0.0, 2 May 2022: 44 | % 1) Changed the code such that the input "segment" and output "cylinder" 45 | % are not needed anymore, which simplified the code in many places. 46 | % Cylinder info is now computed in "cylinders" function. 47 | 48 | % Changes from version 2.0.0 to 2.1.0, 25 Jan 2020: 49 | % 1) Chanced the coding to simplify and shorten the code 50 | % 2) Added branch area and zenith direction as new fields in the 51 | % branch-structure array 52 | % 3) Removed the line were 'ChildCyls' and'CylsInSegment' fields are 53 | % removed from the cylinder-structure array 54 | 55 | Rad = cylinder.radius; 56 | Len = cylinder.length; 57 | Axe = cylinder.axis; 58 | 59 | %% Branches 60 | nc = size(Rad,1); % number of cylinder 61 | ns = max(cylinder.branch); % number of segments 62 | BData = zeros(ns,9); % branch ord, dia, vol, are, len, ang, hei, azi, zen 63 | ind = (1:1:nc)'; 64 | CiB = cell(ns,1); 65 | for i = 1:ns 66 | C = ind(cylinder.branch == i); 67 | CiB{i} = C; 68 | if ~isempty(C) 69 | 70 | BData(i,1) = cylinder.BranchOrder(C(1)); % branch order 71 | BData(i,2) = 2*Rad(C(1)); % branch diameter 72 | BData(i,3) = 1000*pi*sum(Len(C).*Rad(C).^2); % branch volume 73 | BData(i,4) = 2*pi*sum(Len(C).*Rad(C)); % branch area 74 | BData(i,5) = sum(Len(C)); % branch length 75 | 76 | % if the first cylinder is added to fill a gap, then 77 | % use the second cylinder to compute the angle: 78 | if cylinder.added(C(1)) && length(C) > 1 79 | FC = C(2); % first cyl in the branch 80 | PC = cylinder.parent(C(1)); % parent cylinder of the branch 81 | else 82 | FC = C(1); 83 | PC = cylinder.parent(FC); 84 | end 85 | if PC > 0 86 | BData(i,6) = 180/pi*acos(Axe(FC,:)*Axe(PC,:)'); % branch angle 87 | end 88 | 89 | BData(i,7) = cylinder.start(C(1),3)-cylinder.start(1,3); % branch height 90 | BData(i,8) = 180/pi*atan2(Axe(C(1),2),Axe(C(1),1)); % branch azimuth 91 | BData(i,9) = 180/pi*acos(Axe(C(1),3)); % branch zenith 92 | end 93 | end 94 | BData = single(BData); 95 | 96 | %% Branching structure (topology, parent-child-relation) 97 | branch.order = uint8(BData(:,1)); 98 | BPar = zeros(ns,1); 99 | Chi = cell(nc,1); 100 | for i = 1:nc 101 | c = ind(cylinder.parent == i); 102 | c = c(c ~= cylinder.extension(i)); 103 | Chi{i} = c; 104 | end 105 | for i = 1:ns 106 | C = CiB{i}; 107 | ChildCyls = unique(vertcat(Chi{C})); 108 | CB = unique(cylinder.branch(ChildCyls)); % Child branches 109 | BPar(CB) = i; 110 | end 111 | if ns <= 2^16 112 | branch.parent = uint16(BPar); 113 | else 114 | branch.parent = uint32(BPar); 115 | end 116 | 117 | %% Finish the definition of branch 118 | branch.diameter = BData(:,2); % diameters in meters 119 | branch.volume = BData(:,3); % volumes in liters 120 | branch.area = BData(:,4); % areas in square meters 121 | branch.length = BData(:,5); % lengths in meters 122 | branch.angle = BData(:,6); % angles in degrees 123 | branch.height = BData(:,7); % heights in meters 124 | branch.azimuth = BData(:,8); % azimuth directions in angles 125 | branch.zenith = BData(:,9); % zenith directions in angles 126 | -------------------------------------------------------------------------------- /src/main_steps/point_model_distance.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function pmdistance = point_model_distance(P,cylinder) 17 | 18 | % --------------------------------------------------------------------- 19 | % POINT_MODEL_DISTANCE.M Computes the distances of the points to the 20 | % cylinder model 21 | % 22 | % Version 2.1.1 23 | % Latest update 8 Oct 2021 24 | % 25 | % Copyright (C) 2015-2021 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | 28 | % Changes from version 2.1.0 to 2.1.1, 8 Oct 2021: 29 | % 1) Changed the determinationa NE, the number of empty edge layers, so 30 | % that is now limited in size, before it is given as input for 31 | % cubical_partition function. 32 | 33 | % Changes from version 2.0.0 to 2.1.0, 26 Nov 2019: 34 | % 1) Bug fix: Corrected the computation of the output at the end of the 35 | % code so that trees without branches are computed correctly. 36 | 37 | % Cylinder data 38 | Rad = cylinder.radius; 39 | Len = cylinder.length; 40 | Sta = cylinder.start; 41 | Axe = cylinder.axis; 42 | BOrd = cylinder.BranchOrder; 43 | 44 | % Select randomly 25 % or max one million points for the distance comput. 45 | np0 = size(P,1); 46 | a = min(0.25*np0,1000000); 47 | I = logical(round(0.5/(1-a/np0)*rand(np0,1))); 48 | P = P(I,:); 49 | 50 | % Partition the points into cubes 51 | L = 2*median(Len); 52 | NE = max(3,min(10,ceil(max(Len)/L)))+3; 53 | [Partition,~,Info] = cubical_partition(P,L,NE); 54 | Min = Info(1:3); 55 | EL = Info(7); 56 | NE = Info(8); 57 | 58 | % Calculates the cube-coordinates of the starting points 59 | CC = floor([Sta(:,1)-Min(1) Sta(:,2)-Min(2) Sta(:,3)-Min(3)]/EL)+NE+1; 60 | 61 | % Compute the number of cubes needed for each starting point 62 | N = ceil(Len/L); 63 | 64 | % Correct N so that cube indexes are not too small or large 65 | I = CC(:,1) < N+1; 66 | N(I) = CC(I,1)-1; 67 | I = CC(:,2) < N+1; 68 | N(I) = CC(I,2)-1; 69 | I = CC(:,3) < N+1; 70 | N(I) = CC(I,3)-1; 71 | I = CC(:,1)+N+1 > Info(4); 72 | N(I) = Info(4)-CC(I,1)-1; 73 | I = CC(:,2)+N+1 > Info(5); 74 | N(I) = Info(5)-CC(I,2)-1; 75 | I = CC(:,3)+N+1 > Info(6); 76 | N(I) = Info(6)-CC(I,3)-1; 77 | 78 | % Calculate the distances to the cylinders 79 | n = size(Rad,1); 80 | np = size(P,1); 81 | Dist = zeros(np,2); % Distance and the closest cylinder of each points 82 | Dist(:,1) = 2; % Large distance initially 83 | Points = zeros(ceil(np/10),1,'int32'); % Auxiliary variable 84 | Data = cell(n,1); 85 | for i = 1:n 86 | Par = Partition(CC(i,1)-N(i):CC(i,1)+N(i),CC(i,2)-N(i):CC(i,2)+N(i),... 87 | CC(i,3)-N(i):CC(i,3)+N(i)); 88 | if N(i) > 1 89 | S = cellfun('length',Par); 90 | I = S > 0; 91 | S = S(I); 92 | Par = Par(I); 93 | stop = cumsum(S); 94 | start = [0; stop]+1; 95 | for k = 1:length(stop) 96 | Points(start(k):stop(k)) = Par{k}(:); 97 | end 98 | points = Points(1:stop(k)); 99 | else 100 | points = vertcat(Par{:}); 101 | end 102 | [d,~,h] = distances_to_line(P(points,:),Axe(i,:),Sta(i,:)); 103 | d = abs(d-Rad(i)); 104 | Data{i} = [d h double(points)]; 105 | I = d < Dist(points,1); 106 | J = h >= 0; 107 | K = h <= Len(i); 108 | L = d < 0.5; 109 | M = I&J&K&L; 110 | points = points(M); 111 | Dist(points,1) = d(M); 112 | Dist(points,2) = i; 113 | end 114 | 115 | % Calculate the distances to the cylinders for points not yet calculated 116 | % because they are not "on side of cylinder 117 | for i = 1:n 118 | if ~isempty(Data{i}) 119 | d = Data{i}(:,1); 120 | h = Data{i}(:,2); 121 | points = Data{i}(:,3); 122 | I = d < Dist(points,1); 123 | J = h >= -0.1 & h <= 0; 124 | K = h <= Len(i)+0.1 & h >= Len(i); 125 | L = d < 0.5; 126 | M = I&(J|K)&L; 127 | points = points(M); 128 | Dist(points,1) = d(M); 129 | Dist(points,2) = i; 130 | end 131 | end 132 | 133 | % Select only the shortest 95% of distances for each cylinder 134 | N = zeros(n,1); 135 | O = zeros(np,1); 136 | for i = 1:np 137 | if Dist(i,2) > 0 138 | N(Dist(i,2)) = N(Dist(i,2))+1; 139 | O(i) = N(Dist(i,2)); 140 | end 141 | end 142 | Cyl = cell(n,1); 143 | for i = 1:n 144 | Cyl{i} = zeros(N(i),1); 145 | end 146 | for i = 1:np 147 | if Dist(i,2) > 0 148 | Cyl{Dist(i,2)}(O(i)) = i; 149 | end 150 | end 151 | DistCyl = zeros(n,1); % Average point distance to each cylinder 152 | for i = 1:n 153 | I = Cyl{i}; 154 | m = length(I); 155 | if m > 19 % select the smallest 95% of distances 156 | d = sort(Dist(I,1)); 157 | DistCyl(i) = mean(d(1:floor(0.95*m))); 158 | elseif m > 0 159 | DistCyl(i) = mean(Dist(I,1)); 160 | end 161 | end 162 | 163 | % Define the output 164 | pmdistance.CylDist = single(DistCyl); 165 | pmdistance.median = median(DistCyl(:,1)); 166 | pmdistance.mean = mean(DistCyl(:,1)); 167 | pmdistance.max = max(DistCyl(:,1)); 168 | pmdistance.std = std(DistCyl(:,1)); 169 | 170 | T = BOrd == 0; 171 | B1 = BOrd == 1; 172 | B2 = BOrd == 2; 173 | B = DistCyl(~T,1); 174 | T = DistCyl(T,1); 175 | B1 = DistCyl(B1,1); 176 | B2 = DistCyl(B2,1); 177 | 178 | pmdistance.TrunkMedian = median(T); 179 | pmdistance.TrunkMean = mean(T); 180 | pmdistance.TrunkMax = max(T); 181 | pmdistance.TrunkStd = std(T); 182 | 183 | if ~isempty(B) 184 | pmdistance.BranchMedian = median(B); 185 | pmdistance.BranchMean = mean(B); 186 | pmdistance.BranchMax = max(B); 187 | pmdistance.BranchStd = std(B); 188 | else 189 | pmdistance.BranchMedian = 0; 190 | pmdistance.BranchMean = 0; 191 | pmdistance.BranchMax = 0; 192 | pmdistance.BranchStd = 0; 193 | end 194 | 195 | if ~isempty(B1) 196 | pmdistance.Branch1Median = median(B1); 197 | pmdistance.Branch1Mean = mean(B1); 198 | pmdistance.Branch1Max = max(B1); 199 | pmdistance.Branch1Std = std(B1); 200 | else 201 | pmdistance.Branch1Median = 0; 202 | pmdistance.Branch1Mean = 0; 203 | pmdistance.Branch1Max = 0; 204 | pmdistance.Branch1Std = 0; 205 | end 206 | 207 | if ~isempty(B2) 208 | pmdistance.Branch2Median = median(B2); 209 | pmdistance.Branch2Mean = mean(B2); 210 | pmdistance.Branch2Max = max(B2); 211 | pmdistance.Branch2Std = std(B2); 212 | else 213 | pmdistance.Branch2Median = 0; 214 | pmdistance.Branch2Mean = 0; 215 | pmdistance.Branch2Max = 0; 216 | pmdistance.Branch2Std = 0; 217 | end 218 | -------------------------------------------------------------------------------- /src/main_steps/relative_size.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function RS = relative_size(P,cover,segment) 17 | 18 | % --------------------------------------------------------------------- 19 | % RELATIVE_SIZE.M Determines relative cover set size for points in new covers 20 | % 21 | % Version 2.00 22 | % Latest update 16 Aug 2017 23 | % 24 | % Copyright (C) 2014-2017 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | % 27 | % Uses existing segmentation and its branching structure to determine 28 | % relative size of the cover sets distributed over new covers. The idea is 29 | % to decrease the relative size as the branch size decreases. This is 30 | % realised so that the relative size at the base of a branch is 31 | % proportional to the size of the stem's base, measured as number of 32 | % cover sets in the first few layers. Also when we approach the 33 | % tip of the branch, the relative size decreases to the minimum. 34 | % Maximum relative size is 256 at the bottom of the 35 | % stem and the minimum is 1 at the tip of every branch. 36 | % 37 | % Output: 38 | % RS Relative size (1-256), uint8-vector, (n_points x 1) 39 | 40 | Bal = cover.ball; 41 | Cen = cover.center; 42 | Nei = cover.neighbor; 43 | Segs = segment.segments; 44 | SChi = segment.ChildSegment; 45 | np = size(P,1); % number of points 46 | ns = size(Segs,1); % number of segments 47 | 48 | %% Use branching order and height as apriori info 49 | % Determine the branch orders of the segments 50 | Ord = zeros(ns,1); 51 | C = SChi{1}; 52 | order = 0; 53 | while ~isempty(C) 54 | order = order+order; 55 | Ord(C) = order; 56 | C = vertcat(SChi{C}); 57 | end 58 | maxO = order+1; % maximum branching order (plus one) 59 | 60 | % Determine tree height 61 | Top = max(P(Cen,3)); 62 | Bot = min(P(Cen,3)); 63 | H = Top-Bot; 64 | 65 | %% Determine "base size" compared to the stem base 66 | % BaseSize is the relative size of the branch base compared to the stem 67 | % base, measured as number of cover sets in the first layers of the cover 68 | % sets. If it is larger than apriori upper limit based on branching order 69 | % and branch height, then correct to the apriori limit 70 | BaseSize = zeros(ns,1); 71 | % Determine first the base size at the stem 72 | S = Segs{1}; 73 | n = size(S,1); 74 | if n >= 2 75 | m = min([6 n]); 76 | BaseSize(1) = mean(cellfun(@length,S(2:m))); 77 | else 78 | BaseSize(1) = length(S{1}); 79 | end 80 | % Then define base size for other segments 81 | for i = 2:ns 82 | S = Segs{i}; 83 | n = size(S,1); 84 | if n >= 2 85 | m = min([6 n]); 86 | BaseSize(i) = ceil(mean(cellfun(@length,S(2:m)))/BaseSize(1)*256); 87 | else 88 | BaseSize(i) = length(S{1})/BaseSize(1)*256; 89 | end 90 | bot = min(P(Cen(S{1}),3)); 91 | h = bot-Bot; % height of the segment's base 92 | BS = ceil(256*(maxO-Ord(i))/maxO*(H-h)/H); % maximum apriori base size 93 | if BaseSize(i) > BS 94 | BaseSize(i) = BS; 95 | end 96 | end 97 | BaseSize(1) = 256; 98 | 99 | %% Determine relative size for points 100 | TS = 1; 101 | RS = zeros(np,1,'uint8'); 102 | for i = 1:ns 103 | S = Segs{i}; 104 | s = size(S,1); 105 | for j = 1:s 106 | Q = S{j}; 107 | RS(vertcat(Bal{Q})) = BaseSize(i)-(BaseSize(i)-TS)*sqrt((j-1)/s); 108 | end 109 | end 110 | 111 | %% Adjust the relative size at the base of child segments 112 | RS0 = RS; 113 | for i = 1:ns 114 | C = SChi{i}; 115 | n = length(C); 116 | if n > 0 117 | for j = 1:n 118 | S = Segs{C(j)}; 119 | B = S{1}; 120 | N = vertcat(Nei{B}); 121 | if size(S,1) > 1 122 | N = setdiff(N,S{2}); 123 | end 124 | N = union(N,B); 125 | N = vertcat(Bal{N}); 126 | RS(N) = RS0(N)/2; 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /src/make_models.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function QSMs = make_models(dataname,savename,Nmodels,inputs) 17 | 18 | % --------------------------------------------------------------------- 19 | % MAKE_MODELS.M Makes QSMs of given point clouds. 20 | % 21 | % Version 1.1.0 22 | % Latest update 9 May 2022 23 | % 24 | % Copyright (C) 2013-2022 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | % 27 | % Makes QSMs of given point clouds specified by the "dataname" and by the 28 | % other inputs. The results are saved into file named "savename". 29 | % Notice, the code does not save indivual QSM runs into their own .mat or 30 | % .txt files but saves all models into one big .mat file. 31 | % 32 | % Inputs: 33 | % dataname String specifying the .mat-file containing the point 34 | % clouds that are used for the QSM reconstruction. 35 | % savename String, the name of the file where the QSMs are saved 36 | % Nmodels (Optional) Number of models generated for each input 37 | % (cloud and input parameters). Default value is 5. 38 | % inputs (Optional) The input parameters structure. Can be defined 39 | % below as part of this code. Can also be given as a 40 | % structure array where each tree gets its own, possibly 41 | % uniquely, defined parameters (e.g. optimal parameters) 42 | % but each tree has to have same number of parameter values. 43 | % 44 | % Output: 45 | % QSMs Structure array containing all the QSMs generated 46 | % --------------------------------------------------------------------- 47 | 48 | % Changes from version 1.1.0 to 1.1.1, 18 Aug 2020: 49 | % 1) Removed the inputs "lcyl" and "FilRad" from the inputs and the 50 | % calculations of number of input parameters 51 | 52 | % Changes from version 1.0.0 to 1.1.0, 03 Oct 2019: 53 | % 1) Added try-catch structure where "treeqsm" is called, so that if there 54 | % is an error during the reconstruction process of one tree, then the 55 | % larger process of making multiple QSMs from multiple tree is not 56 | % stopped. 57 | % 2) Changed the way the data is loaded. Previously all the data was 58 | % loaded into workspace, now only one point cloud is in the workspace. 59 | % 3) Corrected a bug where incomplete QSM was saved as complete QSM 60 | % 4) Changed where the input-structure for each tree is reconstructed 61 | 62 | if nargin < 2 63 | disp('Not enough inputs, no models generated!') 64 | QSMs = struct([]); 65 | return 66 | end 67 | 68 | if nargin == 2 69 | Nmodels = 5; % Number of models per inputs, usually about 5 models is enough 70 | end 71 | 72 | %% Define the parameter values 73 | if nargin == 3 || nargin == 2 74 | % The following parameters can be varied and should be optimised 75 | % (each can have multiple values): 76 | % Patch size of the first uniform-size cover: 77 | inputs.PatchDiam1 = [0.08 0.1]; 78 | % Minimum patch size of the cover sets in the second cover: 79 | inputs.PatchDiam2Min = [0.015 0.025]; 80 | % Maximum cover set size in the stem's base in the second cover: 81 | inputs.PatchDiam2Max = [0.06 0.08]; 82 | 83 | % The following parameters can be varied and but usually can be kept as 84 | % shown (i.e. as little bigger than PatchDiam parameters): 85 | % Ball radius used for the first uniform-size cover generation: 86 | inputs.BallRad1 = inputs.PatchDiam1+0.02; 87 | % Maximum ball radius used for the second cover generation: 88 | inputs.BallRad2 = inputs.PatchDiam2Max+0.01; 89 | 90 | % The following parameters can be usually kept fixed as shown: 91 | inputs.nmin1 = 3; % Minimum number of points in BallRad1-balls, good value is 3 92 | inputs.nmin2 = 1; % Minimum number of points in BallRad2-balls, good value is 1 93 | inputs.OnlyTree = 1; % If "1", then point cloud contains points only from the tree 94 | inputs.Tria = 0; % If "1", then triangulation produces 95 | inputs.Dist = 1; % If "1", then computes the point-model distances 96 | 97 | % Different cylinder radius correction options for modifying too large and 98 | % too small cylinders: 99 | % Traditional TreeQSM choices: 100 | % Minimum cylinder radius, used particularly in the taper corrections: 101 | inputs.MinCylRad = 0.0025; 102 | % Child branch cylinders radii are always smaller than the parent 103 | % branche's cylinder radii: 104 | inputs.ParentCor = 1; 105 | % Use partially linear (stem) and parabola (branches) taper corrections: 106 | inputs.TaperCor = 1; 107 | % Growth volume correction approach introduced by Jan Hackenberg, 108 | % allometry: GrowthVol = a*Radius^b+c 109 | % Use growth volume correction: 110 | inputs.GrowthVolCor = 0; 111 | % fac-parameter of the growth vol. approach, defines upper and lower 112 | % boundary: 113 | inputs.GrowthVolFac = 2.5; 114 | 115 | inputs.name = 'test'; 116 | inputs.tree = 0; 117 | inputs.plot = 0; 118 | inputs.savetxt = 0; 119 | inputs.savemat = 0; 120 | inputs.disp = 0; 121 | end 122 | 123 | % Compute the number of input parameter combinations 124 | in = inputs(1); 125 | ninputs = prod([length(in.PatchDiam1) length(in.PatchDiam2Min)... 126 | length(in.PatchDiam2Max)]); 127 | 128 | %% Load data 129 | matobj = matfile([dataname,'.mat']); 130 | names = fieldnames(matobj); 131 | i = 1; 132 | n = max(size(names)); 133 | while i <= n && ~strcmp(names{i,:},'Properties') 134 | i = i+1; 135 | end 136 | I = (1:1:n); 137 | I = setdiff(I,i); 138 | names = names(I,1); 139 | names = sort(names); 140 | nt = max(size(names)); % number of trees/point clouds 141 | 142 | %% make the models 143 | QSMs = struct('cylinder',{},'branch',{},'treedata',{},'rundata',{},... 144 | 'pmdistance',{},'triangulation',{}); 145 | 146 | % Generate Inputs struct that contains the input parameters for each tree 147 | if max(size(inputs)) == 1 148 | for i = 1:nt 149 | Inputs(i) = inputs; 150 | Inputs(i).name = names{i}; 151 | Inputs(i).tree = i; 152 | Inputs(i).plot = 0; 153 | Inputs(i).savetxt = 0; 154 | Inputs(i).savemat = 0; 155 | Inputs(i).disp = 0; 156 | end 157 | else 158 | Inputs = inputs; 159 | end 160 | 161 | m = 1; 162 | for t = 1:nt % trees 163 | disp(['Modelling tree ',num2str(t),'/',num2str(nt),' (',Inputs(t).name,'):']) 164 | P = matobj.(Inputs(t).name); 165 | j = 1; % model number under generation, make "Nmodels" models per tree 166 | inputs = Inputs(t); 167 | while j <= Nmodels % generate N models per input 168 | k = 1; 169 | n0 = 0; 170 | inputs.model = j; 171 | while k <= 5 % try up to five times to generate non-empty models 172 | try 173 | QSM = treeqsm(P,inputs); 174 | catch 175 | QSM = struct('cylinder',{},'branch',{},'treedata',{},... 176 | 'rundata',{},'pmdistance',{},'triangulation',{}); 177 | QSM(ninputs).treedata = 0; 178 | end 179 | 180 | n = max(size(QSM)); 181 | Empty = false(n,1); 182 | for b = 1:n 183 | if isempty(QSM(b).branch) 184 | Empty(b) = true; 185 | end 186 | end 187 | if n < ninputs || any(Empty) 188 | n = nnz(~Empty); 189 | k = k+1; 190 | if n >= n0 191 | qsm = QSM(~Empty); 192 | n0 = n; 193 | end 194 | else 195 | % Succesfull models generated 196 | QSMs(m:m+n-1) = QSM; 197 | m = m+n; 198 | k = 10; 199 | end 200 | end 201 | if k == 6 202 | disp('Incomplete run!!') 203 | QSMs(m:m+n0-1) = qsm; 204 | m = m+n0; 205 | end 206 | j = j+1; 207 | end 208 | stri = ['results/',savename]; 209 | save(stri,'QSMs') 210 | end 211 | -------------------------------------------------------------------------------- /src/make_models_parallel.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function QSMs = make_models_parallel(dataname,savename,Nmodels,inputs) 17 | 18 | % --------------------------------------------------------------------- 19 | % MAKE_MODELS.M Makes QSMs of given point clouds. 20 | % 21 | % Version 1.1.2 22 | % Latest update 9 May 2022 23 | % 24 | % Copyright (C) 2013-2022 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | % 27 | % Makes QSMs of given point clouds specified by the "dataname" and by the 28 | % other inputs. The results are saved into file named "savename". 29 | % Notice, the code does not save indivual QSM runs into their own .mat or 30 | % .txt files but saves all models into one big .mat file. Same as 31 | % MAKE_MODELS but uses parfor command (requires Parallel Computing Toolbox) 32 | % which allows the utilization of multiple processors/cores to compute in 33 | % parallel number of QSMs with the same inputs. 34 | % 35 | % Inputs: 36 | % dataname String specifying the .mat-file containing the point 37 | % clouds that are used for the QSM reconstruction. 38 | % savename String, the name of the file where the QSMs are saved 39 | % Nmodels (Optional) Number of models generated for each input 40 | % (cloud and input parameters). Default value is 5. 41 | % inputs (Optional) The input parameters structure. Can be defined 42 | % below as part of this code. Can also be given as a 43 | % structure array where each tree gets its own, possibly 44 | % uniquely, defined parameters (e.g. optimal parameters) 45 | % but each tree has to have same number of parameter values. 46 | % 47 | % Output: 48 | % QSMs Structure array containing all the QSMs generated 49 | % --------------------------------------------------------------------- 50 | 51 | % Changes from version 1.1.1 to 1.1.2, 18 Aug 2020: 52 | % 1) Removed the inputs "lcyl" and "FilRad" from the inputs and the 53 | % calculations of number of input parameters 54 | 55 | % Changes from version 1.1.0 to 1.1.1, 13 Jan 2020: 56 | % 1) Changed "m = m+n;" to "m = m+n(j);" at the end of the function. 57 | 58 | % Changes from version 1.0.0 to 1.1.0, 03 Oct 2019: 59 | % 1) Added try-catch structure where "treeqsm" is called, so that if there 60 | % is an error during the reconstruction process of one tree, then the 61 | % larger process of making multiple QSMs from multiple tree is not 62 | % stopped. 63 | % 2) Changed the way the data is loaded. Previously all the data was 64 | % loaded into workspace, now only one point cloud is in the workspace. 65 | % 3) Corrected a bug where incomplete QSM was saved as complete QSM 66 | % 4) Changed where the input-structure for each tree reconstructed 67 | % 5) Changed the coding to separate more the results of the different 68 | % parallel processes (less warnings and errors) 69 | 70 | if nargin < 2 71 | disp('Not enough inputs, no models generated!') 72 | QSMs = struct([]); 73 | return 74 | end 75 | 76 | if nargin == 2 77 | Nmodels = 5; % Number of models per inputs, usually about 5 models is enough 78 | end 79 | 80 | %% Define the parameter values 81 | if nargin == 3 || nargin == 2 82 | % The following parameters can be varied and should be optimised 83 | % (each can have multiple values): 84 | % Patch size of the first uniform-size cover: 85 | inputs.PatchDiam1 = [0.08 0.15]; 86 | % Minimum patch size of the cover sets in the second cover: 87 | inputs.PatchDiam2Min = [0.015 0.025]; 88 | % Maximum cover set size in the stem's base in the second cover: 89 | inputs.PatchDiam2Max = [0.06 0.08]; 90 | 91 | % The following parameters can be varied and but usually can be kept as 92 | % shown (i.e. as little bigger than PatchDiam parameters): 93 | % Ball radius used for the first uniform-size cover generation: 94 | inputs.BallRad1 = inputs.PatchDiam1+0.02; 95 | % Maximum ball radius used for the second cover generation: 96 | inputs.BallRad2 = inputs.PatchDiam2Max+0.01; 97 | 98 | % The following parameters can be usually kept fixed as shown: 99 | inputs.nmin1 = 3; % Minimum number of points in BallRad1-balls, good value is 3 100 | inputs.nmin2 = 1; % Minimum number of points in BallRad2-balls, good value is 1 101 | inputs.OnlyTree = 1; % If "1", then point cloud contains points only from the tree 102 | inputs.Tria = 0; % If "1", then triangulation produces 103 | inputs.Dist = 1; % If "1", then computes the point-model distances 104 | 105 | % Different cylinder radius correction options for modifying too large and 106 | % too small cylinders: 107 | % Traditional TreeQSM choices: 108 | % Minimum cylinder radius, used particularly in the taper corrections: 109 | inputs.MinCylRad = 0.0025; 110 | % Child branch cylinders radii are always smaller than the parent 111 | % branche's cylinder radii: 112 | inputs.ParentCor = 1; 113 | % Use partially linear (stem) and parabola (branches) taper corrections: 114 | inputs.TaperCor = 1; 115 | % Growth volume correction approach introduced by Jan Hackenberg, 116 | % allometry: GrowthVol = a*Radius^b+c 117 | % Use growth volume correction: 118 | inputs.GrowthVolCor = 0; 119 | % fac-parameter of the growth vol. approach, defines upper and lower 120 | % boundary: 121 | inputs.GrowthVolFac = 2.5; 122 | 123 | inputs.name = 'test'; 124 | inputs.tree = 0; 125 | inputs.plot = 0; 126 | inputs.savetxt = 0; 127 | inputs.savemat = 0; 128 | inputs.disp = 0; 129 | end 130 | 131 | % Compute the number of input parameter combinations 132 | in = inputs(1); 133 | ninputs = prod([length(in.PatchDiam1) length(in.PatchDiam2Min)... 134 | length(in.PatchDiam2Max)]); 135 | 136 | 137 | %% Load data 138 | matobj = matfile([dataname,'.mat']); 139 | names = fieldnames(matobj); 140 | i = 1; 141 | n = max(size(names)); 142 | while i <= n && ~strcmp(names{i,:},'Properties') 143 | i = i+1; 144 | end 145 | I = (1:1:n); 146 | I = setdiff(I,i); 147 | names = names(I,1); 148 | names = sort(names); 149 | nt = max(size(names)); % number of trees/point clouds 150 | 151 | %% make the models 152 | QSMs = struct('cylinder',{},'branch',{},'treedata',{},'rundata',{},... 153 | 'pmdistance',{},'triangulation',{}); 154 | 155 | % Generate Inputs struct that contains the input parameters for each tree 156 | if max(size(inputs)) == 1 157 | for i = 1:nt 158 | Inputs(i) = inputs; 159 | Inputs(i).name = names{i}; 160 | Inputs(i).tree = i; 161 | Inputs(i).plot = 0; 162 | Inputs(i).savetxt = 0; 163 | Inputs(i).savemat = 0; 164 | Inputs(i).disp = 0; 165 | end 166 | else 167 | Inputs = inputs; 168 | end 169 | 170 | m = 1; 171 | for t = 1:nt % trees 172 | disp(['Modelling tree ',num2str(t),'/',num2str(nt),' (',Inputs(t).name,'):']) 173 | P = matobj.(Inputs(t).name); 174 | qsms = cell(Nmodels,1); % save here the accepted models 175 | qsm = cell(Nmodels,1); % cell-structure to keep different models separate 176 | n = ones(Nmodels,1); 177 | n0 = zeros(Nmodels,1); 178 | k = ones(Nmodels,1); 179 | parfor j = 1:Nmodels % generate N models per input 180 | inputs = Inputs(t); 181 | inputs.model = j; 182 | while k(j) <= 5 % try up to five times to generate non-empty models 183 | try 184 | qsm{j} = treeqsm(P,inputs); 185 | catch 186 | qsm{j} = struct('cylinder',{},'branch',{},'treedata',{},... 187 | 'rundata',{},'pmdistance',{},'triangulation',{}); 188 | qsm{j}(ninputs).treedata = 0; 189 | end 190 | n(j) = max(size(qsm{j})); 191 | Empty = false(n(j),1); 192 | for b = 1:n(j) 193 | if isempty(qsm{j}(b).branch) 194 | Empty(b) = true; 195 | end 196 | end 197 | if n(j) < ninputs || any(Empty) 198 | n(j) = nnz(~Empty); 199 | k(j) = k(j)+1; 200 | if n(j) > n0(j) 201 | qsms{j} = qsm{j}(~Empty); 202 | n0(j) = n(j); 203 | end 204 | else 205 | % Successful models generated 206 | qsms{j} = qsm{j}; 207 | k(j) = 10; 208 | end 209 | end 210 | if k(j) == 6 211 | disp('Incomplete run!!') 212 | end 213 | end 214 | % Save the models 215 | for j = 1:Nmodels 216 | QSM = qsms{j}; 217 | a = max(size(QSM)); 218 | QSMs(m:m+a-1) = QSM; 219 | m = m+n(j); 220 | end 221 | str = ['results/',savename]; 222 | save(str,'QSMs') 223 | end 224 | -------------------------------------------------------------------------------- /src/plotting/plot2d.m: -------------------------------------------------------------------------------- 1 | function h = plot2d(X,Y,fig,strtit,strx,stry,leg,E) 2 | 3 | % 2D-plots, where the data (X and Y), figure number, title, xlabel, ylabel, 4 | % legends and error bars can be specied with the inputs. 5 | 6 | lw = 1.5; % linewidth 7 | n = size(Y,1); 8 | if n < 9 9 | col = ['-b '; '-r '; '-g '; '-c '; '-m '; '-k '; '-y '; '-.b']; 10 | else 11 | col = [ 12 | 0.00 0.00 1.00 13 | 0.00 0.50 0.00 14 | 1.00 0.00 0.00 15 | 0.00 0.75 0.75 16 | 0.75 0.00 0.75 17 | 0.75 0.75 0.00 18 | 0.25 0.25 0.25 19 | 0.75 0.25 0.25 20 | 0.95 0.95 0.00 21 | 0.25 0.25 0.75 22 | 0.75 0.75 0.75 23 | 0.00 1.00 0.00 24 | 0.76 0.57 0.17 25 | 0.54 0.63 0.22 26 | 0.34 0.57 0.92 27 | 1.00 0.10 0.60 28 | 0.88 0.75 0.73 29 | 0.10 0.49 0.47 30 | 0.66 0.34 0.65 31 | 0.99 0.41 0.23]; 32 | if n > 20 33 | k = ceil(n/20); 34 | col = repmat(col,[k 1]); 35 | end 36 | end 37 | figure(fig) 38 | if nargin <= 7 39 | % plots without errorbars 40 | if ~iscell(Y) 41 | if ~isempty(X) 42 | if n < 9 43 | h = plot(X(1,:),Y(1,:),'-b','Linewidth',lw); 44 | else 45 | h = plot(X(1,:),Y(1,:),'Color',col(1,:),'Linewidth',lw); 46 | end 47 | else 48 | if n < 9 49 | h = plot(Y(1,:),'-b','Linewidth',lw); 50 | else 51 | h = plot(Y(1,:),'Color',col(1,:),'Linewidth',lw); 52 | end 53 | end 54 | if n > 1 55 | hold on 56 | if ~isempty(X) 57 | if n < 9 58 | for i = 2:n 59 | plot(X(i,:),Y(i,:),col(i,:),'Linewidth',lw) 60 | end 61 | else 62 | for i = 2:n 63 | plot(X(i,:),Y(i,:),'Color',col(i,:),'Linewidth',lw) 64 | end 65 | end 66 | else 67 | if n < 9 68 | for i = 2:n 69 | plot(Y(i,:),col(i,:),'Linewidth',lw) 70 | end 71 | else 72 | for i = 2:n 73 | plot(Y(i,:),'Color',col(i,:),'Linewidth',lw) 74 | end 75 | end 76 | end 77 | hold off 78 | end 79 | else 80 | if ~isempty(X) 81 | x = X{1}; 82 | end 83 | y = Y{1}; 84 | if ~isempty(X) 85 | if n < 9 86 | h = plot(x,y,'-b','Linewidth',lw); 87 | else 88 | h = plot(x,y,'Color',col(1,:),'Linewidth',lw); 89 | end 90 | else 91 | if n < 9 92 | h = plot(y,'-b','Linewidth',lw); 93 | else 94 | h = plot(y,'Color',col(1,:),'Linewidth',lw); 95 | end 96 | end 97 | if n > 1 98 | hold on 99 | if ~isempty(X) 100 | for i = 2:n 101 | x = X{i}; 102 | y = Y{i}; 103 | if n < 9 104 | plot(x,y,col(i,:),'Linewidth',lw) 105 | else 106 | plot(x,y,'Color',col(i,:),'Linewidth',lw) 107 | end 108 | end 109 | else 110 | for i = 2:n 111 | y = Y{i}; 112 | if n < 9 113 | plot(y,col(i,:),'Linewidth',lw) 114 | else 115 | plot(y,'Color',col(i,:),'Linewidth',lw) 116 | end 117 | end 118 | end 119 | hold off 120 | end 121 | end 122 | 123 | else 124 | % plots with errorbars 125 | if ~iscell(Y) 126 | if ~isempty(X) 127 | if n < 9 128 | h = errorbar(X(1,:),Y(1,:),E(1,:),'-b','Linewidth',lw); 129 | else 130 | h = errorbar(X(1,:),Y(1,:),E(1,:),'Color',col(1,:),'Linewidth',lw); 131 | end 132 | else 133 | if n < 9 134 | h = errorbar(Y(1,:),E(1,:),'-b','Linewidth',lw); 135 | else 136 | h = errorbar(Y(1,:),E(1,:),'Color',col(1,:),'Linewidth',lw); 137 | end 138 | end 139 | if n > 1 140 | hold on 141 | if ~isempty(X) 142 | if n < 9 143 | for i = 2:n 144 | errorbar(X(i,:),Y(i,:),E(1,:),col(i,:),'Linewidth',lw) 145 | end 146 | else 147 | for i = 2:n 148 | errorbar(X(i,:),Y(i,:),E(1,:),'Color',col(i,:),'Linewidth',lw) 149 | end 150 | end 151 | else 152 | if n < 9 153 | for i = 2:n 154 | errorbar(Y(i,:),E(1,:),col(i,:),'Linewidth',lw) 155 | end 156 | else 157 | for i = 2:n 158 | errorbar(Y(i,:),E(1,:),'Color',col(i,:),'Linewidth',lw) 159 | end 160 | end 161 | end 162 | hold off 163 | end 164 | else 165 | if ~isempty(X) 166 | x = X{1}; 167 | end 168 | y = Y{1}; 169 | e = E{1}; 170 | if ~isempty(X) 171 | if n < 9 172 | h = errorbar(x,y,e(1,:),'-b','Linewidth',lw); 173 | else 174 | h = errorbar(x,y,'Color',e(1,:),col(1,:),'Linewidth',lw); 175 | end 176 | else 177 | if n < 9 178 | h = errorbar(y,e(1,:),'-b','Linewidth',lw); 179 | else 180 | h = errorbar(y,e(1,:),'Color',col(1,:),'Linewidth',lw); 181 | end 182 | end 183 | if n > 1 184 | hold on 185 | if ~isempty(X) 186 | for i = 2:n 187 | x = X{i}; 188 | y = Y{i}; 189 | e = E{i}; 190 | if n < 9 191 | h = errorbar(x,y,e(1,:),col(i,:),'Linewidth',lw); 192 | else 193 | h = errorbar(x,y,e(1,:),'Color',col(i,:),'Linewidth',lw); 194 | end 195 | end 196 | else 197 | for i = 2:n 198 | y = Y{i}; 199 | e = E{i}; 200 | if n < 9 201 | errorbar(y,e(1,:),col(i,:),'Linewidth',lw); 202 | else 203 | errorbar(y,e(1,:),'Color',col(i,:),'Linewidth',lw); 204 | end 205 | end 206 | end 207 | hold off 208 | end 209 | end 210 | end 211 | 212 | grid on 213 | t = title(strtit); 214 | x = xlabel(strx); 215 | y = ylabel(stry); 216 | if nargin > 6 217 | legend(leg) 218 | end 219 | set(gca,'fontsize',12) 220 | set(gca,'FontWeight','bold') 221 | set(t,'fontsize',12) 222 | set(t,'FontWeight','bold') 223 | set(x,'fontsize',12) 224 | set(x,'FontWeight','bold') 225 | set(y,'fontsize',12) 226 | set(y,'FontWeight','bold') -------------------------------------------------------------------------------- /src/plotting/plot_branch_segmentation.m: -------------------------------------------------------------------------------- 1 | function plot_branch_segmentation(P,cover,segment,Color,fig,ms,segind,BO) 2 | 3 | % --------------------------------------------------------------------- 4 | % PLOT_BRANCH_SEGMENTATION.M Plots branch-segmented point cloud, coloring 5 | % based on branching order or branches 6 | % 7 | % Version 1.0.0 8 | % Latest update 13 July 2020 9 | % 10 | % Copyright (C) 2013-2020 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % If the coloring is based on branches (Color = 'branch'), then each segment 14 | % is colored with unique color. If the coloring is based on branching order 15 | % (Color = 'order'), then Blue = trunk, Green = 1st-order branches, 16 | % Red = 2nd-order branches, etc. 17 | % 18 | % If segind = 1 and BO = 0, then plots the stem. If segind = 1 and BO = 1, 19 | % then plots the stem and the 1st-order branches. If segind = 1 and 20 | % BO >= maximum branching order or BO input is not given, then plots the 21 | % whole tree. If segind = 2 and BO is not given or it is high enough, then 22 | % plots the branch whose index is 2 and all its sub-branches. 23 | % 24 | % Inputs 25 | % P Point cloud 26 | % cover Cover sets structure 27 | % segment Segments structure 28 | % Color Color option, 'order' or 'branch' 29 | % fig Figure number 30 | % ms Marker size 31 | % segind Index of the segment where the plotting of tree structure starts. 32 | % BO How many branching orders are plotted. 0 = stem, 1 = 1st order, etc 33 | 34 | n = nargin; 35 | if n < 8 36 | BO = 1000; 37 | if n < 7 38 | segind = 1; 39 | if n < 6 40 | ms = 1; 41 | if n < 5 42 | fig = 1; 43 | if n == 3 44 | Color = 'order'; 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | Bal = cover.ball; 52 | Segs = segment.segments; 53 | SChi = segment.ChildSegment; 54 | SPar = segment.ParentSegment; 55 | ns = max(size(Segs)); 56 | 57 | if iscell(Segs{1}) 58 | Seg = cell(ns,1); 59 | for i = 1:ns 60 | m = size(Segs{i},1); 61 | S = zeros(0); 62 | for j = 1:m 63 | s = Segs{i}(j); 64 | s = s{:}; 65 | S = [S; s]; 66 | end 67 | Seg{i} = S; 68 | end 69 | else 70 | Seg = Segs; 71 | end 72 | 73 | if strcmp(Color,'branch') 74 | Color = 1; 75 | % Color the segments with unique colors 76 | col = rand(ns,3); 77 | for i = 2:ns 78 | C = col(SPar(i),:); 79 | c = col(i,:); 80 | while sum(abs(C-c)) < 0.2 81 | c = rand(1,3); 82 | end 83 | col(i,:) = c; 84 | end 85 | elseif strcmp(Color,'order') 86 | Color = 0; 87 | % Color the cylinders in branches based on the branch order 88 | col = [ 89 | 0.00 0.00 1.00 90 | 0.00 0.50 0.00 91 | 1.00 0.00 0.00 92 | 0.00 0.75 0.75 93 | 0.75 0.00 0.75 94 | 0.75 0.75 0.00 95 | 0.25 0.25 0.25 96 | 0.75 0.25 0.25 97 | 0.95 0.95 0.00 98 | 0.25 0.25 0.75 99 | 0.75 0.75 0.75 100 | 0.00 1.00 0.00 101 | 0.76 0.57 0.17 102 | 0.54 0.63 0.22 103 | 0.34 0.57 0.92 104 | 1.00 0.10 0.60 105 | 0.88 0.75 0.73 106 | 0.10 0.49 0.47 107 | 0.66 0.34 0.65 108 | 0.99 0.41 0.23]; 109 | col = repmat(col,[10,1]); 110 | end 111 | 112 | segments = segind; 113 | C = SChi{segind}; 114 | b = 1; 115 | order = 1; 116 | while ~isempty(C) && b <= BO 117 | b = b+1; 118 | segments = [segments; C]; 119 | order = [order; b*ones(length(C),1)]; 120 | C = vertcat(SChi{C}); 121 | end 122 | 123 | ns = length(segments); 124 | figure(fig) 125 | for i = 1:ns 126 | if i == 2 127 | hold on 128 | end 129 | S = vertcat(Bal{Seg{segments(i)}}); 130 | if Color 131 | % Coloring based on branch 132 | plot3(P(S,1),P(S,2),P(S,3),'.','Color',col(segments(i),:),'Markersize',ms) 133 | else 134 | % Coloring based on branch order 135 | plot3(P(S,1),P(S,2),P(S,3),'.','Color',col(order(i),:),'Markersize',ms) 136 | end 137 | end 138 | hold off 139 | axis equal 140 | -------------------------------------------------------------------------------- /src/plotting/plot_branches.m: -------------------------------------------------------------------------------- 1 | function plot_branches(P,cover,segment,fig,ms,segind,BO) 2 | 3 | n = nargin; 4 | if n < 7 5 | BO = 1000; 6 | if n < 6 7 | segind = 1; 8 | if n < 5 9 | ms = 1; 10 | if n == 3 11 | fig = 1; 12 | end 13 | end 14 | end 15 | end 16 | 17 | Bal = cover.ball; 18 | Segs = segment.segments; 19 | SChi = segment.ChildSegment; 20 | SPar = segment.ParentSegment; 21 | 22 | if iscell(Segs{1}) 23 | ns = max(size(Segs)); 24 | Seg = cell(ns,1); 25 | for i = 1:ns 26 | m = size(Segs{i},1); 27 | S = zeros(0); 28 | for j = 1:m 29 | s = Segs{i}(j); 30 | s = s{:}; 31 | S = [S; s]; 32 | end 33 | Seg{i} = S; 34 | end 35 | else 36 | Seg = Segs; 37 | end 38 | 39 | % Color the segments with unique colors 40 | col = rand(ns,3); 41 | for i = 2:ns 42 | C = col(SPar(i),:); 43 | c = col(i,:); 44 | while sum(abs(C-c)) < 0.2 45 | c = rand(1,3); 46 | end 47 | col(i,:) = c; 48 | end 49 | 50 | segments = segind; 51 | C = SChi{segind}; 52 | b = 0; 53 | while ~isempty(C) && b <= BO 54 | b = b+1; 55 | segments = [segments; C]; 56 | C = SChi{segind}; 57 | end 58 | 59 | ns = length(segment); 60 | figure(fig) 61 | for i = 1:ns 62 | if i == 2 63 | hold on 64 | end 65 | S = vertcat(Bal{Seg{segments(i)}}); 66 | plot3(P(S,1),P(S,2),P(S,3),'.','Color',col(segments(i),:),'Markersize',ms) 67 | end 68 | hold off 69 | -------------------------------------------------------------------------------- /src/plotting/plot_comparison.m: -------------------------------------------------------------------------------- 1 | function plot_comparison(P1,P2,fig,ms1,ms2) 2 | 3 | % Plots two point clouds "P1" and "P2" so that those points of "P2" which are 4 | % not in "P1" are plotted in red whereas the common points are plotted in 5 | % blue. "fig" and "ms1" and "ms2" are the figure number and marker sizes. 6 | 7 | if nargin == 3 8 | ms1 = 3; 9 | ms2 = 3; 10 | elseif nargin == 4 11 | ms2 = 3; 12 | end 13 | 14 | if ms1 == 0 15 | ms1 = 3; 16 | end 17 | if ms2 == 0 18 | ms2 = 3; 19 | end 20 | 21 | P2 = setdiff(P2,P1,'rows'); 22 | 23 | figure(fig) 24 | if size(P1,2) == 3 25 | plot3(P1(:,1),P1(:,2),P1(:,3),'.b','Markersize',ms1) 26 | hold on 27 | plot3(P2(:,1),P2(:,2),P2(:,3),'.r','Markersize',ms2) 28 | elseif size(P1,2) == 2 29 | plot(P1(:,1),P1(:,2),'.b','Markersize',ms1) 30 | hold on 31 | plot(P2(:,1),P2(:,2),'.r','Markersize',ms2) 32 | end 33 | hold off 34 | axis equal -------------------------------------------------------------------------------- /src/plotting/plot_cone_model.m: -------------------------------------------------------------------------------- 1 | function plot_cone_model(cylinder,fig,nf,alp,Ind) 2 | 3 | % Plots the given cylinder model as truncated cones defined by the cylinders. 4 | % cylinder Structure array containin the cylinder info 5 | % (radius, length, start, axis, BranchOrder) 6 | % fig Figure number 7 | % nf Number of facets in the cyliders (in the thickest cylinder, 8 | % scales down with radius to 4 which is the minimum) 9 | % alp Alpha value (1 = no trasparency, 0 = complete transparency) 10 | % Ind Indexes of cylinders to be plotted from a subset of cylinders 11 | % (Optional, if not given then all cylinders are plotted) 12 | 13 | 14 | if isstruct(cylinder) 15 | Rad = cylinder.radius; 16 | Len = cylinder.length; 17 | Sta = cylinder.start; 18 | %Sta = mat_vec_subtraction(Sta,Sta(1,:)); 19 | Axe = cylinder.axis; 20 | Bran = cylinder.branch; 21 | PiB = cylinder.PositionInBranch; 22 | nb = max(Bran); 23 | else 24 | Rad = cylinder(:,1); 25 | Len = cylinder(:,2); 26 | Sta = cylinder(:,3:5); 27 | %Sta = mat_vec_subtraction(Sta,Sta(1,:)); 28 | Axe = cylinder(:,6:8); 29 | Bran = cylinder(:,12); 30 | PiB = cylinder(:,14); 31 | nb = max(Bran); 32 | end 33 | if nargin == 5 34 | Rad = Rad(Ind); 35 | Len = Len(Ind); 36 | Sta = Sta(Ind,:); 37 | Axe = Axe(Ind,:); 38 | end 39 | 40 | nc = size(Rad,1); 41 | 42 | Cir = cell(nf,2); 43 | for i = 4:nf 44 | Cir{i,1} = [cos((1/i:1/i:1)*2*pi)' sin((1/i:1/i:1)*2*pi)' zeros(i,1)]; 45 | Cir{i,2} = [(1:1:i)' (i+1:1:2*i)' [(i+2:1:2*i)'; i+1] [(2:1:i)'; 1]]; 46 | end 47 | 48 | Vert = zeros(2*nc*(nf+1),3); 49 | Facets = zeros(nc*(nf+1),4); 50 | t = 1; 51 | f = 1; 52 | 53 | % Scale, rotate and translate the standard cylinders 54 | Ind = (1:1:nc)'; 55 | for j = 1:nb 56 | I = Bran == j; 57 | I = Ind(I); 58 | if ~isempty(I) 59 | P = PiB(I); 60 | [P,J] = sort(P); 61 | I = I(J); 62 | n = ceil(sqrt(mean(Rad(I))/Rad(1))*nf); 63 | n = min(n,nf); 64 | n = max(n,4); 65 | C0 = Cir{n,1}; 66 | m = length(I); 67 | for i = 1:m 68 | C = C0; 69 | 70 | % Scale radius 71 | C(1:n,1:2) = Rad(I(i))*C(1:n,1:2); 72 | if i == m 73 | % Define the last circle of the branch 74 | C1 = C; 75 | C1(:,1:2) = min(0.005/Rad(I(i)),1)*C(:,1:2); 76 | end 77 | 78 | % Rotate 79 | if i == 1 80 | ang = real(acos(Axe(I(i),3))); 81 | Axis = cross([0 0 1]',Axe(I(i),:)'); 82 | Rot = rotation_matrix(Axis,ang); 83 | C = C*Rot'; 84 | elseif i > 1 85 | ang = real(acos(Axe(I(i),3))); 86 | Axis = cross([0 0 1]',Axe(I(i),:)'); 87 | Rot = rotation_matrix(Axis,ang); 88 | C = C*Rot'; 89 | %%% Should be somehow corrected so that high angles between 90 | %%% cylinders do not cause narrowing the surface!!! 91 | 92 | 93 | if i == m 94 | ang = real(acos(Axe(I(i),3))); 95 | Axis = cross([0 0 1]',Axe(I(i),:)'); 96 | Rot = rotation_matrix(Axis,ang); 97 | C1 = C1*Rot'; 98 | end 99 | end 100 | 101 | % Translate 102 | C = mat_vec_subtraction(C,-Sta(I(i),:)); 103 | if i == m 104 | C1 = mat_vec_subtraction(C1,-(Sta(I(i),:)+Len(I(i))*Axe(I(i),:))); 105 | end 106 | 107 | % Save the new vertices 108 | Vert(t:t+n-1,:) = C; 109 | if i == m 110 | t = t+n; 111 | Vert(t:t+n-1,:) = C1; 112 | end 113 | t = t+n; 114 | 115 | % Define the new facets 116 | if i == 1 && i == m 117 | Facets(f:f+n-1,:) = Cir{n,2}+t-2*n-1; 118 | f = f+n; 119 | elseif i > 1 && i < m 120 | Facets(f:f+n-1,:) = Cir{n,2}+t-2*n-1; 121 | f = f+n; 122 | elseif i > 1 && i == m 123 | Facets(f:f+n-1,:) = Cir{n,2}+t-3*n-1; 124 | f = f+n; 125 | Facets(f:f+n-1,:) = Cir{n,2}+t-2*n-1; 126 | f = f+n; 127 | end 128 | end 129 | end 130 | end 131 | 132 | t = t-1; 133 | f = f-1; 134 | Vert = Vert(1:t,:); 135 | Facets = Facets(1:f,:); 136 | fvd = [139/255*ones(f,1) 69/255*ones(f,1) 19/255*ones(f,1)]; 137 | 138 | figure(fig) 139 | plot3(Vert(1,1),Vert(1,2),Vert(1,3)) 140 | patch('Vertices',Vert,'Faces',Facets,'FaceVertexCData',fvd,'FaceColor','flat') 141 | alpha(alp) 142 | axis equal 143 | grid on 144 | view(-37.5,30) 145 | -------------------------------------------------------------------------------- /src/plotting/plot_cylinder_model.m: -------------------------------------------------------------------------------- 1 | function plot_cylinder_model(cylinder,Color,fig,nf,alp,Ind) 2 | 3 | % --------------------------------------------------------------------- 4 | % PLOT_CYLINDER_MODEL.M Plots the given cylinder model 5 | % 6 | % Version 1.2.0 7 | % Latest update 3 Aug 2021 8 | % 9 | % Copyright (C) 2013-2021 Pasi Raumonen 10 | % --------------------------------------------------------------------- 11 | 12 | % Plots the cylinder model. 13 | % cylinder Structure array containin the cylinder info 14 | % (radius, length, start, axis, BranchOrder) 15 | % fig Figure number 16 | % nf Number of facets in the cyliders (in the thickest cylinder, 17 | % scales down with radius to 4 which is the minimum) 18 | % alp Alpha value (1 = no trasparency, 0 = complete transparency) 19 | % Color If equals to "order", colors the cylinders based on branching 20 | % order, otherwise colors each branch with unique color 21 | % Ind Indexes of cylinders to be plotted from a subset of cylinders 22 | % (Optional, if not given then all cylinders are plotted) 23 | 24 | % Changes from version 1.1.0 to 1.2.0, 3 Aug 2021: 25 | % 1) Changed the surface plot ("patch") so that the edges are not plotted 26 | % with separate color, so the surface looks more smooth. Similarly added 27 | % shading. (These are added at the end of the file) 28 | % 2) Added cylinder branch "Bran" and branch order "BOrd" vectors where the 29 | % coloring options are defined to prevent some errors 30 | 31 | % Changes from version 1.0.0 to 1.1.0, 13 July 2020: 32 | % 1) Added option for choosing the coloring based either on branch order or 33 | % unique color for each branch 34 | % 2) Removed the possibility of the input "cylinder" being a matrix 35 | % 3) Added default values for inputs 36 | 37 | n = nargin; 38 | if n < 5 39 | alp = 1; 40 | if n < 4 41 | nf = 20; 42 | if n < 3 43 | fig = 1; 44 | if n == 1 45 | Color = 'order'; 46 | end 47 | end 48 | end 49 | end 50 | 51 | Rad = cylinder.radius; 52 | Len = cylinder.length; 53 | Sta = cylinder.start; 54 | %Sta = Sta-Sta(1,:); 55 | Axe = cylinder.axis; 56 | if strcmp(Color,'order') 57 | BOrd = cylinder.BranchOrder; 58 | Bran = cylinder.branch; 59 | end 60 | if strcmp(Color,'branch') 61 | Bran = cylinder.branch; 62 | BOrd = cylinder.BranchOrder; 63 | end 64 | 65 | if nargin == 6 66 | Rad = Rad(Ind); 67 | Len = Len(Ind); 68 | Sta = Sta(Ind,:); 69 | Axe = Axe(Ind,:); 70 | BOrd = BOrd(Ind); 71 | if strcmp(Color,'branch') 72 | Bran = Bran(Ind); 73 | end 74 | end 75 | 76 | nc = size(Rad,1); % Number of cylinder 77 | 78 | if strcmp(Color,'order') 79 | Color = 1; 80 | % Color the cylinders in branches based on the branch order 81 | col = [ 82 | 0.00 0.00 1.00 83 | 0.00 0.50 0.00 84 | 1.00 0.00 0.00 85 | 0.00 0.75 0.75 86 | 0.75 0.00 0.75 87 | 0.75 0.75 0.00 88 | 0.25 0.25 0.25 89 | 0.75 0.25 0.25 90 | 0.95 0.95 0.00 91 | 0.25 0.25 0.75 92 | 0.75 0.75 0.75 93 | 0.00 1.00 0.00 94 | 0.76 0.57 0.17 95 | 0.54 0.63 0.22 96 | 0.34 0.57 0.92 97 | 1.00 0.10 0.60 98 | 0.88 0.75 0.73 99 | 0.10 0.49 0.47 100 | 0.66 0.34 0.65 101 | 0.99 0.41 0.23]; 102 | col = repmat(col,[10,1]); 103 | elseif strcmp(Color,'branch') 104 | Color = 0; 105 | % Color the cylinders in branches with an unique color of each branch 106 | N = double(max(Bran)); 107 | col = rand(N,3); 108 | Par = cylinder.parent; 109 | for i = 2:nc 110 | if Par(i) > 0 && Bran(Par(i)) ~= Bran(i) 111 | C = col(Bran(Par(i)),:); 112 | c = col(Bran(i),:); 113 | while sum(abs(C-c)) < 0.2 114 | c = rand(1,3); 115 | end 116 | col(Bran(i),:) = c; 117 | end 118 | end 119 | end 120 | 121 | Cir = cell(nf,2); 122 | for i = 4:nf 123 | B = [cos((1/i:1/i:1)*2*pi)' sin((1/i:1/i:1)*2*pi)' zeros(i,1)]; 124 | T = [cos((1/i:1/i:1)*2*pi)' sin((1/i:1/i:1)*2*pi)' ones(i,1)]; 125 | Cir{i,1} = [B; T]; 126 | Cir{i,2} = [(1:1:i)' (i+1:1:2*i)' [(i+2:1:2*i)'; i+1] [(2:1:i)'; 1]]; 127 | end 128 | 129 | Vert = zeros(2*nc*(nf+1),3); 130 | Facets = zeros(nc*(nf+1),4); 131 | fvd = zeros(nc*(nf+1),3); 132 | t = 1; 133 | f = 1; 134 | 135 | % Scale, rotate and translate the standard cylinders 136 | for i = 1:nc 137 | n = ceil(sqrt(Rad(i)/Rad(1))*nf); 138 | n = min(n,nf); 139 | n = max(n,4); 140 | C = Cir{n,1}; 141 | % Scale 142 | C(:,1:2) = Rad(i)*C(:,1:2); 143 | C(n+1:end,3) = Len(i)*C(n+1:end,3); 144 | % Rotate 145 | ang = real(acos(Axe(i,3))); 146 | Axis = cross([0 0 1]',Axe(i,:)'); 147 | Rot = rotation_matrix(Axis,ang); 148 | C = (Rot*C')'; 149 | % Translate 150 | C = mat_vec_subtraction(C,-Sta(i,:)); 151 | Vert(t:t+2*n-1,:) = C; 152 | Facets(f:f+n-1,:) = Cir{n,2}+t-1; 153 | if Color == 1 154 | fvd(f:f+n-1,:) = repmat(col(BOrd(i)+1,:),[n 1]); 155 | else 156 | fvd(f:f+n-1,:) = repmat(col(Bran(i),:),[n 1]); 157 | end 158 | t = t+2*n; 159 | f = f+n; 160 | end 161 | t = t-1; 162 | f = f-1; 163 | Vert = Vert(1:t,:); 164 | Facets = Facets(1:f,:); 165 | fvd = fvd(1:f,:); 166 | 167 | figure(fig) 168 | plot3(Vert(1,1),Vert(1,2),Vert(1,3)) 169 | patch('Vertices',Vert,'Faces',Facets,'FaceVertexCData',fvd,'FaceColor','flat') 170 | alpha(alp) 171 | axis equal 172 | grid on 173 | view(-37.5,30) 174 | 175 | shading flat 176 | lightangle(gca,-45,30) 177 | lighting gouraud -------------------------------------------------------------------------------- /src/plotting/plot_cylinder_model2.m: -------------------------------------------------------------------------------- 1 | function plot_cylinder_model2(cylinder,fig,nf,alp,Ind) 2 | 3 | % Plots the cylinder model. 4 | % cylinder Structure array containin the cylinder info 5 | % (radius, length, start, axis, BranchOrder) 6 | % fig Figure number 7 | % nf Number of facets in the cyliders (in the thickest cylinder, 8 | % scales down with radius to 4 which is the minimum) 9 | % alp Alpha value (1 = no trasparency, 0 = complete transparency) 10 | % Ind Indexes of cylinders to be plotted from a subset of cylinders 11 | % (Optional, if not given then all cylinders are plotted) 12 | 13 | 14 | Rad = cylinder.radius; 15 | Rad2 = cylinder.TopRadius; 16 | Len = cylinder.length; 17 | Sta = cylinder.start; 18 | Sta = mat_vec_subtraction(Sta,Sta(1,:)); 19 | Axe = cylinder.axis; 20 | BOrd = cylinder.BranchOrder; 21 | if nargin == 5 22 | Rad = Rad(Ind); 23 | Len = Len(Ind); 24 | Sta = Sta(Ind,:); 25 | Axe = Axe(Ind,:); 26 | BOrd = BOrd(Ind); 27 | end 28 | 29 | nc = size(Rad,1); 30 | 31 | col = [ 32 | 0.00 0.00 1.00 33 | 0.00 0.50 0.00 34 | 1.00 0.00 0.00 35 | 0.00 0.75 0.75 36 | 0.75 0.00 0.75 37 | 0.75 0.75 0.00 38 | 0.25 0.25 0.25 39 | 0.75 0.25 0.25 40 | 0.95 0.95 0.00 41 | 0.25 0.25 0.75 42 | 0.75 0.75 0.75 43 | 0.00 1.00 0.00 44 | 0.76 0.57 0.17 45 | 0.54 0.63 0.22 46 | 0.34 0.57 0.92 47 | 1.00 0.10 0.60 48 | 0.88 0.75 0.73 49 | 0.10 0.49 0.47 50 | 0.66 0.34 0.65 51 | 0.99 0.41 0.23]; 52 | 53 | N = max(BOrd)+1; 54 | if N <= 20 55 | col = col(1:N,:); 56 | else 57 | m = ceil(N/20); 58 | col = repmat(col,[m,1]); 59 | col = col(1:N,:); 60 | end 61 | 62 | Cir = cell(nf,2); 63 | for i = 4:nf 64 | B = [cos((1/i:1/i:1)*2*pi)' sin((1/i:1/i:1)*2*pi)' zeros(i,1)]; 65 | T = [cos((1/i:1/i:1)*2*pi)' sin((1/i:1/i:1)*2*pi)' ones(i,1)]; 66 | Cir{i,1} = [B; T]; 67 | Cir{i,2} = [(1:1:i)' (i+1:1:2*i)' [(i+2:1:2*i)'; i+1] [(2:1:i)'; 1]]; 68 | end 69 | 70 | Vert = zeros(2*nc*(nf+1),3); 71 | Facets = zeros(nc*(nf+1),4); 72 | fvd = zeros(nc*(nf+1),3); 73 | t = 1; 74 | f = 1; 75 | 76 | % Scale, rotate and translate the standard cylinders 77 | for i = 1:nc 78 | n = ceil(sqrt(Rad(i)/Rad(1))*nf); 79 | n = min(n,nf); 80 | n = max(n,4); 81 | C = Cir{n,1}; 82 | % Scale 83 | m = size(C,1); 84 | C(1:m/2,1:2) = Rad(i)*C(1:m/2,1:2); 85 | C(m/2+1:m,1:2) = Rad2(i)*C(m/2+1:m,1:2); 86 | C(n+1:end,3) = Len(i)*C(n+1:end,3); 87 | % Rotate 88 | ang = real(acos(Axe(i,3))); 89 | Axis = cross([0 0 1]',Axe(i,:)'); 90 | Rot = rotation_matrix(Axis,ang); 91 | C = (Rot*C')'; 92 | % Translate 93 | C = mat_vec_subtraction(C,-Sta(i,:)); 94 | Vert(t:t+2*n-1,:) = C; 95 | Facets(f:f+n-1,:) = Cir{n,2}+t-1; 96 | fvd(f:f+n-1,:) = repmat(col(BOrd(i)+1,:),[n 1]); 97 | t = t+2*n; 98 | f = f+n; 99 | end 100 | t = t-1; 101 | f = f-1; 102 | Vert = Vert(1:t,:); 103 | Facets = Facets(1:f,:); 104 | fvd = fvd(1:f,:); 105 | 106 | figure(fig) 107 | plot3(Vert(1),Vert(2),Vert(3)) 108 | patch('Vertices',Vert,'Faces',Facets,'FaceVertexCData',fvd,'FaceColor','flat') 109 | alpha(alp) 110 | axis equal 111 | grid on 112 | view(-37.5,30) 113 | -------------------------------------------------------------------------------- /src/plotting/plot_distribution.m: -------------------------------------------------------------------------------- 1 | function plot_distribution(QSM,fig,rela,cumu,dis,dis2,dis3,dis4) 2 | 3 | % --------------------------------------------------------------------- 4 | % PLOT_DISTRIBUTION Plots the specified distribution(s) in the 5 | % "treedata" field of the QSM structure array. 6 | % 7 | % Version 1.1.0 8 | % Latest update 3 May 2022 9 | % 10 | % Copyright (C) 2020-2022 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % Inputs: 14 | % QSM The output of treeqsm function, may contain multiple models if 15 | % only one distribution. If multiple distributions are plotted, 16 | % then only one model. 17 | % fig Figure number 18 | % rela If rela = 1, then plots relative values (%), otherwise plots 19 | % absolute values 20 | % cumu If cumu = 1, then plot cumulative distribution 21 | % dis Distribution to be plotted, string name, e.g. 'VolCylDia'. 22 | % The name string is the one used in the "treedata" 23 | % dis2 Optional, Second distribution to be plotted. Notice with more 24 | % than one distribution, only one model. 25 | % dis3 Optional, Third distribution to be plotted 26 | % dis4 Optional, Fourth distribution to be plotted 27 | % --------------------------------------------------------------------- 28 | 29 | % Changes from version 1.0.0 to 1.1.0, 3 May 2022: 30 | % 1) Added new input "cum" for plottig the distributions as cumulative. 31 | % 2) Added return if distributions are empty or all zero 32 | 33 | % Generate strings for title, xlabel and ylabel: 34 | if strcmp(dis(1:3),'Vol') 35 | str = 'volume'; 36 | ylab = 'Volume (L)'; 37 | elseif strcmp(dis(1:3),'Are') 38 | str = 'area'; 39 | ylab = 'Area (m^2)'; 40 | elseif strcmp(dis(1:3),'Len') 41 | str = 'length'; 42 | ylab = 'Length (m)'; 43 | elseif strcmp(dis(1:3),'Num') 44 | str = 'number'; 45 | ylab = 'Number'; 46 | end 47 | 48 | if strcmp(dis(end-2:end),'Dia') 49 | str2 = 'diameter'; 50 | xlab = 'diameter (cm)'; 51 | elseif strcmp(dis(end-2:end),'Hei') 52 | str2 = 'height'; 53 | xlab = 'height (m)'; 54 | elseif strcmp(dis(end-2:end),'Ord') 55 | str2 = 'order'; 56 | xlab = 'order'; 57 | elseif strcmp(dis(end-2:end),'Ang') 58 | str2 = 'angle'; 59 | xlab = 'angle (deg)'; 60 | elseif strcmp(dis(end-2:end),'Azi') 61 | str2 = 'azimuth direction'; 62 | xlab = 'azimuth direction (deg)'; 63 | elseif strcmp(dis(end-2:end),'Zen') 64 | str2 = 'zenith direction'; 65 | xlab = 'zenith direction (deg)'; 66 | end 67 | 68 | % Collect the distributions 69 | if nargin == 5 70 | % Multiple QSMs, one and the same distribution 71 | m = max(size(QSM)); 72 | D = QSM(1).treedata.(dis); 73 | n = size(D,2); 74 | for i = 2:m 75 | d = QSM(i).treedata.(dis); 76 | k = size(d,2); 77 | if k > n 78 | n = k; 79 | D(m,n) = 0; 80 | D(i,1:n) = d; 81 | elseif k < n 82 | D(i,1:k) = d; 83 | else 84 | D(i,:) = d; 85 | end 86 | end 87 | D = D(:,1:n); 88 | else 89 | % One QSM, multiple distributions of the same type 90 | % (e.g. diameter distributions: 'NumCylDia', 'VolCylDia' and 'LenCylDia') 91 | m = nargin-4; 92 | D = QSM.treedata.(dis); 93 | n = size(D,2); 94 | if n == 0 || all(D == 0) 95 | return 96 | end 97 | for i = 2:m 98 | if i == 2 99 | D(m,n) = 0; 100 | D(i,:) = QSM.treedata.(dis2); 101 | elseif i == 3 102 | D(i,:) = QSM.treedata.(dis3); 103 | else 104 | D(i,:) = QSM.treedata.(dis4); 105 | end 106 | end 107 | end 108 | 109 | if rela 110 | % use relative value 111 | for i = 1:m 112 | D(i,:) = D(i,:)/sum(D(i,:))*100; 113 | end 114 | ylab = 'Relative value (%)'; 115 | end 116 | 117 | if cumu 118 | % use cumulative distribution 119 | D = cumsum(D,2); 120 | end 121 | 122 | % Generate the bar plot 123 | figure(fig) 124 | if strcmp(dis(end-3:end),'hAzi') || strcmp(dis(end-3:end),'1Azi') || strcmp(dis(end-2:end),'Azi') 125 | bar(-170:10:180,D') 126 | elseif strcmp(dis(end-2:end),'Zen') || strcmp(dis(end-2:end),'Ang') 127 | bar(10:10:10*n,D') 128 | else 129 | bar(1:1:n,D') 130 | end 131 | 132 | % Generate the title of the plot 133 | if strcmp(dis(end-2:end),'Ord') && ~strcmp(dis(1:3),'Num') 134 | tit = ['Branch ',str,' per branching order']; 135 | elseif strcmp(dis(end-2:end),'Ord') 136 | tit = 'Number of branches per branching order'; 137 | elseif strcmp(dis(1:3),'Num') 138 | tit = ['Number of branches per ',str2,' class']; 139 | elseif strcmp(dis(end-3),'h') || strcmp(dis(end-3),'1') 140 | tit = ['Branch ',str,' per ',str2,' class']; 141 | else 142 | tit = ['Tree segment ',str,' per ',str2,' class']; 143 | end 144 | n = nargin; 145 | if n > 5 146 | if ~strcmp(dis(1:3),dis2(1:3)) 147 | if strcmp(dis(4),'C') 148 | tit = 'Tree segment distribution'; 149 | else 150 | tit = 'Branch distribution'; 151 | end 152 | elseif n > 6 153 | if ~strcmp(dis(1:3),dis3(1:3)) 154 | if strcmp(dis(4),'C') 155 | tit = 'Tree segment distribution'; 156 | else 157 | tit = 'Branch distribution'; 158 | end 159 | elseif n > 7 160 | if ~strcmp(dis(1:3),dis4(1:3)) 161 | if strcmp(dis(4),'C') 162 | tit = 'Tree segment distribution'; 163 | else 164 | tit = 'Branch distribution'; 165 | end 166 | end 167 | end 168 | end 169 | end 170 | title(tit) 171 | 172 | % Generate the x-axis label 173 | if strcmp(dis(end-5:end-3),'Cyl') 174 | xlab = ['Cylinder ',xlab]; 175 | else 176 | xlab = ['Branch ',xlab]; 177 | end 178 | xlabel(xlab) 179 | 180 | % Generate the y-axis label 181 | ylabel(ylab); 182 | 183 | % Tight axes and grid lines 184 | axis tight 185 | grid on 186 | 187 | m = max(size(QSM)); 188 | % Add legends, if needed 189 | if m > 1 190 | L = cell(m,1); 191 | for i = 1:m 192 | L{i} = ['model',num2str(i)]; 193 | end 194 | legend(L,'location','best') 195 | elseif nargin > 5 196 | m = nargin-4; 197 | L = cell(m,1); 198 | for i = 1:m 199 | if i == 1 200 | L{i} = dis(1:end-3); 201 | elseif i == 2 202 | L{i} = dis2(1:end-3); 203 | elseif i == 3 204 | L{i} = dis3(1:end-3); 205 | else 206 | L{i} = dis4(1:end-3); 207 | end 208 | end 209 | legend(L,'location','best') 210 | end -------------------------------------------------------------------------------- /src/plotting/plot_large_point_cloud.m: -------------------------------------------------------------------------------- 1 | function plot_large_point_cloud(P,fig,ms,rel) 2 | 3 | % Plots a random subset of a large point cloud. The user specifies the 4 | % relative size of the subset (input "rel" given as in percentage points). 5 | % 6 | % Inputs: 7 | % P Point cloud 8 | % fig Figure number 9 | % ms Marker size 10 | % rel Subset size in percentage points (%). 11 | % E.g. if rel = 12, then about 12 % poinst are plotted 12 | 13 | rel = 0.5/(1-rel/100); % Compute a coeffiecient 14 | 15 | I = logical(round(rel*rand(size(P,1),1))); 16 | plot_point_cloud(P(I,:),fig,ms) -------------------------------------------------------------------------------- /src/plotting/plot_models_segmentations.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function plot_models_segmentations(P,cover,segment,cylinder,trunk,triangulation) 17 | 18 | % --------------------------------------------------------------------- 19 | % PLOT_MODELS_SEGMENTATION.M Plots the segmented point clouds and 20 | % cylinder/triangulation models 21 | % 22 | % Version 1.1.0 23 | % Latest update 13 July 2020 24 | % 25 | % Copyright (C) 2013-2020 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | 28 | % Inputs: 29 | % P Point cloud 30 | % cover cover-structure array 31 | % segment segment-structure array 32 | % cylinder cylinder-structure array 33 | % trunk point cloud of the trunk 34 | % triangulation triangulation-structure array 35 | 36 | % Changes from version 1.0.0 to 1.1.0, 13 July 2020: 37 | % 1) plots now figure 1 and 2 with two subplots; in the first the colors 38 | % are based on branching order and in the second they are based on 39 | % branch 40 | 41 | %% figure 1: branch-segmented point cloud 42 | % colors denote the branching order and branches 43 | figure(1) 44 | subplot(1,2,1) 45 | plot_branch_segmentation(P,cover,segment,'order') 46 | subplot(1,2,2) 47 | plot_branch_segmentation(P,cover,segment,'branch') 48 | 49 | %% figure 2: cylinder model 50 | % colors denote the branching order and branches 51 | Sta = cylinder.start; 52 | P = P-Sta(1,:); 53 | if nargin > 5 54 | trunk = trunk-Sta(1,:); 55 | Vert = double(triangulation.vert); 56 | Vert = Vert-Sta(1,:); 57 | end 58 | Sta = Sta-Sta(1,:); 59 | cylinder.start = Sta; 60 | figure(2) 61 | subplot(1,2,1) 62 | plot_cylinder_model(cylinder,'order',2,10) 63 | subplot(1,2,2) 64 | plot_cylinder_model(cylinder,'branch',2,10) 65 | 66 | %% figure 3, segmented point cloud and cylinder model 67 | plot_branch_segmentation(P,cover,segment,'order',3,1) 68 | hold on 69 | plot_cylinder_model(cylinder,'order',3,10,0.7) 70 | hold off 71 | 72 | if nargin > 4 73 | %% figure 4, triangulation model (bottom) and cylinder model (top) 74 | % of the stem 75 | Facets = double(triangulation.facet); 76 | CylInd = triangulation.cylind; 77 | fvd = triangulation.fvd; 78 | if max(size(Vert)) > 5 79 | Bran = cylinder.branch; 80 | nc = size(Bran,1); 81 | ind = (1:1:nc)'; 82 | C = ind(Bran == 1); 83 | n = size(trunk,1); 84 | I = logical(round(0.55*rand(n,1))); 85 | figure(4) 86 | point_cloud_plotting(trunk(I,:),4,3) 87 | patch('Vertices',Vert,'Faces',Facets,'FaceVertexCData',fvd,... 88 | 'FaceColor','flat') 89 | alpha(1) 90 | hold on 91 | plot_cylinder_model(cylinder,'order',4,20,1,(CylInd:C(end))) 92 | axis equal 93 | hold off 94 | else 95 | disp('No triangulation model generated!') 96 | end 97 | end -------------------------------------------------------------------------------- /src/plotting/plot_point_cloud.m: -------------------------------------------------------------------------------- 1 | function plot_point_cloud(P,fig,ms,col) 2 | 3 | % Plots the given point cloud. 4 | % 5 | % PLOT_POINT_CLOUD(P,FIG,MS,col) plots point cloud P in figure FIG using 6 | % marker size MS and point color COL (string). P is a 2- or 3-column matrix 7 | % where the first, second and third column gives the X-, Y-, and 8 | % Z-coordinates of the points. 9 | % 10 | % PLOT_POINT_CLOUD(P,FIG) plots point cloud P in figure FIG using 11 | % marker size 3 and color blue ('b'). 12 | % 13 | % PLOT_POINT_CLOUD(P) plots point cloud P in figure 1 using marker size 3 14 | % and color blue ('b'). 15 | 16 | if nargin == 1 17 | fig = 1; 18 | ms = 3; 19 | col = 'b'; 20 | elseif nargin == 2 21 | ms = 3; 22 | col = 'b'; 23 | elseif nargin == 3 24 | col = 'b'; 25 | end 26 | if ms == 0 27 | ms = 3; 28 | end 29 | 30 | col = ['.',col]; 31 | 32 | figure(fig) 33 | if size(P,2) == 3 34 | plot3(P(:,1),P(:,2),P(:,3),col,'Markersize',ms) 35 | elseif size(P,2) == 2 36 | plot(P(:,1),P(:,2),col,'Markersize',ms) 37 | end 38 | axis equal 39 | -------------------------------------------------------------------------------- /src/plotting/plot_scatter.m: -------------------------------------------------------------------------------- 1 | function plot_scatter(P,C,fig,ms) 2 | 3 | % A scatter plot where the color of each 2d or 3d point is specified by a 4 | % number. 5 | % 6 | % Inputs: 7 | % P point cloud 8 | % C color data (vector, value for each point in P) 9 | % fig figure number 10 | % ms marker size 11 | 12 | S = normalize(C); 13 | figure(fig) 14 | if size(P,2) == 3 15 | scatter3(P(:,1),P(:,2),P(:,3),ms*ones(size(P,1),1),C,'filled') 16 | %scatter3(P(:,1),P(:,2),P(:,3),ms*S.*ones(size(P,1),1),C,'filled') 17 | elseif size(P,2) == 2 18 | scatter(P(:,1),P(:,2),ms*S.*ones(size(P,1),1),C,'filled') 19 | end 20 | axis equal 21 | if size(C,2) == 1 22 | colormap(jet(25)) 23 | %caxis([0 max(C)]) 24 | if min(C) < max(C) 25 | caxis([min(C) max(C)]) 26 | end 27 | colorbar 28 | end 29 | -------------------------------------------------------------------------------- /src/plotting/plot_segments.m: -------------------------------------------------------------------------------- 1 | function plot_segments(P,Bal,fig,ms,seg1,seg2,seg3,seg4,seg5) 2 | 3 | % Plots point cloud segments/subsets defined as subsets of cover sets. 4 | % If the subsets intersect, then assiggnes the common points to the 5 | % segments given first. 6 | % 7 | % Inputs 8 | % P Point cloud 9 | % Bal Cover sets. Bal = cover.ball 10 | % fig figure number 11 | % seg1 Segment/subset 1, color blue 12 | % seg2 (Optional) Segment/subset 2, color red 13 | % seg3 (Optional) Segment/subset 3, color green 14 | % seg4 (Optional) Segment/subset 4, color cyan 15 | % seg5 (Optional) Segment/subset 5, color magenta 16 | 17 | 18 | if nargin == 5 19 | S1 = unique(vertcat(Bal{seg1})); 20 | figure(fig) 21 | plot3(P(S1,1),P(S1,2),P(S1,3),'b.','Markersize',ms) 22 | axis equal 23 | elseif nargin == 6 24 | S1 = unique(vertcat(Bal{seg1})); 25 | S2 = unique(vertcat(Bal{seg2})); 26 | S2 = setdiff(S2,S1); 27 | figure(fig) 28 | plot3(P(S1,1),P(S1,2),P(S1,3),'b.','Markersize',1.5*ms) 29 | hold on 30 | plot3(P(S2,1),P(S2,2),P(S2,3),'r.','Markersize',ms) 31 | axis equal 32 | hold off 33 | elseif nargin == 7 34 | S1 = unique(vertcat(Bal{seg1})); 35 | S2 = unique(vertcat(Bal{seg2})); 36 | S3 = unique(vertcat(Bal{seg3})); 37 | S2 = setdiff(S2,S1); 38 | S3 = setdiff(S3,S1); 39 | S3 = setdiff(S3,S2); 40 | figure(fig) 41 | plot3(P(S1,1),P(S1,2),P(S1,3),'b.','Markersize',ms) 42 | hold on 43 | plot3(P(S2,1),P(S2,2),P(S2,3),'r.','Markersize',ms) 44 | plot3(P(S3,1),P(S3,2),P(S3,3),'g.','Markersize',ms) 45 | axis equal 46 | hold off 47 | elseif nargin == 8 48 | S1 = unique(vertcat(Bal{seg1})); 49 | S2 = unique(vertcat(Bal{seg2})); 50 | S3 = unique(vertcat(Bal{seg3})); 51 | S4 = unique(vertcat(Bal{seg4})); 52 | S2 = setdiff(S2,S1); 53 | S3 = setdiff(S3,S1); 54 | S3 = setdiff(S3,S2); 55 | S4 = setdiff(S4,S1); 56 | S4 = setdiff(S4,S2); 57 | S4 = setdiff(S4,S3); 58 | figure(fig) 59 | plot3(P(S1,1),P(S1,2),P(S1,3),'b.','Markersize',ms) 60 | hold on 61 | plot3(P(S2,1),P(S2,2),P(S2,3),'r.','Markersize',ms) 62 | plot3(P(S3,1),P(S3,2),P(S3,3),'g.','Markersize',ms) 63 | plot3(P(S4,1),P(S4,2),P(S4,3),'c.','Markersize',ms) 64 | axis equal 65 | hold off 66 | elseif nargin == 9 67 | S1 = unique(vertcat(Bal{seg1})); 68 | S2 = unique(vertcat(Bal{seg2})); 69 | S3 = unique(vertcat(Bal{seg3})); 70 | S4 = unique(vertcat(Bal{seg4})); 71 | S5 = unique(vertcat(Bal{seg5})); 72 | S2 = setdiff(S2,S1); 73 | S3 = setdiff(S3,S1); 74 | S3 = setdiff(S3,S2); 75 | S4 = setdiff(S4,S1); 76 | S4 = setdiff(S4,S2); 77 | S4 = setdiff(S4,S3); 78 | S5 = setdiff(S5,S1); 79 | S5 = setdiff(S5,S2); 80 | S5 = setdiff(S5,S3); 81 | S5 = setdiff(S5,S4); 82 | figure(fig) 83 | plot3(P(S1,1),P(S1,2),P(S1,3),'b.','Markersize',ms) 84 | hold on 85 | plot3(P(S2,1),P(S2,2),P(S2,3),'r.','Markersize',ms) 86 | plot3(P(S3,1),P(S3,2),P(S3,3),'g.','Markersize',ms) 87 | plot3(P(S4,1),P(S4,2),P(S4,3),'c.','Markersize',ms) 88 | plot3(P(S5,1),P(S5,2),P(S5,3),'m.','Markersize',ms) 89 | axis equal 90 | hold off 91 | end -------------------------------------------------------------------------------- /src/plotting/plot_segs.m: -------------------------------------------------------------------------------- 1 | function plot_segs(P,comps,fig,ms,Bal) 2 | 3 | % Plots the point cloud segments given in the cell array "comps". 4 | % If 4 inputs, cells contain the point indexes. If 5 input, cells contain 5 | % the indexes of the cover sets given by "Bal". 6 | % "fig" is the figure number and "ms" is the marker size. 7 | 8 | 9 | col = [ 10 | 0.00 0.00 1.00 11 | 0.00 0.50 0.00 12 | 1.00 0.00 0.00 13 | 0.00 0.75 0.75 14 | 0.75 0.00 0.75 15 | 0.75 0.75 0.00 16 | 0.25 0.25 0.25 17 | 0.75 0.25 0.25 18 | 0.95 0.95 0.00 19 | 0.25 0.25 0.75 20 | 0.75 0.75 0.75 21 | 0.00 1.00 0.00 22 | 0.76 0.57 0.17 23 | 0.54 0.63 0.22 24 | 0.34 0.57 0.92 25 | 1.00 0.10 0.60 26 | 0.88 0.75 0.73 27 | 0.10 0.49 0.47 28 | 0.66 0.34 0.65 29 | 0.99 0.41 0.23]; 30 | 31 | n = max(size(comps)); 32 | if n < 100 33 | col = repmat(col,[ceil(n/20),1]); 34 | else 35 | col = rand(n,3); 36 | end 37 | 38 | S = comps{1}; 39 | if iscell(S) 40 | n = size(comps,1); 41 | for i = 1:n 42 | S = comps{i}; 43 | if ~isempty(S) 44 | S = vertcat(S{:}); 45 | comps{i} = S; 46 | else 47 | comps{i} = zeros(0,1); 48 | end 49 | end 50 | end 51 | 52 | 53 | if nargin == 4 54 | 55 | % Plot the segments 56 | figure(fig) 57 | C = comps{1}; 58 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(1,:),'Markersize',ms) 59 | hold on 60 | for i = 2:n 61 | C = comps{i}; 62 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(i,:),'Markersize',ms) 63 | end 64 | axis equal 65 | hold off 66 | pause(0.1) 67 | 68 | else 69 | 70 | np = size(P,1); 71 | D = false(np,1); 72 | C = unique(vertcat(Bal{comps{1}})); 73 | figure(fig) 74 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(1,:),'Markersize',ms) 75 | hold on 76 | for i = 2:n 77 | if ~isempty(comps{i}) 78 | C = unique(vertcat(Bal{comps{i}})); 79 | I = D(C); 80 | C = C(~I); 81 | D(C) = true; 82 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(i,:),'Markersize',ms) 83 | end 84 | end 85 | hold off 86 | axis equal 87 | pause(0.1) 88 | end -------------------------------------------------------------------------------- /src/plotting/plot_spreads.m: -------------------------------------------------------------------------------- 1 | function plot_spreads(treedata,fig,lw,rel) 2 | 3 | % Plots the spreads as a polar plot with different height layers presented 4 | % with different colors. Inputs "fig" and "lw" define the figure number and 5 | % the line width. Input Rel = 1 specifies relative spreads, i.e. the 6 | % maximum spread is one, otherwise use the actual values. 7 | 8 | if nargin == 2 9 | lw = 1; 10 | rel = 1; 11 | elseif nargin == 3 12 | rel = 1; 13 | end 14 | 15 | spreads = treedata.spreads; 16 | figure(fig) 17 | n = size(spreads,1); 18 | col = zeros(n,3); 19 | col(:,1) = (0:1/n:(n-1)/n)'; 20 | col(:,3) = (1:-1/n:1/n)'; 21 | d = max(max(spreads)); 22 | D = [spreads(1,end) spreads(1,:)]; 23 | if rel 24 | polarplot(D/d,'-','Color',col(1,:),'Linewidth',lw) 25 | else 26 | polarplot(D,'-','Color',col(1,:),'Linewidth',lw) 27 | end 28 | hold on 29 | for i = 1:n 30 | D = [spreads(i,end) spreads(i,:)]; 31 | if rel 32 | polarplot(D/d,'-','Color',col(i,:),'Linewidth',lw) 33 | else 34 | polarplot(D,'-','Color',col(i,:),'Linewidth',lw) 35 | end 36 | end 37 | hold off 38 | if rel 39 | rlim([0 1]) 40 | else 41 | rlim([0 d]) 42 | end 43 | -------------------------------------------------------------------------------- /src/plotting/plot_tree_structure.m: -------------------------------------------------------------------------------- 1 | function plot_tree_structure(P,cover,segment,fig,ms,segind,BO) 2 | 3 | % --------------------------------------------------------------------- 4 | % PLOT_TREE_STRUCTURE.M Plots branch-segmented point cloud with unique 5 | % color for each branching order 6 | % 7 | % Version 1.1.0 8 | % Latest update 13 July 2020 9 | % 10 | % Copyright (C) 2013-2020 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % Blue = trunk, Green = 1st-order branches, Red = 2nd-order branches, etc. 14 | % If segind = 1 and BO = 0, then plots the stem. If segind = 1 and BO = 1, 15 | % then plots the stem and the 1st-order branches. If segind = 1 and 16 | % BO >= maximum branching order or BO input is not given, then plots the 17 | % whole tree. If segind = 2 and BO is not given or it is high enough, then 18 | % plots the branch whose index is 2 and all its sub-branches. 19 | % 20 | % Inputs 21 | % P Point cloud 22 | % cover Cover sets structure 23 | % Segs Segments structure 24 | % fig Figure number 25 | % ms Marker size 26 | % segind Index of the segment where the plotting of tree structure 27 | % starts. 28 | % BO How many branching orders are plotted. 0 = stem, 1 = 1st order, etc 29 | % 30 | 31 | % Changes from version 1.0.0 to 1.1.0, 13 July 2020: 32 | % 1) Added option for choosing the coloring based either on branch order or 33 | % unique color for each branch 34 | 35 | n = nargin; 36 | if n < 7 37 | BO = 1000; 38 | if n < 6 39 | segind = 1; 40 | if n < 5 41 | ms = 1; 42 | if n == 3 43 | fig = 1; 44 | end 45 | end 46 | end 47 | end 48 | 49 | Bal = cover.ball; 50 | Segs = segment.segments; 51 | SChi = segment.ChildSegment; 52 | 53 | col = [ 54 | 0.00 0.00 1.00 55 | 0.00 0.50 0.00 56 | 1.00 0.00 0.00 57 | 0.00 0.75 0.75 58 | 0.75 0.00 0.75 59 | 0.75 0.75 0.00 60 | 0.25 0.25 0.25 61 | 0.75 0.25 0.25 62 | 0.95 0.95 0.00 63 | 0.25 0.25 0.75 64 | 0.75 0.75 0.75 65 | 0.00 1.00 0.00 66 | 0.76 0.57 0.17 67 | 0.54 0.63 0.22 68 | 0.34 0.57 0.92 69 | 1.00 0.10 0.60 70 | 0.88 0.75 0.73 71 | 0.10 0.49 0.47 72 | 0.66 0.34 0.65 73 | 0.99 0.41 0.23]; 74 | col = repmat(col,[1000,1]); 75 | 76 | if iscell(Segs{1}) 77 | n = max(size(Segs)); 78 | Seg = cell(n,1); 79 | for i = 1:n 80 | m = size(Segs{i},1); 81 | S = zeros(0); 82 | for j = 1:m 83 | s = Segs{i}(j); 84 | s = s{:}; 85 | S = [S; s]; 86 | end 87 | Seg{i} = S; 88 | end 89 | else 90 | Seg = Segs; 91 | end 92 | 93 | S = vertcat(Bal{Seg{segind}}); 94 | figure(fig) 95 | plot3(P(S,1),P(S,2),P(S,3),'.','Color',col(1,:),'Markersize',ms) 96 | axis equal 97 | %forb = S; 98 | if BO > 0 99 | hold on 100 | c = SChi{segind}; 101 | order = 1; 102 | while (order <= BO) && (~isempty(c)) 103 | C = vertcat(Bal{vertcat(Seg{c})}); 104 | %C = setdiff(C,forb); 105 | figure(fig) 106 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(order+1,:),'Markersize',ms) 107 | axis equal 108 | c = unique(vertcat(SChi{c})); 109 | order = order+1; 110 | %forb = union(forb,C); 111 | end 112 | hold off 113 | end 114 | -------------------------------------------------------------------------------- /src/plotting/plot_tree_structure2.m: -------------------------------------------------------------------------------- 1 | function plot_tree_structure2(P,Bal,Segs,SChi,fig,ms,BO,segind) 2 | 3 | % Plots the branch-segmented tree point cloud so that each branching order 4 | % has its own color Blue = trunk, green = 1st-order branches, 5 | % red = 2nd-order branches, etc. 6 | % 7 | % Inputs 8 | % P Point cloud 9 | % Bal Cover sets, Bal = cover.bal 10 | % Segs Segments, Segs = segment.segments 11 | % SChi Child segments, SChi = segment.ChildSegment 12 | % fig Figure number 13 | % ms Marker size 14 | % BO How many branching orders are plotted. 0 = all orders 15 | % segind Index of the segment where the plotting of tree structure 16 | % starts. If segnum = 1 and BO = 0, then plots the whole 17 | % tree. If segnum = 1 and B0 = 2, then plots the stem and 18 | % the 1st-order branches. If segnum = 2 and BO = 0, then 19 | % plots the branch whose index is 2 and all its sub-branches. 20 | 21 | 22 | col = [ 23 | 0.00 0.00 1.00 24 | 0.00 0.50 0.00 25 | 1.00 0.00 0.00 26 | 0.00 0.75 0.75 27 | 0.75 0.00 0.75 28 | 0.75 0.75 0.00 29 | 0.25 0.25 0.25 30 | 0.75 0.25 0.25 31 | 0.95 0.95 0.00 32 | 0.25 0.25 0.75 33 | 0.75 0.75 0.75 34 | 0.00 1.00 0.00 35 | 0.76 0.57 0.17 36 | 0.54 0.63 0.22 37 | 0.34 0.57 0.92 38 | 1.00 0.10 0.60 39 | 0.88 0.75 0.73 40 | 0.10 0.49 0.47 41 | 0.66 0.34 0.65 42 | 0.99 0.41 0.23]; 43 | col = repmat(col,[1000,1]); 44 | 45 | if iscell(Segs{1}) 46 | n = max(size(Segs)); 47 | Seg = cell(n,1); 48 | for i = 1:n 49 | m = size(Segs{i},1); 50 | S = zeros(0); 51 | for j = 1:m 52 | s = Segs{i}(j); 53 | s = s{:}; 54 | S = [S; s]; 55 | end 56 | Seg{i} = S; 57 | end 58 | else 59 | Seg = Segs; 60 | end 61 | 62 | if BO == 0 63 | BO = 1000; 64 | end 65 | 66 | S = vertcat(Bal{Seg{segind}}); 67 | figure(fig) 68 | plot3(P(S,1),P(S,2),P(S,3),'.','Color',col(1,:),'Markersize',ms) 69 | axis equal 70 | forb = S; 71 | if BO > 1 72 | %pause 73 | hold on 74 | c = SChi{segind}; 75 | i = 2; 76 | while (i <= BO) && (~isempty(c)) 77 | C = vertcat(Bal{unique(vertcat(Seg{c}))}); 78 | C = setdiff(C,forb); 79 | figure(fig) 80 | plot3(P(C,1),P(C,2),P(C,3),'.','Color',col(i,:),'Markersize',ms) 81 | axis equal 82 | c = unique(vertcat(SChi{c})); 83 | i = i+1; 84 | forb = union(forb,C); 85 | if i <= BO 86 | %pause 87 | end 88 | end 89 | hold off 90 | end 91 | -------------------------------------------------------------------------------- /src/plotting/plot_triangulation.m: -------------------------------------------------------------------------------- 1 | function plot_triangulation(QSM,fig,nf,AllTree) 2 | 3 | % Plots the triangulation model of the stem's bottom part and the cylinder 4 | % model (rest of the stem or the rest of the tree). The optional inputs 5 | % "fig", "nf", "All" are the figure number, number of facets for the 6 | % cylinders, and if All = 1, then all the tree is plotted. 7 | 8 | n = nargin; 9 | if n < 4 10 | AllTree = 0; 11 | if n < 3 12 | nf = 20; 13 | if n == 1 14 | fig = 1; 15 | end 16 | end 17 | end 18 | 19 | Vert = double(QSM.triangulation.vert); 20 | Facets = double(QSM.triangulation.facet); 21 | CylInd = QSM.triangulation.cylind; 22 | fvd = QSM.triangulation.fvd; 23 | Bran = QSM.cylinder.branch; 24 | nc = size(Bran,1); 25 | ind = (1:1:nc)'; 26 | C = ind(Bran == 1); 27 | figure(fig) 28 | patch('Vertices',Vert,'Faces',Facets,'FaceVertexCData',fvd,'FaceColor','flat') 29 | hold on 30 | if AllTree 31 | Ind = (CylInd:1:nc)'; 32 | else 33 | Ind = (CylInd:1:C(end))'; 34 | end 35 | plot_cylinder_model(QSM.cylinder,fig,nf,1,'branch',Ind) 36 | axis equal 37 | hold off 38 | alpha(1) 39 | -------------------------------------------------------------------------------- /src/plotting/point_cloud_plotting.m: -------------------------------------------------------------------------------- 1 | function point_cloud_plotting(P,fig,ms,Bal,Sub) 2 | 3 | % Plots the given point cloud "P". With additional inputs one can plot only 4 | % those points that are included in the cover sets "Bal" or in the 5 | % subcollection "Sub" of the cover sets. 6 | % "fig" and "ms" are the figure number and marker size. 7 | 8 | if nargin == 2 9 | ms = 3; 10 | elseif ms == 0 11 | ms = 3; 12 | end 13 | 14 | if nargin < 4 15 | figure(fig) 16 | if size(P,2) == 3 17 | plot3(P(:,1),P(:,2),P(:,3),'.b','Markersize',ms) 18 | elseif size(P,2) == 2 19 | plot(P(:,1),P(:,2),'.b','Markersize',ms) 20 | end 21 | axis equal 22 | 23 | elseif nargin == 4 24 | I = vertcat(Bal{:}); 25 | figure(fig) 26 | plot3(P(I,1),P(I,2),P(I,3),'.b','Markersize',ms) 27 | axis equal 28 | 29 | else 30 | if iscell(Sub) 31 | S = vertcat(Sub{:}); 32 | Sub = vertcat(S{:}); 33 | I = vertcat(Bal{Sub}); 34 | figure(fig) 35 | plot3(P(I,1),P(I,2),P(I,3),'.b','Markersize',ms) 36 | axis equal 37 | else 38 | I = vertcat(Bal{Sub}); 39 | figure(fig) 40 | plot3(P(I,1),P(I,2),P(I,3),'.b','Markersize',ms) 41 | axis equal 42 | end 43 | end -------------------------------------------------------------------------------- /src/results/qsm.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InverseTampere/TreeQSM/6630bbf516f8b53adb7d60a2cccbd21e6fe51226/src/results/qsm.mat -------------------------------------------------------------------------------- /src/tools/average.m: -------------------------------------------------------------------------------- 1 | function A = average(X) 2 | 3 | % Computes the average of columns of the matrix X 4 | 5 | n = size(X,1); 6 | if n > 1 7 | A = sum(X)/n; 8 | else 9 | A = X; 10 | end -------------------------------------------------------------------------------- /src/tools/change_precision.m: -------------------------------------------------------------------------------- 1 | function v = change_precision(v) 2 | 3 | % Decrease the number of nonzero decimals in the vector v according to the 4 | % exponent of the number for displaying and writing. 5 | 6 | n = length(v); 7 | for i = 1:n 8 | if abs(v(i)) >= 1e3 9 | v(i) = round(v(i)); 10 | elseif abs(v(i)) >= 1e2 11 | v(i) = round(10*v(i))/10; 12 | elseif abs(v(i)) >= 1e1 13 | v(i) = round(100*v(i))/100; 14 | elseif abs(v(i)) >= 1e0 15 | v(i) = round(1000*v(i))/1000; 16 | elseif abs(v(i)) >= 1e-1 17 | v(i) = round(10000*v(i))/10000; 18 | else 19 | v(i) = round(100000*v(i))/100000; 20 | end 21 | end -------------------------------------------------------------------------------- /src/tools/connected_components.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [Components,CompSize] = connected_components(Nei,Sub,MinSize,Fal) 17 | 18 | % --------------------------------------------------------------------- 19 | % CONNECTED_COMPONENTS.M Determines the connected components of cover 20 | % sets using their neighbour-relation 21 | % 22 | % Version 1.1 23 | % Latest update 16 Aug 2017 24 | % 25 | % Copyright (C) 2013-2017 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | 28 | % Determines connected components of the subset of cover sets defined 29 | % by "Sub" such that each component has at least "MinSize" 30 | % number of cover sets. 31 | % 32 | % Inputs: 33 | % Nei Neighboring cover sets of each cover set, (n_sets x 1)-cell 34 | % Sub Subset whose components are determined, 35 | % length(Sub) < 2 means no subset and thus the whole point cloud 36 | % "Sub" may be also a vector of cover set indexes in the subset 37 | % or a logical (n_sets)-vector, where n_sets is the number of 38 | % all cover sets 39 | % MinSize Minimum number of cover sets in an acceptable component 40 | % Fal Logical false vector for the cover sets 41 | % 42 | % Outputs: 43 | % Components Connected components, (n_comp x 1)-cell 44 | % CompSize Number of sets in the components, (n_comp x 1)-vector 45 | 46 | if length(Sub) <= 3 && ~islogical(Sub) && Sub(1) > 0 47 | % Very small subset, i.e. at most 3 cover sets 48 | n = length(Sub); 49 | if n == 1 50 | Components = cell(1,1); 51 | Components{1} = uint32(Sub); 52 | CompSize = 1; 53 | elseif n == 2 54 | I = Nei{Sub(1)} == Sub(2); 55 | if any(I) 56 | Components = cell(1,1); 57 | Components{1} = uint32((Sub)); 58 | CompSize = 1; 59 | else 60 | Components = cell(2,1); 61 | Components{1} = uint32(Sub(1)); 62 | Components{2} = uint32(Sub(2)); 63 | CompSize = [1 1]; 64 | end 65 | elseif n == 3 66 | I = Nei{Sub(1)} == Sub(2); 67 | J = Nei{Sub(1)} == Sub(3); 68 | K = Nei{Sub(2)} == Sub(3); 69 | if any(I)+any(J)+any(K) >= 2 70 | Components = cell(1,1); 71 | Components{1} = uint32(Sub); 72 | CompSize = 1; 73 | elseif any(I) 74 | Components = cell(2,1); 75 | Components{1} = uint32(Sub(1:2)); 76 | Components{2} = uint32(Sub(3)); 77 | CompSize = [2 1]; 78 | elseif any(J) 79 | Components = cell(2,1); 80 | Components{1} = uint32(Sub([1 3])); 81 | Components{2} = uint32(Sub(2)); 82 | CompSize = [2 1]; 83 | elseif any(K) 84 | Components = cell(2,1); 85 | Components{1} = uint32(Sub(2:3)); 86 | Components{2} = uint32(Sub(1)); 87 | CompSize = [2 1]; 88 | else 89 | Components = cell(3,1); 90 | Components{1} = uint32(Sub(1)); 91 | Components{2} = uint32(Sub(2)); 92 | Components{3} = uint32(Sub(3)); 93 | CompSize = [1 1 1]; 94 | end 95 | end 96 | 97 | elseif any(Sub) || (length(Sub) == 1 && Sub(1) == 0) 98 | nb = size(Nei,1); 99 | if nargin == 3 100 | Fal = false(nb,1); 101 | end 102 | if length(Sub) == 1 && Sub == 0 103 | % All the cover sets 104 | ns = nb; 105 | if nargin == 3 106 | Sub = true(nb,1); 107 | else 108 | Sub = ~Fal; 109 | end 110 | elseif ~islogical(Sub) 111 | % Subset of cover sets 112 | ns = length(Sub); 113 | if nargin == 3 114 | sub = false(nb,1); 115 | else 116 | sub = Fal; 117 | end 118 | sub(Sub) = true; 119 | Sub = sub; 120 | else 121 | % Subset of cover sets 122 | ns = nnz(Sub); 123 | end 124 | 125 | Components = cell(ns,1); 126 | CompSize = zeros(ns,1,'uint32'); 127 | nc = 0; % number of components found 128 | m = 1; 129 | while ~Sub(m) 130 | m = m+1; 131 | end 132 | i = 0; 133 | Comp = zeros(ns,1,'uint32'); 134 | while i < ns 135 | Add = Nei{m}; 136 | I = Sub(Add); 137 | Add = Add(I); 138 | a = length(Add); 139 | Comp(1) = m; 140 | Sub(m) = false; 141 | t = 1; 142 | while a > 0 143 | Comp(t+1:t+a) = Add; 144 | Sub(Add) = false; 145 | t = t+a; 146 | Add = vertcat(Nei{Add}); 147 | I = Sub(Add); 148 | Add = Add(I); 149 | % select the unique elements of Add: 150 | n = length(Add); 151 | if n > 2 152 | I = true(n,1); 153 | for j = 1:n 154 | if ~Fal(Add(j)) 155 | Fal(Add(j)) = true; 156 | else 157 | I(j) = false; 158 | end 159 | end 160 | Fal(Add) = false; 161 | Add = Add(I); 162 | elseif n == 2 163 | if Add(1) == Add(2) 164 | Add = Add(1); 165 | end 166 | end 167 | a = length(Add); 168 | end 169 | i = i+t; 170 | if t >= MinSize 171 | nc = nc+1; 172 | Components{nc} = uint32(Comp(1:t)); 173 | CompSize(nc) = t; 174 | end 175 | if i < ns 176 | while m <= nb && Sub(m) == false 177 | m = m+1; 178 | end 179 | end 180 | end 181 | Components = Components(1:nc); 182 | CompSize = CompSize(1:nc); 183 | else 184 | Components = cell(0,1); 185 | CompSize = 0; 186 | end -------------------------------------------------------------------------------- /src/tools/cross_product.m: -------------------------------------------------------------------------------- 1 | function C = cross_product(A,B) 2 | 3 | % Calculates the cross product C of the 3-vectors A and B 4 | 5 | C = [A(2)*B(3)-A(3)*B(2); A(3)*B(1)-A(1)*B(3); A(1)*B(2)-A(2)*B(1)]; -------------------------------------------------------------------------------- /src/tools/cubical_averaging.m: -------------------------------------------------------------------------------- 1 | function DSP = cubical_averaging(P,CubeSize) 2 | 3 | tic 4 | % Downsamples the given point cloud by averaging points from each 5 | % cube of side length CubeSize. 6 | 7 | % The vertices of the big cube containing P 8 | Min = double(min(P)); 9 | Max = double(max(P)); 10 | 11 | % Number of cubes with edge length "EdgeLength" in the sides 12 | % of the big cube 13 | N = double(ceil((Max-Min)/CubeSize)+1); 14 | 15 | CubeCoord = floor([P(:,1)-Min(1) P(:,2)-Min(2) P(:,3)-Min(3)]/CubeSize)+1; 16 | 17 | % Sorts the points according a lexicographical order 18 | LexOrd = [CubeCoord(:,1) CubeCoord(:,2)-1 CubeCoord(:,3)-1]*[1 N(1) N(1)*N(2)]'; 19 | [LexOrd,SortOrd] = sort(LexOrd); 20 | nc = size(unique(LexOrd),1); % number of points in the downsampled point cloud 21 | np = size(P,1); % number of points 22 | DSP = zeros(nc,3); % Downsampled point cloud 23 | p = 1; % The index of the point under comparison 24 | q = 0; 25 | while p <= np 26 | t = 1; 27 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 28 | t = t+1; 29 | end 30 | q = q+1; 31 | DSP(q,:) = average(P(SortOrd(p:p+t-1),:)); 32 | p = p+t; 33 | end 34 | toc 35 | 36 | disp([' Points before: ',num2str(np)]) 37 | disp([' Filtered points: ',num2str(np-nc)]) 38 | disp([' Points left: ',num2str(nc)]); 39 | -------------------------------------------------------------------------------- /src/tools/cubical_downsampling.m: -------------------------------------------------------------------------------- 1 | function Pass = cubical_downsampling(P,CubeSize) 2 | 3 | % Downsamples the given point cloud by selecting one point from each 4 | % cube of side length "CubeSize". 5 | 6 | % The vertices of the big cube containing P 7 | Min = double(min(P)); 8 | Max = double(max(P)); 9 | 10 | % Number of cubes with edge length "EdgeLength" in the sides 11 | % of the big cube 12 | N = double(ceil((Max-Min)/CubeSize)+1); 13 | 14 | % Process the data in 1e7-point blocks to consume much less memory 15 | np = size(P,1); 16 | m = 1e7; 17 | if np < m 18 | m = np; 19 | end 20 | nblocks = ceil(np/m); % number of blocks 21 | 22 | % Downsample 23 | R = cell(nblocks,1); 24 | p = 1; 25 | for i = 1:nblocks 26 | if i < nblocks 27 | % Compute the cube coordinates of the points 28 | C = floor([double(P(p:p+m-1,1))-Min(1) double(P(p:p+m-1,2))-Min(2)... 29 | double(P(p:p+m-1,3))-Min(3)]/CubeSize)+1; 30 | % Compute the lexicographical order of the cubes 31 | S = [C(:,1) C(:,2)-1 C(:,3)-1]*[1 N(1) N(1)*N(2)]'; 32 | [S,I] = unique(S); % Select the unique cubes 33 | J = (p:1:p+m-1)'; 34 | J = J(I); 35 | R{i} = [S J]; 36 | else 37 | C = floor([double(P(p:end,1))-Min(1) double(P(p:end,2))-Min(2)... 38 | double(P(p:end,3))-Min(3)]/CubeSize)+1; 39 | S = [C(:,1) C(:,2)-1 C(:,3)-1]*[1 N(1) N(1)*N(2)]'; 40 | [S,I] = unique(S); 41 | J = (p:1:np)'; 42 | J = J(I); 43 | R{i} = [S J]; 44 | end 45 | p = p+m; 46 | end 47 | % Select the unique cubes and their points 48 | R = vertcat(R{:}); 49 | [~,I] = unique(R(:,1)); 50 | Pass = false(np,1); 51 | Pass(R(I,2)) = true; 52 | -------------------------------------------------------------------------------- /src/tools/cubical_partition.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [Partition,CubeCoord,Info,Cubes] = cubical_partition(P,EL,NE) 17 | 18 | % --------------------------------------------------------------------- 19 | % CUBICAL_PARTITION.M Partitions the point cloud into cubes. 20 | % 21 | % Version 1.1.0 22 | % Latest update 6 Oct 2021 23 | % 24 | % Copyright (C) 2015-2021 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | 27 | % Inputs: 28 | % P Point cloud, (n_points x 3)-matrix 29 | % EL Length of the cube edges 30 | % NE Number of empty edge layers 31 | % 32 | % Outputs: 33 | % Partition Point cloud partitioned into cubical cells, 34 | % (nx x ny x nz)-cell, where nx,ny,nz are the number 35 | % of cubes in x,y,z-directions, respectively. If "Cubes" 36 | % is outputed, then "Partition" is (n x 1)-cell, where each 37 | % cell corresponds to a nonempty cube. 38 | % 39 | % CC (n_points x 3)-matrix whose rows are the cube coordinates 40 | % of each point: x,y,z-coordinates 41 | % Info The minimum coordinate values and number of cubes in each 42 | % coordinate direction 43 | % Cubes (Optional) (nx x ny x nz)-matrix (array), each nonzero 44 | % element indicates that its cube is nonempty and the 45 | % number indicates which cell in "Partition" contains the 46 | % points of the cube. 47 | % --------------------------------------------------------------------- 48 | 49 | % Changes from version 1.0.0 to 1.1.0, 6 Oct 2021: 50 | % 1) Changed the determinationa EL and NE so that the while loop don't 51 | % continue endlessly in some cases 52 | 53 | if nargin == 2 54 | NE = 3; 55 | end 56 | 57 | % The vertices of the big cube containing P 58 | Min = double(min(P)); 59 | Max = double(max(P)); 60 | 61 | % Number of cubes with edge length "EdgeLength" in the sides 62 | % of the big cube 63 | N = double(ceil((Max-Min)/EL)+2*NE+1); 64 | t = 0; 65 | while t < 10 && 8*N(1)*N(2)*N(3) > 4e9 66 | t = t+1; 67 | EL = 1.1*EL; 68 | N = double(ceil((Max-Min)/EL)+2*NE+1); 69 | end 70 | if 8*N(1)*N(2)*N(3) > 4e9 71 | NE = 3; 72 | N = double(ceil((Max-Min)/EL)+2*NE+1); 73 | end 74 | Info = [Min N EL NE]; 75 | 76 | % Calculates the cube-coordinates of the points 77 | CubeCoord = floor([P(:,1)-Min(1) P(:,2)-Min(2) P(:,3)-Min(3)]/EL)+NE+1; 78 | 79 | % Sorts the points according a lexicographical order 80 | LexOrd = [CubeCoord(:,1) CubeCoord(:,2)-1 CubeCoord(:,3)-1]*[1 N(1) N(1)*N(2)]'; 81 | CubeCoord = uint16(CubeCoord); 82 | [LexOrd,SortOrd] = sort(LexOrd); 83 | SortOrd = uint32(SortOrd); 84 | LexOrd = uint32(LexOrd); 85 | 86 | if nargout <= 3 87 | % Define "Partition" 88 | Partition = cell(N(1),N(2),N(3)); 89 | np = size(P,1); % number of points 90 | p = 1; % The index of the point under comparison 91 | while p <= np 92 | t = 1; 93 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 94 | t = t+1; 95 | end 96 | q = SortOrd(p); 97 | Partition{CubeCoord(q,1),CubeCoord(q,2),CubeCoord(q,3)} = SortOrd(p:p+t-1); 98 | p = p+t; 99 | end 100 | 101 | else 102 | nc = size(unique(LexOrd),1); 103 | 104 | % Define "Partition" 105 | Cubes = zeros(N(1),N(2),N(3),'uint32'); 106 | Partition = cell(nc,1); 107 | np = size(P,1); % number of points 108 | p = 1; % The index of the point under comparison 109 | c = 0; 110 | while p <= np 111 | t = 1; 112 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 113 | t = t+1; 114 | end 115 | q = SortOrd(p); 116 | c = c+1; 117 | Partition{c,1} = SortOrd(p:p+t-1); 118 | Cubes(CubeCoord(q,1),CubeCoord(q,2),CubeCoord(q,3)) = c; 119 | p = p+t; 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /src/tools/define_input.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function inputs = define_input(Clouds,nPD1,nPD2Min,nPD2Max) 17 | 18 | % --------------------------------------------------------------------- 19 | % DEFINE_INPUT.M Defines the required inputs (PatchDiam and BallRad 20 | % parameters) for TreeQSM based in estimated tree 21 | % radius. 22 | % 23 | % Version 1.0.0 24 | % Latest update 4 May 2022 25 | % 26 | % Copyright (C) 2013-2022 Pasi Raumonen 27 | % --------------------------------------------------------------------- 28 | 29 | % Takes in a single tree point clouds, that preferably contains only points 30 | % from the tree and not e.g. from groung. User defines the number of 31 | % PatchDiam1, PatchDiam2Min, PatchDiam2Max parameter values needed. Then 32 | % the code estimates automatically these parameter values based on the 33 | % tree stem radius and tree height. Thus this code can be used to generate 34 | % the inputs needed for QSM reconstruction with TreeQSM. 35 | % 36 | % Inputs: 37 | % P Point cloud of a tree OR string specifying the name of the .mat 38 | % file where multiple point clouds are saved 39 | % nPD1 Number of parameter values estimated for PatchDiam1 40 | % nPD2Min Number of parameter values estimated for PatchDiam2Min 41 | % nPD2Max Number of parameter values estimated for PatchDiam2Max 42 | % 43 | % Output: 44 | % inputs Input structure with the estimated parameter values 45 | % --------------------------------------------------------------------- 46 | 47 | 48 | % Create inputs-structure 49 | create_input 50 | Inputs = inputs; 51 | 52 | % If given multiple clouds, extract the names 53 | if ischar(Clouds) || isstring(Clouds) 54 | matobj = matfile([Clouds,'.mat']); 55 | names = fieldnames(matobj); 56 | i = 1; 57 | n = max(size(names)); 58 | while i <= n && ~strcmp(names{i,:},'Properties') 59 | i = i+1; 60 | end 61 | I = (1:1:n); 62 | I = setdiff(I,i); 63 | names = names(I,1); 64 | names = sort(names); 65 | nt = max(size(names)); % number of trees/point clouds 66 | else 67 | P = Clouds; 68 | nt = 1; 69 | end 70 | inputs(nt).PatchDiam1 = 0; 71 | 72 | 73 | %% Estimate the PatchDiam and BallRad parameters 74 | for i = 1:nt 75 | if nt > 1 76 | % Select point cloud 77 | P = matobj.(names{i}); 78 | inputs(i) = Inputs; 79 | inputs(i).name = names{i}; 80 | inputs(i).tree = i; 81 | inputs(i).plot = 0; 82 | inputs(i).savetxt = 0; 83 | inputs(i).savemat = 0; 84 | inputs(i).disp = 0; 85 | end 86 | 87 | %% Estimate the stem diameter close to bottom 88 | % Define height 89 | Hb = min(P(:,3)); 90 | Ht = max(P(:,3)); 91 | TreeHeight = double(Ht-Hb); 92 | Hei = P(:,3)-Hb; 93 | 94 | % Select a section (0.02-0.1*tree_height) from the bottom of the tree 95 | hSecTop = min(4,0.1*TreeHeight); 96 | hSecBot = 0.02*TreeHeight; 97 | hSec = hSecTop-hSecBot; 98 | Sec = Hei > hSecBot & Hei < hSecTop; 99 | StemBot = P(Sec,1:3); 100 | 101 | % Estimate stem axis (point and direction) 102 | AxisPoint = mean(StemBot); 103 | V = StemBot-AxisPoint; 104 | V = normalize(V); 105 | AxisDir = optimal_parallel_vector(V); 106 | 107 | % Estimate stem diameter 108 | d = distances_to_line(StemBot,AxisDir,AxisPoint); 109 | Rstem = double(median(d)); 110 | 111 | % Point resolution (distance between points) 112 | Res = sqrt((2*pi*Rstem*hSec)/size(StemBot,1)); 113 | 114 | %% Define the PatchDiam parameters 115 | % PatchDiam1 is around stem radius divided by 3. 116 | pd1 = Rstem/3;%*max(1,TreeHeight/20); 117 | if nPD1 == 1 118 | inputs(i).PatchDiam1 = pd1; 119 | else 120 | n = nPD1; 121 | inputs(i).PatchDiam1 = linspace((0.90-(n-2)*0.1)*pd1,(1.10+(n-2)*0.1)*pd1,n); 122 | end 123 | 124 | % PatchDiam2Min is around stem radius divided by 6 and increased for 125 | % over 20 m heigh trees. 126 | pd2 = Rstem/6*min(1,20/TreeHeight); 127 | if nPD2Min == 1 128 | inputs(i).PatchDiam2Min = pd2; 129 | else 130 | n = nPD2Min; 131 | inputs(i).PatchDiam2Min = linspace((0.90-(n-2)*0.1)*pd2,(1.10+(n-2)*0.1)*pd2,n); 132 | end 133 | 134 | % PatchDiam2Max is around stem radius divided by 2.5. 135 | pd3 = Rstem/2.5;%*max(1,TreeHeight/20); 136 | if nPD2Max == 1 137 | inputs(i).PatchDiam2Max = pd3; 138 | else 139 | n = nPD2Max; 140 | inputs(i).PatchDiam2Max = linspace((0.90-(n-2)*0.1)*pd3,(1.10+(n-2)*0.1)*pd3,n); 141 | end 142 | 143 | % Define the BallRad parameters: 144 | inputs(i).BallRad1 = max([inputs(i).PatchDiam1+1.5*Res; 145 | min(1.25*inputs(i).PatchDiam1,inputs(i).PatchDiam1+0.025)]); 146 | inputs(i).BallRad2 = max([inputs(i).PatchDiam2Max+1.25*Res; 147 | min(1.2*inputs(i).PatchDiam2Max,inputs(i).PatchDiam2Max+0.025)]); 148 | 149 | %plot_point_cloud(P,1,1) 150 | end 151 | -------------------------------------------------------------------------------- /src/tools/dimensions.m: -------------------------------------------------------------------------------- 1 | function [D,dir] = dimensions(points,varargin) 2 | 3 | % Calculates the box-dimensions and dimension estimates of the point set 4 | % "points". Returns also the corresponding direction vectors. 5 | 6 | 7 | if nargin == 2 8 | P = varargin{1}; 9 | points = P(points,:); 10 | elseif nargin == 3 11 | P = varargin{1}; 12 | Bal = varargin{2}; 13 | I = vertcat(Bal{points}); 14 | points = P(I,:); 15 | end 16 | 17 | if size(points,2) == 3 18 | X = cov(points); 19 | [U,S,~] = svd(X); 20 | 21 | dp1 = points*U(:,1); 22 | dp2 = points*U(:,2); 23 | dp3 = points*U(:,3); 24 | 25 | D = [max(dp1)-min(dp1) max(dp2)-min(dp2) max(dp3)-min(dp3) ... 26 | (S(1,1)-S(2,2))/S(1,1) (S(2,2)-S(3,3))/S(1,1) S(3,3)/S(1,1)]; 27 | 28 | dir = [U(:,1)' U(:,2)' U(:,3)']; 29 | else 30 | X = cov(points); 31 | [U,S,~] = svd(X); 32 | 33 | dp1 = points*U(:,1); 34 | dp2 = points*U(:,2); 35 | 36 | D = [max(dp1)-min(dp1) max(dp2)-min(dp2) ... 37 | (S(1,1)-S(2,2))/S(1,1) S(2,2)/S(1,1)]; 38 | 39 | dir = [U(:,1)' U(:,2)']; 40 | end 41 | -------------------------------------------------------------------------------- /src/tools/display_time.m: -------------------------------------------------------------------------------- 1 | function display_time(T1,T2,string,display) 2 | 3 | % Display the two times given. "T1" is the time named with the "string" and 4 | % "T2" is named "Total". 5 | 6 | % Changes 12 Mar 2018: moved the if statement with display from the end to 7 | % the beginning 8 | 9 | if display 10 | [tmin,tsec] = sec2min(T1); 11 | [Tmin,Tsec] = sec2min(T2); 12 | if tmin < 60 && Tmin < 60 13 | if tmin < 1 && Tmin < 1 14 | str = [string,' ',num2str(tsec),' sec. Total: ',num2str(Tsec),' sec']; 15 | elseif tmin < 1 16 | str = [string,' ',num2str(tsec),' sec. Total: ',num2str(Tmin),... 17 | ' min ',num2str(Tsec),' sec']; 18 | else 19 | str = [string,' ',num2str(tmin),' min ',num2str(tsec),... 20 | ' sec. Total: ',num2str(Tmin),' min ',num2str(Tsec),' sec']; 21 | end 22 | elseif tmin < 60 23 | Thour = floor(Tmin/60); 24 | Tmin = Tmin-Thour*60; 25 | str = [string,' ',num2str(tmin),' min ',num2str(tsec),... 26 | ' sec. Total: ',num2str(Thour),' hours ',num2str(Tmin),' min']; 27 | else 28 | thour = floor(tmin/60); 29 | tmin = tmin-thour*60; 30 | Thour = floor(Tmin/60); 31 | Tmin = Tmin-Thour*60; 32 | str = [string,' ',num2str(thour),' hours ',num2str(tmin),... 33 | ' min. Total: ',num2str(Thour),' hours ',num2str(Tmin),' min']; 34 | end 35 | disp(str) 36 | end -------------------------------------------------------------------------------- /src/tools/distances_between_lines.m: -------------------------------------------------------------------------------- 1 | function [DistLines,DistOnRay,DistOnLines] = distances_between_lines(PointRay,DirRay,PointLines,DirLines) 2 | 3 | % Calculates the distances between a ray and lines 4 | 5 | % PointRay A point of the ray 6 | % DirRay Unit direction vector of the line 7 | % PointLines One point of every line 8 | % DirLines Unit direction vectors of the lines 9 | 10 | PointLines = double(PointLines); 11 | PointRay = double(PointRay); 12 | DirLines = double(DirLines); 13 | DirRay = double(DirRay); 14 | 15 | % Calculate unit vectors N orthogonal to the ray and the lines 16 | N = [DirRay(2)*DirLines(:,3)-DirRay(3)*DirLines(:,2) ... 17 | DirRay(3)*DirLines(:,1)-DirRay(1)*DirLines(:,3) ... 18 | DirRay(1)*DirLines(:,2)-DirRay(2)*DirLines(:,1)]; 19 | l = sqrt(sum(N.*N,2)); 20 | N = [1./l.*N(:,1) 1./l.*N(:,2) 1./l.*N(:,3)]; 21 | 22 | % Calculate the distances between the lines 23 | A = -mat_vec_subtraction(PointLines,PointRay); 24 | DistLines = sqrt(abs(sum(A.*N,2))); % distance between lines and the ray 25 | 26 | % Calculate the distances on ray and on lines 27 | b = DirLines*DirRay'; 28 | d = A*DirRay'; 29 | e = sum(A.*DirLines,2); 30 | DistOnRay = (b.*e-d)./(1-b.^2); % Distances to PointRay from the closest points on the ray 31 | DistOnLines = (e-b.*d)./(1-b.^2); % Distances to PointLines from the closest points on the lines 32 | -------------------------------------------------------------------------------- /src/tools/distances_to_line.m: -------------------------------------------------------------------------------- 1 | function [d,V,h,B] = distances_to_line(Q,LineDirec,LinePoint) 2 | 3 | % Calculates the distances of the points, given in the rows of the 4 | % matrix Q, to the line defined by one of its point and its direction. 5 | % "LineDirec" must be a unit (1x3)-vector and LinePoint must be a (1x3)-vector. 6 | % 7 | % Last update 8 Oct 2021 8 | 9 | A = Q-LinePoint; 10 | h = A*LineDirec'; 11 | B = h*LineDirec; 12 | V = A-B; 13 | d = sqrt(sum(V.*V,2)); -------------------------------------------------------------------------------- /src/tools/dot_product.m: -------------------------------------------------------------------------------- 1 | function C = dot_product(A,B) 2 | 3 | % Computes the dot product of the corresponding rows of the matrices A and B 4 | 5 | C = sum(A.*B,2); -------------------------------------------------------------------------------- /src/tools/expand.m: -------------------------------------------------------------------------------- 1 | function C = expand(Nei,C,n,Forb) 2 | 3 | % Expands the given subset "C" of cover sets "n" times with their neighbors, 4 | % and optionally, prevents the expansion into "Forb" sets. "C" is a vector 5 | % and "Forb" can be a number vector or a logical vector. 6 | 7 | if nargin == 3 8 | for i = 1:n 9 | C = union(C,vertcat(Nei{C})); 10 | end 11 | if size(C,2) > 1 12 | C = C'; 13 | end 14 | else 15 | if islogical(Forb) 16 | for i = 1:n 17 | C = union(C,vertcat(Nei{C})); 18 | I = Forb(C); 19 | C = C(~I); 20 | end 21 | else 22 | for i = 1:n 23 | C = union(C,vertcat(Nei{C})); 24 | C = setdiff(C,Forb); 25 | end 26 | end 27 | if size(C,2) > 1 28 | C = C'; 29 | end 30 | end -------------------------------------------------------------------------------- /src/tools/growth_volume_correction.m: -------------------------------------------------------------------------------- 1 | function cylinder = growth_volume_correction(cylinder,inputs) 2 | 3 | % --------------------------------------------------------------------- 4 | % GROWTH_VOLUME_CORRECTION.M Use growth volume allometry approach to 5 | % modify the radius of cylinders. 6 | % 7 | % Version 2.0.0 8 | % Latest update 16 Sep 2021 9 | % 10 | % Copyright (C) 2013-2021 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | % 13 | % Use growth volume (= the total volume "supported by the cylinder") 14 | % allometry approach to modify the radius of too large and too small 15 | % cylinders. Uses the allometry: 16 | % 17 | % Radius = a * GrowthVolume^b + c 18 | % 19 | % If cylinder's radius is over fac-times or under 1/fac-times the radius 20 | % predicted from the growth volume allometry, then correct the radius to 21 | % match the allometry. However, the radius of the cylinders in the branch 22 | % tips are never incresed, only decreased by the correction. More details 23 | % can be from Jan Hackenberg's "SimpleTree" papers and documents. 24 | % --------------------------------------------------------------------- 25 | % Inputs: 26 | % cylinder Structure array that needs to contains the following fields: 27 | % radius (Rad) Radii of the cylinders, vector 28 | % length (Len) Lengths of the cylinders, vector 29 | % parent (CPar) Parents of the cylinders, vector 30 | % inputs.GrowthVolFac The factor "fac", defines the upper and lower 31 | % allowed radius from the predicted one: 32 | % 1/fac*predicted_rad <= rad <= fac*predicted_rad 33 | % --------------------------------------------------------------------- 34 | 35 | % Changes from version 1.0.0 to 2.0.0, 16 Sep 2021: 36 | % 1) Changed the roles of RADIUS and GROWTH_VOLUME in the allometry, i.e. 37 | % the radius is now predicted from the growth volume 38 | % 2) Do not increase the radius of the branch tip cylinders 39 | 40 | disp('----------') 41 | disp('Growth volume based correction of cylinder radii:') 42 | 43 | Rad = double(cylinder.radius); 44 | Rad0 = Rad; 45 | Len = double(cylinder.length); 46 | CPar = cylinder.parent; 47 | CExt = cylinder.extension; 48 | 49 | initial_volume = round(1000*pi*sum(Rad.^2.*Len)); 50 | disp([' Initial_volume (L): ',num2str(initial_volume)]) 51 | 52 | %% Define the child cylinders for each cylinder 53 | n = length(Rad); 54 | CChi = cell(n,1); 55 | ind = (1:1:n)'; 56 | for i = 1:n 57 | CChi{i} = ind(CPar == i); 58 | end 59 | 60 | %% Compute the growth volume 61 | GrowthVol = zeros(n,1); % growth volume 62 | S = cellfun('length',CChi); 63 | modify = S == 0; 64 | GrowthVol(modify) = pi*Rad(modify).^2.*Len(modify); 65 | parents = unique(CPar(modify)); 66 | if parents(1) == 0 67 | parents = parents(2:end); 68 | end 69 | while ~isempty(parents) 70 | V = pi*Rad(parents).^2.*Len(parents); 71 | m = length(parents); 72 | for i = 1:m 73 | GrowthVol(parents(i)) = V(i)+sum(GrowthVol(CChi{parents(i)})); 74 | end 75 | parents = unique(CPar(parents)); 76 | if parents(1) == 0 77 | parents = parents(2:end); 78 | end 79 | end 80 | 81 | %% Fit the allometry: Rad = a*GV^b; 82 | options = optimset('Display','off'); 83 | X = lsqcurvefit(@allometry,[0.5 0.5 0],GrowthVol,Rad,[],[],options); 84 | disp(' Allometry model parameters R = a*GV^b+c:') 85 | disp([' Multiplier a: ', num2str(X(1))]) 86 | disp([' Exponent b: ', num2str(X(2))]) 87 | if length(X) > 2 88 | disp([' Intersect c: ', num2str(X(3))]) 89 | end 90 | 91 | %% Compute the predicted radius from the allometry 92 | PredRad = allometry(X,GrowthVol); 93 | 94 | %% Correct the radii based on the predictions 95 | % If cylinder's radius is over fac-times or under 1/fac-times the 96 | % predicted radius, then correct the radius to match the allometry 97 | fac = inputs.GrowthVolFac; 98 | modify = Rad < PredRad/fac | Rad > fac*PredRad; 99 | modify(Rad < PredRad/fac & CExt == 0) = 0; % Do not increase the radius at tips 100 | CorRad = PredRad(modify); 101 | 102 | % Plot allometry and radii modification 103 | gvm = max(GrowthVol); 104 | gv = (0:0.001:gvm); 105 | PRad = allometry(X,gv); 106 | figure(1) 107 | plot(GrowthVol,Rad,'.b','Markersize',2) 108 | hold on 109 | plot(gv,PRad,'-r','Linewidth',2) 110 | plot(gv,PRad/fac,'-g','Linewidth',2) 111 | plot(gv,fac*PRad,'-g','Linewidth',2) 112 | hold off 113 | grid on 114 | xlabel('Growth volume (m^3)') 115 | ylabel('Radius (m)') 116 | legend('radius','predicted radius','minimum radius','maximum radius','Location','NorthWest') 117 | 118 | figure(2) 119 | histogram(CorRad-Rad(modify)) 120 | xlabel('Change in radius') 121 | title('Number of cylinders per change in radius class') 122 | 123 | % Determine the maximum radius change 124 | R = Rad(modify); 125 | D = max(abs(R-CorRad)); % Maximum radius change 126 | J = abs(R-CorRad) == D; 127 | D = CorRad(J)-R(J); 128 | 129 | % modify the radius according to allometry 130 | Rad(modify) = CorRad; 131 | cylinder.radius = Rad; 132 | 133 | disp([' Modified ',num2str(nnz(modify)),' of the ',num2str(n),' cylinders']) 134 | disp([' Largest radius change (cm): ',num2str(round(1000*D)/10)]) 135 | corrected_volume = round(1000*pi*sum(Rad.^2.*Len)); 136 | disp([' Corrected volume (L): ', num2str(corrected_volume)]) 137 | disp([' Change in volume (L): ', num2str(corrected_volume-initial_volume)]) 138 | disp('----------') 139 | 140 | % % Plot cylinder models where the color indicates change (green = no change, 141 | % % red = decreased radius, cyan = increased radius) 142 | % cylinder.branch = ones(n,1); 143 | % cylinder.BranchOrder = ones(n,1); 144 | % I = Rad < Rad0; 145 | % cylinder.BranchOrder(I) = 2; 146 | % I = Rad > Rad0; 147 | % cylinder.BranchOrder(I) = 3; 148 | % plot_cylinder_model(cylinder,'order',3,20,1) 149 | % 150 | % cyl = cylinder; 151 | % cyl.radius = Rad0; 152 | % plot_cylinder_model(cyl,'order',4,20,1) 153 | 154 | end % End of main function 155 | 156 | 157 | function F = allometry(x,xdata) 158 | F = x(1)*xdata.^x(2)+x(3); 159 | end 160 | -------------------------------------------------------------------------------- /src/tools/intersect_elements.m: -------------------------------------------------------------------------------- 1 | function Set = intersect_elements(Set1,Set2,False1,False2) 2 | 3 | % Determines the intersection of Set1 and Set2. 4 | 5 | Set = unique_elements([Set1; Set2],False1); 6 | False1(Set1) = true; 7 | False2(Set2) = true; 8 | I = False1(Set)&False2(Set); 9 | Set = Set(I); 10 | -------------------------------------------------------------------------------- /src/tools/mat_vec_subtraction.m: -------------------------------------------------------------------------------- 1 | function A = mat_vec_subtraction(A,v) 2 | 3 | % Subtracts from each row of the matrix A the vector v. 4 | % If A is (n x m)-matrix, then v needs to be m-vector. 5 | 6 | for i = 1:size(A,2) 7 | A(:,i) = A(:,i)-v(i); 8 | end -------------------------------------------------------------------------------- /src/tools/median2.m: -------------------------------------------------------------------------------- 1 | function y = median2(X) 2 | 3 | % Computes the median of the given vector. 4 | 5 | n = size(X,1); 6 | if n > 1 7 | X = sort(X); 8 | m = floor(n/2); 9 | if 2*m == n 10 | y = (X(m)+X(m+1))/2; 11 | elseif m == 0 12 | y = (X(1)+X(2))/2; 13 | else 14 | y = X(m+1); 15 | end 16 | else 17 | y = X; 18 | end -------------------------------------------------------------------------------- /src/tools/normalize.m: -------------------------------------------------------------------------------- 1 | function [A,L] = normalize(A) 2 | 3 | % Normalize rows of the matrix 4 | 5 | L = sqrt(sum(A.*A,2)); 6 | n = size(A,2); 7 | for i = 1:n 8 | A(:,i) = A(:,i)./L; 9 | end 10 | -------------------------------------------------------------------------------- /src/tools/optimal_parallel_vector.m: -------------------------------------------------------------------------------- 1 | function [v,mean_res,sigmah,residual] = optimal_parallel_vector(V) 2 | 3 | % For a given set of unit vectors (the rows of the matrix "V"), 4 | % returns a unit vector ("v") that is the most parallel to them all 5 | % in the sense that the sum of squared dot products of v with the 6 | % vectors of V is maximized. 7 | 8 | A = V'*V; 9 | [U,~,~] = svd(A); 10 | v = U(:,1)'; 11 | 12 | if nargout > 1 13 | residual = abs(V*v'); 14 | mean_res = mean(residual); 15 | sigmah = std(residual); 16 | end -------------------------------------------------------------------------------- /src/tools/orthonormal_vectors.m: -------------------------------------------------------------------------------- 1 | function [V,W] = orthonormal_vectors(U) 2 | 3 | % Generate vectors V and W that are unit vectors orthogonal to themselves 4 | % and to the input vector U 5 | 6 | V = rand(3,1); 7 | V = cross_product(V,U); 8 | while norm(V) == 0 9 | V = rand(3,1); 10 | V = cross_product(V,U); 11 | end 12 | W = cross_product(V,U); 13 | W = W/norm(W); 14 | V = V/norm(V); 15 | if size(V,2) > 1 16 | V = V'; 17 | end 18 | if size(W,2) > 1 19 | W = W'; 20 | end -------------------------------------------------------------------------------- /src/tools/rotation_matrix.m: -------------------------------------------------------------------------------- 1 | function R = rotation_matrix(A,angle) 2 | 3 | % Returns the rotation matrix for the given axis A and angle (in radians) 4 | 5 | A = A/norm(A); 6 | R = zeros(3,3); 7 | c = cos(angle); 8 | s = sin(angle); 9 | R(1,:) = [A(1)^2+(1-A(1)^2)*c A(1)*A(2)*(1-c)-A(3)*s A(1)*A(3)*(1-c)+A(2)*s]; 10 | R(2,:) = [A(1)*A(2)*(1-c)+A(3)*s A(2)^2+(1-A(2)^2)*c A(2)*A(3)*(1-c)-A(1)*s]; 11 | R(3,:) = [A(1)*A(3)*(1-c)-A(2)*s A(2)*A(3)*(1-c)+A(1)*s A(3)^2+(1-A(3)^2)*c]; 12 | -------------------------------------------------------------------------------- /src/tools/save_model_text.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function save_model_text(QSM,savename) 17 | 18 | % --------------------------------------------------------------------- 19 | % SAVE_MODEL_TEXT.M Saves QSM (cylinder, branch, treedata) into text 20 | % files 21 | % 22 | % Version 1.1.0 23 | % Latest update 17 Aug 2020 24 | % 25 | % Copyright (C) 2013-2020 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | 28 | % Save the cylinder, branch, and treedata structures in text-formats (.txt) 29 | % into /result-folder with the input "savename" defining the file names: 30 | % 'cylinder_',savename,'.txt' 31 | % 'branch_',savename,'.txt' 32 | % 'treedata_',savename,'.txt' 33 | % !!! Notice that only part of the treedata, the single number tree 34 | % attributes are saved in the text-file. 35 | % Every user can change this code easily to define what is saved into 36 | % their text-files. 37 | 38 | % Changes from version 1.0.0 to 1.1.0, 17 Aug 2020: 39 | % 1) Added the new fields of cylinder, branch and treedata structures 40 | % 2) Added header names to the files 41 | % 3) Changed the names of the files to be saved 42 | % 4) Changed the name of second input from "string" to "savename" 43 | % 5) Changed the rounding of some parameters and attributes 44 | 45 | cylinder = QSM.cylinder; 46 | branch = QSM.branch; 47 | treedata = QSM.treedata; 48 | 49 | %% Form cylinder data, branch data and tree data 50 | % Use less decimals 51 | Rad = round(10000*cylinder.radius)/10000; % radius (m) 52 | Len = round(10000*cylinder.length)/10000; % length (m) 53 | Sta = round(10000*cylinder.start)/10000; % starting point (m) 54 | Axe = round(10000*cylinder.axis)/10000; % axis (m) 55 | CPar = single(cylinder.parent); % parent cylinder 56 | CExt = single(cylinder.extension); % extension cylinder 57 | Added = single(cylinder.added); % is cylinder added to fil a gap 58 | Rad0 = round(10000*cylinder.UnmodRadius)/10000; % unmodified radius (m) 59 | B = single(cylinder.branch); % branch index of the cylinder 60 | BO = single(cylinder.BranchOrder); % branch order of the branch 61 | PIB = single(cylinder.PositionInBranch); % position of the cyl. in the branch 62 | Mad = single(round(10000*cylinder.mad)/10000); % mean abso. distance (m) 63 | SC = single(round(10000*cylinder.SurfCov)/10000); % surface coverage 64 | CylData = [Rad Len Sta Axe CPar CExt B BO PIB Mad SC Added Rad0]; 65 | NamesC = ['radius (m)',"length (m)","start_point","axis_direction",... 66 | "parent","extension","branch","branch_order","position_in_branch",... 67 | "mad","SurfCov","added","UnmodRadius (m)"]; 68 | 69 | BOrd = single(branch.order); % branch order 70 | BPar = single(branch.parent); % parent branch 71 | BDia = round(10000*branch.diameter)/10000; % diameter (m) 72 | BVol = round(10000*branch.volume)/10000; % volume (L) 73 | BAre = round(10000*branch.area)/10000; % area (m^2) 74 | BLen = round(1000*branch.length)/1000; % length (m) 75 | BAng = round(10*branch.angle)/10; % angle (deg) 76 | BHei = round(1000*branch.height)/1000; % height (m) 77 | BAzi = round(10*branch.azimuth)/10; % azimuth (deg) 78 | BZen = round(10*branch.zenith)/10; % zenith (deg) 79 | BranchData = [BOrd BPar BDia BVol BAre BLen BHei BAng BAzi BZen]; 80 | NamesB = ["order","parent","diameter (m)","volume (L)","area (m^2)",... 81 | "length (m)","height (m)","angle (deg)","azimuth (deg)","zenith (deg)"]; 82 | 83 | % Extract the field names of treedata 84 | Names = fieldnames(treedata); 85 | n = 1; 86 | while ~strcmp(Names{n},'location') 87 | n = n+1; 88 | end 89 | n = n-1; 90 | Names = Names(1:n); 91 | 92 | TreeData = zeros(n,1); 93 | % TreeData contains TotalVolume, TrunkVolume, BranchVolume, etc 94 | for i = 1:n 95 | TreeData(i) = treedata.(Names{i,:}); 96 | end 97 | TreeData = change_precision(TreeData); % use less decimals 98 | NamesD = string(Names); 99 | 100 | %% Save the data as text-files 101 | str = ['results/cylinder_',savename,'.txt']; 102 | fid = fopen(str, 'wt'); 103 | fprintf(fid, [repmat('%s\t', 1, size(NamesC,2)-1) '%s\n'], NamesC.'); 104 | fprintf(fid, [repmat('%g\t', 1, size(CylData,2)-1) '%g\n'], CylData.'); 105 | fclose(fid); 106 | 107 | str = ['results/branch_',savename,'.txt']; 108 | fid = fopen(str, 'wt'); 109 | fprintf(fid, [repmat('%s\t', 1, size(NamesB,2)-1) '%s\n'], NamesB.'); 110 | fprintf(fid, [repmat('%g\t', 1, size(BranchData,2)-1) '%g\n'], BranchData.'); 111 | fclose(fid); 112 | 113 | str = ['results/treedata_',savename,'.txt']; 114 | fid = fopen(str, 'wt'); 115 | NamesD(:,2) = TreeData; 116 | fprintf(fid,'%s\t %g\n',NamesD.'); 117 | fclose(fid); 118 | -------------------------------------------------------------------------------- /src/tools/sec2min.m: -------------------------------------------------------------------------------- 1 | function [Tmin,Tsec] = sec2min(T) 2 | 3 | % Transforms the given number of seconds into minutes and residual seconds 4 | 5 | Tmin = floor(T/60); 6 | Tsec = round((T-Tmin*60)*10)/10; -------------------------------------------------------------------------------- /src/tools/select_cylinders.m: -------------------------------------------------------------------------------- 1 | function cylinder = select_cylinders(cylinder,Ind) 2 | 3 | Names = fieldnames(cylinder); 4 | n = size(Names,1); 5 | for i = 1:n 6 | cylinder.(Names{i}) = cylinder.(Names{i})(Ind,:); 7 | end -------------------------------------------------------------------------------- /src/tools/set_difference.m: -------------------------------------------------------------------------------- 1 | function Set1 = set_difference(Set1,Set2,False) 2 | 3 | % Performs the set difference so that the common elements of Set1 and Set2 4 | % are removed from Set1, which is the output. Uses logical vector whose 5 | % length must be up to the maximum element of the sets. 6 | 7 | False(Set2) = true; 8 | I = False(Set1); 9 | Set1 = Set1(~I); -------------------------------------------------------------------------------- /src/tools/surface_coverage.m: -------------------------------------------------------------------------------- 1 | function [SurfCov,Dis,CylVol,dis] = surface_coverage(P,Axis,Point,nl,ns,Dmin,Dmax) 2 | 3 | % --------------------------------------------------------------------- 4 | % SURFACE_COVERAGE.M Computes point surface coverage measure 5 | % 6 | % Version 1.1.0 7 | % Last update 7 Oct 2021 8 | % 9 | % Copyright (C) 2017-2021 Pasi Raumonen 10 | % --------------------------------------------------------------------- 11 | % Inputs: 12 | % Axis Axis direction (1 x 3) 13 | % Point Starting point of the cylinder (1 x 3) 14 | % nl Number of layers in the axis direction used for to partition 15 | % the cylinder surface into layer/sectors 16 | % ns Number of sectors used to partition the cylinder surface into 17 | % layer/sectors 18 | % Dmin (Optional) Minimum point distance from the axis to be included 19 | % into SurfCov calculations 20 | % Dmax (Optional) Maximum point distance from the axis to be included 21 | % into SurfCov calculations 22 | % 23 | % Output: 24 | % SurfCov Number between 0 and 1 descring how big portion of the cylinder 25 | % surface is covered with points 26 | % Dis (Optional) Mean distances of the distances of the layer/sectors 27 | % CylVol (Optional) Volume of the cylinder estimated by the mean 28 | % distances of the layer/sectors as cylindrical sections 29 | % dis (Optional) Same as "Dis" but empty cells are interpolated 30 | % --------------------------------------------------------------------- 31 | % Computes surface coverage (number between 0 and 1) of points on cylinder 32 | % surface defined by "Axis" and "Point". 33 | 34 | % Changes from version 1.0.0 to 1.1.0, 7 Oct 2021: 35 | % 1) Added two possible inputs, minimum and maximum distance, 36 | % Dmin and Dmax, which can be used to filter out points for the surface 37 | % coverage calculations 38 | % 2) Computes the SurfCov estimate with four baseline directions used in 39 | % the sector determination and selects the largest value 40 | % 3) Smalle changes to speed up computations 41 | 42 | %% Compute the distances and heights of the points 43 | [d,V,h] = distances_to_line(P,Axis,Point); 44 | h = h-min(h); 45 | Len = max(h); 46 | 47 | %% (Optional) Filter out points based on the distance to the axis 48 | if nargin >= 6 49 | Keep = d > Dmin; 50 | if nargin == 7 51 | Keep = Keep & d < Dmax; 52 | end 53 | V = V(Keep,:); 54 | h = h(Keep); 55 | end 56 | 57 | %% Compute SurfCov 58 | % from 4 different baseline directions to determine the angles and select 59 | % the maximum value 60 | V0 = V; 61 | [U,W] = orthonormal_vectors(Axis); % First planar axes 62 | R = rotation_matrix(Axis,2*pi/ns/4); % Rotation matrix to rotate the axes 63 | SurfCov = zeros(1,4); 64 | for i = 1:4 65 | %% Rotate the axes 66 | if i > 1 67 | U = R*U; 68 | W = R*W; 69 | end 70 | 71 | %% Compute the angles (sectors) of the points 72 | V = V0*[U W]; 73 | ang = atan2(V(:,2),V(:,1))+pi; 74 | 75 | %% Compute lexicographic order (sector,layer) of every point 76 | Layer = ceil(h/Len*nl); 77 | Layer(Layer <= 0) = 1; 78 | Layer(Layer > nl) = nl; 79 | Sector = ceil(ang/2/pi*ns); 80 | Sector(Sector <= 0) = 1; 81 | LexOrd = [Layer Sector-1]*[1 nl]'; 82 | 83 | %% Compute SurfCov 84 | Cov = zeros(nl,ns); 85 | Cov(LexOrd) = 1; 86 | SurfCov(i) = nnz(Cov)/nl/ns; 87 | end 88 | SurfCov = max(SurfCov); 89 | 90 | 91 | %% Compute volume estimate 92 | if nargout > 1 93 | % Sort according to increasing lexicographic order 94 | [LexOrd,SortOrd] = sort(LexOrd); 95 | d = d(SortOrd); 96 | 97 | % Compute mean distance of the sector-layer intersections 98 | Dis = zeros(nl,ns); % mean distances 99 | np = length(LexOrd); % number of points 100 | p = 1; 101 | while p <= np 102 | t = 1; 103 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 104 | t = t+1; 105 | end 106 | Dis(LexOrd(p)) = average(d(p:p+t-1)); 107 | p = p+t; 108 | end 109 | 110 | if nargout > 2 111 | % Interpolate missing distances 112 | D = Dis; 113 | dis = Dis; 114 | Dinv = D((nl:-1:1)',:); 115 | D = [Dinv Dinv Dinv; D D D; Dinv Dinv Dinv]; 116 | Zero = Dis == 0; 117 | RadMean = average(Dis(Dis > 0)); 118 | for i = 1:nl 119 | for j = 1:ns 120 | if Zero(i,j) 121 | if nnz(D(i+nl-1:i+nl+1,j+ns-1:j+ns+1)) > 1 122 | d = D(i+nl-1:i+nl+1,j+ns-1:j+ns+1); 123 | dis(i,j) = average(d(d > 0)); 124 | elseif nnz(D(i+nl-2:i+nl+2,j+ns-2:j+ns+2)) > 1 125 | d = D(i+nl-2:i+nl+2,j+ns-2:j+ns+2); 126 | dis(i,j) = average(d(d > 0)); 127 | elseif nnz(D(i+nl-3:i+nl+3,j+ns-3:j+ns+3)) > 1 128 | d = D(i+nl-3:i+nl+3,j+ns-3:j+ns+3); 129 | dis(i,j) = average(d(d > 0)); 130 | else 131 | dis(i,j) = RadMean; 132 | end 133 | end 134 | end 135 | end 136 | % Compute the volume estimate 137 | r = dis(:); 138 | CylVol = 1000*pi*sum(r.^2)/ns*Len/nl; 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /src/tools/surface_coverage2.m: -------------------------------------------------------------------------------- 1 | function SurfCov = surface_coverage2(Axis,Len,Vec,height,nl,ns) 2 | 3 | % Computes surface coverage (number between 0 and 1) of points on cylinder 4 | % surface defined by "Axis" and "Len". "Vec" are the vectors connecting 5 | % points to the Axis and "height" are the heights of the points from 6 | % the base of the cylinder 7 | 8 | [U,W] = orthonormal_vectors(Axis); 9 | Vec = Vec*[U W]; 10 | ang = atan2(Vec(:,2),Vec(:,1))+pi; 11 | I = ceil(height/Len*nl); 12 | I(I == 0) = 1; 13 | I(I > nl) = nl; 14 | J = ceil(ang/2/pi*ns); 15 | J(J == 0) = 1; 16 | K = [I J-1]*[1 nl]'; 17 | SurfCov = length(unique(K))/nl/ns; -------------------------------------------------------------------------------- /src/tools/surface_coverage_filtering.m: -------------------------------------------------------------------------------- 1 | function [Pass,c] = surface_coverage_filtering(P,c,lh,ns) 2 | 3 | % --------------------------------------------------------------------- 4 | % SURFACE_COVERAGE_FILTERING.M Filters a point cloud based on the 5 | % assumption that it samples a cylinder 6 | % 7 | % Version 1.1.0 8 | % Latest update 6 Oct 2021 9 | % 10 | % Copyright (C) 2017-2021 Pasi Raumonen 11 | % --------------------------------------------------------------------- 12 | 13 | % Filter a 3d-point cloud based on given cylinder (axis and radius) by 14 | % dividing the point cloud into "ns" equal-angle sectors and "lh"-height 15 | % layers along the axis. For each sector-layer intersection (a region in 16 | % the cylinder surface) keep only the points closest to the axis. 17 | 18 | % Inputs: 19 | % P Point cloud, (n_points x 3)-matrix 20 | % c Cylinder, stucture array with fields "axis", "start", 21 | % "length" 22 | % lh Height of the layers 23 | % ns Number of sectors 24 | % 25 | % Outputs: 26 | % Pass Logical vector indicating which points pass the filtering 27 | % c Cylinder, stucture array with additional fields "radius", 28 | % "SurfCov", "mad", "conv", "rel", estimated from the 29 | % filtering 30 | % --------------------------------------------------------------------- 31 | 32 | % Changes from version 1.0.0 to 1.1.0, 6 Oct 2021: 33 | % 1) Small changes to make the code little faster 34 | % 2) Change the radius estimation to make it much faster 35 | 36 | 37 | % Compute the distances, heights and angles of the points 38 | [d,V,h] = distances_to_line(P,c.axis,c.start); 39 | h = h-min(h); 40 | [U,W] = orthonormal_vectors(c.axis); 41 | V = V*[U W]; 42 | ang = atan2(V(:,2),V(:,1))+pi; 43 | 44 | % Sort based on lexicographic order of (sector,layer) 45 | nl = ceil(c.length/lh); 46 | Layer = ceil(h/c.length*nl); 47 | Layer(Layer == 0) = 1; 48 | Layer(Layer > nl) = nl; 49 | Sector = ceil(ang/2/pi*ns); 50 | Sector(Sector == 0) = 1; 51 | LexOrd = [Layer Sector-1]*[1 nl]'; 52 | [LexOrd,SortOrd] = sort(LexOrd); 53 | ds = d(SortOrd); 54 | 55 | % Estimate the distances for each sector-layer intersection 56 | Dis = zeros(nl,ns); 57 | np = size(P,1); % number of points 58 | p = 1; 59 | while p <= np 60 | t = 1; 61 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 62 | t = t+1; 63 | end 64 | D = min(ds(p:p+t-1)); 65 | Dis(LexOrd(p)) = min(1.05*D,D+0.02); 66 | p = p+t; 67 | end 68 | 69 | % Compute the number of sectors based on the estimated radius 70 | R = median(Dis(Dis > 0)); 71 | a = max(0.02,0.2*R); 72 | ns = ceil(2*pi*R/a); 73 | ns = min(36,max(ns,8)); 74 | nl = ceil(c.length/a); 75 | nl = max(nl,3); 76 | 77 | % Sort based on lexicographic order of (sector,layer) 78 | Layer = ceil(h/c.length*nl); 79 | Layer(Layer == 0) = 1; 80 | Layer(Layer > nl) = nl; 81 | Sector = ceil(ang/2/pi*ns); 82 | Sector(Sector == 0) = 1; 83 | LexOrd = [Layer Sector-1]*[1 nl]'; 84 | [LexOrd,SortOrd] = sort(LexOrd); 85 | d = d(SortOrd); 86 | 87 | % Filtering for each sector-layer intersection 88 | Dis = zeros(nl,ns); 89 | Pass = false(np,1); 90 | p = 1; % index of point under processing 91 | k = 0; % number of nonempty cells 92 | r = max(0.01,0.05*R); % cell diameter from the closest point 93 | while p <= np 94 | t = 1; 95 | while (p+t <= np) && (LexOrd(p) == LexOrd(p+t)) 96 | t = t+1; 97 | end 98 | ind = p:p+t-1; 99 | D = d(ind); 100 | Dmin = min(D); 101 | I = D <= Dmin+r; 102 | Pass(ind(I)) = true; 103 | Dis(LexOrd(p)) = min(1.05*Dmin,Dmin+0.02); 104 | p = p+t; 105 | k = k+1; 106 | end 107 | d = d(Pass); 108 | 109 | % Sort the "Pass"-vector back to original point cloud order 110 | n = length(SortOrd); 111 | InvSortOrd = zeros(n,1); 112 | InvSortOrd(SortOrd) = (1:1:n)'; 113 | Pass = Pass(InvSortOrd); 114 | 115 | % Compute radius, SurfCov and mad 116 | R = median(Dis(Dis > 0)); 117 | mad = sum(abs(d-R))/length(d); 118 | 119 | c.radius = R; 120 | c.SurfCov = k/nl/ns; 121 | c.mad = mad; 122 | c.conv = 1; 123 | c.rel = 1; 124 | -------------------------------------------------------------------------------- /src/tools/unique2.m: -------------------------------------------------------------------------------- 1 | function SetUni = unique2(Set) 2 | 3 | 4 | n = length(Set); 5 | if n > 0 6 | Set = sort(Set); 7 | d = Set(2:n)-Set(1:n-1); 8 | A = Set(2:n); 9 | I = d > 0; 10 | SetUni = [Set(1); A(I)]; 11 | else 12 | SetUni = Set; 13 | end -------------------------------------------------------------------------------- /src/tools/unique_elements.m: -------------------------------------------------------------------------------- 1 | function Set = unique_elements(Set,False) 2 | 3 | n = length(Set); 4 | if n > 2 5 | I = true(n,1); 6 | for j = 1:n 7 | if ~False(Set(j)) 8 | False(Set(j)) = true; 9 | else 10 | I(j) = false; 11 | end 12 | end 13 | Set = Set(I); 14 | elseif n == 2 15 | if Set(1) == Set(2) 16 | Set = Set(1); 17 | end 18 | end -------------------------------------------------------------------------------- /src/tools/verticalcat.m: -------------------------------------------------------------------------------- 1 | function [Vector,IndElements] = verticalcat(CellArray) 2 | 3 | % Vertical concatenation of the given cell-array into a vector. 4 | 5 | CellSize = cellfun('length',CellArray); % determine the size of each cell 6 | nc = max(size(CellArray)); % number of cells 7 | IndElements = ones(nc,2); % indexes for elements in each cell 8 | IndElements(:,2) = cumsum(CellSize); 9 | IndElements(2:end,1) = IndElements(2:end,1)+IndElements(1:end-1,2); 10 | Vector = zeros(sum(CellSize),1); % concatenation of the cell-array into a vector 11 | for j = 1:nc 12 | Vector(IndElements(j,1):IndElements(j,2)) = CellArray{j}; 13 | end -------------------------------------------------------------------------------- /src/triangulation/boundary_curve.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [Curve,Ind] = boundary_curve(P,Curve0,rball,dmax) 17 | 18 | % --------------------------------------------------------------------- 19 | % BOUNDARY_CURVE.M Determines the boundary curve based on the 20 | % previously defined boundary curve. 21 | % 22 | % Version 1.1.0 23 | % Latest update 3 May 2022 24 | % 25 | % Copyright (C) 2015-2022 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | % 28 | % Inputs: 29 | % P Point cloud of the cross section 30 | % Curve0 Seed points from previous cross section curve 31 | % rball Radius of the balls centered at seed points 32 | % dmax Maximum distance between concecutive curve points, if larger, 33 | % then create a new one between the points 34 | % --------------------------------------------------------------------- 35 | 36 | % Changes from version 1.0.0 to 1.1.0, 3 May 2022: 37 | % 1) Increased the cubical neighborhood in the generation of the segments 38 | 39 | %% Partition the point cloud into cubes 40 | Min = double(min([P(:,1:2); Curve0(:,1:2)])); 41 | Max = double(max([P(:,1:2); Curve0(:,1:2)])); 42 | N = double(ceil((Max-Min)/rball)+5); 43 | % cube coordinates of the section points 44 | CC = floor([P(:,1)-Min(1) P(:,2)-Min(2)]/rball)+3; 45 | % Sorts the points according a lexicographical order 46 | S = [CC(:,1) CC(:,2)-1]*[1 N(1)]'; 47 | [S,I] = sort(S); 48 | % Define "partition" 49 | np = size(P,1); 50 | partition = cell(N(1),N(2)); 51 | p = 1; % The index of the point under comparison 52 | while p <= np 53 | t = 1; 54 | while (p+t <= np) && (S(p) == S(p+t)) 55 | t = t+1; 56 | end 57 | q = I(p); 58 | partition{CC(q,1),CC(q,2)} = I(p:p+t-1); 59 | p = p+t; 60 | end 61 | 62 | 63 | %% Define segments using the previous points 64 | % cube coordinates of the seed points: 65 | CC = floor([Curve0(:,1)-Min(1) Curve0(:,2)-Min(2)]/rball)+3; 66 | I = CC < 3; 67 | CC(I) = 3; 68 | nc = size(Curve0,1); % number of sets 69 | Dist = 1e8*ones(np,1); % distance of point to the closest center 70 | SoP = zeros(np,1); % the segment the points belong to 71 | Radius = rball^2; 72 | for i = 1:nc 73 | points = partition(CC(i,1)-2:CC(i,1)+2,CC(i,2)-2:CC(i,2)+2); 74 | points = vertcat(points{:}); 75 | V = [P(points,1)-Curve0(i,1) P(points,2)-Curve0(i,2)]; 76 | dist = sum(V.*V,2); 77 | PointsInBall = dist < Radius; 78 | points = points(PointsInBall); 79 | dist = dist(PointsInBall); 80 | D = Dist(points); 81 | L = dist < D; 82 | I = points(L); 83 | Dist(I) = dist(L); 84 | SoP(I) = i; 85 | end 86 | 87 | %% Finalise the segments 88 | % Number of points in each segment and index of each point in its segment 89 | Num = zeros(nc,1); 90 | IndPoints = zeros(np,1); 91 | for i = 1:np 92 | if SoP(i) > 0 93 | Num(SoP(i)) = Num(SoP(i))+1; 94 | IndPoints(i) = Num(SoP(i)); 95 | end 96 | end 97 | % Continue if enough non-emtpy segments 98 | if nnz(Num) > 0.05*nc 99 | % Initialization of the "Seg" 100 | Seg = cell(nc,1); 101 | for i = 1:nc 102 | Seg{i} = zeros(Num(i),1); 103 | end 104 | % Define the "Seg" 105 | for i = 1:np 106 | if SoP(i) > 0 107 | Seg{SoP(i),1}(IndPoints(i),1) = i; 108 | end 109 | end 110 | 111 | %% Define the new curve points as the average of the segments 112 | Curve = zeros(nc,3); % the new boundary curve 113 | Empty = false(nc,1); 114 | for i = 1:nc 115 | S = Seg{i}; 116 | if ~isempty(S) 117 | Curve(i,:) = mean(P(S,:),1); 118 | if norm(Curve(i,:)-Curve0(i,:)) > 1.25*dmax 119 | Curve(i,:) = Curve0(i,:); 120 | end 121 | else 122 | Empty(i) = true; 123 | end 124 | end 125 | 126 | %% Interpolate for empty segments 127 | % For empty segments create points by interpolation from neighboring 128 | % non-empty segments 129 | if any(Empty) 130 | for i = 1:nc 131 | if Empty(i) 132 | if i > 1 && i < nc 133 | k = 0; 134 | while i+k <= nc && Empty(i+k) 135 | k = k+1; 136 | end 137 | if i+k <= nc 138 | LineEle = Curve(i+k,:)-Curve(i-1,:); 139 | else 140 | LineEle = Curve(1,:)-Curve(i-1,:); 141 | end 142 | if k < 5 143 | for j = 1:k 144 | Curve(i+j-1,:) = Curve(i-1,:)+j/(k+1)*LineEle; 145 | end 146 | else 147 | Curve(i:i+k-1,:) = Curve0(i:i+k-1,:); 148 | end 149 | elseif i == 1 150 | a = 0; 151 | while Empty(end-a) 152 | a = a+1; 153 | end 154 | b = 1; 155 | while Empty(b) 156 | b = b+1; 157 | end 158 | LineEle = Curve(b,:)-Curve(nc-a,:); 159 | n = a+b-1; 160 | if n < 5 161 | for j = 1:a-1 162 | Curve(nc-a+1+j,:) = Curve(nc-a,:)+j/n*LineEle; 163 | end 164 | for j = 1:b-1 165 | Curve(j,:) = Curve(nc-a,:)+(j+a-1)/n*LineEle; 166 | end 167 | else 168 | Curve(nc-a+2:nc,1:2) = Curve0(nc-a+2:nc,1:2); 169 | Curve(nc-a+2:nc,3) = Curve0(nc-a+2:nc,3); 170 | Curve(1:b-1,1:2) = Curve0(1:b-1,1:2); 171 | Curve(1:b-1,3) = Curve0(1:b-1,3); 172 | end 173 | elseif i == nc 174 | LineEle = Curve(1,:)-Curve(nc-1,:); 175 | Curve(i,:) = Curve(nc-1,:)+0.5*LineEle; 176 | end 177 | end 178 | end 179 | end 180 | 181 | % Correct the height 182 | Curve(:,3) = min(Curve(:,3)); 183 | 184 | % Check self-intersection 185 | [Intersect,IntersectLines] = check_self_intersection(Curve(:,1:2)); 186 | 187 | % If self-intersection, try to modify the curve 188 | j = 1; 189 | while Intersect && j <= 5 190 | n = size(Curve,1); 191 | InterLines = (1:1:n)'; 192 | NumberOfIntersections = cellfun('length',IntersectLines(:,1)); 193 | I = NumberOfIntersections > 0; 194 | InterLines = InterLines(I); 195 | CrossLen = vertcat(IntersectLines{I,2}); 196 | if length(CrossLen) == length(InterLines) 197 | LineEle = Curve([2:end 1],:)-Curve(1:end,:); 198 | d = sqrt(sum(LineEle.*LineEle,2)); 199 | m = length(InterLines); 200 | for i = 1:2:m 201 | if InterLines(i) ~= n 202 | Curve(InterLines(i)+1,:) = Curve(InterLines(i),:)+... 203 | 0.9*CrossLen(i)/d(InterLines(i))*LineEle(InterLines(i),:); 204 | else 205 | Curve(1,:) = Curve(InterLines(i),:)+... 206 | 0.9*CrossLen(i)/d(InterLines(i))*LineEle(InterLines(i),:); 207 | end 208 | end 209 | [Intersect,IntersectLines] = check_self_intersection(Curve(:,1:2)); 210 | j = j+1; 211 | else 212 | j = 6; 213 | end 214 | end 215 | 216 | %% Add new points if too large distances 217 | LineEle = Curve([2:end 1],:)-Curve(1:end,:); 218 | d = sum(LineEle.*LineEle,2); 219 | Large = d > dmax^2; 220 | m = nnz(Large); 221 | if m > 0 222 | Curve0 = zeros(nc+m,3); 223 | Ind = zeros(nc+m,2); 224 | t = 0; 225 | for i = 1:nc 226 | if Large(i) 227 | t = t+1; 228 | Curve0(t,:) = Curve(i,:); 229 | if i < nc 230 | Ind(t,:) = [i i+1]; 231 | else 232 | Ind(t,:) = [i 1]; 233 | end 234 | t = t+1; 235 | Curve0(t,:) = Curve(i,:)+0.5*LineEle(i,:); 236 | if i < nc 237 | Ind(t,:) = [i+1 0]; 238 | else 239 | Ind(t,:) = [1 0]; 240 | end 241 | else 242 | t = t+1; 243 | Curve0(t,:) = Curve(i,:); 244 | if i < nc 245 | Ind(t,:) = [i i+1]; 246 | else 247 | Ind(t,:) = [i 1]; 248 | end 249 | end 250 | end 251 | Curve = Curve0; 252 | 253 | else 254 | Ind = [(1:1:nc)' [(2:1:nc)'; 1]]; 255 | end 256 | 257 | 258 | %% Remove new points if too small distances 259 | nc = size(Curve,1); 260 | LineEle = Curve([2:end 1],:)-Curve(1:end,:); 261 | d = sum(LineEle.*LineEle,2); 262 | Small = d < (0.333*dmax)^2; 263 | m = nnz(Small); 264 | if m > 0 265 | for i = 1:nc-1 266 | if ~Small(i) && Small(i+1) 267 | Ind(i,2) = -1; 268 | elseif Small(i) && Small(i+1) 269 | Small(i+1) = false; 270 | end 271 | end 272 | if ~Small(nc) && Small(1) 273 | Ind(nc,2) = -1; 274 | Ind(1,2) = -1; 275 | Small(1) = false; 276 | Small(nc) = true; 277 | I = Ind(:,2) > 0; 278 | Ind(2:end,1) = Ind(2:end,1)+1; 279 | Ind(I,2) = Ind(I,2)+1; 280 | 281 | end 282 | Ind = Ind(~Small,:); 283 | Curve = Curve(~Small,:); 284 | end 285 | 286 | else 287 | % If not enough new points, return the old curve 288 | Ind = [(1:1:nc)' [(2:1:nc)'; 1]]; 289 | Curve = Curve0; 290 | end 291 | -------------------------------------------------------------------------------- /src/triangulation/boundary_curve2.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function Curve = boundary_curve2(P,Curve0,rball,dmax) 17 | 18 | % --------------------------------------------------------------------- 19 | % BOUNDARY_CURVE2.M Determines the boundary curve based on the 20 | % previously defined boundary curve. 21 | % 22 | % Version 1.0 23 | % Latest update 16 Aug 2017 24 | % 25 | % Copyright (C) 2015-2017 Pasi Raumonen 26 | % --------------------------------------------------------------------- 27 | % 28 | % Inputs: 29 | % P Point cloud of the cross section 30 | % Curve0 Seed points from previous cross section curve 31 | % rball Radius of the balls centered at seed points 32 | % dmax Maximum distance between concecutive curve points, if larger, 33 | % then create a new one between the points 34 | 35 | 36 | %% Partition the point cloud into cubes 37 | Min = double(min([P(:,1:2); Curve0(:,1:2)])); 38 | Max = double(max([P(:,1:2); Curve0(:,1:2)])); 39 | N = double(ceil((Max-Min)/rball)+5); 40 | CC = floor([P(:,1)-Min(1) P(:,2)-Min(2)]/rball)+3; % cube coordinates of the section points 41 | % Sorts the points according a lexicographical order 42 | S = [CC(:,1) CC(:,2)-1]*[1 N(1)]'; 43 | [S,I] = sort(S); 44 | % Define "partition" 45 | np = size(P,1); 46 | partition = cell(N(1),N(2)); 47 | p = 1; % The index of the point under comparison 48 | while p <= np 49 | t = 1; 50 | while (p+t <= np) && (S(p) == S(p+t)) 51 | t = t+1; 52 | end 53 | q = I(p); 54 | partition{CC(q,1),CC(q,2)} = I(p:p+t-1); 55 | p = p+t; 56 | end 57 | 58 | 59 | %% Define segments using the previous points 60 | CC = floor([Curve0(:,1)-Min(1) Curve0(:,2)-Min(2)]/rball)+3; % cube coordinates of the seed points 61 | I = CC < 3; 62 | CC(I) = 3; 63 | nc = size(Curve0,1); % number of sets 64 | Dist = 1e8*ones(np,1); % distance of point to the closest center 65 | SoP = zeros(np,1); % the segment the points belong to 66 | Radius = rball^2; 67 | for i = 1:nc 68 | points = partition(CC(i,1)-1:CC(i,1)+1,CC(i,2)-1:CC(i,2)+1); 69 | points = vertcat(points{:}); 70 | V = [P(points,1)-Curve0(i,1) P(points,2)-Curve0(i,2)]; 71 | dist = sum(V.*V,2); 72 | PointsInBall = dist < Radius; 73 | points = points(PointsInBall); 74 | dist = dist(PointsInBall); 75 | D = Dist(points); 76 | L = dist < D; 77 | I = points(L); 78 | Dist(I) = dist(L); 79 | SoP(I) = i; 80 | end 81 | 82 | %% Finalise the segments 83 | % Number of points in each segment and index of each point in its segment 84 | Num = zeros(nc,1); 85 | IndPoints = zeros(np,1); 86 | for i = 1:np 87 | if SoP(i) > 0 88 | Num(SoP(i)) = Num(SoP(i))+1; 89 | IndPoints(i) = Num(SoP(i)); 90 | end 91 | end 92 | % Continue if enough non-emtpy segments 93 | if nnz(Num) > 0.05*nc 94 | % Initialization of the "Seg" 95 | Seg = cell(nc,1); 96 | for i = 1:nc 97 | Seg{i} = zeros(Num(i),1); 98 | end 99 | % Define the "Seg" 100 | for i = 1:np 101 | if SoP(i) > 0 102 | Seg{SoP(i),1}(IndPoints(i),1) = i; 103 | end 104 | end 105 | 106 | %% Define the new curve points as the average of the segments 107 | Curve = zeros(nc,3); % the new boundary curve 108 | for i = 1:nc 109 | S = Seg{i}; 110 | if ~isempty(S) 111 | Curve(i,:) = mean(P(S,:),1); 112 | if norm(Curve(i,:)-Curve0(i,:)) > 1.25*dmax 113 | Curve(i,:) = Curve0(i,:); 114 | end 115 | else 116 | Curve(i,:) = Curve0(i,:); 117 | end 118 | end 119 | 120 | %% Add new points if too large distances 121 | V = Curve([2:end 1],:)-Curve(1:end,:); 122 | d = sum(V.*V,2); 123 | Large = d > dmax^2; 124 | m = nnz(Large); 125 | if m > 0 126 | Curve0 = zeros(nc+m,3); 127 | t = 0; 128 | for i = 1:nc 129 | if Large(i) 130 | t = t+1; 131 | Curve0(t,:) = Curve(i,:); 132 | t = t+1; 133 | Curve0(t,:) = Curve(i,:)+0.5*V(i,:); 134 | else 135 | t = t+1; 136 | Curve0(t,:) = Curve(i,:); 137 | end 138 | end 139 | Curve = Curve0; 140 | end 141 | 142 | %% Remove new points if too small distances 143 | nc = size(Curve,1); 144 | V = Curve([2:end 1],:)-Curve(1:end,:); 145 | d = sum(V.*V,2); 146 | Small = d < (0.333*dmax)^2; 147 | m = nnz(Small); 148 | if m > 0 149 | for i = 1:nc-1 150 | if Small(i) && Small(i+1) 151 | Small(i+1) = false; 152 | end 153 | end 154 | if ~Small(nc) && Small(1) 155 | Small(1) = false; 156 | Small(nc) = true; 157 | end 158 | Curve = Curve(~Small,:); 159 | end 160 | 161 | else 162 | % If not enough new points, return the old curve 163 | Curve = Curve0; 164 | end 165 | -------------------------------------------------------------------------------- /src/triangulation/check_self_intersection.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function [Intersect,IntersectLines] = check_self_intersection(Curve) 17 | 18 | % The function takes in a curve (the coordinates of the vertices, in the 19 | % right order) and checks if the curve intersects itself 20 | % 21 | % Outputs: 22 | % Intersect Logical value indicating if the curve self-intersects 23 | % IntersectLines Cell array containing for each line element which are 24 | % the intersecting elements and how far away along 25 | % the line the intersection point is 26 | 27 | 28 | if ~isempty(Curve) 29 | dim = size(Curve,2); % two or three dimensional curve 30 | n = size(Curve,1); % number of points in the curve 31 | V = Curve([(2:n)'; 1],:)-Curve; % line elements forming the curve 32 | L = sqrt(sum(V.*V,2)); % the lengths of the line elements 33 | i = 1; % the line element under inspection 34 | Ind = (1:1:n)'; % indexes of the line elements 35 | if dim == 2 % 2d curves 36 | % directions (unit vectors) of the line elements: 37 | DirLines = [1./L.*V(:,1) 1./L.*V(:,2)]; 38 | Intersect = false; 39 | if nargout == 1 % check only if the curve intersects 40 | while i <= n-1 && ~Intersect 41 | % Select the line elements that can intersect element i 42 | if i > 1 43 | I = Ind > i+1 | Ind < i-1; 44 | else 45 | I = Ind > i+1 & Ind < n; 46 | end 47 | ind = Ind(I)'; 48 | for j = ind 49 | % Solve for the crossing points of every line element 50 | A = [DirLines(j,:)' -DirLines(i,:)']; 51 | b = Curve(i,:)'-Curve(j,:)'; 52 | Ainv = 1/(A(1,1)*A(2,2)-A(1,2)*A(2,1))*[A(2,2) -A(1,2); -A(2,1) A(1,1)]; 53 | x = Ainv*b; % signed length along the line elements to the crossing 54 | if x(1) >= 0 && x(1) <= L(j) && x(2) >= 0 && x(2) <= L(i) 55 | Intersect = true; 56 | end 57 | end 58 | i = i+1; % study the next line element 59 | end 60 | else % determine also all intersection points (line elements) 61 | IntersectLines = cell(n,2); 62 | for i = 1:n-1 63 | % Select the line elements that can intersect element i 64 | if i > 1 65 | I = Ind > i+1 | Ind < i-1; 66 | else 67 | I = Ind > i+1 & Ind < n; 68 | end 69 | ind = Ind(I)'; 70 | for j = ind 71 | % Solve for the crossing points of every line element 72 | A = [DirLines(j,:)' -DirLines(i,:)']; 73 | b = Curve(i,:)'-Curve(j,:)'; 74 | Ainv = 1/(A(1,1)*A(2,2)-A(1,2)*A(2,1))*[A(2,2) -A(1,2); -A(2,1) A(1,1)]; 75 | x = Ainv*b; 76 | if x(1) >= 0 && x(1) <= L(j) && x(2) >= 0 && x(2) <= L(i) 77 | Intersect = true; 78 | % which line elements cross element i: 79 | IntersectLines{i,1} = [IntersectLines{i,1}; j]; 80 | % which line elements cross element j: 81 | IntersectLines{j,1} = [IntersectLines{j,1}; i]; 82 | % distances along element i to intersection points: 83 | IntersectLines{i,2} = [IntersectLines{i,2}; x(1)]; 84 | % distances along element j to intersection points: 85 | IntersectLines{j,2} = [IntersectLines{j,2}; x(2)]; 86 | end 87 | end 88 | end 89 | % remove possible multiple values 90 | for i = 1:n 91 | IntersectLines{i,1} = unique(IntersectLines{i,1}); 92 | IntersectLines{i,2} = min(IntersectLines{i,2}); 93 | end 94 | end 95 | 96 | elseif dim == 3 % 3d curves 97 | % directions (unit vectors) of the line elements 98 | DirLines = [1./L.*V(:,1) 1./L.*V(:,2) 1./L.*V(:,3)]; 99 | Intersect = false; 100 | if nargout == 1 % check only if the curve intersects 101 | while i <= n-1 102 | % Select the line elements that can intersect element i 103 | if i > 1 104 | I = Ind > i+1 | Ind < i-1; 105 | else 106 | I = Ind > i+1 & Ind < n; 107 | end 108 | % Solve for possible intersection points 109 | [~,DistOnRay,DistOnLines] = distances_between_lines(... 110 | Curve(i,:),DirLines(i,:),Curve(I,:),DirLines(I,:)); 111 | if any(DistOnRay >= 0 & DistOnRay <= L(i) &... 112 | DistOnLines > 0 & DistOnLines <= L(I)) 113 | Intersect = true; 114 | i = n; 115 | else 116 | i = i+1; % study the next line element 117 | end 118 | end 119 | else % determine also all intersection points (line elements) 120 | IntersectLines = cell(n,2); 121 | for i = 1:n-1 122 | % Select the line elements that can intersect element i 123 | if i > 1 124 | I = Ind > i+1 | Ind < i-1; 125 | else 126 | I = Ind > i+1 & Ind < n; 127 | end 128 | % Solve for possible intersection points 129 | [D,DistOnRay,DistOnLines] = distances_between_lines(... 130 | Curve(i,:),DirLines(i,:),Curve(I,:),DirLines(I,:)); 131 | if any(DistOnRay >= 0 & DistOnRay <= L(i) & ... 132 | DistOnLines > 0 & DistOnLines <= L(I)) 133 | Intersect = true; 134 | J = DistOnRay >= 0 & DistOnRay <= L(i) & ... 135 | DistOnLines > 0 & DistOnLines <= L(I); 136 | ind = Ind(I); 137 | ind = ind(J); 138 | DistOnLines = DistOnLines(J); 139 | IntersectLines{i,1} = ind; 140 | IntersectLines{i,2} = DistOnRay(J); 141 | % Record the elements intersecting 142 | for j = 1:length(ind) 143 | IntersectLines{ind(j),1} = [IntersectLines{ind(j),1}; i]; 144 | IntersectLines{ind(j),2} = [IntersectLines{ind(j),2}; DistOnLines(j)]; 145 | end 146 | end 147 | end 148 | % remove possible multiple values 149 | for i = 1:n 150 | IntersectLines{i} = unique(IntersectLines{i}); 151 | IntersectLines{i,2} = min(IntersectLines{i,2}); 152 | end 153 | end 154 | end 155 | else % Empty curve 156 | Intersect = false; 157 | IntersectLines = cell(1,1); 158 | end 159 | -------------------------------------------------------------------------------- /src/triangulation/initial_boundary_curve.m: -------------------------------------------------------------------------------- 1 | % This file is part of TREEQSM. 2 | % 3 | % TREEQSM is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % TREEQSM is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with TREEQSM. If not, see . 15 | 16 | function Curve = initial_boundary_curve(P,TriaWidth) 17 | 18 | % --------------------------------------------------------------------- 19 | % INITIAL_BOUNDARY_CURVE.M Determines the boundary curve adaptively. 20 | % 21 | % Version 1.0.1 22 | % Latest update 26 Nov 2019 23 | % 24 | % Copyright (C) 2015-2017 Pasi Raumonen 25 | % --------------------------------------------------------------------- 26 | 27 | % Changes from version 1.0.0 to 1.0.1, 26 Nov 2019: 28 | % 1) Bug fix: Added "return" if the "Curve" is empty after it is first defined. 29 | 30 | %% Define suitable center 31 | % Use xy-data and even the z-coordinate to the top 32 | Top = max(P(:,3)); 33 | P = [P(:,1:2) Top*ones(size(P,1),1)]; 34 | 35 | % Define the "center" of points as the mean 36 | Center = mean(P); 37 | Center0 = Center; 38 | 39 | % If the center is outside or close to the boundary, define new center 40 | i = 0; 41 | A0 = 61; 42 | ShortestDist = 0; 43 | while ShortestDist < 0.075 && i < 100 44 | Center = Center0+[3*ShortestDist*randn(1,2) 0]; % Randomly move the center 45 | % Compute angles of points as seen from the center 46 | V = mat_vec_subtraction(P(:,1:2),Center(1:2)); 47 | angle = 180/pi*atan2(V(:,2),V(:,1))+180; 48 | % % Check if the center is outside or near the boundary of the cross section 49 | A = false(70,1); 50 | a = ceil(angle/5); 51 | I = a > 0; 52 | A(a(I)) = true; 53 | if i == 0 54 | ShortestDist = 0.025; 55 | elseif nnz(A) < A0 56 | ShortestDist = 0.05; 57 | else 58 | PointDist = sqrt(sum(V.*V,2)); 59 | [ShortestDist,FirstPoint] = min(PointDist); 60 | end 61 | i = i+1; 62 | if i == 100 && ShortestDist < 0.075 63 | i = 0; 64 | A0 = A0-2; 65 | end 66 | end 67 | 68 | %% Define first boundary curve based on the center 69 | Curve = zeros(18,1); % the boundary curve, contains indexed of the point cloud rows 70 | Curve(1) = FirstPoint; % start the curve from the point the closest the center 71 | % Modify the angles so that first point has the angle 0 72 | a0 = angle(FirstPoint); 73 | I = angle < a0; 74 | angle(I) = angle(I)+(360-a0); 75 | angle(~I) = angle(~I)-a0; 76 | % Select the rest of the points as the closest point in 15 deg sectors 77 | % centered at 20 deg intervals 78 | np = size(P,1); 79 | Ind = (1:1:np)'; 80 | t = 0; 81 | for i = 2:18 82 | J = angle > 12.5+20*(i-2) & angle < 27.5+20*(i-2); 83 | if ~any(J) % if no points, try 18 deg sector 84 | J = angle > 11+20*(i-2) & angle < 29+20*(i-2); 85 | end 86 | if any(J) 87 | % if sector has points, select the closest point as the curve point 88 | D = PointDist(J); 89 | ind = Ind(J); 90 | [~,J] = min(D); 91 | t = t+1; 92 | Curve(t) = ind(J); 93 | end 94 | end 95 | Curve = Curve(1:t); 96 | if isempty(Curve) 97 | return 98 | end 99 | I = true(np,1); 100 | I(Curve) = false; 101 | Ind = Ind(I); 102 | 103 | 104 | %% Adapt the initial curve to the data 105 | V = P(Curve([(2:t)'; 1]),:)-P(Curve,:); 106 | D = sqrt(sum(V(:,1:2).*V(:,1:2),2)); 107 | n = t; 108 | n0 = 1; 109 | % Continue adding new points as long as too long edges exists 110 | while any(D > 1.25*TriaWidth) && n > n0 111 | N = [V(:,2) -V(:,1) V(:,3)]; 112 | M = P(Curve,:)+0.5*V; 113 | 114 | Curve1 = Curve; 115 | t = 0; 116 | for i = 1:n 117 | if D(i) > 1.25*TriaWidth 118 | [d,~,hc] = distances_to_line(P(Curve1,:),N(i,:),M(i,:)); 119 | I = hc > 0.01 & d < D(i)/2; 120 | if any(I) 121 | H = min(hc(I)); 122 | else 123 | H = 1; 124 | end 125 | [d,~,h] = distances_to_line(P(Ind,:),N(i,:),M(i,:)); 126 | I = d < D(i)/3 & h > -TriaWidth/2 & h < H; 127 | 128 | if any(I) 129 | ind = Ind(I); 130 | h = h(I); 131 | [h,J] = min(h); 132 | I = ind(J); 133 | 134 | t = t+1; 135 | if i < n 136 | Curve1 = [Curve1(1:t); I; Curve1(t+1:end)]; 137 | else 138 | Curve1 = [Curve1(1:t); I]; 139 | end 140 | J = Ind ~= I; 141 | Ind = Ind(J); 142 | t = t+1; 143 | 144 | else 145 | t = t+1; 146 | end 147 | else 148 | t = t+1; 149 | end 150 | end 151 | Curve = Curve1(1:t); 152 | 153 | n0 = n; 154 | n = size(Curve,1); 155 | V = P(Curve([(2:n)'; 1]),:)-P(Curve,:); 156 | D = sqrt(sum(V.*V,2)); 157 | end 158 | 159 | %% Refine the curve for longer edges if far away points 160 | n0 = n-1; 161 | while n > n0 162 | N = [V(:,2) -V(:,1) V(:,3)]; 163 | M = P(Curve,:)+0.5*V; 164 | 165 | Curve1 = Curve; 166 | t = 0; 167 | for i = 1:n 168 | if D(i) > 0.5*TriaWidth 169 | [d,~,hc] = distances_to_line(P(Curve1,:),N(i,:),M(i,:)); 170 | I = hc > 0.01 & d < D(i)/2; 171 | if any(I) 172 | H = min(hc(I)); 173 | else 174 | H = 1; 175 | end 176 | [d,~,h] = distances_to_line(P(Ind,:),N(i,:),M(i,:)); 177 | I = d < D(i)/3 & h > -TriaWidth/3 & h < H; 178 | ind = Ind(I); 179 | h = h(I); 180 | [h,J] = min(h); 181 | 182 | if h > TriaWidth/10 183 | I = ind(J); 184 | t = t+1; 185 | if i < n 186 | Curve1 = [Curve1(1:t); I; Curve1(t+1:end)]; 187 | else 188 | Curve1 = [Curve1(1:t); I]; 189 | end 190 | J = Ind ~= I; 191 | Ind = Ind(J); 192 | t = t+1; 193 | 194 | else 195 | t = t+1; 196 | end 197 | else 198 | t = t+1; 199 | end 200 | 201 | end 202 | Curve = Curve1(1:t); 203 | 204 | n0 = n; 205 | n = size(Curve,1); 206 | V = P(Curve([(2:n)'; 1]),:)-P(Curve,:); 207 | D = sqrt(sum(V.*V,2)); 208 | end 209 | 210 | %% Smooth the curve by defining the points by means of neighbors 211 | Curve = P(Curve,:); % Change the curve from point indexes to coordinates 212 | Curve = boundary_curve2(P,Curve,0.04,TriaWidth); 213 | if isempty(Curve) 214 | return 215 | end 216 | 217 | %% Add points for too long edges 218 | n = size(Curve,1); 219 | V = Curve([(2:n)'; 1],:)-Curve; 220 | D = sqrt(sum(V.*V,2)); 221 | Curve1 = Curve; 222 | t = 0; 223 | for i = 1:n 224 | if D(i) > TriaWidth 225 | m = floor(D(i)/TriaWidth); 226 | t = t+1; 227 | W = zeros(m,3); 228 | for j = 1:m 229 | W(j,:) = Curve(i,:)+j/(m+1)*V(i,:); 230 | end 231 | Curve1 = [Curve1(1:t,:); W; Curve1(t+1:end,:)]; 232 | t = t+m ; 233 | else 234 | t = t+1; 235 | end 236 | end 237 | Curve = Curve1; 238 | n = size(Curve,1); 239 | 240 | %% Define the curve again by equalising the point distances along the curve 241 | V = Curve([(2:n)'; 1],:)-Curve; 242 | D = sqrt(sum(V.*V,2)); 243 | L = cumsum(D); 244 | m = ceil(L(end)/TriaWidth); 245 | TriaWidth = L(end)/m; 246 | Curve1 = zeros(m,3); 247 | Curve1(1,:) = Curve(1,:); 248 | b = 1; 249 | for i = 2:m 250 | while L(b) < (i-1)*TriaWidth 251 | b = b+1; 252 | end 253 | if b > 1 254 | a = ((i-1)*TriaWidth-L(b-1))/D(b); 255 | Curve1(i,:) = Curve(b,:)+a*V(b,:); 256 | else 257 | a = (L(b)-(i-1)*TriaWidth)/D(b); 258 | Curve1(i,:) = Curve(b,:)+a*V(b,:); 259 | end 260 | end 261 | Curve = Curve1; 262 | 263 | Intersect = check_self_intersection(Curve(:,1:2)); 264 | if Intersect 265 | Curve = zeros(0,3); 266 | end 267 | 268 | --------------------------------------------------------------------------------