Skip to content

Commit

Permalink
added Recognizer webapp
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-nv committed Apr 13, 2023
1 parent d41e24f commit 8e5bf1e
Show file tree
Hide file tree
Showing 18 changed files with 12,177 additions and 0 deletions.
117 changes: 117 additions & 0 deletions python/www/recognizer/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
#
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the 'Software'),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#

import os
import http
import flask
import werkzeug
import argparse

from stream import Stream
from utils import rest_property


parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, epilog=Stream.usage())

parser.add_argument("--host", default='0.0.0.0', type=str, help="interface for the webserver to use (default is all interfaces, 0.0.0.0)")
parser.add_argument("--port", default=8050, type=int, help="port used for webserver (default is 8050)")
parser.add_argument("--ssl-key", default=os.getenv('SSL_KEY'), type=str, help="path to PEM-encoded SSL/TLS key file for enabling HTTPS")
parser.add_argument("--ssl-cert", default=os.getenv('SSL_CERT'), type=str, help="path to PEM-encoded SSL/TLS certificate file for enabling HTTPS")
parser.add_argument("--title", default='Hello AI World | Recognizer', type=str, help="the title of the webpage as shown in the browser")
parser.add_argument("--input", default='webrtc://@:8554/input', type=str, help="input camera stream or video file")
parser.add_argument("--output", default='webrtc://@:8554/output', type=str, help="WebRTC output stream to serve from --input")
parser.add_argument("--classification", default='', type=str, help="load classification model (see imageNet arguments)")
parser.add_argument("--labels", default='', type=str, help="path to labels.txt for loading a custom model")
parser.add_argument("--colors", default='', type=str, help="path to colors.txt for loading a custom model")
parser.add_argument("--input-layer", default='', type=str, help="name of input layer for loading a custom model")
parser.add_argument("--output-layer", default='', type=str, help="name of output layer(s) for loading a custom model (comma-separated if multiple)")
parser.add_argument("--data", default='data', type=str, help="path to store dataset and models under")

args = parser.parse_known_args()[0]


# create Flask & stream instance
app = flask.Flask(__name__)
stream = Stream(args)

# Flask routes
@app.route('/')
def index():
return flask.render_template('index.html', title=args.title, send_webrtc=args.input.startswith('webrtc'),
input_stream=args.input, output_stream=args.output,
classification=os.path.basename(args.classification))

@app.route('/dataset/classes', methods=['GET'])
def dataset_classes():
return stream.dataset.classes

@app.route('/dataset/active_tags', methods=['GET', 'PUT'])
def dataset_active_tags():
return rest_property(stream.dataset.GetActiveTags, stream.dataset.SetActiveTags, str)

@app.route('/dataset/recording', methods=['GET', 'PUT'])
def dataset_recording():
return rest_property(stream.dataset.IsRecording, stream.dataset.SetRecording, bool)

@app.route('/dataset/upload', methods=['POST'])
def dataset_upload():
file = flask.request.files.get('file')

if not file or not file.filename:
print('/dataset/upload -- invalid request (missing file)')
return ('', http.HTTPStatus.BAD_REQUEST)

file.filename = werkzeug.utils.secure_filename(file.filename)
saved_path = stream.dataset.Upload(file)

if not saved_path:
print(f"/dataset/upload -- failed to save '{file.mimetype}' to dataset ({file.filename})")
return ('', http.HTTPStatus.INTERNAL_SERVER_ERROR)

return (saved_path, http.HTTPStatus.OK)

if args.classification:
@app.route('/classification/enabled', methods=['GET', 'PUT'])
def classification_enabled():
return rest_property(stream.model.IsEnabled, stream.model.SetEnabled, bool)

@app.route('/classification/confidence_threshold', methods=['GET', 'PUT'])
def classification_confidence_threshold():
return rest_property(stream.model.net.GetThreshold, stream.model.net.SetThreshold, float)

@app.route('/classification/output_smoothing', methods=['GET', 'PUT'])
def classification_output_smoothing():
return rest_property(stream.model.net.GetSmoothing, stream.model.net.SetSmoothing, float)


