3

I've been working for a while now to combine LAS 1.2 formatted LiDAR data with a matching SBET file to fill in the LASF_X/Y/Z fields in the LAS file. The matching of LiDAR points to SBET trajectory points is done by matching the second of week time format.

The desired output format for these LASF_X/Y/Z fields is WGS84 latitude, longitude, and elevation to be clear. I realize it's a weird choice. It's a requirement for a client. The problem I'm having is that it just doesn't display anything reasonable at all regardless of what I output.

I used LocalCartesian centered on each SBET point and called Reverse to calculate the new WGS84 point resulting from the translation of the SBET point by the LiDAR points converted directly from the angular and distance data and the device orientation. I get good WGS84 points as output but I'm getting results which just don't look right when plotted using basic lasview or Quick Terrain.

I'll post more detail if it helps. But I'm mostly asking if this process works the way I believe it should.

The header of my LAS 1.2 file output:

0000000 414c 4653 0000 0000 0000 0000 0000 0000
0000010 0000 0000 0000 0000 0201 4547 2d4f 4433
0000020 535f 5359 4554 004d 0000 0000 0000 0000
0000030 0000 0000 0000 0000 0000 3354 5f44 4143
0000040 5450 5255 0045 0000 0000 0000 0000 0000
0000050 0000 0000 0000 0000 0000 0047 07e4 00e3
0000060 00e3 0000 0000 0000 2203 8700 4c8a 8700
0000070 4c8a 0000 0000 0000 0000 0000 0000 0000
0000080 0000 3a00 308c 8ee2 4579 3a3e 308c 8ee2
0000090 4579 fc3e f1a9 4dd2 5062 003f 0000 0000
00000a0 0000 0000 0000 0000 0000 0000 0000 0000
00000b0 0000 9d00 392c 1417 3040 003f 48b8 0090
00000c0 8924 26bf 9dae fb07 519f a03f 33bf 014e
00000d0 7f38 fcbf f1a9 4dd2 63d2 d740 70a3 0a3d
00000e0 6073 00c0 0000 0000 0000 0000 0000 7600
00000f0 11b7 0001 0000 3e00 bc30 a7e1 eaa2 0040
0000100 0000 0000 0000 0000 0000 0000 a300 0000
0000110 c300 1194 0001 0000 5300 f671 a7e1 eaa2
0000120 0040 0000 0000 0000 0000 0000 0000 b800
0000130 0000 ce00 118a 0001 0000 6700 30b2 a7e2
  • LAS data should already be in X,Y,Z absolute though it's probably in a local projected coordinate system. Your SBET (smooth best estimate of trajectory) is from your dGPS and needs lever arm adjustment which should already be taken into account in the calibration file when the software converts raw pulses (SDC/SDF) into ASPRS LAS files. Can you view your LAS with LASView rapidlasso.com/lastools/lasview ? I think that's one of the free-ish tools from LASTools which I totally recommend purchasing if you're going to work with LiDAR. Can you include a HEX dump of the first 227 bytes. – Michael Stimson Mar 12 at 4:06
  • I've attempted to view the output with LASTools, i.e. lasview. It gives me a very odd view, as if the min and max for the spatial variables aren't correct. But they appear to be. I also tried using Quick Terrain. It pops up a small window which shows the whole which I can't get to look right in the main window. As for the LAS files, the only data we have is the raw input from the device in angular format. – Ryan Daly Mar 12 at 4:11
  • @MichaelStimson I added the hexdump to the original question. It wasn't formatting well in my comment. – Ryan Daly Mar 12 at 4:24
  • Are you little endian or big endian? I have 4C 41 53 46 00 00 11 00 ... 01 02 which is Intel byte order where yours looks Motorola byte order 41 4C 46 53 but that's an ASPRS LAS file that should already be in local coordinates. When converting to geographical coordinates the view can be quite odd: X and Y are in degrees but Z remains in feet or metres which isn't adequately defined in the header so the LAS files appear like vertical bars. There should be a setting in your viewer for your horizontal units to display that properly. – Michael Stimson Mar 12 at 4:42
  • LAS 1.2 is a bit old but here's a link to 1.3 asprs.org/a/society/committees/standards/LAS_1_4_r13.pdf so you can see the format. In the header there are x and y offset and scale parameters but the units are not defined; as of LAS 1.4 spatial reference is mandatory in WKT so the software can tell what units are being used. As the coordinates are stored as X, Y and Z the need for a SBET has expired. Your point data record format appears to be 3 with a size of 0x22 bytes which contains RGB values as byte. – Michael Stimson Mar 12 at 4:46
3

