/// <summary> /// Read the LAS common header structure. /// NOTE: I don't want to compile /unsafe, so the only way to do this is by interpreting the byte stream directly. Normally I would used a fixed struct to do this. /// </summary> /// <param name="data"></param> /// <returns>A public header block strucuture which is filled in from the bytes in data[]</returns> protected LASPublicHeaderBlock ReadLASHeader(ref byte[] data) { LASPublicHeaderBlock header = new LASPublicHeaderBlock(); long pos = 0; //current position in file header.FileSignature = ReadString(ref pos, ref data, 4); if (header.FileSignature != "LASF") { Debug.Log("LidarLASReader.ReadLASHeader: Error reading header, FileSignature expected 'LASF' but found '" + header.FileSignature + "'. Load aborted."); return(header); } header.FileSourceID = ReadUnsignedShort(ref pos, ref data); header.GlobalEncoding = ReadUnsignedShort(ref pos, ref data); header.ProjectID1 = ReadUnsignedLong(ref pos, ref data); header.ProjectID2 = ReadUnsignedShort(ref pos, ref data); header.ProjectID3 = ReadUnsignedShort(ref pos, ref data); header.ProjectID4 = ReadString(ref pos, ref data, 8); header.VersionMajor = ReadByte(ref pos, ref data); header.VersionMinor = ReadByte(ref pos, ref data); header.SystemIdentifier = ReadString(ref pos, ref data, 32); header.GeneratingSoftware = ReadString(ref pos, ref data, 32); header.FileCreationDayofYear = ReadUnsignedShort(ref pos, ref data); header.FileCreationYear = ReadUnsignedShort(ref pos, ref data); header.HeaderSize = ReadUnsignedShort(ref pos, ref data); header.OffsetToPointData = ReadUnsignedLong(ref pos, ref data); header.NumberofVariableLengthRecords = ReadUnsignedLong(ref pos, ref data); header.PointDataRecordFormat = (byte)(ReadByte(ref pos, ref data) & 0x7f); //strip the top bit off as the unsigned char original type seems to set the MSB header.PointDataRecordLength = ReadUnsignedShort(ref pos, ref data); header.LegacyNumberofPointRecords = ReadUnsignedLong(ref pos, ref data); header.LegacyNumberofPointsByReturn = new UInt32[] { ReadUnsignedLong(ref pos, ref data), ReadUnsignedLong(ref pos, ref data), ReadUnsignedLong(ref pos, ref data), ReadUnsignedLong(ref pos, ref data), ReadUnsignedLong(ref pos, ref data) }; header.XScaleFactor = ReadDouble(ref pos, ref data); header.YScaleFactor = ReadDouble(ref pos, ref data); header.ZScaleFactor = ReadDouble(ref pos, ref data); header.XOffset = ReadDouble(ref pos, ref data); header.YOffset = ReadDouble(ref pos, ref data); header.ZOffset = ReadDouble(ref pos, ref data); header.MaxX = ReadDouble(ref pos, ref data); header.MinX = ReadDouble(ref pos, ref data); header.MaxY = ReadDouble(ref pos, ref data); header.MinY = ReadDouble(ref pos, ref data); header.MaxZ = ReadDouble(ref pos, ref data); header.MinZ = ReadDouble(ref pos, ref data); //TODO: these are only in the 1.4 spec files float version = header.VersionMajor + header.VersionMajor / 10; //so 1,4 become 1.4 if (version >= 1.4) { header.StartofWaveformDataPacketRecord = ReadUnsignedLongLong(ref pos, ref data); header.StartofFirstExtendedVariableLengthRecord = ReadUnsignedLong(ref pos, ref data); //See note above on long long values header.NumberofExtendedVariableLengthRecords = ReadUnsignedLong(ref pos, ref data); header.NumberofPointRecords = ReadUnsignedLongLong(ref pos, ref data); header.NumberofPointsByReturn = new UInt64[15]; for (int i = 0; i < 15; i++) { header.NumberofPointsByReturn[i] = ReadUnsignedLongLong(ref pos, ref data); } } else { //spec says if they're not used they should be zero header.StartofWaveformDataPacketRecord = 0; header.StartofFirstExtendedVariableLengthRecord = 0; header.NumberofExtendedVariableLengthRecords = 0; header.NumberofPointRecords = 0; header.NumberofPointsByReturn = new UInt64[15]; for (int i = 0; i < 15; i++) { header.NumberofPointsByReturn[i] = 0; } } return(header); }
/// <summary> /// Read in the point cloud data and store the points in the coverage data grid. /// For the moment, the maximum value for each grid square is recorded in order to make a height map later. This might need to be changed at a later date. /// TODO: store the (half a million) points as point data, rather than as a coverage. AND do something with the classification. /// NOTE: the data is an XY grid on the ground with Z as the height. Original projection is OSGB36. /// </summary> /// <param name="header"></param> /// <param name="data"></param> protected void ReadLASPoints(LASPublicHeaderBlock header, ref byte[] data) { //set up my coverage grid to put the data into - I'm making this square double width = header.MaxX - header.MinX; double height = header.MaxY - header.MinY; double maxDim = Math.Max(width, height); double gridspacing = 1.0f; //1.0 metre resolution grid int isize = (int)Math.Ceiling(maxDim / gridspacing); this.cellsize = (float)(maxDim / (float)isize); //this is the spacing on the XY axes to match the value in the height field (Z) CoverageData = new float[isize, isize]; Classification = new byte[isize, isize]; for (int y = 0; y < isize; y++) { for (int x = 0; x < isize; x++) { CoverageData[x, y] = 0; // -float.MaxValue; } } ulong NumPoints = Math.Max(header.LegacyNumberofPointRecords, header.NumberofPointRecords); //you have to use the correct one based on version long pos = header.OffsetToPointData; uint PDRFormat = header.PointDataRecordFormat; //0..10 designates which format of point data is being used - all points are the same format (preferred formats in LAS 1.4 are 6-10) for (ulong i = 0; i < NumPoints; i++) { LASPointDataRecordFormat P = null; switch (PDRFormat) { case 0: P = new LASPointDataRecordFormat0(); break; case 1: P = new LASPointDataRecordFormat1(); break; case 2: P = new LASPointDataRecordFormat2(); break; case 3: P = new LASPointDataRecordFormat3(); break; case 4: P = new LASPointDataRecordFormat4(); break; case 5: P = new LASPointDataRecordFormat5(); break; case 6: P = new LASPointDataRecordFormat6(); break; case 7: P = new LASPointDataRecordFormat7(); break; case 8: P = new LASPointDataRecordFormat8(); break; case 9: P = new LASPointDataRecordFormat9(); break; case 10: P = new LASPointDataRecordFormat10(); break; } P.FromData(ref pos, ref data); //apply scale and offset factor from header to point integer offset data to get final point double X = P.X * header.XScaleFactor + header.XOffset; double Y = P.Y * header.YScaleFactor + header.YOffset; double Z = P.Z * header.ZScaleFactor + header.ZOffset; //and do something with the data - TODO: classification might be interesting? int gx = (int)((X - header.MinX) / gridspacing); int gy = (int)((Y - header.MinY) / gridspacing); float val = (float)Z; if ((gx >= isize) || (gy >= isize) || (gx < 0) || (gy < 0)) { Debug.Log("LidarLASReader.ReadLASPoints: Error, index out of range, gx=" + gx + " gy=" + gy + " isize=" + isize); continue; //skip it } if (val > CoverageData[gx, gy]) { CoverageData[gx, gy] = val; //max filter point cloud data } //HACK! I want to see the classification data from this, so I've added the following line - NOTE: there might be conflicts with different classifications of points in the same grid Classification[gx, gy] = ((LASPointDataRecordFormat1)P).Classification; //HACK! might not be a classification point data record format! } }
/// <summary> /// Do the work... /// </summary> protected void ReadLASLidar() { byte[] bytes = File.ReadAllBytes(Filename); Header = ReadLASHeader(ref bytes); ReadLASPoints(Header, ref bytes); }