用可視化地圖講照片的故事(Python+Leaflet)

  • 2019 年 10 月 8 日
  • 筆記

本文轉載自蟄蟲始航

手機和數位相機拍的照片里除了我們能看到的RGB像元數據,還包含了拍攝時間、影像解析度、感光值、GPS坐標等屬性,記錄在Exif(Exchangeable image file format)模組里。

隨著手機像素越來越高,用手機記錄身邊的事(和自拍)已經變成很自然的動作,在一年裡我們的手機肯定存了很多照片,照片和Exif數據塊中的位置可以做哪些有趣的事情?一張圖片和對應的拍攝位置如果沒那麼多可能性,那一系列照片和位置呢?

我們可以直觀看近些年都去了哪裡;可以製作和(男/女)朋友一起出去玩的地圖故事;可以根據拍照時間和位置動態可視化遊覽路線;可以基於坐標的聚類整理照片,如拍了800張照片,把每個城市的照片批量整理到各自文件夾;……

地理位置屬於個人隱私數據,相關應用需要注意隱私問題,之前挺火的一個謠言是可以根據別人朋友圈發的圖知道別人的具體位置,但實際上微信會對朋友圈的圖片進行壓縮,Exif里的坐標數據是會刪除掉的,所以朋友圈的圖片是提取不了坐標的。以下實踐基於部分自己這些年拍的照片,避免侵犯其他人隱私。

查看照片的Exif屬性信

本文主要做的:批量提照片中的坐標->可視化照片位置->製作遊歷故事地圖

所用到的工具:

  • Python和exifread庫
  • Leaflet和兩個插件

1,批量提取照片中的坐標

照片中的地理坐標記錄在Exif塊里,Exif資訊以0xFFE1作為開頭標記,採用TIFF格式,可以自己解析或直接用輪子exifread庫,exifread是一個很方便使用的讀取tiff和jpeg格式圖片的Python庫,在pypi上的介紹是:

Easy to use Python module to extract Exif metadata from tiff and jpeg files.

通過 pip install exifread安裝後就可以使用了,我們現在只關心照片的坐標和拍攝時間,根據其教程探索參數和用法。

Exifread庫的使用

寫程式碼提取這部分數據:

def extractExif(fpath):#提取照片坐標和拍攝時間函數      try:          with open(fpath,'rb') as rf:              exif=exifread.process_file(rf)          eDate=exif['EXIF DateTimeOriginal'].printable          eLon=exif['GPS GPSLongitude'].printable          eLat=exif['GPS GPSLatitude'].printable          lon=eLon[1:-1].replace(' ','').replace('/',',').split(',')          #'[116, 29, 10533/500]' to [116,29,10533,500]  type==(list)          lon=float(lon[0])+float(lon[1])/60+float(lon[2])/float(lon[3])/3600          lat=eLat[1:-1].replace(' ','').replace('/',',').split(',')          lat=float(lat[0])+float(lat[1])/60+float(lat[2])/float(lat[3])/3600          return [lon,lat,eDate]  #經度,緯度,拍攝時間      except Exception as e:          print(e,fpath)          return None

注意的是如果拍照時沒有讀取地理位置許可權那就不好記錄拍照時的坐標了,所以使用時需要做一個判斷。調用上面的函數批量取一個文件夾下照片的坐標:

wpt='J:/DS_refine/SQL-lyn/exifExtract/image' #圖片文件路徑  latLons=[]  for root, dirs, files in os.walk(wpt):  print(len(files))      for f in files:          exif=extractExif('{0}/{1}'.format(wpt,f))          if exif:              exif[2]=exif[2]+' '+f              latLons.append(exif)          else:              print(f,'exif is None')

有了照片和對應的位置,可以做可視化講故事了。下面的實踐需要了解一些前端HTML和JavaScript知識。

2,在地圖中展示坐標

直接展示地理點坐標有很多工具,百度/高德地圖的API、Echarts、Leaflet、OpenLayers及Mapbox等。

