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.
- Check if the image has extension in the allowed extensions
- Get the full path to the current image by combining the uploads directory and the image filename
- Extract the exif data of an image
- Get the latitude and longitude of an image
- Check if latitude or longitude exists in the image metadata
- 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.
Link to each image
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.
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-images
I hope you enjoyed this topic! If you have any question, just drop them in the comments below. Happy coding! 😄