Skip to content

Add support for FLIR thermal image metadata #295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions MetadataExtractor.Samples/FlirSamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using MetadataExtractor.Formats.Flir;
using MetadataExtractor.Formats.Jpeg;
using MetadataExtractor.Util;

namespace MetadataExtractor.Samples
{
public static class FlirSamples
{
public static void Main()
{
var inputFile = "my-input.jpg"; // path to a FLIR JPEG file
var outputFile = "my-output.png"; // path to the output thermal image

if (TryGetThermalImageBytesFromJpeg(inputFile, out byte[]? bytes, out int width, out int height))
{
WritePng(bytes, width, height, outputFile);
}
}

public static bool TryGetThermalImageBytesFromJpeg(
string jpegFile,
[NotNullWhen(returnValue: true)] out byte[]? imageBytes,
out int width,
out int height)
{
var readers = JpegMetadataReader
.AllReaders
.Where(reader => reader is not FlirReader)
.Concat(new[] { new FlirReader { ExtractRawThermalImage = true } })
.ToList();

var directories = JpegMetadataReader.ReadMetadata(jpegFile, readers);

var flirRawDataDirectory = directories.OfType<FlirRawDataDirectory>().FirstOrDefault();

if (flirRawDataDirectory is null)
{
imageBytes = null;
width = 0;
height = 0;
return false;
}

width = flirRawDataDirectory.GetInt32(FlirRawDataDirectory.TagRawThermalImageWidth);
height = flirRawDataDirectory.GetInt32(FlirRawDataDirectory.TagRawThermalImageHeight);
imageBytes = flirRawDataDirectory.GetByteArray(FlirRawDataDirectory.TagRawThermalImage);

return imageBytes != null;
}

public static void WritePng(byte[] thermalImageBytes, int width, int height, string outputFile)
{
var fileType = FileTypeDetector.DetectFileType(new MemoryStream(thermalImageBytes));

if (fileType == FileType.Png)
{
// Data is already in PNG format.
// It is likely already coloured and ready for presentation to a human.

File.WriteAllBytes(outputFile, thermalImageBytes);
return;
}

// Assume data is in uint16 grayscale.
//
// It is "raw" meaning uncoloured but, more importantly, the levels are unadjusted.
// For example, if the scene did not have much temperature variation relative to the
// range of the sensor, most values may be within a very narrow band of the 16-bit spectrum,
// making the image appear a flat gray. Opening it in Photoshop/Gimp/etc and adjusting levels
// will reveal the image.
//
// There is other metadata in the image that may inform this process (untested -- please
// share if you find a good process here).

var pixelFormats = PixelFormats.Gray16;

var bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format16bppGrayScale);

var data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bitmap.PixelFormat);

Marshal.Copy(thermalImageBytes, 0, data.Scan0, thermalImageBytes.Length);

var source = BitmapSource.Create(
width,
height,
bitmap.HorizontalResolution,
bitmap.VerticalResolution,
pixelFormats,
null,
data.Scan0,
data.Stride * height,
data.Stride);

bitmap.UnlockBits(data);

using var stream = new FileStream(outputFile, FileMode.Create);

var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
encoder.Save(stream);
}
}
}
16 changes: 10 additions & 6 deletions MetadataExtractor.Samples/MetadataExtractor.Samples.csproj
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net5.0;net35;net45</TargetFrameworks>
<TargetFramework>net48</TargetFramework>
<OutputType>Exe</OutputType>
<StartupObject>MetadataExtractor.Samples.Program</StartupObject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\MetadataExtractor\MetadataExtractor.csproj" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
<Reference Include="System" />
<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="WindowsBase" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

