12
12
import pandas as pd
13
13
import numpy as np
14
14
from scipy .optimize import linear_sum_assignment
15
- from shapely import Polygon , LineString , polygonize , polygonize_full , make_valid
15
+ from shapely import Polygon
16
16
from bbox import BBox3D
17
17
from bbox .metrics import iou_3d
18
18
from terminaltables import AsciiTable
24
24
25
25
ZERO_TOLERANCE = 1e-6
26
26
LARGE_COST_VALUE = 1e6
27
+ LAYOUTS = ["wall" , "door" , "window" ]
27
28
OBJECTS = [
28
29
"curtain" ,
29
30
"nightstand" ,
40
41
"side table" ,
41
42
"air conditioner" ,
42
43
"dresser" ,
43
- ]
44
- THIN_OBJECTS = [
44
+ "stool" ,
45
+ "refrigerator" ,
45
46
"painting" ,
46
47
"carpet" ,
47
48
"tv" ,
48
- "door" ,
49
- "window" ,
50
49
]
51
50
52
51
@@ -87,37 +86,8 @@ def calc_poly_iou(poly1, poly2):
87
86
return poly_iou
88
87
89
88
90
- def construct_polygon (lines : List [LineString ]):
91
- try :
92
- poly = polygonize (lines )
93
- if poly .is_empty :
94
- candidates = []
95
- for p in polygonize_full (lines ):
96
- if p .is_empty :
97
- continue
98
-
99
- candidate = p .geoms [0 ]
100
- if isinstance (candidate , Polygon ):
101
- candidates .append (candidate )
102
- elif isinstance (candidate , LineString ):
103
- candidates .append (Polygon (candidate ))
104
- else :
105
- log .warning (
106
- f"Unsupported geom_type { candidate .geom_type } to construct polygon."
107
- )
108
-
109
- candidates .sort (key = lambda x : x .area , reverse = True )
110
- poly = candidates [0 ]
111
- if not poly .is_valid :
112
- poly = make_valid (poly )
113
- return poly
114
- except Exception as e :
115
- log .error (f"Fail to construct polygon by lines { lines } " , e )
116
- return Polygon ()
117
-
118
-
119
89
def read_label_mapping (
120
- label_path : str , label_from = "spatiallm59" , label_to = "spatiallm18 "
90
+ label_path : str , label_from = "spatiallm59" , label_to = "spatiallm23 "
121
91
):
122
92
assert os .path .isfile (label_path ), f"Label mapping file { label_path } does not exist"
123
93
mapping = dict ()
@@ -142,6 +112,13 @@ def assign_class_map(entities: List[Bbox], class_map=Dict[str, str]):
142
112
return res_entities
143
113
144
114
115
+ def assign_minimum_scale (entities : List [Bbox ], minimum_scale : float = 0.1 ):
116
+ for entity in entities :
117
+ entity .scale_x = max (entity .scale_x , minimum_scale )
118
+ entity .scale_y = max (entity .scale_y , minimum_scale )
119
+ entity .scale_z = max (entity .scale_z , minimum_scale )
120
+
121
+
145
122
def get_entity_class (entity ):
146
123
try :
147
124
return entity .class_name
@@ -194,22 +171,32 @@ def calc_bbox_tp(
194
171
return EvalTuple (tp , num_pred , num_gt )
195
172
196
173
174
+ def is_valid_wall (entity : Wall ):
175
+ wall_extent_x = max (max (entity .ax , entity .bx ) - min (entity .ax , entity .bx ), 0 )
176
+ wall_extent_y = max (max (entity .ay , entity .by ) - min (entity .ay , entity .by ), 0 )
177
+ return wall_extent_x > ZERO_TOLERANCE or wall_extent_y > ZERO_TOLERANCE
178
+
179
+
197
180
def is_valid_dw (entity : Door | Window , wall_id_lookup : Dict [int , Wall ]):
198
181
attach_wall = wall_id_lookup .get (entity .id , None )
199
182
if attach_wall is None :
200
183
return False
184
+ return is_valid_wall (attach_wall )
201
185
202
- wall_extent_x = max (
203
- max (attach_wall .ax , attach_wall .bx ) - min (attach_wall .ax , attach_wall .bx ), 0
204
- )
205
- wall_extent_y = max (
206
- max (attach_wall .ay , attach_wall .by ) - min (attach_wall .ay , attach_wall .by ), 0
207
- )
208
- return wall_extent_x > ZERO_TOLERANCE or wall_extent_y > ZERO_TOLERANCE
209
186
210
-
211
- def get_corners (entity : Door | Window | Bbox , wall_id_lookup : Dict [int , Wall ]):
212
- if isinstance (entity , (Door , Window )):
187
+ def get_corners (
188
+ entity : Wall | Door | Window | Bbox , wall_id_lookup : Dict [int , Wall ] = None
189
+ ):
190
+ if isinstance (entity , Wall ):
191
+ return np .array (
192
+ [
193
+ [entity .ax , entity .ay , entity .az ],
194
+ [entity .bx , entity .by , entity .bz ],
195
+ [entity .bx , entity .by , entity .bz + entity .height ],
196
+ [entity .ax , entity .ay , entity .az + entity .height ],
197
+ ]
198
+ )
199
+ elif isinstance (entity , (Door , Window )):
213
200
attach_wall = wall_id_lookup .get (entity .id , None )
214
201
if attach_wall is None :
215
202
log .error (f"{ entity } attach wall is None" )
@@ -305,7 +292,7 @@ def calc_thin_bbox_iou_2d(
305
292
if are_planes_parallel_and_close (
306
293
corners_1 , corners_2 , parallel_tolerance , dist_tolerance
307
294
):
308
- p1 , p2 , _ , p4 = corners_1
295
+ p1 , p2 , _ , p4 = corners_2
309
296
v1 = np .subtract (p2 , p1 )
310
297
v2 = np .subtract (p4 , p1 )
311
298
basis1 = v1 / np .linalg .norm (v1 )
@@ -334,9 +321,9 @@ def calc_thin_bbox_iou_2d(
334
321
return 0
335
322
336
323
337
- def calc_thin_bbox_tp (
338
- pred_entities : List [Door | Window | Bbox ],
339
- gt_entities : List [Door | Window | Bbox ],
324
+ def calc_layout_tp (
325
+ pred_entities : List [Wall | Door | Window ],
326
+ gt_entities : List [Wall | Door | Window ],
340
327
pred_wall_id_lookup : Dict [int , Wall ],
341
328
gt_wall_id_lookup : Dict [int , Wall ],
342
329
iou_threshold : float = 0.25 ,
@@ -400,14 +387,16 @@ def calc_thin_bbox_tp(
400
387
required = True ,
401
388
help = "Path to the label mapping file" ,
402
389
)
390
+ parser .add_argument ("--label_from" , type = str , default = "spatiallm59" )
391
+ parser .add_argument ("--label_to" , type = str , default = "spatiallm20" )
403
392
args = parser .parse_args ()
404
393
405
394
df = pd .read_csv (args .metadata )
406
395
scene_id_list = df ["id" ].tolist ()
407
- class_map = read_label_mapping (args .label_mapping )
396
+ class_map = read_label_mapping (args .label_mapping , args . label_from , args . label_to )
408
397
409
- floorplan_ious = list ( )
410
- classwise_eval_tuples : Dict [str , List [EvalTuple ]] = defaultdict (list )
398
+ classwise_eval_tuples_25 : Dict [ str , List [ EvalTuple ]] = defaultdict ( list )
399
+ classwise_eval_tuples_50 : Dict [str , List [EvalTuple ]] = defaultdict (list )
411
400
for scene_id in scene_id_list :
412
401
log .info (f"Evaluating scene { scene_id } " )
413
402
with open (os .path .join (args .pred_dir , f"{ scene_id } .txt" ), "r" ) as f :
@@ -416,25 +405,71 @@ def calc_thin_bbox_tp(
416
405
gt_layout = Layout (f .read ())
417
406
pred_layout .bboxes = assign_class_map (pred_layout .bboxes , class_map )
418
407
gt_layout .bboxes = assign_class_map (gt_layout .bboxes , class_map )
408
+ assign_minimum_scale (pred_layout .bboxes , minimum_scale = 0.1 )
409
+ assign_minimum_scale (gt_layout .bboxes , minimum_scale = 0.1 )
419
410
420
- # Floorplan, IoU
421
- pred_poly = construct_polygon (
422
- [LineString ([(w .ax , w .ay ), (w .bx , w .by )]) for w in pred_layout .walls ]
411
+ # Layout, F1
412
+ pred_wall_id_lookup = {w .id : w for w in pred_layout .walls }
413
+ gt_wall_id_lookup = {w .id : w for w in gt_layout .walls }
414
+
415
+ pred_layout_instances = list (
416
+ filter (lambda e : is_valid_wall (e ), pred_layout .walls )
417
+ ) + list (
418
+ filter (
419
+ lambda e : is_valid_dw (e , pred_wall_id_lookup ),
420
+ pred_layout .doors + pred_layout .windows ,
421
+ )
423
422
)
424
- gt_poly = construct_polygon (
425
- [LineString ([(w .ax , w .ay ), (w .bx , w .by )]) for w in gt_layout .walls ]
423
+ gt_layout_instances = list (
424
+ filter (lambda e : is_valid_wall (e ), gt_layout .walls )
425
+ ) + list (
426
+ filter (
427
+ lambda e : is_valid_dw (e , gt_wall_id_lookup ),
428
+ gt_layout .doors + gt_layout .windows ,
429
+ )
426
430
)
427
- floorplan_ious .append (calc_poly_iou (pred_poly , gt_poly ))
431
+ for class_name in LAYOUTS :
432
+ classwise_eval_tuples_25 [class_name ].append (
433
+ calc_layout_tp (
434
+ pred_entities = [
435
+ b
436
+ for b in pred_layout_instances
437
+ if get_entity_class (b ) == class_name
438
+ ],
439
+ gt_entities = [
440
+ b
441
+ for b in gt_layout_instances
442
+ if get_entity_class (b ) == class_name
443
+ ],
444
+ pred_wall_id_lookup = pred_wall_id_lookup ,
445
+ gt_wall_id_lookup = gt_wall_id_lookup ,
446
+ iou_threshold = 0.25 ,
447
+ )
448
+ )
449
+
450
+ classwise_eval_tuples_50 [class_name ].append (
451
+ calc_layout_tp (
452
+ pred_entities = [
453
+ b
454
+ for b in pred_layout_instances
455
+ if get_entity_class (b ) == class_name
456
+ ],
457
+ gt_entities = [
458
+ b
459
+ for b in gt_layout_instances
460
+ if get_entity_class (b ) == class_name
461
+ ],
462
+ pred_wall_id_lookup = pred_wall_id_lookup ,
463
+ gt_wall_id_lookup = gt_wall_id_lookup ,
464
+ iou_threshold = 0.50 ,
465
+ )
466
+ )
428
467
429
468
# Normal Objects, F1
430
- pred_normal_objects = [
431
- b for b in pred_layout .bboxes if b .class_name in OBJECTS
432
- ]
433
- gt_normal_objects = [
434
- b for b in gt_layout .bboxes if b .class_name in OBJECTS
435
- ]
469
+ pred_normal_objects = [b for b in pred_layout .bboxes if b .class_name in OBJECTS ]
470
+ gt_normal_objects = [b for b in gt_layout .bboxes if b .class_name in OBJECTS ]
436
471
for class_name in OBJECTS :
437
- classwise_eval_tuples [class_name ].append (
472
+ classwise_eval_tuples_25 [class_name ].append (
438
473
calc_bbox_tp (
439
474
pred_entities = [
440
475
b
@@ -446,65 +481,54 @@ def calc_thin_bbox_tp(
446
481
for b in gt_normal_objects
447
482
if get_entity_class (b ) == class_name
448
483
],
484
+ iou_threshold = 0.25 ,
449
485
)
450
486
)
451
487
452
- # Thin Objects, F1
453
- pred_thin_objects = [
454
- b for b in pred_layout .bboxes if b .class_name in THIN_OBJECTS
455
- ]
456
- gt_thin_objects = [b for b in gt_layout .bboxes if b .class_name in THIN_OBJECTS ]
457
- pred_wall_id_lookup = {w .id : w for w in pred_layout .walls }
458
- gt_wall_id_lookup = {w .id : w for w in gt_layout .walls }
459
- pred_thin_objects += [
460
- e
461
- for e in pred_layout .doors + pred_layout .windows
462
- if is_valid_dw (e , pred_wall_id_lookup )
463
- ]
464
- gt_thin_objects += [
465
- e
466
- for e in gt_layout .doors + gt_layout .windows
467
- if is_valid_dw (e , gt_wall_id_lookup )
468
- ]
469
-
470
- for class_name in THIN_OBJECTS :
471
- classwise_eval_tuples [class_name ].append (
472
- calc_thin_bbox_tp (
488
+ classwise_eval_tuples_50 [class_name ].append (
489
+ calc_bbox_tp (
473
490
pred_entities = [
474
491
b
475
- for b in pred_thin_objects
492
+ for b in pred_normal_objects
476
493
if get_entity_class (b ) == class_name
477
494
],
478
495
gt_entities = [
479
- b for b in gt_thin_objects if get_entity_class (b ) == class_name
496
+ b
497
+ for b in gt_normal_objects
498
+ if get_entity_class (b ) == class_name
480
499
],
481
- pred_wall_id_lookup = pred_wall_id_lookup ,
482
- gt_wall_id_lookup = gt_wall_id_lookup ,
500
+ iou_threshold = 0.50 ,
483
501
)
484
502
)
485
503
486
- # table print
487
- headers = ["Floorplan" , "mean IoU" ]
504
+ headers = ["Layouts" , "F1 @.25 IoU" , "F1 @.50 IoU" ]
488
505
table_data = [headers ]
489
- table_data += [["wall" , np .mean (floorplan_ious )]]
506
+ for class_name in LAYOUTS :
507
+ tuples = classwise_eval_tuples_25 [class_name ]
508
+ f1_25 = np .ma .masked_where (
509
+ [t .masked for t in tuples ], [t .f1 for t in tuples ]
510
+ ).mean ()
511
+
512
+ tuples = classwise_eval_tuples_50 [class_name ]
513
+ f1_50 = np .ma .masked_where (
514
+ [t .masked for t in tuples ], [t .f1 for t in tuples ]
515
+ ).mean ()
516
+
517
+ table_data .append ([class_name , f1_25 , f1_50 ])
490
518
print ("\n " + AsciiTable (table_data ).table )
491
519
492
- headers = ["Objects" , "F1 @.25 IoU" ]
520
+ headers = ["Objects" , "F1 @.25 IoU" , "F1 @.50 IoU" ]
493
521
table_data = [headers ]
494
522
for class_name in OBJECTS :
495
- tuples = classwise_eval_tuples [class_name ]
496
- f1 = np .ma .masked_where (
523
+ tuples = classwise_eval_tuples_25 [class_name ]
524
+ f1_25 = np .ma .masked_where (
497
525
[t .masked for t in tuples ], [t .f1 for t in tuples ]
498
526
).mean ()
499
- table_data .append ([class_name , f1 ])
500
- print ("\n " + AsciiTable (table_data ).table )
501
527
502
- headers = ["Thin Objects" , "F1 @.25 IoU" ]
503
- table_data = [headers ]
504
- for class_name in THIN_OBJECTS :
505
- tuples = classwise_eval_tuples [class_name ]
506
- f1 = np .ma .masked_where (
528
+ tuples = classwise_eval_tuples_50 [class_name ]
529
+ f1_50 = np .ma .masked_where (
507
530
[t .masked for t in tuples ], [t .f1 for t in tuples ]
508
531
).mean ()
509
- table_data .append ([class_name , f1 ])
532
+
533
+ table_data .append ([class_name , f1_25 , f1_50 ])
510
534
print ("\n " + AsciiTable (table_data ).table )
0 commit comments