To access a LAS file in C# it is best to use unsafe code, a dynamic cast as the same block of bytes (fixed to ensure the memory manager doesn't shift the bytes separate to the struct) to the struct means that changes can be made and the same block of memory written back to the file as bytes it can be done:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
unsafe struct LASHeader_1p2
{
    public fixed Byte FileSig[4]; //= "LASF";    // 4
    public UInt16 FileSource;                    // 2   6
    public UInt16 Reserved_Unused;               // 2   8
    public UInt32 Project_ID_Data1;              // 4  12
    public UInt16 Project_ID_Data2;              // 2  14
    public UInt16 Project_ID_Data3;              // 2  16
    public fixed Byte Project_ID_Data4[8];       // 8  24
    public Byte Version_Major;                   // 1  25
    public Byte Version_Minor;                   // 1  26
    public fixed Byte System_ID[32];             //32  58
    public fixed Byte Software[32];              //32  90
    public UInt16 FC_Day;
    public UInt16 FC_Year;
    public UInt16 Header_Size;                   // 2,2,2 96
    public UInt32 Offset_to_Data;                // 4 100 0x60
    public UInt32 VarLenRecs;                    // 4 104 0x64
    public Byte Pt_DataFormat;                   // 1 105 0x65
    public UInt16 Pt_DataRecLen;                 // 2 107 0x68
    public UInt32 PointCount;                    // 4 111 0x6A
    public UInt32 Point_by_Return_0;             // 4 115
    public UInt32 Point_by_Return_1;             // 4 119
    public UInt32 Point_by_Return_2;             // 4 123
    public UInt32 Point_by_Return_3;             // 4 127
    public UInt32 Point_by_Return_4;             // 4 131
    public double Xscale;                        // 8 139
    public double Yscale;                        // 8 147
    public double Zscale;                        // 8 155
    public double Xoffset;                       // 8 163
    public double Yoffset;                       // 8 171
    public double Zoffset;                       // 8 179
    public double MaxX;                          // 8 187
    public double MinX;                          // 8 195
    public double MaxY;                          // 8 203
    public double MinY;                          // 8 211
    public double MaxZ;                          // 8 219
    public double MinZ;                          // 8 227
};

Is the LAS 1.2 header block. Ignore the comments, that's counting up the bytes to make sure I hadn't forgotten a field. Use fixed keyword to cast bytes to header:

byte[] InHdrReadBytes = new byte[227]; // size of header is 227 bytes
// Quite a few lines in here to find the LAS file, open as binary reader and read the bytes
fixed (byte* bInHdrPtr = &InHdrReadBytes[0]) // pin the memory in place
{
    LASHeader_1p2* OutHdr = (LASHeader_1p2*)bInHdrPtr;

Point records work the same:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct LASrec0
{
    public Int32 X; 
    public Int32 Y; 
    public Int32 Z; 
    public UInt16 Intensity; 
    public Byte DataByte; 
    public Byte Classification; 
    public byte ScanAngle; 
    public Byte UserData;  
    public UInt16 PointSourceID; 
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct LASrec2
{
    public Int32 X; 
    public Int32 Y; 
    public Int32 Z; 
    public UInt16 Intensity; 
    public Byte DataByte; 
    public Byte Classification; 
    public byte ScanAngle; 
    public Byte UserData;  
    public UInt16 PointSourceID; 
    public UInt16 Red;
    public UInt16 Green;
    public UInt16 Blue;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct LASrec3
{
    public Int32 X; 
    public Int32 Y; 
    public Int32 Z; 
    public UInt16 Intensity; 
    public Byte DataByte; 
    public Byte Classification; 
    public byte ScanAngle; 
    public Byte UserData;  
    public UInt16 PointSourceID; 
    public double GPS_Time;
    public UInt16 Red;
    public UInt16 Green;
    public UInt16 Blue;
};

Note that the Pack = 1 is absolutely vital as C# (and C++) will word align structs for speed which will mean your structure becomes disjoint as soon as a word follows a byte, in the header this occurs at 107 making anything that follows garbage if the struct isn't packed to the byte. Many MANY facepalm moments occurred before I found this out.

Although it is a bit concerning if this is the first foray into unsafe code I can assure you it works out quicker and easier than trying to byte[] cast with BitConverter.

| improve this answer | |
  • Thanks! This is very useful. It looks a little more complete than what I'm using right now. – Ryan Daly Mar 12 at 20:43
  • 1
    You're welcome. This is from code that I developed when liblas was in its infancy and LASTools wasn't widely known, it's straight out of the ASPRS LAS specification 1.2, which isn't easy to find online; LAS specification 1.3 and 1.4 are improvements on 1.2 so their spec will do in a pinch. The true gem here is Pack = 1 and the fixed syntax, as stated in the post many many facepalm moments occurred before I discovered these two essential requirements. – Michael Stimson Mar 12 at 23:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.