Data Processing

In order to process data with fluxEngine, a so-called processing context must be created. A processing context knows about:

  • How processing will be parallelized (it takes that information from the current handle) – changing parallelization settings will invalidate a given context

  • The instrument device that supplies the data that is being processed. This is currently either a PushBroom camera or a spectrometer.

    • It is also possible to process data loaded from files on disk.

  • What kind of processing should occur.

    • Should a preview of the data be generated?

    • Should the data be preprocessed so that it may be recorded to disk?

    • Should a fluxEngine model be used to process the data from the device?

  • Any white and dark reference data that should be used her.

Processing contexts are represented by the ProcessingContext class. Special static methods are available to create processing contexts for various different purposes.

After a context has been created data may be processed. The basic logic is the following:

  • Set the source (input) data of the context to a buffer from the instrument device via the correct overload of ProcessingContext.SetSourceData(). Note that this only sets a pointer in the processing context and the buffer must not be returned to the instrument device until processing has completed.

  • Call ProcessingContext.ProcessNext() to process the data supplied to the processing context.

  • Obtain the result of the processing via the ProcessingContext.OutputSinkData() method. This will return only a pointer to the processing result that is stored internally in the processing context. This is described in detail further down in Obtaining Results.

The following sections will describe different types of processing contexts that may be created for processing data from instrument devices.

Instrument Preview

Sometimes it is useful to obtain a preview image that may be shown to the user. The following device types will generate the following preview data:

  • PushBroom cameras: the preview data from a PushBroom camera will consist of data that is averaged over all wavelengths, so that only an intensity value is returned. The resulting tensor structure will always be (1, width, 1) for each buffer that is being processed.

  • Spectrometers: the preview data will be a single spectrum (tensor structure (bands)) that may be plotted.

  • HSI imager cameras: the preview data will be a single grayscale image that consists of the intensities averaged over all wavelengths. The tensor structure of the preview data will always be (height, width, 1) for each buffer that is being processed.

To create a processing context for preview data, one may use the ProcessingContext.CreateForInstrumentPreview() static method. There are two overloads for this method: one that just takes a InstrumentDevice that will create the processing context associated with the main processing queue set of the fluxEngine handle, and one that takes an additional reference to a ProcessingQueueSet.

To use the processing context to create the preview, one may call the overload of ProcessingContext.SetSourceData() that takes a Buffer argument. After the source data has been set, a call to ProcessingContext.ProcessNext() will perform the data processing.

The following example code will endlessly loop to create preview images from data that is acquired from a PushBroom camera:

 1 try
 2 {
 3     var ctx = LuxFlux.fluxEngineNET.ProcessingContext.CreateForInstrumentPreview(instrumentDevice);
 4
 5     var parameters = new LuxFlux.fluxEngineNET.InstrumentDevice.AcquisitionParameters();
 6     instrumentDevice.StartAcquisition(parameters);
 7     while (true)
 8     {
 9         LuxFlux.fluxEngineNET.Buffer buffer = primaryDevice.RetrieveBuffer(new TimeSpan(0, 0, 1));
10         if (buffer == null)
11             continue;
12
13         try
14         {
15             ctx.SetSourceData(buffer);
16             ctx.ProcessNext();
17             // See below for details on how the output sink logic
18             // works.
19             var outputData = ctx.OutputSinkData(0);
20             // Get a tensor view on the output data
21             var tensorData = outputData.AsTensorCopy();
22             Int64 width = tensorData.Dimensions[1];
23             for (Int64 x = 0; x < width; ++x) {
24                 float pixelValue = 0.0f;
25                 if (tensorData.DataType == LuxFlux.fluxEngineNET.DataType.Float32)
26                     pixelValue = tensorData.Value<float>(0, x, 0);
27                 else if (tensorData.DataType == LuxFlux.fluxEngineNET.DataType.Float64)
28                     pixelValue = (float)tensorData.Value<double>(0, x, 0);
29                 // (etc. handle the other cases)
30
31                 // Do something with the pixel value here
32             }
33         }
34         finally
35         {
36             instrumentDevice.ReturnBuffer(buffer);
37         }
38     }
39     instrumentDevice.StopAcquisition();
40 }
41 catch (Exception e)
42 {
43     Console.WriteLine($"Error: {e.Message}");
44 }

The context that has been created in this manner may be reused for multiple acquisitions. Note, however, that the strucure of the input data at the time of context creation determines what kind of data the context expects. If parameters are changed, especially things such as ROI, the context may no longer be compatible to the buffers that the device provides.

