Skip to content

Commit 13230c9

Browse files
committed
update with SpatialLM1.1
1 parent 09127ce commit 13230c9

20 files changed

+1952
-223
lines changed

README.md

Lines changed: 121 additions & 47 deletions
Large diffs are not rendered by default.

eval.py

Lines changed: 128 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pandas as pd
1313
import numpy as np
1414
from scipy.optimize import linear_sum_assignment
15-
from shapely import Polygon, LineString, polygonize, polygonize_full, make_valid
15+
from shapely import Polygon
1616
from bbox import BBox3D
1717
from bbox.metrics import iou_3d
1818
from terminaltables import AsciiTable
@@ -24,6 +24,7 @@
2424

2525
ZERO_TOLERANCE = 1e-6
2626
LARGE_COST_VALUE = 1e6
27+
LAYOUTS = ["wall", "door", "window"]
2728
OBJECTS = [
2829
"curtain",
2930
"nightstand",
@@ -40,13 +41,11 @@
4041
"side table",
4142
"air conditioner",
4243
"dresser",
43-
]
44-
THIN_OBJECTS = [
44+
"stool",
45+
"refrigerator",
4546
"painting",
4647
"carpet",
4748
"tv",
48-
"door",
49-
"window",
5049
]
5150

5251

@@ -87,37 +86,8 @@ def calc_poly_iou(poly1, poly2):
8786
return poly_iou
8887

8988

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-
11989
def read_label_mapping(
120-
label_path: str, label_from="spatiallm59", label_to="spatiallm18"
90+
label_path: str, label_from="spatiallm59", label_to="spatiallm23"
12191
):
12292
assert os.path.isfile(label_path), f"Label mapping file {label_path} does not exist"
12393
mapping = dict()
@@ -142,6 +112,13 @@ def assign_class_map(entities: List[Bbox], class_map=Dict[str, str]):
142112
return res_entities
143113

144114

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+
145122
def get_entity_class(entity):
146123
try:
147124
return entity.class_name
@@ -194,22 +171,32 @@ def calc_bbox_tp(
194171
return EvalTuple(tp, num_pred, num_gt)
195172

196173

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+
197180
def is_valid_dw(entity: Door | Window, wall_id_lookup: Dict[int, Wall]):
198181
attach_wall = wall_id_lookup.get(entity.id, None)
199182
if attach_wall is None:
200183
return False
184+
return is_valid_wall(attach_wall)
201185

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
209186

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)):
213200
attach_wall = wall_id_lookup.get(entity.id, None)
214201
if attach_wall is None:
215202
log.error(f"{entity} attach wall is None")
@@ -305,7 +292,7 @@ def calc_thin_bbox_iou_2d(
305292
if are_planes_parallel_and_close(
306293
corners_1, corners_2, parallel_tolerance, dist_tolerance
307294
):
308-
p1, p2, _, p4 = corners_1
295+
p1, p2, _, p4 = corners_2
309296
v1 = np.subtract(p2, p1)
310297
v2 = np.subtract(p4, p1)
311298
basis1 = v1 / np.linalg.norm(v1)
@@ -334,9 +321,9 @@ def calc_thin_bbox_iou_2d(
334321
return 0
335322

336323

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],
340327
pred_wall_id_lookup: Dict[int, Wall],
341328
gt_wall_id_lookup: Dict[int, Wall],
342329
iou_threshold: float = 0.25,
@@ -400,14 +387,16 @@ def calc_thin_bbox_tp(
400387
required=True,
401388
help="Path to the label mapping file",
402389
)
390+
parser.add_argument("--label_from", type=str, default="spatiallm59")
391+
parser.add_argument("--label_to", type=str, default="spatiallm20")
403392
args = parser.parse_args()
404393

405394
df = pd.read_csv(args.metadata)
406395
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)
408397

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)
411400
for scene_id in scene_id_list:
412401
log.info(f"Evaluating scene {scene_id}")
413402
with open(os.path.join(args.pred_dir, f"{scene_id}.txt"), "r") as f:
@@ -416,25 +405,71 @@ def calc_thin_bbox_tp(
416405
gt_layout = Layout(f.read())
417406
pred_layout.bboxes = assign_class_map(pred_layout.bboxes, class_map)
418407
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)
419410

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+
)
423422
)
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+
)
426430
)
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+
)
428467

429468
# 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]
436471
for class_name in OBJECTS:
437-
classwise_eval_tuples[class_name].append(
472+
classwise_eval_tuples_25[class_name].append(
438473
calc_bbox_tp(
439474
pred_entities=[
440475
b
@@ -446,65 +481,54 @@ def calc_thin_bbox_tp(
446481
for b in gt_normal_objects
447482
if get_entity_class(b) == class_name
448483
],
484+
iou_threshold=0.25,
449485
)
450486
)
451487

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(
473490
pred_entities=[
474491
b
475-
for b in pred_thin_objects
492+
for b in pred_normal_objects
476493
if get_entity_class(b) == class_name
477494
],
478495
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
480499
],
481-
pred_wall_id_lookup=pred_wall_id_lookup,
482-
gt_wall_id_lookup=gt_wall_id_lookup,
500+
iou_threshold=0.50,
483501
)
484502
)
485503

486-
# table print
487-
headers = ["Floorplan", "mean IoU"]
504+
headers = ["Layouts", "F1 @.25 IoU", "F1 @.50 IoU"]
488505
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])
490518
print("\n" + AsciiTable(table_data).table)
491519

492-
headers = ["Objects", "F1 @.25 IoU"]
520+
headers = ["Objects", "F1 @.25 IoU", "F1 @.50 IoU"]
493521
table_data = [headers]
494522
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(
497525
[t.masked for t in tuples], [t.f1 for t in tuples]
498526
).mean()
499-
table_data.append([class_name, f1])
500-
print("\n" + AsciiTable(table_data).table)
501527

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(
507530
[t.masked for t in tuples], [t.f1 for t in tuples]
508531
).mean()
509-
table_data.append([class_name, f1])
532+
533+
table_data.append([class_name, f1_25, f1_50])
510534
print("\n" + AsciiTable(table_data).table)

figures/scannet.jpg

139 KB
Loading

figures/stru3d.jpg

82.5 KB
Loading

figures/zeroshot.jpg

69.8 KB
Loading

0 commit comments

Comments
 (0)