From 6f168d60be270ffa75d0577bbc7344c278c45329 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Thu, 19 May 2022 23:04:12 +0100 Subject: [PATCH 1/2] v1 --- apps/dash-lyft-explorer/.gitignore | 196 +++++++++- apps/dash-lyft-explorer/README.md | 2 +- apps/dash-lyft-explorer/app.py | 364 ++---------------- apps/dash-lyft-explorer/assets/css/header.css | 51 +++ apps/dash-lyft-explorer/assets/dash-logo.png | Bin 17304 -> 0 bytes .../assets/{ => github}/demo.gif | Bin .../assets/images/dash-logo.png | Bin 0 -> 23021 bytes apps/dash-lyft-explorer/constants.py | 31 ++ apps/dash-lyft-explorer/requirements.txt | 20 +- apps/dash-lyft-explorer/runtime.txt | 2 +- apps/dash-lyft-explorer/utils/components.py | 105 +++++ .../utils/helper_functions.py | 15 + apps/dash-lyft-explorer/utils/model.py | 159 ++++++++ 13 files changed, 593 insertions(+), 352 deletions(-) create mode 100644 apps/dash-lyft-explorer/assets/css/header.css delete mode 100644 apps/dash-lyft-explorer/assets/dash-logo.png rename apps/dash-lyft-explorer/assets/{ => github}/demo.gif (100%) create mode 100644 apps/dash-lyft-explorer/assets/images/dash-logo.png create mode 100644 apps/dash-lyft-explorer/constants.py create mode 100644 apps/dash-lyft-explorer/utils/components.py create mode 100644 apps/dash-lyft-explorer/utils/helper_functions.py create mode 100644 apps/dash-lyft-explorer/utils/model.py diff --git a/apps/dash-lyft-explorer/.gitignore b/apps/dash-lyft-explorer/.gitignore index 95500b68e..90ecc9b06 100644 --- a/apps/dash-lyft-explorer/.gitignore +++ b/apps/dash-lyft-explorer/.gitignore @@ -1,5 +1,191 @@ -__pycache__ -.vscode -*.ipynb_checkpoints* -./data -./data.zip \ No newline at end of file +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ +data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-lyft-explorer/README.md b/apps/dash-lyft-explorer/README.md index c6b19855b..6bf8a1b95 100644 --- a/apps/dash-lyft-explorer/README.md +++ b/apps/dash-lyft-explorer/README.md @@ -19,7 +19,7 @@ This shows how to build a real-time AV explorer from scratch using Dash Deck and 📰 [Article](https://medium.com/plotly/the-history-of-autonomous-vehicle-datasets-and-3-open-source-python-apps-for-visualizing-them-afee9d13f58a) -![demo](assets/demo.gif) +![demo](assets/github/demo.gif) ## Instructions diff --git a/apps/dash-lyft-explorer/app.py b/apps/dash-lyft-explorer/app.py index d2bd79af5..c20cdd1f1 100644 --- a/apps/dash-lyft-explorer/app.py +++ b/apps/dash-lyft-explorer/app.py @@ -1,335 +1,32 @@ -import os -import time -import colorlover as cl -import dash +from dash import Dash, html, dcc, Input, Output, State, callback_context, no_update import dash_bootstrap_components as dbc -import dash_core_components as dcc -import dash_html_components as html -import dash_deck -from dash.dependencies import Input, Output, State -from lyft_dataset_sdk.lyftdataset import LyftDataset, LyftDatasetExplorer -from lyft_dataset_sdk.utils.data_classes import Box, LidarPointCloud, RadarPointCloud -import numpy as np import pandas as pd -from PIL import Image -import plotly.graph_objects as go -import plotly.express as px -import pydeck as pdk - - -def Header(name, app): - title = html.H2(name, style={"margin-top": 5}) - logo = html.Img( - src=app.get_asset_url("dash-logo.png"), style={"float": "right", "height": 60} - ) - link = html.A(logo, href="https://plotly.com/dash/") - - return dbc.Row([dbc.Col(title, md=8), dbc.Col(link, md=4)]) - - -def unsnake(st): - """BECAUSE_WE_DONT_READ_LIKE_THAT""" - return st.replace("_", " ").title() - - -def build_deck(mode, pc_df, polygon_data): - if mode == "first_person": - view = pdk.View(type="FirstPersonView", controller=True) - view_state = pdk.ViewState(latitude=0, longitude=0, bearing=-90, pitch=15) - point_size = 10 - elif mode == "orbit": - view = pdk.View(type="OrbitView", controller=True) - view_state = pdk.ViewState( - target=[0, 0, 1e-5], - controller=True, - zoom=23, - rotation_orbit=-90, - rotation_x=15, - ) - point_size = 3 - - else: - view_state = pdk.ViewState( - latitude=0, - longitude=0, - bearing=45, - pitch=50, - zoom=20, - max_zoom=30, - position=[0, 0, 1e-5], - ) - view = pdk.View(type="MapView", controller=True) - point_size = 1 - - pc_layer = pdk.Layer( - "PointCloudLayer", - data=pc_df, - get_position=["x", "y", "z"], - get_color=[255, 255, 255], - auto_highlight=True, - pickable=False, - point_size=point_size, - coordinate_system=2, - coordinate_origin=[0, 0], - ) - - box_layer = pdk.Layer( - "PolygonLayer", - data=polygon_data, - stroked=True, - pickable=True, - filled=True, - extruded=True, - opacity=0.2, - wireframe=True, - line_width_min_pixels=1, - get_polygon="polygon", - get_fill_color="color", - get_line_color=[255, 255, 255], - get_line_width=0, - coordinate_system=2, - get_elevation="elevation", - ) - - tooltip = {"html": "Label: {name}"} - - r = pdk.Deck( - [pc_layer, box_layer], - initial_view_state=view_state, - views=[view], - tooltip=tooltip, - map_provider=None, - ) - - return r - - -def compute_pointcloud_for_image( - lv5, - sample_token: str, - dot_size: int = 2, - pointsensor_channel: str = "LIDAR_TOP", - camera_channel: str = "CAM_FRONT", - out_path: str = None, -): - """Scatter-plots a point-cloud on top of image. - Args: - sample_token: Sample token. - dot_size: Scatter plot dot size. - pointsensor_channel: RADAR or LIDAR channel name, e.g. 'LIDAR_TOP'. - camera_channel: Camera channel name, e.g. 'CAM_FRONT'. - out_path: Optional path to save the rendered figure to disk. - Returns: - tuple containing the points, array of colors and a pillow image - """ - sample_record = lv5.get("sample", sample_token) - - # Here we just grab the front camera and the point sensor. - pointsensor_token = sample_record["data"][pointsensor_channel] - camera_token = sample_record["data"][camera_channel] - - points, coloring, im = lv5.explorer.map_pointcloud_to_image( - pointsensor_token, camera_token - ) - - return points, coloring, im - - -def render_box_in_image(lv5, im, sample: str, camera_channel: str): - camera_token = sample["data"][camera_channel] - data_path, boxes, camera_intrinsic = lv5.get_sample_data( - camera_token, flat_vehicle_coordinates=False - ) - - arr = np.array(im) - - for box in boxes: - c = NAME2COLOR[box.name] - box.render_cv2(arr, normalize=True, view=camera_intrinsic, colors=(c, c, c)) - - new = Image.fromarray(arr) - return new - - -def get_token_list(scene): - token_list = [scene["first_sample_token"]] - sample = lv5.get("sample", token_list[0]) - - while sample["next"] != "": - token_list.append(sample["next"]) - sample = lv5.get("sample", sample["next"]) - - return token_list - - -def build_figure(lv5, sample, lidar, camera, overlay): - points, coloring, im = compute_pointcloud_for_image( - lv5, sample["token"], pointsensor_channel=lidar, camera_channel=camera - ) - - if "boxes" in overlay: - im = render_box_in_image(lv5, im, sample, camera_channel=camera) - - fig = px.imshow(im, binary_format="jpeg", binary_compression_level=2) - - if "pointcloud" in overlay: - fig.add_trace( - go.Scattergl( - x=points[0,], - y=points[1,], - mode="markers", - opacity=0.4, - marker_color=coloring, - marker_size=3, - ) - ) - - fig.update_layout( - margin=dict(l=10, r=10, t=0, b=0), - paper_bgcolor="rgba(0,0,0,0)", - plot_bgcolor="rgba(0,0,0,0)", - hovermode=False, - ) - fig.update_xaxes(showticklabels=False, showgrid=False, range=(0, im.size[0])) - fig.update_yaxes(showticklabels=False, showgrid=False, range=(im.size[1], 0)) - - return fig - +import numpy as np +from lyft_dataset_sdk.utils.data_classes import LidarPointCloud -# Variables -CAMERAS = [ - "CAM_FRONT", - "CAM_BACK", - "CAM_FRONT_ZOOMED", - "CAM_FRONT_LEFT", - "CAM_FRONT_RIGHT", - "CAM_BACK_RIGHT", - "CAM_BACK_LEFT", -] -LIDARS = ["LIDAR_TOP", "LIDAR_FRONT_RIGHT", "LIDAR_FRONT_LEFT"] +from constants import INITIAL_TOKEN, token_list, lv5, NAME2COLOR +from utils.components import Header, CONTROLS, DECK_CARD +from utils.model import build_figure, build_deck -NAME2COLOR = dict( - zip( - ["bus", "car", "other_vehicle", "pedestrian", "truck"], - cl.to_numeric(cl.scales["5"]["div"]["Spectral"]), - ) +app = Dash( + __name__, + external_stylesheets=[dbc.themes.CYBORG], + title="Lyft Interactive Dashboard", ) - -# Create Lyft object -lv5 = LyftDataset(data_path="./data", json_path="./data/train_data", verbose=True) -# Load a single scene -scene = lv5.scene[0] -token_list = get_token_list(scene) -INITIAL_TOKEN = scene["first_sample_token"] - - -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.CYBORG]) server = app.server -controls = [ - dbc.FormGroup( - [ - dbc.Label("Camera Position"), - dbc.Select( - id="camera", - options=[ - {"label": unsnake(s.replace("CAM_", "")), "value": s} - for s in CAMERAS - ], - value=CAMERAS[0], - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label("Image Overlay"), - dbc.Checklist( - id="overlay", - value=[], - options=[ - {"label": x.title(), "value": x} for x in ["pointcloud", "boxes"] - ], - inline=True, - switch=True, - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label("Frame"), - html.Br(), - dbc.Spinner( - dbc.ButtonGroup( - [ - dbc.Button( - "Prev", id="prev", n_clicks=0, color="primary", outline=True - ), - dbc.Button("Next", id="next", n_clicks=0, color="primary"), - ], - id="button-group", - style={"width": "100%"}, - ), - spinner_style={"margin-top": 0, "margin-bottom": 0}, - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label("Progression"), - dbc.Spinner( - dbc.Input( - id="progression", type="range", min=0, max=len(token_list), value=0 - ), - spinner_style={"margin-top": 0, "margin-bottom": 0}, - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label("Lidar Position"), - dbc.Select( - id="lidar", - value=LIDARS[0], - options=[ - {"label": unsnake(s.replace("LIDAR_", "")), "value": s} - for s in LIDARS - ], - ), - ] - ), - dbc.FormGroup( - [ - dbc.Label("Lidar View Mode"), - dbc.Select( - id="view-mode", - value="map", - options=[ - {"label": unsnake(x), "value": x} - for x in ["first_person", "orbit", "map"] - ], - ), - ] - ), -] - -deck_card = dbc.Card( - dash_deck.DeckGL(id="deck-pointcloud", tooltip={"html": "Label: {name}"}), - body=True, - style={"height": "calc(95vh - 215px)"}, -) app.layout = dbc.Container( [ Header("Dash Lyft Perception", app), - html.Br(), - dbc.Card(dbc.Row([dbc.Col(c) for c in controls], form=True), body=True), - html.Br(), + dbc.Card(dbc.Row([dbc.Col(c) for c in CONTROLS]), body=True), dbc.Row( [ dbc.Col(dbc.Card(dcc.Graph(id="graph-camera"), body=True), md=5), - dbc.Col(deck_card, md=7), - ] + dbc.Col(DECK_CARD, md=7), + ], + className="app-body" ), dcc.Store(id="sample-token", data=INITIAL_TOKEN), ], @@ -339,11 +36,12 @@ def build_figure(lv5, sample, lidar, camera, overlay): @app.callback( Output("progression", "value"), - [Input("prev", "n_clicks"), Input("next", "n_clicks")], - [State("progression", "value")], + Input("prev", "n_clicks"), + Input("next", "n_clicks"), + State("progression", "value"), ) def update_current_token(btn_prev, btn_next, curr_progress): - ctx = dash.callback_context + ctx = callback_context prop_id = ctx.triggered[0]["prop_id"] if "next" in prop_id: @@ -351,23 +49,19 @@ def update_current_token(btn_prev, btn_next, curr_progress): elif "prev" in prop_id: return max(0, int(curr_progress) - 1) else: - return dash.no_update + return no_update @app.callback( - [ - Output("graph-camera", "figure"), - Output("deck-pointcloud", "data"), - Output("button-group", "children"), - Output("progression", "type"), - ], - [ - Input("progression", "value"), - Input("camera", "value"), - Input("lidar", "value"), - Input("overlay", "value"), - Input("view-mode", "value"), - ], + Output("graph-camera", "figure"), + Output("deck-pointcloud", "data"), + Output("button-group", "children"), + Output("progression", "type"), + Input("progression", "value"), + Input("camera", "value"), + Input("lidar", "value"), + Input("overlay", "value"), + Input("view-mode", "value"), ) def update_graphs(progression, camera, lidar, overlay, view_mode): token = token_list[int(progression)] @@ -402,7 +96,7 @@ def update_graphs(progression, camera, lidar, overlay, view_mode): fig = build_figure(lv5, sample, lidar, camera, overlay) r = build_deck(view_mode, pc_df, polygon_data) - return fig, r.to_json(), dash.no_update, dash.no_update + return fig, r.to_json(), no_update, no_update if __name__ == "__main__": diff --git a/apps/dash-lyft-explorer/assets/css/header.css b/apps/dash-lyft-explorer/assets/css/header.css new file mode 100644 index 000000000..e6d0e204b --- /dev/null +++ b/apps/dash-lyft-explorer/assets/css/header.css @@ -0,0 +1,51 @@ +.header { + padding: 5px 15px 5px 10px; +} + +.header-logos { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + gap: 5%; +} + +.app-body { + padding: 20px 10px 10px 10px; +} + + +/* Demo button css */ +.demo-button { + font-family: Open Sans,sans-serif; + text-decoration: none; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + height: 2.5rem; + font-size: 13px; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 2px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 15px; + padding-bottom: 15px; +} +.demo-button:hover { + color: #7A76FF; +} diff --git a/apps/dash-lyft-explorer/assets/dash-logo.png b/apps/dash-lyft-explorer/assets/dash-logo.png deleted file mode 100644 index 9ed7cd16048d9edfd25eda43682bdaba43369ca1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17304 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 VZHA12E1=h)7s^`CN|h`_|1THltRw&c diff --git a/apps/dash-lyft-explorer/assets/demo.gif b/apps/dash-lyft-explorer/assets/github/demo.gif similarity index 100% rename from apps/dash-lyft-explorer/assets/demo.gif rename to apps/dash-lyft-explorer/assets/github/demo.gif diff --git a/apps/dash-lyft-explorer/assets/images/dash-logo.png b/apps/dash-lyft-explorer/assets/images/dash-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..984dd57ab53b7f9fa47df219e25df91a0885613d GIT binary patch literal 23021 zcmZsD1wd5K_Ao3fDJ@+~ONVqW-3`(WN_Tg+q?E)`N|(|NE-jtX0@B^hxBg!I-h2NK zhP`v|otaZJ=ggUNmS|OF8FW-)R5&;|bU9f`bvQVnFswd+j0pSO-~7}KyTH4u%ZS5O zjgs!dZo({e$Z#*FCl62Z@47nt>wmTZz`;dY z!vX)ZR8cO3yPDht}7fIEA^j0 zc(2c*9&(RqZX^OhI1ub`GurUcyv= zwGe>S|2$@+0{zv*%~qI7M@bbV;pk!s;$dZHWv3EB1%W_9E*9?u)Fq|=*&TK#Ol9Tf z<|M$z=IQCl>iLG%(d9iG2R}bQ8#^Z(CnpQ61&gb7ZMyC7*0-7 zOw$YgupKRtbk2KK@Eax^S|T*)6-$iI!T3jua^_XCh!|?MnnpEwMvqr7Bm}iS%7!n3 zUMrjy@--#JCezOF>@6f2CWV;pnoa`i+rPe!mMdki>6v$t7pqp1kEu4tI7%Iw#iMBV zQU56W65LkX>iWD|fRs)4qmW|z!h9mijuyh=l=b+Cx z8@H#m)es0_>oc#&!`1I;KGL3~F`=8rMy0sltC?fGo?Pc_y-jJ3BGGft8&%C&6e#&T z+)5%>1jB-^xaN+I4#a`Oy*Hb8a@nu9wQa6D4z)6m3JDN?J=1^z|5F~3y~?`+v{-X} z{f@hLnN)I1pMIzq-=v4*vMK+r3Hm6tc_)ser-wGQHLr8D^hIfeqVr&{*m%D7Q9_U+ zWBAAVZD-)-7?XAmzrW+J>D_%7BSnG@+g?=seU5FHVjf3+SHH#S?bWBGK>uqKpWTwv zpGY0Gu4Y2l(Mq>VB>nv^xX6fe=yMLjwNnce`ko%N>Bju6pM3q?g?K%$$(}9v$*^Bu zt#1Tgwug=!>N_|(EOXV9Q<(XdI+_`?9{Bu-IuB1#`2Z1l9`yAz90LxR)K!T6FO8Yh zLBChm&aR)2R7CFXj+?F>T56JwjcJDIxBQdI)r;%b9#)tFwo2buH*eWH-cOSahqBOW zYz)t)dm1hBo&IJT%Vnuxi_%BiP>{=sMcck?YAcd?lOXkGT}Y6l`D}`*`ldbb{8wP= zLil-iXo8ALr4q9dM}t5sxiJ%K+fM3*745gTuD4r1QUA6K=ot>HUw;V7I!Un%vx--z7rJ z&daWBewW;$URQmy01m53iG`e1uAdKA`!UB0x4Rd@m#5kk50l;6T=A>i9W9^Ii#2Cc z0B00WgJ!qu>z$sH61k*_^t1s!y?B?#If4C3?K*Vs2DwM`X~vJ`gMJQWsRK=~(r;Lu zJ-jCiza0(f9sT%OU(Z`~TAGmXICd<`(O_=7t8QRqHsm;Ip3`mfjsIz*kK*}>&G@>z zs0!~e84e}!ze@?N(`oBVLpS-JjY}2(Yl<(O*WXE6voYLcbFsWPL`5!ppWUw|kPx`1 z4Q{KC9$~0IU);9i%6dt1Uw+k~S1_*ac(BkEW?W=jdA?k1*na!uqfr%I?IX#AyK!4u znSu*jD$RHH6I`YC{%Zr*5K<;P7OB}73O-%`8E%T31Y=)TEq^W-1I_=l7^oHV-cN4y z{ic}qVWCo0Mnz3OxZaVc=kaaXO{m2G_2n2z$>*283&((1Jh{EsQfZhwu= z+B*FIK(2i)>gPT3GuhsUxV?7|e-mBZVsO45%WZh~xa0pb8uz;VMqYtUe=b+V{Wx9Z zY6glUh#7cii`2+9S4UGxv!#@ByHEbG^qit&wr!puBAm0@;qJ-yq?W*&^i#L_F6Nn7 z3v)5$zh?n#QqU;;0Xz&5eYfwi%Ps2H;I`Q)0vVv+4ZQ7U(`tYSRI*Tl4*KS1&-6#L z^`}Q_)1R1Jm|WXOO9eW-U5_5i++NJQ5#evhATfTvoXJ!2pEF*rJrwX8`Mll=&-C}i z1IGbBjl9D)6C;g6=b+TvZsLfL(fG5Z^5VK8iXtDpb}u{&pPfo6j!}!1{d!b{XWU`H zU8`+!mM;w5j}G)($>#hI(7cAssphiB?UDE+qme`dxsr;y=Cw3B3j{?bR+o4#1)b;f z(Q!cV|18MjU>wqt=a&}boM=gyZTKMfY$nrT@xOG_8uonRMbqA9X#!Mg%d$VdlY>>C zMKB>?XTsXAeye5HHfK?(oC!DbY2!Icn1MZ0-?w$NX~BKFzu!y+Rg=PhtFQ%fX4TvD zcK+^LrT>436od{;oMvB(q~U8xwI;3b5bQSpLnnI~kcUPXwcjYtzPs)6$Ioc$Yv?^* z`f2^Q#y;3JBic9ZtXDxdTCh~>LmW9RM%3v z@@{`KL#|Hbl4oF(S2_g7LnIvR#f>inF ze_F~{m@DAPI!hy&T^n5){mxY(ndnf-G+lrTHoemV)xSjcPuT^(#5I|u{!ZZd%Fr=apZ`sa z^P6{n%k^K4*8Z?m*{If?wkhU>Wz=rr;_{^O=hu|3g`4?Rck-#nJK@GLP4~t5yg>WI zrHDDd|6`V5k`YF>ll95|^hw}nVQ$*@a@-AfmKfjH!lEKXw#mN_ZlDw7RaX3d+rFz0 z8Th@zA9=Ex-z8`)VDM_s=FWX-i(P-XJ+Eht^p5(QfB#e`AcQ~Y88x-6L4Tiz4@Pet z(62tq8eBm~hoJL?&I_&VBILHoQf7i#6t+;jjqucZihj%e39;20HG2dU> z|BvE^-ImbTQ0_XLEl&5(WbIU)HkW$}EkDL}6n0)kc0SDyma^17&_Va+bzI%2ppSP2 zyH&S-4nKASq#q|{X6kiPWO2G-2IN_tLeBHU23xk@)py6%faZH$a@zlq0X&YZU@*E| z0&n0|dxEg%A$P41gVXvDlSn2oo1m)g?A&=?xYm1ks`K=zvA4s-$Z%L|*2VkKSTL!& zp>5mnjc0%N*X6$Z)u(U6Be?G&Hlkl_MFZ~)4&=xTYgO6)cY4H8!C+y+pM+`~v>Nk9 zZF|}|9z3K7mbVk0qdrZ+Ciq8A^OuhS-<-gp2bd%G_3i*-nT<;*bJz8zu zIKdS0uzUWl+O;e`AxFS->$_t!&s9-8<8x``{}oPpEAy`N`S(t($L>qf^9p6Y)y}Km z$K$1eiANNM-!`rhXQEx6=ohPW?k+JOV*3krq9#4;xp^Cthiw%ODk^x!4%+?uqSnnn zHe9!D^!0!0ADb@RA)NMjxW(-hEx{Q3zpAz_AvJOIATty zZhxwgJ`F4JCg2j&u~MY9Fwb(r&t+<>Z&CED`ekj3W>T@R(GQh2{Yd{?z2RmwTVJ;w zi~`qH(ZEZkk#7Wb7ZcI%6IdRfM)z^NaTt&hX8*4wcU1nEv`Wn7_J@lLrt9oguF{J} z$^fV3dX>j-p}rcYslh)JmOZ9a8jL@~UMQIom*&)KAd<*eqV&0D<~Pbh!sFYql?*ZO1I0 z6vH2$C-hhe_`T~R=!KMX^_L^V`jLEp#@;tQl4{0i@Kf1YhF$?W&U3U z^b$AtA=Z?IH zwTgQW?5J~p5TJB89Nm}{)CO#^$cTQxm%%=#kJLnF57?on z#bO7A^Wd;veTEEpY@%imBI^~k5BN%Fa4lfEK!BP!CX*7hN_#LHL-+!q^Ynu0Y_os5 z`JCdX&(*_-z6ylJND*kD=u@E>2NABf-)}TtYWS_yRX7;sIg<3?cn+?ep-ZE55F zx$XDEZDy<-nJ#l-(Cp7arK6wGr=3Eg93Ib(em94i2Cqdg##axse{eq|FEh0`xQbV@$~Vyr{SA50Yq34}ATHH8(sy)mEJT1E?SPdmY0i}i#{bgWG_r6h zmV#XcmF;EBR~a|5CkwrndR1>|2G%>;A@k0+M8#x4oe-`C1oRrmzFj7JbW!*VVLIW6 zXU;d`|6$I9^MLGXJP6FwEn1aG^@Ddmsn=Y^W-0^g;s@tMBffxM%K@4z*Tfw@4~xz|jqUbwazBbH5jGEWxiUZINYgLL?BdF;l#Yx%g&=+M~7( zFe{Z;#f~k95;0Q3V}Xq=CPRgT|JViqR?|(f0D#Lx>o2r|6>$k4!yzKA2rfiU0LQt1Z&Y$5p<-0UD8<|&LO0|^G zNdcV<6`YD!jjd7E?fGSYznbI5yW?S2d_|Sd_)jK`s|3kmKE_%9g$oXCP;Z5wUZo8O zQ!U{c#qc<`YV+&#O&w=sWFk~^)l!9geNCx%pz+UX^yR);F)KgAaJ5}r<2dQ1I{>L19e(s?C{xOhqb;T1sT;Z)3rFw^Hi z%K@7!z0M@+aWf1b`Z6xYk<-imuiqjIVgV-ps975ZSnw7tYDAr-?(oa{>)=o*`mEV} z{ZIM)v)+Y_#qQtt8U?%4-M>un3vmkhpi}WbnEr2l_E#k;D`<$KF=6pq^(x~6IsC(i zf9|UO+f|!XhS6VVOnE2s_q6`^{HDQ*>5?pYYbW`??>-@~A-(!NR$2b=Z6ZLZa{K%? z0uoaJ+g>MAC5|z#-v4b;Y30-K6zc ziMjux&Eyrbmx^~?GMY7}H!;Sj7(S?f%@^IwhoflO6GkVsOpuCC+!F2kE}BFkctzBh zlb$fbL>$Q{j3z&ub@5FN^4n?2D{8!Xj=a2`=z~W2`=UKQj3M)Kbz-ca1kS%C_W6vy zzNChC4AB#}Lo9+z@@J5WQnbp`D`jFS>FoxHV>@g4=f5iC?CJ>UbW1%o_5OO z`Pb3uUr6nyF#lU7g_3Z%!3Bzlpnw=fQdF^$4b zHQj8kUV>?Ypw|**i7iyGtBR{hr_;G+WL~CGkvo?NrH_Q<( z8Osj)OEI6)DOoL4lLRsY*f7>(ZXOI=%@cF$a%2qp@?x`U&s}mjJ;^3#*>)0q0Pw3a zX+D@GNq^Q(#78(poy_cEsGw|Vq06ci`X~ViFKT+m&;npCj;hT$3C@fstAqw z;X;90N#y?Yg)4U3?m(jj2iBj_#^yNjEX(!I_Hz=`ye zTv1|ep}_cn6uZ?-v<8s}S>D8Sn$P!iNat1EkAh&_I<@3WY+JmmW0YUtNf#C<7iv*Z zO6Y1xP=5!gs#Rv(!C3KtZ8G_tf_@feFp=fpc)jIxra`6zSyM*$?wEPisRv$&NL#a* zea{dmC#LBcb-+~za{=TdB`YHv_?;iOy0P2=YZL{RM!S!V9W^Lh!M-}uV=h+X&V8N} zf$Z&kpL0Vf#B#osK4gSnMFxmxCl8QE9%%aocUOpzzB_{SXR^)zgxs-V>u>5GO6+2SzU(Q~9`-Hs92O0>q@v zOamCsygbWJKPmk96vMYYK@b})wYFLlqocKRa?D|y3=RZ*<6}|r|9Gdpp$l8RYf&+yn{NGnmb2yA%7U1`}Q=npvpnD;9N1YQzDKbv!K&$D6M5lI) zXZ=5sU0p*6AZ1ULHg>aztE+{vH4Zww+LIPaoK+}l^XIva+W>|DeKLTaDs7>NP(X$#9oJkxq>`yupX(-e$XF<9O zRdONHI2Lqw(KllS5kbtV5zU0Cp()pjL}pCWrIL6n1TJK>(G_JceLGcq8Ek!s2}Q}! zl^w^UpLBxI5vYy{u(hs9CcZL_UW);`N(n0pYB;C>~+sjtJKpq+OvWHk510R(X?53t6J7m8cBZ`;Xp|SEZA|Af~_RF zy}kVjsSu1uNRWT)&<(AJ2Kp{acH9OJ)-H`HZMkZUz&8E-5oj6T@w1-qPM)9E!|Rta z(V8C`wLjT*wqB3&3a3SP3DG#X@kE0kap4??m%{Wc0pBT_6UILGQrw9Fa)52k=IH+QQLgwj zHi4;^5j!dOaNDRs-Qs{Ul$$$B4LJ{?TgRuVl;^r9%%d^k-)Ys4yBy7w?ktPSkW8=3 zgaE${wt)Lx1kt;C(YBNNRsu#trY{E|L}J|ao=OQUqBd3quQi?WgT7G!REh*`GDW6b zY*Js&;~^cX)477Z;jGu*-1hUJpKDpVifojk(uZw%ZRPmK5s2^uv=&K3fAwd%gI@eF zccLwPpuqtA0CSOcW*Nld0=3Fv54N?tY@zG$aSEbIb8d`7A^c4+N$?nSL4cUG=1mYBIdC88+R#uDlDXU z7I4TN%*n7=H;jzOX((leJWo{%3c3(}i{0@x_VGX*reqpoqszkmph99b}RA^3gHaM7A zZEBF}o3r3eF07&blR&%j2_K3iZ4gw3%nUsC6xFgxt{PtwIs)5=wP5XJ zUGUwFs*pX1c3kFQ+6e^08~Y&}8(ki_@^)O^i{3=m(TcFwe8fsc%S5MG_EFH~8xHA< zE0OU&XA?Q8r?37|DgA-1(Vo+|K{W6}?l*?C<_`y>5lp@8mf)tJ|HBci5CJD}!ODH^kfOZ?-^dBE+S$f~c(ib~CJYj02+;j!0OWI*r z2uey}3!{zOmZ*La@29>FJRG!dp6OEAx%k@d%^A4SLok_g180ld!FqAV3q8G*Vz@Ya zhYKLjx}jnZeX_hS80S3kjfl^wlz&`D zmN%|HEBdv_w=7regh4l=t4x*wW# zGTdCzp3R$Ult1ne*>4Sgj^V*CK!2XmO#v2QpCqRs)`cng4d4d$S{c=F!{)SL@NhEu zS>fku-@Vr7()Q$`$Nhk_2Y2JZu^zEh0g0XD=n2IrkAP1D(C#Y@HSL_SWmveTYN&E2 zgRi2s&2sAI2uTETlaPgKZdzG5P<)z((K{^%AP+`Z0YbnJ*d_U&s;*l_G1seUPvw`G zpO6pE@rj>o-Z$Z|*Zq3>8iB=(E)ZJsu{kqdeJ1~-fLtC$a067P)8=vX=cDo&DGWh& zKsv2(mG>{`I$p7|3Qz;L;;J0-cxq6Czh~o{Ipyt6+-mbFoUCv^lMqAs_uHd6YsA1D zK2iP#k=;q_FU1Zu+|Djv-oN0cXROgsEsW8V&0~UZ-B%-D0>d3k=ptJytUIu$br2|a z-L=vspAgoER~}+2<>^g%brJ>Zy)HXgc69%Nj#=o~6th%BjDsxMrQmmfHofzC=$n`E zMVVLrN4P;gDF?cTAULlsshcxJqW#`DASYu{_`_(G=;@lPzi-j60D)9so$^+WH zT@LzykgFQa{wXPl-w?63@vd2Jq;wD*K^+1sC!L>^W-X#7$<@?(_RUor^sMfC6}F-m za-xk280B1dFFtw0{33AMmEdJ&&q-p+5o zGOEUno*vK|C9aoSF0LnRulS44rYjWZIp&)gQ!ysFn;Wiz#oRS>#u7d}BJKDnrizIA zv<*JjL0VAvGR93A#wGA*#$&sL?&|zjmkzZ|p$UsofzutclZVN}JtI3FX21lwg74Vf z_kDdHG-zgyCI>o_CuNZMdGz+$F+;4#(qH1Np(%x8Y+A`+RRA4vQ)1rrH~uyXrn3N< zMNNDnwzUk|a>B}-hRUf1w!jtM`qR2wA}az?R4dhxSHj(2Y@OQ*B?j~Lp=AD0t8$J0 zj*D=_yRZXW7Tm*yY9aS6?M37LrF07JR8iCW29;$>+zcJmJcai!1=b`_E93fRUZaLI z0PhUBMtTw%5z z)Ha99a?**6w7EiW29Ck%JYMvkZr99HxcVKSe+`AFCb-XJh{}h1KaBM~rHeUFB4Fn& zA>Y>*p~MZDY(~b`@838nA6`ZmB6su_(jAkevcp0J4eeenCF-{z!>qZtj#4>$c|NFP zB&hDUYFcL+a+W#K2CioQuhmNHBC7_YYsC{Z1TgzYG!L2u5DFYBf%y6DF~Q$f#!yEE zSG@Hb2>piVH9_Cb^jtH;g$qZJMz%N$JBU+?hij-C4gz>}vR&0=puw^f3?}B{MUb1E zgFKrZv;5CwE!-p+WbhY*0(be$q?!pDXWO=w0mn$a>`J|Nk~z$DHpCa|v?yUzLDw31 z$K~wTXt;>w`Ek;Az`$WuN;9zL>bAvF3ztWC8nc&u6tjn+ordn(d+d1KM1I+6s=*)y z(meeOvWNk{B09FOnrodXfl{o7+6PDsZ%UY{>cE#r-C>QZ08qj7ATp>?-w3^?MKj7RLCNx!>G62YVl;VSyxE#8`*5Z3& z%V3h`U1F<7G%8-R<=#nqkiuO%$#&g4NP@(3fWbDEB#!w9&{a{#Tve3EJ*YtCedblu zPMz5DPihleC*7?OOLQMLjc5^;YaKFIvCP*3a3cak0~uz0E9GwO?nAiWYZgk950zDi$Ui@hsEeC0rw3~Ms2Tu9 zhXZ}E6T`PFGY--y9jfI65+ouKl0+I;9kUWI-*M!TP%dJazfo{iRTww!sn;??@+lkF z4vR9%>76XiZuuqy=RX!PH%u(l$hC9uP2SJ?aKeJ2Qe(E@0l7=%o!(2OcHH0$FfWXUh=t zD^Udfx33Uju|2g{wJa!5NfTd14=XxfV0@3I?!c^OD@G2!+fm5Q1DCNw8MU#K1`|Ta zXx27WRWZD?R!@Y8_u2QL(Rxu-C7Gp zxi4ir84Qsa1HUtOvm^KTU78?Hm<7Xhn^%eF`)icNt%nF<-SG|Y+)u{`Lt11a!7P#`Lj-AGox-BnB0*D>elg|IG$fnRkvgk$D?JOrhs{lZ zL-XSKZG(#9E~(t3lJ7fy6%odqbjRVU(Fw4ioB3k(BzI=u;7mjNO!TQ^L;I$KB;fd| zQs0aW3%Y#0V&ubIuxO_YP(!K1A^%VkgIM1m@)pqtp}@9+E8o{5c1aQY!9fWQ-}%vu zh}yJY`sF=$FUP*~WKpioH)*&>rM?a+^niV3=qF-7BScJ@P@^&$cv|4NsNrh?Qca>h ze`>2N_)D_vIrmO76M^Yn->xo<8BPOgq>*<$+*&I`T2&lj;?mH=nLJl;Y?3%IBqa~gJG&+zmHGduC> zc$IV(vL|(Vl-G1?VdL7U6JKm`j~~-7dg{v^5imx(Jq^v5X{TyTH>5t@OtbQ*kH^ow zkMDZ83NzT@Vv@TBUl+sJOVFtFXjv?6SPfybb8I`*@MPYf6fN@^``y<;rhP#0b}q1u zj@1!7CVc^ZX=bF#x0~t-`@iH+gp~qLz_i^o97OJmLI74vTnQ5G& zgLpAm4F&QX`k@(`s>Mgibmx<~Eo9!+!}CAH0oL?bUm#`KB(J70N)iIkpfb&4x3a}T z%dSZuOYGaCPTTX}TAJAR_)ZfNxi4JOJBno^n`oRf(7$gIIh_27FX^+>n#>ZlDWMCcfwRldG7Gm2M@ z7u0BG;)rR($t0qFy<(~>U9MSy9Ez7bUVpy{3dN;DIF*?af3Kv`xQsl+k^UN;PsFbUee3HdfVP zUQuFRk=W~ZJ!UFa%G{__**wYYmOf_Ma`zVEnp14S;Y+be*ESZD81o|#^kFn6 zz^E_>EeR-wed!o3I|;JN)c7HB5ZSCvAuVW1g?0=UDiVQ8-{V+_7UhKQ)PrvUo!c zQt{0mP-4CeZZg_SIUYBkfSTA&;;$JatfuOtJp)PW#y_4n z7C7Yf+G&RhV~BgA7sn(P3YT37R@e!N0eZvo^HjWx<=d<}>5DL?R9dU#5{&N-?#XYrNQ~|iDPD-}k#?TNyb*2N z`oxQ5{k`QEa@+pus#*5X2T{B#_v5$QtXz-hHxU>&YbGQ4^#9t3eM_Cr~} z|J!T*krRDq!adWkYGBf3^20csT(Zx#5e;RJKdXLeu%er4SY`v+$p<%m)k2kSKR*-z z+P?tEav0OF8>hSztK~qyflF9Vsp56Jb5Q7@!yqD&ghSW8JpM|30IFtoC^}K&OWE;e zP2F&RmB@Ka_03(*H?pB(?%@bUsI^ElO5L}XHz1WKo%Kf4z_zCEG$LOfmmn>4!qC%9 zwx^@z)w|m{1_eLao{ds%!)L^&UVOae9!UJ+is!Jc7M%#quUc5{NA>0VoO)R>3A}#R z)=7+(3ab1s!$AuLveU8-x}+|fPtaG?{JnkO<@3rIYUE$FXMFr*RV~oWz^Z#jfrerc z4R?g%fE;mhC_Na}I4wYsh9 zX|vpDFib{N_S0zld3$5qRqs-h+vvC@dgm|aJ@IevRz|~QtfK5nl7kA>GpP)@rF`^k zVk&d6f0li!t8-%U#x0rMue1BUG;n|Ga3MJ8S}50*6(!wrd<}YU7Ob}8JUV7NrSdt* zeKW;$X2TIo01FO^k-1@cJ5v}6&vgOs?=;~9JBL}hVMyFpDKYmkaZtoNsTkhB+YUgCZ@tvwG2rx`& zwG3uiFFkn8Q+S}gfOr2{!BDK$Or};uYfn>!PRmsTkN%QIrE7LA#Ba5oRJ!`w`oJ!u z0bybO6a6Z}k8>^*NvbtOJ9}USq<3#7NSOd(_V{i4^q~pF{8D5);&z&!iO1YUlW;b$&Pn8pay2ropbc*+v57)9Ignk1 zR99N{410f}tI<>d$YFKO(*|xC&gKL_+GN*7c4)fkOtWH}c;3Z{%-6Ul$oqCAle2^x zoo&p{?)L|r=jY%uHS-ScZ|OYAIP!bVQCGSyeMt#Qe`qTGz5HI}ys`1O_IdTd?>-mX z=B?GkW#2a`q%3?(Uy_1Sq>F_hd-G)vG#U<@@x@tLCcq_=8T0a-x1DAPV2N6s8dD|9 zWTk9KUx*~Ry~k!agbKxAI_q46*CaySz)5&IE``^;c9ST14I#Pey_NEhUqw1h$@QGML9pPo@NG=5o7UP?DETklD| znZ3k3MacT#;*6`kt0(uFbB|a6pe4ywO<9KOCIEjS?@uPR{wMu}Pz2x}R=40gNIKjuO<%+#5-u6DVS+r?U!U~2~@wE zG@ow4+@KA%!*%+0DAEoKmw|$HP&B!9%Wo!P>9Z84&+p}OC{im0-r7d$;}B8;-cY}? z{UpKNR(<&Sr_9}~7%Wta0rL9`qkqIDzk}gaBnId$6^P9^{FU#N&>Mcs6x#mf=_Ynr z;aCA*>gTearrw~Jq|b2w$tIyTD5Lv#xH>otWuEw8xTteUg)NziHhb=LhWjaVAz;lL z7L*5J!L#v`mY_3^na8~eE0tz7hR^(mA2T7Ej>hSGCw2 z)qnHeh8keu3S&(bFuO_yi9#sobaa;+AH!jk*cm(Og5Gw+CD{XZ$ocOgj+{J4k7IMqwL+#Y07p9qxq5lf`-qQ`G8|o38c4K>k4|wIc&=#S*Ug5LvEZM+$cV35r~N(hI@Oy0dQ(xi(Ke)-n|po@ zQaS^d%7{yELUR<1iB#jnCdM2kRPXfC1(_2wGyJ7O4|OERq_mJ}!1${XAAGqW1o5i0e89`w}-68zGa13QmK*Zma{2|*YGG~|^VVgBe5Y}B36aGAeHH7P;%f>;b) zk7yALYv`y|&QEF26s6n9!5pvNpuU`-O9btH@ui^Y@e`r};l$l8dCsLSUn=D}zF8`2 z#cfK(fVwDXjpZaY;sDow(Z|8H(Jn)o5Z!*6pAJccUgufE1B<9DddIDIYhL7 zryva^Tyh7Qsv)3eHkGpupUkOMe!PciKA~ibY(I z;wDF_QOM#q1`k7g!unVDGIO#ZRCrxkBu1~*mSEzGYJ=WgJ{dOXv&r;^tx9IiF=DqEQ~{RKgADdeai^%sA00R{p+P~%(8PEUN`mj<*~ zZ?}aQa=|c*EGT32@!Vf-6J2LJ1(D2#4!M@{6UI4{8(RUua8%^$8CD zQi=3!@>>Yy2ee-A1X=-2Nqz}6mreZrv-j_ z3)8pC*0@_oLAv~B273Or>)++ z7UNX_!m@cJgZjU~6lpCkVQR14fl1^K6GnO@Vmg)&l&uag=>q^WH%fP3Gv0Rk@sG*V zum=Tktob!gT{<8kkeBBL%&!(C785yi0an9GPH6M?cdIbySS52^7`msjK-Xevcs_<& zCAKtL9J?IJ={aI4;CCnkBQ%v#yVokt8*Y=tC5WBGG4A+y`x>8FD+5Ynsx=ZOyo0Kz zq~JE<+O9WK7P(S?W0O2jLUTK;=z=yqvFSXJxLiq(qkXz~NXmRwyft}9FP_ar4@j`) z0#ha`8Rk&cgI|f6Vp^c!)c;r~CRR(0bMb{`blvW~@xU1%4^}5O?ECOKHQ;lS7AAyB zX{%*NasN{FFnx;lii?b~Xprb`G6h_}ooS)0GnnyN3nwZZ#?@54D4rZ8s6$4v(3Q!l zgFEAfZ6L5~G=i|C9Sw*3R_*kCYJCeVC!~-BkDMBo70wiV$e_ed7!xej&cWy*(<-1{ z`$FQ+ma#|C*JXUfT9KI(j<9!ms2k_65MGE&CzQ!cpi zFnE%moceS!FZfTJr@{lfCV~k|JeVxJ!IwtGp5_<^J23C`9`-$eLQ#rbSW2@j1^gR- z*55CCMVezSKO5%kdA(=nc>RUJY79cuD>9=|rBa1JZxzFqOy}M}*Df|QR9x^O?}H8m z_!SJdk&hx8`ZR-&Qc3qL#Vp-*HSEQv2ftWz2L~`=ocjvYB~`7Tvo4%NAwPzt;w3!! zdBF?;$R9Xl?J}r8{o*m;C=Y8dl=0=1w<3TP{WOEvOPlv;@WsEaIl$R**Op>H$>2buA3x+l!-ts7HAp!N_$Gr-`nWKTr zu#9nHx>L%zWLjWwbe{c5A49>1U(pAi_(({b*yKOI79|Gp-Viw{6~S4vp|?4KxD6x- z^zm_INrJ5}OsS>J6{QgJS`c%rwY^_ql=mbo_T^4`?3*3j5#8xb8UI^oSA?dP%8$GhS|GduF7C)CF@~Pb0Jlic-B;wK{rBdu zFV|R8nRF9TTSuOdCq;*~6@SEU+a~>t`Ym74jGT+-MYM%pUmI7P@j4PICTHm>Zt+I3 zdX#{I74@~VmL>jaF@szwW0mQ&hLq|#3L@Q=uB^!pvdPPj%^`SH?6>7C9#I}SN^zqI z2u2j)%Y8utLS#un?9!_>dG9C-7^)(s zz=Kl{-+JMPB_>S*g?jNl4V5na%`opJz3?}~HOD2Yl4LZA;s72$)Hm8CbgvYsDO(Gz z8t~b7)VuB~j<8`d`mO@;u31V~>?paj4P-Z6Y_}R~S-_e@K`8=C@&Wh~-4ef=##?K} z=_8vgNEhIdymcDzx+g#?6f+(%M_09!`pDXVo@sE+)c7uH2}vvclTCQ8h)n2;t`j@g z&S&{cVA-D5-K5oRS+6`zA_rha*u+z|@i6hQfTvtWtLy3s*=z8fch96FN{Y~Vxo^q- zB`h&hNl$zeLvqH#o*d>Sn@2~(d1bNJ!>OGjVv|J*<;+`B218L43q|R;#nRX8qhhYp zc5K+{uLugi9xvax3;pIy2%x(DHXr+bZqkJB40G}F3}sGC`?)+=CO_SfTm=c<;8{Qj zg%e);*NyY*iJ4=~8yPZKDrS%;CLdNAN5Ce|u?(-pV#x@x8eWY*2(U?ptlczZm_oh+ zm2?1?(OWygg*G6eJ!EK)LOa(p{izRTWE1erl(x)SU!c>3C< zYv^DmZI^CsBsB-wNDq5K+FmcFEa(scy9B9hWGElmP00tGg06NMCR1#VwgMja&aTG8 zKA~G|p-bm0Gi@)Vv2_Fl{ZLaL!~ zdFwx8x?d?Jc7Xvdp&wsV^4Gi@KpnTwFRcoT^wOGY&%mQ5dYvXsL5ofKSXA;of|pKU zyj?#sK;^wb%CAp-gj-$mz5w~c=fXUJssQ}-9JQu*aU7-q!a3$9Cm@dH1Z$v8dp_UC zyt@YKv}7|1pR&-(*~k~?!c6Gb(V&X`?;pgC-C!Y~9XF|rPc!cwLf^T5XX;Ru0yjw$ z**)G-@(cwqeIrMKg28v2fWF$YC9u}d6ld&pFytt!_Vds-^Zu#G<;<-_%$qfher_#+ z0Pj149sI|ZA)Fg1_pe&F7r%B>nyRk%A5f=?|9`K0~*RY}#CBgO8 z;z;&wm7Ir3wb9!Uo4s^hEwMV;;7nZ+20twN{~W~T3%VIb*4U~JP{^>|)xK7pTTHwF z7Xd`7aEdFN%;ZCJ_?r8ynb7TaC3ujR5O@ujWPiLqD)5Il6O$n;5F}#8B_r_AVa2K8 z3Qa_bc;IIAUg(N4Lp#)$tCbkXwOGgLtHArBmce5bINiRJ6X7V+wy%o1LBD84t2$FA zoJ$|e9SOcHRO}*owrBSOaN;KQs?}9XPEV9K|1b|p>~{xXa}7F z0%udS=ZBW^UZqHgRX5|N7)!jT8!S?@Qf+~QBS`x5zW`(^5|q)GQ}@Xdk9u`mEejuC zT+4x^fEK6CL0M@uqRg@r)s7`hyu!`ct%=@IN8!wh(6f5++LY*!7mWxq?jna1>!6L_ z*anW7YZ-~+`_Q~f?Nzb%_aNuERt55Ov8qxb$5PE%ne-E1svqteh}z~J62kvtM-W<-P*=pIc?a2J=+NbK5UZCoXSGbQH{7f2m7Q9VmGi*MM! zE!w@;etqYS;aG_%A6hCE+7A8K0awtZ=w9O6BoXx>Lab5}&r+zp>K&~YbI*~1V#8u} zsNy)8?Ds{pZ$`u?E(e*bB<*~8ubpegh?LJ%Bz!1u<;yVTj0QUi00(a3sXgnvtk55W#e%bW z@`YaH8!^mH#}WtEP|{B36Uj+U8jiOr1No@1IQ61=gD8S8mHHN&1M5>l0OfHOu_Rr= zQE%acF)qAB%1RksyQZKX5S0^i2FpdE*e*8FI;!d`{g+e26<5=vVor>?ZvN>WibGT@ zM9!+kkY`TaN&a7of(4Se=98lBgDb(Wf|LJWH`f`}B)Jo9{}fHN{H-clC#7;;g8;JBo5Cyfl;IrevT z^{K~orn~Qp6dU>w*BUPQtu-x@at2->yQlu?qt%29kV+|%b}(khbjO-$nDmq(6uFM9 z5g5VE_qM=^Z`cRFE(cy^ojulN=tu5NP^gdMnH9Bq5|b@`l2P(5yxXW=QJPC-1`3nR zXA&^51|YR%{@|=MFZI2zp5R4>(ULluVmJG=u7tVr_Uoe0t2z9=SJlSiTJy7r@qD5@ zHOy=@a6JU4MBOn*wHB`;rpS&_I|zhm(#k# zubh5;^K!m)@D}%>q*vN%YDN_w)|Ke=iiZCV_RR8M30afV=u|hVvA=iNBFU(r4&fZy!=r zUREf?rd-Vwd};hHN?SZo3zGkZci2LfOu%vrG#jVGW)qdHK1rOlD1i75;~$jVI=XC? zZp$t(y)mc6b{p_2Gsz+`AQ0IsOo(xn#BJ95K2cfyo0UBN(fy;)y^(J>Sj3<`J^^JVJxo~bfa`)r_?WQdkoIPbc!XC!bb^+qCG?5cX+TONyKZD5bwk5Gfc z|LVRnecziG28eti#d`&!^PJvLU_GWbB?1S~Olzz2{}fdqCK8klMgknAwmmBm4iB54 z!E(NfkN6?Gg}!%Nx(A?)r5oQgh~C_M8WSbf01_Pt>h$7AnV(DXV6?__AZ{s|h2JVQ zc4j(1JM(GCfO|QkH_v+turA#xNnM&#krT{UvalyLhk7O$IoFOt)(2kkwc@6!HY72h0vL0iShE2-<$Faly9isEuVe;vc~R}@8} zvtRc7g8Kw_iv>^gDA*yn%cb$ci%wzte;q24 z(A}Zp(Gt~#7XViQRfyRQabGEbiyVC9@_*6X37}l^6>08tLidHxHgcnVyN!xXT_k&%GA>OJV zD|wN{-`Y<2n_ZxklZK~$y?&+Ge|Tn7#we5*71V)L#v#!`}`;AkNl(keQw@-QOmNe|NEMy+3h z-F%7ce6Ljna`G#I&11^oRO6#~2JOR|x-2|LT{Dd5rCN z9AqeD=!@vCr*;jpw`A7Yc(Qh~L90Q`Jq(OQsDGo5>1t5^qU21NvZ?bnP8>3Vl} z^gN=xA12}VBEG#PL`Uwh-oOa|*wc^KcazHgGB@!93%Mh2X-4C#^|~4yGhVkQuS{DW zjkC6$(tmKFZCNj<2I$+$&tB z#RKgNJ@cmsV**(`s-0(c8HhYE7V3*STBeiJlwJKaek4)NnJh06Reb%WgFLfx12G#) zgJUcLRR$cUOK&=)!7iGm>squHoM#P_&YPbXWBpTZ~&sG|BJ$D4*;m*5sG-7}uBJj%T%w zD{Q>gv{sw@n6_2N0}CPp5g8e)jeSP|%caD)cbj_Ea8M(bsv(TXhqIoW<7%^>3&yDD@ZB0zQtbx342&C1|j zF8i?`Vx|Xr$_l|E%i4+$}JG|9G<0+!0V`6HWrsUq9Kho`VV| zKafm(;=H7rDRpG{S?ge2rERS<@4FKsQ6DJ&Zl@FNJ-Ym#oPknz!GgQEg^9@nf)CVb z{DR$_z2QdkG68mzqI?t_DQ&K5R6SUAbwwN+ImRZ%H+ZucF>&%q1_rVBid5!uV#0Q~ z0*0ZL;wN)sn3ULf*_ecQh-&8v5gWioDc#gzwR@DKk@dg}0er6X)$oZ1uFZ9}LCS!s z*BrZFZdfqmVc`a?p5;_Pg5=xz?CCL?RfGt=&rex!Hi3;EUxRDkNxD6U_;jAFX>Pzr zp)i__oqmEsG>VYW-xgVnD&AumdgA#?BISS7H|3Q+I7M=DJE%nGTR5ODai37@Q?F zr(R^0(_K@g%{=ztIyEQI)-%@d`Bo9QgdILmXwb;lcD+P?a)y3y6r>e=IHen%?+Gz*;DJ=7dy6tT?h~$G9=9nV0jW(fV4`khp_VG&{2AG z{75lo;I1g!cXfnygRRp%sdwtQ(UW7y9m@w*{9-JiLu#cga=K1NY{--_j-l&{q>4nE z!P}IE%Sz)3fy?b{-*Fqtli{(i!ULC@wRNqV8JXXcWII1ck_-Lbd|Tdof&2<;JmV^p{j?RR zLwEzn6e?OIjvEv zld@X!DvFslXG`pcJu3wPQq9)ZYYapZWw>T*e~W`k1H@BVW2%Ry!MPFhRT;7Ovj_Ax zxBeR?5D&y1biJzg{E9oEfNc6O4V9w$n_BTk=x?QU8Xa3Usd60r%uvC}&~$q!pbLP~ z9aZ*}_|e-eteyYGr5b$B&aE;LsZ3cIPRQ>y{26~C-W3EcHT%%o@pqnA*1125=(@ms z1wR3-o%d6nhx_)7upoUPZhKw``pn5h|J}#UTfQbEB}^*>o)RW6*yy$nWf)?E~RN$ zX^)+%6B$$O!kNBmb&0}Xl^IxJ14iRIIuq4UPM0l~HBbb^plPHe^xriAU*wp~ zz53Yrq^ls8=^qCNV8skH4Ra{6#1#kVr~r5tNQLp8Tm^DWmHdPWj>R{;DHe*jitH<=7Ib_0KYAhnLb4N&(FUiaB< zP09hJQ9XG>6$Up4UB)_+B30d2B03+7P?dgMrD!V9-7@g6gXE60@DBHa9Cy5M3%JIl zsdtMtPtgE`4r>Mn0fm#|SkT>QWR3+ieLdA)CwMp^r2fs?T_9`>D3>`F2BW}YA1b?E z%bcbxGuWpU^yf)y1Hk-#sFbxS0a;hrr3wh>x=ai?xbB@_$59DkBQdoR+4d3*&y;J? zB`?nJ{Bb~Lpku15W2@!j{CWnGA);D^<{+2U|h(*Ys(X4}idUK!(CL2E)UaGg}0 zJruK?g^dM2G=7kOjc4DFEP^~tOtnqnB;jAhoYn)F=UqFv`L~o3(2n09_aXq_cg=~Z zTbA=bKCkfr@HJs4q_?F3nC7pCI1ku>%z8fs`g@0|e5|2CdLE@reG<3|blxRq&GuhI z(s48~!nDTs@HZ@a%*+QBYap}@FMsihu)XvBm;}{~g$p9_ZP$We+=fHgwh^ZT{mpZ4 zsq*$GMG3?{Ji_k~aLy4X-4Je^QQtcJ1H3M@it6CR-tgaM3`BcHSWySWmj*tg9AEc1 zGGCGMSc+CiVj8dhS9s`5IfZHTaR1i|w%3b&2~7O|U(;kw5WcUx82w+sF~v?iiSEo> zu}?cV5dLyCal}c6$TIaf(U^Xj<=7R}@0#)BJIUnQ-kAwQTXXLk;Bo8EQifgj2@4NG7`JmEx?5XZna+k7$3Q#0XPa-4&HkG*{dNk0L+7o~d$PdJ1 z4!V8N`9mI9FzBjr`f;$cZvHcG-w}0`gVQ|yYreqirirlcDOtV+^~Z&#+{5<~F%HKA z&;E8=ZR?Rh2D7f3NLcW6ih>*gzjDa$@B8z6(6xj#g~wC(S*kIIXuSy|trQKD{d_XD zza>okY%fUktZ_)3-gCrk!pW1JD2#xNuO~RZ53-X0|~7BK=6Q1t&shge+y}hl+#*V?t6;1uVxh zWsUTgle_sA$%P^$)3#y>zt% z5kwuQLI?ZcCATyCYq=P9L{ia7Q(?%_zvJ;3PR6m=`zPb{A2dx*_vT;fU#eg!*KN7P zE`F>V#=rR|kbp^xBz%^D9*MCnQ$#5l`DC#!a88_py6VRYIe1Adgm*vWcQGePAh6}W zHxAwi85_UD8VI6eqIwhEi_`1dI->pn8OUAv9Cyd>E^&t|6yG7hW*z?$TPm5Er{^HdsA`#8xhCC_3~~nC?ZJxg=D57?MHd4V30Hn7reb-`L%S z=iwy0Label: {name}"}), + body=True, + style={"height": "calc(95vh - 215px)"}, +) \ No newline at end of file diff --git a/apps/dash-lyft-explorer/utils/helper_functions.py b/apps/dash-lyft-explorer/utils/helper_functions.py new file mode 100644 index 000000000..8a179e137 --- /dev/null +++ b/apps/dash-lyft-explorer/utils/helper_functions.py @@ -0,0 +1,15 @@ +def unsnake(st): + """ + Converts a string with _ to space. + """ + return st.replace("_", " ").title() + +def get_token_list(scene, lv5): + token_list = [scene["first_sample_token"]] + sample = lv5.get("sample", token_list[0]) + + while sample["next"] != "": + token_list.append(sample["next"]) + sample = lv5.get("sample", sample["next"]) + + return token_list \ No newline at end of file diff --git a/apps/dash-lyft-explorer/utils/model.py b/apps/dash-lyft-explorer/utils/model.py new file mode 100644 index 000000000..f9d94300a --- /dev/null +++ b/apps/dash-lyft-explorer/utils/model.py @@ -0,0 +1,159 @@ +import plotly.graph_objects as go +import plotly.express as px +import numpy as np +import pydeck as pdk +from PIL import Image + +from constants import NAME2COLOR + +def build_deck(mode, pc_df, polygon_data): + if mode == "first_person": + view = pdk.View(type="FirstPersonView", controller=True) + view_state = pdk.ViewState(latitude=0, longitude=0, bearing=-90, pitch=15) + point_size = 10 + elif mode == "orbit": + view = pdk.View(type="OrbitView", controller=True) + view_state = pdk.ViewState( + target=[0, 0, 1e-5], + controller=True, + zoom=23, + rotation_orbit=-90, + rotation_x=15, + ) + point_size = 3 + + else: + view_state = pdk.ViewState( + latitude=0, + longitude=0, + bearing=45, + pitch=50, + zoom=20, + max_zoom=30, + position=[0, 0, 1e-5], + ) + view = pdk.View(type="MapView", controller=True) + point_size = 1 + + pc_layer = pdk.Layer( + "PointCloudLayer", + data=pc_df, + get_position=["x", "y", "z"], + get_color=[255, 255, 255], + auto_highlight=True, + pickable=False, + point_size=point_size, + coordinate_system=2, + coordinate_origin=[0, 0], + ) + + box_layer = pdk.Layer( + "PolygonLayer", + data=polygon_data, + stroked=True, + pickable=True, + filled=True, + extruded=True, + opacity=0.2, + wireframe=True, + line_width_min_pixels=1, + get_polygon="polygon", + get_fill_color="color", + get_line_color=[255, 255, 255], + get_line_width=0, + coordinate_system=2, + get_elevation="elevation", + ) + + tooltip = {"html": "Label: {name}"} + + r = pdk.Deck( + [pc_layer, box_layer], + initial_view_state=view_state, + views=[view], + tooltip=tooltip, + map_provider=None, + ) + + return r + + +def compute_pointcloud_for_image( + lv5, + sample_token: str, + dot_size: int = 2, + pointsensor_channel: str = "LIDAR_TOP", + camera_channel: str = "CAM_FRONT", + out_path: str = None, +): + """Scatter-plots a point-cloud on top of image. + Args: + sample_token: Sample token. + dot_size: Scatter plot dot size. + pointsensor_channel: RADAR or LIDAR channel name, e.g. 'LIDAR_TOP'. + camera_channel: Camera channel name, e.g. 'CAM_FRONT'. + out_path: Optional path to save the rendered figure to disk. + Returns: + tuple containing the points, array of colors and a pillow image + """ + sample_record = lv5.get("sample", sample_token) + + # Here we just grab the front camera and the point sensor. + pointsensor_token = sample_record["data"][pointsensor_channel] + camera_token = sample_record["data"][camera_channel] + + points, coloring, im = lv5.explorer.map_pointcloud_to_image( + pointsensor_token, camera_token + ) + + return points, coloring, im + + +def render_box_in_image(lv5, im, sample: str, camera_channel: str): + camera_token = sample["data"][camera_channel] + data_path, boxes, camera_intrinsic = lv5.get_sample_data( + camera_token, flat_vehicle_coordinates=False + ) + + arr = np.array(im) + + for box in boxes: + c = NAME2COLOR[box.name] + box.render_cv2(arr, normalize=True, view=camera_intrinsic, colors=(c, c, c)) + + new = Image.fromarray(arr) + return new + + +def build_figure(lv5, sample, lidar, camera, overlay): + points, coloring, im = compute_pointcloud_for_image( + lv5, sample["token"], pointsensor_channel=lidar, camera_channel=camera + ) + + if "boxes" in overlay: + im = render_box_in_image(lv5, im, sample, camera_channel=camera) + + fig = px.imshow(im, binary_format="jpeg", binary_compression_level=2) + + if "pointcloud" in overlay: + fig.add_trace( + go.Scattergl( + x=points[0,], + y=points[1,], + mode="markers", + opacity=0.4, + marker_color=coloring, + marker_size=3, + ) + ) + + fig.update_layout( + margin=dict(l=10, r=10, t=0, b=0), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + hovermode=False, + ) + fig.update_xaxes(showticklabels=False, showgrid=False, range=(0, im.size[0])) + fig.update_yaxes(showticklabels=False, showgrid=False, range=(im.size[1], 0)) + + return fig From 5fb128e9264e2a23c22caad2f4223515d6a3b6a1 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Thu, 19 May 2022 23:18:49 +0100 Subject: [PATCH 2/2] v2 --- apps/dash-lyft-explorer/requirements.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/dash-lyft-explorer/requirements.txt b/apps/dash-lyft-explorer/requirements.txt index a78bbebd8..0e5ee0855 100644 --- a/apps/dash-lyft-explorer/requirements.txt +++ b/apps/dash-lyft-explorer/requirements.txt @@ -6,8 +6,4 @@ numpy==1.22.3 colorlover==0.3.0 lyft-dataset-sdk==0.0.8 pydeck==0.7.1 -gunicorn==20.1.0 - -pydeck==0.5.* -Pillow -opencv-python==4.2.0.34 +gunicorn==20.1.0 \ No newline at end of file