File size: 5,434 Bytes
301a7b2
 
 
08db879
301a7b2
 
 
08db879
301a7b2
 
08db879
301a7b2
 
08db879
f664a16
301a7b2
 
 
08db879
 
301a7b2
 
f664a16
301a7b2
 
 
f664a16
301a7b2
 
08db879
301a7b2
f664a16
301a7b2
 
 
f664a16
301a7b2
08db879
301a7b2
 
 
 
 
08db879
 
 
 
 
 
 
 
 
 
301a7b2
 
f664a16
301a7b2
cf9f2b4
08db879
 
301a7b2
f664a16
 
301a7b2
 
 
f664a16
08db879
f664a16
 
08db879
301a7b2
f664a16
 
08db879
 
 
301a7b2
f664a16
 
08db879
301a7b2
f664a16
 
08db879
 
301a7b2
08db879
 
 
f664a16
 
08db879
 
 
f664a16
 
301a7b2
 
 
08db879
301a7b2
f664a16
08db879
301a7b2
f664a16
 
301a7b2
08db879
 
 
 
 
 
301a7b2
 
 
f664a16
301a7b2
 
f664a16
301a7b2
f664a16
 
08db879
301a7b2
f664a16
301a7b2
 
f664a16
301a7b2
f664a16
301a7b2
f664a16
 
301a7b2
08db879
f664a16
301a7b2
 
08db879
 
301a7b2
 
 
08db879
301a7b2
 
cf9f2b4
08db879
 
 
301a7b2
 
 
08db879
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import laspy
import numpy as np
import cv2
from math import pi
from multiprocessing import Process, shared_memory, cpu_count
import gradio as gr

# === Constants ===
PRECISION = 16
FACTOR = 2 ** PRECISION
NUM_WORKERS = min(cpu_count(), 4)
SHARED_MEMORY_NAME = 'shared_canvas'

# === Parameters ===
IMAGE_HEIGHT = 4096  # Default image height
POINT_SIZE = 3
SCALE_COLORS = False
FULL_RES = False
MAX_RADIUS = 30


def create_shared_memory_array(data, name, dtype):
    print("[STEP 7] Creating shared memory...")
    shm = shared_memory.SharedMemory(create=True, size=data.nbytes, name=name)
    shared_array = np.ndarray(data.shape, dtype=dtype, buffer=shm.buf)
    shared_array[:] = data
    print("[STEP 7] Shared memory initialized.")
    return shm


def release_shared_memory(name):
    print("[STEP 12] Releasing shared memory...")
    shm = shared_memory.SharedMemory(name=name)
    shm.close()
    shm.unlink()
    print("[STEP 12] Shared memory released.")


def draw_points(start, length, canvas_shape, theta, phi, radius, color):
    shm = shared_memory.SharedMemory(name=SHARED_MEMORY_NAME)
    canvas = np.ndarray(canvas_shape, dtype=np.uint16, buffer=shm.buf)

    for i in range(start, start + length):
        if 0 <= theta[i] < canvas_shape[1] * FACTOR and 0 <= phi[i] < canvas_shape[0] * FACTOR:
            cv2.circle(
                canvas,
                (int(theta[i]), int(phi[i])),
                int(radius[i]),
                color=(int(color[i][0]), int(color[i][1]), int(color[i][2])),
                thickness=cv2.FILLED,
                shift=PRECISION
            )


def project_point_cloud(input_file):
    print("[STEP 1] Reading LAS file...")
    try:
        point_cloud = laspy.read(input_file.name)
    except Exception as e:
        return f"Failed to read LAS file: {e}"

    print("[STEP 1] LAS file loaded successfully.")

    height, width = IMAGE_HEIGHT, IMAGE_HEIGHT * 2
    output_shape = (height, width, 3)
    total_points = point_cloud.header.point_count
    print(f"[STEP 2] Point cloud shape: {total_points} points")

    # === Step 3: Create blank canvas ===
    print("[STEP 3] Initializing blank canvas...")
    canvas = np.zeros(output_shape, dtype=np.uint16)

    # === Step 4: Compute distances ===
    print("[STEP 4] Computing 3D distances...")
    xyz = np.vstack((point_cloud.x, point_cloud.y, point_cloud.z)).T
    distances = np.linalg.norm(xyz, axis=1)
    distances[distances == 0] = 1e-6  # Avoid divide by zero

    # === Step 5: Normalize distances for depth-aware radius ===
    print("[STEP 5] Normalizing distances...")
    dist_norm = (distances - distances.min()) / (distances.max() - distances.min())

    # === Step 6: Compute angles ===
    print("[STEP 6] Calculating spherical projection angles...")
    theta = ((np.arctan2(point_cloud.y, point_cloud.x) + pi) / (2 * pi)) * width * FACTOR
    theta = np.mod(theta, width * FACTOR).astype(np.uint64)

    z_norm = np.clip(point_cloud.z / distances, -1.0, 1.0)
    phi = ((np.arccos(z_norm) / pi) * height * FACTOR).astype(np.uint64)

    # === Step 6.5: Compute radius based on distance ===
    print("[STEP 6.5] Calculating radii for depth effect...")
    radii = (FACTOR * POINT_SIZE * (1 - dist_norm) + 1).astype(np.uint64)
    radii = np.clip(radii, 1, MAX_RADIUS)

    # === Step 6.6: Compute colors ===
    print("[STEP 6.6] Extracting and adjusting point colors...")
    colors = np.stack([point_cloud.blue, point_cloud.green, point_cloud.red], axis=-1).astype(np.uint16)
    if SCALE_COLORS:
        colors *= 256
    colors = np.clip(colors, 0, 65535)

    # === Step 7: Create shared memory canvas ===
    shm = create_shared_memory_array(canvas, SHARED_MEMORY_NAME, np.uint16)

    # === Step 8: Start multiprocessing drawing ===
    print("[STEP 8] Launching drawing processes...")
    processes = []
    for i in range(NUM_WORKERS):
        start = int(i * total_points / NUM_WORKERS)
        end = int((i + 1) * total_points / NUM_WORKERS)
        p = Process(target=draw_points, args=(
            start, end - start, output_shape, theta, phi, radii, colors
        ))
        p.start()
        processes.append(p)

    print("[STEP 9] Waiting for drawing to complete...")
    for p in processes:
        p.join()
    print("[STEP 9] All drawing processes finished.")

    # === Step 10: Convert canvas back ===
    print("[STEP 10] Retrieving final image from shared memory...")
    final_image = np.ndarray(output_shape, dtype=np.uint16, buffer=shm.buf)
    if not FULL_RES:
        print("[STEP 10.1] Downsampling image to 8-bit for display...")
        final_image = (final_image / 256).astype(np.uint8)

    # === Step 11: Save image ===
    output_file = "output_image.jpg"
    print(f"[STEP 11] Saving image to: {output_file}")
    cv2.imwrite(output_file, final_image)

    # === Step 12: Cleanup shared memory ===
    release_shared_memory(SHARED_MEMORY_NAME)

    print("[STEP 13] Projection pipeline complete.")
    return output_file


# === Gradio Interface ===
def main(input_file):
    return project_point_cloud(input_file)


iface = gr.Interface(
    fn=main,
    inputs=gr.File(label="Upload LAS File"),
    outputs=gr.Image(type="filepath", label="Projected Image"),
    title="Equirectangular Point Cloud Projection",
    description="Upload a LAS point cloud file and project it into a 360° equirectangular image."
)

if __name__ == "__main__":
    iface.launch()