Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draw bbox #118

Merged
merged 7 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
build_sites_markers,
display_alert_selection_area,
past_ndays_live_events,
process_bbox,
retrieve_site_from_device_id,
)

Expand Down Expand Up @@ -117,6 +118,7 @@
id="store_live_alerts_data", storage_type="session", data=json.dumps({"status": "never_loaded_alerts_data"})
),
dcc.Store(id="images_url_live_alerts", storage_type="session", data={}),
dcc.Store(id="images_boxes_live_alerts", storage_type="session", data={}),
dcc.Store(id="last_displayed_event_id", storage_type="session"),
dcc.Store(id="images_to_display_on_big_screen", data={"frame_URLs": "no_images"}, storage_type="session"),
html.Div(id="alert_frame_update_new_event", style={"display": "none"}),
Expand Down Expand Up @@ -215,6 +217,7 @@ def change_layer_style(n_clicks=None):
[
Output("store_live_alerts_data", "data"),
Output("images_url_live_alerts", "data"),
Output("images_boxes_live_alerts", "data"),
Output("loaded_frames", "data"),
Output("main_api_fetch_interval", "interval"),
Output("sites_with_live_alerts", "children"),
Expand All @@ -223,6 +226,7 @@ def change_layer_style(n_clicks=None):
[
State("store_live_alerts_data", "data"),
State("images_url_live_alerts", "data"),
State("images_boxes_live_alerts", "data"),
State("devices_data_storage", "data"),
State("loaded_frames", "data"),
State("site_devices_data_storage", "data"),
Expand All @@ -235,6 +239,7 @@ def update_live_alerts_data(
n_intervals,
ongoing_live_alerts,
ongoing_frame_urls,
ongoing_frame_boxes,
devices_data,
already_loaded_frames,
site_devices_data,
Expand Down Expand Up @@ -285,6 +290,7 @@ def update_live_alerts_data(

# Fetching live_alerts frames urls and instantiating a dict of live_alerts urls having event_id keys
dict_images_url_live_alerts: Dict[str, List[str]] = {}
dict_images_box_live_alerts: Dict[str, List[str]] = {}

# This void list will store the names of the sites for which a live alert is being displayed,
# Enabling us later on to hide the corresponding site marker and only display the alert one
Expand All @@ -299,14 +305,20 @@ def update_live_alerts_data(
# This is just a security in case we cannot retrieve the URL of the detection frame
img_url = ""

# Process bounding boxes
localization = row["localization"]
localization = process_bbox(localization)

# We now want to fill-in the dictionary that will contain the URLs of the detection frames
if str(row["event_id"]) not in dict_images_url_live_alerts.keys():
# This is a new event, so we need to instantiate the key / value pair
dict_images_url_live_alerts[str(row["event_id"])] = [img_url]
dict_images_box_live_alerts[str(row["event_id"])] = [localization]

else:
# We already have some URLs for this event and we simply append the latest frame to the list of URLs
dict_images_url_live_alerts[str(row["event_id"])].append(img_url)
dict_images_box_live_alerts[str(row["event_id"])].append(localization)

try:
# We use the device ID to retrieve the name of the corresponding site
Expand All @@ -332,6 +344,7 @@ def update_live_alerts_data(
# We convert the live_alerts DataFrame into a JSON that can be stored in a dcc.Store component
live_alerts.to_json(orient="records"),
dict_images_url_live_alerts,
dict_images_box_live_alerts,
{"loaded_frames": new_loaded_frames},
5 * 1000,
sites_with_live_alerts,
Expand Down Expand Up @@ -359,9 +372,11 @@ def update_live_alerts_data(

if len(live_alerts) < len(ongoing_live_alerts): # alerts have been acknowledged
dict_images_url_live_alerts = ongoing_frame_urls.copy()
dict_images_box_live_alerts = ongoing_frame_boxes.copy()
for event_id in ongoing_frame_urls.keys():
if int(event_id) not in list(live_events["id"]):
del dict_images_url_live_alerts[event_id]
del dict_images_box_live_alerts[event_id]

new_loaded_frames = list(live_alerts["id"].unique())

Expand All @@ -381,6 +396,7 @@ def update_live_alerts_data(
return [
live_alerts.to_json(orient="records"),
dict_images_url_live_alerts,
dict_images_box_live_alerts,
{"loaded_frames": new_loaded_frames},
5 * 1000,
sites_with_live_alerts,
Expand All @@ -397,6 +413,9 @@ def update_live_alerts_data(
# We start from a copy of the existing one (which we got from the dedicated dcc.Store component)
dict_images_url_live_alerts = ongoing_frame_urls.copy()

# Same for bounding boxes
dict_images_box_live_alerts = ongoing_frame_boxes.copy()

# This void list will store the names of the sites for which a live alert is being displayed,
# Enabling us later on to hide the corresponding site marker and only display the alert one
sites_with_live_alerts = []
Expand All @@ -410,12 +429,20 @@ def update_live_alerts_data(
# This is just a security in case we cannot retrieve the URL of the detection frame
img_url = ""

# Process bounding boxes
localization = row["localization"]
localization = process_bbox(localization)

# We update the detection frame URL dictionary with the same method as above
if str(row["event_id"]) not in dict_images_url_live_alerts.keys():
# This is a new event, so we need to instantiate the key / value pair
dict_images_url_live_alerts[str(row["event_id"])] = [img_url]
dict_images_box_live_alerts[str(row["event_id"])] = [localization]

else:
# We already have some URLs for this event and we simply append the latest frame to the list of URLs
dict_images_url_live_alerts[str(row["event_id"])].append(img_url)
dict_images_box_live_alerts[str(row["event_id"])].append(localization)

try:
# We use the device ID to retrieve the name of the corresponding site
Expand All @@ -442,6 +469,7 @@ def update_live_alerts_data(
return [
live_alerts.to_json(orient="records"),
dict_images_url_live_alerts,
dict_images_box_live_alerts,
{"loaded_frames": new_loaded_frames},
5 * 1000,
sites_with_live_alerts,
Expand All @@ -457,6 +485,7 @@ def update_live_alerts_data(
return [
dash.no_update,
dict_images_url_live_alerts,
dict_images_box_live_alerts,
{"loaded_frames": new_loaded_frames},
5 * 1000,
sites_with_live_alerts,
Expand Down Expand Up @@ -1058,22 +1087,42 @@ def display_alert_modal(n_clicks):


@app.callback(
Output({"type": "alert_frame", "index": MATCH}, "src"),
Input({"type": "alert_slider", "index": MATCH}, "value"),
State({"type": "individual_alert_frame_storage", "index": MATCH}, "children"),
[
Output({"type": "alert_frame", "index": MATCH}, "src"),
Output({"type": "alert_bbox_container", "index": MATCH}, "children"),
],
[
Input({"type": "alert_slider", "index": MATCH}, "value"),
Input({"type": "bbox_toggle_checklist", "index": MATCH}, "value"),
],
[
State({"type": "individual_alert_frame_storage", "index": MATCH}, "children"),
State("images_boxes_live_alerts", "data"),
],
)
def select_alert_frame_to_display(slider_value, urls):
"""
--- Choosing the alert frame to be displayed in an alert modal ---

When the user has opened an alert modal, he or she can choose the alert frame to view thanks to the slider. This
callback is triggered by a change in value of the slider and return the URL address of the frame to be displayed.
Like there is one alert modal per event, there is one alert slider per event, which allows to use MATCH here.
"""
def select_alert_frame_to_display(slider_value, bbox_toggle_value, urls, bboxes_dict):
if slider_value is None:
raise PreventUpdate

return urls[slider_value - 1] # Slider value starts at 1 and not 0
event_id = dash.callback_context.inputs_list[0]["id"]["index"]
bboxes = bboxes_dict.get(event_id, [])[slider_value - 1]

bbox_children = []
if "ON" in bbox_toggle_value: # If ON is in the value list of checklist, show the bounding box
for bbox in bboxes:
x_center, y_center, width, height = bbox
bbox_style = {
"position": "absolute",
"left": f"{x_center - width / 2}px",
"top": f"{y_center - height / 2}px",
"width": f"{width}px",
"height": f"{height}px",
"border": "2px solid red",
"z-index": 2,
}
bbox_children.append(html.Div(style=bbox_style))

return urls[slider_value - 1], bbox_children


# ----------------------------------------------------------------------------------------------------------------------
Expand Down
69 changes: 43 additions & 26 deletions app/utils/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"""


import ast
import json
from datetime import datetime, timedelta
from typing import List

import dash_bootstrap_components as dbc
import dash_core_components as dcc
Expand Down Expand Up @@ -291,19 +293,34 @@ def build_alerts_elements(images_url_live_alerts, live_alerts, map_style):
style={"position": "absolute", "top": "10px", "right": "30px", "z-index": "1000"},
)

individual_alert_frame_placeholder_children = []
for event_id, frame_url_list in images_url_live_alerts.items():
individual_alert_frame_placeholder_children.append(
html.Div(
id={"type": "individual_alert_frame_storage", "index": str(event_id)},
children=frame_url_list,
style={"display": "none"},
)
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True that it's actually never used, but this means that this Callback is actually creating the div

return [alert_button, alerts_markers_layer, navbar_color, navbar_title]


def process_bbox(input_str, image_width=700, width_height_ratio=0.5625):
new_boxes: List[List[int]] = []

# Check if input_str is not None and is a valid string
if not isinstance(input_str, str) or not input_str:
return new_boxes

try:
boxes = ast.literal_eval(input_str)
except (ValueError, SyntaxError):
print("error parsing")
# Return an empty list if there's a parsing error
return new_boxes

for x0, y0, x1, y1, _ in boxes:
width = (x1 - x0) * image_width
height = (y1 - y0) * image_width * width_height_ratio
x_center = x0 * image_width + width / 2
y_center = y0 * image_width * width_height_ratio + height / 2

new_boxes.append([int(x_center), int(y_center), int(width), int(height)])

return new_boxes


def build_alert_modal(event_id, device_id, lat, lon, site_name, urls):
number_of_images = len(urls)

Expand Down Expand Up @@ -347,20 +364,10 @@ def build_alert_modal(event_id, device_id, lat, lon, site_name, urls):
style={"backgroundColor": "#5BBD8C", "fontColor": "#2C796E"},
),
html.Br(),
html.Div(
id={"type": "alert_relevance_space", "index": str(event_id)},
children=[
html.P("L'alerte est-elle pertinente ?", style={"text-indent": "15px"}),
dbc.RadioItems(
id={"type": "alert_relevance_radio_button", "index": str(event_id)},
options=[
{"label": "Oui", "value": True},
{"label": "Non", "value": False},
],
value=None,
labelStyle={"display": "inline-block"},
),
],
dcc.Checklist(
options=[{"label": " Afficher la predicition", "value": "ON"}],
value=["ON"], # default to showing the bounding box
id={"type": "bbox_toggle_checklist", "index": str(event_id)},
),
html.Br(),
html.Div(
Expand All @@ -385,8 +392,18 @@ def build_alert_modal(event_id, device_id, lat, lon, site_name, urls):
),
dbc.Col(
[
html.Img(
id={"type": "alert_frame", "index": str(event_id)}, src=urls[0], width="700px"
html.Div(
style={"position": "relative"},
children=[
html.Img(
id={"type": "alert_frame", "index": str(event_id)},
src=urls[0],
width="700px",
),
html.Div(
id={"type": "alert_bbox_container", "index": str(event_id)}, children=[]
),
],
),
dcc.Markdown("---"),
dcc.Slider(
Expand Down
Loading