├── .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 | [](https://zenodo.org/badge/latestdoi/100592530)
7 |
8 | 
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 |
--------------------------------------------------------------------------------