Как стать автором
Обновить

Geotagging — привязка фотографий к карте

Время на прочтение9 мин
Количество просмотров39K
Уверен что про geotagging слышало подавляющее большинство хабраюзеров, особенно те кто интересуется фотографией. Для тех же, кто все таки не слышал поясню — в двух словах geotagging есть внедрение Exif тегов содержащих информацию с координатами GPS в фотографии с последующей привязкой фотографий к карте.

Возможность просмотра привязанных к картам фотографий предоставляет большинство современных фото-хостингов — PicasaWeb, Flickr, Яндекс-Фотки и прочие, да и десктопные программы подтягиваются, например Picasa.



Идеальный вариант для геотеггинга это приобретение специальных GPS-логгеров, тем более что цены на них в последнее время постоянно падают. Принцип действия GPS-логгеров прост до безобразия — устройство периодически сбрасывает текущие координаты на карту памяти, параллельно фотографируются фотки (желательно с включенной функцией «шедевр»). После фотосессии на фотки и трек с гео-логгера натравливается спец-софт, прописывающий в фотографии GPS координаты, синхронизируясь по времени.

Можно также использовать для этих целей коммуникатор с GPS — алгоритм аналогичен, хотя и не без минусов. Например, время работы коммуникатора, учитывая то что он не специально заточен под эти цели, оставляет желать лучшего. Но что делать если волшебного девайса нету, а фотки привязать к карте все-таки хочется?

Если все же сильно хочется, то можно залить фотографии на фотохостинг и вручную привязать их к карте, минус данного способа в том что на диске остаются фотографии без координат, да и делать что-то вручную не достойно правоверных айтишников/программистов с обостренной врожденной ленью.

Учитывая все вышенаписанное, было потрачено около часа времени на написание простого кода выдирающего GPS координаты с карты при щелчке мыши и прописывающего их в Exif фотографии.

Итак, задачу можно разделить на два этапа:
1. Вывод карты на экран и определение GPS координат.
2. Добавление тегов с GPS координатами в Exif информацию JPEG файла.

Вывод карты и определение GPS координат



Решить данную задачу при помощи Google Maps API гораздо проще чем может показаться на первый взгляд.

Создадим HTML файл с компонентом Google Maps, растянутым на весь экран без марджинов:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
  3. <head>
  4.   <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  5.   <title></title>
  6.   <style type="text/css">
  7.     body, html
  8.     {
  9.       padding: 0;
  10.       margin: 0;
  11.     }
  12.     #map_canvas
  13.     {
  14.       position: absolute;
  15.       overflow: auto;
  16.       margin: 0;
  17.       width: 100%;
  18.       height: 100%;
  19.     }
  20.   </style>
  21.  
  22.   <script src="http://maps.google.com/maps?file=api&v=2&    key=ABQIAAAAzr2EBOXUKnm_jVnk0OJI7xSosDVG8KKPE1-m51RBrvYughuyMxQ-i1QfUnH94QxWIa6N4U6MouMmBA"
  23.     type="text/javascript"></script>
  24.  
  25.   <script type="text/javascript">
  26.   var map;
  27.  
  28.   function initialize() {
  29.     // Настройки карты
  30.     var mapOptions = {
  31.       
  32.     }
  33.  
  34.     // Создание карты
  35.     map = new GMap2(document.getElementById("map_canvas"), mapOptions);
  36.     map.setCenter(new GLatLng(40.1876601,44.5190477), 15);
  37.     map.setUIToDefault();    
  38.   }
  39.  
  40.   </script>
  41.  
  42. </head>
  43. <body onload="initialize()">
  44.   <div id="map_canvas">
  45.   </div>
  46. </body>
  47. </html>
* This source code was highlighted with Source Code Highlighter.


* Ключ для использования Google Maps API можно получить здесь, в примере используется ключ используемый в примерах в документации Maps API.

Загрузим файл в компонент WebBrowser (для удобства я поместил файл в ресурсы проекта):

  1. webBrowser.AllowWebBrowserDrop = false;
  2. webBrowser.IsWebBrowserContextMenuEnabled = false;
  3. webBrowser.WebBrowserShortcutsEnabled = false;
  4. webBrowser.ObjectForScripting = this;
  5. webBrowser.DocumentText = Resources.google;
* This source code was highlighted with Source Code Highlighter.


Скомпилировав приложение на данном этапе, увидим следующее:



Добавим к компоненту карты возможность поиска, для этого включим GoogleBar, добавив следующий код к функции initialize():

  1. var mapOptions = {
  2.   // Настройки строки поиска
  3.   googleBarOptions : {
  4.     style : "new"
  5.   }
  6. }
  7.  
  8. // Включаем строку поиска
  9. map.enableGoogleBar();
* This source code was highlighted with Source Code Highlighter.


Теперь добавим обработчик клика мыши на карте, для этого определим следующую функцию:

  1. // Обработчик клика по карте
  2. function getLatLng(overlay,latlng) {
  3.   if (latlng != null) {
  4.     
  5.     // Добавим pin с координатами на карту
  6.     map.clearOverlays();
  7.     marker = new GMarker(latlng);
  8.     map.addOverlay(marker);
  9.     marker.openInfoWindowHtml('<b>Latitude:</b>' + latlng.lat() + '<br>' + '<b>Longitude:</b>' + latlng.lng());
  10.   }
  11. }