Recording HSI Data

fluxEngine also has the capability of preprocessing data in order for it to be stored as HSI data cubes.

There are certain preprocessing steps that are always done, such as applying corrections to the data obtained from the camera (if the camera has corrections that are to be applied in software), as well as normalizing the storage order. (fluxEngine always uses the BIP data layout internally, with wavelengths in ascending order.)

Additionally, the user may request further normalizations:

  • The user may select whether they want to store the recorded data as intensities or as reflectances. If the data is stored as intensities the user has the option to include white and dark references (if measured).

  • The user may select a wavelength grid to use instead of the raw wavelengths from the camera. By default hyperspectral cameras will have slight variations in the wavelengths that each pixel corresponds to due to manufacturing tolerances. For this reason fluxEngine provides the user with the ability to normalize the wavelengths onto a regularized grid.

    (When fluxEngine processes data in models wavelengths are always normalized, but this allows the user to already perform the normalization during recording.)

The processing context that is created for the recording of HSI data requires more input than just the preview context. In addition to the device itself (in order to automatically obtain the structure of the input data) the context requires additional information.

The output tensor structure will always be (1, width, bands) for each PushBroom line that is being processed.

The white and dark references may be provided via the ProcessingContext.InstrumentParameters structure. It allows the user to comfortably provide buffer containers for this purpose (see Measuring References).

The static method used to create a processing context for this purpose is ProcessingContext.CreateForInstrumentHSIRecording().

The following example shows how the white and dark reference buffer containers that were recorded in Measuring References can be used for creating a processing context for recording HSI data:

 1 // Declare variables for later use
 2 LuxFlux.fluxEngineNET.HSIRecordingResult ctxAndInfo;
 3 LuxFlux.fluxEngineNET.ProcessingContext ctx;
 4 // For storing the recording result
 5 LuxFlux.fluxEngineNET.BufferContainer recordedData;
 6 // The following were measured previously:
 7 LuxFlux.fluxEngineNET.BufferContainer whiteReference = ..., darkReference = ...;
 8 try
 9 {
10     var processingParameters = new LuxFlux.fluxEngineNET.ProcessingContext.InstrumentParameters();
11     processingParameters.ReferenceInput = new LuxFlux.fluxEngineNET.ProcessingContext.BufferReferenceInput();
12     processingParameters.ReferenceInput.WhiteReference = whiteReference;
13     processingParameters.ReferenceInput.DarkReference = darkReference;
14     // Measure raw intensities
15     LuxFlux.fluxEngineNET.ValueType valueType = LuxFlux.fluxEngineNET.ValueType.Intensity;
16     // null vector -> don't normalize wavelength grid
17     double[] targetWavelengths = null;
18
19     ctxAndInfo = LuxFlux.fluxEngineNET.ProcessingContext.CreateForInstrumentHSIRecording(instrumentDevice, valueType, processingParameters, targetWavelengths);
20     ctx = ctxAndInfo.Context;
21
22     // ctxAndInfo.Wavelengths contains the actual wavelengths
23     //     of ths HSI data
24     // ctxAndInfo.WhiteReference contains the white reference
25     //     data after it has been normalized in the same manner
26     //     as the original data (or NULL to indicate it's not
27     /      present)
28     // ctxAndInfo.DarkReference contains the dark reference
29     //     data after it has been normalized in the same manner
30     //     as the original data (or NULL to indicate it's not
31     /      present)
32     // Other fields contain further information
33
34     // Store up to 1000 lines
35     recordedData = LuxFlux.fluxEngineNET.Util.CreateBufferContainer(ctx, 1000);
36
37     var acqParameters = new LuxFlux.fluxEngineNET.InstrumentDevice.AcquisitionParameters();
38     instrumentDevice.StartAcquisition(acqParameters);
39     // Record exactly 1000 lines
40     while (recordedData.Count < 1000)
41     {
42         LuxFlux.fluxEngineNET.Buffer buffer = primaryDevice.RetrieveBuffer(new TimeSpan(0, 0, 1));
43         if (buffer == null)
44             continue;
45         try
46         {
47             ctx.SetSourceData(buffer);
48             ctx.ProcessNext();
49             recordedData.AddLastResult(ctx);
50         }
51         finally
52         {
53             instrumentDevice.ReturnBuffer(buffer);
54         }
55     }
56     instrumentDevice->stopAcquisition();
57
58     // The data was stored in recordedData
59 }
60 catch (Exception e)
61 {
62     Console.WriteLine($"Error: {e.Message}");
63 }

