-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgenerateall.py
More file actions
1508 lines (1204 loc) · 51.7 KB
/
Copy pathgenerateall.py
File metadata and controls
1508 lines (1204 loc) · 51.7 KB
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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
整合脚本:从原始 LiDAR scan 数据到完整的道路网络
包含:原始点云 + 墙体检测 + 道路中心线 + 路口识别
"""
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
from shapely.geometry import LineString, MultiPoint, Point
import json
import os
import sys
from functools import partial
# 添加当前目录到Python路径,以便导入其他模块的函数
sys.path.insert(0, os.path.dirname(__file__))
# =========================
# 配置参数
# =========================
# 运行模式:'single' 或 'batch'
RUN_MODE = 'batch' # 改为 'single' 可切换到单帧模式
# 单帧模式配置
INPUT_LASERSCAN_PATH = "extracted_lidar_data_code/extracted_lidar_data/laserscan_json2/laserscan_000457.json"
# 批量模式配置
INPUT_DIR = "extracted_lidar_data_code/extracted_lidar_data/laserscan_json2"
BATCH_INTERVAL = 1 # 每N个文件处理一个
# 输出目录
OUTPUT_DIR = "extracted_lidar_data_code/all_in_one_results2"
# 墙体检测参数
MAX_RANGE = 50.0
X_LIMIT = 10.0
Y_LIMIT = 5.0
JUMP_DIST_THRESH = 0.4
MIN_SEGMENT_POINTS = 8
RDP_EPSILON = 0.03
MIN_SPLIT_POINTS = 3
MIN_RDP_SEGMENT_LENGTH = 0.3 # RDP分割后子段最小长度(米)
WALL_RMSE_THRESH = 0.07
MIN_SEGMENT_LENGTH = 0.3 # 构成墙体的每个线段最小长度(米)
PARAM_DBSCAN_EPS = 0.3
PARAM_DBSCAN_MIN_SAMPLES = 1
ALPHA_SCALE = 0.30 # θ的缩放系数
BETA_SCALE = 0.3 # rho的动态阈值系数
GAP_THRESH = 1.2
MIN_WALL_LENGTH = 0.8
MIN_SEGMENTS_PER_WALL = 0.8
# 道路中心线参数
MAX_ROAD_WIDTH = 3.0
MIN_ROAD_WIDTH = 0.8
PARALLEL_ANGLE_THRESH = 10
MIN_OVERLAP_LENGTH = 0.5 # 最小重叠长度(米)
# 交叉口识别参数
EXTEND_LENGTH = 4.0 # 墙体延长线长度(米)
SUCCESSOR_LENGTH = 4.0 # 后继车道延伸长度(米)
BLOCKING_DISTANCE = 0.5 # 墙体遮挡判定阈值(米)
PERPENDICULAR_ANGLE_THRESH = 30 # 垂直判定角度阈值(度):只用与中心线夹角在60-120度的墙体
# =========================
# 1. 墙体检测核心函数
# =========================
def laserscan_to_xy(laserscan, max_range=None):
"""将 LaserScan 数据转换为笛卡尔坐标"""
angle_min = laserscan['angle_min']
angle_increment = laserscan['angle_increment']
ranges = np.asarray(laserscan['ranges'], dtype=np.float64)
# 有效性与量程过滤
valid = np.isfinite(ranges) & (ranges > 0.0)
if max_range is not None:
valid &= (ranges <= max_range)
if not np.any(valid):
return np.empty((0, 2), dtype=np.float64)
ranges = ranges[valid]
thetas = angle_min + np.arange(len(ranges)) * angle_increment
x = ranges * np.cos(thetas)
y = ranges * np.sin(thetas)
return np.column_stack([x, y])
def segment_by_jump(points_xy, jump_thresh, min_points):
"""基于扫描顺序的跳变分段"""
if len(points_xy) == 0:
return []
segments = []
start_idx = 0
for i in range(1, len(points_xy)):
d = np.linalg.norm(points_xy[i] - points_xy[i-1])
if not np.isfinite(d) or d > jump_thresh:
seg = points_xy[start_idx:i]
if len(seg) >= min_points:
segments.append(seg)
start_idx = i
last = points_xy[start_idx:]
if len(last) >= min_points:
segments.append(last)
return segments
def fit_line_pca(points):
"""PCA 直线拟合"""
if len(points) < 2:
return None
mean = np.mean(points, axis=0)
centered = points - mean
# PCA
cov = centered.T @ centered
eigvals, eigvecs = np.linalg.eigh(cov)
# 主方向(最大特征值对应的特征向量)
direction = eigvecs[:, 1]
# 计算 RMSE
normal = np.array([-direction[1], direction[0]])
distances = np.abs((centered @ normal))
rmse = np.sqrt(np.mean(distances**2))
# 计算端点
t = centered @ direction
t_min, t_max = np.min(t), np.max(t)
p1 = mean + t_min * direction
p2 = mean + t_max * direction
length = float(t_max - t_min)
return {
'p1': p1,
'p2': p2,
'dir': direction,
'length': length,
'rmse': rmse,
'mean': mean
}
def rdp_recursive(points, eps, start_idx=0, end_idx=None):
"""RDP 算法递归实现"""
if end_idx is None:
end_idx = len(points) - 1
if end_idx - start_idx < 1:
return [start_idx, end_idx]
# 计算点到线段的最大距离
p1 = points[start_idx]
p2 = points[end_idx]
vec = p2 - p1
norm = np.linalg.norm(vec)
if norm < 1e-12:
return [start_idx, end_idx]
# 计算所有中间点到线段的距离
distances = np.abs(np.cross(points[start_idx+1:end_idx] - p1, vec)) / norm
if len(distances) == 0:
return [start_idx, end_idx]
max_dist = np.max(distances)
max_idx = start_idx + 1 + np.argmax(distances)
if max_dist > eps:
# 递归处理左右两段
left_indices = rdp_recursive(points, eps, start_idx, max_idx)
right_indices = rdp_recursive(points, eps, max_idx, end_idx)
return left_indices[:-1] + right_indices
else:
return [start_idx, end_idx]
def split_by_rdp(points, eps, min_points, min_length=0.0):
"""使用 RDP 算法分割点云"""
if len(points) < min_points:
return []
key_indices = rdp_recursive(points, eps)
segments = []
for i in range(len(key_indices) - 1):
seg = points[key_indices[i]:key_indices[i+1]+1]
# 过滤条件1:点数太少
if len(seg) < min_points:
continue
# 过滤条件2:线段太短(如果指定了min_length)
if min_length > 0:
seg_length = np.linalg.norm(seg[-1] - seg[0])
if seg_length < min_length:
continue
segments.append(seg)
return segments
def angle_diff_undirected(a, b):
"""计算无向直线的角度差,范围 [-π/2, π/2]"""
d = a - b
d = (d + 0.5 * np.pi) % np.pi - 0.5 * np.pi
return d
def seg_metric(u, v, theta_scale=0.3, k_rho=0.3):
"""自定义线段距离度量(带角度wrap和距离自适应)"""
theta_u, rho_u, r_u = u[0], u[1], u[2]
theta_v, rho_v, r_v = v[0], v[1], v[2]
# ----- 角度部分:带 wrap -----
dtheta = angle_diff_undirected(theta_u, theta_v)
dtheta_norm = dtheta / theta_scale
# ----- rho 部分:动态阈值 T(r) = k_rho * r -----
drho = rho_u - rho_v
r_bar = 0.5 * (r_u + r_v) + 1e-3
T_r = k_rho * r_bar
drho_norm = drho / T_r
# 综合距离(欧氏距离)
return np.sqrt(dtheta_norm**2 + drho_norm**2)
def segment_to_hessian(p1, p2):
"""将线段转换为Hessian法线形式 (θ, ρ)(修复版)"""
# 中点
m = 0.5 * (p1 + p2)
# 方向向量
d = p2 - p1
# 法向量(逆时针旋转90度):方向(dx, dy) → 法向(-dy, dx)
nx = -d[1]
ny = d[0]
norm = np.sqrt(nx**2 + ny**2) + 1e-12
nx /= norm
ny /= norm
# 原点到直线的有符号距离
rho = nx * m[0] + ny * m[1]
# 法向量的角度
theta = np.arctan2(ny, nx)
# 归一化到 [0, π)(因为直线无方向性)
# 如果 theta < 0,加 π,同时翻转 rho 符号(因为法向量反向了)
if theta < 0:
theta += np.pi
rho = -rho
return float(theta), float(rho)
def are_segments_collinear(seg1, seg2, distance_thresh):
"""判断两个线段是否共线
方法:计算一个线段的端点到另一个线段所在直线的距离
如果距离都很小,说明共线
"""
p1_1 = seg1['fit']['p1']
p1_2 = seg1['fit']['p2']
p2_1 = seg2['fit']['p1']
p2_2 = seg2['fit']['p2']
# 计算seg1的方向向量(用于定义直线)
dir1 = p1_2 - p1_1
norm1 = np.linalg.norm(dir1)
if norm1 < 1e-6:
return False
dir1 = dir1 / norm1
# 计算seg2的两个端点到seg1所在直线的距离
# 点到直线距离公式:|(P - P0) × dir|
vec_to_p2_1 = p2_1 - p1_1
vec_to_p2_2 = p2_2 - p1_1
# 2D叉积的绝对值
dist_p2_1 = abs(vec_to_p2_1[0] * dir1[1] - vec_to_p2_1[1] * dir1[0])
dist_p2_2 = abs(vec_to_p2_2[0] * dir1[1] - vec_to_p2_2[1] * dir1[0])
# 如果两个端点到直线的距离都很小,说明共线
return dist_p2_1 < distance_thresh and dist_p2_2 < distance_thresh
def cluster_segments_in_param_space(fitted_segments, eps, min_samples, alpha, beta):
"""在 (θ, ρ, r) 参数空间对线段进行DBSCAN聚类(带角度wrap和距离自适应)"""
if len(fitted_segments) == 0:
return np.array([]), np.array([])
# 预分配数组
n_segs = len(fitted_segments)
features = np.zeros((n_segs, 3), dtype=np.float64) # 现在是3维:[theta, rho, r]
# 向量化处理所有线段
for i, seg in enumerate(fitted_segments):
fit = seg['fit']
p1 = fit['p1']
p2 = fit['p2']
# 转换到 (θ, ρ) 空间
theta, rho = segment_to_hessian(p1, p2)
# 计算线段中点到原点的距离
midpoint = 0.5 * (p1 + p2)
r = np.linalg.norm(midpoint)
# 特征:[theta, rho, r](注意:theta不除以alpha,alpha传递给metric)
features[i, 0] = theta
features[i, 1] = rho
features[i, 2] = r
# 🔑 预处理:检查共线线段的边界跳变问题
THETA_BOUNDARY_THRESH = 0.15 # 边界阈值(弧度)
COLLINEAR_DISTANCE_THRESH = 0.2 # 共线判定阈值(米)
for i in range(n_segs):
for j in range(i + 1, n_segs):
theta_i, rho_i = features[i, 0], features[i, 1]
theta_j, rho_j = features[j, 0], features[j, 1]
# 检查是否在边界附近且 ρ 符号相反
theta_near_0 = min(theta_i, theta_j) < THETA_BOUNDARY_THRESH
theta_near_pi = max(theta_i, theta_j) > (np.pi - THETA_BOUNDARY_THRESH)
if (theta_near_0 or theta_near_pi) and rho_i * rho_j < 0:
# 可能是边界跳变,检查是否共线
# 获取原始线段
seg_i = fitted_segments[i]
seg_j = fitted_segments[j]
# 检查共线性:计算一个线段的端点到另一个线段的距离
if are_segments_collinear(seg_i, seg_j, COLLINEAR_DISTANCE_THRESH):
# 共线!统一表示:将 θ 接近 π 的转换到接近 0
if theta_i > np.pi / 2:
features[i, 0] = theta_i - np.pi
features[i, 1] = -rho_i
if theta_j > np.pi / 2:
features[j, 0] = theta_j - np.pi
features[j, 1] = -rho_j
# 使用自定义metric进行DBSCAN聚类
# 将alpha和k_rho参数固化到metric函数中
metric_fn = partial(seg_metric, theta_scale=alpha, k_rho=beta)
db = DBSCAN(eps=eps, min_samples=min_samples, metric=metric_fn)
labels = db.fit_predict(features)
return features, labels
def merge_segments_in_cluster(cluster_segments, gap_thresh):
"""合并簇内的线段"""
if len(cluster_segments) == 0:
return []
# 计算平均方向
all_directions = np.array([seg['fit']['dir'] for seg in cluster_segments])
avg_dir = np.mean(all_directions, axis=0)
avg_dir = avg_dir / np.linalg.norm(avg_dir)
# 收集所有点
all_points = []
for seg in cluster_segments:
all_points.append(seg['points'])
all_points = np.vstack(all_points)
# 投影到平均方向
t_vals = all_points @ avg_dir
# 构建区间
intervals = []
for seg in cluster_segments:
pts = seg['points']
t = pts @ avg_dir
intervals.append((np.min(t), np.max(t)))
# 合并区间
intervals = sorted(intervals)
merged = []
current_start, current_end = intervals[0]
for start, end in intervals[1:]:
if start - current_end <= gap_thresh:
current_end = max(current_end, end)
else:
merged.append((current_start, current_end))
current_start, current_end = start, end
merged.append((current_start, current_end))
# 为每个区间重新拟合
walls = []
wall_normal = np.array([-avg_dir[1], avg_dir[0]])
for t_min, t_max in merged:
mask = (t_vals >= t_min - 0.01) & (t_vals <= t_max + 0.01)
interval_points = all_points[mask]
if len(interval_points) >= 2:
fit = fit_line_pca(interval_points)
if fit is not None:
walls.append({'fit': fit, 'points': interval_points})
return walls
def detect_walls(xy):
"""墙体检测主流程"""
# 1. Jump 分段
segments_points = segment_by_jump(xy, JUMP_DIST_THRESH, MIN_SEGMENT_POINTS)
# 2. RDP 分割
split_segments = []
for seg in segments_points:
split_segments.extend(split_by_rdp(seg, RDP_EPSILON, MIN_SPLIT_POINTS, MIN_RDP_SEGMENT_LENGTH))
# 3. PCA 拟合
fitted_segments = []
for seg_pts in split_segments:
fit = fit_line_pca(seg_pts)
if fit is None:
continue
if fit['rmse'] > WALL_RMSE_THRESH:
continue
if fit['length'] < MIN_SEGMENT_LENGTH:
continue
fitted_segments.append({'fit': fit, 'points': seg_pts})
# 保存初步拟合的线段供可视化使用
initial_fitted_segments = fitted_segments.copy()
# 4. 参数空间聚类
if len(fitted_segments) == 0:
return [], {
'segments_points': segments_points,
'split_segments': split_segments,
'initial_fitted_segments': [],
'features': np.array([]),
'labels': np.array([])
}
features, labels = cluster_segments_in_param_space(
fitted_segments, PARAM_DBSCAN_EPS, PARAM_DBSCAN_MIN_SAMPLES, ALPHA_SCALE, BETA_SCALE
)
# 5. 区间合并
merged_walls = []
wall_id = 0
for cluster_id in set(labels):
if cluster_id == -1:
continue
cluster_mask = (labels == cluster_id)
cluster_segments = [fitted_segments[i] for i in np.where(cluster_mask)[0]]
if len(cluster_segments) < MIN_SEGMENTS_PER_WALL:
continue
walls = merge_segments_in_cluster(cluster_segments, GAP_THRESH)
for wall in walls:
if wall['fit']['length'] >= MIN_WALL_LENGTH:
wall['id'] = wall_id
wall_id += 1
merged_walls.append(wall)
# 返回墙体和中间结果
intermediate_data = {
'segments_points': segments_points,
'split_segments': split_segments,
'initial_fitted_segments': initial_fitted_segments,
'features': features,
'labels': labels
}
return merged_walls, intermediate_data
# =========================
# 2. 道路中心线核心函数
# =========================
def are_parallel(wall1, wall2, angle_thresh_deg):
"""判断两面墙是否平行"""
dot = np.abs(np.dot(wall1['fit']['dir'], wall2['fit']['dir']))
angle_diff = np.degrees(np.arccos(np.clip(dot, -1.0, 1.0)))
return angle_diff < angle_thresh_deg or angle_diff > (180 - angle_thresh_deg)
def compute_wall_distance(wall1, wall2):
"""计算两面墙之间的距离"""
normal1 = np.array([-wall1['fit']['dir'][1], wall1['fit']['dir'][0]])
d1 = np.dot(wall2['fit']['p1'] - wall1['fit']['p1'], normal1)
d2 = np.dot(wall2['fit']['p2'] - wall1['fit']['p1'], normal1)
return np.abs(0.5 * (d1 + d2))
def compute_projection_overlap(wall1, wall2):
"""计算两面墙的投影重叠长度(米)"""
dir_vec = wall1['fit']['dir']
t1_start = np.dot(wall1['fit']['p1'], dir_vec)
t1_end = np.dot(wall1['fit']['p2'], dir_vec)
if t1_start > t1_end:
t1_start, t1_end = t1_end, t1_start
t2_start = np.dot(wall2['fit']['p1'], dir_vec)
t2_end = np.dot(wall2['fit']['p2'], dir_vec)
if t2_start > t2_end:
t2_start, t2_end = t2_end, t2_start
overlap_start = max(t1_start, t2_start)
overlap_end = min(t1_end, t2_end)
overlap_length = max(0, overlap_end - overlap_start)
# 返回绝对重叠长度(米)
return overlap_length
def find_wall_pairs(walls):
"""找到可以配对的墙体(允许墙体被多次使用)"""
pairs = []
# 🔑 改进:遍历所有可能的墙体对,不限制单次使用
for i, wall1 in enumerate(walls):
for j, wall2 in enumerate(walls):
if i >= j: # 避免重复配对和自己配对自己
continue
# 检查是否平行
if not are_parallel(wall1, wall2, PARALLEL_ANGLE_THRESH):
continue
# 检查距离是否在道路宽度范围内
dist = compute_wall_distance(wall1, wall2)
if dist < MIN_ROAD_WIDTH or dist > MAX_ROAD_WIDTH:
continue
# 检查重叠长度
overlap_length = compute_projection_overlap(wall1, wall2)
if overlap_length < MIN_OVERLAP_LENGTH:
continue
# 所有条件满足,添加配对
pairs.append((wall1, wall2, dist))
# 🔑 按重叠长度排序,优先使用重叠长度更长的配对
pairs_with_score = []
for wall1, wall2, dist in pairs:
overlap_length = compute_projection_overlap(wall1, wall2)
ideal_width = 1.5
width_score = 1.0 - abs(dist - ideal_width) / MAX_ROAD_WIDTH
overlap_score = min(overlap_length / 5.0, 1.0)
score = overlap_score * 0.7 + width_score * 0.3
pairs_with_score.append((wall1, wall2, dist, score))
# 按得分降序排序
pairs_with_score.sort(key=lambda x: x[3], reverse=True)
# 返回排序后的配对(去掉得分)
return [(w1, w2, d) for w1, w2, d, _ in pairs_with_score]
def generate_centerline(wall1, wall2):
"""根据墙体对生成道路中心线"""
dir_vec = wall1['fit']['dir']
# 投影端点
t1 = np.dot(wall1['fit']['p1'], dir_vec)
t2 = np.dot(wall1['fit']['p2'], dir_vec)
t3 = np.dot(wall2['fit']['p1'], dir_vec)
t4 = np.dot(wall2['fit']['p2'], dir_vec)
# 重叠区间
t_start = max(min(t1, t2), min(t3, t4))
t_end = min(max(t1, t2), max(t3, t4))
# 计算中心线端点
def get_point_at_t(wall, t):
t1 = np.dot(wall['fit']['p1'], dir_vec)
t2 = np.dot(wall['fit']['p2'], dir_vec)
if abs(t2 - t1) < 1e-6:
return wall['fit']['p1']
ratio = (t - t1) / (t2 - t1)
return wall['fit']['p1'] + ratio * (wall['fit']['p2'] - wall['fit']['p1'])
start1 = get_point_at_t(wall1, t_start)
start2 = get_point_at_t(wall2, t_start)
centerline_start = 0.5 * (start1 + start2)
end1 = get_point_at_t(wall1, t_end)
end2 = get_point_at_t(wall2, t_end)
centerline_end = 0.5 * (end1 + end2)
width = compute_wall_distance(wall1, wall2)
# 🔑 确保方向:p1(起点)在后,p2(终点)在前
# 使用朝向判断:中心线方向应该指向前方(X轴正方向)
centerline_dir = centerline_end - centerline_start
# 如果方向向量的X分量为负(朝后),则交换起点和终点
if centerline_dir[0] < 0:
centerline_start, centerline_end = centerline_end, centerline_start
return {
'p1': centerline_start,
'p2': centerline_end,
'width': width,
'wall_pair': (wall1, wall2)
}
def select_main_road(centerlines, robot_pos=np.array([0.0, 0.0])):
"""选择机器人所在的主干道(距离机器人最近的中心线)"""
if len(centerlines) == 0:
return None
min_dist = float('inf')
main_centerline = None
for cl in centerlines:
# 计算机器人到中心线的最近距离(点到线段的距离)
p1 = cl['p1']
p2 = cl['p2']
# 线段方向向量
line_vec = p2 - p1
line_length_sq = np.dot(line_vec, line_vec)
if line_length_sq < 1e-12:
# 退化为点的情况
dist = np.linalg.norm(robot_pos - p1)
else:
# 计算投影参数 t
robot_vec = robot_pos - p1
t = np.dot(robot_vec, line_vec) / line_length_sq
t = np.clip(t, 0.0, 1.0) # 限制在线段范围内
# 最近点
closest_point = p1 + t * line_vec
dist = np.linalg.norm(robot_pos - closest_point)
if dist < min_dist:
min_dist = dist
main_centerline = cl
return main_centerline
# =========================
# 3. 交叉口识别核心函数
# =========================
def normalize(v):
"""归一化向量"""
v = np.array(v)
norm = np.linalg.norm(v)
if norm < 1e-6:
return v
return v / norm
def extend_line(p1, p2, length):
"""延长线段"""
p1, p2 = np.array(p1), np.array(p2)
direction = normalize(p2 - p1)
p1_extended = p1 - direction * length
p2_extended = p2 + direction * length
return p1_extended, p2_extended
def get_perpendicular_line(p1, p2, length):
"""构建垂线"""
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
perp_dir = (-dy, dx)
norm = np.sqrt(perp_dir[0]**2 + perp_dir[1]**2)
if norm < 1e-6:
return None
perp_dir = (perp_dir[0] / norm, perp_dir[1] / norm)
p1_ext = (p2[0] + perp_dir[0] * length, p2[1] + perp_dir[1] * length)
p2_ext = (p2[0] - perp_dir[0] * length, p2[1] - perp_dir[1] * length)
return [p1_ext, p2_ext]
def get_intersection(p1, p2, q1, q2):
"""计算两条线段的交点"""
line1 = LineString([p1, p2])
line2 = LineString([q1, q2])
intersection = line1.intersection(line2)
if intersection.is_empty:
return None
elif intersection.geom_type == 'Point':
return np.array([intersection.x, intersection.y])
else:
return None
def detect_intersection(walls, centerline):
"""检测交叉口区域"""
if centerline is None:
return None, None, []
# 获取道路边界墙体
wall1, wall2 = centerline['wall_pair']
self_wall_ids = [wall1['id'], wall2['id']]
self_walls = [wall1, wall2]
other_walls = [w for w in walls if w['id'] not in self_wall_ids]
# 🔑 过滤:只保留与中心线垂直的墙体
# 计算中心线方向
road_dir = normalize(centerline['p2'] - centerline['p1'])
filtered_other_walls = []
for wall in other_walls:
wall_dir = normalize(wall['fit']['p2'] - wall['fit']['p1'])
# 计算夹角(度)
dot = np.abs(np.dot(wall_dir, road_dir))
angle_deg = np.degrees(np.arccos(np.clip(dot, 0.0, 1.0)))
# 只保留接近垂直的墙体(60-120度之间)
if 90 - PERPENDICULAR_ANGLE_THRESH <= angle_deg <= 90 + PERPENDICULAR_ANGLE_THRESH:
filtered_other_walls.append(wall)
# 延长墙体
self_wall_extended = []
for wall in self_walls:
p1_ext, p2_ext = extend_line(wall['fit']['p1'], wall['fit']['p2'], EXTEND_LENGTH)
self_wall_extended.append([p1_ext, p2_ext])
other_wall_extended = []
for wall in filtered_other_walls:
p1_ext, p2_ext = extend_line(wall['fit']['p1'], wall['fit']['p2'], EXTEND_LENGTH)
other_wall_extended.append([p1_ext, p2_ext])
# 构建垂线(在中心线终点p2,即前方)
perp_line = get_perpendicular_line(centerline['p1'], centerline['p2'], EXTEND_LENGTH)
intersection_coords = []
# 🔑 改进的交点收集策略
# 1. 道路边界墙与垂线的交点(垂线在前方,这些点构成路口的后边界)
if perp_line is not None:
for line in self_wall_extended:
inter = get_intersection(line[0], line[1], perp_line[0], perp_line[1])
if inter is not None:
intersection_coords.append(inter)
# 2. 道路边界墙与其他墙的交点
# 只保留在道路前方的交点
road_direction = centerline['p2'] - centerline['p1'] # 道路方向向量
for self_line in self_wall_extended:
for other_line in other_wall_extended:
inter = get_intersection(self_line[0], self_line[1], other_line[0], other_line[1])
if inter is not None:
# 🔑 筛选条件:交点在道路前方
# 计算交点相对于中心线终点的方向
to_intersection = inter - centerline['p2']
# 如果与道路方向同向(点积>0),说明在前方
if np.dot(to_intersection, road_direction) > 0:
intersection_coords.append(inter)
# 计算凸包
polygon = None
centroid = None
if len(intersection_coords) >= 3:
points = [tuple(coord) for coord in intersection_coords]
multipoint = MultiPoint(points)
polygon = multipoint.convex_hull
if polygon and not polygon.is_empty:
centroid = np.array([polygon.centroid.x, polygon.centroid.y])
# 🔑 返回额外的调试信息:延长线和垂线
debug_info = {
'self_wall_extended': self_wall_extended,
'other_wall_extended': other_wall_extended,
'perp_line': perp_line,
'intersection_coords': intersection_coords
}
return polygon, centroid, debug_info
def detect_successor_lanes(walls, centerline, polygon, centroid, intersection_coords):
"""检测路口的后继车道(4个方向:前、后、左、右)
Args:
walls: 所有墙体列表
centerline: 主干道中心线
polygon: 路口多边形
centroid: 路口质心
intersection_coords: 路口交点坐标列表
Returns:
successor_lanes: 过滤后的后继车道列表
rectangle_info: 矩形信息(用于可视化)
"""
if centerline is None or polygon is None or centroid is None or len(intersection_coords) < 3:
return [], None
# 1. 计算中心线方向(自车方向)
road_dir = normalize(centerline['p2'] - centerline['p1'])
road_angle = np.arctan2(road_dir[1], road_dir[0])
# 2. 构建旋转矩阵(将中心线方向旋转到X轴)
cos_a = np.cos(-road_angle)
sin_a = np.sin(-road_angle)
R = np.array([[cos_a, -sin_a], [sin_a, cos_a]])
# 3. 旋转交点坐标到aligned坐标系
centered_coords = np.array(intersection_coords) - centroid
rotated_coords = centered_coords @ R.T
# 4. 计算AABB(轴对齐边界框 = 最小矩形)
x_min, y_min = rotated_coords.min(axis=0)
x_max, y_max = rotated_coords.max(axis=0)
# 5. 生成3个后继方向的车道(从路口质心向外延伸)
# 在旋转坐标系中定义标准方向,确保left和right垂直于forward
successor_lanes = []
R_inv = R.T
# 定义3个标准方向(在旋转坐标系中)
directions_rotated = {
'forward': np.array([1.0, 0.0]), # X正方向
'left': np.array([0.0, 1.0]), # Y正方向(垂直于forward)
'right': np.array([0.0, -1.0]) # Y负方向(垂直于forward)
}
for direction, dir_vec_rotated in directions_rotated.items():
# 旋转回世界坐标系
lane_dir = normalize(dir_vec_rotated @ R_inv.T)
# 生成车道线段
p1 = centroid
p2 = centroid + lane_dir * SUCCESSOR_LENGTH
# 计算边界中点(用于可视化)
if direction == 'forward':
edge_center_rot = np.array([x_max, (y_min + y_max) / 2])
elif direction == 'left':
edge_center_rot = np.array([(x_min + x_max) / 2, y_max])
else: # right
edge_center_rot = np.array([(x_min + x_max) / 2, y_min])
edge_center = edge_center_rot @ R_inv.T + centroid
successor_lanes.append({
'direction': direction,
'p1': p1,
'p2': p2,
'lane_dir': lane_dir,
'edge_center': edge_center
})
# 8. 墙体遮挡检测
filtered_lanes = []
for lane in successor_lanes:
if not is_blocked_by_wall(lane, walls, centerline):
filtered_lanes.append(lane)
# 返回矩形信息和过滤后的车道
# 将矩形的4个角点转回世界坐标(用于可视化)
rect_corners_rotated = np.array([
[x_min, y_min],
[x_max, y_min],
[x_max, y_max],
[x_min, y_max]
])
rect_corners_world = rect_corners_rotated @ R_inv.T + centroid
rectangle_info = {
'centroid': centroid,
'road_angle': road_angle,
'corners': rect_corners_world,
'R': R
}
return filtered_lanes, rectangle_info
def is_blocked_by_wall(lane, walls, main_centerline):
"""检测车道是否被墙体遮挡
策略:直接检查后继车道线段是否与墙体距离很近(说明被墙堵住)
不区分方向,所有3个方向都用同样的逻辑检查
"""
# 获取主干道墙体ID(排除它们,因为它们是自车所在道路的边界)
road_wall_ids = set()
if main_centerline is not None:
wall1, wall2 = main_centerline['wall_pair']
road_wall_ids = {wall1['id'], wall2['id']}
# 创建车道线段
lane_line = LineString([lane['p1'], lane['p2']])
# 检查所有其他墙体
for wall in walls:
# 跳过主干道的墙体(它们是自车所在道路的边界)
if wall['id'] in road_wall_ids:
continue
# 创建墙体线段
wall_line = LineString([wall['fit']['p1'], wall['fit']['p2']])
# 计算车道线段到墙体的最短距离
dist = lane_line.distance(wall_line)
# 如果距离很近,说明这个方向被墙堵住了
if dist < BLOCKING_DISTANCE:
return True
return False
# =========================
# 4. 完整处理流程
# =========================
def process_full_pipeline(laserscan_path):
"""完整处理流程:点云 → 墙体 → 中心线 → 路口"""
print("=" * 70)
print(" 道路网络完整处理流程")
print("=" * 70)
# 1. 加载点云数据
print("\n【步骤1】加载点云数据")
with open(laserscan_path, "r") as f:
data = json.load(f)
laserscan = data["laserscan"] if "laserscan" in data else data
xy = laserscan_to_xy(laserscan, max_range=MAX_RANGE)
# 空间过滤
mask = (xy[:,0] >= -X_LIMIT) & (xy[:,0] <= X_LIMIT) & \
(xy[:,1] >= -Y_LIMIT) & (xy[:,1] <= Y_LIMIT)
xy = xy[mask]
xy = xy[np.isfinite(xy).all(axis=1)]
print(f" 点云数量: {len(xy)}")
# 2. 墙体检测
print("\n【步骤2】墙体检测")
walls, intermediate_data = detect_walls(xy)
print(f" 检测到墙体数: {len(walls)}")
# 3. 道路中心线生成
print("\n【步骤3】道路中心线生成")
wall_pairs = find_wall_pairs(walls)
print(f" 找到墙体对: {len(wall_pairs)}")
centerlines = []
for wall1, wall2, dist in wall_pairs:
cl = generate_centerline(wall1, wall2)
centerlines.append(cl)
print(f" 中心线 #{len(centerlines)}: 宽度={cl['width']:.2f}m")
# 🔑 选择主干道(距离机器人最近的中心线)
main_centerline = select_main_road(centerlines, robot_pos=np.array([0.0, 0.0]))
if main_centerline is not None:
# 使用身份比较(is)而不是值比较(==)来查找索引
main_idx = next((i for i, cl in enumerate(centerlines) if cl is main_centerline), None)
if main_idx is not None:
print(f" ✅ 选择主干道: 中心线 #{main_idx + 1} (距离机器人最近)")
else:
print(f" ⚠️ 找到主干道但无法确定索引")
else:
print(f" ⚠️ 未找到主干道")
# 4. 交叉口识别(只为主干道检测)
print("\n【步骤4】交叉口识别")
polygons = []
centroids = []
all_debug_info = []