Skip to content

Commit d79ec6d

Browse files
authored
Merge pull request #295 from drewnoakes/flir
Add support for FLIR thermal image metadata
2 parents c7cbaef + ac4bf1c commit d79ec6d

19 files changed

+925
-8
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// 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.
2+
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Drawing;
5+
using System.Drawing.Imaging;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Runtime.InteropServices;
9+
using System.Windows.Media;
10+
using System.Windows.Media.Imaging;
11+
using MetadataExtractor.Formats.Flir;
12+
using MetadataExtractor.Formats.Jpeg;
13+
using MetadataExtractor.Util;
14+
15+
namespace MetadataExtractor.Samples
16+
{
17+
public static class FlirSamples
18+
{
19+
public static void Main()
20+
{
21+
var inputFile = "my-input.jpg"; // path to a FLIR JPEG file
22+
var outputFile = "my-output.png"; // path to the output thermal image
23+
24+
if (TryGetThermalImageBytesFromJpeg(inputFile, out byte[]? bytes, out int width, out int height))
25+
{
26+
WritePng(bytes, width, height, outputFile);
27+
}
28+
}
29+
30+
public static bool TryGetThermalImageBytesFromJpeg(
31+
string jpegFile,
32+
[NotNullWhen(returnValue: true)] out byte[]? imageBytes,
33+
out int width,
34+
out int height)
35+
{
36+
var readers = JpegMetadataReader
37+
.AllReaders
38+
.Where(reader => reader is not FlirReader)
39+
.Concat(new[] { new FlirReader { ExtractRawThermalImage = true } })
40+
.ToList();
41+
42+
var directories = JpegMetadataReader.ReadMetadata(jpegFile, readers);
43+
44+
var flirRawDataDirectory = directories.OfType<FlirRawDataDirectory>().FirstOrDefault();
45+
46+
if (flirRawDataDirectory is null)
47+
{
48+
imageBytes = null;
49+
width = 0;
50+
height = 0;
51+
return false;
52+
}
53+
54+
width = flirRawDataDirectory.GetInt32(FlirRawDataDirectory.TagRawThermalImageWidth);
55+
height = flirRawDataDirectory.GetInt32(FlirRawDataDirectory.TagRawThermalImageHeight);
56+
imageBytes = flirRawDataDirectory.GetByteArray(FlirRawDataDirectory.TagRawThermalImage);
57+
58+
return imageBytes != null;
59+
}
60+
61+
public static void WritePng(byte[] thermalImageBytes, int width, int height, string outputFile)
62+
{
63+
var fileType = FileTypeDetector.DetectFileType(new MemoryStream(thermalImageBytes));
64+
65+
if (fileType == FileType.Png)
66+
{
67+
// Data is already in PNG format.
68+
// It is likely already coloured and ready for presentation to a human.
69+
70+
File.WriteAllBytes(outputFile, thermalImageBytes);
71+
return;
72+
}
73+
74+
// Assume data is in uint16 grayscale.
75+
//
76+
// It is "raw" meaning uncoloured but, more importantly, the levels are unadjusted.
77+
// For example, if the scene did not have much temperature variation relative to the
78+
// range of the sensor, most values may be within a very narrow band of the 16-bit spectrum,
79+
// making the image appear a flat gray. Opening it in Photoshop/Gimp/etc and adjusting levels
80+
// will reveal the image.
81+
//
82+
// There is other metadata in the image that may inform this process (untested -- please
83+
// share if you find a good process here).
84+
85+
var pixelFormats = PixelFormats.Gray16;
86+
87+
var bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format16bppGrayScale);
88+
89+
var data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bitmap.PixelFormat);
90+
91+
Marshal.Copy(thermalImageBytes, 0, data.Scan0, thermalImageBytes.Length);
92+
93+
var source = BitmapSource.Create(
94+
width,
95+
height,
96+
bitmap.HorizontalResolution,
97+
bitmap.VerticalResolution,
98+
pixelFormats,
99+
null,
100+
data.Scan0,
101+
data.Stride * height,
102+
data.Stride);
103+
104+
bitmap.UnlockBits(data);
105+
106+
using var stream = new FileStream(outputFile, FileMode.Create);
107+
108+
var encoder = new PngBitmapEncoder();
109+
encoder.Frames.Add(BitmapFrame.Create(source));
110+
encoder.Save(stream);
111+
}
112+
}
113+
}
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net5.0;net35;net45</TargetFrameworks>
4+
<TargetFramework>net48</TargetFramework>
55
<OutputType>Exe</OutputType>
6+
<StartupObject>MetadataExtractor.Samples.Program</StartupObject>
67
</PropertyGroup>
78

