Creating a Leaflet map from phone photo points

A simple tutorial on turning geotagged phone photos into an interactive map in R

Published

March 25, 2026

1 Introduction

If location tagging is turned on when you take photos in the field, your phone can save the geographic coordinates of each image in its EXIF metadata. That means each photo can be treated as point data.

In this tutorial you will learn how to:

  1. Turn on geotagging on your phone before taking photos.
  2. Read a folder of geotagged photos into R.
  3. Extract latitude and longitude from the photo metadata.
  4. Build a clean table of photo points.
  5. Edit the captions for each photo point.
  6. Create a leaflet map where each point opens a popup with a caption and photo.

This can be useful for field validation work, documenting site visits, and creating simple maps to communicate project work in workshops and presentations.

2 Before you begin - turn on geotagging on your phone

Your phone must save location information with each photo. If this setting is turned off, you will not be able to extract locational information from your photo metadata.

2.1 iPhone

On iPhone, check the following:

  1. Open Settings
  2. Go to Privacy & Security
  3. Open Location Services
  4. Make sure Location Services is on
  5. Scroll down to Camera
  6. Set location access to While Using the App

2.2 Android

The exact steps vary by device, but typically:

  1. Open the Camera app
  2. Open Settings
  3. Look for an option such as:
    • Location tags
    • Save location
    • Geotagging
  4. Turn it on

After that, take a few test photos and then upload them or move them into a single folder on your computer.

3 Setup - load packages

Install packages first if needed.

A note on the required packages:

  • exifr - enables extraction of EXIF locational data from photos
  • dplyr - facilitates data handling workflows
  • magick - enables image processing and format conversion
  • leaflet - enables you to create interactive webmaps
install.packages(c("exifr", "dplyr", "magick", "leaflet"))

Now load them.

library(exifr)
library(dplyr)
library(magick)
library(leaflet)

4 Point to your photo folder

Create one folder containing the field photos you want to map.

Check your working directory with getwd(), and then set your working directory if needed.

getwd()
setwd("")

Load your photo folder into R.

# Here, we load our photo folder and assign it to the object 'photo_dir'
photo_dir <- "example_folder"

Now list the photo files in that folder.

# Here, we tell R to list all files in 'photo_dir' with format heic, jpg, or jpeg,
# and assign them to a new object called 'photo_files'
photo_files <- list.files(
  photo_dir,
  pattern = "\\.(heic|jpg|jpeg)$",
  full.names = TRUE,
  ignore.case = TRUE
)

length(photo_files)
head(photo_files)

Note
Some phones save photos as .jpg or .jpeg, while others may use .heic. The workflow below handles both. JPG files can be used directly. HEIC files are converted to JPG before they are used in map popups.

5 Read the photo metadata

We now extract the EXIF metadata from the photo files.

Note
EXIF stands for Exchangeable Image File Format, and is typically the metadata embedded in photos including date, time, camera settings and, if enabled, location.

# Using our exifr package, we read the photo metadata
meta1 <- read_exif(photo_files)

dim(meta1)
names(meta1)

To inspect only the GPS-related fields:

# Since we are only interested in the locational data, we tell R to find column names
# that include the words 'GPS', 'Lat', or 'Lon'
meta1[, grepl("GPS|Lat|Lon", names(meta1), ignore.case = TRUE), drop = FALSE]

At this point you should usually see fields such as:

  • GPSLatitude
  • GPSLongitude
  • GPSLatitudeRef
  • GPSLongitudeRef

Let’s check what we’re working with. If those fields are empty, the photos were probably not geotagged.

head(meta1)

6 Extract latitude and longitude

In many cases, read_exif() already returns the GPS coordinates in decimal degrees.
If your GPSLatitude and GPSLongitude columns already contain decimal numbers, you can use them directly.

meta1$lat <- meta1$GPSLatitude
meta1$lon <- meta1$GPSLongitude

Now keep only the photos that have usable coordinates.

meta1 <- meta1 |>
  filter(!is.na(lat), !is.na(lon))

Check the result:

head(meta1[, c("GPSLatitude", "GPSLongitude", "lat", "lon")])
summary(meta1[, c("lat", "lon")])

7 Make a first quick map

Before adding images and captions, it is a good idea to check that the points plot in the right place.

leaflet(meta1) |>
  addProviderTiles(providers$Esri.WorldImagery) |>
  addMarkers(
    lng = ~lon,
    lat = ~lat
  )

If the points appear in the right location, your coordinates are working correctly.

8 Prepare image paths for the popups

For the popup map, we want each point to link to an image file that the webpage can load.

  • If a photo is already .jpg or .jpeg, we use it directly.
  • If a photo is .heic, we convert it to .jpg.

The key output of this step is a new column called image_path. This is the relative path that the popup will use to display the photo.

