Deploying a Flask App for Deep Learning on Gradient

In this followup to the introduction to GFP-GAN, we look at using Gradient Deployments to create a Flask API making full use of the model.

a month ago   •   11 min read

By James Skelton
Table of contents

In last week's tutorial, we covered using the awesome GFP-GAN neural network on Gradient notebooks to radically upscale the quality of damaged or low quality photographs. The use cases for such a model are much more apparent than you typically see in cutting edge ML projects; the desire to recover the quality of these images that could range from family heirlooms to low-quality meme fodder is rapidly growing. This can be seen in a lot of social media communities in particular.  

In today's tutorial, we are going to take the lessons from last week's tutorial, and apply them to constructing a rudimentary Flask application, and then using Gradient Deployments to access the API in our browser. This will let us access a simple interface where we can quickly upload a photo to be processed and restored by GFP-GAN in a two step process.

By the end of this tutorial you will know how to write a Flask app for deploying the PyTorch model GFP-GAN, how to write and create the necessary Docker file and upload it to Dockerhub for access to Gradient, and how to use Deployments to turn your Flask app into a cloud GPU supercharged proto-web page.

Be sure to read the previous blog and paper by Xintao et al.

The workflow to do this will be as follows:

  • Set up the local environment
  • Create the flask app in a Docker container
  • Test it locally and push to Dockerhub
  • Use the image to Deploy on Gradient
  • Restore photos!
Sample of GFP-GANs capabilities on a sample from Reddit

Set Up

First, we need to make some installs. These are all the libraries required to run GFP-GAN:

torch>=1.7
numpy<1.21  # numba requires numpy<1.21,>=1.17
opencv-python
torchvision
scipy
tqdm
lmdb
pyyaml
tb-nightly
yapf
Flask

#These four have to be installed directly for reasons I haven't been able to figure out (see Dockerfile section)
opencv-python-headless
realesrgan==0.2.2.4
pip install basicsr
facexlib

Most of these will be installed using a requirements.txt script, and they are necessary to run the model on the Flask application as well as to test it on your local machine. Once the installations are complete, open up your preferred code editor.

At this time, go ahead and clone the GFP-GAN repo to your local machine. Open a workspace for the GFP-GAN repo in your editor, and navigate to the file app.py. This is where the structure of the Flask application lies.

Bring this project to life

The Flask app

import sys
import io
import json
import argparse
import cv2
import glob
import numpy as np
import os
from basicsr.utils import imwrite
from os import listdir
from os.path import isfile, join
from werkzeug.utils import secure_filename

import torch
import torchvision.transforms as transforms
from PIL import Image

from flask import Flask, request, render_template, redirect, url_for

from gfpgan import GFPGANer

These are all the imports needed to make this sample application run. GFP-GAN is written in PyTorch, so torch and torchvision are also required for the container to run. The rest serve to ensure the Flask application functions properly.

UPLOAD_FOLDER = 'inputs/whole_imgs'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__,static_folder='results')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

This next block of code is used to instantiate our Flask app. In the process, we assign the whole_imgs directory as the destination for uploaded files and the results directory to hold the static images for the HTML to display. These images are restricted to the more common typings to ensure there isn't any issues in training.

@app.route("/", methods = ['GET', 'POST'])
def index():
    return render_template("index.html")

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route("/upload", methods = ['GET', 'POST'])
def upload_file():
	# Clear the input and source folders, and dump previous images to saved. You can build another app route to take advantage of these saved images as needed.
    source = 'inputs/whole_imgs/'
    destination = 'inputs/saved/'
    out = 'results/restored_imgs'
    for f in os.listdir(source):
        os.remove(os.path.join(source, f))
    for f in os.listdir(destination):
        os.remove(os.path.join(destination, f))
    for f in os.listdir(out):
        os.remove(os.path.join(out, f))
    # Get the file upload
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            print('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            print('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('main', name=filename))
    #HTML form for uploading the file to the app
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

The /upload API endpoint of the new site is where users are able to directly upload a file of their choice to be enhanced and restored by GFP-GAN. These scripts take the file, ensure its compatibility with the model, and upload it to the directory the application is pulling from for restoration. This all takes the form of a conventional 'Choose File' button with an 'Upload' button following.