* This source code was highlighted with Source Code Highlighter.


И соответствюший listener в функцию initialize():

  1. GEvent.addListener(map, "click", getLatLng);
* This source code was highlighted with Source Code Highlighter.


Функция getLatLng() довольно проста. Сначала мы вычищаем все оверлеи с карты, создаем новый маркер с координатами места где пользователь кликнул по карте, ну и наконец, из эстетических соображений выводим popup с координатами.

Вот что у нас получилось:



Все выглядит красиво и юзабельно, но как же передать данные с координатами в приложение-хост (WinForms с компонентом WebBrowser)?

Для этого можно воспользоваться свойством WebBrowser.ObjectForScripting. Обьект присвоенный этому свойству доступен скриптам в странице как window.external.
Замечу что обьект присваемый свойству ObjectForScripting (в данном случае обьект Form содержащий компонент WebBrowser) должен быть помечен аттрибутом ComVisible и в скриптах будут доступны только public методы и свойства.

Итак, добавим в функцию getLatLng() следуюшую строку:

  1. // Передача координат в хост-приложение
  2. window.external.SetValue(latlng.lat() + '|' + latlng.lng());
* This source code was highlighted with Source Code Highlighter.


Как я уже сказал, доступ к обьекту присвоенному ObjectForScripting осуществляется через window.external, следовательно чтобы добавленный код заработал надо добавить в класс формы public метод SetValue(string):

  1. public void SetValue(string value)
  2. {
  3.   string[] coordinates = value.Split('|');
  4.  
  5.   _latitude = new GPSCoordinate(
  6.     Convert.ToDouble(coordinates[0]),
  7.     GPSCoordinateType.Latitude);
  8.  
  9.   _longitude = new GPSCoordinate(
  10.     Convert.ToDouble(coordinates[1]),
  11.     GPSCoordinateType.Longitude);
  12.  
  13.   lblLatitude.Text = _latitude.ToString();
  14.   lblLongitude.Text = _longitude.ToString();
  15. }
* This source code was highlighted with Source Code Highlighter.


Небольшое отступление, в методе SetValue используется класс GPSCoordinates.

Класс GPSCoordinates



Google Maps выдает координаты в десятичной форме, в Exif данные пишутся в виде массива байтов (об этом — ниже).
Класс GPSCoordinates переводит десятичные GPS координаты в формат DMS (Degrees, Minutes, Seconds).
Метод GetExifBytes() возвращает массив байтов подходящий для записи в Exif, об этом — в следующей части статьи.

  1. internal class GPSCoordinate
  2. {
  3.   private readonly double _coord;
  4.   public byte Degrees { get; set; }
  5.   public byte Minutes { get; set; }
  6.   public double Seconds { get; set; }
  7.   public char Reference { get; private set; }
  8.  
  9.   public GPSCoordinate(double coord, GPSCoordinateType type)
  10.   {
  11.     _coord = Math.Abs(coord);
  12.  
  13.     Degrees = (byte)_coord;
  14.     double minutes = (_coord - Degrees) * 60;
  15.     Minutes = (byte)minutes;
  16.     Seconds = Math.Round((minutes - Minutes) * 60, 2);
  17.  
  18.     if (type == GPSCoordinateType.Latitude)
  19.       Reference = coord < 0 ? 'S' : 'N';
  20.     else
  21.       Reference = coord < 0 ? 'W' : 'E';
  22.   }
  23.  
  24.  
  25.   public override string ToString()
  26.   {
  27.     return string.Format("{0} {1}° {2}' {3}\"", Reference, Degrees, Minutes, Seconds);
  28.   }
  29.  
  30.   public byte[] GetExifBytes()
  31.   {
  32.     var bytes = new byte[24];
  33.     for (int i = 0; i < 24; i++)
  34.       bytes[i] = 0;
  35.     
  36.     bytes[0] = Degrees;
  37.     bytes[4] = 1;
  38.     bytes[8] = Minutes;
  39.     bytes[12] = 1;
  40.  
  41.     int seconds = (int) (Seconds*100);
  42.     
  43.     byte[] temp = BitConverter.GetBytes(seconds);
  44.     Array.Copy(temp, 0, bytes, 16, 4);
  45.     bytes[20] = 100;
  46.     
  47.     return bytes;
  48.   }
  49. }
  50.  
  51. internal enum GPSCoordinateType
  52. {
  53.   Latitude,
  54.   Longitude
  55. }
* This source code was highlighted with Source Code Highlighter.




Итак на данный момент мы имеем рабочую карту с поиском и вытягиванием координат в удобоваримом для дальнейших действий виде. Осталось прописать эти данные в Exif заголовок JPEG файла.

Добавление тегов в JPEG файл



Спецификация Exif 2.2 определяет кучу GPS тегов, однако для нашей задачи нам достаточно воспользоваться следуюшими:

Tag Name Field Name ID (Dec) ID (Hex) Type Bytes Count
GPS tag version GPSVersionID 0 0 BYTE 4
North or South Latitude GPSLatitudeRef 1 1 ASCII 2
Latitude GPSLatitude 2 2 RATIONAL 3
East or West Longitude GPSLongitudeRef 3 3 ASCII 2
Longitude GPSLongitude 4 4 RATIONAL 3


Тип RATIONAL определяется в спецификации как два LONG числа, первое из которых — числитель, второе — знаменатель.
Т.е. каждый из DMS компонентов координаты записывается в виде 8-байтового Rational, что собственно и делает метод GPSCoordinate.GetExifBytes().

В .Net до Exif тегов можно добраться при помощи свойства Image.PropertyItems, а также методов Image.GetPropertyItem(int) и Image.SetPropertyItem(PropertyItem). Казалось бы можно создать новый обьект PropertyItem присвоить все значения и добавить к имеющимся в файле используя метод SetPropertyItem(PropertyItem).

Но не тут-то было, по каким-то, несомненно веским причинам, Microsoft закрыл конструктор данного типа, поэтому создать новый обьект при помощи new не получится.

Данное ограничение можно, конечно, обойти при помощо рефлексии (reflection), но производительность этого подхода оставляет желать лучшего. Есть и другой метод обхода — создать JPEG файл-«заглушку» с GPS тегами, потом брать их из него при помощи GetPropertyItem, менять данные на нужные нам и добавлять в новый файл.

Данный код демонстрирует этот подход (файл-«заглушка» для удобства перемещен в ресурсы проекта):

  1. private void btnWriteTag_Click(object sender, EventArgs e)
  2. {
  3.   if (_image == null || _latitude == null || _longitude == null)
  4.     return;
  5.  
  6.   // GPS Tag Version
  7.   PropertyItem pitem = CreateNewPropertyItem(0x0);
  8.   pitem.Value[0] = 2;
  9.   pitem.Value[1] = 2;
  10.   _image.SetPropertyItem(pitem);
  11.  
  12.   // Latitude
  13.   pitem = CreateNewPropertyItem(0x2);
  14.   pitem.Value = _latitude.GetExifBytes();
  15.   _image.SetPropertyItem(pitem);
  16.  
  17.   // LatitudeRef (North or South)
  18.   pitem = CreateNewPropertyItem(0x1);
  19.   pitem.Value[0] = (byte)_latitude.Reference;
  20.   _image.SetPropertyItem(pitem);
  21.  
  22.   // Longitude
  23.   pitem = CreateNewPropertyItem(0x4);
  24.   pitem.Value = _longitude.GetExifBytes();
  25.   _image.SetPropertyItem(pitem);
  26.  
  27.   // LatitudeRef (East or West)
  28.   pitem = CreateNewPropertyItem(0x3);
  29.   pitem.Value[0] = (byte)_longitude.Reference;
  30.   _image.SetPropertyItem(pitem);
  31. }
  32.  
  33. private static PropertyItem CreateNewPropertyItem(int id)
  34. {
  35.   return Resources.gps_jpg.GetPropertyItem(id);      
  36. }
* This source code was highlighted with Source Code Highlighter.


Конечно оба метода (описаный выше, и на основе рефлексии) не очень эстетичны, может в будущем MS даст возможность создавать обьекты типа PropertyItem и эти костыли более не понадобятся.

Нам осталось только сохранить файл с новыми тегами:

  1. if (_saveFileDialog.ShowDialog() == DialogResult.OK)
  2. {
  3.   _image.Save(_saveFileDialog.FileName);
  4. }
* This source code was highlighted with Source Code Highlighter.


Кстати, данные изменения не приводят к необходимости перекодирования файла, т.к. они касаються только мета-информации.

Эпилог



Ну вот, собственно, и все. Остается добавить что координаты записаные в полученые в итоге файлы файлы спокойно определяются Flickr-ом и Яндекс-Фотками, почему-то PicasaWeb не хочет понимает координаты, хотя у десктопной Picasa таких проблем нет.
Покопавшись в сети нашел много баг-репортов с той же проблемой, скорее всего проблема у PicasaWeb, хотя никто кроме страждущих в баг-репортах не отписывался.

Исходники проекта можно скачать тут.

ВНИМАНИЕ: В примере отсутствуют проверки на все что возможно, код был написан за час и не является готовой программой для геотеггинга, скорее proof-of-concept, так что делайте бекапы. За сохранность фоток редакция ответственности не несет ;)

P.S. Кстати, есть отличная тулза для геотеггинга — GeoSetter, понимает кучу форматов, в том числе и RAW (хотя заслуга тут скорее небезызвестного ExifTool написанного Phil Harvey).
Теги:
Хабы:
Всего голосов 56: ↑38 и ↓18+20
Комментарии14

Публикации

Истории

Работа

.NET разработчик
62 вакансии

Ближайшие события

Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
AdIndex City Conference 2024
Дата26 июня
Время09:30
Место
Москва
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область