89
<ItemGroup>
910
<ProjectReference Include="..\MetadataExtractor\MetadataExtractor.csproj" />
1011
</ItemGroup>
1112

12-
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
13-
<Reference Include="System" />
13+
<ItemGroup>
14+
<PackageReference Include="Nullable" Version="1.3.0">
15+
<PrivateAssets>all</PrivateAssets>
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
17+
</PackageReference>
1418
</ItemGroup>
1519

16-
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
17-
<Reference Include="System" />
18-
<Reference Include="Microsoft.CSharp" />
20+
<ItemGroup>
21+
<Reference Include="PresentationCore" />
22+
<Reference Include="WindowsBase" />
1923
</ItemGroup>
2024

2125
</Project>

MetadataExtractor.Tools.FileProcessor/MetadataExtractor.Tools.FileProcessor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net5.0</TargetFrameworks>
4+
<TargetFramework>net5.0</TargetFramework>
55
<OutputType>Exe</OutputType>
66
</PropertyGroup>
77

MetadataExtractor.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<s:Boolean x:Key="/Default/UserDictionary/Words/=Finepix/@EntryIndexedValue">True</s:Boolean>
5454
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fisheye/@EntryIndexedValue">True</s:Boolean>
5555
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flashpix/@EntryIndexedValue">True</s:Boolean>
56+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flir/@EntryIndexedValue">True</s:Boolean>
5657
<s:Boolean x:Key="/Default/UserDictionary/Words/=Foveon/@EntryIndexedValue">True</s:Boolean>
5758
<s:Boolean x:Key="/Default/UserDictionary/Words/=ftyp/@EntryIndexedValue">True</s:Boolean>
5859
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fujifilm/@EntryIndexedValue">True</s:Boolean>

