Display Uploaded Images

June 17, 2024
Reading Time: 9 minute/s

Recap

From the previous chapter, we learned how to upload images using python and flask. We learned how to restrict the file types accepted by our app.

But so far, the images displayed are hard-coded image data and not the ones we've uploaded. In this article, we will learn how to display our uploaded images.

What to Learn

The following are what we will learn in this chapter:

  • Get all files from a directory
  • Create dictionary for each file
  • Filter only the files with allowed extensions
  • Use the None value type to filter images with geotag data
  • Display images using a safe URL generated from a directory using send_from_directory() function
  • Refactoring

Listing of Files from a Directory

To get the list of files from a directory, we will use the os module to interact with the operating system.

The syntax will look like this:

files_list = os.listdir(<folder_name>)

And since we display the main page in the index route, we will modify it now.

First is we will delete the images list, so from:

@app.route("/")
def index():
    images = [
        {
            "name": "Calle Crisologo",
            "url": "https://ik.imagekit.io/tvlk/blog/2017/11/Calle-Crisologo-by-night-750x469.jpg?tr=dpr-2,w-675,",
            "latitude": 17.57278,
            "longitude": 120.38889
        },
        {
            "name": "Boracay",
            "url": "https://www.rappler.com/tachyon/2022/07/boracay.jpg",
            "latitude": 11.967222,
            "longitude": 121.924722
        },
        {
            "name": "Mayon Volcano",
            "url": "https://gttp.imgix.net/266095/x/0/guide-to-mayon-volcano-in-albay-bicol-world-s-most-perfect-volcanic-cone-4.jpg?auto=compress%2Cformat&ch=Width%2CDPR&dpr=1&ixlib=php-3.3.0&w=883",
            "latitude": 13.254722,
            "longitude": 123.685833
        },
        {
            "name": "Siargao Island",
            "url": "https://www.agoda.com/wp-content/uploads/2020/01/Things-to-do-in-Siargao-Island-Cloud-9-surfing-area-in-General-Luna.jpg",
            "latitude": 9.854167,
            "longitude": 126.040278
        },
        {
            "name": "Mt. Pinatubo",
            "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQs5noQ1YdK7lM-h1KOfQ7opRjb90XSVD_s0Q&s",
            "latitude": 15.143056,
            "longitude": 120.349444
        }
    ]

    number_of_items = len(images)

    # Define empty lists
    latitudes = []
    longitudes = []

    # Loop through the images list
    for image in images:
        latitudes.append(image['latitude'])
        longitudes.append(image['longitude'])

    # Now calculate the averages of latitudes and longitudes
    lat_average = sum(latitudes) / number_of_items
    lon_average = sum(longitudes) / number_of_items

    return  render_template("hello.html",
                            images=images,
                            latitude=lat_average,
                            longitude=lon_average)

to

@app.route("/")
def index():

    number_of_items = len(images)

    # Define empty lists
    latitudes = []
    longitudes = []

    # Loop through the images list
    for image in images:
        latitudes.append(image['latitude'])
        longitudes.append(image['longitude'])

    # Now calculate the averages of latitudes and longitudes
    lat_average = sum(latitudes) / number_of_items
    lon_average = sum(longitudes) / number_of_items

    return  render_template("hello.html",
                            images=images,
                            latitude=lat_average,
                            longitude=lon_average)

Now don't run the app yet because the images variable does not exist and we will get an error.

We will get the files now from our uploads directory:

image_files = os.listdir(app.config['UPLOAD_FOLDER'])

and then let's create an empty images list

image_files = os.listdir(app.config['UPLOAD_FOLDER'])
images = []

Install Necessary Packages for Image Manipulation and Exif Viewer

Let's install a tool that we need to extract geotag data.

pip install exifread

We will then iterate through the image_files list and create an image dictionary for each image.

image_files = os.listdir(app.config['UPLOAD_FOLDER'])
    images = []

    for img in image_files:
        pass

What we will get in the img data is the filename of the image.

