-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathaxedetect.m
413 lines (347 loc) · 13.5 KB
/
axedetect.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
function [Clusters,Axes,remainPc] = axedetect(datapc,beamWidth)
% AXEDETECT
%
% Function to detect the main axes in a planar point cloud and segment it
% automatically according to the detected axes. The algorithm employs
% Principal Componen Analysis (PCA) to transform the 3D point cloud into a
% 2D binary image, and then uses 2D Hough Transform to detect the edges and
% thereafter determines the axes.
%
% Inputs:
% - datapc: input point cloud. The point cloud should be (more or less)
% planar
% - beamWidth: approximate width of the point cloud
%
% Outputs:
% - Clusters: a struct containing the segmented point clouds (segmented
% according the detected axes)
% - Axes: a struct containing the attributes of the axes (in 2D)
% - remainPc : the remaining unsegmented point cloud
%
% (c) Arnadi Murtiyoso (INSA Strasbourg - ICube-TRIO UMR 7357)
% tic
%% step 1: transform the point cloud into a planar XY coordinate system
% compute the Principal Component Analysis
coeffs = pca(datapc.Location);
% transform the point cloud to PC-aligned system
TransformPCA = datapc.Location*coeffs(:,1:3);
% figure('name','Transformed Point Cloud')
% pcshow(pcTransformPCA)
% project the point cloud to a plane (Z=0)
TransformPCA(:,3)=0;
pcTransformPCA = pointCloud(TransformPCA);
% determine the point cloud boundaries
minX=min(TransformPCA(:,1));
maxX=max(TransformPCA(:,1));
minY=min(TransformPCA(:,2));
maxY=max(TransformPCA(:,2));
% raster dimensions in project unit
width_m=max(TransformPCA(:,1))-min(TransformPCA(:,1));
height_m=max(TransformPCA(:,2))-min(TransformPCA(:,2));
% resolution of the raster in project unit; modifiable
resolution_pix=0.01; %this is for standard
%resolution_pix=0.005; %this is for 1cm subsampled data
% raster dimensions in pixel
width_pix=ceil(width_m/resolution_pix);
height_pix=ceil(height_m/resolution_pix);
%% step 2: create the binary image of the projected point cloud
% initialise the empty raster
raster=zeros(height_pix,width_pix);
% optional: create a waitbar
f = waitbar(0,'Converting to BW image...','Name','axedetect.m');
k=1;
% fill the pixels
for i=1:height_pix
for j=1:width_pix
% find the points located inside the pixel
I=find(TransformPCA(:,1)>minX & TransformPCA(:,1)< ...
minX+resolution_pix & TransformPCA(:,2)>minY & ...
TransformPCA(:,2)<minY+resolution_pix);
% if the pixel contains no points, give a black color
if isempty(I)
raster(i,j)=0;
% if the pixel contains points, give a white color
else
raster(i,j)=1;
end
% continuing on for the rows...
minX=minX+resolution_pix;
waitbar(k/(width_pix*height_pix),f)
k=k+1;
end
% continuing on for the columns...
minY=minY+resolution_pix;
minX=min(TransformPCA(:,1));
end
% flip the resulting raster (needed because of the raster coord system)
raster=flip(raster);
% close the waitbar
close(f)
figure('name','Transformed Raster')
imshow(raster)
%% step 3: perform Hough Transform to detect lines
% detect edges in the raster
BW = edge(raster,'canny');
% Compute the Hough transform of the binary image returned by edge
[H,theta,rho] = hough(BW);
% % Optional: Display the transform, H, returned by the hough function.
% figure
% imshow(imadjust(rescale(H)),[],...
% 'XData',theta,...
% 'YData',rho,...
% 'InitialMagnification','fit');
% xlabel('\theta (degrees)')
% ylabel('\rho')
% axis on
% axis normal
% hold on
% colormap(gca,hot)
% Find the peaks in the Hough transform matrix, H, using the houghpeaks
% function.
%P = houghpeaks(H,10,'threshold',ceil(0.3*max(H(:))));
P = houghpeaks(H,15,'threshold',ceil(0.3*max(H(:))));
% Find lines in the image using the houghlines function.
%lines = houghlines(BW,theta,rho,P,'FillGap',5,'MinLength',7);
lines = houghlines(BW,theta,rho,P);
% % Optional: create a plot that displays the original image with the lines
% % superimposed on it.
% figure, imshow(raster), hold on
% for k = 1:length(lines)
% xy = [lines(k).point1; lines(k).point2];
% plot(xy(:,1),xy(:,2),'LineWidth',2,'Color','green');
% % waitforbuttonpress;
%
% % Plot beginnings and ends of lines
% plot(xy(1,1),xy(1,2),'x','LineWidth',2,'Color','yellow');
% plot(xy(2,1),xy(2,2),'x','LineWidth',2,'Color','red');
%
% end
%% step 4: filter the lines generated by Hough Transform (remove colinear vectors)
% create a dummy duplicate of the list
if isempty(lines)
disp('Data quality insufficient to determine axes! Assuming 1 axis...');
Clusters(1).ptCloud=datapc;
Axes=[];
remainPc=[];
return
end
linesDummy=lines;
j=1;
% while the duplicate still has elements...
while ~isempty(linesDummy)
[~,nbLines]=size(linesDummy);
% take the first row as reference
refTheta=linesDummy(1).theta;
refRho=linesDummy(1).rho;
% create Clusters to stock the colinear lines
ClusterName=strcat('Cluster',num2str(j));
% create a struct to stock the clusters
lines2.(ClusterName){1}=linesDummy(1);
for i=2:nbLines
% use the subsequent rows as check
checkTheta=linesDummy(i).theta;
checkRho=linesDummy(i).rho;
% if the difference between reference and check is less than 10
% degrees an distance less than 10 object units, add to cluster
if abs(refTheta-checkTheta)<10 && abs(refRho-checkRho)<10
lines2.(ClusterName){i}=linesDummy(i);
%when a row has been added to the cluster, put its values as NaN
linesDummy(i).point1=NaN;
linesDummy(i).point2=NaN;
linesDummy(i).theta=NaN;
linesDummy(i).rho=NaN;
end
end
% put the values of the reference (1st row) as NaN
linesDummy(1).point1=NaN;
linesDummy(1).point2=NaN;
linesDummy(1).theta=NaN;
linesDummy(1).rho=NaN;
% delete the struct row with NaNs
F = @(s)any(structfun(@(a)isscalar(a)&&isnan(a),s)); % or ALL
X = arrayfun(F,linesDummy);
linesDummy(X) = [];
% eliminate empty struct
lines2.(ClusterName)=lines2.(ClusterName)(~cellfun('isempty',...
lines2.(ClusterName)));
% not important: transpose the result
lines2.(ClusterName)=transpose(lines2.(ClusterName));
j=j+1;
end
%% step 5: simplify the colinear Hough lines (merge them)
nbColinears=numel(fieldnames(lines2));
for i=1:nbColinears
ClusterNm=string(strcat('Cluster',{num2str(i)}));
[nbColinearEls,~]=size(lines2.(ClusterNm));
%look for extremes
listPotentialExtremes1 = zeros(nbColinearEls,2);
listPotentialExtremes2 = zeros(nbColinearEls,2);
for k=1:nbColinearEls
%get a list of point 1s
listPotentialExtremes1(k,1) = lines2.(ClusterNm){k}.point1(1);
listPotentialExtremes1(k,2) = lines2.(ClusterNm){k}.point1(2);
%get a list of point 2s
listPotentialExtremes2(k,1) = lines2.(ClusterNm){k}.point2(1);
listPotentialExtremes2(k,2) = lines2.(ClusterNm){k}.point2(2);
end
%merge the list
listPotentialExtremes=[listPotentialExtremes1;listPotentialExtremes2];
%compute the centroid of these points
centroid=[mean(listPotentialExtremes(:,1)), ...
mean(listPotentialExtremes(:,2))];
%compute the length of each node to the centroid
for k=1:length(listPotentialExtremes)
listPotentialExtremes(k,3)=sqrt((centroid(1)- ...
listPotentialExtremes(k,1))^2+((centroid(2)- ...
listPotentialExtremes(k,2))^2));
end
% take the two points located farthest from centroid as extremities
[~, ind1] = max(listPotentialExtremes(:,3));
listPotentialExtremes(ind1,3) = -Inf;
[~, ind2] = max(listPotentialExtremes(:,3));
listPotentialExtremes(ind2,3) = -Inf;
% create a struct to store the simplified lines, taking the ind1 and
% ind2 points as the extremities
Vector(i).point1=listPotentialExtremes(ind1,1:2)*resolution_pix;
Vector(i).point1(1)=Vector(i).point1(1)+minX;
Vector(i).point1(2)=maxY-Vector(i).point1(2);
Vector(i).point2=listPotentialExtremes(ind2,1:2)*resolution_pix;
Vector(i).point2(1)=Vector(i).point2(1)+minX;
Vector(i).point2(2)=maxY-Vector(i).point2(2);
Vector(i).theta=lines2.(ClusterNm){1,1}.theta;
Vector(i).rho=lines2.(ClusterNm){1,1}.rho;
Vector(i).length=sqrt((Vector(i).point1(1)-Vector(i).point2(1))^2+...
(Vector(i).point1(2)-Vector(i).point2(2))^2);
end
% % Optional: show the detected merged lines superposed on the point cloud
% figure
% pcshow(pcTransformPCA)
% hold on
% for k = 1:length(Vector)
% xy = [Vector(k).point1; Vector(k).point2];
% plot3(xy(:,1),xy(:,2),[0;0],'LineWidth',2);
% end
%% step 6: determine the number of axes in the input data
i=1;
% create a duplicate of the previous structure
VectorDummy=Vector;
if isempty(Vector)
disp('Data quality insufficient to determine axes! Assuming 1 axis...');
Clusters(1).ptCloud=datapc;
Axes=[];
remainPc=[];
return
end
Axes=[];
while ~isempty(VectorDummy)
%take the longest row as reference
a=[VectorDummy.length];
[~,idx]=max(a);
refTheta=VectorDummy(idx).theta;
% look for rows whose theta column is similar to the reference
fun = @(x) VectorDummy(x).theta > refTheta-5 && ...
VectorDummy(x).theta < refTheta+5; % useful for complicated fields
tf2 = arrayfun(fun, 1:numel(VectorDummy));
index2 = find(tf2);
% get the number of lines having the same theta direction
[~,nbLines] = size(index2);
% if there is only 1 line or more than 2, impossible to determine the
% axe...
if nbLines<2 || nbLines>2
VectorDummy(idx).theta=NaN;
%delete the struct row with NaNs
F = @(s)any(structfun(@(a)isscalar(a)&&isnan(a),s)); % or ALL
X = arrayfun(F,VectorDummy);
VectorDummy(X) = [];
continue
end
% the lines represent the edges of the point cloud. We want to get the
% axe which is located at the center. Solution: averaging the two line
% equations
% first, determine the line equation components (a and b in y=ax+b)
x1a=VectorDummy(index2(1)).point1(1);
y1a=VectorDummy(index2(1)).point1(2);
x2a=VectorDummy(index2(1)).point2(1);
y2a=VectorDummy(index2(1)).point2(2);
% a and b of the first line
aa=(y1a-y2a)/(x1a-x2a);
ba=(y2a*x1a-y1a*x2a)/(x1a-x2a);
x1b=VectorDummy(index2(2)).point1(1);
y1b=VectorDummy(index2(2)).point1(2);
x2b=VectorDummy(index2(2)).point2(1);
y2b=VectorDummy(index2(2)).point2(2);
% a and b of the second line
ab=(y1b-y2b)/(x1b-x2b);
bb=(y2b*x1b-y1b*x2b)/(x1b-x2b);
% put the information in a struct 'Axes'
Axes(i).a=(aa+ab)/2;
Axes(i).b=(ba+bb)/2;
Axes(i).theta=VectorDummy(index2(1)).theta;
% set an arbitrary first and second point for the line representing the
% axe
Axes(i).x1 = minX;
Axes(i).y1 = (minX*Axes(i).a)+Axes(i).b;
Axes(i).z1 = 0;
Axes(i).x2 = maxX;
Axes(i).y2 = (maxX*Axes(i).a)+Axes(i).b;
Axes(i).z2 = 0;
% when the duplicate row has been used, set one of its elements to NaN
VectorDummy(index2(1)).theta=NaN;
VectorDummy(index2(2)).theta=NaN;
%delete the struct row with NaNs
F = @(s)any(structfun(@(a)isscalar(a)&&isnan(a),s)); % or ALL
X = arrayfun(F,VectorDummy);
VectorDummy(X) = [];
i=i+1;
end
if isempty(Axes)
disp('Data quality insufficient to determine axes! Assuming 1 axis...');
Clusters(1).ptCloud=datapc;
remainPc=[];
return
elseif length(Axes)==1
disp('1 axis was found!');
Clusters(1).ptCloud=datapc;
remainPc=[];
Axes=[];
return
end
nbAxes = length(Axes);
disp(strcat(num2str(nbAxes),32,'axes were found!'));
% Plot the axes superposed on the (PCA-transformed) point cloud
figure
pcshow(pcTransformPCA)
title(strcat('Number of axes found:',num2str(nbAxes)))
hold on
for k = 1:nbAxes
plot3([Axes(k).x1;Axes(k).x2],[Axes(k).y1;Axes(k).y2],[0;0],...
'LineWidth',2);
end
%% step 7: segment the point cloud according to the axe
% create duplicates just in case
ptCloudinput=datapc;
ptCloudinput2=pcTransformPCA;
% do a loop for each detected axe
for k=1:nbAxes
% create a line with the two arbitrary points
P = [Axes(k).x1 Axes(k).y1;Axes(k).x2 Axes(k).y2];
% create a buffer zone around this line, using the width as input
polyout1 = polybuffer(P,'lines',(beamWidth+0.2*beamWidth)/2);
% check if there are points inside the buffer zone
TFin = isinterior(polyout1,ptCloudinput2.Location(:,1), ...
ptCloudinput2.Location(:,2));
% retrieve the indices of points located inside the buffer zone
rows = find(TFin(:,1)==1);
% segment the point cloud with the inlier indices directly from the
% original input point cloud
ptCloudAxe=select(ptCloudinput,rows);
% retrive the remaining points
rows2 = find(TFin(:,1)==0);
remainPcPCA=select(ptCloudinput2,rows2);
ptCloudinput2=remainPcPCA;
remainPc=select(ptCloudinput,rows2);
ptCloudinput=remainPc;
% create a struct containing the segmented point clouds
Clusters(k).ptCloud=ptCloudAxe;
end
% toc