這裡用Leaflet框架和 marker-clustering.js 實現坐標點展示和縮小時點聚合的效果,這樣能適應各種縮放層級。效果如下:

展示照片坐標效果圖

實現方式是在前端的html頁面里引入 leaflet.jsleaflet.markercluster-src.js,對map元素進行配置和設置好坐標數據,把html文件配置好之後,把數據寫入js文件再調用就好。基於1中提取的坐標,保存為js文件,然後在瀏覽器打開html文件,就是上圖中的效果了。另外需要說明的是,這些標記點(marker)點擊之後都是能看到具體的文本的,展示的文本就是title里的內容。

<div id="map" style="width:100%; height:650px;"></div>  <script type="text/javascript">      var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {          maxZoom: 18,          attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'      }),      latlng = L.latLng(37.552897,115.60571); //設置地圖的坐標中心點      var map = L.map('map', {center: latlng, zoom: 5, layers: [tiles]});      var markers = L.markerClusterGroup();        for (var i = 0; i < addressPoints.length; i++) {          var a = addressPoints[i];          var title = a[2];          var marker = L.marker(new L.LatLng(a[0], a[1]), { title: title });          marker.bindPopup(title);          markers.addLayer(marker);      }      map.addLayer(markers);  </script>

所調用文件及結構展示

而把這些坐標放到百度地圖的效果如下:

百度地圖的點坐標可視化

坐標多的話就是密密麻麻的紅點。

註:百度地圖中採用的坐標需要是百度坐標系(bd-09),而我們提取的坐標是GPS坐標,用的是WGS84坐標系,需要做轉換,可以調用coordTransform_py進行轉換,高德地圖採用的是火星坐標系,也需要進行轉換。

只是展示坐標不怎麼有趣,下面做一個左側圖文描述右側可視化坐標的效果。

3,遊歷故事地圖

給那些年去過的地方寫一個地圖遊記。示例效果如下:

那些年去過的地方

還是用之前提取的坐標和Leaflet框架。用到的插件是storymap.js,同樣引用js之後,改變其中的坐標數據,因為是講一個故事,具體內容當然按自己想講述的寫,將 <sectiondata-place="bodo">中的bodo和js程式碼中markers里的bodo對應好就好,例如bodo改為beijing。

var markers = {      beijing: {lat:39.886426, lon: 116.404762, zoom: 6},      tianjin: {lat:39.134594, lon: 117.191961, zoom: 7},      fuyang: {lat:32.645140, lon: 116.268333, zoom: 7},      ningbo: {lat:29.763531, lon: 121.898233, zoom: 8},      liuzhou: {lat:24.313703, lon: 109.406884, zoom: 7}  };

4,整合聚類點到遊歷地圖裡

在我們做的遊歷地圖裡增加點聚類的效果,一個簡單做法是在storymap.js里增加對markercluster.js的調用,從而可以用markerClusterGroup() 重寫基本的marker標記點類型。效果如下:

地圖故事效果圖

在html里可以根據自己的想法增加更多的內容,例如具體的地址文本,只需要調用百度/高德地圖的Web服務 API中的逆地理編碼服務就可以實現,逆地理編碼就是指將經緯度轉換為詳細結構化的地址,如把WGS84坐標系的坐標[116.421046,39.903004]逆地理編碼對應北京市東城區北京站。

也可以繼續探索更多的Leaflet插件。

另外可以換底圖,例如換成Satellite衛星底圖,改map初始化時地圖瓦片圖層的調用url就行 L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png',效果如下,是不是也很生動呢?

用Satellite底圖的效果

空間位置可以做很多分析和很多有趣的事情,Python也是很強大的工具,僅需要發揮想像力。

參考資料

  • ExifRead :便捷讀取Exif的Python庫
  • Leaflet:便捷友好的交互地圖開源js庫
  • markercluster.js:地圖標記點聚類庫,Leaflet插件
  • storymap.js:地圖上的故事,Leaflet插件