Last active
September 14, 2021 01:56
-
-
Save ksasao/934851ff2cfead8d6656027bf6b368cf to your computer and use it in GitHub Desktop.
HEIF (.heic), JPEG などに含まれる EXIF 情報などから緯度経度・時刻・姿勢(Unity準拠)を読み込む C# サンプル。 https://twitter.com/ksasao/status/1426522324623265800
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using MetadataExtractor; | |
using MetadataExtractor.Formats.Exif; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Numerics; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
namespace metadata | |
{ | |
// NuGet で MetadataExtractor を追加してください | |
public class PhotoData | |
{ | |
/// <summary> | |
/// 撮影日時 | |
/// </summary> | |
public DateTime Date { get; set; } = new DateTime(0); | |
/// <summary> | |
/// タイムゾーン | |
/// </summary> | |
public string TimeZone { get; set; } = ""; | |
/// <summary> | |
/// 緯度(Google Maps準拠) | |
/// </summary> | |
public double Latitude { get; set; } = Double.MinValue; | |
/// <summary> | |
/// 経度(Google Maps準拠) | |
/// </summary> | |
public double Longitude { get; set; } = Double.MinValue; | |
/// <summary> | |
/// 高度(m) | |
/// </summary> | |
public double Altitude { get; set; } = Double.MinValue; | |
/// <summary> | |
/// 北を0, 東を90 とする方位角(°) | |
/// </summary> | |
public double Azimuth { get; set; } = Double.MinValue; | |
/// <summary> | |
/// Unity のカメラの Rotation(カメラの向きに合わせて補正済み)。 | |
/// Yは常に0。必要に応じて Azimuth を利用する。 | |
/// </summary> | |
public Vector3 Rotation { get; set; } = new Vector3 { X = float.MinValue, Y = float.MinValue, Z = float.MinValue }; | |
private string[] dateTimeFormats = { "yyyy:MM:dd HH:mm:ss.fff", "yyyy:MM:dd HH:mm:ss" }; | |
/// <summary> | |
/// 値が設定されているか | |
/// </summary> | |
/// <param name="obj">PhotoDataクラスのプロパティ</param> | |
/// <returns>値が設定されていれば true</returns> | |
public static bool HasValue(Object obj) | |
{ | |
Type type = obj.GetType(); | |
if (type == typeof(DateTime)) | |
{ | |
return ((DateTime)obj) != new DateTime(0); | |
} else if (type == typeof(string)) | |
{ | |
return !string.IsNullOrEmpty((string)obj); | |
} else if(type == typeof(double)) | |
{ | |
return ((double)obj) != double.MinValue; | |
}else if(type == typeof(Vector3)) | |
{ | |
return (((Vector3)obj).X != float.MinValue); | |
} | |
return false; | |
} | |
public PhotoData() { } | |
public PhotoData(string filename) | |
{ | |
IEnumerable<Directory> directories= ImageMetadataReader.ReadMetadata(filename); | |
Parse(directories); | |
} | |
private void Parse(IEnumerable<Directory> directories) | |
{ | |
// 日付・時刻 | |
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault(); | |
var ifdo = directories.OfType<ExifIfd0Directory>().FirstOrDefault(); | |
var dateTime = subIfdDirectory?.GetDescription(ExifDirectoryBase.TagDateTimeOriginal); | |
var timeZone = subIfdDirectory?.GetDescription(ExifDirectoryBase.TagTimeZoneOriginal); | |
if(dateTime == null) | |
{ | |
dateTime = ifdo?.GetDescription(ExifDirectoryBase.TagDateTimeOriginal); | |
} | |
// .mov / .mp4 の場合 | |
if(dateTime == null) | |
{ | |
var str = GetString(directories, "QuickTime Movie Header", "Created"); | |
if (str != null) | |
{ | |
var utc = DateTime.ParseExact(str.Substring(str.IndexOf(" ")+1), | |
"M d HH:mm:ss yyyy", | |
System.Globalization.DateTimeFormatInfo.InvariantInfo, | |
System.Globalization.DateTimeStyles.None); | |
var local = TimeZoneInfo.ConvertTimeFromUtc(utc,TimeZoneInfo.Local); | |
var offset = local - utc; | |
dateTime = local.ToString("yyyy:MM:dd HH:mm:ss"); | |
timeZone = $"{offset.Hours:+0,0;-0,0;+0,0}:{offset.Minutes:00}"; | |
} | |
} | |
if (dateTime != null) | |
{ | |
Date = DateTime.ParseExact(dateTime, | |
dateTimeFormats, | |
System.Globalization.DateTimeFormatInfo.InvariantInfo, | |
System.Globalization.DateTimeStyles.None); | |
} | |
if(!string.IsNullOrEmpty(timeZone)) | |
{ | |
TimeZone = timeZone; | |
} | |
// 緯度・経度・高度・方位角 | |
var gps = directories.OfType<GpsDirectory>().FirstOrDefault(); | |
var latitude = gps?.GetDescription(GpsDirectory.TagLatitude); | |
var longitude = gps?.GetDescription(GpsDirectory.TagLongitude); | |
var altitudeRef = gps?.GetDescription(GpsDirectory.TagAltitudeRef); | |
var altitude = gps?.GetDescription(GpsDirectory.TagAltitude); | |
var direction = gps?.GetDescription(GpsDirectory.TagImgDirection); | |
if (latitude != null && longitude != null) | |
{ | |
Latitude = GetDecimal(latitude); // 方位により符号が異なる | |
Longitude = GetDecimal(longitude); // 方位により符号が異なる | |
} | |
if (altitudeRef != null && altitude != null) | |
{ | |
Altitude = GetMetres(altitude) * (altitudeRef == "Sea level" ? 1.0 : -1.0); | |
} | |
if (direction != null) | |
{ | |
Azimuth = GetAzimuth(direction); | |
} | |
// .mov / .mp4 の場合 | |
if (!HasValue(Latitude)) | |
{ | |
var str = GetString(directories, "QuickTime Metadata Header", "GPS Location"); | |
if (str != null) | |
{ | |
MatchCollection matches = Regex.Matches(str, @"[+-]?\d+(?:\.\d+)?"); | |
if(matches.Count >= 2) | |
{ | |
Latitude = Convert.ToDouble(matches[0].Value); | |
Longitude = Convert.ToDouble(matches[1].Value); | |
} | |
if(matches.Count == 3) | |
{ | |
Altitude = Convert.ToDouble(matches[2].Value); | |
} | |
} | |
} | |
// 加速度センサ | |
var apple = directories.Where(x => x.Name == "Apple Makernote").FirstOrDefault(); | |
var acceleration = apple?.Tags.Where(x => x.Name == "Acceleration Vector").FirstOrDefault(); | |
var orientation = ifdo?.GetDescription(ExifDirectoryBase.TagOrientation); | |
if (acceleration != null && orientation != null) | |
{ | |
Rotation = GetVector3(acceleration.Description, orientation); | |
} | |
} | |
private double GetDecimal(string deg) | |
{ | |
double result = double.MinValue; | |
int p0 = deg.IndexOf("°"); | |
int p1 = deg.IndexOf("\'"); | |
int p2 = deg.IndexOf("\""); | |
if(p0 > 0 && p1 > p0 && p2 > p1) | |
{ | |
double h = Convert.ToDouble(deg.Substring(0, p0).Trim()); | |
double m = Convert.ToDouble(deg.Substring(p0 + 1, p1 - p0 - 1).Trim()) / 60.0; | |
double s = Convert.ToDouble(deg.Substring(p1 + 1, p2 - p1 - 1).Trim()) / 3600.0; | |
if(h > 0) | |
{ | |
result = h + m + s; | |
} | |
else | |
{ | |
result = h - m - s; | |
} | |
} | |
return result; | |
} | |
private string GetString(IEnumerable<Directory> directories, string directoryName, string tag) | |
{ | |
var dir = directories.Where(x => x.Name == directoryName).FirstOrDefault(); | |
var str= dir?.Tags.Where(x => x.Name == tag).FirstOrDefault()?.Description; | |
return str; | |
} | |
private double GetMetres(string alt) | |
{ | |
double result = double.MinValue; | |
int p = alt.IndexOf("metre"); | |
if (p > 0) | |
{ | |
result = Convert.ToDouble(alt.Substring(0, p).Trim()); | |
} | |
return result; | |
} | |
private double GetAzimuth(string dir) | |
{ | |
double result = double.MinValue; | |
int p = dir.IndexOf("degree"); | |
if(p > 0) | |
{ | |
result = Convert.ToDouble(dir.Substring(0, p).Trim()); | |
} | |
return result; | |
} | |
private Vector3 GetVector3(string acc, string orientation) | |
{ | |
Vector3 a = new Vector3 { X = float.MinValue, Y = float.MinValue, Z = float.MinValue }; | |
string[] s = acc.Split(','); | |
if (s.Length != 3) | |
{ | |
return a; | |
} | |
for (int i = 0; i < 3; i++) | |
{ | |
string[] t = s[i].Trim().Split(' '); | |
float f = GetFloat(t[0]); | |
switch (t[1].Trim()) | |
{ | |
case "up": | |
a.X = -f; | |
break; | |
case "down": | |
a.X = f; | |
break; | |
case "left": | |
a.Y = -f; | |
break; | |
case "right": | |
a.Y = f; | |
break; | |
case "forward": | |
a.Z = -f; | |
break; | |
case "backward": | |
a.Z = f; | |
break; | |
default: | |
break; | |
} | |
} | |
// Unity におけるカメラの Rotation に合わせる | |
// 加速度センサでは方位はわからないため y は常に 0 | |
// y には必要に応じて Azimuth を利用する | |
float x = 0f; | |
float z = 0f; | |
switch (orientation[0]) | |
{ | |
case 'T': | |
x = (float)(Math.Atan2(a.Z, a.Y) * 180.0 / Math.PI); | |
z = (float)(Math.Atan2(a.X, a.Y) * 180.0 / Math.PI); | |
break; | |
case 'R': | |
x = (float)(Math.Atan2(a.Z, -a.X) * 180.0 / Math.PI); | |
z = (float)(Math.Atan2(a.Y, -a.X) * 180.0 / Math.PI); | |
break; | |
case 'B': | |
x = (float)(Math.Atan2(a.Z, -a.Y) * 180.0 / Math.PI); | |
z = (float)(Math.Atan2(-a.X, -a.Y) * 180.0 / Math.PI); | |
break; | |
case 'L': | |
x = (float)(Math.Atan2(a.Z, a.X) * 180.0 / Math.PI); | |
z = (float)(Math.Atan2(-a.Y, a.X) * 180.0 / Math.PI); | |
break; | |
default: | |
break; | |
} | |
Vector3 result = new Vector3(x, 0, z); | |
return result; | |
} | |
private float GetFloat(string str) | |
{ | |
string s = str.Trim(); | |
return Convert.ToSingle(s.Substring(0, s.Length - 1)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using MetadataExtractor; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace metadata | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
string path = args[0]; | |
IEnumerable<Directory> directories = ImageMetadataReader.ReadMetadata(path); | |
foreach (var directory in directories) | |
{ | |
foreach (var tag in directory.Tags) | |
{ | |
Console.WriteLine($"{directory.Name} - {tag.Name} = {tag.Description}"); | |
} | |
} | |
Console.WriteLine(); | |
PhotoData photoData = new PhotoData(path); | |
Console.Write("撮影日時 : "); | |
if (PhotoData.HasValue(photoData.Date)) | |
{ | |
Console.WriteLine(photoData.Date.ToString("yyyy/MM/dd HH:mm:ss")); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.Write("TimeZone : "); | |
if (PhotoData.HasValue(photoData.TimeZone)) | |
{ | |
Console.WriteLine(photoData.TimeZone); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.Write("緯度・経度: "); | |
if (PhotoData.HasValue(photoData.Latitude)) | |
{ | |
Console.WriteLine($"{photoData.Latitude},{photoData.Longitude}"); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.Write("高度(m) : "); | |
if (PhotoData.HasValue(photoData.Altitude)) | |
{ | |
Console.WriteLine(photoData.Altitude); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.Write("方位角 : "); | |
if (PhotoData.HasValue(photoData.Azimuth)) | |
{ | |
Console.WriteLine(photoData.Azimuth); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.Write("Rotation : "); | |
if (PhotoData.HasValue(photoData.Rotation)) | |
{ | |
Console.WriteLine(photoData.Rotation); | |
} | |
else | |
{ | |
Console.WriteLine("No data"); | |
} | |
Console.ReadLine(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment