1 /******************************************************************************
2 * Copyright (c) 2011, Howard Butler, hobu.inc@gmail.com
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following
8 * conditions are met:
9 *
10 * * Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided
15 * with the distribution.
16 * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the
17 * names of its contributors may be used to endorse or promote
18 * products derived from this software without specific prior
19 * written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
32 * OF SUCH DAMAGE.
33 ****************************************************************************/
34
35 /*
36
37 Airborne Topographic Mapper (ATM) Project
38 NASA, Goddard Space Flight Center, Wallops Flight Facility
39 Principal Investigator: Bill Krabill (William.B.Krabill@nasa.gov)
40
41 Description of ATM QFIT Output Data
42 (revised 2009-feb-13 sm)
43
44 ATM data is generally distributed in the output format of the
45 processing program, qfit, which combines airborne laser ranging
46 data and aircraft attitude from the INS with positioning
47 information from a processed kinematic differential GPS
48 trajectory. Qfit output files, which usually have names ending
49 in a .qi extension, are organized as 32-bit (4-byte)
50 binary words, equivalent to a C or IDL long integer, which are
51 scaled to retain the precision of the measurements. The format
52 and the scaling factors are presented below. The qfit program is
53 run on an Apple PowerPC processor (and formerly Sun/Motorola).
54 Accordingly, the output is written in a big-endian format and
55 must be byte-swapped to be read with a PC (Intel processor)
56 which uses a little-endian format to store 32-bit integers.
57
58 The files are organized into fixed-length logical records. The
59 beginning of the file contains a header of one or more records
60 followed by a data segment, in which there is one record per
61 laser shot. It is not necessary to interpret the header
62 to use the laser data->
63
64 The first word of the header (and the file) is a 32-bit binary
65 integer giving the number of bytes in each logical record. Commonly
66 qfit files have 12 words per record and this integer will be the
67 number 48. The remainder of the initial logical record is padded
68 with blank bytes (in this case 44 blank bytes). 10-word and 14-word
69 formats have also been used, as described below.
70
71 The remainder of the header is generally a series of logical
72 records containing the processing history of the file. In these
73 logical records, the initial word contains a 32-bit binary
74 integer with a value between -9000000 and -9000008. The
75 remaining bytes in each header record is filled with a string of
76 ascii characters containing information on file processing
77 history. In this case, the byte offset (as a longword integer)
78 from the start of file to the start of laser data will be the
79 second word of the second record of the header. (Note: The header
80 records can be removed by eliminating records that begin with a
81 negative value since the first word of records in the data segment
82 is always a positive number.)
83
84 In the data segment of the file, the information contained in
85 words 1-11 of the output record pertains to the laser pulse, its
86 footprint, and aircraft attitude. The last word of each record is
87 always the GPS time of day when the laser measurement was acquired.
88
89 Prior to 2008 surveys, the GPS trajectory was edited to restrict PDOP<9
90 in order to limit GPS errors to be less than roughly 5cm. The output
91 survey data would therefore have occasional gaps where the PDOP>9.
92 Some applications of ATM data have less stringent accuracy requirements
93 that would be better served by preserving the data in these gaps.
94 Starting in 2008, the PDOP limit was changed to 20, which could allow
95 occasional GPS errors up to about 15cm. The PDOP value is carried in
96 the qfit output and can be used to edit data for applications requiring
97 greater precision. Any file in the 10-word format, or files in the 12-word
98 format processed prior to January 2009, will have PDOP limited
99 <9.
100
101 The three data formats are described below. The format is designated by
102 the logical record length given in the first word of the data file.
103
104 The qi 12-word format (in use since 2006):
105 Word # Content
106 1 Relative Time (msec from start of data file)
107 2 Laser Spot Latitude (degrees X 1,000,000)
108 3 Laser Spot Longitude (degrees X 1,000,000)
109 4 Elevation (millimeters)
110 5 Start Pulse Signal Strength (relative)
111 6 Reflected Laser Signal Strength (relative)
112 7 Scan Azimuth (degrees X 1,000)
113 8 Pitch (degrees X 1,000)
114 9 Roll (degrees X 1,000)
115 10 GPS PDOP (dilution of precision) (X 10)
116 11 Laser received pulse width (digitizer samples)
117 12 GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
118
119 10-word format (used prior to 2006):
120 Word # Content
121 1 Relative Time (msec from start of data file)
122 2 Laser Spot Latitude (degrees X 1,000,000)
123 3 Laser Spot Longitude (degrees X 1,000,000)
124 4 Elevation (millimeters)
125 5 Start Pulse Signal Strength (relative)
126 6 Reflected Laser Signal Strength (relative)
127 7 Scan Azimuth (degrees X 1,000)
128 8 Pitch (degrees X 1,000)
129 9 Roll (degrees X 1,000)
130 10 GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
131
132
133 Between 1997 and 2004 some ATM surveys included
134 a separate sensor to measure passive brightness.
135 In the 14-word format, words 10-13 pertain to the
136 passive brightness signal, which is essentially a relative
137 measure of radiance reflected from the earth's surface within
138 the vicinity of the laser pulse. The horizontal position of the
139 passive footprint is determined relative to the laser footprint
140 by a delay formulated during ground testing at Wallops. The
141 elevation of the footprint is synthesized from surrounding laser
142 elevation data-> NOTE: The passive data is not calibrated and
143 its use, if any, should be qualitative in nature. It may aid
144 the interpretation of terrain features. The measurement capability
145 was engineered into the ATM sensors to aid in the identification
146 of the water/beach interface acquired with the instrument in
147 coastal mapping applications.
148
149 14-word format:
150 Word # Content
151 1 Relative Time (msec from start of data file)
152 2 Laser Spot Latitude (degrees X 1,000,000)
153 3 Laser Spot Longitude (degrees X 1,000,000)
154 4 Elevation (millimeters)
155 5 Start Pulse Signal Strength (relative)
156 6 Reflected Laser Signal Strength (relative)
157 7 Scan Azimuth (degrees X 1,000)
158 8 Pitch (degrees X 1,000)
159 9 Roll (degrees X 1,000)
160 10 Passive Signal (relative)
161 11 Passive Footprint Latitude (degrees X 1,000,000)
162 12 Passive Footprint Longitude (degrees X 1,000,000)
163 13 Passive Footprint Synthesized Elevation (millimeters)
164 14 GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
165 */
166
167 #include "QfitReader.hpp"
168
169 #include <pdal/PointView.hpp>
170 #include <pdal/util/Extractor.hpp>
171 #include <pdal/util/portable_endian.hpp>
172 #include <pdal/util/ProgramArgs.hpp>
173
174 #include <algorithm>
175 #include <map>
176
177 #pragma warning(disable: 4127) // conditional expression is constant
178
179 namespace pdal
180 {
181
182 static StaticPluginInfo const s_info
183 {
184 "readers.qfit",
185 "QFIT Reader",
186 "http://pdal.io/stages/readers.qfit.html",
187 { "qi" }
188 };
189
CREATE_STATIC_STAGE(QfitReader,s_info)190 CREATE_STATIC_STAGE(QfitReader, s_info)
191
192 std::string QfitReader::getName() const { return s_info.name; }
193
QfitReader()194 QfitReader::QfitReader()
195 : pdal::Reader()
196 , m_format(QFIT_Format_Unknown)
197 , m_size(0)
198 , m_littleEndian(false)
199 , m_istream()
200 {}
201
202
initialize()203 void QfitReader::initialize()
204 {
205 ISwitchableStream str(m_filename);
206 if (!str)
207 throwError("Unable to open file '" + m_filename + "'");
208 str.seek(0);
209
210 int32_t int4(0);
211
212 str >> int4;
213
214 // They started writting little-endian data->
215
216 /* For years we produced ATM data in big-endian format. With changes in
217 computer hardware, we reluctantly changed our standard output to
218 little-endian. The transition occurred between the two 2010 campaigns. The
219 format of the binary ( .qi, for example) files can be identified by
220 examining the first four bytes of the file. Read as a long integer (one
221 4-byte word), the value will contain the record length (in bytes) of the
222 data records. For example, a .qi file containing 14 words per record will
223 have a value 56 (=4*14). If the format of the file matches the format of
224 your processor, the value will be reasonable without byte-swapping. If the
225 format of the file differs from your processor, then the value is
226 interpreted as some very large number, unless you swap the byte order. If
227 you use Intel or equivalent processors, then you had to byte-swap files
228 from spring 2010 and earlier, you do not swap the ones from fall 2010 and
229 later. */
230
231 // If the size comes back something other than 4*no_dimensions, we assume
232 // The data were flipped
233 if (int4 < 100)
234 {
235 m_littleEndian = true;
236 }
237 else
238 {
239 str.switchToBigEndian();
240 }
241
242 if (!m_littleEndian)
243 int4 = int32_t(be32toh(uint32_t(int4)));
244
245 if (int4 % 4 != 0)
246 throwError("Base QFIT format is not a multiple of 4, "
247 "unrecognized format!");
248
249 m_size = int4;
250 m_format = static_cast<QFIT_Format_Type>(m_size / sizeof(m_size));
251
252 // The offset to start reading point data should be here.
253 str.seek(m_size + sizeof(int4));
254
255 str >> int4;
256 m_offset = static_cast<std::size_t>(int4);
257
258 // Seek to the end
259 str.seek(0, std::istream::end);
260 std::ios::pos_type end = str.position();
261
262 // First integer is the format of the file
263 std::ios::off_type offset = static_cast<std::ios::off_type>(m_offset);
264 m_point_bytes = end - offset;
265 }
266
267
addArgs(ProgramArgs & args)268 void QfitReader::addArgs(ProgramArgs& args)
269 {
270 args.add("flip_coordinates", "Flip coordinates from 0-360 to -180-180",
271 m_flip_x);
272 args.add("scale_z", "Z scale. Use 0.001 to go from mm to m",
273 m_scale_z, 0.001);
274 }
275
276
addDimensions(PointLayoutPtr layout)277 void QfitReader::addDimensions(PointLayoutPtr layout)
278 {
279 using namespace Dimension;
280
281 m_size = 0;
282 layout->registerDim(Id::OffsetTime);
283 layout->registerDim(Id::Y);
284 layout->registerDim(Id::X);
285 layout->registerDim(Id::Z);
286 layout->registerDim(Id::StartPulse);
287 layout->registerDim(Id::ReflectedPulse);
288 layout->registerDim(Id::Azimuth);
289 layout->registerDim(Id::Pitch);
290 layout->registerDim(Id::Roll);
291 m_size += 36;
292
293 if (m_format == QFIT_Format_12)
294 {
295 layout->registerDim(Id::Pdop);
296 layout->registerDim(Id::PulseWidth);
297 m_size += 8;
298 }
299 else if (m_format == QFIT_Format_14)
300 {
301 layout->registerDim(Id::PassiveSignal);
302 layout->registerDim(Id::PassiveY);
303 layout->registerDim(Id::PassiveX);
304 layout->registerDim(Id::PassiveZ);
305 m_size += 16;
306 }
307 m_size += 4; // For the GPS time that we currently discard.
308 }
309
310
ready(PointTableRef)311 void QfitReader::ready(PointTableRef)
312 {
313 m_numPoints = m_point_bytes / m_size;
314 if (m_point_bytes % m_size)
315 throwError("Error calculating file point count. File size is "
316 "inconsistent with point size.");
317 m_index = 0;
318 m_istream.reset(new IStream(m_filename));
319 m_istream->seek(m_offset);
320 }
321
322
read(PointViewPtr data,point_count_t count)323 point_count_t QfitReader::read(PointViewPtr data, point_count_t count)
324 {
325 if (!m_istream->good())
326 throwError("Corrupted file/file read error.");
327 if (m_istream->stream()->eof())
328 throwError("End of file detected.");
329
330 count = (std::min)(m_numPoints - m_index, count);
331 std::vector<char> buf(m_size);
332 PointId nextId = data->size();
333 point_count_t numRead = 0;
334 while (count--)
335 {
336 m_istream->get(buf);
337 SwitchableExtractor extractor(buf.data(), m_size, m_littleEndian);
338
339 // always read the base fields
340 {
341 int32_t time, y, xi, z, start_pulse, reflected_pulse, scan_angle,
342 pitch, roll;
343 extractor >> time >> y >> xi >> z >> start_pulse >>
344 reflected_pulse >> scan_angle >> pitch >> roll;
345 double x = xi / 1000000.0;
346 if (m_flip_x && x > 180)
347 x -= 360;
348
349 data->setField(Dimension::Id::OffsetTime, nextId, time);
350 data->setField(Dimension::Id::Y, nextId, y / 1000000.0);
351 data->setField(Dimension::Id::X, nextId, x);
352 data->setField(Dimension::Id::Z, nextId, z * m_scale_z);
353 data->setField(Dimension::Id::StartPulse, nextId, start_pulse);
354 data->setField(Dimension::Id::ReflectedPulse, nextId,
355 reflected_pulse);
356 data->setField(Dimension::Id::Azimuth, nextId,
357 scan_angle / 1000.0);
358 data->setField(Dimension::Id::Pitch, nextId, pitch / 1000.0);
359 data->setField(Dimension::Id::Roll, nextId, roll / 1000.0);
360 }
361
362 if (m_format == QFIT_Format_12)
363 {
364 int32_t pdop, pulse_width;
365 extractor >> pdop >> pulse_width;
366 data->setField(Dimension::Id::Pdop, nextId, pdop / 10.0);
367 data->setField(Dimension::Id::PulseWidth, nextId, pulse_width);
368 }
369 else if (m_format == QFIT_Format_14)
370 {
371 int32_t passive_signal, passive_y, passive_x, passive_z;
372 extractor >> passive_signal >> passive_y >> passive_x >> passive_z;
373 double x = passive_x / 1000000.0;
374 if (m_flip_x && x > 180)
375 x -= 360;
376 data->setField(Dimension::Id::PassiveSignal, nextId, passive_signal);
377 data->setField(Dimension::Id::PassiveY, nextId,
378 passive_y / 1000000.0);
379 data->setField(Dimension::Id::PassiveX, nextId, x);
380 data->setField(Dimension::Id::PassiveZ, nextId,
381 passive_z * m_scale_z);
382 }
383 // GPS time is really a GPS offset from the start of the GPS day
384 // encoded in this odd way: 153320100 = 15 hours 33 minutes
385 // 20 seconds 100 milliseconds.
386 // Not sure why we have that AND the other offset time. For now
387 // we'll just extract this time and drop it.
388 int32_t gpstime;
389 extractor >> gpstime;
390
391 if (m_cb)
392 m_cb(*data, nextId);
393
394 numRead++;
395 nextId++;
396 }
397 m_index += numRead;
398
399 return numRead;
400 }
401
402
done(PointTableRef)403 void QfitReader::done(PointTableRef)
404 {
405 m_istream.reset();
406 }
407
408 } // namespace pdal
409