mahdichamseddine's picture
Initial release: PT-v3m1 segmentation model (BIMStruct3D pipeline)
7ab05dd verified
Raw History Blame Contribute Delete
20.9 kB
"""
Preprocessing Script for Structured3D
Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Yujia Zhang (yujia.zhang.cs@gmail.com)
Please cite our work if the code is helpful to you.
"""
import argparse
import io
import os
from PIL import Image
import cv2
import zipfile
import numpy as np
import multiprocessing as mp
VALID_CLASS_IDS_25 = (
1,
2,
3,
4,
5,
6,
7,
8,
9,
11,
14,
15,
16,
17,
18,
19,
22,
24,
25,
32,
34,
35,
38,
39,
40,
)
CLASS_LABELS_25 = (
"wall",
"floor",
"cabinet",
"bed",
"chair",
"sofa",
"table",
"door",
"window",
"picture",
"desk",
"shelves",
"curtain",
"dresser",
"pillow",
"mirror",
"ceiling",
"refrigerator",
"television",
"nightstand",
"sink",
"lamp",
"otherstructure",
"otherfurniture",
"otherprop",
)
def normal_from_cross_product(points_2d: np.ndarray) -> np.ndarray:
xyz_points_pad = np.pad(points_2d, ((0, 1), (0, 1), (0, 0)), mode="symmetric")
xyz_points_ver = (xyz_points_pad[:, :-1, :] - xyz_points_pad[:, 1:, :])[:-1, :, :]
xyz_points_hor = (xyz_points_pad[:-1, :, :] - xyz_points_pad[1:, :, :])[:, :-1, :]
xyz_normal = np.cross(xyz_points_hor, xyz_points_ver)
xyz_dist = np.linalg.norm(xyz_normal, axis=-1, keepdims=True)
xyz_normal = np.divide(
xyz_normal, xyz_dist, out=np.zeros_like(xyz_normal), where=xyz_dist != 0
)
return xyz_normal
class Structured3DReader:
def __init__(self, files):
super().__init__()
if isinstance(files, str):
files = [files]
self.readers = [zipfile.ZipFile(f, "r") for f in files]
self.names_mapper = dict()
for idx, reader in enumerate(self.readers):
for name in reader.namelist():
self.names_mapper[name] = idx
def filelist(self):
return list(self.names_mapper.keys())
def listdir(self, dir_name):
dir_name = dir_name.lstrip(os.path.sep).rstrip(os.path.sep)
file_list = list(
np.unique(
[
f.replace(dir_name + os.path.sep, "", 1).split(os.path.sep)[0]
for f in self.filelist()
if f.startswith(dir_name + os.path.sep)
]
)
)
if "" in file_list:
file_list.remove("")
return file_list
def read(self, file_name):
split = self.names_mapper[file_name]
return self.readers[split].read(file_name)
def read_camera(self, camera_path):
z2y_top_m = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]], dtype=np.float32)
cam_extr = np.fromstring(self.read(camera_path), dtype=np.float32, sep=" ")
cam_t = np.matmul(z2y_top_m, cam_extr[:3] / 1000)
if cam_extr.shape[0] > 3:
cam_front, cam_up = cam_extr[3:6], cam_extr[6:9]
cam_n = np.cross(cam_front, cam_up)
cam_r = np.stack((cam_front, cam_up, cam_n), axis=1).astype(np.float32)
cam_r = np.matmul(z2y_top_m, cam_r)
cam_f = cam_extr[9:11]
else:
cam_r = np.eye(3, dtype=np.float32)
cam_f = None
return cam_r, cam_t, cam_f
def read_depth(self, depth_path):
depth = cv2.imdecode(
np.frombuffer(self.read(depth_path), np.uint8), cv2.IMREAD_UNCHANGED
)[..., np.newaxis]
depth[depth == 0] = 65535
return depth
def read_color(self, color_path):
color = cv2.imdecode(
np.frombuffer(self.read(color_path), np.uint8), cv2.IMREAD_UNCHANGED
)[..., :3][..., ::-1]
return color
def read_segment(self, segment_path):
segment = np.array(Image.open(io.BytesIO(self.read(segment_path))))[
..., np.newaxis
]
return segment
def parse_scene(
scene,
dataset_root,
output_root,
ignore_index=-1,
grid_size=None,
fuse_prsp=True,
fuse_pano=True,
parse_pointclouds=True,
parse_depths=True,
vis=False,
):
assert fuse_prsp or fuse_pano
pc_output_root = output_root
im_output_root = os.path.join(output_root, "images")
reader = Structured3DReader(
[
os.path.join(dataset_root, f)
for f in os.listdir(dataset_root)
if f.endswith(".zip")
]
)
scene_id = int(os.path.basename(scene).split("_")[-1])
if scene_id < 3000:
split = "train"
elif 3000 <= scene_id < 3250:
split = "val"
else:
split = "test"
print(f"Processing: {scene} in {split}")
rooms = reader.listdir(os.path.join("Structured3D", scene, "2D_rendering"))
for room in rooms:
im_save_path = os.path.join(
im_output_root, split, os.path.basename(scene), f"room_{room}"
)
pc_save_path = os.path.join(
pc_output_root, split, os.path.basename(scene), f"room_{room}"
)
if os.path.exists(im_save_path) and os.path.exists(pc_save_path):
print(f"exist {split}/{os.path.basename(scene)}/room_{room}")
continue
room_path = os.path.join("Structured3D", scene, "2D_rendering", room)
coord_list = list()
color_list = list()
normal_list = list()
segment_list = list()
prsp_list = list()
pano_list = list()
prsp_depth_list = list()
pano_depth_list = list()
prsp_correspondence_list = list()
pano_correspondence_list = list()
Ks_list = list()
Ts_list = list()
if fuse_prsp and scene:
prsp_path = os.path.join(room_path, "perspective", "full")
frames = reader.listdir(prsp_path)
for frame_id, frame in enumerate(frames):
try:
cam_r, cam_t, cam_f = reader.read_camera(
os.path.join(prsp_path, frame, "camera_pose.txt")
)
depth = reader.read_depth(
os.path.join(prsp_path, frame, "depth.png")
)
color = reader.read_color(
os.path.join(prsp_path, frame, "rgb_rawlight.png")
)
segment = reader.read_segment(
os.path.join(prsp_path, frame, "semantic.png")
)
except:
print(
f"Skipping {scene}_room{room}_frame{frame} perspective view due to loading error"
)
else:
fx, fy = cam_f
height, width = depth.shape[0], depth.shape[1]
pixel = np.transpose(np.indices((width, height)), (2, 1, 0))
pixel = pixel.reshape((-1, 2))
pixel = np.hstack((pixel, np.ones((pixel.shape[0], 1))))
k = np.diag([1.0, 1.0, 1.0])
k[0, 2] = width / 2
k[1, 2] = height / 2
k[0, 0] = k[0, 2] / np.tan(fx)
k[1, 1] = k[1, 2] / np.tan(fy)
coord = (
depth.reshape((-1, 1)) * (np.linalg.inv(k) @ pixel.T).T
).reshape(height, width, 3)
coord = coord @ np.array([[0, 0, 1], [0, -1, 0], [1, 0, 0]])
normal = normal_from_cross_product(coord)
# Filtering invalid points
view_dist = np.maximum(
np.linalg.norm(coord, axis=-1, keepdims=True), float(10e-5)
)
cosine_dist = np.sum(
(coord * normal / view_dist), axis=-1, keepdims=True
)
cosine_dist = np.abs(cosine_dist)
mask = ((cosine_dist > 0.15) & (depth < 65535) & (segment > 0))[
..., 0
].reshape(-1)
coord = np.matmul(coord / 1000, cam_r.T) + cam_t
normal = normal_from_cross_product(coord)
T = np.eye(4)
T[:3, :3] = cam_r
T[:3, 3] = cam_t
Ks_list.append(k)
Ts_list.append(T)
pixel[:, 2] = pixel[:, 2] * frame_id
pixel_valid = pixel[mask]
if sum(mask) > 0:
prsp_depth_list.append(depth)
coord_list.append(coord.reshape(-1, 3)[mask])
color_list.append(color.reshape(-1, 3)[mask])
normal_list.append(normal.reshape(-1, 3)[mask])
segment_list.append(segment.reshape(-1, 1)[mask])
prsp_list.append(color)
prsp_correspondence_list.append(pixel_valid)
else:
print(
f"Skipping {scene}_room{room}_frame{frame} perspective view due to all points are filtered out"
)
if fuse_pano:
pano_path = os.path.join(room_path, "panorama")
try:
_, cam_t, _ = reader.read_camera(
os.path.join(pano_path, "camera_xyz.txt")
)
depth = reader.read_depth(os.path.join(pano_path, "full", "depth.png"))
color = reader.read_color(
os.path.join(pano_path, "full", "rgb_rawlight.png")
)
segment = reader.read_segment(
os.path.join(pano_path, "full", "semantic.png")
)
except:
print(f"Skipping {scene}_room{room} panorama view due to loading error")
else:
p_h, p_w = depth.shape[:2]
pixel = np.transpose(np.indices((p_w, p_h)), (2, 1, 0))
pixel = pixel.reshape((-1, 2))
p_a = np.arange(p_w, dtype=np.float32) / p_w * 2 * np.pi - np.pi
p_b = np.arange(p_h, dtype=np.float32) / p_h * np.pi * -1 + np.pi / 2
p_a = np.tile(p_a[None], [p_h, 1])[..., np.newaxis]
p_b = np.tile(p_b[:, None], [1, p_w])[..., np.newaxis]
p_a_sin, p_a_cos, p_b_sin, p_b_cos = (
np.sin(p_a),
np.cos(p_a),
np.sin(p_b),
np.cos(p_b),
)
x = depth * p_a_cos * p_b_cos
y = depth * p_b_sin
z = depth * p_a_sin * p_b_cos
coord = np.concatenate([x, y, z], axis=-1) / 1000
normal = normal_from_cross_product(coord)
# Filtering invalid points
view_dist = np.maximum(
np.linalg.norm(coord, axis=-1, keepdims=True), float(10e-5)
)
cosine_dist = np.sum(
(coord * normal / view_dist), axis=-1, keepdims=True
)
cosine_dist = np.abs(cosine_dist)
mask = ((cosine_dist > 0.15) & (depth < 65535) & (segment > 0))[
..., 0
].reshape(-1)
coord = coord + cam_t
pixel = np.hstack((pixel, -np.ones((pixel.shape[0], 1))))
pixel_valid = pixel[mask]
if sum(mask) > 0:
pano_depth_list.append(depth)
coord_list.append(coord.reshape(-1, 3)[mask])
color_list.append(color.reshape(-1, 3)[mask])
normal_list.append(normal.reshape(-1, 3)[mask])
segment_list.append(segment.reshape(-1, 1)[mask])
pano_list.append(color)
pano_correspondence_list.append(pixel_valid)
else:
print(
f"Skipping {scene}_room{room} panorama view due to all points are filtered out"
)
if len(prsp_correspondence_list) > 0 and len(pano_correspondence_list) > 0:
coord = np.concatenate(coord_list, axis=0)
coord = coord @ np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]])
color = np.concatenate(color_list, axis=0)
normal = np.concatenate(normal_list, axis=0)
normal = normal @ np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]])
segment = np.concatenate(segment_list, axis=0)
segment25 = np.ones_like(segment, dtype=np.int64) * ignore_index
Ks_list = np.stack(Ks_list, axis=0)
Ts_list = np.stack(Ts_list, axis=0)
Ts_list = Ts_list @ np.array(
[[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
)
for idx, value in enumerate(VALID_CLASS_IDS_25):
mask = np.all(segment == value, axis=-1)
segment25[mask] = idx
correspondence = np.concatenate(prsp_correspondence_list, axis=0)
correspondence = np.concatenate(
[correspondence, pano_correspondence_list[0]], axis=0
)
data_dict = dict(
coord=coord.astype(np.float32),
color=color.astype(np.uint8),
normal=normal.astype(np.float32),
segment=segment25.astype(np.int16),
correspondence=correspondence.astype(np.int32),
)
# exclude ignore, wall, floor, ceiling
valid = np.sum(~np.isin(data_dict["segment"], [-1, 0, 1, 16]))
if valid == 0:
print(
f"Skip {scene}_room{room} due to no effective points (exclude wall, floor, ceiling)"
)
continue
# Grid sampling data
if grid_size is not None:
grid_coord = np.floor(coord / grid_size).astype(int)
_, idx = np.unique(grid_coord, axis=0, return_index=True)
# coord = coord[idx]
for key in data_dict.keys():
data_dict[key] = data_dict[key][idx]
correspondence = data_dict["correspondence"]
correspondence = np.concatenate(
[correspondence, np.arange(correspondence.shape[0])[:, None]], axis=1
)
frame_id_list = np.unique(correspondence[:, 2])
frame_id_list = frame_id_list[frame_id_list != -1]
pano_correspondence_list = [
correspondence[correspondence[:, 2] == -1][:, [0, 1, 3]]
]
prsp_correspondence_list = [
correspondence[correspondence[:, 2] == i][:, [0, 1, 3]]
for i in frame_id_list
]
if parse_pointclouds:
os.makedirs(pc_save_path, exist_ok=True)
# Save data
for key in data_dict.keys():
if key in ["correspondence"]:
continue
np.save(os.path.join(pc_save_path, f"{key}.npy"), data_dict[key])
os.makedirs(im_save_path, exist_ok=True)
if fuse_prsp:
prsp_save_path = os.path.join(im_save_path, "color", "prsp")
prsp_correspondence_save_path = os.path.join(
im_save_path, "correspondence", "prsp_correspondence"
)
Ks_save_path = os.path.join(im_save_path, "intrinsic")
Ts_save_path = os.path.join(im_save_path, "pose")
os.makedirs(prsp_correspondence_save_path, exist_ok=True)
os.makedirs(prsp_save_path, exist_ok=True)
os.makedirs(Ks_save_path, exist_ok=True)
os.makedirs(Ts_save_path, exist_ok=True)
if parse_depths:
prsp_depth_save_path = os.path.join(im_save_path, "depth", "prsp")
os.makedirs(prsp_depth_save_path, exist_ok=True)
for idx in range(len(prsp_list)):
prsp_img_path = os.path.join(prsp_save_path, f"{idx}.png")
cv2.imwrite(prsp_img_path, prsp_list[idx][..., ::-1])
if parse_depths:
prsp_depth_img_path = os.path.join(
prsp_depth_save_path, f"{idx}.png"
)
cv2.imwrite(prsp_depth_img_path, prsp_depth_list[idx])
np.save(
os.path.join(prsp_correspondence_save_path, f"{idx}.npy"),
prsp_correspondence_list[idx],
)
np.save(
os.path.join(Ks_save_path, f"{idx}.npy"),
Ks_list[idx],
)
np.save(
os.path.join(Ts_save_path, f"{idx}.npy"),
Ts_list[idx],
)
if fuse_pano:
pano_save_path = os.path.join(im_save_path, "color", "pano")
pano_correspondence_save_path = os.path.join(
im_save_path, "correspondence", "pano_correspondence"
)
os.makedirs(pano_save_path, exist_ok=True)
os.makedirs(pano_correspondence_save_path, exist_ok=True)
if parse_depths:
pano_depth_save_path = os.path.join(im_save_path, "depth", "pano")
os.makedirs(pano_depth_save_path, exist_ok=True)
for idx in range(len(pano_list)):
pano_img_path = os.path.join(pano_save_path, f"{idx}.png")
cv2.imwrite(pano_img_path, pano_list[idx][..., ::-1])
if parse_depths:
pano_depth_img_path = os.path.join(
pano_depth_save_path, f"{idx}.png"
)
cv2.imwrite(pano_depth_img_path, pano_depth_list[idx])
for idx, pano_correspondence in enumerate(pano_correspondence_list):
np.save(
os.path.join(pano_correspondence_save_path, f"{idx}.npy"),
pano_correspondence,
)
if vis:
from pointcept.utils.visualization import save_point_cloud
os.makedirs("./vis", exist_ok=True)
save_point_cloud(
coord, color / 255, f"./vis/{scene}_room{room}_color.ply"
)
save_point_cloud(
coord, (normal + 1) / 2, f"./vis/{scene}_room{room}_normal.ply"
)
else:
print(f"Skipping {scene}_room{room} due to no valid points")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--dataset_root",
required=True,
help="Path to the Structured3D dataset containing scene folders.",
)
parser.add_argument(
"--output_root",
required=True,
help="Output path where train/val folders will be located.",
)
parser.add_argument(
"--num_workers",
default=mp.cpu_count(),
type=int,
help="Num workers for preprocessing.",
)
parser.add_argument(
"--thread_id",
default=0,
type=int,
help="thread_id.",
)
parser.add_argument(
"--grid_size", default=None, type=float, help="Grid size for grid sampling."
)
parser.add_argument("--ignore_index", default=-1, type=float, help="Ignore index.")
parser.add_argument(
"--fuse_prsp", action="store_true", help="Whether fuse perspective view."
)
parser.add_argument(
"--fuse_pano", action="store_true", help="Whether fuse panorama view."
)
parser.add_argument(
"--parse_pointclouds", action="store_true", help="Whether parse point clouds"
)
parser.add_argument(
"--parse_depths", action="store_true", help="Whether parse depths"
)
config = parser.parse_args()
reader = Structured3DReader(
[
os.path.join(config.dataset_root, f)
for f in os.listdir(config.dataset_root)
if f.endswith(".zip")
]
)
scenes_list = reader.listdir("Structured3D")
scenes_list = sorted(scenes_list)
split_scenes_list = np.array_split(scenes_list, config.num_workers)
split_scenes_list_ = split_scenes_list[config.thread_id]
for scenes_list_i in split_scenes_list_:
parse_scene(
scenes_list_i,
config.dataset_root,
config.output_root,
config.ignore_index,
config.grid_size,
config.fuse_prsp,
config.fuse_pano,
config.parse_pointclouds,
config.parse_depths,
)