Skip to content

Commit 37241dd

Browse files
committed
updated dashboard
1 parent 131d212 commit 37241dd

File tree

7 files changed

+131
-18
lines changed

7 files changed

+131
-18
lines changed

Diff for: python/www/dash/app.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@
3737

3838
from layout import create_grid, create_navbar, create_stream_dialog, create_model_dialog
3939

40-
#import os
41-
#print(f'loaded {__file__} module from {__name__} (pid {os.getpid()})')
4240

4341
# create the dash app
4442
external_scripts = ['https://webrtc.github.io/adapter/adapter-latest.js']
@@ -54,6 +52,8 @@
5452
if len(config['dash']['users']) > 0:
5553
auth = dash_auth.BasicAuth(app, config['dash']['users'])
5654

55+
56+
# define the default layout
5757
app.layout = dash.html.Div([
5858
create_navbar(),
5959
create_grid(),
@@ -63,6 +63,7 @@
6363
dcc.Interval(id='refresh_timer', interval=config['dash']['refresh'])
6464
])
6565

66+
6667
@app.callback(Output('server_resources', 'data'),
6768
Input('refresh_timer', 'n_intervals'),
6869
Input('server_resources', 'data'))

Diff for: python/www/dash/assets/dropdown.css

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
/* input box */
3+
/*.Select-control {
4+
background-color: black;
5+
}*/
6+
7+
/* changes the text color of input box */
8+
/*.Select-value-label {
9+
color: white !important;
10+
}*/
11+
12+
/*.Select--single > .Select-control .Select-value, .Select-placeholder {
13+
border: 1px solid grey;
14+
border-radius: 4px;
15+
}*/
16+
17+
/* dropdown menu options */
18+
.VirtualizedSelectOption {
19+
background-color: black;
20+
color: white;
21+
}
22+
23+
/* dropdown menu hover effect */
24+
.VirtualizedSelectFocusedOption {
25+
background-color: black;
26+
opacity: .7;
27+
}
28+
29+
/* border on focus - default is blue
30+
* shadow box around input box. default is blue
31+
*/
32+
.is-focused:not(.is-open) > .Select-control {
33+
border-color: var(--primary);
34+
box-shadow: none;
35+
}
36+
37+
/* this colors the input box text and x of multi dropdown*/
38+
/*.Select--multi .Select-value {
39+
color: white;
40+
}*/

Diff for: python/www/dash/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
'title' : 'WebRTC Dashboard', # title of the dash app (used in browser title bar and navbar)
3030
'host' : '0.0.0.0', # hostname/IP of the frontend webserver (ignored by gunicorn)
3131
'port' : 8050, # port used for the frontend webserver (ignored by gunicorn)
32-
'refresh' : 1000, # the interval at which the server state is refreshed
32+
'refresh' : 2500, # the interval at which the server state is refreshed
3333
'users' : { # to enable basic authentication logins, add username/password pairs here
3434
# 'username' : 'password',
3535
},

Diff for: python/www/dash/layout/stream_options.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ def create_stream_options(stream={}):
3737

3838
html.Div([
3939
dbc.Label('Model', html_for='stream_options_model'),
40-
dbc.Select(options=list_models(), id='stream_options_model'),
40+
#dbc.Select(options=list_models(), multi=True, id='stream_options_model'),
41+
dcc.Dropdown(options=list_models(), multi=True, id='stream_options_model'),
4142
dbc.FormText("The DNN model to use for processing the stream"),
4243
], className='mb-3'),
4344

@@ -109,7 +110,7 @@ def stream_submit(n_clicks, name, source, model):
109110
raise PreventUpdate
110111

111112
print(f"adding stream {name} from source {source} with model {model}")
112-
Server.instance.add_resource('streams', name, source, [model])
113+
Server.instance.add_resource('streams', name, source, model)
113114
raise PreventUpdate
114115

