In this tutorial, we’ll learn how to transform our command-line based NFT Search Engine from the previous tutorial and turn it into a full-blown web application using Python and Flask.
If you are pretty new to Flask, I recommend starting with the official quick start guild. Or, if you love video tutorials, my favorite two are both by Corey Schafer and freeCodeCamp.org. Feel free to check them out yourself.
Today’s tutorial is part two in our two-part series of building an NFT Search Engine using Python, OpenCV and Flask:
- Building an NFT Search Engine in 3 steps using Python and OpenCV (previous tutorial)
- Adding a web interface to our NFT Search Engine with Flask (this tutorial)
Configuring your Development Environment
This tutorial on building a web interface for our NFT Image Search Engine uses OpenCV, Flask and other necessary libraries. If you intend to follow this tutorial, I suggest you take at least a few minutes of your time to configure your development environment.
Good thing you don’t have to worry too much about OpenCV’s installation, as I’ve covered them in this tutorial here. Mostly for Linux users. You can easily pip install them, and you’re ready to go.
Project Structure
Before we get started implementing our Python script for our NFT search engine with Python and OpenCV, let’s first review our project directory structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ tree . --dirsfirst . ├── neuraspike │ ├── __init__.py │ ├── config.py │ ├── descriptor.py │ ├── searcher.py │ └── utils.py ├── static │ ├── css │ │ └── main.css │ ├── output │ │ └── features.csv │ ├── upload [3 entries] │ └── img [3 categories, 300 images] ├── templates │ └── index.html ├── app.py ├── extract_features.py └── requirements.txt 6 directories, 14 files |
The static directory contains static files, like style sheets, images, JavaScript files, etc.
The templates
directory houses our application template, as you have seen the wonderfully structured and designed front-end part of the application.
Inside of the neurapsike
module, we have three Python scripts for describing our image. Which performs a quick search between the image uploaded by the user and among the already indexed images and, some helper functions for building this project.
The extract_features.py
file is our training script, which performs the process of extracting essential features from each image and saving the result into a database (in our case, a CSV file)
The app.py
file is the backend script, which lets the user upload their image and then searches among other images, returning the result based on the level of relevance. This means the first image represents the image close to the query image.
Now we have all these details cleared out, let’s head to the implementation section.
Workflow
Since we’ve already built the NFT search engine in the previous tutorial, we just need to transfer the relevant code already written to our flask application.
Backend
For now, we’ll focus on implementing a single route (also known as an endpoint):
- The main route (“/”): This page only works with POST and GET requests. Once the user uploads an image and clicks the search button (sends a post request), the results are computed before being displayed to the user.
Main Route
This route is meant to perform the following operations:
- Handle the POST and GET requests.
- Receive an image uploaded by the user and searches for similar NFT tokens (using the features.csv file)
- Returns similar NFT tokens (in a JSON format), which are rendered to the user.
Open a new file called app.py
, paste the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | # import necessary packages from flask import Flask, render_template, request, jsonify from neuraspike import descriptor from neuraspike import searcher from neuraspike import config from neuraspike import utils from skimage import io import os # initialize the flask application app = Flask(__name__) # defined the path to the indexed feature, database of images, and query image index_path = os.path.sep.join([config.BASE_STATIC_PATH, config.PATH_TO_INDEX]) image_path = os.path.sep.join([config.BASE_STATIC_PATH, config.PATH_TO_IMAGE]) upload_path = os.path.sep.join([config.BASE_STATIC_PATH, config.PATH_TO_UPLOAD_DIR]) INDEX = os.path.join(os.path.dirname(__file__), index_path) print("[Info] web application is up and running") @app.route('/', methods=["GET", "POST"]) def index(): if request.method == "POST": # initialize an empty list to store the results RESULT = [] # request the path to the file image_url = request.files['query-image'] # perform a quick check if an image was uploaded or not if image_url is None or image_url.filename == "": return jsonify({'error': 'Sorry, no file was uploaded'}) # check if the image format uploaded is considered as a type # of image based on the uploaded format (.png, .jpg, .jpeg) if not utils.is_upload_image(image_url.filename): return jsonify({'error': 'Sorry, image format not supported'}) try: # load the query image, reverse the input color-space from RGB into BGR # as the extract_color_histogram() function expects the input to be in BGR. # and then describe the image using color histograms query_image = io.imread(image_url) query_image = query_image[:, :, ::-1] features = descriptor.extract_color_histogram(query_image) # perform the search for similar images within among other features results = searcher.perform_image_search(features, INDEX) # loop over the results and append the correct path to the image folders for (image_id, score) in results: # update the path to the img of images image_id = os.path.sep.join([image_path, image_id]) RESULT.append((round(score, 2), image_id)) # defined the path to the uploaded image upload_image_path = os.path.sep.join([upload_path, image_url.filename]) # return success by rendering the found similar images return render_template("index.html", query_image_path=upload_image_path, output=RESULT) except Exception as e: # return error return jsonify({"sorry": f"Sorry, no results! Please try again./" f"\nReason: {repr(e)}"}), 500 else: return render_template("index.html") if __name__ == '__main__': app.run(port=5000, debug=True) |
What’s happening here?
- We defined the endpoint (or route),
"/"
along with both the allowed HTTP request and respond methods:methods=["GET", "POST"]
. Then check if a"POST"
or"GET"
method was made. - If it was a post method, we’ll grab the path to the image, validate if the image was loaded or not; and if it wasn’t, return an error response.
- If everything is alright, we’ll check if the uploaded image file is supported; in this scenario, we will be using only:
.png
,.jpg
, and.jpeg
files. - Once the checks are done, we’ll further try to load the image while encapsulating it within a try/except block to handle unexpected errors.
- Then finally, on Line 62 – 63, the list of results is passed dynamically from our route handler to the template by appending the key/value pair to the
render_templates
function. Those are the uploaded image and the result from the search.
Config.py
Next, in the configuration file, defined variables which hold the paths to where certainly files and folders are located within the web application.
1 2 3 4 5 6 7 8 9 10 11 12 13 | import os # specify the path to the where the extracted features are been stored PATH_TO_INDEX = os.path.sep.join(['output', "features.csv"]) # initialize the path to where our output will be stored BASE_STATIC_PATH = "static" # initialize the path to where the uploaded images will be stored PATH_TO_IMAGE = "img" # initialize the path to the query images PATH_TO_UPLOAD_DIR = "upload" |
Utils.py
In the utils.py
script, we included a function that validates if the image uploaded includes the following file extensions format: “.jpg”, “.png” and “.jpeg”, before making a query to the database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # different type of image formats to consider image_extensions = (".jpg", ".png", ".jpeg") def is_upload_image(image_path): # check for only image file that was uploaded is on any of these # images types which included ".jpg", ".png", ".jpeg", ".bmp", # ".tif" or ".tiff" image_path = image_path.lower() is_image_type = image_path.endswith(image_extensions) if is_image_type: return True else: return False |
Front-end
With our back-end code written, we need to fix the structure of how the elements are structured and feel of the displayed result using (HTML, CSS and BOOTSTRAP).
Open the index.html
template file and paste the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <!-- title of the project --> <title>Image Search Engine</title> <!-- stylesheets --> <link rel="stylesheet" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous"> <link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet"> </head> <body> <div class="container my-3"> <div class="row"> <div class="col-12"> <h1 class="text-center mb-5">NFT Search Engine</h1> </div> <!-- Display the query image --> <div class="col-md-6 col-12 mb-3 mx-auto"> <div class="card"> <form method="POST" enctype="multipart/form-data" class="p-3"> <div class="form-row"> <div class="col-12 mb-3"> <label for="uploadImage">Upload an Image</label> <input type="file" name="query-image" class="form-control h-auto" id="uploadImage" required> </div> <div class="col-12 text-center"> <button type="submit" class="btn submit-button"> Submit </button> </div> {% if query_image_path %} <div class="col-12 mt-3 text-center"> <img src="{{ query_image_path }}" width="250px" alt="image" class="img-fluid"> </div> {% endif %} </div> </form> </div> </div> </div> <!-- Display the results of the queried images --> <div class="row"> {% if output %} <div class="col-12 mb-3"> <h2 class="text-center">Results</h2> </div> {% endif %} {% for (score, path_to_image) in output %} <div class="col-12 mb-3"> <div class="card"> <div class="row"> <div class="col-md-5 mb-3"> <img src="{{ path_to_image }}" height="200px" alt="image" class="img-fluid rounded"> </div> <div class="col-md-7"> <h4>Score: <b>{{ score }}</b></h4> </div> </div> </div> </div> {% endfor %} </div> </div> </body> </html> |
CSS
With that covered, let’s add some style sheets to help make the overall user interface of the application more user-friendly and easy for the eyes.
So open the main.css
file and type the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | * { box-sizing: border-box; } body { padding: 0; margin: 0; background: #8e9eab; /* fallback for old browsers */ /* Chrome 10-25, Safari 5.1-6 */ background: -webkit-linear-gradient(to bottom, #eef2f3, #8e9eab); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ background: linear-gradient(to bottom, #eef2f3, #8e9eab); background-repeat: no-repeat; min-height: 100vh; } .card { padding: 1rem; border: none; border-radius: 30px; box-shadow: rgba(0, 0, 0, 0.01) 0 0 1px, rgba(0, 0, 0, 0.04) 0 4px 8px, rgba(0, 0, 0, 0.04) 0 16px 24px, rgba(0, 0, 0, 0.01) 0 24px 32px; } .submit-button { background: #b92b27; /* fallback for old browsers */ /* Chrome 10-25, Safari 5.1-6 */ background: -webkit-linear-gradient(to right, #1565C0, #b92b27); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ background: linear-gradient(to right, #1565C0, #b92b27); color: #fff; width: 150px; border-radius: 30px; padding: 12px 0; } .submit-button:hover { color: #e7e7e7; } |
Application Demo
Let’s put our NFTs Image Search Engine into action. So fire up your terminal and run the app.py
file.
1 2 | $ python3 app.py $ |
Credits
The fronted part of the application was designed by my Friend Vanja Paunović. Who assisted me in making the feel of the application look much more appealing. Also, it will be great to support his YouTube Channel by hitting the subscribe button.
Further Reading
We have listed some useful resources below if you thirst for more reading.
- Building an NFT Search Engine in 3 steps using Python and OpenCV
- Content-based image retrieval, wikipedia.
- 3 Rookie Mistakes People Make Installing OpenCV | Avoid It!
- Why is Python the most popular language for Data Science
- A Simple Walk-through with NumPy for Data Science
- Training an Emotion Detection System using PyTorch
- Real-time Emotion Detection System with PyTorch and OpenCV