1+ #!/usr/bin/python
2+ # txt2las_wdp
3+ # copyright 2021 3DGeo Heidelberg
4+ # Script to transform a HELIOS++ xyz point cloud and fullwave file into las1.4 and external WDP file
5+ # Result can be viewed e.g. with CloudCompare (open file with "open"-> select "las 1.3 and las 1.4" as file type)
6+
7+ import struct
8+ import laspy
9+ import numpy as np
10+ from collections import OrderedDict
11+
12+ wfs = OrderedDict ()
13+ with open (r"..\output\Survey Playback\als_hd_demo\points\leg000_fullwave.txt" , 'r' ) as inf :
14+ for line in inf .readlines ():
15+ data = [float (x ) for x in line .split (" " )]
16+ wf_idx = int (data [0 ])
17+ tmin = data [7 ]
18+ tmax = data [8 ]
19+ fwf = np .abs (np .array (data [10 :]))
20+ fwf = fwf / np .max (fwf ) * 255.
21+ fwf = fwf .astype (int )
22+ dx , dy , dz = data [4 ], data [5 ], data [6 ]
23+ wfs [wf_idx ] = [tmin , tmax , dx , dy , dz , fwf ]
24+
25+ max_len = max ([len (x [- 1 ][- 1 ]) for x in wfs .items ()])
26+ wfs_lut = {idx : val for idx , val in enumerate (np .unique (wfs .keys ())[0 ])}
27+ wfs_lut_rev = {val : idx for idx , val in wfs_lut .items ()}
28+ pts = np .loadtxt (r"..\output\Survey Playback\als_hd_demo\points\leg000_points.xyz" , delimiter = " " )
29+
30+ las = laspy .create (file_version = "1.4" , point_format = 4 )
31+ xyz = pts [:, :3 ]
32+ las .header .offsets = np .min (xyz , axis = 0 )
33+ las .header .scales = [0.001 , 0.001 , 0.001 ]
34+ las .x = xyz [:, 0 ]
35+ las .y = xyz [:, 1 ]
36+ las .z = xyz [:, 2 ]
37+ las .intensity = pts [:, 3 ]
38+ las .wavepacket_index = np .ones ((xyz .shape [0 ])) # 1 refers to the value 100 (99+1) in the VLR below
39+ las .wavepacket_offset = 60 + np .array ([wfs_lut_rev [idx ] for idx in pts [:, 7 ]]) * (max_len )
40+ las .wavepacket_size = [max_len ] * xyz .shape [0 ] # always write a wavepacket of the longest size
41+ las .return_point_wave_location = [0 ] * xyz .shape [0 ] # adapt this if you need the information on which position this point
42+ # represents in the waveform
43+ las .x_t = [wfs [idx ][2 ] for idx in pts [:, 7 ]]
44+ las .y_t = [wfs [idx ][3 ] for idx in pts [:, 7 ]]
45+ las .z_t = [wfs [idx ][4 ] for idx in pts [:, 7 ]]
46+
47+ VLR_data = struct .pack ("<BBLLdd" ,
48+ 8 , # bits per sample
49+ 0 , # compression type
50+ max_len , # number of samples
51+ 10 , # temporal sample spacing (ps)
52+ 1 , # digitizer gain
53+ 0 , # digitizer offset
54+ )
55+ new_vlr = laspy .VLR (user_id = "LASF_Spec" , record_id = 100 , record_data = VLR_data )
56+ las .vlrs .append (new_vlr )
57+
58+ las .write ("test.las" )
59+
60+ wf_data = bytes () # WDP header (same as EVLR header)
61+ wf_data = b'' .join ([wf_data , struct .pack ("<H" , 0 )]) #reserved
62+ wf_data = b'' .join ([wf_data , struct .pack ("<s" , b"LASF_Spec " )]) #user id
63+ wf_data = b'' .join ([wf_data , struct .pack ("<H" , 65535 )]) #record id
64+ wf_data = b'' .join ([wf_data , struct .pack ("<Q" , max_len * len (wfs ))]) #record length after header
65+ wf_data = b'' .join ([wf_data , struct .pack ("<s" , b" " * 32 )]) #descripton
66+
67+ with open ("test.wdp" , 'wb' ) as f :
68+ f .write (wf_data )
69+
70+ for wf in wfs .items ():
71+ byt = np .zeros ((max_len ,), dtype = int )
72+ byt [:len (wf [1 ][- 1 ])] = [int (d ) for d in wf [1 ][- 1 ]]
73+ f .write (struct .pack ("<%dB" % max_len , * byt ))
74+
75+
76+ with open ("test.las" , 'r+b' ) as f :
77+ f .seek (6 )
78+ f .write (struct .pack ("<H" , int ('0000000000000100' , 2 ))) # set "external waveform" bit to 1 in las file (not possible with laspy)
0 commit comments