115116
'''

Diff for: python/www/dash/server/model.py

+55-5
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,35 @@
2121
#
2222

2323
from jetson_inference import imageNet, detectNet
24-
from jetson_utils import Log
24+
from jetson_utils import cudaFont, Log
2525

2626

2727
class Model:
2828
"""
29-
Represents a DNN model
29+
Represents DNN models for classification, detection, segmentation, ect.
30+
These can be either built-in models or user-provided / user-trained.
3031
"""
3132
def __init__(self, server, name, type, model, labels='', input_layers='', output_layers=''):
33+
"""
34+
Load the model, either from a built-in pre-trained model or from a user-provided model.
35+
36+
Parameters:
37+
38+
server (Server) -- the backend server instance
39+
name (string) -- the name of the model
40+
type (string) -- the type of the model (classification, detection, ect)
41+
model (string) -- either a path to the model or name of the built-in model
42+
labels (string) -- path to the model's labels.txt file (optional)
43+
input_layers (string or dict) -- the model's input layer(s)
44+
output_layers (string or dict) -- the model's output layers()
45+
"""
3246
self.name = name
3347
self.type = type
3448
self.server = server
3549

3650
if type == 'classification':
3751
self.net = imageNet(model=model, labels=labels, input_blob=input_layers, output_blob=output_layers)
38-
52+
self.font = cudaFont()
3953
elif type == 'detection':
4054
if not output_layers:
4155
output_layers = {'scores': '', 'bbox': ''}
@@ -45,9 +59,45 @@ def __init__(self, server, name, type, model, labels='', input_layers='', output
4559
self.net = detectNet(model=model, labels=labels, input_blob=input_layers,
4660
output_cvg=output_layers['scores'],
4761
output_bbox=output_layers['bbox'])
48-
62+
else:
63+
raise ValueError(f"invalid model type '{type}'")
64+
4965
def get_config(self):
66+
"""
67+
Return a dict representation of the object.
68+
"""
5069
return {
5170
'name' : self.name,
5271
'type' : self.type,
53-
}
72+
}
73+
74+
def get_num_classes(self):
75+
"""
76+
Get the number of classes that the model supports.
77+
"""
78+
return self.net.GetNumClasses()
79+
80+
def get_class_name(self, class_id):
81+
"""
82+
Return the class name or description for the given class ID.
83+
"""
84+
return self.net.GetClassDesc(class_id)
85+
86+
def process(self, img):
87+
"""
88+
Process an image with the model and return the results.
89+
"""
90+
if self.type == 'classification':
91+
return self.net.Classify(img)
92+
elif self.type == 'detection':
93+
return self.net.Detect(img, overlay='none')
94+
95+
def visualize(self, img, results):
96+
"""
97+
Visualize the results on an image.
98+
"""
99+
if self.type == 'classification':
100+
str = "{:05.2f}% {:s}".format(results[1] * 100, self.get_class_name(results[0]))
101+
self.font.OverlayText(img, img.width, img.height, str, 5, 5, self.font.White, self.font.Gray40)
102+
elif self.type == 'detection':
103+
self.net.Overlay(img, results)

Diff for: python/www/dash/server/server.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
import time
2727
import psutil
2828
import random
29+
import traceback
2930
import threading
3031
import multiprocessing
3132
import setproctitle
3233

33-
from xmlrpc.server import SimpleXMLRPCServer
3434
from xmlrpc.client import ServerProxy
35+
from xmlrpc.server import SimpleXMLRPCServer
3536

3637
from jetson_utils import Log
3738

@@ -220,8 +221,11 @@ def process(self):
220221
"""
221222
Perform one interation of the processing loop.
222223
"""
223-
for stream in self.resources['streams'].values(): # TODO don't spin if no streams
224-
stream.process()
224+
if len(self.resources['streams']) > 0:
225+
for stream in self.resources['streams'].values():
226+
stream.process()
227+
else:
228+
time.sleep(1.0)
225229

226230
def add_resource(self, group, name, *args, **kwargs):
227231
"""
@@ -246,7 +250,7 @@ def add_resource(self, group, name, *args, **kwargs):
246250
return
247251
except Exception as error:
248252
Log.Error(f"[{self.name}] failed to create resource '{name}' in group '{group}'")
249-
Log.Error(f"{error}")
253+
traceback.print_exc()
250254
return
251255

252256
self.resources[group][name] = resource

Diff for: python/www/dash/server/stream.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
from jetson_utils import videoSource, videoOutput, Log
2424

25+
import pprint
26+
import traceback
2527

2628
class Stream:
2729
"""
@@ -49,6 +51,9 @@ def __init__(self, server, name, source, models=[]):
4951
# lookup models
5052
self.models = []
5153

54+
if isinstance(models, str):
55+
models = [models]
56+
5257
for model in models:
5358
if model in server.resources['models']:
5459
self.models.append(server.resources['models'][model])
@@ -57,11 +62,21 @@ def __init__(self, server, name, source, models=[]):
5762

5863

5964
def process(self):
65+
"""
66+
Perform one capture/process/output iteration
67+
"""
6068
try:
6169
img = self.source.Capture()
62-
except Exception as error:
70+
results = [model.process(img) for model in self.models]
71+
72+
print('model results:')
73+
pprint.pprint(results)
74+
75+
for model, result in zip(self.models, results):
76+
model.visualize(img, result)
77+
except:
6378
# TODO check if stream is still open, if not reconnect?
64-
Log.Error(f"{error}")
79+
traceback.print_exc()
6580
return
6681

6782
if self.frame_count % 25 == 0 or self.frame_count < 15:
@@ -71,8 +86,10 @@ def process(self):
7186
self.frame_count += 1
7287

7388
def get_config(self):
74-
# TODO add stats or runtime_stats option for easy frontend state-change comparison?
75-
# the videoOptions could be dynamic as well... (i.e. framerate - actually that is not?)
89+
"""
90+
TODO add stats or runtime_stats option for easy frontend state-change comparison?
91+
the videoOptions could be dynamic as well... (i.e. framerate - actually that is not?)
92+
"""
7693
return {
7794
"name" : self.name,
7895
"source" : self.source.GetOptions(),

0 commit comments

Comments
 (0)