The previous example used a LuxFlux.fluxEngineNET.BufferContainer to also store the result of the recording. That is the simplest way to do this, but it is also possible to directly access the data via

 1 while (recordedData.Count < 1000)
 2 {
 3     LuxFlux.fluxEngineNET.Buffer buffer = primaryDevice.RetrieveBuffer(new TimeSpan(0, 0, 1));
 4     if (buffer == null)
 5         continue;
 6     try
 7     {
 8         ctx.SetSourceData(buffer);
 9         ctx.ProcessNext();
10         // See below for details on how the output sink logic
11         // works.
12         var outputData = ctx.OutputSinkData(0);
13         var tensorData = outputData.AsTensorCopy();
14         // For HSI data:
15         //   tensorData.Order == 3
16         //   tensorData.Dimensions[0] == 1 (1 line)
17         //   tensorData.Dimensions[1] == width
18         //   tensorData.Dimensions[2] == bands (# wavelengths)
19         //   tensorData.DataType will differ, depending on
20         //       the device, and with what options the
21         //       context was created
22         // Here user code could do something with the data
23     }
24     finally
25     {
26         instrumentDevice.ReturnBuffer(buffer);
27     }
28 }

The previous example selected ValueType.Intensity and provided a white reference when creating a processing context. The following table illustrates the various possible combinations:

Value Type

White reference supplied

Allowed

White reference included in result

Intensity

no

yes

no

Intensity

yes

yes

yes

Reflectance

no

no [1]

-

Reflectance

yes

yes

yes

It is also possible to specify the white reference directly in the form of raw tensor data manually, instead of specifying it in form of buffer containers. Please take a look at the reference documentation of the ProcessingContext.MemoryReferenceInput class that may passed to ProcessingContext.CreateForInstrumentHSIRecording() instead for details on this.

Footnotes

Note

As with all context creation functions, it is also possible to specify a processing queue set as an optional second parameter to the method to associate the context with a different processing queue set.

Model Processing

Finally it is possible to use data from an instrument as the input of models that have been loaded. The user must have first loaded a fluxEngine model from disk using the functions described in Models.

Creating a processing context for model processing takes the following inputs:

  • The instrument device to create the context for

  • The model to use

  • Optionally a white & dark reference, again in the form of a ProcessingContext.InstrumentParameters structure supplied by the user

The selected model must be compatible with the input data the connected instrument device generates, otherwise processing context creation will fail.

The static method ProcessingContext.CreateForInstrumentProcessing() is used to create a context for instrument data processing. If the model doesn’t require reflectance data is its input, it is not necessary to specify a white reference. Most models, however, will require a white reference, as most models will require input data in reflectances.

The following example code shows how to create a processing context that processes data obtained directly from an instrument:

 1 // The following were measured previously:
 2 LuxFlux.fluxEngineNET.BufferContainer whiteReference = ..., darkReference = ...;
 3 // This was loaded previously
 4 LuxFlux.fluxEngineNet.Model model = ...;
 5 try
 6 {
 7     LuxFlux.fluxEngineNET.ProcessingContext ctx;
 8
 9     var processingParameters = new LuxFlux.fluxEngineNET.ProcessingContext.InstrumentParameters();
10     processingParameters.ReferenceInput = new LuxFlux.fluxEngineNET.ProcessingContext.BufferReferenceInput();
11     processingParameters.ReferenceInput.WhiteReference = whiteReference;
12     processingParameters.ReferenceInput.DarkReference = darkReference;
13
14     ctx = LuxFlux.fluxEngineNET.ProcessingContext.CreateForInstrumentProcessing(instrumentDevice,
15         model, processingParameters);
16
17     var acqParameters = new LuxFlux.fluxEngineNET.InstrumentDevice.AcquisitionParameters();
18     instrumentDevice.StartAcquisition(acqParameters);
19
20     while (true)
21     {
22         LuxFlux.fluxEngineNET.Buffer buffer = primaryDevice.RetrieveBuffer(new TimeSpan(0, 0, 1));
23         if (buffer == null)
24             continue;
25
26         try
27         {
28             ctx.SetSourceData(buffer);
29             ctx.ProcessNext();
30             // TODO: obtain processing result from context
31         }
32         finally
33         {
34             instrumentDevice.ReturnBuffer(buffer);
35         }
36     }
37     instrumentDevice.StopAcquisition();
38
39     // The data was stored in recordedData
40 }
41 catch (Exception e)
42 {
43     Console.WriteLine($"Error: {e.Message}");
44 }

