Django and Celery Next Steps

In our first article on Django and Celery we set up a very basic system that added two numbers together. Unlikely you're going to use this in practice! But as a first step it showed something useful. In this article we try out something a bit more interesting - something genuinely useful.

Integrating with AWS Rekognition

One of the features we added to ORDNA was to check an uploaded video for the presence of faces. Generally speaking in a surgical video, you don't want to see anyone's face - particularly the face of a patient. There are all manner of privacy and data protection concerns if you can! And detecting a face isn't that difficult these days, with many off-the-shelf solutions available to you.

Since ORDNA runs on AWS, the obvious choice for facial recognition was to use the (aptly named) AWS Rekognition service to do so. There are a few steps to integrate with Rekognition:

  • Submitting the "job" to Rekognition, supplying a link to a video on S3
  • Receiving a notification of job completion from Rekognition, which uses AWS SNS
  • Retrieving all the faces detected from Rekognition

Submitting the job to Rekognition

This is the most straightforward part of the operation. Assuming you have a video ready to go, and have set up all the necessary S3 bucket permissions, just ask boto3 to start the job, supply parameters in the necessary format. Here's a snippet of the code we use to submit a video for face detection:

def detect_faces(self):
    video = {
        "S3Object": {
            "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
            "Name": self.s3path(),
        }
    }
    channel = {
        "RoleArn": settings.REKOGNITION_ROLE,
        "SNSTopicArn": settings.REKOGNITION_TOPIC,
    }
    logger.info(f"Submitting Rekognition job : video: {video} channel: {channel}")
    output = self.rk.start_face_detection(Video=video, NotificationChannel=channel)
    jobId = output["JobId"]
    logger.info(f"...submitted => {jobId}")
    return output

Tell Rekognition where to find the video, and give it the necessary role and topic definitions so that Rekognition is able to let you know when the job has been finished.

Receiving a notification of job completion with SNS

AWS SNS is a lightweight mechanism to manage a publish-subscribe workflow, and Rekognition uses this to let you know when it has finished work by notifying an entity in SNS known as a "Topic". Setting up a Django endpoint to receive SNS subscription notifications is left as an exercise to the reader (I'm afraid this is a bit of Airsource IP that we won't be sharing!), but once you have you can expect to receive an HTTP call when there are results that need to be processed (or when there is an error to be handled).

Once you've set up the necessaries, you can make a synchronous call to your task when the SNS topic subscription fires:

get_faces(video.id, message["JobId"])

and now we're ready to consume all the information about faces from Rekognition.

Getting all the faces

The key issue you will run into is when there are more than 1000 faces detected in your video, which is all that "get_face_detection" will return you in a single call. For the types of surgical videos we are processing, there could be several hours of video, and if these videos are commentary with a "talking head" in the corner, then it's very easy to exceed this number. The problem is that you want the notification sent by Rekognition to be processed quickly, and so including loops that call out to Rekognition to retrieve face data is not a good idea. Enter a new Celery task:

@shared_task
def get_faces(videoid, jobid, nextToken=""):
    """Retrieves all faces from Rekognition for the given jobid"""
    rk = boto3.client("rekognition", region_name="<your_region>")
    output = rk.get_face_detection(JobId=jobid, NextToken=nextToken)
    logger.debug(f"Face detection results received: {output}")
    video = Video.objects.get(id=videoid)

    # Do something useful with the face output here!

    if "NextToken" in output:
        get_faces.delay_on_commit(video.id, jobid, output["NextToken"])
    else:
       # We're done, do whatever you want to do to clean up / finish up
       pass

This will "chain" calls to itself via Celery, ensuring that the system remains responsive. By the end of this task, you will have safely retrieved all faces detected by Rekognition.

Wrapping up

In this article I've shown you a simple example of using a Celery task to communicate with an external entity while ensuring that a Django backend remains responsive. Hope you found it useful!

Nick Clarey
in Technical Tagged technical django python celery

Airsource design and develop apps for ourselves and for our clients. We help people like you to turn concepts into reality, taking ideas from initial design, development and ongoing maintenance and support.

Contact us today to find out how we can help you build and maintain your app.