Expand Down
1 change: 1 addition & 0 deletions MetadataExtractor.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Finepix/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fisheye/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flashpix/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flir/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Foveon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ftyp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fujifilm/@EntryIndexedValue">True</s:Boolean>
Expand Down
5 changes: 5 additions & 0 deletions MetadataExtractor/Formats/Exif/ExifTiffHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@ private bool ProcessMakernote(int makernoteOffset, ICollection<int> processedIfd
PushDirectory(new DjiMakernoteDirectory());
TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset);
}
else if (string.Equals("FLIR Systems", cameraMake, StringComparison.Ordinal))
{
PushDirectory(new FlirMakernoteDirectory());
TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset);
}
else
{
// The makernote is not comprehended by this library.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Diagnostics.CodeAnalysis;

namespace MetadataExtractor.Formats.Exif.Makernotes
{
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class FlirMakernoteDescriptor : TagDescriptor<FlirMakernoteDirectory>
{
public FlirMakernoteDescriptor(FlirMakernoteDirectory directory)
: base(directory)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace MetadataExtractor.Formats.Exif.Makernotes
{
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class FlirMakernoteDirectory : Directory
{
public const int TagImageTemperatureMax = 0x0001;
public const int TagImageTemperatureMin = 0x0002;
public const int TagEmissivity = 0x0003;
public const int TagUnknownTemperature = 0x0004;
public const int TagCameraTemperatureRangeMin = 0x0005;
public const int TagCameraTemperatureRangeMax = 0x0006;

private static readonly Dictionary<int, string> _tagNameMap = new()
{
{ TagImageTemperatureMax, "Image Temperature Max" },
{ TagImageTemperatureMin, "Image Temperature Min" },
{ TagEmissivity, "Emissivity" },
{ TagUnknownTemperature, "Unknown Temperature" },
{ TagCameraTemperatureRangeMin, "Camera Temperature Range Max" },
{ TagCameraTemperatureRangeMax, "Camera Temperature Range Min" }
};

public FlirMakernoteDirectory() : base(_tagNameMap)
{
SetDescriptor(new FlirMakernoteDescriptor(this));
}

public override string Name => "FLIR Makernote";
}
}
47 changes: 47 additions & 0 deletions MetadataExtractor/Formats/Flir/FlirCameraInfoDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using static MetadataExtractor.Formats.Flir.FlirCameraInfoDirectory;

namespace MetadataExtractor.Formats.Flir
{
public sealed class FlirCameraInfoDescriptor : TagDescriptor<FlirCameraInfoDirectory>
{
public FlirCameraInfoDescriptor(FlirCameraInfoDirectory directory)
: base(directory)
{
}

public override string? GetDescription(int tagType)
{
return tagType switch
{
TagReflectedApparentTemperature => KelvinToCelcius(),
TagAtmosphericTemperature => KelvinToCelcius(),
TagIRWindowTemperature => KelvinToCelcius(),
TagRelativeHumidity => RelativeHumidity(),
TagCameraTemperatureRangeMax => KelvinToCelcius(),
TagCameraTemperatureRangeMin => KelvinToCelcius(),
TagCameraTemperatureMaxClip => KelvinToCelcius(),
TagCameraTemperatureMinClip => KelvinToCelcius(),
TagCameraTemperatureMaxWarn => KelvinToCelcius(),
TagCameraTemperatureMinWarn => KelvinToCelcius(),
TagCameraTemperatureMaxSaturated => KelvinToCelcius(),
TagCameraTemperatureMinSaturated => KelvinToCelcius(),
_ => base.GetDescription(tagType)
};

string KelvinToCelcius()
{
float f = Directory.GetSingle(tagType) - 273.15f;
return $"{f:N1} C";
}

string RelativeHumidity()
{
float f = Directory.GetSingle(tagType);
float val = (f > 2 ? f / 100 : f);
return $"{(val * 100):N1} %";
}
}
}
}
109 changes: 109 additions & 0 deletions MetadataExtractor/Formats/Flir/FlirCameraInfoDirectory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Collections.Generic;

namespace MetadataExtractor.Formats.Flir
{
public sealed class FlirCameraInfoDirectory : Directory
{
public const int TagEmissivity = 32;
public const int TagObjectDistance = 36;
public const int TagReflectedApparentTemperature = 40;
public const int TagAtmosphericTemperature = 44;
public const int TagIRWindowTemperature = 48;
public const int TagIRWindowTransmission = 52;
public const int TagRelativeHumidity = 60;
public const int TagPlanckR1 = 88;
public const int TagPlanckB = 92;
public const int TagPlanckF = 96;
public const int TagAtmosphericTransAlpha1 = 112;
public const int TagAtmosphericTransAlpha2 = 116;
public const int TagAtmosphericTransBeta1 = 120;
public const int TagAtmosphericTransBeta2 = 124;
public const int TagAtmosphericTransX = 128;
public const int TagCameraTemperatureRangeMax = 144;
public const int TagCameraTemperatureRangeMin = 148;
public const int TagCameraTemperatureMaxClip = 152;
public const int TagCameraTemperatureMinClip = 156;
public const int TagCameraTemperatureMaxWarn = 160;
public const int TagCameraTemperatureMinWarn = 164;
public const int TagCameraTemperatureMaxSaturated = 168;
public const int TagCameraTemperatureMinSaturated = 172;
public const int TagCameraModel = 212;
public const int TagCameraPartNumber = 244;
public const int TagCameraSerialNumber = 260;
public const int TagCameraSoftware = 276;
public const int TagLensModel = 368;
public const int TagLensPartNumber = 400;
public const int TagLensSerialNumber = 416;
public const int TagFieldOfView = 436;
public const int TagFilterModel = 492;
public const int TagFilterPartNumber = 508;
public const int TagFilterSerialNumber = 540;
public const int TagPlanckO = 776;
public const int TagPlanckR2 = 780;
public const int TagRawValueRangeMin = 784;
public const int TagRawValueRangeMax = 786;
public const int TagRawValueMedian = 824;
public const int TagRawValueRange = 828;
public const int TagDateTimeOriginal = 900;
public const int TagFocusStepCount = 912;
public const int TagFocusDistance = 1116;
public const int TagFrameRate = 1124;

public override string Name => "FLIR Camera Info";

private static readonly Dictionary<int, string> _nameByTag = new()
{
{ TagEmissivity, "Emissivity" },
{ TagObjectDistance, "Object Distance" },
{ TagReflectedApparentTemperature, "Reflected Apparent Temperature" },
{ TagAtmosphericTemperature, "Atmospheric Temperature" },
{ TagIRWindowTemperature, "IR Window Temperature" },
{ TagIRWindowTransmission, "IR Window Transmission" },
{ TagRelativeHumidity, "Relative Humidity" },
{ TagPlanckR1, "Planck R1" },
{ TagPlanckB, "Planck B" },
{ TagPlanckF, "Planck F" },
{ TagAtmosphericTransAlpha1, "Atmospheric Trans Alpha1" },
{ TagAtmosphericTransAlpha2, "Atmospheric Trans Alpha2" },
{ TagAtmosphericTransBeta1, "Atmospheric Trans Beta1" },
{ TagAtmosphericTransBeta2, "Atmospheric Trans Beta2" },
{ TagAtmosphericTransX, "Atmospheric Trans X" },
{ TagCameraTemperatureRangeMax, "Camera Temperature Range Max" },
{ TagCameraTemperatureRangeMin, "Camera Temperature Range Min" },
{ TagCameraTemperatureMaxClip, "Camera Temperature Max Clip" },
{ TagCameraTemperatureMinClip, "Camera Temperature Min Clip" },
{ TagCameraTemperatureMaxWarn, "Camera Temperature Max Warn" },
{ TagCameraTemperatureMinWarn, "Camera Temperature Min Warn" },
{ TagCameraTemperatureMaxSaturated, "Camera Temperature Max Saturated" },
{ TagCameraTemperatureMinSaturated, "Camera Temperature Min Saturated" },
{ TagCameraModel, "Camera Model" },
{ TagCameraPartNumber, "Camera Part Number" },
{ TagCameraSerialNumber, "Camera Serial Number" },
{ TagCameraSoftware, "Camera Software" },
{ TagLensModel, "Lens Model" },
{ TagLensPartNumber, "Lens Part Number" },
{ TagLensSerialNumber, "Lens Serial Number" },
{ TagFieldOfView, "Field Of View" },
{ TagFilterModel, "Filter Model" },
{ TagFilterPartNumber, "Filter Part Number" },
{ TagFilterSerialNumber, "Filter Serial Number" },
{ TagPlanckO, "Planck O" },
{ TagPlanckR2, "Planck R2" },
{ TagRawValueRangeMin, "Raw Value Range Min" },
{ TagRawValueRangeMax, "Raw Value Range Max" },
{ TagRawValueMedian, "Raw Value Median" },
{ TagRawValueRange, "Raw Value Range" },
{ TagDateTimeOriginal, "Date Time Original" },
{ TagFocusStepCount, "Focus Step Count" },
{ TagFocusDistance, "Focus Distance" },
{ TagFrameRate, "Frame Rate" }
};

public FlirCameraInfoDirectory() : base(_nameByTag)
{
SetDescriptor(new FlirCameraInfoDescriptor(this));
}
}
}
Loading