pyASDReader 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyASDReader/__init__.py +41 -0
- pyASDReader/_version.py +12 -0
- pyASDReader/asd_file_reader.py +940 -0
- pyASDReader/constant.py +138 -0
- pyASDReader/file_attributes.py +113 -0
- pyASDReader/logger_setup.py +35 -0
- pyasdreader-1.2.0.dist-info/METADATA +497 -0
- pyasdreader-1.2.0.dist-info/RECORD +11 -0
- pyasdreader-1.2.0.dist-info/WHEEL +5 -0
- pyasdreader-1.2.0.dist-info/licenses/LICENSE +21 -0
- pyasdreader-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# !/usr/bin/env python
|
|
4
|
+
# -*- encoding: utf-8 -*-
|
|
5
|
+
'''
|
|
6
|
+
@File : ASD_File_Reader.py
|
|
7
|
+
@Time : 2024/11/19 03:52:34
|
|
8
|
+
@Author : Kai Cao
|
|
9
|
+
@Version : 1.0.0
|
|
10
|
+
@Contact : caokai_cgs@163.com
|
|
11
|
+
@License : (C)Copyright 2024-
|
|
12
|
+
Copyright Statement: Full Copyright
|
|
13
|
+
@Desc : According to "ASD File Format version 8: Revision B"
|
|
14
|
+
'''
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import struct
|
|
18
|
+
import re
|
|
19
|
+
import logging
|
|
20
|
+
import numpy as np
|
|
21
|
+
import xml.etree.ElementTree as ET
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from collections import namedtuple
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from .constant import (FileVersion_e, InstrumentType_e, InstrumentModel_e, SpectraType_e, SignatureState_e, AuditLogType_e, DataType_e, DataFormat_e, IT_ms_e, CalibrationType_e, SaturationError_e, ClassiferDataType_e)
|
|
26
|
+
from .logger_setup import setup_logging
|
|
27
|
+
from .file_attributes import FileAttributes
|
|
28
|
+
|
|
29
|
+
# Initialize module-level logger
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
class ASDFile(object):
|
|
33
|
+
|
|
34
|
+
DEFAULT_DERIVATIVE_GAP = 5
|
|
35
|
+
|
|
36
|
+
def __init__(self, filepath: str = None):
|
|
37
|
+
"""Initialize ASDFile instance.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
filepath: Optional path to ASD file. If provided, file will be read automatically.
|
|
41
|
+
"""
|
|
42
|
+
self.asdFileVersion = 0
|
|
43
|
+
self.metadata = None
|
|
44
|
+
self.spectrumData = None
|
|
45
|
+
self.referenceFileHeader = None
|
|
46
|
+
self.referenceData = None
|
|
47
|
+
self.classifierData = None
|
|
48
|
+
self.dependants = None
|
|
49
|
+
self.calibrationHeader = None
|
|
50
|
+
self.calibrationSeriesABS = None
|
|
51
|
+
self.calibrationSeriesBSE = None
|
|
52
|
+
self.calibrationSeriesLMP = None
|
|
53
|
+
self.calibrationSeriesFO = None
|
|
54
|
+
self.auditLog = None
|
|
55
|
+
self.signature = None
|
|
56
|
+
self.__asdFileStream = None
|
|
57
|
+
self.wavelengths = None
|
|
58
|
+
|
|
59
|
+
# Auto-read file if filepath is provided
|
|
60
|
+
if filepath is not None:
|
|
61
|
+
self.read(filepath)
|
|
62
|
+
|
|
63
|
+
def read(self: object, filePath: str) -> bool:
|
|
64
|
+
readSuccess = False
|
|
65
|
+
|
|
66
|
+
# Check if filePath is valid
|
|
67
|
+
if filePath is None or not isinstance(filePath, (str, bytes, os.PathLike)):
|
|
68
|
+
logger.error(f"Invalid file path: {filePath}")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Check if file exists
|
|
72
|
+
if not (os.path.exists(filePath) and os.path.isfile(filePath)):
|
|
73
|
+
logger.error(f"File does not exist or is not a file: {filePath}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# read in file to memory(buffer)
|
|
78
|
+
with open(filePath, 'rb') as fileHandle:
|
|
79
|
+
self.__asdFileStream = fileHandle.read()
|
|
80
|
+
if self.__asdFileStream[-3:] == b'\xFF\xFE\xFD':
|
|
81
|
+
self.__bom = self.__asdFileStream[-3:]
|
|
82
|
+
self.__asdFileStream = self.__asdFileStream[:-3]
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.exception(f"Error in reading the file.\nError: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# refering C# Line 884 to identify the file version
|
|
88
|
+
self.asdFileVersion, offset = self.__validate_fileVersion()
|
|
89
|
+
|
|
90
|
+
# Check if file version is valid
|
|
91
|
+
if self.asdFileVersion.value <= 0:
|
|
92
|
+
logger.error(f"Invalid ASD file version")
|
|
93
|
+
return False
|
|
94
|
+
if self.asdFileVersion.value > 0:
|
|
95
|
+
try:
|
|
96
|
+
offset = self.__parse_metadata(offset)
|
|
97
|
+
self.wavelengths = np.arange(self.metadata.channel1Wavelength, self.metadata.channel1Wavelength + self.metadata.channels * self.metadata.wavelengthStep, self.metadata.wavelengthStep)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.exception(f"Error in parsing the metadata.\nError: {e}")
|
|
100
|
+
else:
|
|
101
|
+
try:
|
|
102
|
+
offset = self.__parse_spectrumData(offset)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.exception(f"Error in parsing the metadata and spectrum data.\nError: {e}")
|
|
105
|
+
if self.asdFileVersion.value >= 2:
|
|
106
|
+
try:
|
|
107
|
+
offset = self.__parse_referenceFileHeader(offset)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.exception(f"Error in parsing the reference file header.\nError: {e}")
|
|
110
|
+
else:
|
|
111
|
+
try:
|
|
112
|
+
offset = self.__parse_referenceData(offset)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.exception(f"Error in parsing the reference data.\nError: {e}")
|
|
115
|
+
if self.asdFileVersion.value >= 6:
|
|
116
|
+
try:
|
|
117
|
+
# Read Classifier Data
|
|
118
|
+
offset = self.__parse_classifierData(offset)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.exception(f"Error in parsing the classifier data.\nError: {e}")
|
|
121
|
+
else:
|
|
122
|
+
try:
|
|
123
|
+
offset = self.__parse_dependentVariables(offset)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.exception(f"Error in parsing the depndant variables.\nError: {e}")
|
|
126
|
+
if self.asdFileVersion.value >= 7:
|
|
127
|
+
try:
|
|
128
|
+
# Read Calibration Header
|
|
129
|
+
offset = self.__parse_calibrationHeader(offset)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.exception(f"Error in parsing the calibration header.\nError: {e}")
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
if self.calibrationHeader and (self.calibrationHeader.calibrationNum > 0):
|
|
135
|
+
# Parsing the calibration data according to 'ASD File Format version 8: Revision B', through the suquence of 'Absolute Calibration Data', 'Base Calibration Data', 'Lamp Calibration Data', 'Fiber Optic Data' successively.
|
|
136
|
+
for hdr in self.calibrationHeader.calibrationSeries: # Number of calibrationSeries buffers in the file.
|
|
137
|
+
if hdr[0] == CalibrationType_e.cb_ABSOLUTE:
|
|
138
|
+
self.calibrationSeriesABS, _, _, offset = self.__parse_spectra(offset)
|
|
139
|
+
elif hdr[0] == CalibrationType_e.cb_BASE:
|
|
140
|
+
self.calibrationSeriesBSE, _, _, offset = self.__parse_spectra(offset)
|
|
141
|
+
elif hdr[0] == CalibrationType_e.cb_LAMP:
|
|
142
|
+
self.calibrationSeriesLMP, _, _, offset = self.__parse_spectra(offset)
|
|
143
|
+
elif hdr[0] == CalibrationType_e.cb_FIBER:
|
|
144
|
+
self.calibrationSeriesFO, _, _, offset = self.__parse_spectra(offset)
|
|
145
|
+
# else:
|
|
146
|
+
# logger.info(f"Calibration data is not available.")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.exception(f"Error in parsing the calibration data.\nError: {e}")
|
|
149
|
+
if self.asdFileVersion.value >= 8:
|
|
150
|
+
try:
|
|
151
|
+
# Read Audit Log
|
|
152
|
+
offset = self.__parse_auditLog(offset)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.exception(f"Error in parsing the audit log.\nError: {e}")
|
|
155
|
+
# Read Signature
|
|
156
|
+
else:
|
|
157
|
+
try:
|
|
158
|
+
offset = self.__parse_signature(offset)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.exception(f"Error in parsing the signature.\nError: {e}")
|
|
161
|
+
readSuccess = True
|
|
162
|
+
return readSuccess
|
|
163
|
+
|
|
164
|
+
def update(self, field_name: str, new_value):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def write(self: object, file: str) -> bool:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def __check_offset(func):
|
|
171
|
+
def wrapper(self: object, offset: int = None, *args, **kwargs):
|
|
172
|
+
# Check if offset is None or out of range
|
|
173
|
+
# TODO: add 0 <= offset < len(self.__asdFileStream) check
|
|
174
|
+
if isinstance(offset, int) and 0 <= offset:
|
|
175
|
+
if offset < len(self.__asdFileStream):
|
|
176
|
+
return func(self, offset, *args, **kwargs)
|
|
177
|
+
else:
|
|
178
|
+
logger.info("Reached the end of the binary byte stream. offset: {offset}")
|
|
179
|
+
return None, None
|
|
180
|
+
else:
|
|
181
|
+
logger.error(f"Invalid offset: {offset}. It should be a non-negative integer.")
|
|
182
|
+
return None, None
|
|
183
|
+
return wrapper
|
|
184
|
+
|
|
185
|
+
@__check_offset
|
|
186
|
+
def __parse_metadata(self: object, offset) -> int:
|
|
187
|
+
|
|
188
|
+
asdMetadataFormat = '<157s 18s B B b b l b l f f b b b b b H 128s 56s L h h H H f f f f h b 4b H H H b L H H H H f f 27s 5b'
|
|
189
|
+
asdMetadatainfo = namedtuple('metadata', "asdFileVersion comments when_datetime daylighSavingsFlag programVersion fileVersion iTime \
|
|
190
|
+
darkCorrected darkTime dataType referenceTime channel1Wavelength wavelengthStep dataFormat \
|
|
191
|
+
old_darkCurrentCount old_refCount old_sampleCount application channels appData gpsData \
|
|
192
|
+
intergrationTime_ms fo darkCurrentCorrention calibrationSeries instrumentNum yMin yMax xMin xMax \
|
|
193
|
+
ipNumBits xMode flags1 flags2 flags3 flags4 darkCurrentCount refCount sampleCount instrument \
|
|
194
|
+
calBulbID swir1Gain swir2Gain swir1Offset swir2Offset splice1_wavelength splice2_wavelength smartDetectorType \
|
|
195
|
+
spare1 spare2 spare3 spare4 spare5 byteStream byteStreamLength")
|
|
196
|
+
try:
|
|
197
|
+
comments, when, programVersion, fileVersion, iTime, darkCorrected, darkTime, \
|
|
198
|
+
dataType, referenceTime, channel1Wavelength, wavelengthStep, dataFormat, \
|
|
199
|
+
old_darkCurrentCount, old_refCount, old_sampleCount, \
|
|
200
|
+
application, channels, appData, gpsData, intergrationTime_ms, fo, darkCurrentCorrention, \
|
|
201
|
+
calibrationSeries, instrumentNum, yMin, yMax, xMin, xMax, ipNumBits, xMode, \
|
|
202
|
+
flags1, flags2, flags3, flags4, darkCurrentCount, refCount, \
|
|
203
|
+
sampleCount, instrument, calBulbID, swir1Gain, swir2Gain, swir1Offset, swir2Offset, \
|
|
204
|
+
splice1_wavelength, splice2_wavelength, smartDetectorType, \
|
|
205
|
+
spare1, spare2, spare3, spare4, spare5 = struct.unpack_from(asdMetadataFormat, self.__asdFileStream, offset)
|
|
206
|
+
asdFileVersion, _ = self.__validate_fileVersion()
|
|
207
|
+
comments = comments.strip(b'\x00') # remove null bytes
|
|
208
|
+
# Parse the time from the buffer, format is year, month, day, hour, minute, second
|
|
209
|
+
when_datetime, daylighSavingsFlag = self.__parse_ASDFilewhen((struct.unpack_from('9h', when))) # 9 short integers
|
|
210
|
+
programVersion = self.__parseVersion(programVersion)
|
|
211
|
+
fileVersion = self.__parseVersion(fileVersion)
|
|
212
|
+
darkCorrected = bool(darkCorrected)
|
|
213
|
+
darkTime = datetime.fromtimestamp(darkTime)
|
|
214
|
+
dataType = DataType_e(dataType)
|
|
215
|
+
referenceTime = datetime.fromtimestamp(referenceTime)
|
|
216
|
+
dataFormat = DataFormat_e(dataFormat)
|
|
217
|
+
intergrationTime = IT_ms_e(intergrationTime_ms)
|
|
218
|
+
calibrationSeries = CalibrationType_e(calibrationSeries)
|
|
219
|
+
flags2 = self.__parseSaturationError(flags2)
|
|
220
|
+
instrument = InstrumentType_e(instrument)
|
|
221
|
+
ByteStream = self.__asdFileStream[:484]
|
|
222
|
+
ByteStreamLength = len(ByteStream)
|
|
223
|
+
offset += struct.calcsize(asdMetadataFormat)
|
|
224
|
+
self.metadata = asdMetadatainfo._make(
|
|
225
|
+
(asdFileVersion, comments, when_datetime, daylighSavingsFlag, programVersion, fileVersion, iTime, darkCorrected, darkTime, \
|
|
226
|
+
dataType, referenceTime, channel1Wavelength, wavelengthStep, dataFormat, old_darkCurrentCount, old_refCount, old_sampleCount, \
|
|
227
|
+
application, channels, appData, gpsData, intergrationTime, fo, darkCurrentCorrention, calibrationSeries, instrumentNum, \
|
|
228
|
+
yMin, yMax, xMin, xMax, ipNumBits, xMode, flags1, flags2, flags3, flags4, darkCurrentCount, refCount, \
|
|
229
|
+
sampleCount, instrument, calBulbID, swir1Gain, swir2Gain, swir1Offset, swir2Offset, \
|
|
230
|
+
splice1_wavelength, splice2_wavelength, smartDetectorType, \
|
|
231
|
+
spare1, spare2, spare3, spare4, spare5 , ByteStream, ByteStreamLength))
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.exception(f"Metadata (ASD File Header) parse error: {e}")
|
|
234
|
+
return None
|
|
235
|
+
# logger.info(f"Read: metadata end offset: {offset}")
|
|
236
|
+
return offset
|
|
237
|
+
|
|
238
|
+
@__check_offset
|
|
239
|
+
def __parse_spectrumData(self: object, offset: int) -> int:
|
|
240
|
+
try:
|
|
241
|
+
spectrumDataInfo = namedtuple('spectrumData', 'spectra byteStream byteStreamLength')
|
|
242
|
+
spectra, spectrumDataStream, spectrumDataStreamLength, offset = self.__parse_spectra(offset)
|
|
243
|
+
self.spectrumData = spectrumDataInfo._make((spectra, spectrumDataStream, spectrumDataStreamLength))
|
|
244
|
+
# logger.info(f"Read: spectrum data end offset: {offset}")
|
|
245
|
+
return offset
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.exception(f"Spectrum Data parse error: {e}")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
@__check_offset
|
|
251
|
+
def __parse_referenceFileHeader(self: object, offset: int) -> int:
|
|
252
|
+
initOffset = offset
|
|
253
|
+
asdReferenceFormat = 'd d'
|
|
254
|
+
asdreferenceFileHeaderInfo = namedtuple('referenceFileHeader', "referenceFlag referenceTime spectrumTime referenceDescription byteStream byteStreamLength")
|
|
255
|
+
try:
|
|
256
|
+
referenceFlag, offset = self.__parse_Bool(offset)
|
|
257
|
+
referenceTime_doublefloat, spectrumTime_doublefloat = struct.unpack_from(asdReferenceFormat, self.__asdFileStream, offset)
|
|
258
|
+
referenceTime_datetime = self.__parseTimeOLE(referenceTime_doublefloat) # Convert to datetime
|
|
259
|
+
spectrumTime_datetime = self.__parseTimeOLE(spectrumTime_doublefloat) # Convert to datetime
|
|
260
|
+
offset += struct.calcsize(asdReferenceFormat)
|
|
261
|
+
referenceDescription, offset = self.__parse_bstr(offset)
|
|
262
|
+
byteStream = self.__asdFileStream[initOffset:offset]
|
|
263
|
+
byteStreamLength = len(byteStream)
|
|
264
|
+
self.referenceFileHeader = asdreferenceFileHeaderInfo._make((referenceFlag, referenceTime_datetime, spectrumTime_datetime, referenceDescription, byteStream, byteStreamLength))
|
|
265
|
+
# logger.info(f"Read: reference file header end offset: {offset}")
|
|
266
|
+
return offset
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.exception(f"Reference File Header parse error: {e}")
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
@__check_offset
|
|
272
|
+
def __parse_referenceData(self: object, offset: int) -> int:
|
|
273
|
+
try:
|
|
274
|
+
referenceDataInfo = namedtuple('referenceData', 'spectra byteStream byteStreamLength')
|
|
275
|
+
spectra, referenceDataStream, referenceDataStreamLength, offset = self.__parse_spectra(offset)
|
|
276
|
+
self.referenceData = referenceDataInfo._make((spectra, referenceDataStream, referenceDataStreamLength))
|
|
277
|
+
# logger.info(f"Read: reference data end offset: {offset}")
|
|
278
|
+
return offset
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.exception(f"Reference Data parse error: {e}")
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@__check_offset
|
|
284
|
+
def __parse_classifierData(self: object, offset: int) -> int:
|
|
285
|
+
try:
|
|
286
|
+
initOffset = offset
|
|
287
|
+
yCode, yModelType = struct.unpack_from('bb', self.__asdFileStream, offset)
|
|
288
|
+
offset += struct.calcsize('bb')
|
|
289
|
+
title_str, offset = self.__parse_bstr(offset)
|
|
290
|
+
subtitle_str, offset = self.__parse_bstr(offset)
|
|
291
|
+
productName_str, offset = self.__parse_bstr(offset)
|
|
292
|
+
vendor_str, offset = self.__parse_bstr(offset)
|
|
293
|
+
lotNumber_str, offset = self.__parse_bstr(offset)
|
|
294
|
+
sample__str, offset = self.__parse_bstr(offset)
|
|
295
|
+
modelName_str, offset = self.__parse_bstr(offset)
|
|
296
|
+
operator_str, offset = self.__parse_bstr(offset)
|
|
297
|
+
dateTime_str, offset = self.__parse_bstr(offset)
|
|
298
|
+
instrument_str, offset = self.__parse_bstr(offset)
|
|
299
|
+
serialNumber_str, offset = self.__parse_bstr(offset)
|
|
300
|
+
displayMode_str, offset = self.__parse_bstr(offset)
|
|
301
|
+
comments_str, offset = self.__parse_bstr(offset)
|
|
302
|
+
units_str, offset = self.__parse_bstr(offset)
|
|
303
|
+
filename_str, offset = self.__parse_bstr(offset)
|
|
304
|
+
username_str, offset = self.__parse_bstr(offset)
|
|
305
|
+
reserved1_str, offset = self.__parse_bstr(offset)
|
|
306
|
+
reserved2_str, offset = self.__parse_bstr(offset)
|
|
307
|
+
reserved3_str, offset = self.__parse_bstr(offset)
|
|
308
|
+
reserved4_str, offset = self.__parse_bstr(offset)
|
|
309
|
+
constituantCount_int, = struct.unpack_from('H', self.__asdFileStream, offset)
|
|
310
|
+
offset += struct.calcsize('H')
|
|
311
|
+
asdClassifierDataInfo = namedtuple('classifierData', 'yCode yModelType title subtitle productName vendor lotNumber sample modelName operator dateTime instrument serialNumber displayMode comments units filename username reserved1 reserved2 reserved3 reserved4 constituantCount constituantItems byteStream byteStreamLength')
|
|
312
|
+
# Past the constituants
|
|
313
|
+
if constituantCount_int > 0:
|
|
314
|
+
offset += 10
|
|
315
|
+
# logger.info(f"constituant items ")
|
|
316
|
+
constituantItems = []
|
|
317
|
+
for i in range(constituantCount_int):
|
|
318
|
+
# logger.info(f"constituant items sequence: {i}")
|
|
319
|
+
item, offset = self.__parse_constituantType(offset)
|
|
320
|
+
constituantItems.append(item)
|
|
321
|
+
if constituantCount_int == 0:
|
|
322
|
+
constituantItems = []
|
|
323
|
+
offset += 2
|
|
324
|
+
byteStream = self.__asdFileStream[initOffset:offset]
|
|
325
|
+
byteStreamLength = len(byteStream)
|
|
326
|
+
self.classifierData = asdClassifierDataInfo._make((yCode, yModelType, title_str, subtitle_str, productName_str, vendor_str, lotNumber_str, sample__str, modelName_str, operator_str, dateTime_str, instrument_str, serialNumber_str, displayMode_str, comments_str, units_str, filename_str, username_str, reserved1_str, reserved2_str, reserved3_str, reserved4_str, constituantCount_int, constituantItems, byteStream, byteStreamLength))
|
|
327
|
+
# logger.info(f"Read: classifier Data end offset: {offset}")
|
|
328
|
+
return offset
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.exception(f"classifier Data parse error: {e}")
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
@__check_offset
|
|
334
|
+
def __parse_dependentVariables(self: object, offset: int) -> int:
|
|
335
|
+
try:
|
|
336
|
+
initOffset = offset
|
|
337
|
+
dependantInfo = namedtuple('dependants', 'saveDependentVariables dependentVariableCount dependentVariableLabels dependentVariableValue byteStream byteStreamLength')
|
|
338
|
+
saveDependentVariables, offset = self.__parse_Bool(offset)
|
|
339
|
+
dependant_format = 'h'
|
|
340
|
+
dependentVariableCount, = struct.unpack_from(dependant_format, self.__asdFileStream, offset)
|
|
341
|
+
offset += struct.calcsize(dependant_format)
|
|
342
|
+
if dependentVariableCount > 0:
|
|
343
|
+
offset += 10
|
|
344
|
+
dependantVariableLabels_list = []
|
|
345
|
+
for i in range(dependentVariableCount):
|
|
346
|
+
dependentVariableLabel, offset = self.__parse_bstr(offset)
|
|
347
|
+
dependantVariableLabels_list.append(dependentVariableLabel)
|
|
348
|
+
offset += 10
|
|
349
|
+
dependantVariableValues_list = []
|
|
350
|
+
for i in range(dependentVariableCount):
|
|
351
|
+
dependentVariableValue, = struct.unpack_from('<f', self.__asdFileStream, offset)
|
|
352
|
+
dependantVariableValues_list.append(dependentVariableValue)
|
|
353
|
+
offset += struct.calcsize('<f')
|
|
354
|
+
self.dependants = dependantInfo._make((saveDependentVariables, dependentVariableCount, dependantVariableLabels_list, dependantVariableValues_list, self.__asdFileStream[initOffset:offset], len(self.__asdFileStream[initOffset:offset])))
|
|
355
|
+
# if there are no dependent variables, skip 4 bytes (corresponding to 4 empty byte positions b'\x00')
|
|
356
|
+
if dependentVariableCount == 0:
|
|
357
|
+
offset += 4
|
|
358
|
+
dependantVariableLabels_list = []
|
|
359
|
+
dependantVariableValues_list = []
|
|
360
|
+
self.dependants = dependantInfo._make((saveDependentVariables, dependentVariableCount, dependantVariableLabels_list, dependantVariableValues_list, self.__asdFileStream[initOffset:offset], len(self.__asdFileStream[initOffset:offset])))
|
|
361
|
+
# logger.info(f"Read: dependant variables end offset: {offset}")
|
|
362
|
+
return offset
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.exception(f"Dependant variables parse error: {e}")
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
@__check_offset
|
|
368
|
+
def __parse_calibrationHeader(self: object, offset: int) -> int:
|
|
369
|
+
try:
|
|
370
|
+
calibrationHeaderCountNum_format = 'b'
|
|
371
|
+
calibrationSeries_buffer_format = '<b 20s i h h'
|
|
372
|
+
calibrationHeaderInfo = namedtuple('calibrationHeader', 'calibrationNum calibrationSeries, byteStream byteStreamLength')
|
|
373
|
+
calibrationHeaderCount, = struct.unpack_from(calibrationHeaderCountNum_format, self.__asdFileStream, offset)
|
|
374
|
+
byteStream = self.__asdFileStream[offset:offset + struct.calcsize(calibrationHeaderCountNum_format) + struct.calcsize(calibrationSeries_buffer_format)*calibrationHeaderCount]
|
|
375
|
+
byteStreamLength = len(byteStream)
|
|
376
|
+
offset += struct.calcsize(calibrationHeaderCountNum_format)
|
|
377
|
+
if calibrationHeaderCount > 0:
|
|
378
|
+
calibrationSeries = []
|
|
379
|
+
for i in range(calibrationHeaderCount):
|
|
380
|
+
(cbtype, cbname, cbIntergrationTime_ms, cbSwir1Gain, cbWwir2Gain) = struct.unpack_from(calibrationSeries_buffer_format, self.__asdFileStream, offset)
|
|
381
|
+
cbtype_e = CalibrationType_e(cbtype)
|
|
382
|
+
name = cbname.strip(b'\x00')
|
|
383
|
+
cbIntergrationTime = IT_ms_e(cbIntergrationTime_ms)
|
|
384
|
+
calibrationSeries.append(((cbtype_e, name, cbIntergrationTime, cbSwir1Gain, cbWwir2Gain)))
|
|
385
|
+
offset += struct.calcsize(calibrationSeries_buffer_format)
|
|
386
|
+
self.calibrationHeader = calibrationHeaderInfo._make((calibrationHeaderCount, calibrationSeries, byteStream, byteStreamLength))
|
|
387
|
+
else:
|
|
388
|
+
calibrationSeries = []
|
|
389
|
+
self.calibrationHeader = calibrationHeaderInfo._make((calibrationHeaderCount, calibrationSeries, byteStream, byteStreamLength))
|
|
390
|
+
# logger.info(f"Read: calibration header end offset: {offset}")
|
|
391
|
+
return offset
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.exception(f"Calibration Header parse error: {e}")
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
@__check_offset
|
|
397
|
+
def __parse_auditLog(self: object, offset: int) -> int:
|
|
398
|
+
try:
|
|
399
|
+
initOffset = offset
|
|
400
|
+
auditLogInfo = namedtuple('auditLog', 'auditCount auditEvents byteStream byteStreamLength')
|
|
401
|
+
additCount, = struct.unpack_from('l', self.__asdFileStream, offset)
|
|
402
|
+
offset += struct.calcsize('l')
|
|
403
|
+
if additCount > 0:
|
|
404
|
+
offset += 10
|
|
405
|
+
auditEvents, auditEventsLength = self.__parse_auditEvents(offset)
|
|
406
|
+
offset += auditEventsLength
|
|
407
|
+
self.auditLog = auditLogInfo._make((additCount, auditEvents, self.__asdFileStream[initOffset:offset], len(self.__asdFileStream[initOffset:offset])))
|
|
408
|
+
# logger.info(f"Read: audit log header end offset: {offset}")
|
|
409
|
+
return offset
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.exception(f"Audit Log Header parse error: {e}")
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
@__check_offset
|
|
415
|
+
def __parse_signature(self: object, offset: int) -> int:
|
|
416
|
+
try:
|
|
417
|
+
initOffset = offset
|
|
418
|
+
signatureInfo = namedtuple('signature', 'signed, signatureTime, userDomain, userLogin, userName, source, reason, notes, publicKey, signature, byteStream, byteStreamLength')
|
|
419
|
+
signed_int, = struct.unpack_from('b', self.__asdFileStream, offset)
|
|
420
|
+
# 0 – Unsigned
|
|
421
|
+
# 1 - Signed
|
|
422
|
+
signed_map = {0: SignatureState_e.UN_SIGNED, 1: SignatureState_e.SIGNED}
|
|
423
|
+
if signed_int not in signed_map:
|
|
424
|
+
signed = SignatureState_e.SIGNED_INVALID
|
|
425
|
+
# set the file version based on the version string
|
|
426
|
+
else:
|
|
427
|
+
signed = signed_map[signed_int]
|
|
428
|
+
offset += struct.calcsize('b')
|
|
429
|
+
signatureTime_int, = struct.unpack_from('q', self.__asdFileStream, offset)
|
|
430
|
+
#! The timestamp is to be parsed
|
|
431
|
+
signatureTime = signatureTime_int
|
|
432
|
+
# start_date = datetime(1, 1, 1)
|
|
433
|
+
# signatureTime = start_date + timedelta(seconds=signatureTime_int // 10000000)
|
|
434
|
+
# signatureTime = datetime.fromtimestamp(signatureTime_int, tz=) # Convert to datetime
|
|
435
|
+
offset += struct.calcsize('q')
|
|
436
|
+
userDomain, offset = self.__parse_bstr(offset)
|
|
437
|
+
userLogin, offset = self.__parse_bstr(offset)
|
|
438
|
+
userName, offset = self.__parse_bstr(offset)
|
|
439
|
+
source, offset = self.__parse_bstr(offset)
|
|
440
|
+
reason, offset = self.__parse_bstr(offset)
|
|
441
|
+
notes, offset = self.__parse_bstr(offset)
|
|
442
|
+
publicKey, offset = self.__parse_bstr(offset)
|
|
443
|
+
# signature, offset = self.__parse_bstr(offset)
|
|
444
|
+
signature, = struct.unpack_from('128s', self.__asdFileStream, offset)
|
|
445
|
+
offset += struct.calcsize('128s')
|
|
446
|
+
byteStream = self.__asdFileStream[initOffset:offset]
|
|
447
|
+
byteStreamLength = len(byteStream)
|
|
448
|
+
self.signature = signatureInfo._make((signed, signatureTime, userDomain, userLogin, userName, source, reason, notes, publicKey, signature, byteStream, byteStreamLength))
|
|
449
|
+
# logger.info(f"Read: signature end offset: {offset}")
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.exception(f"Signature parse error: {e}")
|
|
452
|
+
return None
|
|
453
|
+
return offset
|
|
454
|
+
|
|
455
|
+
@__check_offset
|
|
456
|
+
def __parse_spectra(self: object, offset: int) -> tuple[np.array, bytes, int, int]:
|
|
457
|
+
try:
|
|
458
|
+
spectra = np.array(struct.unpack_from('<{}d'.format(self.metadata.channels), self.__asdFileStream, offset))
|
|
459
|
+
offset += (self.metadata.channels * 8)
|
|
460
|
+
spectrumDataStream = self.__asdFileStream[offset:offset + self.metadata.channels * 8]
|
|
461
|
+
spectrumDataStreamLength = len(spectrumDataStream)
|
|
462
|
+
return spectra, spectrumDataStream, spectrumDataStreamLength, offset
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.exception(f"Spectrum data parse error: {e}")
|
|
465
|
+
return None, None, None, None
|
|
466
|
+
|
|
467
|
+
@__check_offset
|
|
468
|
+
def __parse_constituantType(self: object, offset: int) -> tuple[tuple, int]:
|
|
469
|
+
try:
|
|
470
|
+
constituentName, offset = self.__parse_bstr(offset)
|
|
471
|
+
passFail, offset = self.__parse_bstr(offset)
|
|
472
|
+
fmt = '<d d d d d d d d d l d d'
|
|
473
|
+
mDistance, mDistanceLimit, concentration, concentrationLimit, fRatio, residual, residualLimit, scores, scoresLimit, modelType, reserved1, reserved2 = struct.unpack_from(fmt, self.__asdFileStream, offset)
|
|
474
|
+
merterialReportInfo = namedtuple('itemsInMeterialReport', 'constituentName passFail mDistance mDistanceLimit concentration concentrationLimit fRatio residual residualLimit scores scoresLimit modelType reserved1 reserved2')
|
|
475
|
+
itemsInMeterialReport = merterialReportInfo._make((constituentName, passFail, mDistance, mDistanceLimit, concentration, concentrationLimit, fRatio, residual, residualLimit, scores, scoresLimit, modelType, reserved1, reserved2))
|
|
476
|
+
offset += struct.calcsize(fmt)
|
|
477
|
+
# logger.info(f"Read: constituant type end offset: {offset}")
|
|
478
|
+
return itemsInMeterialReport, offset
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.exception(f"Constituant Type parse error {e}")
|
|
481
|
+
return None, None
|
|
482
|
+
|
|
483
|
+
@__check_offset
|
|
484
|
+
def __parse_bstr(self: object, offset: int) -> tuple[str, int]:
|
|
485
|
+
try:
|
|
486
|
+
size, = struct.unpack_from('<h', self.__asdFileStream, offset)
|
|
487
|
+
offset += struct.calcsize('<h')
|
|
488
|
+
bstr_format = '<{}s'.format(size)
|
|
489
|
+
str = ''
|
|
490
|
+
if size >= 0:
|
|
491
|
+
bstr, = struct.unpack_from(bstr_format, self.__asdFileStream, offset)
|
|
492
|
+
str = bstr.decode('utf-8')
|
|
493
|
+
offset += struct.calcsize(bstr_format)
|
|
494
|
+
return str, offset
|
|
495
|
+
except struct.error as err:
|
|
496
|
+
logger.exception(f"Byte string parse error: {err}")
|
|
497
|
+
return None, None
|
|
498
|
+
|
|
499
|
+
@__check_offset
|
|
500
|
+
def __parse_Bool(self: object, offset: int) -> tuple[bool, int]:
|
|
501
|
+
try:
|
|
502
|
+
buffer = self.__asdFileStream[offset:offset + 2]
|
|
503
|
+
if buffer == b'\xFF\xFF':
|
|
504
|
+
return True, offset + 2
|
|
505
|
+
elif buffer == b'\x00\x00':
|
|
506
|
+
return False, offset + 2
|
|
507
|
+
else:
|
|
508
|
+
raise ValueError("Invalid Boolean value")
|
|
509
|
+
except Exception as e:
|
|
510
|
+
return None, None
|
|
511
|
+
|
|
512
|
+
@__check_offset
|
|
513
|
+
def __parse_auditEvents(self: object, offset: int) -> tuple[list, int]:
|
|
514
|
+
try:
|
|
515
|
+
auditEvents_str = self.__asdFileStream[offset:].decode('utf-8', errors='ignore')
|
|
516
|
+
auditPattern = re.compile(r'<Audit_Event>(.*?)</Audit_Event>', re.DOTALL)
|
|
517
|
+
auditEvents = auditPattern.findall(auditEvents_str)
|
|
518
|
+
auditEvents_list = []
|
|
519
|
+
auditEventLength = 0
|
|
520
|
+
for auditEvent in auditEvents:
|
|
521
|
+
auditEvent = "<Audit_Event>" + auditEvent + "</Audit_Event>"
|
|
522
|
+
auditEventLength += len(auditEvent.encode('utf-8')) + 2
|
|
523
|
+
auditEvents_list.append(auditEvent)
|
|
524
|
+
auditEventsTuple_list = []
|
|
525
|
+
for auditEvent in auditEvents_list:
|
|
526
|
+
auditEventtuple = self.__parse_auditLogEvent(auditEvent)
|
|
527
|
+
auditEventsTuple_list.append(auditEventtuple)
|
|
528
|
+
return auditEventsTuple_list, auditEventLength
|
|
529
|
+
except Exception as e:
|
|
530
|
+
logger.exception(f"Audit Event parse error: {e}")
|
|
531
|
+
return None, None
|
|
532
|
+
|
|
533
|
+
def __parse_auditLogEvent(self: object, event: str) -> tuple:
|
|
534
|
+
try:
|
|
535
|
+
auditInfo = namedtuple('event', 'application appVersion name login time source function notes')
|
|
536
|
+
# Security note: xml.etree.ElementTree in Python 3.8+ has XXE protection by default
|
|
537
|
+
# External entities and DTD processing are disabled automatically
|
|
538
|
+
root = ET.fromstring(event)
|
|
539
|
+
application = root.find('Audit_Application').text
|
|
540
|
+
appVersion = root.find('Audit_AppVersion').text
|
|
541
|
+
name = root.find('Audit_Name').text
|
|
542
|
+
login = root.find('Audit_Login').text
|
|
543
|
+
time = root.find('Audit_Time').text
|
|
544
|
+
source = root.find('Audit_Source').text
|
|
545
|
+
function = root.find('Audit_Function').text
|
|
546
|
+
notes = root.find('Audit_Notes').text
|
|
547
|
+
auditEvents = auditInfo._make((application, appVersion, name, login, time, source, function, notes))
|
|
548
|
+
return auditEvents
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.exception(f"Audit Log Data parse error: {e}")
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
def __validate_fileVersion(self: object) -> int:
|
|
554
|
+
try:
|
|
555
|
+
# read the file version from the first 3 bytes of the file
|
|
556
|
+
version_data = self.__asdFileStream[:3]
|
|
557
|
+
version_map = {b'ASD': FileVersion_e.FILE_VERSION_1, b'as2': FileVersion_e.FILE_VERSION_2, b'as3': FileVersion_e.FILE_VERSION_3, b'as4': FileVersion_e.FILE_VERSION_4, b'as5': FileVersion_e.FILE_VERSION_5, b'as6': FileVersion_e.FILE_VERSION_6, b'as7': FileVersion_e.FILE_VERSION_7, b'as8': FileVersion_e.FILE_VERSION_8}
|
|
558
|
+
if version_data not in version_map:
|
|
559
|
+
fileversion = FileVersion_e.FILE_VERSION_INVALID
|
|
560
|
+
# set the file version based on the version string
|
|
561
|
+
else:
|
|
562
|
+
fileversion = version_map[version_data]
|
|
563
|
+
# logger.info(f"File Version: {fileversion}")
|
|
564
|
+
return fileversion, 3
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.exception(f"File Version Validation Error:\n{e}")
|
|
567
|
+
return FileVersion_e.FILE_VERSION_INVALID, 3
|
|
568
|
+
|
|
569
|
+
def __parseVersion(self, version: int) -> str:
|
|
570
|
+
major = (version & 0xF0) >> 4
|
|
571
|
+
minor = version & 0x0F
|
|
572
|
+
return f"{major}.{minor}"
|
|
573
|
+
|
|
574
|
+
# Parse the storage time through 9 short integers and store it as a datetime type
|
|
575
|
+
def __parse_ASDFilewhen(self: object, when: bytes) -> tuple:
|
|
576
|
+
seconds = when[0] # seconds [0,61]
|
|
577
|
+
minutes = when[1] # minutes [0,59]
|
|
578
|
+
hour = when[2] # hour [0,23]
|
|
579
|
+
day = when[3] # day of the month [1,31]
|
|
580
|
+
month = when[4] # month of year [0,11]
|
|
581
|
+
year = when[5] # years since 1900
|
|
582
|
+
weekDay = when[6] # day of week [0,6] (Sunday = 0)
|
|
583
|
+
daysInYear = when[7] # day of year [0,365]
|
|
584
|
+
daylighSavingsFlag = when[8] # daylight savings flag
|
|
585
|
+
if year < 1900:
|
|
586
|
+
year = year + 1900
|
|
587
|
+
date_datetime = datetime(year, month + 1, day, hour, minutes, seconds)
|
|
588
|
+
return date_datetime, daylighSavingsFlag
|
|
589
|
+
|
|
590
|
+
def __parse_gps(self: object, gps_field: bytes) -> tuple:
|
|
591
|
+
# Domumentation: ASD File Format Version 8, page 4
|
|
592
|
+
gpsDataInfo = namedtuple('gpsdata', 'trueHeading, speed, latitude, longitude, altitude, lock, hardwareMode, ss, mm, hh, flags1, flags2, satellites, filler1, filler2')
|
|
593
|
+
try:
|
|
594
|
+
gpsDatadFormat = '<d d d d d h b b b b b h 5s b b'
|
|
595
|
+
trueHeading, speed, latitude, longitude, altitude, lock, hardwareMode, ss, mm, hh, flags1, flags2, satellites, filler1, filler2 = struct.unpack(gpsDatadFormat, gps_field)
|
|
596
|
+
gpsData = gpsDataInfo._make((trueHeading, speed, latitude, longitude, altitude, lock, hardwareMode, ss, mm, hh, flags1, flags2, satellites, filler1, filler2))
|
|
597
|
+
return gpsData
|
|
598
|
+
except Exception as e:
|
|
599
|
+
logger.exception(f"GPS parse error: {e}")
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
def __parse_SmartDetector(self: object, smartDetectorData: bytes) -> tuple:
|
|
603
|
+
try:
|
|
604
|
+
smartDetectorFormat = '<i f f f h b f f'
|
|
605
|
+
smartDetectorInfo = namedtuple('smartDetector', 'serialNumber signal dark ref status avg humid temp')
|
|
606
|
+
serialNumber, signal, dark, ref, status, avg, humid, temp = struct.unpack(smartDetectorFormat, smartDetectorData)
|
|
607
|
+
smartDetector = smartDetectorInfo._make((serialNumber, signal, dark, ref, status, avg, humid, temp))
|
|
608
|
+
return smartDetector
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.exception(f"Smart Detector parse error: {e}")
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
def __parseSaturationError(self, flags2: int) -> list:
|
|
614
|
+
errors = []
|
|
615
|
+
if flags2 & 0x01:
|
|
616
|
+
errors.append(SaturationError_e.VNIR_SATURATION)
|
|
617
|
+
if flags2 & 0x02:
|
|
618
|
+
errors.append(SaturationError_e.SWIR1_SATURATION)
|
|
619
|
+
if flags2 & 0x04:
|
|
620
|
+
errors.append(SaturationError_e.SWIR2_SATURATION)
|
|
621
|
+
if flags2 & 0x08:
|
|
622
|
+
errors.append(SaturationError_e.SWIR1_TEC_ALARM)
|
|
623
|
+
if flags2 & 0x10: # Fixed: was 0x16 (22), should be 0x10 (16)
|
|
624
|
+
errors.append(SaturationError_e.SWIR2_TEC_ALARM)
|
|
625
|
+
return errors
|
|
626
|
+
|
|
627
|
+
def __parseTimeOLE(self: object, timeole: float) -> datetime:
|
|
628
|
+
try:
|
|
629
|
+
ole_base_date = datetime(1899, 12, 30)
|
|
630
|
+
days = int(timeole)
|
|
631
|
+
fraction = timeole - days
|
|
632
|
+
total_hours = fraction * 24
|
|
633
|
+
hours = int(total_hours)
|
|
634
|
+
minutes = int((total_hours - hours) * 60)
|
|
635
|
+
seconds = int(((total_hours - hours) * 60 - minutes) * 60)
|
|
636
|
+
microseconds = int((((total_hours - hours) * 60 - minutes) * 60 - seconds) * 1000000)
|
|
637
|
+
time_delta = timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds)
|
|
638
|
+
result_datetime = ole_base_date + time_delta
|
|
639
|
+
return result_datetime
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.exception(f"OLE time parse error: {e}")
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
#! Need to check the result of the function
|
|
645
|
+
@property
|
|
646
|
+
def digitalNumber(self):
|
|
647
|
+
return self.spectrumData.spectra if self.spectrumData is not None else None
|
|
648
|
+
|
|
649
|
+
@property
|
|
650
|
+
def whiteReference(self):
|
|
651
|
+
if self.referenceData is not None:
|
|
652
|
+
return self.__normalise_spectrum(self.referenceData.spectra)
|
|
653
|
+
else:
|
|
654
|
+
return None
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def reflectance(self):
|
|
658
|
+
if self.metadata.asdFileVersion.value >= 2:
|
|
659
|
+
try:
|
|
660
|
+
if self.metadata.referenceTime and self.metadata.dataType == DataType_e.dt_REF_TYPE:
|
|
661
|
+
reflectance = np.divide(self.__normalise_spectrum(self.spectrumData.spectra), self.__normalise_spectrum(self.referenceData.spectra), where=self.__normalise_spectrum(self.referenceData.spectra) != 0)
|
|
662
|
+
return reflectance
|
|
663
|
+
else:
|
|
664
|
+
return None
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.info(f"Reflectance calculation error: {e}")
|
|
667
|
+
return None
|
|
668
|
+
else:
|
|
669
|
+
logger.info("Reflectance calculation error: Unsupported file version")
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
@property
|
|
673
|
+
def reflectanceNoDeriv(self):
|
|
674
|
+
return self.reflectance
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def reflectance1stDeriv(self):
|
|
678
|
+
if self.reflectance is not None:
|
|
679
|
+
return self.__derivative(self.reflectance)
|
|
680
|
+
else:
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
@property
|
|
684
|
+
def reflectance2ndDeriv(self):
|
|
685
|
+
if self.reflectance1stDeriv is not None:
|
|
686
|
+
return self.__derivative(self.reflectance1stDeriv)
|
|
687
|
+
else:
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
#* Need to check the result of the function, not available in the SpecView
|
|
691
|
+
# 3rd Derivative
|
|
692
|
+
@property
|
|
693
|
+
def reflectance3rdDeriv(self):
|
|
694
|
+
if self.reflectance2ndDeriv is not None:
|
|
695
|
+
return self.__derivative(self.reflectance2ndDeriv)
|
|
696
|
+
else:
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
#! Need to check the result of the function
|
|
700
|
+
# Reflectance (Transmittance)
|
|
701
|
+
@property
|
|
702
|
+
def transmitance(self):
|
|
703
|
+
pass
|
|
704
|
+
|
|
705
|
+
def __normalise_spectrum(self: object, spectrum) -> np.array:
|
|
706
|
+
# normalise the spectrum data, for VNIR and SWIR1, SWIR2, the data is normalised based on the integration time and gain
|
|
707
|
+
if spectrum is not None:
|
|
708
|
+
spectra = np.array(spectrum)
|
|
709
|
+
splice1_index = int(self.metadata.splice1_wavelength)
|
|
710
|
+
splice2_index = int(self.metadata.splice2_wavelength)
|
|
711
|
+
spectra[:splice1_index] = spectra[:splice1_index] / self.metadata.intergrationTime_ms.value
|
|
712
|
+
#
|
|
713
|
+
spectra[splice1_index:splice2_index] = spectra[splice1_index:splice2_index] * self.metadata.swir1Gain / 2048
|
|
714
|
+
spectra[splice2_index:] = spectra[splice2_index:] * self.metadata.swir2Gain / 2048
|
|
715
|
+
return spectra
|
|
716
|
+
else:
|
|
717
|
+
return None
|
|
718
|
+
|
|
719
|
+
def __derivative(self, data: np.array) -> np.array:
|
|
720
|
+
derivative = np.zeros_like(data)
|
|
721
|
+
D1 = ASDFile.DEFAULT_DERIVATIVE_GAP // 2
|
|
722
|
+
D2 = ASDFile.DEFAULT_DERIVATIVE_GAP - 1
|
|
723
|
+
derivative[D1:-D1] = (data[D1*2:] - data[:-D1*2]) / D2
|
|
724
|
+
# for i in range(D1, len(data) - D1):
|
|
725
|
+
# derivative[i] = (data[i + D1] - data[i - D1]) / D2
|
|
726
|
+
return derivative
|
|
727
|
+
|
|
728
|
+
@property
|
|
729
|
+
def absoluteReflectance(self):
|
|
730
|
+
if self.calibrationSeriesABS is not None:
|
|
731
|
+
return np.multiply(self.reflectance, self.calibrationSeriesABS)
|
|
732
|
+
else:
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
@property
|
|
736
|
+
def log1R(self):
|
|
737
|
+
if self.reflectance is not None:
|
|
738
|
+
return np.log(1/self.reflectance)/np.log(10)
|
|
739
|
+
else:
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
#! Need to check the result of the function
|
|
743
|
+
@property
|
|
744
|
+
def log1T(self):
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
@property
|
|
748
|
+
def log1RNoDeriv(self):
|
|
749
|
+
if self.log1R is not None:
|
|
750
|
+
return self.log1R
|
|
751
|
+
else:
|
|
752
|
+
return None
|
|
753
|
+
|
|
754
|
+
@property
|
|
755
|
+
def log1R1stDeriv(self):
|
|
756
|
+
if self.log1R is not None:
|
|
757
|
+
return self.__derivative(self.log1R)
|
|
758
|
+
else:
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def log1R2ndDeriv(self):
|
|
763
|
+
if self.log1R1stDeriv is not None:
|
|
764
|
+
return self.__derivative(self.log1R1stDeriv)
|
|
765
|
+
else:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
#! Need to check the result of the function
|
|
770
|
+
@property
|
|
771
|
+
def radiance(self, pcc: bool = False):
|
|
772
|
+
if self.calibrationHeader is not None:
|
|
773
|
+
if self.calibrationHeader.calibrationNum >= 3 and (all(x is not None for x in [self.calibrationSeriesABS, self.calibrationSeriesLMP, self.calibrationSeriesBSE]) or all(x is not None for x in [self.calibrationSeriesBSE, self.calibrationSeriesLMP, self.calibrationSeriesFO])):
|
|
774
|
+
for i in range(self.calibrationHeader.calibrationNum):
|
|
775
|
+
if self.calibrationHeader.calibrationSeries[i][0] == CalibrationType_e.cb_FIBER:
|
|
776
|
+
responseCal_info = namedtuple('responseCal', 'cbIT cbS1Gain cbS2Gain')
|
|
777
|
+
cbIT, cbS1Gain, cbS2Gain = self.calibrationHeader.calibrationSeries[i][2:5]
|
|
778
|
+
responseCal = responseCal_info._make((cbIT, cbS1Gain, cbS2Gain))
|
|
779
|
+
if self.metadata.fo >= 180:
|
|
780
|
+
radiance = self.__calc_irradiance(responseCal)
|
|
781
|
+
else:
|
|
782
|
+
radiance = self.__calc_radiance(responseCal)
|
|
783
|
+
if pcc == True:
|
|
784
|
+
radiance = self.__parabolic_correction(radiance)
|
|
785
|
+
else:
|
|
786
|
+
logger.info("Radiance calculation error: Invalid calibration header data")
|
|
787
|
+
return None
|
|
788
|
+
return radiance
|
|
789
|
+
else:
|
|
790
|
+
logger.info("Radiance calculation error: Invalid calibration header data")
|
|
791
|
+
return None
|
|
792
|
+
|
|
793
|
+
def __calc_radiance(self, response_cal) -> np.ndarray:
|
|
794
|
+
radiance = self.__calc_irradiance(response_cal)
|
|
795
|
+
radiance *= (self.calibrationSeriesBSE / np.pi)
|
|
796
|
+
return radiance
|
|
797
|
+
|
|
798
|
+
def __calc_irradiance(self, responseCal) -> np.ndarray:
|
|
799
|
+
DEFAULT_GAIN = 2048.0
|
|
800
|
+
radiance = np.zeros(self.metadata.channels)
|
|
801
|
+
dVnirConstant = 0.0
|
|
802
|
+
dSwir1Constant = 0.0
|
|
803
|
+
dSwir2Constant = 0.0
|
|
804
|
+
dSplice1 = 0.0
|
|
805
|
+
dSplice2 = 0.0
|
|
806
|
+
# Determine the last wavelength
|
|
807
|
+
dLastWavelength = self.metadata.channel1Wavelength + (self.metadata.channels - 1) * self.metadata.wavelengthStep
|
|
808
|
+
# Set the Splice Points
|
|
809
|
+
instrument = self.metadata.instrument
|
|
810
|
+
if instrument in [InstrumentType_e.UNKNOWN_INSTRUMENT, InstrumentType_e.PSII_INSTRUMENT, InstrumentType_e.LSVNIR_INSTRUMENT, InstrumentType_e.FSVNIR_INSTRUMENT, InstrumentType_e.HAND_HELD_INSTRUMENT]:
|
|
811
|
+
dSplice1 = dLastWavelength
|
|
812
|
+
dSplice2 = dLastWavelength
|
|
813
|
+
elif instrument == InstrumentType_e.FSFR_INSTRUMENT:
|
|
814
|
+
dSplice1 = self.metadata.splice1_wavelength
|
|
815
|
+
dSplice2 = self.metadata.splice2_wavelength
|
|
816
|
+
elif instrument == InstrumentType_e.FSNIR_INSTRUMENT:
|
|
817
|
+
dSplice1 = self.metadata.channel1Wavelength
|
|
818
|
+
dSplice2 = self.metadata.splice2_wavelength
|
|
819
|
+
# Set the Starting Wavelength
|
|
820
|
+
dWavelength = self.metadata.channel1Wavelength
|
|
821
|
+
i = 0
|
|
822
|
+
if instrument != InstrumentType_e.FSNIR_INSTRUMENT:
|
|
823
|
+
# VNIR
|
|
824
|
+
dVnirConstant = float(responseCal.cbIT.value) / float(self.metadata.intergrationTime_ms.value)
|
|
825
|
+
while dWavelength <= dSplice1 and i < self.metadata.channels:
|
|
826
|
+
if self.calibrationSeriesFO[i] != 0:
|
|
827
|
+
radiance[i] = float(self.calibrationSeriesLMP[i]) * (float(self.spectrumData.spectra[i]) / float(self.calibrationSeriesFO[i])) * dVnirConstant
|
|
828
|
+
else:
|
|
829
|
+
radiance[i] = 0
|
|
830
|
+
i += 1
|
|
831
|
+
dWavelength += self.metadata.wavelengthStep
|
|
832
|
+
if instrument in [InstrumentType_e.FSFR_INSTRUMENT, InstrumentType_e.FSNIR_INSTRUMENT]:
|
|
833
|
+
# SWiR1
|
|
834
|
+
dSwir1Constant = ((DEFAULT_GAIN / responseCal.cbS1Gain) / (DEFAULT_GAIN / self.metadata.swir1Gain))
|
|
835
|
+
while dWavelength <= dSplice2 and i < self.metadata.channels:
|
|
836
|
+
if self.calibrationSeriesFO[i] != 0:
|
|
837
|
+
radiance[i] = self.calibrationSeriesLMP[i] * (self.spectrumData.spectra[i] / self.calibrationSeriesFO[i]) * dSwir1Constant
|
|
838
|
+
else:
|
|
839
|
+
radiance[i] = 0
|
|
840
|
+
i += 1
|
|
841
|
+
dWavelength += self.metadata.wavelengthStep
|
|
842
|
+
# SWiR2
|
|
843
|
+
dSwir2Constant = ((DEFAULT_GAIN / responseCal.cbS2Gain) / (DEFAULT_GAIN / self.metadata.swir2Gain))
|
|
844
|
+
while dWavelength <= dLastWavelength and i < self.metadata.channels:
|
|
845
|
+
if self.calibrationSeriesFO[i] != 0:
|
|
846
|
+
radiance[i] = self.calibrationSeriesLMP[i] * (self.spectrumData.spectra[i] / self.calibrationSeriesFO[i]) * dSwir2Constant
|
|
847
|
+
else:
|
|
848
|
+
radiance[i] = 0
|
|
849
|
+
i += 1
|
|
850
|
+
dWavelength += self.metadata.wavelengthStep
|
|
851
|
+
return radiance
|
|
852
|
+
|
|
853
|
+
#! Need to check the result of the function
|
|
854
|
+
def __parabolic_correction(self, radiance: np.ndarray) -> np.ndarray:
|
|
855
|
+
|
|
856
|
+
DEFAULT_GAP = 3
|
|
857
|
+
iAvgLen = 0
|
|
858
|
+
iIndex = 0
|
|
859
|
+
dPC = 0.0
|
|
860
|
+
|
|
861
|
+
iStartingWavelength = int(self.metadata.channel1_wavelength)
|
|
862
|
+
iEndingWavelength = int(self.metadata.channels - 1) + int(self.metadata.channel1_wavelength)
|
|
863
|
+
|
|
864
|
+
InstrumentType = self.get_instrument_type(iStartingWavelength, iEndingWavelength)
|
|
865
|
+
|
|
866
|
+
iSplice1 = int(self.metadata.splice1_wavelength)
|
|
867
|
+
iSplice2 = int(self.metadata.splice2_wavelength)
|
|
868
|
+
iVertex1 = 675
|
|
869
|
+
iVertex2 = 1975
|
|
870
|
+
|
|
871
|
+
if (((InstrumentType & InstrumentModel_e.itVnir.value) == InstrumentModel_e.itVnir.value) and
|
|
872
|
+
((InstrumentType & InstrumentModel_e.itSwir1.value) == InstrumentModel_e.itSwir1.value) and
|
|
873
|
+
((InstrumentType & InstrumentModel_e.itSwir2.value) == InstrumentModel_e.itSwir2.value)) or \
|
|
874
|
+
(((InstrumentType & InstrumentModel_e.itVnir.value) == InstrumentModel_e.itVnir.value) and
|
|
875
|
+
((InstrumentType & InstrumentModel_e.itSwir1.value) == InstrumentModel_e.itSwir1.value)) or \
|
|
876
|
+
(((InstrumentType & InstrumentModel_e.itSwir1.value) == InstrumentModel_e.itSwir1.value) and
|
|
877
|
+
((InstrumentType & InstrumentModel_e.itSwir2.value) == InstrumentModel_e.itSwir2.value)):
|
|
878
|
+
|
|
879
|
+
iAvgLen = iSplice1 - iVertex1
|
|
880
|
+
iIndex = iSplice1 - iStartingWavelength
|
|
881
|
+
# 计算 VNIR 或 SWIR1 的 pfactor
|
|
882
|
+
dPC = radiance[iIndex]
|
|
883
|
+
|
|
884
|
+
if dPC == 0:
|
|
885
|
+
dPC = 1
|
|
886
|
+
|
|
887
|
+
dPCFactor1 = (self.average(radiance, iIndex + 1, DEFAULT_GAP) - radiance[iIndex]) / (dPC * iAvgLen * iAvgLen)
|
|
888
|
+
|
|
889
|
+
nPoint = abs(iVertex1 - iStartingWavelength)
|
|
890
|
+
iWavelength = iVertex1
|
|
891
|
+
dE = len(radiance)
|
|
892
|
+
|
|
893
|
+
while iWavelength <= iSplice1:
|
|
894
|
+
if nPoint <= dE:
|
|
895
|
+
radiance[nPoint] *= (dPCFactor1 * (iWavelength - iVertex1) ** 2 + 1)
|
|
896
|
+
nPoint += 1
|
|
897
|
+
iWavelength += 1
|
|
898
|
+
|
|
899
|
+
if InstrumentType == InstrumentModel_e.itVnirSwir1Swir2.value:
|
|
900
|
+
# 计算 SWIR2 的 PC
|
|
901
|
+
iAvgLen = iSplice2 - iVertex2
|
|
902
|
+
|
|
903
|
+
iIndex = iSplice2 - iStartingWavelength
|
|
904
|
+
# 计算 SWIR2 的 pfactor
|
|
905
|
+
dPC = self.average(radiance, iIndex + 1, DEFAULT_GAP)
|
|
906
|
+
|
|
907
|
+
if dPC == 0:
|
|
908
|
+
dPC = 1
|
|
909
|
+
|
|
910
|
+
dPCFactor2 = (self.average(radiance, iIndex - 2, DEFAULT_GAP) -
|
|
911
|
+
self.average(radiance, iIndex + 1, DEFAULT_GAP)) / (dPC * iAvgLen * iAvgLen)
|
|
912
|
+
|
|
913
|
+
nPoint = (iSplice2 - iStartingWavelength) + 1
|
|
914
|
+
iWavelength = iSplice2 + 1
|
|
915
|
+
|
|
916
|
+
while iWavelength <= iVertex2:
|
|
917
|
+
radiance[nPoint] *= (dPCFactor2 * (iWavelength - iVertex2) ** 2 + 1)
|
|
918
|
+
nPoint += 1
|
|
919
|
+
iWavelength += 1
|
|
920
|
+
|
|
921
|
+
return radiance
|
|
922
|
+
|
|
923
|
+
# TODO: Implement the following functions
|
|
924
|
+
# Radiometric Calculation
|
|
925
|
+
# Parabolic Correction
|
|
926
|
+
# Splice Correction
|
|
927
|
+
# Lambda Integration
|
|
928
|
+
# Quantum lntensity
|
|
929
|
+
# Interpolate
|
|
930
|
+
# Statistics
|
|
931
|
+
# NEDL
|
|
932
|
+
# ASCll Export
|
|
933
|
+
# Import Ascii X,Y
|
|
934
|
+
# JCAMP-DX Export
|
|
935
|
+
# Bran+Luebbe
|
|
936
|
+
# Colorimetry..
|
|
937
|
+
# GPS Log
|
|
938
|
+
# Convex Hull
|
|
939
|
+
# Custom...
|
|
940
|
+
|