# start stream thread
stream.start()

# check if HTTPS/SSL requested
ssl_context = None

if args.ssl_cert and args.ssl_key:
ssl_context = (args.ssl_cert, args.ssl_key)

# start the webserver
app.run(host=args.host, port=args.port, ssl_context=ssl_context, debug=True, use_reloader=False)
180 changes: 180 additions & 0 deletions python/www/recognizer/dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
#
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the 'Software'),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
import os
import json
import queue
import datetime
import threading
import traceback

from jetson_utils import cudaMemcpy, saveImage


class Dataset(threading.Thread):
"""
Class for saving multi-label image tagging datasets.
"""
def __init__(self, args):
"""
Create dataset object.
"""
super().__init__()

self.args = args
self.tags = {} # map from image filename => tags
self.active_tags = [] # list of tags to be applied to new images
self.classes = [] # ['apple', 'banana', 'orange']

self.queue = queue.Queue()
self.recording = False

# create directory structure
self.root_dir = self.args.data
self.image_dir = os.path.join(self.root_dir, 'images')

os.makedirs(self.image_dir, exist_ok=True)

# load existing annotations
self.tags_path = os.path.join(self.root_dir, 'tags.json')

if os.path.exists(self.tags_path):
with open(self.tags_path, 'r') as file:
self.tags = json.load(file)
self.update_class_labels()
print(f"dataset -- loaded tags for {len(self.tags)} images, {len(self.classes)} from {self.tags_path}")

def process(self):
"""
Process the queue of incoming images.
"""
try:
img, timestamp = self.queue.get(timeout=1)
except queue.Empty:
pass
else:
filename = f"{timestamp.strftime('%Y%m%d_%H%M%S_%f')}.jpg"
filepath = os.path.join(self.image_dir, filename)

saveImage(filepath, img, quality=85)
self.ApplyTags(filename)

del img

def run(self):
"""
Run the dataset thread's main loop.
"""
while True:
try:
self.process()
except:
traceback.print_exc()

def AddImage(self, img):
"""
Adds an image to the queue to be saved to the dataset.
"""
if not self.recording or len(self.active_tags) == 0:
return

timestamp = datetime.datetime.now()
img_copy = cudaMemcpy(img)
self.queue.put((img_copy, timestamp))

def Upload(self, file):
path = os.path.join(self.image_dir, file.filename)
print(f"/dataset/upload -- saving '{file.mimetype}' to {path}")
file.save(path)
self.ApplyTags(file.filename)
return path

def IsRecording(self):
"""
Returns true if the stream is currently being recorded, false otherwise
"""
return self.recording

def SetRecording(self, recording):
"""
Enable/disable recording of the input stream.
"""
self.recording = recording

def GetActiveTags(self):
"""
Return a comma-separated string of the currently active labels applied to images as they are recorded.
"""
return ','.join(self.active_tags)

def SetActiveTags(self, labels):
"""
Set the list of active labels (as a comma-separated or semicolon-separated string)
that will be applied to incoming images as they are recorded into the dataset.
"""
if labels:
self.active_tags = labels.replace(';', ',').split(',')
self.active_tags = [label.strip().lower() for label in self.active_tags]
else:
self.active_tags = []

def ApplyTags(self, filename, tags=None, flush=True):
"""
Apply tag annotations to the image and save them to disk (by default, the active tags will be applied)
"""
if tags is None:
tags = self.active_tags

if len(tags) == 0:
return

self.tags[filename] = self.active_tags
self.update_class_labels()

if flush:
self.SaveTags()

def SaveTags(self, path=''):
"""
Flush the image tags to the JSON annotations file on disk.
"""
if not path:
path = self.tags_path

with open(path, 'w') as file:
json.dump(self.tags, file, indent=4)

def update_class_labels(self):
"""
Sync the list of class labels from the tag annotations.
"""
classes = []

for tags in self.tags.values():
for tag in tags:
if tag not in classes:
classes.append(tag)

self.classes = sorted(classes)
print(f'dataset -- class labels: {self.classes}')


Loading

0 comments on commit 8e5bf1e

Please sign in to comment.