Let's enumerate the actions we will do in each iteration.

  1. Check if the image has extension in the allowed extensions
  2. Get the full path to the current image by combining the uploads directory and the image filename
  3. Extract the exif data of an image
  4. Get the latitude and longitude of an image
  5. Check if latitude or longitude exists in the image metadata
  6. Create dictionary for each image in the form
image = {
    'name': <image_name>,
    'latitude': <latitude>,
    'longitude': <longitude>
}

Now let's continue our iteration

image_files = os.listdir(app.config['UPLOAD_FOLDER'])
    images = []

    for img in image_files:
        filename = os.path.join(app.config['UPLOAD_FOLDER'], img)  # Construct the full path

        tags = {}

        with open(filename, 'rb') as image_file:
            tags = exifread.process_file(filename)
        
        # Declare latitude and longitude values as initially null
        lat = None
        lon = None

For simplicity of the code, we will create a helper function that converts a latitude and longitude values into decimal in degress.

def convert_to_degress(value):
    d = float(value[0].num) / float(value[0].den)
    m = float(value[1].num) / float(value[1].den)
    s = float(value[2].num) / float(value[2].den)

    return d + (m / 60.0) + (s / 3600.0)

The above code is a sample function that has a return value.

image_files = os.listdir(app.config['UPLOAD_FOLDER'])
    images = []

    # Iterate through the image_files using for loop
    for img in image_files:
        filename = os.path.join(app.config['UPLOAD_FOLDER'], img)  # Construct the full path

        exif_data = {}

        with open(filename, 'rb') as image_file:
            exif_data = exifread.process_file(filename)  # Place the exif data into the `tags` variable
        
        # Declare latitude and longitude values as initially null
        lat = None
        lon = None

        # Extract the coordinates
        if 'GPS GPSLatitude' in exif_data and 'GPS GPSLongitude' in exif_data and \
            'GPS GPSLatitudeRef' in exif_data and 'GPS GPSLongitudeRef' in exif_data:
            lat_value = exif_data['GPS GPSLatitude'].values
            lat_ref = exif_data['GPS GPSLatitudeRef'].values[0]
            lon_value = exif_data['GPS GPSLongitude'].values
            lon_ref = exif_data['GPS GPSLongitudeRef'].values[0]

            lat = convert_to_degress(lat_value)
            if lat_ref != 'N':
                lat = -lat

            lon = convert_to_degress(lon_value)
            if lon_ref != 'E':
                lon = -lon
        
        # Declare the image dictionary for each one
        image_dict = {
            'name': img,
            'latitude': lat,
            'longitude': lon
        }

        # Before adding them to the `images` list, check first if the latitude is null
        if lat is not None:
            images.append(image_dict)

Refactoring

The whole portion above is what we use to generate the contents of our images variable.

As you can see, our code is a little messy. Let's do some refactoring to make it more readable in the future.

From our code, we can extract two code groups, first is the extracting of the exif data and the second one is the extraction of latitude and longitude if they exist. So let's do that.

Let's create the function for extraction of exif data

def get_exif_data(filename):
    exif_data = {}

    with open(filename, 'rb') as image_file:           # The 'rb' means open the file for reading of binary data
        exif_data = exifread.process_file(image_file)  # Place the exif data into the `exif_data` variable

With this created, let's look at our code above

image_files = os.listdir(app.config['UPLOAD_FOLDER'])
images = []

# Iterate through the image_files using for loop
for img in image_files:
    filename = os.path.join(app.config['UPLOAD_FOLDER'], img)  # Construct the full path

    exif_data = get_exif_data(filename)  # Call the created function for extracting exif data
        
    # Declare latitude and longitude values as initially null
    lat = None
    lon = None

    # Extract the coordinates
    if 'GPS GPSLatitude' in exif_data and 'GPS GPSLongitude' in exif_data and \
        'GPS GPSLatitudeRef' in exif_data and 'GPS GPSLongitudeRef' in exif_data:
        lat_value = exif_data['GPS GPSLatitude'].values
        lat_ref = exif_data['GPS GPSLatitudeRef'].values[0]
        lon_value = exif_data['GPS GPSLongitude'].values
        lon_ref = exif_data['GPS GPSLongitudeRef'].values[0]

        lat = convert_to_degress(lat_value)
        if lat_ref != 'N':
            lat = -lat

        lon = convert_to_degress(lon_value)
        if lon_ref != 'E':
            lon = -lon
        
    # Declare the image dictionary for each one
    image_dict = {
        'name': img,
        'latitude': lat,
        'longitude': lon
    }

    # Before adding them to the `images` list, check first if the latitude is null
    if lat is not None:
        images.append(image_dict)

