import multiprocessing import os from pathlib import Path import h5py import numpy as np import torch from tqdm import tqdm def _quantize_coords_batch(coords, coord_scale, coord_offset): coords = np.asarray(coords, dtype=np.float64) coord_offset = np.asarray(coord_offset, dtype=np.float64) return np.rint((coords - coord_offset) / coord_scale).astype(np.int32) def _save_batch(save_path, vis, logits, masks, targets, coords, vmin, vmax, coord_scale, coord_offset, tile_offset=None): if tile_offset is None: tile_offset = np.zeros(3, dtype=np.float64) tile_offset = np.asarray(tile_offset, dtype=np.float64) coord_offset = np.asarray(coord_offset, dtype=np.float64) coords_global = np.asarray(coords, dtype=np.float64) + tile_offset coords_offset_global = coord_offset + tile_offset batch = { "visibility": torch.tensor(np.array(vis), dtype=torch.float32), "logits": torch.tensor(np.array(logits), dtype=torch.float32), "mask": torch.tensor(np.array(masks), dtype=torch.bool), "target": torch.tensor(np.array(targets), dtype=torch.long), "coords_int": torch.tensor(_quantize_coords_batch(coords_global, coord_scale, coords_offset_global), dtype=torch.int32), "coords_scale": torch.tensor(coord_scale, dtype=torch.float64), "coords_offset": torch.tensor(coords_offset_global, dtype=torch.float64), "coords_tile_offset": torch.tensor(tile_offset, dtype=torch.float64), } batch["visibility"] = normalize_visibility(batch["visibility"], vmin, vmax) save_path.parent.mkdir(parents=True, exist_ok=True) torch.save(batch, save_path, pickle_protocol=4) def decode_coordinates(dataset, handle): coords = np.asarray(dataset) if np.issubdtype(coords.dtype, np.integer) and "coords_scale" in handle.attrs: scale = float(handle.attrs["coords_scale"]) offset = np.asarray(handle.attrs.get("coords_offset", np.zeros(3, dtype=np.float32)), dtype=np.float32) return coords.astype(np.float32) * scale + offset return coords.astype(np.float32) def decode_visibility(dataset, handle): visibility = np.asarray(dataset) if np.issubdtype(visibility.dtype, np.integer) and "visibility_quant_max" in handle.attrs: quant_max = float(handle.attrs["visibility_quant_max"]) vmin = np.asarray(handle.attrs["visibility_vmin"], dtype=np.float32) vmax = np.asarray(handle.attrs["visibility_vmax"], dtype=np.float32) normalized = visibility.astype(np.float32) / max(quant_max, 1.0) return normalized * (vmax - vmin) + vmin return visibility.astype(np.float32) def decode_logits(dataset, handle): logits = np.asarray(dataset) if np.issubdtype(logits.dtype, np.integer) and "logits_quant_max" in handle.attrs: quant_max = float(handle.attrs["logits_quant_max"]) return logits.astype(np.float32) / max(quant_max, 1.0) return logits.astype(np.float32) def decode_dense_visibility(visibility, observations): quant_max = float(observations["visibility_quant_max"]) vmin = np.asarray(observations["visibility_vmin"], dtype=np.float32) vmax = np.asarray(observations["visibility_vmax"], dtype=np.float32) normalized = visibility.astype(np.float32) / max(quant_max, 1.0) return normalized * (vmax - vmin) + vmin def decode_dense_logits(logits, observations): quant_max = float(observations["logits_quant_max"]) return logits.astype(np.float32) / max(quant_max, 1.0) def normalize_visibility(vis, vmin, vmax, eps=1e-8): mins = torch.tensor(vmin, dtype=torch.float32).view(1, 1, -1) maxs = torch.tensor(vmax, dtype=torch.float32).view(1, 1, -1) vis_clamped = torch.max(torch.min(vis, maxs), mins) return (vis_clamped - mins) / (maxs - mins + eps) def _pad_views(array, max_views, pad_value=0, dtype=np.float32): current_views = array.shape[0] if current_views >= max_views: return array[:max_views] pad_shape = (max_views - current_views,) + array.shape[1:] padding = np.full(pad_shape, pad_value, dtype=dtype) return np.concatenate([array, padding], axis=0) def process_h5_file(args): path, output_dir, vmin, vmax, batch_size, max_views, coord_scale, train, val, test = args batch_idx = 0 filename_prefix = Path(path).stem with h5py.File(path, "r") as handle: points = handle["points"] keys = list(points.keys()) for start in range(0, len(keys), batch_size): batch_keys = keys[start : start + batch_size] vis = [] logits = [] masks = [] targets = [] coords = [] for key in batch_keys: point = points[key] visibility = decode_visibility(point["visibility"], handle) logit_vectors = decode_logits(point["logit_vectors"], handle) num_views = min(len(visibility), max_views) vis.append(_pad_views(visibility, max_views, pad_value=0.0, dtype=np.float32)) logits.append(_pad_views(logit_vectors, max_views, pad_value=0.0, dtype=np.float32)) mask = np.zeros(max_views, dtype=bool) mask[:num_views] = True masks.append(mask) targets.append(point["ground_truth"][()]) coords.append(decode_coordinates(point["coordinates"], handle)) if path in train: save_path = Path(output_dir) / "train" / f"{filename_prefix}_batch_{batch_idx:05d}.pt" elif path in test: save_path = Path(output_dir) / "test" / f"{filename_prefix}_batch_{batch_idx:05d}.pt" else: save_path = Path(output_dir) / "val" / f"{filename_prefix}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords, dtype=np.float32), axis=0) _save_batch(save_path, vis, logits, masks, targets, coords, vmin, vmax, coord_scale, coord_offset, np.zeros(3, dtype=np.float64)) batch_idx += 1 return f"{path} done" def save_tile_observations_to_pt( cfg, tile_name, split_name, coords_array, tile_offset, all_observations, output_dir, batch_size=1024, ): output_dir = Path(output_dir) / split_name output_dir.mkdir(parents=True, exist_ok=True) max_views = int(cfg["selection"]["max_views"]) min_views = int(cfg["selection"]["min_views"]) vmin = cfg["data"]["vmin"] vmax = cfg["data"]["vmax"] batch_idx = 0 coord_scale = float(cfg.get("storage", {}).get("coord_scale", 0.001)) vis = [] logits = [] masks = [] targets = [] coords = [] saved_paths = [] if isinstance(all_observations, dict) and all_observations.get("mode") == "dense": counts = all_observations["counts"] ground_truth = all_observations["ground_truth"] for point_idx in range(len(counts)): num_views = int(counts[point_idx]) if num_views < min_views: continue gt = int(ground_truth[point_idx]) if gt < 0: continue visibility = decode_dense_visibility(all_observations["visibility"][point_idx, :num_views], all_observations) logit_vectors = decode_dense_logits(all_observations["logit_vectors"][point_idx, :num_views], all_observations) vis.append(_pad_views(visibility, max_views, pad_value=0.0, dtype=np.float32)) logits.append(_pad_views(logit_vectors, max_views, pad_value=0.0, dtype=np.float32)) mask = np.zeros(max_views, dtype=bool) mask[:num_views] = True masks.append(mask) targets.append(gt) coords.append(np.asarray(coords_array[point_idx], dtype=np.float32)) if len(vis) == batch_size: save_path = output_dir / f"{tile_name}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords, dtype=np.float32), axis=0) _save_batch(save_path, vis, logits, masks, targets, coords, vmin, vmax, coord_scale, coord_offset, tile_offset) saved_paths.append(str(save_path)) vis, logits, masks, targets, coords = [], [], [], [], [] batch_idx += 1 else: for point_idx, observations in all_observations.items(): if len(observations["camera"]) < min_views: continue gt = observations["ground_truth"] if gt is None: continue visibility = np.asarray(observations["visibility"], dtype=np.float32) logit_vectors = np.asarray(observations["logit_vectors"], dtype=np.float32) num_views = min(len(visibility), max_views) vis.append(_pad_views(visibility, max_views, pad_value=0.0, dtype=np.float32)) logits.append(_pad_views(logit_vectors, max_views, pad_value=0.0, dtype=np.float32)) mask = np.zeros(max_views, dtype=bool) mask[:num_views] = True masks.append(mask) targets.append(gt) coords.append(np.asarray(coords_array[point_idx], dtype=np.float32)) if len(vis) == batch_size: save_path = output_dir / f"{tile_name}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords, dtype=np.float32), axis=0) _save_batch(save_path, vis, logits, masks, targets, coords, vmin, vmax, coord_scale, coord_offset, tile_offset) saved_paths.append(str(save_path)) vis, logits, masks, targets, coords = [], [], [], [], [] batch_idx += 1 if vis: save_path = output_dir / f"{tile_name}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords, dtype=np.float32), axis=0) _save_batch(save_path, vis, logits, masks, targets, coords, vmin, vmax, coord_scale, coord_offset, tile_offset) saved_paths.append(str(save_path)) return saved_paths def save_compact_payloads_to_pt( cfg, tile_name, split_name, coords_array, tile_offset, payloads, output_dir, batch_size=1024, ): output_dir = Path(output_dir) / split_name output_dir.mkdir(parents=True, exist_ok=True) max_views = int(cfg["selection"]["max_views"]) min_views = int(cfg["selection"]["min_views"]) vmin = cfg["data"]["vmin"] vmax = cfg["data"]["vmax"] payloads = [payload for payload in payloads if payload is not None and payload.get("point_indices") is not None] if not payloads: return [] point_indices = np.concatenate([payload["point_indices"] for payload in payloads], axis=0) if point_indices.size == 0: return [] visibility = np.concatenate([payload["visibility"] for payload in payloads], axis=0) logits = np.concatenate([payload["logit_vectors"] for payload in payloads], axis=0) ground_truth = np.concatenate([payload["ground_truth"] for payload in payloads], axis=0) order = np.argsort(point_indices, kind="mergesort") point_indices = point_indices[order] visibility = visibility[order] logits = logits[order] ground_truth = ground_truth[order] quant_max_vis = float(payloads[0]["visibility_quant_max"]) quant_max_logits = float(payloads[0]["logits_quant_max"]) payload_vmin = np.asarray(payloads[0]["visibility_vmin"], dtype=np.float32) payload_vmax = np.asarray(payloads[0]["visibility_vmax"], dtype=np.float32) saved_paths = [] vis_batch = [] logits_batch = [] masks_batch = [] targets_batch = [] coords_batch = [] batch_idx = 0 coord_scale = float(cfg.get("storage", {}).get("coord_scale", 0.001)) group_starts = np.r_[0, np.flatnonzero(np.diff(point_indices)) + 1] group_ends = np.r_[group_starts[1:], point_indices.size] for start, end in zip(group_starts, group_ends): point_idx = int(point_indices[start]) num_obs = end - start if num_obs < min_views: continue gt_slice = ground_truth[start:end] valid_gt = gt_slice[gt_slice >= 0] if valid_gt.size == 0: continue gt = int(valid_gt[0]) local_visibility = visibility[start:end] if num_obs > max_views: keep = np.lexsort((local_visibility[:, 0], local_visibility[:, 1]))[:max_views] local_visibility = local_visibility[keep] local_logits = logits[start:end][keep] num_views = max_views else: local_logits = logits[start:end] num_views = num_obs vis_float = (local_visibility.astype(np.float32) / max(quant_max_vis, 1.0)) * (payload_vmax - payload_vmin) + payload_vmin logits_float = local_logits.astype(np.float32) / max(quant_max_logits, 1.0) vis_batch.append(_pad_views(vis_float, max_views, pad_value=0.0, dtype=np.float32)) logits_batch.append(_pad_views(logits_float, max_views, pad_value=0.0, dtype=np.float32)) mask = np.zeros(max_views, dtype=bool) mask[:num_views] = True masks_batch.append(mask) targets_batch.append(gt) coords_batch.append(np.asarray(coords_array[point_idx], dtype=np.float32)) if len(vis_batch) == batch_size: save_path = output_dir / f"{tile_name}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords_batch, dtype=np.float32), axis=0) _save_batch( save_path, vis_batch, logits_batch, masks_batch, targets_batch, coords_batch, vmin, vmax, coord_scale, coord_offset, tile_offset, ) saved_paths.append(str(save_path)) vis_batch, logits_batch, masks_batch, targets_batch, coords_batch = [], [], [], [], [] batch_idx += 1 if vis_batch: save_path = output_dir / f"{tile_name}_batch_{batch_idx:05d}.pt" coord_offset = np.min(np.asarray(coords_batch, dtype=np.float32), axis=0) _save_batch( save_path, vis_batch, logits_batch, masks_batch, targets_batch, coords_batch, vmin, vmax, coord_scale, coord_offset, tile_offset, ) saved_paths.append(str(save_path)) return saved_paths def save_dataset_to_pt_parallel(cfg, train, val, test, output_dir, batch_size=1024, num_workers=4): output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) max_views = cfg["selection"]["max_views"] vmin = cfg["data"]["vmin"] vmax = cfg["data"]["vmax"] h5_paths = train + val + test args_list = [ (path, str(output_dir), vmin, vmax, batch_size, max_views, float(cfg.get("storage", {}).get("coord_scale", 0.001)), train, val, test) for path in h5_paths ] with multiprocessing.Pool(num_workers) as pool: for result in pool.imap_unordered(process_h5_file, args_list): print(result)