forked from graphdeco-inria/gaussian-splatting
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path3dgsconverter.py
743 lines (594 loc) · 32.7 KB
/
3dgsconverter.py
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
""" https://github.com/francescofugazzi/3dgsconverter
A tool for converting 3D Gaussian Splatting .ply files into a format suitable for Cloud Compare and vice-versa.
Enhance your point cloud editing with added functionalities like RGB coloring, density filtering, and flyer removal.
1. Conversion from 3DGS to Cloud Compare format with RGB addition:
python 3dgsconverter.py -i input_3dgs.ply -o output_cc.ply -f cc --rgb
2. Conversion from Cloud Compare format back to 3DGS:
python 3dgsconverter.py -i input_cc.ply -o output_3dgs.ply -f 3dgs
3. Applying Density Filter during conversion:
python 3dgsconverter.py -i input_3dgs.ply -o output_cc.ply -f cc --density_filter
4. Applying Density Filter and Removing floaters during conversion:
python 3dgsconverter.py -i input_3dgs.ply -o output_cc.ply -f cc --density_filter --remove fliers
Hui:
python 3dgsconverter.py -i output/out_bonsai_new_cut/point_cloud/iteration_30000/out_bonsai_new_cut_cut.ply -o output/out_bonsai_new_cut/point_cloud/iteration_30000/out_bonsai_new_cut_cut_cc.ply -f cc --rgb
Convert between standard 3D Gaussian Splat and Cloud Compare formats.
optional arguments:
-h, --help show this help message and exit
--input INPUT, -i INPUT
Path to the source point cloud file.
--output OUTPUT, -o OUTPUT
Path to save the converted point cloud file.
--target_format {3dgs,cc}, -f {3dgs,cc}
Target point cloud format.
--debug, -d Enable debug prints.
--bbox minX minY minZ maxX maxY maxZ
Specify the 3D bounding box to crop the point cloud.
--rgb Add RGB values to the output file based on f_dc values
(only applicable when converting to Cloud Compare
format).
--density_filter Filter the points to keep only regions with higher
point density.
--remove_flyers Remove flyer points that are distant from the main
cloud.
"""
import argparse
import numpy as np
import multiprocessing
import sys
import signal
import os
from plyfile import PlyData, PlyElement
#from tqdm import tqdm
from collections import deque
from multiprocessing import Pool, cpu_count
from sklearn.neighbors import NearestNeighbors
DEBUG = False
class Utility:
@staticmethod
def text_based_detect_format(file_path):
debug_print("[DEBUG] Executing 'text_based_detect_format' function...")
"""Detect if the given file is in '3dgs' or 'cc' format."""
with open(file_path, 'rb') as file:
header_bytes = file.read(2048) # Read the beginning to detect the format
header = header_bytes.decode('utf-8', errors='ignore')
if "property float f_dc_0" in header:
debug_print("[DEBUG] Detected format: 3dgs")
return "3dgs"
elif "property float scal_f_dc_0" in header or "property float scalar_scal_f_dc_0" in header or "property float scalar_f_dc_0" in header:
debug_print("[DEBUG] Detected format: cc")
return "cc"
else:
return None
@staticmethod
def copy_data_with_prefix_check(source, target, possible_prefixes):
debug_print("[DEBUG] Executing 'copy_data_with_prefix_check' function...")
"""
Given two structured numpy arrays (source and target), copy the data from source to target.
If a field exists in source but not in target, this function will attempt to find the field
in target by adding any of the possible prefixes to the field name.
"""
for name in source.dtype.names:
if name in target.dtype.names:
target[name] = source[name]
else:
copied = False
for prefix in possible_prefixes:
# If the field starts with the prefix, try the field name without the prefix
if name.startswith(prefix):
stripped_name = name[len(prefix):]
if stripped_name in target.dtype.names:
target[stripped_name] = source[name]
copied = True
break
# If the field doesn't start with any prefix, try adding the prefix
else:
prefixed_name = prefix + name
if prefixed_name in target.dtype.names:
debug_print(f"[DEBUG] Copying data from '{name}' to '{prefixed_name}'")
target[prefixed_name] = source[name]
copied = True
break
if not copied:
print(f"Warning: Field {name} not found in target.")
@staticmethod
def compute_rgb_from_vertex(vertices):
debug_print("[DEBUG] Executing 'compute_rgb_from_vertex' function...")
# Depending on the available field names, choose the appropriate ones
if 'f_dc_0' in vertices.dtype.names:
f_dc = np.column_stack((vertices['f_dc_0'], vertices['f_dc_1'], vertices['f_dc_2']))
else:
f_dc = np.column_stack((vertices['scal_f_dc_0'], vertices['scal_f_dc_1'], vertices['scal_f_dc_2']))
colors = (f_dc + 1) * 127.5
colors = np.clip(colors, 0, 255).astype(np.uint8)
debug_print("[DEBUG] RGB colors computed.")
return colors
@staticmethod
def parallel_voxel_counting(vertices, voxel_size=1.0):
debug_print("[DEBUG] Executing 'parallel_voxel_counting' function...")
"""Counts the number of points in each voxel in a parallelized manner."""
num_processes = cpu_count()
chunk_size = len(vertices) // num_processes
chunks = [vertices[i:i + chunk_size] for i in range(0, len(vertices), chunk_size)]
num_cores = max(1, multiprocessing.cpu_count() - 1)
with Pool(processes=num_cores, initializer=init_worker) as pool:
results = pool.starmap(Utility.count_voxels_chunk, [(chunk, voxel_size) for chunk in chunks])
# Aggregate results from all processes
total_voxel_counts = {}
for result in results:
for k, v in result.items():
if k in total_voxel_counts:
total_voxel_counts[k] += v
else:
total_voxel_counts[k] = v
debug_print(f"[DEBUG] Voxel counting completed with {len(total_voxel_counts)} unique voxels found.")
return total_voxel_counts
@staticmethod
def count_voxels_chunk(vertices_chunk, voxel_size):
debug_print("[DEBUG] Executing 'count_voxels_chunk' function for a chunk...")
"""Count the number of points in each voxel for a chunk of vertices."""
voxel_counts = {}
for vertex in vertices_chunk:
voxel_coords = (int(vertex['x'] / voxel_size), int(vertex['y'] / voxel_size), int(vertex['z'] / voxel_size))
if voxel_coords in voxel_counts:
voxel_counts[voxel_coords] += 1
else:
voxel_counts[voxel_coords] = 1
debug_print(f"[DEBUG] Chunk processed with {len(voxel_counts)} voxels counted.")
return voxel_counts
@staticmethod
def get_neighbors(voxel_coords):
debug_print(f"[DEBUG] Getting neighbors for voxel: {voxel_coords}...")
"""Get the face-touching neighbors of the given voxel coordinates."""
x, y, z = voxel_coords
neighbors = [
(x-1, y, z), (x+1, y, z),
(x, y-1, z), (x, y+1, z),
(x, y, z-1), (x, y, z+1)
]
return neighbors
@staticmethod
def knn_worker(args):
debug_print(f"[DEBUG] Executing 'knn_worker' function for vertex: {args[0]}...")
"""Utility function for parallel KNN computation."""
coords, tree, k = args
coords = coords.reshape(1, -1) # Reshape to a 2D array
distances, _ = tree.kneighbors(coords)
avg_distance = np.mean(distances[:, 1:])
debug_print(f"[DEBUG] Average distance computed for vertex: {args[0]} is {avg_distance}.")
return avg_distance
@staticmethod
def some_other_utility_function():
...
class BaseConverter:
def __init__(self, data):
self.data = data
def extract_vertex_data(vertices, has_scal=True, has_rgb=False):
"""Extract and convert vertex data from a structured numpy array of vertices."""
debug_print("[DEBUG] Executing 'extract_vertex_data' function...")
converted_data = []
# Determine the prefix to be used based on whether "scal_" should be included
prefix = 'scal_' if has_scal else ''
debug_print(f"[DEBUG] Prefix determined as: {prefix}")
# Iterate over each vertex and extract the necessary attributes
for vertex in vertices:
entry = (
vertex['x'], vertex['y'], vertex['z'],
vertex['nx'], vertex['ny'], vertex['nz'],
vertex[f'{prefix}f_dc_0'], vertex[f'{prefix}f_dc_1'], vertex[f'{prefix}f_dc_2'],
*[vertex[f'{prefix}f_rest_{i}'] for i in range(45)],
vertex[f'{prefix}opacity'],
vertex[f'{prefix}scale_0'], vertex[f'{prefix}scale_1'], vertex[f'{prefix}scale_2'],
vertex[f'{prefix}rot_0'], vertex[f'{prefix}rot_1'], vertex[f'{prefix}rot_2'], vertex[f'{prefix}rot_3']
)
# If the point cloud contains RGB data, append it to the entry
if has_rgb:
entry += (vertex['red'], vertex['green'], vertex['blue'])
converted_data.append(entry)
debug_print("[DEBUG] 'extract_vertex_data' function completed.")
return converted_data
def apply_density_filter(self, voxel_size=1.0, threshold_percentage=0.32):
debug_print("[DEBUG] Executing 'apply_density_filter' function...")
vertices = self.data['vertex'].data
# Convert threshold_percentage into a ratio
threshold_ratio = threshold_percentage / 100.0
# Parallelized voxel counting
voxel_counts = Utility.parallel_voxel_counting(vertices, voxel_size)
threshold = int(len(vertices) * threshold_ratio)
dense_voxels = {k: v for k, v in voxel_counts.items() if v >= threshold}
visited = set()
max_cluster = set()
for voxel in dense_voxels:
if voxel not in visited:
current_cluster = set()
queue = deque([voxel])
while queue:
current_voxel = queue.popleft()
visited.add(current_voxel)
current_cluster.add(current_voxel)
for neighbor in Utility.get_neighbors(current_voxel):
if neighbor in dense_voxels and neighbor not in visited:
queue.append(neighbor)
visited.add(neighbor)
if len(current_cluster) > len(max_cluster):
max_cluster = current_cluster
filtered_vertices = [vertex for vertex in vertices if (int(vertex['x'] / voxel_size), int(vertex['y'] / voxel_size), int(vertex['z'] / voxel_size)) in max_cluster]
new_vertex_element = PlyElement.describe(np.array(filtered_vertices, dtype=vertices.dtype), 'vertex')
# Update the plydata elements and the internal self.data
converted_data = self.data
converted_data.elements = (new_vertex_element,) + converted_data.elements[1:]
self.data = converted_data # Update the internal data with the filtered data
print(f"After density filter, retained {len(filtered_vertices)} out of {len(vertices)} vertices.")
def remove_flyers(self, k=25, threshold_factor=10.5, chunk_size=50000):
debug_print("[DEBUG] Executing 'remove_flyers' function...")
# Extract vertex data from the current object's data
vertices = self.data['vertex'].data
# Display the number of input vertices
debug_print(f"[DEBUG] Number of input vertices: {len(vertices)}")
# Number of chunks
num_chunks = len(vertices) // chunk_size + (len(vertices) % chunk_size > 0)
masks = []
# Create a pool of workers
num_cores = max(1, cpu_count() - 1) # Leave one core free
with Pool(processes=num_cores, initializer=init_worker) as pool:
for i in range(num_chunks):
start_idx = i * chunk_size
end_idx = start_idx + chunk_size
chunk_coords = np.vstack((vertices['x'][start_idx:end_idx], vertices['y'][start_idx:end_idx], vertices['z'][start_idx:end_idx])).T
# Compute K-Nearest Neighbors for the chunk
nbrs = NearestNeighbors(n_neighbors=k+1, algorithm='ball_tree').fit(chunk_coords)
avg_distances = pool.map(Utility.knn_worker, [(coord, nbrs, k) for coord in chunk_coords])
# Calculate the threshold for removal based on the mean and standard deviation of the average distances
threshold = np.mean(avg_distances) + threshold_factor * np.std(avg_distances)
# Create a mask for points to retain for this chunk
mask = np.array(avg_distances) < threshold
masks.append(mask)
# Combine masks from all chunks
combined_mask = np.concatenate(masks)
# Generate a new PlyElement with the filtered vertices based on the combined mask
new_vertex_element = PlyElement.describe(vertices[combined_mask], 'vertex')
# Update the plydata elements and the internal self.data
self.data.elements = (new_vertex_element,) + self.data.elements[1:]
print(f"After removing flyers, retained {len(vertices[combined_mask])} out of {len(vertices)} vertices.")
def define_dtype(self, has_scal, has_rgb=False):
debug_print("[DEBUG] Executing 'define_dtype' function...")
prefix = 'scalar_scal_' if has_scal else ''
debug_print(f"[DEBUG] Prefix determined as: {prefix}")
dtype = [
('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
('nx', 'f4'), ('ny', 'f4'), ('nz', 'f4'),
(f'{prefix}f_dc_0', 'f4'), (f'{prefix}f_dc_1', 'f4'), (f'{prefix}f_dc_2', 'f4'),
*[(f'{prefix}f_rest_{i}', 'f4') for i in range(45)],
(f'{prefix}opacity', 'f4'),
(f'{prefix}scale_0', 'f4'), (f'{prefix}scale_1', 'f4'), (f'{prefix}scale_2', 'f4'),
(f'{prefix}rot_0', 'f4'), (f'{prefix}rot_1', 'f4'), (f'{prefix}rot_2', 'f4'), (f'{prefix}rot_3', 'f4')
]
debug_print("[DEBUG] Main dtype constructed.")
if has_rgb:
dtype.extend([('red', 'u1'), ('green', 'u1'), ('blue', 'u1')])
debug_print("[DEBUG] RGB fields added to dtype.")
debug_print("[DEBUG] 'define_dtype' function completed.")
return dtype, prefix
def has_rgb(self):
return 'red' in self.data['vertex'].data.dtype.names and 'green' in self.data['vertex'].data.dtype.names and 'blue' in self.data['vertex'].data.dtype.names
def crop_by_bbox(self, min_x, min_y, min_z, max_x, max_y, max_z):
# Perform cropping based on the bounding box
self.data['vertex'].data = self.data['vertex'].data[
(self.data['vertex'].data['x'] >= min_x) &
(self.data['vertex'].data['x'] <= max_x) &
(self.data['vertex'].data['y'] >= min_y) &
(self.data['vertex'].data['y'] <= max_y) &
(self.data['vertex'].data['z'] >= min_z) &
(self.data['vertex'].data['z'] <= max_z)
]
# Print the number of vertices after cropping
print(f"Number of vertices after cropping: {len(self.data['vertex'].data)}")
class Format3dgs(BaseConverter):
def to_cc(self, bbox=None, apply_density_filter=False, remove_flyers=False, process_rgb=True):
debug_print("[DEBUG] Starting conversion from 3DGS to CC...")
# Crop the data based on the bounding box if specified
if bbox:
min_x, min_y, min_z, max_x, max_y, max_z = bbox
self.crop_by_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
debug_print("[DEBUG] Bounding box cropped.")
# Apply density filter if required
if apply_density_filter:
self.apply_density_filter()
debug_print("[DEBUG] Density filter applied.")
# Remove flyers if required
if remove_flyers:
self.remove_flyers()
debug_print("[DEBUG] Flyers removed.")
# Load vertices from the provided data
vertices = self.data['vertex'].data
debug_print(f"[DEBUG] Loaded {len(vertices)} vertices.")
# Check if RGB processing is required
if process_rgb:
debug_print("[DEBUG] RGB processing is enabled.")
# Compute RGB values for the vertices
rgb_values = Utility.compute_rgb_from_vertex(vertices)
if rgb_values is not None:
# Define a new data type for the vertices that includes RGB
new_dtype, prefix = self.define_dtype(has_scal=True, has_rgb=True)
# Create a new numpy array with the new data type
converted_data = np.zeros(vertices.shape, dtype=new_dtype)
# Copy the vertex data to the new numpy array
Utility.copy_data_with_prefix_check(vertices, converted_data, [prefix])
# Add the RGB values to the new numpy array
converted_data['red'] = rgb_values[:, 0]
converted_data['green'] = rgb_values[:, 1]
converted_data['blue'] = rgb_values[:, 2]
debug_print("RGB processing completed.")
else:
debug_print("[DEBUG] RGB computation failed. Skipping RGB processing.")
process_rgb = False
if not process_rgb:
debug_print("[DEBUG] RGB processing is skipped.")
# Define a new data type for the vertices without RGB
new_dtype, prefix = self.define_dtype(has_scal=True, has_rgb=False)
# Create a new numpy array with the new data type
converted_data = np.zeros(vertices.shape, dtype=new_dtype)
# Copy the vertex data to the new numpy array
Utility.copy_data_with_prefix_check(vertices, converted_data, [prefix])
# For now, we'll just return the converted_data for the sake of this integration
debug_print("[DEBUG] Conversion from 3DGS to CC completed.")
return converted_data
def to_3dgs(self, bbox=None, apply_density_filter=False, remove_flyers=False):
debug_print("[DEBUG] Starting conversion from 3DGS to 3DGS...")
# Crop the data based on the bounding box if specified
if bbox:
min_x, min_y, min_z, max_x, max_y, max_z = bbox
self.crop_by_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
debug_print("[DEBUG] Bounding box cropped.")
# Apply density filter if required
if apply_density_filter:
self.apply_density_filter()
debug_print("[DEBUG] Density filter applied.")
# Remove flyers if required
if remove_flyers:
self.remove_flyers()
debug_print("[DEBUG] Flyers removed.")
# Load vertices from the updated data after all filters
vertices = self.data['vertex'].data
debug_print(f"[DEBUG] Loaded {len(vertices)} vertices.")
# Create a new structured numpy array for 3DGS format
dtype_3dgs = self.define_dtype(has_scal=False, has_rgb=False) # Define 3DGS dtype without any prefix
converted_data = np.zeros(vertices.shape, dtype=dtype_3dgs)
# Use the helper function to copy the data from vertices to converted_data
Utility.copy_data_with_prefix_check(vertices, converted_data, ["", "scal_", "scalar_", "scalar_scal_"])
debug_print("[DEBUG] Data copying completed.")
debug_print("\\n[DEBUG] Sample of converted data (first 5 rows):")
if DEBUG:
for i in range(5):
debug_print(converted_data[i])
debug_print("[DEBUG] Conversion from 3DGS to 3DGS completed.")
return converted_data
def ignore_rgb(self):
debug_print("[DEBUG] Checking RGB for 3DGS data...")
# Initialize converted_data to the original vertex data
converted_data = self.data['vertex'].data
# Check if RGB is present
if self.has_rgb():
# Define a new data type for the data that excludes RGB
new_dtype = Utility.define_dtype(has_scal=True, has_rgb=False)
# Create a new numpy array with the new data type
converted_data_without_rgb = np.zeros(self.data['vertex'].data.shape, dtype=new_dtype)
# Copy the data to the new numpy array, excluding RGB
Utility.copy_data_with_prefix_check(self.data['vertex'].data, converted_data_without_rgb, exclude=['red', 'green', 'blue'])
converted_data = converted_data_without_rgb # Update the converted data
debug_print("[DEBUG] RGB removed from data.")
else:
debug_print("[DEBUG] Data does not have RGB or RGB removal is skipped.")
# For now, we'll just return the converted_data for the sake of this integration
debug_print("[DEBUG] RGB check for 3DGS data completed.")
return converted_data
class FormatCC(BaseConverter):
def to_3dgs(self, bbox=None, apply_density_filter=False, remove_flyers=False):
debug_print("[DEBUG] Starting conversion from CC to 3DGS...")
# Crop the data based on the bounding box if specified
if bbox:
min_x, min_y, min_z, max_x, max_y, max_z = bbox
self.crop_by_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
debug_print("[DEBUG] Bounding box cropped.")
# Apply density filter if required
if apply_density_filter:
self.data = self.apply_density_filter()
debug_print("[DEBUG] Density filter applied.")
# Remove flyers if required
if remove_flyers:
self.data = self.remove_flyers()
debug_print("[DEBUG] Flyers removed.")
# Load vertices from the updated data after all filters
vertices = self.data['vertex'].data
debug_print(f"[DEBUG] Loaded {len(vertices)} vertices.")
# Create a new structured numpy array for 3DGS format
dtype_3dgs = self.define_dtype(has_scal=False, has_rgb=False) # Define 3DGS dtype without any prefix
converted_data = np.zeros(vertices.shape, dtype=dtype_3dgs)
# Use the helper function to copy the data from vertices to converted_data
Utility.copy_data_with_prefix_check(vertices, converted_data, ["", "scal_", "scalar_", "scalar_scal_"])
debug_print("[DEBUG] Data copying completed.")
debug_print("\\n[DEBUG] Sample of converted data (first 5 rows):")
if DEBUG:
for i in range(5):
debug_print(converted_data[i])
debug_print("[DEBUG] Conversion from CC to 3DGS completed.")
return converted_data
def to_cc(self, bbox=None, apply_density_filter=False, remove_flyers=False, process_rgb=False):
debug_print("[DEBUG] Processing CC data...")
# Crop the data based on the bounding box if specified
if bbox:
min_x, min_y, min_z, max_x, max_y, max_z = bbox
self.crop_by_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
debug_print("[DEBUG] Bounding box cropped.")
# Apply density filter if required
if apply_density_filter:
self.apply_density_filter()
debug_print("[DEBUG] Density filter applied.")
# Remove flyers if required
if remove_flyers:
self.remove_flyers()
debug_print("[DEBUG] Flyers removed.")
# Check if RGB processing is required
if process_rgb and not self.has_rgb():
self.add_rgb()
debug_print("[DEBUG] RGB added to data.")
else:
debug_print("[DEBUG] RGB processing is skipped or data already has RGB.")
converted_data = self.data
# For now, we'll just return the converted_data for the sake of this integration
debug_print("[DEBUG] CC data processing completed.")
return converted_data
def add_or_ignore_rgb(self, process_rgb=True):
debug_print("[DEBUG] Checking RGB for CC data...")
# If RGB processing is required and if RGB is not present
if process_rgb and not self.has_rgb():
# Compute RGB values for the data
rgb_values = Utility.compute_rgb_from_vertex(self.data)
# Define a new data type for the data that includes RGB
new_dtype = Utility.define_dtype(has_scal=True, has_rgb=True)
# Create a new numpy array with the new data type
converted_data = np.zeros(self.data.shape, dtype=new_dtype)
# Copy the data to the new numpy array
Utility.copy_data_with_prefix_check(self.data, converted_data)
# Add the RGB values to the new numpy array
converted_data['red'] = rgb_values[:, 0]
converted_data['green'] = rgb_values[:, 1]
converted_data['blue'] = rgb_values[:, 2]
self.data = converted_data # Update the instance's data with the new data
debug_print("[DEBUG] RGB added to data.")
else:
debug_print("[DEBUG] RGB processing is skipped or data already has RGB.")
converted_data = self.data # If RGB is not added or skipped, the converted_data is just the original data.
# Return the converted_data
debug_print("[DEBUG] RGB check for CC data completed.")
return converted_data
def convert(data, source_format, target_format, **kwargs):
debug_print(f"[DEBUG] Starting conversion from {source_format} to {target_format}...")
if source_format == "3dgs":
converter = Format3dgs(data)
elif source_format == "cc":
converter = FormatCC(data)
else:
raise ValueError("Unsupported source format")
# Apply optional operations
if kwargs.get("bbox"):
min_x, min_y, min_z, max_x, max_y, max_z = kwargs.get("bbox")
print("Cropping by bounding box...")
converter.crop_by_bbox(min_x, min_y, min_z, max_x, max_y, max_z)
if kwargs.get("density_filter"):
print("Applying density filter...")
converter.apply_density_filter()
if kwargs.get("remove_flyers"):
print("Removing flyers...")
converter.remove_flyers()
# RGB processing
if source_format == "3dgs" and target_format == "cc":
if kwargs.get("process_rgb", False):
debug_print("[DEBUG] Computing RGB for 3DGS data...")
# No need to explicitly call a function here, as the RGB computation is part of the to_cc() method.
else:
debug_print("[DEBUG] Ignoring RGB for 3DGS data...")
converter.ignore_rgb()
elif source_format == "cc":
if kwargs.get("process_rgb", False) and converter.has_rgb():
print("Error: Source CC file already contains RGB data. Conversion stopped.")
return None
debug_print("[DEBUG] Adding or ignoring RGB for CC data...")
converter.add_or_ignore_rgb(process_rgb=kwargs.get("process_rgb", False))
elif source_format == "3dgs" and target_format == "3dgs":
debug_print("[DEBUG] Ignoring RGB for 3DGS to 3DGS conversion...")
converter.ignore_rgb()
# Conversion operations
process_rgb_flag = kwargs.get("process_rgb", False)
if source_format == "3dgs" and target_format == "cc":
debug_print("[DEBUG] Converting 3DGS to CC...")
return converter.to_cc(process_rgb=process_rgb_flag)
elif source_format == "cc" and target_format == "3dgs":
debug_print("[DEBUG] Converting CC to 3DGS...")
return converter.to_3dgs()
elif source_format == "3dgs" and target_format == "3dgs":
debug_print("[DEBUG] Applying operations on 3DGS data...")
if not any(kwargs.values()): # If no flags are provided
print("[INFO] No flags provided. The conversion will not happen as the output would be identical to the input.")
return data['vertex'].data
else:
return converter.to_3dgs()
elif source_format == "cc" and target_format == "cc":
debug_print("[DEBUG] Applying operations on CC data...")
converted_data = converter.to_cc()
if isinstance(converted_data, np.ndarray):
return converted_data
else:
return data['vertex'].data
else:
raise ValueError("Unsupported conversion")
def init_worker():
signal.signal(signal.SIGINT, signal.SIG_IGN)
def debug_print(message):
if DEBUG:
print(message)
def main():
parser = argparse.ArgumentParser(description="Convert between standard 3D Gaussian Splat and Cloud Compare formats.")
# Arguments for input and output
parser.add_argument("--input", "-i", required=True, help="Path to the source point cloud file.")
parser.add_argument("--output", "-o", required=True, help="Path to save the converted point cloud file.")
parser.add_argument("--target_format", "-f", choices=["3dgs", "cc"], required=True, help="Target point cloud format.")
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug prints.")
# Other flags
parser.add_argument("--bbox", nargs=6, type=float, metavar=('minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'), help="Specify the 3D bounding box to crop the point cloud.")
parser.add_argument("--rgb", action="store_true", help="Add RGB values to the output file based on f_dc values (only applicable when converting to Cloud Compare format).")
parser.add_argument("--density_filter", action="store_true", help="Filter the points to keep only regions with higher point density.")
parser.add_argument("--remove_flyers", action="store_true", help="Remove flyer points that are distant from the main cloud.")
args = parser.parse_args()
global DEBUG
DEBUG = args.debug
if os.path.exists(args.output):
user_response = input(f"File {args.output} already exists. Do you want to overwrite it? (y/N): ").lower()
if user_response != 'y':
print("Operation aborted by the user.")
return
# Detect the format of the input file
source_format = Utility.text_based_detect_format(args.input)
if not source_format:
print("The provided file is not a recognized 3D Gaussian Splat point cloud format.")
return
print(f"Detected source format: {source_format}")
# Check if --rgb flag is set for conversions involving 3dgs as target
if args.target_format == "3dgs" and args.rgb:
if source_format == "3dgs":
print("Error: --rgb flag is not applicable for 3dgs to 3dgs conversion.")
return
else:
print("Error: --rgb flag is not applicable for cc to 3dgs conversion.")
return
# Check for RGB flag and format conditions
if source_format == "cc" and args.target_format == "cc" and args.rgb:
if 'red' in PlyData.read(args.input)['vertex']._property_lookup:
print("Error: Source CC file already contains RGB data. Conversion stopped.")
return
# Read the data from the input file
data = PlyData.read(args.input)
# Print the number of vertices in the header
print(f"Number of vertices in the header: {len(data['vertex'].data)}")
try:
with Pool(initializer=init_worker) as pool:
# If the bbox argument is provided, extract its values
bbox_values = args.bbox if args.bbox else None
# Call the convert function and pass the bbox values (if provided)
converted_data = convert(data, source_format, args.target_format, process_rgb=args.rgb, density_filter=args.density_filter, remove_flyers=args.remove_flyers, bbox=bbox_values, pool=pool)
except KeyboardInterrupt:
print("Caught KeyboardInterrupt, terminating workers")
pool.terminate()
pool.join()
sys.exit(-1)
# Check if the conversion actually happened and save the result
if isinstance(converted_data, np.ndarray):
# Check and append ".ply" extension if absent
if not args.output.lower().endswith('.ply'):
args.output += '.ply'
# Save the converted data to the output file
PlyData([PlyElement.describe(converted_data, 'vertex')], byte_order='=').write(args.output)
print(f"Conversion completed and saved to {args.output}.")
else:
print("Conversion was skipped.")
if __name__ == "__main__":
main()