Spaces:
Running on Zero
Running on Zero
Perf: binary PLY, downsample input, subsample 200k points max
Browse files
app.py
CHANGED
|
@@ -47,12 +47,17 @@ def _run_depth_inference(image: Image.Image):
|
|
| 47 |
# =====================================================================
|
| 48 |
@spaces.GPU(duration=120)
|
| 49 |
def create_3d_point_cloud(input_pil_image):
|
| 50 |
-
"""Processes the image, calculates 3D vertices, and writes a .ply file."""
|
| 51 |
if input_pil_image is None:
|
| 52 |
return None
|
| 53 |
|
| 54 |
-
# Step A:
|
|
|
|
| 55 |
image = input_pil_image.convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
post_processed = _run_depth_inference(image)
|
| 57 |
depth_matrix = (
|
| 58 |
post_processed[0]["predicted_depth"]
|
|
@@ -61,11 +66,12 @@ def create_3d_point_cloud(input_pil_image):
|
|
| 61 |
|
| 62 |
# Step B: Pinhole camera projection (u, v, depth) -> (X, Y, Z)
|
| 63 |
height, width = depth_matrix.shape
|
| 64 |
-
color_np = np.array(image) / 255.0
|
| 65 |
|
| 66 |
-
focal_length = max(width, height)
|
| 67 |
cx, cy = width / 2.0, height / 2.0
|
| 68 |
-
u, v = np.meshgrid(np.arange(width
|
|
|
|
| 69 |
|
| 70 |
z = depth_matrix.flatten()
|
| 71 |
x = ((u.flatten() - cx) * z) / focal_length
|
|
@@ -74,31 +80,46 @@ def create_3d_point_cloud(input_pil_image):
|
|
| 74 |
# Keep objects between 10 cm and 30 m; discard sky / infinite background
|
| 75 |
valid_mask = (z > 0.1) & (z < 30.0)
|
| 76 |
x, y, z = x[valid_mask], y[valid_mask], z[valid_mask]
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
# Step
|
| 82 |
-
ply_filename = "output_scene.ply"
|
| 83 |
num_points = len(z)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
f.write("
|
| 88 |
-
f.write(
|
| 89 |
-
f.write("property float x\n")
|
| 90 |
-
f.write("property float y\n")
|
| 91 |
-
f.write("property float z\n")
|
| 92 |
-
f.write("property uchar red\n")
|
| 93 |
-
f.write("property uchar green\n")
|
| 94 |
-
f.write("property uchar blue\n")
|
| 95 |
-
f.write("end_header\n")
|
| 96 |
-
for i in range(num_points):
|
| 97 |
-
# Invert Y axis so the model loads right-side up
|
| 98 |
-
f.write(
|
| 99 |
-
f"{x[i]:.4f} {-y[i]:.4f} {z[i]:.4f} "
|
| 100 |
-
f"{rgb_ints[i][0]} {rgb_ints[i][1]} {rgb_ints[i][2]}\n"
|
| 101 |
-
)
|
| 102 |
|
| 103 |
return ply_filename
|
| 104 |
|
|
|
|
| 47 |
# =====================================================================
|
| 48 |
@spaces.GPU(duration=120)
|
| 49 |
def create_3d_point_cloud(input_pil_image):
|
| 50 |
+
"""Processes the image, calculates 3D vertices, and writes a binary .ply file."""
|
| 51 |
if input_pil_image is None:
|
| 52 |
return None
|
| 53 |
|
| 54 |
+
# Step A: Downscale large images before inference.
|
| 55 |
+
# DepthPro quality is stable at 512px; this dramatically cuts runtime.
|
| 56 |
image = input_pil_image.convert("RGB")
|
| 57 |
+
MAX_DIM = 512
|
| 58 |
+
if max(image.width, image.height) > MAX_DIM:
|
| 59 |
+
image.thumbnail((MAX_DIM, MAX_DIM), Image.LANCZOS)
|
| 60 |
+
|
| 61 |
post_processed = _run_depth_inference(image)
|
| 62 |
depth_matrix = (
|
| 63 |
post_processed[0]["predicted_depth"]
|
|
|
|
| 66 |
|
| 67 |
# Step B: Pinhole camera projection (u, v, depth) -> (X, Y, Z)
|
| 68 |
height, width = depth_matrix.shape
|
| 69 |
+
color_np = np.array(image, dtype=np.float32) / 255.0
|
| 70 |
|
| 71 |
+
focal_length = float(max(width, height))
|
| 72 |
cx, cy = width / 2.0, height / 2.0
|
| 73 |
+
u, v = np.meshgrid(np.arange(width, dtype=np.float32),
|
| 74 |
+
np.arange(height, dtype=np.float32))
|
| 75 |
|
| 76 |
z = depth_matrix.flatten()
|
| 77 |
x = ((u.flatten() - cx) * z) / focal_length
|
|
|
|
| 80 |
# Keep objects between 10 cm and 30 m; discard sky / infinite background
|
| 81 |
valid_mask = (z > 0.1) & (z < 30.0)
|
| 82 |
x, y, z = x[valid_mask], y[valid_mask], z[valid_mask]
|
| 83 |
+
rgb_ints = (color_np.reshape(-1, 3)[valid_mask] * 255).astype(np.uint8)
|
| 84 |
|
| 85 |
+
# Step C: Subsample to keep the browser renderer fast (<= 200k points)
|
| 86 |
+
MAX_POINTS = 200_000
|
| 87 |
+
if len(z) > MAX_POINTS:
|
| 88 |
+
idx = np.random.choice(len(z), MAX_POINTS, replace=False)
|
| 89 |
+
x, y, z = x[idx], y[idx], z[idx]
|
| 90 |
+
rgb_ints = rgb_ints[idx]
|
| 91 |
|
| 92 |
+
# Step D: Write BINARY PLY via numpy (100x faster than an ASCII for-loop)
|
|
|
|
| 93 |
num_points = len(z)
|
| 94 |
+
vertex_dtype = np.dtype([
|
| 95 |
+
('x', '<f4'), ('y', '<f4'), ('z', '<f4'),
|
| 96 |
+
('red', 'u1'), ('green', 'u1'), ('blue', 'u1'),
|
| 97 |
+
])
|
| 98 |
+
vertices = np.empty(num_points, dtype=vertex_dtype)
|
| 99 |
+
vertices['x'] = x.astype(np.float32)
|
| 100 |
+
vertices['y'] = (-y).astype(np.float32) # invert Y so it loads right-side up
|
| 101 |
+
vertices['z'] = z.astype(np.float32)
|
| 102 |
+
vertices['red'] = rgb_ints[:, 0]
|
| 103 |
+
vertices['green'] = rgb_ints[:, 1]
|
| 104 |
+
vertices['blue'] = rgb_ints[:, 2]
|
| 105 |
+
|
| 106 |
+
ply_header = (
|
| 107 |
+
"ply\n"
|
| 108 |
+
"format binary_little_endian 1.0\n"
|
| 109 |
+
f"element vertex {num_points}\n"
|
| 110 |
+
"property float x\n"
|
| 111 |
+
"property float y\n"
|
| 112 |
+
"property float z\n"
|
| 113 |
+
"property uchar red\n"
|
| 114 |
+
"property uchar green\n"
|
| 115 |
+
"property uchar blue\n"
|
| 116 |
+
"end_header\n"
|
| 117 |
+
)
|
| 118 |
|
| 119 |
+
ply_filename = "/tmp/output_scene.ply"
|
| 120 |
+
with open(ply_filename, "wb") as f:
|
| 121 |
+
f.write(ply_header.encode("ascii"))
|
| 122 |
+
f.write(vertices.tobytes())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
return ply_filename
|
| 125 |
|