Skip to content

Instantly share code, notes, and snippets.

@ksasao
Last active September 14, 2021 01:56
Show Gist options
  • Save ksasao/934851ff2cfead8d6656027bf6b368cf to your computer and use it in GitHub Desktop.
Save ksasao/934851ff2cfead8d6656027bf6b368cf to your computer and use it in GitHub Desktop.
HEIF (.heic), JPEG などに含まれる EXIF 情報などから緯度経度・時刻・姿勢(Unity準拠)を読み込む C# サンプル。 https://twitter.com/ksasao/status/1426522324623265800
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));
}
}
}
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