21 from opals
import Histo, Import, Normals, Segmentation, pyDM
24 import matplotlib.pyplot
as plt
25 from opals.tools.opalsargparse
import ArgumentParser, Boolean
33 parser = ArgumentParser(description=
"The program reads a point cloud file, extracts possible gable roof segments and calculates the ridge line. The output is a 3d plot of the roof segments and their ridge lines")
36 parser.add_argument(
"-inFile",
39 help=
"Point cloud input file to be analyzed"
43 parser.add_argument(
"-outFile",
46 help=
"Base name for generated output files")
49 parser.add_argument(
"-searchRadius",
52 help=
"Search radius (in meters) for normal computation and segmentation"
54 parser.add_argument(
"-neighbors",
57 help=
"Number of neighbors used for normal vector computation"
59 parser.add_argument(
"-skipIfExists",
62 help=
"Skip Import, Normals, and Segmentation steps if outputs already exist"
65 parser.add_argument(
"-minSegSize",
68 help=
"Minimum number of points required per segment"
71 parser.add_argument(
"-maxDist",
74 help=
"Maximum allowed point-to-plane distance during plane extraction"
77 parser.add_argument(
"-maxSigma",
80 help=
"Maximum sigma0 value allowed during plane adjustment"
83 parser.add_argument(
"-normalSigma0",
86 help=
"Points with a NormalSigma0 below this threshold are used as seed candidates"
89 parser.add_argument(
"-alphaRadius",
92 help=
"Radius for alpha shape construction"
95 parser.add_argument(
"-radius",
98 help=
"Search radius for local homogeneity checks")
100 parser.add_argument(
"-searchExpand",
103 help=
"Expands the bounding box by this value (in meters) when searching for intersecting segments"
106 parser.add_argument(
"-mode",
109 choices=[
"all",
"iterative"],
110 help=
"Plot mode: 'all' for a single figure, 'iterative' for one figure per pair"
113 parser.add_argument(
"-outputMode",
116 choices=[
"show",
"save",
"both"],
117 help=
"Output mode: show plots, save them as .svg, or both"
120 args = parser.parse_args()
122 if args.outFile
is None:
123 base = os.path.splitext(os.path.basename(args.inFile))[0]
133 def get_boxes(geometry, expand):
134 box = pyDM.Box(geometry)
143 def check_gable_roof_normals(nx1, ny1, nz1, nx2, ny2, nz2, cogz1, cogz2):
144 angle_thres = math.pi / 180 * 5
145 rel_distance_thres = 0.10
146 height_diff_thres = 0.1
148 if None in (nx1, ny1, nz1, nx2, ny2, nz2):
151 nx1, ny1, nz1 = -nx1, -ny1, -nz1
153 nx2, ny2, nz2 = -nx2, -ny2, -nz2
155 alpha1 = math.atan2(ny1, nx1)
156 d1 = math.hypot(nx1, ny1)
157 alpha2 = math.atan2(ny2, nx2) + math.pi
158 d2 = math.hypot(nx2, ny2)
160 dalpha = alpha1 - alpha2
162 while dalpha < -math.pi:
163 dalpha += 2 * math.pi
164 while dalpha > math.pi:
165 dalpha += 2 * -math.pi
167 angle_crit = abs(dalpha) < angle_thres
168 distance_crit = (1 - d1 / d) < rel_distance_thres
and (1 - d2 / d) < rel_distance_thres
169 heightdiff_crit = abs(cogz1-cogz2) < height_diff_thres
171 return angle_crit
and distance_crit
and heightdiff_crit
178 def compute_ridge_line(c1, n1, c2, n2):
179 n1 = np.asarray(n1, dtype=float)
180 n2 = np.asarray(n2, dtype=float)
181 c1 = np.asarray(c1, dtype=float)
182 c2 = np.asarray(c2, dtype=float)
187 offset = -np.dot(n2, c2 - c1)
191 v_squared = np.dot(v, v)
196 p0_red = (d1 * np.cross(n2, v) + d2 * np.cross(v, n1)) / v_squared
197 p0_global = p0_red + c1
201 def compute_ridge_line_segment(p0, v, poa1, poa2):
204 for i
in range(len(poa1[0])):
205 p = np.array([poa1[0][i], poa1[1][i], poa1[2][i]])
206 lam = np.dot((p - p0), v) / v_2
209 for i
in range(len(poa2[0])):
210 p = np.array([poa2[0][i], poa2[1][i], poa2[2][i]])
211 lam = np.dot((p - p0), v) / v_2
214 p1 = p0 + min(lambdas) * v
215 p2 = p0 + max(lambdas) * v
219 def set_axes_radius(ax, origin, radius):
220 ax.set_xlim3d([origin[0] - radius, origin[0] + radius])
221 ax.set_ylim3d([origin[1] - radius, origin[1] + radius])
222 ax.set_zlim3d([origin[2] - radius, origin[2] + radius])
230 def set_axes_equal(ax):
237 origin = np.mean(limits, axis=1)
238 radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
239 set_axes_radius(ax, origin, radius)
244 if args.skipIfExists ==
False or not os.path.exists(f
"{args.outFile}_segments.odm"):
246 imp = Import.Import()
247 imp.inFile = args.inFile
248 imp.outFile = f
"{args.outFile}.odm"
253 norm = Normals.Normals()
254 norm.inFile = f
"{args.outFile}.odm"
255 norm.neighbours = int(args.neighbors)
256 norm.searchRadius = float(args.searchRadius)
261 seg = Segmentation.Segmentation()
262 seg.inFile = f
"{args.outFile}.odm"
263 seg.searchRadius = float(args.radius)
264 seg.minSegSize = int(args.minSegSize)
265 seg.method = opals.Types.SegmentationMethod.planeExtraction
266 seg.planeExtraction.maxDist = float(args.maxDist)
267 seg.planeExtraction.maxSigma = float(args.maxSigma)
268 seg.alphaRadius = int(args.alphaRadius)
269 seg.planeExtraction.seedCalculator = f
"NormalSigma0<{args.normalSigma0} ? NormalSigma0 : invalid"
270 seg.byproduct = f
"{args.outFile}_segments.odm"
275 f = pyDM.AddInfoLayoutFactory()
276 f.addColumn(pyDM.ColumnSemantic.SegmentID)
277 f.addColumn(pyDM.ColumnType.bool_,
"_AlphaShape")
278 f.addColumn(pyDM.ColumnType.double_,
"_cogx")
279 f.addColumn(pyDM.ColumnType.double_,
"_cogy")
280 f.addColumn(pyDM.ColumnType.double_,
"_cogz")
281 f.addColumn(pyDM.ColumnSemantic.NormalX)
282 f.addColumn(pyDM.ColumnSemantic.NormalY)
283 f.addColumn(pyDM.ColumnSemantic.NormalZ)
284 f.addColumn(pyDM.ColumnType.uint32,
"_PointCount")
285 layout = f.getLayout()
288 odm = pyDM.Datamanager.load(f
"{args.outFile}_segments.odm",
True,
False)
289 pi = odm.getPolylineIndex()
295 for geom
in odm.geometries(layout):
296 box = get_boxes(geom, expand= float(args.searchExpand))
302 nx1, ny1, nz1 = info.get(5), info.get(6), info.get(7)
304 pointCountA = info.get(8)
306 window = pyDM.Window(box)
307 segs = pi.searchGeometry(window, pyDM.SpatialQueryMode.intersect)
310 seg.cloneAddInfoView(layout,
True)
315 nx2, ny2, nz2 = sinfo.get(5), sinfo.get(6), sinfo.get(7)
317 pointCountB = sinfo.get(8)
319 if check_gable_roof_normals(nx1, ny1, nz1, nx2, ny2, nz2, cogz1, cogz2):
320 total_points = pointCountA + pointCountB
321 pair = tuple(sorted((boxID,segID)))
322 existing_pair = next((p
for p
in pairs.keys()
if boxID
in p
or segID
in p),
None)
324 if pairs[existing_pair] < total_points:
325 del pairs[existing_pair]
326 pairs[pair] = total_points
328 pairs[pair] = total_points
329 print(f
"found {len(pairs)} potential roof pairs")
335 for geom
in odm.geometries(layout):
340 n = (info.get(5), info.get(6), info.get(7))
341 c = (info.get(2), info.get(3), info.get(4))
343 boxes[segID] = dict(n=n, c=c, xmin=b.xmin, ymin=b.ymin, xmax=b.xmax, ymax=b.ymax)
346 points_of_alphashape = dict()
347 for geom
in odm.geometries(layout):
353 all_x = [pt.x
for pt
in geom.points()]
354 all_y = [pt.y
for pt
in geom.points()]
355 all_z = [pt.z
for pt
in geom.points()]
356 points_of_alphashape[segID] = [all_x, all_y, all_z]
362 for segA, segB
in pairs.keys():
363 A, B = boxes[segA], boxes[segB]
364 p0, v = compute_ridge_line(A[
"c"], A[
"n"], B[
"c"], B[
"n"])
366 p1, p2 = compute_ridge_line_segment(p0, v, points_of_alphashape[segA], points_of_alphashape[segB])
368 ridge_lines.append((segA, segB, tuple(p1.tolist()), tuple(p2.tolist())))
370 print(f
"{len(ridge_lines)} ridge lines found")
376 def plot_results(pairs, ridge_lines, layout, odm, title=""):
377 fig = plt.figure(figsize=(10, 8))
378 ax = fig.add_subplot(111, projection=
"3d")
381 colors_of_pair = dict()
383 color = np.random.rand(3,)
385 paired_ids.add(segID)
386 colors_of_pair[segID] = color
389 for geom
in odm.geometries(layout):
392 isAlpha = info.get(1)
393 if not isAlpha
or segID
not in paired_ids:
399 for part
in geom.parts():
400 xs = [pt.x
for pt
in part.points()]
401 ys = [pt.y
for pt
in part.points()]
402 zs = [pt.z
for pt
in part.points()]
403 ax.plot(xs, ys, zs, color=colors_of_pair[segID], linewidth=1.0, alpha=0.7)
406 for (a, b, p1, p2)
in ridge_lines:
407 if (a, b)
in pairs
or (b, a)
in pairs:
411 ax.plot(xs, ys, zs, color=
"k", linewidth=1.0)
420 output_mode = args.outputMode
425 print(
"creating one plot to show all roof pairs")
426 fig, ax = plot_results(pairs.keys(), ridge_lines, layout, odm,
"all roof pairs")
427 filename=f
"{args.outFile}_all_roof_pairs.svg"
428 if output_mode
in (
"save",
"both"):
429 plt.savefig(filename)
430 if output_mode ==
"save":
432 if output_mode in(
"show",
"both"):
435 elif mode ==
"iterative":
437 print(
"creating one plot for each of the pairs")
438 for i, pair
in enumerate(pairs.keys()):
439 fig, ax = plot_results([pair], ridge_lines, layout, odm, f
"Roof pair {i + 1}: {pair}")
440 filename = f
"{args.outFile}_roof_pair_{i + 1}.svg"
441 if output_mode
in (
"save",
"both"):
442 plt.savefig(filename)
443 if output_mode ==
"save":
447 if output_mode
in (
"show",
"both"):
452 if __name__ ==
"__main__":