How to Upload Multiple Geotagged Images in Django
Overview
In this post, we look into how to upload multiple geo-tagged/non-geotagged images to aws s3 using plain Django and spatialite as databbase. We use GeoDjango to store the latitude, longitude extracted from geo-tagged images into the database.
Project setup
create django project
django-admin startproject login_boiler_plate
create app python manage.py startapp GisMap
create superuser python manage.py createsuperuser
In settings.py
add the app to installed_app
list and setup the default location for media storage.
= [
INSTALLED_APPS
...'GisMap',
]
= os.path.join(BASE_DIR, 'media')
MEDIA_ROOT = '/media/' MEDIA_URL
Setup the database backend to postgis extenstion of postgresql.
# in settings.py file
= {
DATABASES 'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis', #imp
'NAME': 'database_name_here',
'USER': 'postgres',
'PASSWORD': 'password_here',
'HOST': 'localhost',
'PORT': '5432',
}, }
In models.py
, create model for uploading images. DateTimeField
and user
are not necessary.
from django.db import models
from django.contrib.auth.models import User
class ImageUpload(models.Model):
= models.ForeignKey(User, null=True, on_delete=models.CASCADE)
user = models.ImageField( null=False, blank=False, upload_to = 'images/')
image = models.DateTimeField(auto_now_add=True, null=True)
date_created
def __str__(self):
return self.user.username + " uploaded: "+ self.image.name
In forms.py
, refer to the ImageUpload model for input.
from django.forms import ModelForm
from django.contrib.auth.models import User
from .models import ImageUpload
class ImageForm(ModelForm):
class Meta:
= ImageUpload
model = ('image',) fields
In home.html
, create the form to accept image upload.
<!-- Modal -->
<form method = "post" enctype="multipart/form-data">
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true" >
{% csrf_token %}<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Upload Image</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
{{ image_form.image }}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Image</button>
</div>
</div>
</div>
</div>
</form>
In views.py
, accept the HTTP POST request and save to the database. We will alter this to extract latitude, longitude later.
@login_required(login_url='login')
def home_page(request):
if request.method == 'POST':
= ImageForm(request.POST , request.FILES)
form print(form)
if form.is_valid():
print("is valid")
= form.save(commit=False)
obj = request.user
obj.user
obj.save()return redirect('home')
else:
= ImageForm()
Imageform return render(request, "GisMap/home.html", {'Title': "Home Page", "image_form": ImageForm})
Get Lat, lon from image meta deta (Exchangeable image file format [EXIF] )
- Geodjango is built on top of django and adds spatial functionality such as storing points, lines , polygon and multipolygon. It is prepackaged with Django but requires few additional softwares to make it fully functional. These include- GDAL, PROJ, GEOS, PostGIS. These can be downloaded from osgeo4W which bundles all these libraries. Then application can be added to apps in settings with
django.contrib.gis
to the installed apps.
By default geodjango is not installed in the apps list and thus we do it ourself.
pip install django-geo
NOTE- ensure os4geo is installed: install from here if not done. And make the following changes in settings.py
.
An additional setting is required, which is to locate osgeo4w directory in django. If you install osgeo4w in default directory, you need to put the following code within the settings.py file.
= [
INSTALLED_APPS
...'django.contrib.gis',
]
import os
import posixpath
if os.name == 'nt':
import platform
= r"C:\OSGeo4W"
OSGEO4W if '64' in platform.architecture()[0]:
+= "64"
OSGEO4W assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
'OSGEO4W_ROOT'] = OSGEO4W
os.environ['GDAL_DATA'] = OSGEO4W + r"\share\gdal"
os.environ['PROJ_LIB'] = OSGEO4W + r"\share\proj"
os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH'] os.environ[
In models.py
, add a PointField which can store geospatial information (lat,lon)
from django.contrib.gis.db import models
class ImageUpload():
... = models.PointField( null=True) geom
In views.py
, define functions to extract meta data from image and convert into right format for GeoDjango to understand it. Courtesy of Jayson DeLancey
#________________________________________FUNCTIONS FOR IMAGE EXIF DATA______________________________________________________________________________#
from PIL import Image
from urllib.request import urlopen
from PIL.ExifTags import GPSTAGS
from PIL.ExifTags import TAGS
def get_decimal_from_dms(dms, ref):
= dms[0]
degrees = dms[1] / 60.0
minutes = dms[2] / 3600.0
seconds
if ref in ['S', 'W']:
= -degrees
degrees = -minutes
minutes = -seconds
seconds
return round(degrees + minutes + seconds, 5)
def get_coordinates(geotags):
= get_decimal_from_dms(geotags['GPSLatitude'], geotags['GPSLatitudeRef'])
lat
= get_decimal_from_dms(geotags['GPSLongitude'], geotags['GPSLongitudeRef'])
lon
return (lon, lat)
def get_geotagging(exif):
if not exif:
raise ValueError("No EXIF metadata found")
= {}
geotagging for (idx, tag) in TAGS.items():
if tag == 'GPSInfo':
if idx not in exif:
raise ValueError("No EXIF geotagging found")
for (key, val) in GPSTAGS.items():
if key in exif[idx]:
= exif[idx][key]
geotagging[val]
return geotagging
#_______________________________________________________________________________________________________________________________________#
In views.py
, update home_page function to extract meta data and save the image to database.
from django.contrib.gis.geos import Point
@login_required(login_url='login')
def home_page(request):
if request.method == "POST":
= ImageForm(request.POST, request.FILES)
form = Image.open(request.FILES.get("image"))
img if form.is_valid():
try:
= form.save(commit=False)
obj = request.user
obj.user = obj.image.url
obj.image_url = get_geotagging(img._getexif())
geotags = Point(
obj.geom
get_coordinates(geotags)# X is longitude, Y is latitude, Point(X,Y)
)
obj.save()f"image uploaded succesfully")
messages.success(request, except ValueError as e:
messages.warning(request, e)else:
f"Invalid image type")
messages.warning(request, return redirect("home")
else:
= ImageForm()
Imageform return render(
"GisMap/home.html", {"Title": "Home Page", "image_form": ImageForm}
request, )
Upload to S3 bucket
Install boto3 package and django-storages. Add to installed packages. Additionally, provide Key:Value AWS credentials to access the bucket and change the default file storage to S3.
pip install django-storages
pip install boto3
in settings.py
= [
INSTALLED_APPS
...'storages',
]
= ""
AWS_ACCESS_KEY_ID = ""
AWS_SECRET_ACCESS_KEY = ""
AWS_STORAGE_BUCKET_NAME
= False
AWS_S3_FILE_OVERWRITE = None
AWS_DEFAULT_ACL
= 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE
= False // removes the query string AWS_QUERYSTRING_AUTH
NOTE: Make the bucket public to be able to make HTTP request
Provide policy to make our s3 bucket public. By default, the bucket is private and no read/wrtie access is provided for user from outside the s3 page. There are other ways to access private bucket by either Limiting access to specific IP addresses or Restricting access to a specific HTTP referer. For simplicity we make the bucket public.
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"PublicRead",
"Effect":"Allow",
"Principal": "*",
"Action":["s3:GetObject","s3:GetObjectVersion"],
"Resource":["arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"]
}
]
}
Accept non-geotagged images
At this point, we should be able to upload geotagged images to s3 bucket. Non-geotagged images are not yet accepted by the model and thus we create seperate model for it.
We now make separate model for accepting non-geotagged images similar to ImageUpload
model but without PointField
.
class Photos(models.Model):
= models.ForeignKey(User, null=True, on_delete=models.CASCADE)
user = models.ImageField(upload_to='photos/',null=True,blank=False)
image = models.DateTimeField(auto_now_add=True, null=True)
date_created = models.URLField(max_length=250, null=True, blank=False)
image_url
class Meta:
= 'Photo'
verbose_name = 'Photos'
verbose_name_plural
def __str__(self):
return self.user.username + " uploaded image "+ self.image.name
In views.py
file, extend the home_page function to add a fallback for non-geotagged images.
if request.method == "POST":
# images will be in request.FILES
= request.POST, request.FILES
post_request, files_request
= PhotoForm(post_request or None, files_request or None)
form = request.FILES.getlist(
files "images"
# returns files: [<InMemoryUploadedFile: Image_name.jpg (image/jpeg)>, <InMemoryUploadedFile: Image_name.jpg (image/jpeg)>]
) if form.is_valid():
= request.user
user for f in files:
# returns <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=480x360 at 0x1ED0CCC6280>
= Image.open(f)
img
try:
= get_geotagging(img._getexif())
geotags = ImageUpload(user=user, image=f)
geoimage = geoimage.image.url
geoimageimg_upload.image_url # X is longitude, Y is latitude, Point(X,Y) ; returns eg SRID=4326;POINT (11.88454 43.46708)
= Point(get_coordinates(geotags))
geoimage.geom
geoimage.save()except:
= Photos(user=user, image=f)
nongeoimage = nongeoimage.image.url
nongeoimage.image_url
nongeoimage.save()else:
print("Form invalid")
return redirect("home")
else:
= PhotoForm()
Imageform return render(
"GisMap/home.html", {"Title": "Home Page", "image_form": ImageForm}
request, )
Accept multiple images
Make a new form which accepts multiple image files to be uploaded at once.
class PhotoForm(forms.ModelForm):
= forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
images
class Meta:
= Photos
model = ('images',) fields
In home.html
, add multiple
attribute to allow for multiple selection of images at once.
<div class="form-group">
<label for="note-image"></label>
<input type="file" name="images" class="form-control-file" id="note-image" multiple>
</div>
Final Note:
At this point, you should be able to upload multiple Images to the AWS S3 bucket and have coordinates extracted the geo-tagged images and segregate non-geotagged images.
You learnt-
- How to Setup GeoDjango
- How to Setup AWS S3 bucket
- How to Extract meta data from Image and store in database using PointField
These steps will ensure you have multiple images uploaded at once and all the geolocation information can be stored in database, which later can be import to QGIS for data visualisation. Although both postgresql and django admin allows users to visualise the data.