Now let's create the function to extract the longitude and latitude values from the exif data

def get_geolocation(exif_data):
    def convert_to_degress(value):
        d = float(value[0].num) / float(value[0].den)
        m = float(value[1].num) / float(value[1].den)
        s = float(value[2].num) / float(value[2].den)

        return d + (m / 60.0) + (s / 3600.0)

    latitude = None
    longitude = None

    if 'GPS GPSLatitude' in exif_data and 'GPS GPSLongitude' in exif_data and \
       'GPS GPSLatitudeRef' in exif_data and 'GPS GPSLongitudeRef' in exif_data:
        lat_value = exif_data['GPS GPSLatitude'].values
        lat_ref = exif_data['GPS GPSLatitudeRef'].values[0]
        lon_value = exif_data['GPS GPSLongitude'].values
        lon_ref = exif_data['GPS GPSLongitudeRef'].values[0]

        latitude = convert_to_degress(lat_value)
        if lat_ref != 'N':
            latitude = -latitude

        longitude = convert_to_degress(lon_value)
        if lon_ref != 'E':
            longitude = -longitude

    return latitude, longitude

In python, if your function returns two values, you can do this:

val1, val2 = <function_that_returns_2_values>

And so in our main code, since the function get_geolocation returns latitude and longitude, we can do this

lat, lon = get_geolocation(exif_data)
image_files = os.listdir(app.config['UPLOAD_FOLDER'])
images = []

for img in image_files:
    if allowed_file(img):
        filename = os.path.join(app.config['UPLOAD_FOLDER'], img)
        exif_data = get_exif_data(filename)
        lat, lon = get_geolocation(exif_data)
        if lat is not None:
            images.append({
                'filename': filename,
                'latitude': lat,
                'longitude': lon,
                'name': img
            })

This looks cleaner now right!

Let's check what we've done so far:

  • We've listed all the files from a directory
  • Filtered allowed file extensions only
  • Extract the geo location from images
  • Recreated the images variable for the list of image objects.

In flask, we cannot directly point to the address of our images like localhost:5000/uploads/image1.jpg. It won't allow us for security reasons. For this, we will use a special function from flask called send_from_directory.

In order to do this, we will create a new route the handles the downloading of the images. We will call this in our html template later.

@app.route("/uploads/<filename>")
def uploaded_photo(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

This above is called a dynamic route. The URL definition contains a variable filename that we will pass using our template. Just add it to our main.py file and we are done with our python codes for this.

Update Template

Now that we have a way of generating a download url for images, we will apply it to our html template.

In our template, we will just update one line of code.

From

{% for image in images %}

<div class="card w-full bg-base-100 shadow-xl">
    <div class="card-body">
    <h2 class="card-title">{{ image.name }}</h2>
    </div>
    <figure><img src="{{ image.url }}" alt="{{ image.name }}" height="100%"></figure>
</div>

{% endfor %}

we will use a new function url_for and our uploaded_photo route.

{% for image in images %}

<div class="card w-full bg-base-100 shadow-xl">
    <div class="card-body">
    <h2 class="card-title">{{ image.name }}</h2>
    </div>
    <figure><img src="{{ url_for('uploaded_photo', filename=image.name) }}" alt="{{ image.name }}" height="100%"></figure>
</div>

{% endfor %}

We only changed the src attribute of our img tag.

Below is the example of my uploaded images.

Preview
Preview

Now, if we upload an image that does not contain any geotag data, they will not be displayed here.

To access the code, go ahead to https://github.com/alexiusacademia/geotag-album-tutorial/tree/11-display-uploaded-imagesopen in new window

I hope you enjoyed this topic! If you have any question, just drop them in the comments below. Happy coding! 😄

Last Updated:
Contributors: alexiusacademia