Sequence Ids

When processing data in models the buffer number (frame number) is used as a so-called sequence id. For imager cameras and spectrometers this is mostly irrelevant, but for PushBroom cameras this is used by fluxEngine to modify the behavior slightly whether individual frames have been lost. PushBroom cameras only provide a single line each time a buffer is returned, and an image is constructed by concatenating lines one after another. A missing buffer will mean that a line is missing, and if the data is naively concatenated, the missing line will cause distortions.

What fluxEngine does to mitigate this is the following:

  • For any model that outputs on a per-line basis still only the line in question will be processed. (It will not generate additional output for missing lines.)

  • For any model that uses algorithms that put together the current line with previous lines (such as object detection), if a buffer or more are missing between the last invocation and the current one, the algorithm will behave as if the current line had been repeated as often as there were buffers missing.

    • For example, if a single buffer is missing, the line after the missing buffer will be repeated once when performnig any 2D reconstruction, i.e. it will occur twice.

For data processed from the device directly, the buffer number will be used as the sequence id for this. However, when storing a buffer in a buffer container, the sequence id will not be saved – and when extracting a buffer from a buffer container, the user has the ability to select a sequence id to use, instead. (By default it would use the index within the buffer container as the sequence id.)

Note

Also note that the behavior that a missing buffer is repeated only applies to processing within a fluxEngine model – adding a buffer to a buffer container completely ignores the sequence id; if the user wants a similar behavior here, it is up to them to implement this.

PushBroom Resets

As PushBroom cameras can be thought of as incrementally building up a cube line by line, at some point the user may want to indicate that the current cube is considered complete and a new cube starts. In that case the processing context has to be reset, so that all stateful operations are reset as well, such as object detection, but also kernel-based operations.

To achieve this the method ProcessingContext.ResetState() exists. Its usage is simple:

1 try
2 {
3     context.ResetState();
4 }
5 catch (Exception e)
6 {
7     Console.WriteLine($"Error: {e.Message}");
8 }

It is recommended that any time acquisition is stopped and then restarted that the user performs such a reset for any model they use.

Note

There is no requirement to actually perform such a reset. If fluxEngine is used to process PushBroom frames that are obtained from a camera above a conveyor belt in a continuous process, for example, it is possible to just simply process all incoming frames in a loop and never call the reset method. In that situation the reset method would be called though if the conveyor belt is stopped and has to be started up again. Though, depending on the specific application, that could also mean that a processing context would have to be created again, for example because references have to be measured again, and a simple state reset is not sufficient.

Obtaining Results

After processing has completed via the ProcessingContext.ProcessNext() method, fluxEngine provides a means for the user to obtain the results of that operation.

When designing the model in fluxTrainer that is to be used here, Output Sinks should be added to the model wherever processing results are to be obtained later.

For processing instrument preview and instrument recording processing contexts an automatic output sink with sink index 0 will be created by fluxEngine so the user may extract the preview and/or recording data. Additionally, for instrument recording sinks, the BufferContainer.AddLastResult() method may be used to add the last output data of a model to a buffer container. (Though that method only works for recording contexts.)

Note

If a model contains no output sinks, it can be processed with fluxEngine, but the user will have no possibility of extracting any kind of result from it.

fluxEngine provides a means to introspect a model to obtain information about the output sinks that it contains. The following two identifiers for output sinks have to be distinguished:

  • The output sink index, this ist just a number starting at 0 and ending at one below the number of output sinks that may be used to specify the output sink for the purposes of the fluxEngine API.

    The ordering of output sinks according to this index is non-obvious. Loading the same .fluxmdl file will lead to the same order, but saving models with the same configuration (but constructed separately) can lead to different orders of output sinks.

  • The output id, which is a user-assignable id in fluxTrainer that can be used to mark output sinks for a specific purpose. The output id may not be unique (but should be), and is purely there for informational purposes.

    For each output sink in the model the user will be able to obtain the output id of that sink. There is also a method ProcessingContext.OutputSinkInfoById() that can locate an output sink if the output id of that sink is unique. It will return the information structure of the sink with that output id.