MetadataExtractor/Formats/Exif/ExifTiffHandler.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,11 @@ private bool ProcessMakernote(int makernoteOffset, ICollection<int> processedIfd
600600
PushDirectory(new DjiMakernoteDirectory());
601601
TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset);
602602
}
603+
else if (string.Equals("FLIR Systems", cameraMake, StringComparison.Ordinal))
604+
{
605+
PushDirectory(new FlirMakernoteDirectory());
606+
TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset);
607+
}
603608
else
604609
{
605610
// The makernote is not comprehended by this library.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// 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.
2+
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace MetadataExtractor.Formats.Exif.Makernotes
6+
{
7+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
8+
public class FlirMakernoteDescriptor : TagDescriptor<FlirMakernoteDirectory>
9+
{
10+
public FlirMakernoteDescriptor(FlirMakernoteDirectory directory)
11+
: base(directory)
12+
{
13+
}
14+
}
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// 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.
2+
3+
using System.Collections.Generic;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace MetadataExtractor.Formats.Exif.Makernotes
7+
{
8+
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
9+
public class FlirMakernoteDirectory : Directory
10+
{
11+
public const int TagImageTemperatureMax = 0x0001;
12+
public const int TagImageTemperatureMin = 0x0002;
13+
public const int TagEmissivity = 0x0003;
14+
public const int TagUnknownTemperature = 0x0004;
15+
public const int TagCameraTemperatureRangeMin = 0x0005;
16+
public const int TagCameraTemperatureRangeMax = 0x0006;
17+
18+
private static readonly Dictionary<int, string> _tagNameMap = new()
19+
{
20+
{ TagImageTemperatureMax, "Image Temperature Max" },
21+
{ TagImageTemperatureMin, "Image Temperature Min" },
22+
{ TagEmissivity, "Emissivity" },
23+
{ TagUnknownTemperature, "Unknown Temperature" },
24+
{ TagCameraTemperatureRangeMin, "Camera Temperature Range Max" },
25+
{ TagCameraTemperatureRangeMax, "Camera Temperature Range Min" }
26+
};
27+
28+
public FlirMakernoteDirectory() : base(_tagNameMap)
29+
{
30+
SetDescriptor(new FlirMakernoteDescriptor(this));
31+
}
32+
33+
public override string Name => "FLIR Makernote";
34+
}
35+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// 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.
2+
3+
using static MetadataExtractor.Formats.Flir.FlirCameraInfoDirectory;
4+
5+
namespace MetadataExtractor.Formats.Flir
6+
{
7+
public sealed class FlirCameraInfoDescriptor : TagDescriptor<FlirCameraInfoDirectory>
8+
{
9+
public FlirCameraInfoDescriptor(FlirCameraInfoDirectory directory)
10+
: base(directory)
11+
{
12+
}
13+
14+
public override string? GetDescription(int tagType)
15+
{
16+
return tagType switch
17+
{
18+
TagReflectedApparentTemperature => KelvinToCelcius(),
19+
TagAtmosphericTemperature => KelvinToCelcius(),
20+
TagIRWindowTemperature => KelvinToCelcius(),
21+
TagRelativeHumidity => RelativeHumidity(),
22+
TagCameraTemperatureRangeMax => KelvinToCelcius(),
23+
TagCameraTemperatureRangeMin => KelvinToCelcius(),
24+
TagCameraTemperatureMaxClip => KelvinToCelcius(),
25+
TagCameraTemperatureMinClip => KelvinToCelcius(),
26+
TagCameraTemperatureMaxWarn => KelvinToCelcius(),
27+
TagCameraTemperatureMinWarn => KelvinToCelcius(),
28+
TagCameraTemperatureMaxSaturated => KelvinToCelcius(),
29+
TagCameraTemperatureMinSaturated => KelvinToCelcius(),
30+
_ => base.GetDescription(tagType)
31+
};
32+
33+
string KelvinToCelcius()
34+
{
35+
float f = Directory.GetSingle(tagType) - 273.15f;
36+
return $"{f:N1} C";
37+
}
38+
39+
string RelativeHumidity()
40+
{
41+
float f = Directory.GetSingle(tagType);
42+
float val = (f > 2 ? f / 100 : f);
43+
return $"{(val * 100):N1} %";
44+
}
45+
}
46+
}
47+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// 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.
2+
3+
using System.Collections.Generic;
4+
5+
namespace MetadataExtractor.Formats.Flir
6+
{
7+
public sealed class FlirCameraInfoDirectory : Directory
8+
{
9+
public const int TagEmissivity = 32;
10+
public const int TagObjectDistance = 36;
11+
public const int TagReflectedApparentTemperature = 40;
12+
public const int TagAtmosphericTemperature = 44;
13+
public const int TagIRWindowTemperature = 48;
14+
public const int TagIRWindowTransmission = 52;
15+
public const int TagRelativeHumidity = 60;
16+
public const int TagPlanckR1 = 88;
17+
public const int TagPlanckB = 92;
18+
public const int TagPlanckF = 96;
19+
public const int TagAtmosphericTransAlpha1 = 112;
20+
public const int TagAtmosphericTransAlpha2 = 116;
21+
public const int TagAtmosphericTransBeta1 = 120;
22+
public const int TagAtmosphericTransBeta2 = 124;
23+
public const int TagAtmosphericTransX = 128;
24+
public const int TagCameraTemperatureRangeMax = 144;
25+
public const int TagCameraTemperatureRangeMin = 148;
26+
public const int TagCameraTemperatureMaxClip = 152;
27+
public const int TagCameraTemperatureMinClip = 156;
28+
public const int TagCameraTemperatureMaxWarn = 160;
29+
public const int TagCameraTemperatureMinWarn = 164;
30+
public const int TagCameraTemperatureMaxSaturated = 168;
31+
public const int TagCameraTemperatureMinSaturated = 172;
32+
public const int TagCameraModel = 212;
33+
public const int TagCameraPartNumber = 244;
34+
public const int TagCameraSerialNumber = 260;
35+
public const int TagCameraSoftware = 276;
36+
public const int TagLensModel = 368;
37+
public const int TagLensPartNumber = 400;
38+
public const int TagLensSerialNumber = 416;
39+
public const int TagFieldOfView = 436;
40+
public const int TagFilterModel = 492;
41+
public const int TagFilterPartNumber = 508;
42+
public const int TagFilterSerialNumber = 540;
43+
public const int TagPlanckO = 776;
44+
public const int TagPlanckR2 = 780;
45+
public const int TagRawValueRangeMin = 784;
46+
public const int TagRawValueRangeMax = 786;
47+
public const int TagRawValueMedian = 824;
48+
public const int TagRawValueRange = 828;
49+
public const int TagDateTimeOriginal = 900;
50+
public const int TagFocusStepCount = 912;
51+
public const int TagFocusDistance = 1116;
52+
public const int TagFrameRate = 1124;
53+
54+
public override string Name => "FLIR Camera Info";
55+
56+
private static readonly Dictionary<int, string> _nameByTag = new()
57+
{
58+
{ TagEmissivity, "Emissivity" },
59+
{ TagObjectDistance, "Object Distance" },
60+
{ TagReflectedApparentTemperature, "Reflected Apparent Temperature" },
61+
{ TagAtmosphericTemperature, "Atmospheric Temperature" },
62+
{ TagIRWindowTemperature, "IR Window Temperature" },
63+
{ TagIRWindowTransmission, "IR Window Transmission" },
64+
{ TagRelativeHumidity, "Relative Humidity" },
65+
{ TagPlanckR1, "Planck R1" },
66+
{ TagPlanckB, "Planck B" },
67+
{ TagPlanckF, "Planck F" },
68+
{ TagAtmosphericTransAlpha1, "Atmospheric Trans Alpha1" },
69+
{ TagAtmosphericTransAlpha2, "Atmospheric Trans Alpha2" },
70+
{ TagAtmosphericTransBeta1, "Atmospheric Trans Beta1" },
71+
{ TagAtmosphericTransBeta2, "Atmospheric Trans Beta2" },
72+
{ TagAtmosphericTransX, "Atmospheric Trans X" },
73+
{ TagCameraTemperatureRangeMax, "Camera Temperature Range Max" },
74+
{ TagCameraTemperatureRangeMin, "Camera Temperature Range Min" },
75+
{ TagCameraTemperatureMaxClip, "Camera Temperature Max Clip" },
76+
{ TagCameraTemperatureMinClip, "Camera Temperature Min Clip" },
77+
{ TagCameraTemperatureMaxWarn, "Camera Temperature Max Warn" },
78+
{ TagCameraTemperatureMinWarn, "Camera Temperature Min Warn" },
79+
{ TagCameraTemperatureMaxSaturated, "Camera Temperature Max Saturated" },
80+
{ TagCameraTemperatureMinSaturated, "Camera Temperature Min Saturated" },
81+
{ TagCameraModel, "Camera Model" },
82+
{ TagCameraPartNumber, "Camera Part Number" },
83+
{ TagCameraSerialNumber, "Camera Serial Number" },
84+
{ TagCameraSoftware, "Camera Software" },
85+
{ TagLensModel, "Lens Model" },
86+
{ TagLensPartNumber, "Lens Part Number" },
87+
{ TagLensSerialNumber, "Lens Serial Number" },
88+
{ TagFieldOfView, "Field Of View" },
89+
{ TagFilterModel, "Filter Model" },
90+
{ TagFilterPartNumber, "Filter Part Number" },
91+
{ TagFilterSerialNumber, "Filter Serial Number" },
92+
{ TagPlanckO, "Planck O" },
93+
{ TagPlanckR2, "Planck R2" },
94+
{ TagRawValueRangeMin, "Raw Value Range Min" },
95+
{ TagRawValueRangeMax, "Raw Value Range Max" },
96+
{ TagRawValueMedian, "Raw Value Median" },
97+
{ TagRawValueRange, "Raw Value Range" },
98+
{ TagDateTimeOriginal, "Date Time Original" },
99+
{ TagFocusStepCount, "Focus Step Count" },
100+
{ TagFocusDistance, "Focus Distance" },
101+
{ TagFrameRate, "Frame Rate" }
102+
};
103+
104+
public FlirCameraInfoDirectory() : base(_nameByTag)
105+
{
106+
SetDescriptor(new FlirCameraInfoDescriptor(this));
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)