Maps and geographical data have always been a passing interest for me, from reading and following printed maps (yes I grew up with a Thomas Guide and could actually navigate before mapquest) wanting to and failing to draw fantasy maps, and just generally enjoying working with data systems. I’ve worked with postgis in the past, but nothing fancy, mostly zip code math off of the TIGER shapefiles, but I’ve been wanting to find a project to explore GIS more deeply with, specifically QGIS. While playing around with Immich’s map function, I realized I had an intereting geospatial dataset of my own, right in front of me. I spent a couple of minutes looking around and didn’t find any obvous knobs to get simple meta-data exports of what I wanted, so I turned to the API. The docs gave me a couple of ideas, before I realized that the map marker endpoint would basically hand me everything I needed, I just needed to wrap it in a bit of json and call it a GeoJSON. Almost instantly I had a GeoJSON file with 41k points in it, that imported cleanly into QGIS.. all from my own data generated with and for open source software.
From there I was able to get the points added as a layer, generate some heatmaps, include urls for thumbnails, original image links, etc.
Without a basemap it even kinda looks like the US if you squint!
Plot of my photo locations with no basemap

Heatmap of roughly Santa Barbara County
Also while scrolling around playing ‘GPS anomoly or actual photo in random area I found a fun string of rainbow photos we took driving (I was passenging, don’t worry) northbound on the 101:
Line of photos taken on the highway

Full horizon rainbow
And since it worked, here’s the script if others might find it useful.
#!/usr/bin/env python3
"""
Immich Location Data Exporter
Query an Immich server and export image location data in GeoJSON format for QGIS
"""
import requests
import json
import sys
from typing import List, Dict
class ImmichLocationExporter:
def __init__(self, server_url: str, api_key: str):
"""
Initialize the Immich exporter
Args:
server_url: Base URL of the Immich server (e.g., 'http://localhost:2283')
api_key: API key for authentication
"""
self.server_url = server_url.rstrip('/')
self.api_key = api_key
self.headers = {
'x-api-key': api_key,
'Accept': 'application/json'
}
def get_map_markers(self) -> List[Dict]:
"""
Fetch map markers from the Immich server (assets with location data only)
Returns:
List of marker dictionaries with location data
"""
print(f"Fetching map markers from {self.server_url}...", file=sys.stderr)
url = f"{self.server_url}/api/map/markers"
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
markers = response.json()
print(f"Retrieved {len(markers)} markers with location data", file=sys.stderr)
return markers
except requests.exceptions.RequestException as e:
print(f"Error fetching map markers: {e}", file=sys.stderr)
print(f"Make sure your Immich server supports the map API", file=sys.stderr)
sys.exit(1)
def create_geojson(self, markers: List[Dict]) -> Dict:
"""
Convert map markers to GeoJSON format
Args:
markers: List of map markers with location data
Returns:
GeoJSON FeatureCollection dictionary
"""
features = []
for marker in markers:
lat = marker.get('lat')
lon = marker.get('lon')
# Skip if missing coordinates, no one wants to go to null island today
if lat is None or lon is None:
continue
asset_id = marker.get('id')
# Build URLs only if we have both server URL and asset ID
web_url = None
thumbnail_url = None
original_url = None
if asset_id:
web_url = f"{self.server_url}/photos/{asset_id}"
thumbnail_url = f"{self.server_url}/api/assets/{asset_id}/thumbnail"
original_url = f"{self.server_url}/api/assets/{asset_id}/original"
properties = {
'id': asset_id,
'web_url': web_url,
'thumbnail_url': thumbnail_url,
'original_url': original_url,
'city': marker.get('city'),
'state': marker.get('state'),
'country': marker.get('country'),
}
properties = {k: v for k, v in properties.items() if v is not None}
feature = {
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [lon, lat] # GeoJSON uses [longitude, latitude]
},
'properties': properties
}
features.append(feature)
# Create GeoJSON FeatureCollection
geojson = {
'type': 'FeatureCollection',
'features': features,
'crs': {
'type': 'name',
'properties': {
'name': 'EPSG:4326' # WGS84 coordinate system
}
}
}
return geojson
def export_to_geojson(self, output_file: str) -> None:
"""
Export all map markers with location data to a GeoJSON file
Args:
output_file: Path to output GeoJSON file
"""
markers = self.get_map_markers()
if not markers:
print("No markers with location data found!", file=sys.stderr)
return
geojson = self.create_geojson(markers)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(geojson, f, indent=2, ensure_ascii=False)
print(f"\nSuccessfully exported {len(geojson['features'])} locations to {output_file}", file=sys.stderr)
print(f"You can now import this file into QGIS as a vector layer.", file=sys.stderr)
def main():
"""Main entry point"""
import argparse
import os
parser = argparse.ArgumentParser(
description='Export Immich image location data to GeoJSON for QGIS'
)
parser.add_argument(
'server_url',
help='Immich server URL (e.g., http://localhost:2283)'
)
parser.add_argument(
'api_key',
nargs='?',
help='Immich API key (can also be set via IMMICH_API_KEY environment variable)'
)
parser.add_argument(
'-o', '--output',
default='immich_locations.geojson',
help='Output GeoJSON file (default: immich_locations.geojson)'
)
args = parser.parse_args()
api_key = args.api_key or os.environ.get('IMMICH_API_KEY')
if not api_key:
print("Error: API key must be provided either as an argument or via IMMICH_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
exporter = ImmichLocationExporter(args.server_url, api_key)
exporter.export_to_geojson(args.output)
if __name__ == '__main__':
main()
As always, hit me up on mastodon if you have questions or comments!