To obtain information about all output sinks in the context, the property ProcessingContext.OutputSinkInfos exists, which returns an array of objects that contain information about each output sink. The index of the array is also the output sink index.

 1 try
 2 {
 3     foreach (var sink in context.OutputSinkInfos)
 4     {
 5         Console.WriteLine($"Output sink with index {sink.Index} has name {sink.Name} and id {sink.Id}.");
 6     }
 7 }
 8 catch (Exception e)
 9 {
10     Console.WriteLine($"Error: {e.Message}");
11 }

The ProcessingContext.OutputSinkInfo structure contains the following information:

  • The output id of the output sink

  • The name of the output sink as an UTF-8 string (this is the name the user specified when creating the model in fluxTraineer)

  • Storage type: what kind of data the output sink will return (the current options are either tensor data or detected objects)

  • The structure of the data that will be returned by the output sink

  • The output delay of the output sink (only relevant in the case when PushBroom data is being processed, see Output Delay for PushBroom Cameras for a more detailed discussion of this.

Output sinks store either tensor data or detected objects, depending on the configuration of the output sink, and where it sits in the processing chain.

Tensor Data

Tensor data within fluxEngine always has a well-defined storage order, as most algorithms that work on hyperspectral data are at their most efficient in this memory layout. While fluxEngine supports input data of arbitrary storage order, it will be converted to the internal storage order at the beginning of processing. The output data will always have the following structure:

  • When processing entire HSI cubes it will effectively return data in BIP storage order, that means that the dimension structure will be (y, x, λ) (for data that still has wavelength information) or (y, x, P), where P is a generic dimension, if the data has already passed through dimensionality reduction filters such as PCA.

  • When processing PushBroom frames it will effectively return data in LambdaX storage order, with an additional dimension of size 1 at the beginning. In that case it will either be (1, x, λ) or (1, x, P).

  • Pure per-object data always has a tensor structure of order 2, in the form of (N, λ) or (N, P), where N describes the number of objects detected in this processing iteration. Important: objects themselves are returned as a structure (see below), per-object data is data that is the output of filters such as the Per-Object Averaging or Per-Object Counting filter. Also note that output sinks can combine objects and per-object data, in which case the per-object data will be returned as part of the object structure.

    For PushBroom data it is recommended to always combine per-object data with objects before interpreting it, as the output delay of both nodes may be different, and when combining the data the user does not have to keep track of the relative delays themselves.

To obtain the tensor structure of a given output sink, the property ProcessingContext.OutputSinkInfo.TensorStructure is available. It contains the following information:

  • The scalar data type of the tensor data (this is the same as the data type configured in the output sink)

  • The order of the tensor, which will be 2 or 3 (see above)

  • The maximum sizes of the tensor that can be returned here

  • The fixed sizes of the tensor that will be returned here. If the tensors returned here are always of the same size, the values here will be same as the maximum sizes. Any dimension that is not always the same will have a value of -1 instead. If all of the sizes of the tensor returned here are fixed, the tensor returned will always be of the same size. (There is one notable exception: if the output sink has a non-zero output delay of m, the first m processing iterations will produce a tensor that does not have any data.)

Please refer to the documentation of ProcessingContext.OutputSinkTensorStructure for more information on how this information is returned.

Using ProcessingContext.OutputSinkData() it is possible to obtain that tensor data after a successful processing step.

Tensor data retrieved from an output sink may be copied into a newly allocated tensor via the OutputSinkData.AsTensorCopy() method that allows easy access to tensor elements. There is also an unsafe property OutputSinkData.AsTensor() that returns a view on the internal data of the processing context. That property is unsafe because it is up to the user not to access that view after the processing context is destroyed, its state is reset, or has processed new data.

For example, if we know a given sink with index sinkIndex has signed 16bit integer data that spans the entire cube that is being processed (when processing HSI cubes), the following code could be used to obtain the results:

 1 /* obtained from previous introspection */
 2 int sinkIndex = ...;
 3 Int64 cube_width = ...;
 4 /* from current buffer */
 5 Int64 bufferNumber = ...;
 6 try
 7 {
 8     var data = ProcessingContext.OutputSinkData(sinkIndex);
 9     var tensor = data.AsTensorCopy();
10
11     for (Int64 x = 0; x < cube_width; ++x) {
12         Console.WriteLine($"Classification result for pixel ({x}, {bufferNumber}) = " +
13             tensor.Value<int16_t>(0, x, 0));
14     }
15 }
16 catch (Exception e)
17 {
18     Console.WriteLine($"Error: {e.Message}");
19 }