DEV Community

Cover image for Django Person Image Generator
David
David

Posted on • Edited on

Django Person Image Generator

There is the common problem in web development where you need to have randomly generated people with faces, especially for mockups. I ran into this problem the other day when making a Django app. Knowing that https://thispersondoesnotexist.com/ is a thing, I decided to write a Person model and admin that fetches images from that site. Here are the features I wanted that I will demonstrate below:

  • A way to preview the generated image in the Django admin interface.
  • A way to generate a new image in the admin interface if I didn't like the first one that was generated.
  • A way to handle saving and deleting image files on creation and deletion of the model.

With that said, let's dive into it!

Project Setup

Create a Django project with a persons app.

pip install Django Pillow requests
django-admin startproject project .
cd project
django-admin startapp persons
Enter fullscreen mode Exit fullscreen mode

Make sure to add persons to INSTALLED_APPS in project/settings.py.

INSTALLED_APPS = [
    ...
    'project.persons'
]
Enter fullscreen mode Exit fullscreen mode

At the bottom of project/settings.py add media settings because we will be saving images.

MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
Enter fullscreen mode Exit fullscreen mode

Create media directories in root of project. We will be saving images to media/persons/.

media/
  persons/
project/ # our Django project
  persons/ # our Django app
  ...
...
Enter fullscreen mode Exit fullscreen mode

Image Downloader

Create a view in persons/views.py that uses the requests library to download an image from https://thispersondoesnotexist.com/image. We will be fetching this view from our Person admin later.

from django.http import JsonResponse
from django.conf.global_settings import MEDIA_URL
import requests

def generate_image(request):
    # Use this site to generate an image file of a person
    url = 'https://thispersondoesnotexist.com/image'
    req = requests.get(url)
    # Adjust the names and paths below to fit your project
    filename = '/persons/tmp.jpg'
    with open("media" + filename, "wb+") as f:
        f.write(req.content)

    return JsonResponse(data={
        "path": MEDIA_URL + filename
    })
Enter fullscreen mode Exit fullscreen mode

Add the view and the media static server to urls.py.

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from project.persons.views import generate_image

urlpatterns = [
    path('admin/', admin.site.urls),
    path('generate-image/', generate_image, name="generate-image")
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Enter fullscreen mode Exit fullscreen mode

Visit or curl http://127.0.0.1:8000/generate-image/ to make sure it works.

Models and Admin

Create a Person model in persons/models.py. The save() method is overwritten to make sure that the image saved by default
(our generated image) gets saved as its own file. A little image processing with the Pillow library resizes the image. The delete() method is also overwritten to make sure the image file gets deleted along with the model. For more on overriding these methods, visit here.

from django.db import models
from shutil import copy2
from datetime import datetime
import os
from PIL import Image  

MEDIA_LOCATION = "persons"
DEFAULT = "tmp.jpg"
class Person(models.Model):
    name = models.CharField(max_length=100)
    image = models.ImageField(upload_to=MEDIA_LOCATION, default=f"{MEDIA_LOCATION}/{DEFAULT}")

    def save(self, *args, **kwargs):
        if DEFAULT in self.image.url:
            # Copy image to a new, unique file related to the person's name and the time of download
            new_filename = f"{self.name[:10]}-{datetime.now().isoformat()}"
            new_filename = "".join([c for c in new_filename if c.isalpha() or c.isdigit()]).rstrip() + ".jpg"
            copy2(f"media/{MEDIA_LOCATION}/{DEFAULT}", f"media/{MEDIA_LOCATION}/{new_filename}")
        self.image = f"{MEDIA_LOCATION}/{new_filename}"
        # Save the image and resize it
        super(Person, self).save(*args, **kwargs) 
        # The [1:] slice is to remove the / in front of /media/persons/...
        im = Image.open(self.image.url[1:])  
        if im.width > 150: 
            im = im.resize((150, 150))
            im.save(self.image.url[1:])

    def delete(self, *args, **kwargs):
        # Delete the image file along with the model
        try:
            os.remove(self.image.url[1:])
        except:
            pass
        super(Person, self).delete(*args, **kwargs)

    def __str__(self):
        return f"Person {self.id}: {self.name}"
Enter fullscreen mode Exit fullscreen mode

Register the model in persons/admin.py. We're going to do a little admin customization using a custom widget so we can preview and generate images. For more on widgets, see here.

from django.contrib import admin
from django.db import models
from django.forms.widgets import ClearableFileInput 

from project.persons.models import Person

class ImageWidget(ClearableFileInput):
    template_name = "image_widget.html"

class PersonAdmin(admin.ModelAdmin):
    formfield_overrides = {
        models.ImageField: {'widget': ImageWidget},
    }

admin.site.register(Person, PersonAdmin)
Enter fullscreen mode Exit fullscreen mode

Here's the template to go along with it. Put it in persons/templates/image_widget.html

{% include "django/forms/widgets/clearable_file_input.html" %}

{% if widget.value %}
  {% comment %} This is for when you are changing a person {% endcomment %}
  <img src="{{ widget.value.url }}" id="currentImage" width="100" alt="current image">
{% else %}
  {% comment %} This is for when you are adding a person {% endcomment %}
  <div>
    <h5>If no file above, this generated image will be used</h5>
    <img id="generatedImage" width="100">
    <button id="newImage">Get New Image</button>
    <span id="loading" style="display: none;">Loading...</span>
  </div>
  <script>
    function loadImage() {
      // The date thingy at the end is prevent image caching
      document.getElementById("generatedImage").setAttribute("src", "/media/persons/tmp.jpg?"+new Date().toISOString())
    }
    loadImage()
    async function getNewImage(e) {
      const loading = document.getElementById("loading")
      loading.style.display = "inline"
      e.preventDefault()
      await fetch('{% url "generate-image" %}').then(() => {
        loadImage()
        loading.style.display = "none"
      })
    }
    document.getElementById("newImage").onclick = getNewImage
  </script>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Be sure to migrate the database. I'm just using the default sqlite database from settings. Create a superuser too in order to log in to admin.

python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

Once those things are set up, run the server and visit 127.0.0.1:8000/admin/ to try adding persons. You should see something like the following:


Add Person Admin Page

Click "Get New Image" to fetch another person that doesn't exist using the generate-image view we wrote earlier.
Add Person


Change Person Admin Page

Just shows a preview of the Person's image.
Change Person

End

And that's it! The code for this post is available on GitHub. Good luck!

Top comments (3)

Collapse
 
michaelcurrin profile image
Michael Currin

There's a library if you want to abstract away the logic.

from thispersondoesnotexist import get_online_person
picture = await get_online_person()  

from thispersondoesnotexist import save_picture
await save_picture(picture, "a_beautiful_person.jpeg")
await save_picture(picture)
Enter fullscreen mode Exit fullscreen mode

pypi.org/project/thispersondoesnot...

Collapse
 
michaelcurrin profile image
Michael Currin • Edited

Usually the chunk size is much smaller at 1024.

If you're going to use a massive chunk size like that you might as well leave out chunks and use less code.

r = requests.get(url)
with open("wind-turbine.jpg", "wb") as f:
    f.write(r.content)
Enter fullscreen mode Exit fullscreen mode

jdhao.github.io/2020/06/17/downloa...

Using with block instead of open and close is also safer and more common.

Would also love to see 4 space indention instead of 2 for Python code, to match the recommended style.

Collapse
 
buckldav profile image
David

Fixed! Thanks. I didn't proofread much before upload :)