@app.route('/main', methods=['POST','GET'])
def main():
    """Inference demo for GFPGAN.
    All credit to Xintao et al. at https://github.com/TencentARC/GFPGAN for writing this script i've adapted here for Flask. 
    """
    
    # Normally, this repo was designed to be executed from the command line with a series of optional and required args.
    # Instead, you will need to change the arguments you need to change using the sys.argv command (see line 29)
    parser = argparse.ArgumentParser()
    parser.add_argument('--upscale', type=int, default=2, help='The final upsampling scale of the image')
    parser.add_argument('--arch', type=str, default='clean', help='The GFPGAN architecture. Option: clean | original')
    parser.add_argument('--channel', type=int, default=2, help='Channel multiplier for large networks of StyleGAN2')
    parser.add_argument('--model_path', type=str, default='GFPGANCleanv1-NoCE-C2.pth')
    parser.add_argument('--bg_upsampler', type=str, default='realesrgan', help='background upsampler')
    parser.add_argument(
        '--bg_tile', type=int, default=400, help='Tile size for background sampler, 0 for no tile during testing')
    parser.add_argument('--test_path', type=str, default='upload/', help='Input folder')
    parser.add_argument('--suffix', type=str, default=None, help='Suffix of the restored faces')
    parser.add_argument('--only_center_face', action='store_true', help='Only restore the center face')
    parser.add_argument('--aligned', action='store_true', help='Input are aligned faces')
    parser.add_argument('--paste_back', action='store_false', help='Paste the restored faces back to images')
    parser.add_argument('--save_root', type=str, default='results', help='Path to save root')
    parser.add_argument(
        '--ext',
        type=str,
        default='auto',
        help='Image extension. Options: auto | jpg | png, auto means using the same extension as inputs')

# directly input the args you want to run inference_gfpgan using sys.argv. you can change these as needed, but i have used the defaults for simplicity. 

    sys.argv = ['--model_path GFPGANCleanv1-NoCE-C2.pth --upscale 2 --test_path inputs/whole_imgs --save_root results --bg_upsampler realesrgan']

    args = parser.parse_args()
    if args.test_path.endswith('/'):
        args.test_path = args.test_path[:-1]
    os.makedirs(args.save_root, exist_ok=True)

    # background upsampler
    if args.bg_upsampler == 'realesrgan':
        if not torch.cuda.is_available():  # CPU
            import warnings
            warnings.warn('The unoptimized RealESRGAN is very slow on CPU. We do not use it. '
                          'If you really want to use it, please modify the corresponding codes.')
            bg_upsampler = None
        else:
            from realesrgan import RealESRGANer
            bg_upsampler = RealESRGANer(
                scale=2,
                model_path='https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth',
                tile=args.bg_tile,
                tile_pad=10,
                pre_pad=0,
                half=True)  # need to set False in CPU mode
    else:
        bg_upsampler = None
    args.test_path = 'inputs/whole_imgs'
    # set up GFPGAN restorer
    restorer = GFPGANer(
        model_path=args.model_path,
        upscale=args.upscale,
        arch=args.arch,
        channel_multiplier=args.channel,
        bg_upsampler=bg_upsampler)
    img_list = sorted(glob.glob(os.path.join(args.test_path, '*')))
    print(img_list, '**')
    count = 4
    for img_path in img_list:
        count -= 1
        if count == 0:
            break
        # read image
        print('yes')
        img_name = os.path.basename(img_path)
        print(f'Processing {img_name} ...')
        basename, ext = os.path.splitext(img_name)
        input_img = cv2.imread(img_path, cv2.IMREAD_COLOR)

        # restore faces and background if necessary
        cropped_faces, restored_faces, restored_img = restorer.enhance(
            input_img, has_aligned=args.aligned, only_center_face=args.only_center_face, paste_back=args.paste_back)

        # save faces
        for idx, (cropped_face, restored_face) in enumerate(zip(cropped_faces, restored_faces)):
            print('1')
            # save cropped face
            save_crop_path = os.path.join(args.save_root, 'cropped_faces', f'{basename}_{idx:02d}.png')
            imwrite(cropped_face, save_crop_path)
            # save restored face
            if args.suffix is not None:
                save_face_name = f'{basename}_{idx:02d}_{args.suffix}.png'
            else:
                save_face_name = f'{basename}_{idx:02d}.png'
            save_restore_path = os.path.join(args.save_root, 'restored_faces', save_face_name)
            imwrite(restored_face, save_restore_path)
            # save comparison image
            cmp_img = np.concatenate((cropped_face, restored_face), axis=1)
            imwrite(cmp_img, os.path.join(args.save_root, 'cmp', f'{basename}_{idx:02d}.png'))

        # save restored img
        if restored_img is not None:
            if args.ext == 'auto':
                extension = ext[1:]
            else:
                extension = args.ext

            if args.suffix is not None:
                save_restore_path = os.path.join(args.save_root, 'restored_imgs',
                                                 f'{basename}_{args.suffix}.{extension}')
            else:
                save_restore_path = os.path.join(args.save_root, 'restored_imgs', f'{basename}.{extension}')
            imwrite(restored_img, save_restore_path)

    onlyfiles = [f for f in listdir('results/restored_imgs') if isfile(join('results/restored_imgs', f))]
    onlyfiles.remove('.DS_Store')
    return render_template("index2.html", variable = onlyfiles[0])




