shuishen
10 hours ago 385be2eca72eb3833efa4be0a0088b34e764788a
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
"""Prepare a bounded RGB/XYZ PLY preview for manual point-cloud annotation.
 
This deliberately does not create raster, vector, mesh, or semantic outputs.
Those products require a spatial cell size and are unrelated to a manual
annotation source.
"""
 
from __future__ import annotations
 
import argparse
import hashlib
import json
import time
from datetime import UTC, datetime
from pathlib import Path
 
import laspy
import numpy as np
import open3d as o3d
 
 
SUPPORTED_SUFFIXES = {".ply", ".pcd", ".xyz", ".xyzn", ".xyzrgb", ".las", ".laz"}
 
 
def file_sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()
 
 
def read_cloud(path: Path) -> tuple[np.ndarray, np.ndarray, bool]:
    if path.suffix.lower() in {".las", ".laz"}:
        source = laspy.read(path)
        dimensions = set(source.point_format.dimension_names)
        xyz = np.column_stack((source.x, source.y, source.z)).astype(np.float64)
        has_rgb = {"red", "green", "blue"}.issubset(dimensions)
        if has_rgb:
            raw_rgb = np.column_stack((source.red, source.green, source.blue)).astype(np.float64)
            divisor = 65_535.0 if float(np.nanpercentile(raw_rgb, 99.5)) > 255.0 else 255.0
            rgb = np.clip(raw_rgb / divisor, 0.0, 1.0)
        else:
            rgb = np.full((len(xyz), 3), 0.72, dtype=np.float64)
    else:
        cloud = o3d.io.read_point_cloud(str(path))
        if cloud.is_empty():
            raise ValueError("Point cloud has no readable XYZ vertices.")
        xyz = np.asarray(cloud.points, dtype=np.float64)
        has_rgb = cloud.has_colors()
        rgb = np.asarray(cloud.colors, dtype=np.float64) if has_rgb else np.full((len(xyz), 3), 0.72, dtype=np.float64)
    valid = np.isfinite(xyz).all(axis=1) & np.isfinite(rgb).all(axis=1)
    xyz, rgb = xyz[valid], np.clip(rgb[valid], 0.0, 1.0)
    if len(xyz) < 50:
        raise ValueError("Point cloud needs at least 50 finite RGB/XYZ points.")
    return xyz, rgb, has_rgb
 
 
def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()
    if args.input.suffix.lower() not in SUPPORTED_SUFFIXES:
        raise SystemExit(f"Unsupported point-cloud type: {args.input.suffix or '(none)'}.")
    if not args.input.is_file():
        raise SystemExit("Input point-cloud file is unavailable.")
    if args.output.exists() and any(args.output.iterdir()):
        raise SystemExit("Output directory is not empty; use a new annotation-source run directory.")
    args.output.mkdir(parents=True, exist_ok=True)
 
    started = time.perf_counter()
    xyz, rgb, has_rgb = read_cloud(args.input)
    preview = o3d.geometry.PointCloud()
    preview.points = o3d.utility.Vector3dVector(xyz)
    preview.colors = o3d.utility.Vector3dVector(rgb)
    preview_name = f"{args.input.stem}.annotation-source.ply"
    preview_path = args.output / preview_name
    if not o3d.io.write_point_cloud(str(preview_path), preview, write_ascii=False):
        raise SystemExit("Could not write annotation preview PLY.")
    metadata = {
        "capability": "05-3d-pointcloud",
        "classification": "B",
        "created_at": datetime.now(UTC).isoformat(),
        "annotation_source_job": True,
        "annotation_source": {
            "schema_version": 1,
            "file": preview_name,
            "point_count": int(len(xyz)),
            "sha256": file_sha256(preview_path),
            "kind": "complete readable RGB/XYZ point cloud; no raster, vector, mesh, or semantic labels",
        },
        "input": {"file": args.input.name, "points": int(len(xyz)), "has_rgb": has_rgb},
        "method": "Open3D/Laspy complete RGB-XYZ point-cloud read without display sampling",
        "device": "CPU",
        "elapsed_seconds": round(time.perf_counter() - started, 3),
        "limitations": [
            "This output is a manual annotation preview only; it is not a DSM, DEM, mesh, vector layer, or semantic classification.",
            "The output retains every readable finite source point. Large point clouds require correspondingly more browser memory and GPU resources during direct rendering.",
        ] + ([] if has_rgb else ["The source has no readable vertex RGB values. The neutral preview is viewable and labelable for geometry review, but it cannot be used for the RGB semantic-model trainer."]),
    }
    (args.output / "run_metadata.json").write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(metadata, ensure_ascii=False))
    return 0
 
 
if __name__ == "__main__":
    raise SystemExit(main())