If your photos are already in jpg/jpeg format, you can still run this step and image_path will be set to your original photo folder name, not the files in the jpg folder we create.

# Here we create a jpg folder for any converted HEIC files
jpg_dir <- file.path(photo_dir, "jpg")
dir.create(jpg_dir, showWarnings = FALSE)

meta1$image_path <- NA_character_

for (i in seq_len(nrow(meta1))) {
  f <- meta1$SourceFile[i]
  ext <- tolower(tools::file_ext(f))

  if (ext %in% c("jpg", "jpeg")) {
    meta1$image_path[i] <- file.path("example_folder", basename(f))
    # Replace 'example_folder' with the name of your photo folder

  } else if (ext == "heic") {
    img <- image_read(f)

    out_name <- paste0(tools::file_path_sans_ext(basename(f)), ".jpg")
    out_file <- file.path(jpg_dir, out_name)

    image_write(img, path = out_file, format = "jpeg")
    meta1$image_path[i] <- file.path("example_folder/jpg", out_name)
    # Replace 'example_folder' with the name of your photo folder.
    # Keep 'jpg' the same, as this is the name of the folder we create above.
  }
}

Check the result:

head(meta1[, c("SourceFile", "image_path")])

You should now see a new image_path column that points to the image file the popup will use.

9 Build a clean photo point table

The raw metadata table contains many columns we do not need for mapping. It is often cleaner to build a smaller table containing just the fields we want to map.

photo_points <- data.frame(
  photo_name = tools::file_path_sans_ext(basename(meta1$SourceFile)),
  lat        = meta1$lat,
  lon        = meta1$lon,
  image_path = meta1$image_path,
  stringsAsFactors = FALSE
)

# By default, use photo name as the first caption
photo_points$caption <- photo_points$photo_name

head(photo_points)

At this point you have a clean table with:

  • a photo name
  • latitude
  • longitude
  • the image path used in the popup
  • a default caption

10 Edit the captions for each photo point

The default caption is just the photo file name. In practice, you will usually want to replace that with your own field notes or descriptions.

You can edit the captions directly in R like this:

# Here we are telling R to use the annotation (example captions on right hand side, replace these with your own) as the caption for photo. 
# The number in the square brackets [] is the photo point we are assigning the caption to
photo_points$caption[1] <- "Young tree regeneration near the plot boundary"
photo_points$caption[2] <- "Gully erosion behind the homestead"
photo_points$caption[3] <- "Farmer-managed natural regeneration plot"

# Check your photo and captions
photo_points[, c("photo_name", "caption")]

You can continue this pattern for the rest of your points.

11 Build the popup content

We now create HTML popup text for each point.

# Here we are telling R to build a pop-up bubble for our photo points. You can change the styling by playing with the html below.
photo_points$popup_html <- paste0(
  "<div style='width:260px;'>",
  "<b>", photo_points$caption, "</b><br>",
  "<img src='", photo_points$image_path, "' style='width:100%; border-radius:8px; margin-top:6px;'>",
  "</div>"
)

This popup includes:

  • a caption
  • the linked image file

You could also add more information here, such as date, site name, or notes from the field.

12 Build the final leaflet map

Now we can map the points and attach the popups.

leaflet(photo_points) |>
  addProviderTiles(providers$Esri.WorldImagery) |>
  setView(
    lng = mean(photo_points$lon, na.rm = TRUE),
    lat = mean(photo_points$lat, na.rm = TRUE),
    zoom = 18
  ) |>
  addCircleMarkers(
    lng = ~lon,
    lat = ~lat,
    radius = 6,
    fillColor = "orange",
    color = "white",
    weight = 1,
    fillOpacity = 0.9,
    popup = ~popup_html
  )

This gives you a clickable photo-point map over satellite imagery.


13 Optional: save the clean table for later use

If you want to reuse the prepared point data later, you can save the clean table to CSV.

# Replace the designated output folder with your own filepath
write.csv(photo_points, "data/photo_points.csv", row.names = FALSE)

This can be useful if you want to build a rendered map later from a prepared CSV rather than repeating the full EXIF extraction workflow, or if you want to share with colleagues along with the photo folder.

14 Summary

In this tutorial you learned how to:

Task Key function
Read photo metadata read_exif()
Check point locations quickly leaflet() + addMarkers()
Prepare image paths for popups file.path() + image_write()
Build a clean photo-point table data.frame()
Build popup HTML paste0()
Map photo points interactively leaflet() + addCircleMarkers()

14.1 Things to try next

  • Add the photo date to the popup.
  • Use a different basemap, such as CartoDB.Positron.
  • Filter the points to a single site visit or field day.
  • Save the clean table and use it to build a rendered map in a separate page.
Back to top