if __name__ == '__main__':
    app.run(#host="0.0.0.0")

Above, is the code for the inference_gfpgan.py script from the repo that has been adapted into our application. This script parses the inputted arguments we write using the sys.argv call. These are set to the defaults outlined in the function, but this poses a small problem. The Docker image's sheer size makes it important to consider these parameters before pushing the image to be used in your deployment to Dockerhub. In the future, to productionize the app on Gradient we will need to consider more dynamic methods to alter the parameters for the restoration process.

This GFP-GAN function within the app follows the args to make some adjustments, but it then takes each of the inputted images, enhances their facial image and backgrounds, and then saves the new versions to the predetermined or defaulted results directory.

Docker

Within the GFP-GAN repo is a sample Dockerfile to be used to create a docker image that can later be deployed to a Gradient GPU. Let's look at this one and see how to write a very basic Dockerfile for PyTorch deployments with Flask:

# syntax=docker/dockerfile:1

FROM python:3.8-slim-buster
RUN pip install flask
RUN apt-get update \
  && apt-get install -y wget \
  && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y git
RUN git clone https://github.com/gradient-ai/GFPGAN.git
WORKDIR /GFPGAN
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
RUN pip3 install opencv-python-headless
RUN pip install realesrgan==0.2.2.4
RUN pip install basicsr
RUN pip install facexlib
RUN python setup.py develop
RUN wget https://github.com/TencentARC/GFPGAN/releases/download/v0.2.0/GFPGANCleanv1-NoCE-C2.pth

EXPOSE 5000

COPY . .

ENTRYPOINT [ "python" ]

CMD [ "app.py" ]

This Dockerfile, when ran, will download all the necessary packages and build an image that can be used to run as a container. This page mostly comprised of installs, but also includes 2 other important jobs: RUN wget to grab the model weights, and the execution of setup.py to finish configuring the container environment.

Once we've gotten your files in place, we can build our Docker image by running:

docker build gfpgan . 

Once we've built your image locally, run the container using this following command:

docker run -d -p 5000:5000 gfpgan-flask-demo-gradient:latest

Then we can localhost:5000 and following the two steps needed to run GFP-GAN through the app.

Our application is now running on the localhost:5000 port and you can access it through your browser. Follow the prompts to upload the photo you want to see restored. Check the terminal for the progress during the upload and restoration process to get logs for the Flask app.

When upload_photo() is run, the main() script will be executed and the result saved to the results directory. Once the touched up photo has generated and saved, the app will then display it using HTML in the webpage. From here you can download it directly.

Within the Docker application or the Docker CLI, you can now push your new image to Dockerhub to be shared, versioned, and improved upon as needed. If you don't want to set up your own Dockerfile, you can access mine at this url.

Deploy

image: jameshskelton/paperspace:gfpgan-flask-demo-gradient
port: 5000
resources:
  replicas: 1
  instanceType: P4000

The ease of deploying on Gradient from a Dockerfile cannot be understated. First, create a new Project, and navigate to its Deployments tab in the Gradient GUI. Within the GUI to set up the Deployment, we can see step by step instructions for setting up. At the top of the page, select the link that says 'upload a deployment spec'. This will allow you to directly upload your spec. The sample deployment spec above can be used to deploy your new Dockerized Flask app to Gradient deployments. Simply replace the variables in the text editor that popped up when you clicked the link and paste your spec. You can also create a Deployment using the Gradient CLI command

gradient deployments create --name [deployment name] --projectId [your project id] --spec [path to your deployment spec file]
A sample deployment for this Flask app

Once you've uploaded your spec, navigate to the Deployments tab in your project if it doesn't redirect you, and click on your new Deployment. In the details section, you can grab the url for our newly deployed Flask API. If you click on that link, it will take you to the HTML interface for the application on the /upload page

The HTML forms a simple GUI for you to submit your image to the model for restoration

Following the  prompts in the GUI, upload the image you want to be restored. When you select the "Upload" button, it will automatically load your image into the inputs directory and initialize the GAN to run through the main() function of the app. Once it is complete, the webpage will be automatically redirected to display the new image within the HTML. From here, you can run the model again by returning to the original url and uploading a new photo.

In the first set of pixellated photos of Adele, we can see how the algorithm attempted to infer the curvature of her cheeks and chin but ultimately failed. We can thus conclude that the model is ill suited for artificially damaged or obfuscated images. In the second set, a group of old friends are photographed in a darkened area. The GFP-GAN performs much more appropriately because it is able to infer the subjects faces through low-light quite well. Photos taken from GFP-GAN github and reddit/r/restoration.

It's worth mentioning that this program works best with higher quality images that have been damaged in some way. Above are two sets of sample images, with the right photo being more ideal for the model and the other being an example of misapplication. If your photo is particularly blurry or pixelated, you may end up with some strange effects on your subjects in the photo that vary from the uncanny valley to outright body horror.

Conclusion

In this tutorial, we saw how to construct a Flask app to serve a PyTorch GFP-GAN implementation, to deploy it as a web accessible API endpoint with a simple HTML GUI, and to use this newly deployed application to restore damaged photos. This is a very simplistic Flask app, so feel free to modify it to better suit your needs. One possibility would be connecting the app to Reddit PRAW application and using it to automatically parse and respond to posts in some of the more popular image restoration subreddits, like R/estoration.

Be sure to tweet at us if you want to showcase any of your work building on Gradient with GFP-GAN! Be sure to check out the GitHub repo for today's project as well!

Add speed and simplicity to your Machine Learning workflow today

Get startedContact Sales

Spread the word

Keep reading