scripts/ui/common.py (948 lines of code) (raw):
#!/usr/bin/env python3
# Copyright 2004-present Facebook. All Rights Reserved.
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""Common functions used across the UI tabs.
The UI shares several common functions across its tabs. Unlike dep_util, this file
contains functions that specifically reference elements in the tab. This means, if
further extension of the UI is pursued, this file should be reserved for common
functions that are *explicitly* tied to the UI and dep_util for functions that could
be used in contexts outside the UI.
"""
import collections
import datetime
import glob
import os
import shutil
import subprocess
import sys
from PyQt5 import QtCore, QtWidgets
dir_scripts = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
dir_root = os.path.dirname(dir_scripts)
sys.path.append(dir_root)
sys.path.append(os.path.join(dir_scripts, "aws"))
sys.path.append(os.path.join(dir_scripts, "render"))
sys.path.append(os.path.join(dir_scripts, "util"))
import dep_util
import glog_check as glog
import scripts.render.config as config
from log_reader import LogReader
from scripts.aws.create import (
get_render_pid,
get_staging_info,
has_render_flag,
run_ssh_command,
)
from scripts.aws.util import AWSUtil
from scripts.render.network import LAN
from scripts.util.system_util import (
get_flags,
get_flags_from_flagfile,
image_type_paths,
run_command,
)
from slider_image_thresholds import SliderWidget
script_dir = os.path.dirname(os.path.realpath(__file__))
scripts_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
dep_dir = os.path.join(scripts_dir, os.pardir)
dep_bin_dir = os.path.join(dep_dir, "build", "bin")
dep_res_dir = os.path.join(dep_dir, "res")
dep_flags_dir = os.path.join(dep_res_dir, "flags")
os.makedirs(dep_flags_dir, exist_ok=True)
source_root = os.path.join(dep_dir, "source")
depth_est_src = os.path.join(source_root, "depth_estimation")
render_src = os.path.join(source_root, "render")
render_scripts = os.path.join(scripts_dir, "render")
type_color_var = "color_variance"
type_fg_mask = "fg_mask"
threshold_sliders = {
# attr: type, printed name, slider index, max value, default value
"noise": [type_color_var, "Noise variance", 1, 1.5e-3, 4e-5],
"detail": [type_color_var, "Detail variance", 2, 2e-2, 1e-3],
"blur": [type_fg_mask, "Blur radius", 1, 20, 2],
"closing": [type_fg_mask, "Closing size", 2, 20, 4],
"thresh": [type_fg_mask, "Threshold", 3, 1, 3e-2],
}
def init(parent):
"""Sets up all the UI global internals (logs, data, and flags) and any
tab specific components.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
parent.is_refreshing_data = True
parent.initialize_paths()
parent.set_default_top_level_paths()
parent.setup_logs()
parent.setup_data()
parent.setup_flags()
if "retrieve_missing_flagfiles" in dir(parent):
parent.retrieve_missing_flagfiles()
if "add_default_flags" in dir(parent):
parent.add_default_flags()
if "setup_thresholds" in dir(parent):
parent.setup_thresholds()
if "add_data_type_validators" in dir(parent):
parent.add_data_type_validators()
if "setup_farm" in dir(parent):
parent.setup_farm()
if "update_run_button_text" in dir(parent):
parent.update_run_button_text()
parent.is_refreshing_data = False
def setup_aws_config(parent):
"""Sets up the configuration of the Kubernetes cluster.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
if parent.parent.is_aws:
create_flagfile = os.path.join(
parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_create]
)
if os.path.exists(create_flagfile):
create_flags = get_flags_from_flagfile(create_flagfile)
if "cluster_size" in create_flags:
spin_num_workers = getattr(
parent.dlg, f"spin_{parent.tag}_farm_num_workers", None
)
spin_num_workers.setValue(int(create_flags["cluster_size"]))
if "instance_type" in create_flags:
dd_ec2 = getattr(parent.dlg, f"dd_{parent.tag}_farm_ec2", None)
dd_ec2.setCurrentText(create_flags["instance_type"])
def setup_farm(parent):
"""Sets up the UI to interact with a LAN cluster.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
initialize_farm_groupbox(parent)
ip_begin, _ = parent.parent.ui_flags.master.rsplit(".", 1)
parent.lan = LAN(f"{ip_begin}.255")
def get_tooltip(parent, app_name):
"""Gets the help tooltip display of a binary.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
app_name (str): Name of the binary.
Returns:
str: Help from the binary.
"""
dir = scripts_dir if app_name.endswith(".py") else dep_bin_dir
tooltip = dep_util.get_tooltip(os.path.join(dir, app_name))
if not tooltip:
parent.log_reader.log_warning(f"Cannot get tooltip for: {app_name}")
return tooltip
def initialize_paths(parent):
"""Initializes paths for scripts and flags depending on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
parent.app_name_to_flagfile = {}
if tag in ["bg", "depth", "export"]:
parent.app_name = "render/render.py"
if tag in ["depth", "export"]:
parent.app_aws_clean = "aws/clean.py"
parent.app_aws_create = "aws/create.py"
parent.app_name_to_flagfile[parent.app_aws_clean] = "clean.flags"
if tag == "calibrate":
parent.app_name = "Calibration"
parent.flagfile_basename = "calibration.flags"
elif tag == "bg":
parent.flagfile_basename = "render_background.flags"
elif tag == "depth":
parent.flagfile_basename = "render_depth.flags"
parent.app_name_to_flagfile[parent.app_aws_create] = "aws_create_video.flags"
elif tag == "export":
parent.flagfile_basename = "render_export.flags"
parent.app_name_to_flagfile[parent.app_aws_create] = "aws_create_export.flags"
parent.app_aws_download_meshes = "aws/download_meshes.py"
parent.app_name_to_flagfile[
parent.app_aws_download_meshes
] = "download_meshes.flags"
parent.app_name_to_flagfile[parent.app_name] = parent.flagfile_basename
parent.tooltip = get_tooltip(parent, parent.app_name)
parent.is_refreshing_data = False
parent.is_process_killed = False
parent.threshs_tooltip = "Click and drag to pan, scroll to zoom in and out"
parent.script_dir = script_dir
def setup_logs(parent):
"""Sets up logging system for dialog on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
Returns:
LogReader: Reader configured for the current tab.
"""
tag = parent.tag
qt_text_edit = getattr(parent.dlg, f"text_{tag}_log", None)
qt_tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
tab_idx = qt_tab_widget.count() - 1 # log is always the last tab
ts = dep_util.get_timestamp("%Y%m%d%H%M%S.%f")
name = parent.__class__.__name__
log_file = os.path.join(parent.path_logs, f"{name}_{ts}")
log_reader = LogReader(qt_text_edit, parent, log_file)
log_reader.set_tab_widget(qt_tab_widget, tab_idx)
return log_reader
def setup_flagfile_tab(parent):
"""Sets up the flags according to the corresponding flagfile on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
dlg = parent.dlg
qt_text_edit = getattr(dlg, f"text_{tag}_flagfile_edit", None)
qt_btn_save = getattr(dlg, f"btn_{tag}_flagfile_save", None)
qt_text_edit.textChanged.connect(parent.on_changed_flagfile_edit)
qt_btn_save.clicked.connect(parent.save_flag_file)
qt_btn_save.setEnabled(False)
def setup_file_explorer(parent):
"""Creates the file explorer rooted on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
dlg = parent.dlg
parent.fs_tree = dlg.tree_file_explorer
path = parent.path_project
parent.fs_model, parent.fs_tree = dep_util.setup_file_explorer(parent.fs_tree, path)
parent.fs_tree.clicked.connect(lambda: preview_file(parent))
def preview_file(parent):
"""Displays the file and its label on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
dlg = parent.dlg
frame = dlg.label_preview_image
label = dlg.label_preview_path
project = parent.path_project
prefix = f"{project}/"
dep_util.preview_file(parent.fs_model, parent.fs_tree, frame, label, prefix)
def switch_ui_elements_for_processing(parent, gb, state):
"""Switches element interaction when processing on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
state (str): Identifier of the callback state.
"""
# Buttons
parent.update_buttons(gb)
# Switch all other sections, except the file explorer
dlg = parent.dlg
for gbi in dlg.findChildren(QtWidgets.QGroupBox):
if gbi != gb and not gbi.objectName().endswith("_file_explorer"):
gbi.setEnabled(state)
# Switch current group box elements
prefixes = ["cb_", "dd_", "val_", "label_"]
dep_util.switch_objects_prefix(gb, prefixes, state)
# Switch tabs that are not image preview or log
for w in dlg.findChildren(QtWidgets.QWidget):
name = w.objectName()
ignore = name.endswith("_preview") or name.endswith("_log")
if name.startswith("tab_") and not ignore:
w.setEnabled(state)
# Switch other sections
for s in parent.parent.sections:
if s != parent:
dep_util.set_tab_enabled(parent.dlg.w_steps, s.tag, state)
def cancel_process(parent):
"""Stops a running process on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
running_render = False # Render has to be explicitly killed since it runs detached
if parent.is_farm and parent.is_aws:
processes = parent.log_reader.get_processes()
for process in processes:
if process == "run_aws_create" or process.startswith("run_export"):
running_render = True
if running_render:
aws_util = AWSUtil(
parent.path_aws_credentials, region_name=parent.parent.aws_util.region_name
)
_, ip_staging = get_staging_info(aws_util, parent.path_aws_ip_file)
if ip_staging:
render_pid = get_render_pid(parent.path_aws_key_fn, ip_staging)
if render_pid is not None:
run_ssh_command(
parent.path_aws_key_fn, ip_staging, f"kill -9 {render_pid}"
)
parent.log_reader.kill_all_processes()
parent.is_process_killed = True
if "reset_run_button_text" in dir(parent):
parent.reset_run_button_text()
def is_cloud_running_process(parent):
"""Checks if a render process is being run on the cloud"""
key_fn = parent.path_aws_key_fn
if not parent.is_aws or not parent.is_farm or not os.path.isfile(key_fn):
return False
aws_util = AWSUtil(
parent.path_aws_credentials, region_name=parent.parent.aws_util.region_name
)
_, ip_staging = get_staging_info(
aws_util, parent.path_aws_ip_file, start_instance=False
)
if not ip_staging:
return False
tag = parent.tag
if tag not in ["depth", "export"]:
return False
flag = "run_depth_estimation"
value = tag == "depth"
return has_render_flag(key_fn, ip_staging, flag, value)
def sync_with_s3(parent, gb, subdirs):
"""Synchronizes data from the local directory to S3.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
subdirs (list[str]): Local path to be synced.
"""
run_silently = not parent.parent.ui_flags.verbose
cmds = []
parent.log_reader.log_notice(f"Syncing frames with S3...")
for subdir in subdirs:
local = os.path.join(config.DOCKER_INPUT_ROOT, subdir)
remote = os.path.join(parent.parent.ui_flags.project_root, subdir)
if "_levels" in subdir:
locals = [
os.path.join(local, f"level_{l}") for l in range(len(config.WIDTHS))
]
else:
locals = [local]
# Tar frames
tar_app_path = os.path.join(scripts_dir, "util", "tar_frame.py")
for local_i in locals:
frames = dep_util.get_frame_list(local_i)
if not frames:
if not run_silently:
print(glog.yellow(f"No frames found for S3 syncing in {local_i}"))
continue
for frame in frames:
cmds.append(f"python3.7 {tar_app_path} --src={local_i} --frame={frame}")
cmds.append(f"aws s3 sync {local} {remote} --exclude '*' --include '*.tar'")
p_id = f"sync_results_s3_{parent.tag}"
cmd_and = " && ".join(cmds)
cmd = f'/bin/sh -c "{cmd_and}"'
start_process(parent, cmd, gb, p_id, run_silently)
def on_process_finished(parent, p_id):
"""Callback event handler for a process completing on the specified tab.
Args:
p_id (str): PID of completed process.
"""
if not p_id or p_id.startswith("run"):
parent.log_reader.remove_processes()
else:
parent.log_reader.remove_process(p_id)
parent.refresh_data()
if p_id.startswith("run") and "_export_" not in p_id:
if "update_frame_names" in dir(parent):
parent.update_frame_names()
if "sync_with_s3" in dir(parent) and not parent.is_process_killed:
if parent.parent.is_aws:
parent.sync_with_s3()
if len(parent.log_reader.get_processes()) == 0:
# Re-enable UI elements
switch_ui_elements_for_processing(parent, parent.log_reader.gb, True)
# We may have data to enable other tabs
if p_id.startswith("run"):
[s.refresh_data() for s in parent.parent.sections if s != parent]
if "update_run_button_text" in dir(parent):
parent.update_run_button_text()
parent.is_process_killed = False
def populate_dropdown(parent, gb, dd):
"""Populates a dropdown on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
dd (QtWidgets.QComboBox): Dropdown UI element.
"""
project = parent.parent.path_project
t = dep_util.remove_prefix(gb.objectName(), "gb_")
dd_prev_text = dd.currentText() if dd.count() > 0 else ""
tag = dep_util.remove_prefix(dd.objectName(), f"dd_{t}_")
ps = parent.get_files(tag)
dep_util.populate_dropdown(dd, ps, f"{project}/")
dep_util.update_qt_dropdown(dd, dd_prev_text, add_if_missing=False)
def populate_dropdowns(parent, gb, dd_first=None):
"""Populates the dropdowns on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
dd_first (list[QtWidgets.QGroupBox], optional): Dropdowns to populate first.
"""
if not dd_first:
dd_first = []
for dd in dd_first:
populate_dropdown(parent, gb, dd)
for dd in gb.findChildren(QtWidgets.QComboBox):
if dd not in dd_first:
populate_dropdown(parent, gb, dd)
def refresh_data(parent):
"""Updates UI elements to be in sync with data on disk on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
dlg = parent.dlg
tab = getattr(dlg, f"t_{tag}", None)
if tag in ["bg", "depth", "export"]:
parent.path_rig_json = get_calibrated_rig_json(parent)
if tag == "depth":
parent.update_bg_checkbox()
# This locks the dropdown callbacks while we re-populate them
parent.is_refreshing_data = True
for gb in tab.findChildren(QtWidgets.QGroupBox):
gb.setEnabled(True)
parent.populate_dropdowns(gb)
parent.update_buttons(gb)
if "flagfile_fn" in dir(parent):
sync_data_and_flagfile(parent, parent.flagfile_fn)
parent.disable_tab_if_no_data()
parent.is_refreshing_data = False
def update_flagfile_edit(parent, flagfile_fn, switch_to_flag_tab=False):
"""Updates the edit box for the flagfile on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flagfile_fn (str): Name of the flagfile.
switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
"""
if not os.path.isfile(flagfile_fn):
return
tag = parent.tag
dlg = parent.dlg
text = getattr(dlg, f"text_{tag}_flagfile_edit", None)
preview = getattr(dlg, f"w_{tag}_preview", None)
text.setPlainText(open(flagfile_fn).read())
if switch_to_flag_tab:
preview.setCurrentIndex(1)
def update_data_or_flags(
parent, flagfile_fn, flagfile_from_data, switch_to_flag_tab=False
):
"""Updates the flagfile from the UI elements or vice versa on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flagfile_fn (str): Name of the flagfile.
flagfile_from_data (bool): Whether to load the flagfile from the data (True) or
vice versa (False).
switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
"""
if not flagfile_fn:
return
flags = get_flags_from_flagfile(flagfile_fn)
if flagfile_from_data:
parent.update_flags_from_data(flags)
else:
parent.update_data_from_flags(flags)
if flagfile_from_data:
# Overwrite flag file
sorted_flags = collections.OrderedDict(sorted(flags.items()))
dep_util.write_flagfile(flagfile_fn, sorted_flags)
# Refresh flagfile edit window
parent.update_flagfile_edit(flagfile_fn, switch_to_flag_tab)
def sync_data_and_flagfile(
parent, flagfile_fn, set_label=True, switch_to_flag_tab=False
):
"""Synchronizes displayed UI elements and contents of the flagfile.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flagfile_fn (str): Name of the flagfile.
set_label (bool, optional): Whether or not to update the flagfile label in the UI.
switch_to_flag_tab (bool, optional): Whether or not to switch tabs after updating.
"""
tag = parent.tag
dlg = parent.dlg
label = getattr(dlg, f"label_{tag}_flagfile_path", None)
flagfile = os.path.basename(flagfile_fn)
label.setText(flagfile)
# flag file to data first, then data to flag file for missing info
flagfile_from_data = False
parent.update_data_or_flags(flagfile_fn, flagfile_from_data, switch_to_flag_tab)
parent.update_data_or_flags(flagfile_fn, not flagfile_from_data, switch_to_flag_tab)
def disable_tab_if_no_data(parent, btn_run):
"""Prevents navigation to the tab if the required data is not present on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
btn_run (QtWidgets.QPushButton): UI button for tab switch.
"""
if not btn_run.isEnabled():
dep_util.set_tab_enabled(parent.dlg.w_steps, parent.tag, enabled=False)
def setup_project(parent, mkdirs=False):
"""Retrieves any missing flagfiles and sets the default flags on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
mkdirs (bool, optional): Whether or not to make the defined directories.
"""
parent.is_refreshing_data = True
parent.log_reader.log_header()
parent.refresh_data()
parent.is_refreshing_data = False
def save_flag_file(parent, flagfile_fn):
"""Saves flagfile from the UI to disk on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flagfile_fn (str): Name of the flagfile.
"""
if not os.path.isfile(flagfile_fn):
return
tag = parent.tag
dlg = parent.dlg
text_edit = getattr(dlg, f"text_{tag}_flagfile_edit", None)
btn_save = getattr(dlg, f"btn_{tag}_flagfile_save", None)
with open(flagfile_fn, "w") as f:
f.write(text_edit.toPlainText())
f.close()
# Disable save button
btn_save.setEnabled(False)
# Update corresponding groupbox
flagfile_from_data = False # flagfile to data
parent.update_data_or_flags(flagfile_fn, flagfile_from_data)
def update_flagfile(parent, flagfile_fn):
"""Updates the edit box for the flagfile on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flagfile_fn (str): Name of the flagfile.
"""
parent.update_data_or_flags(flagfile_fn, flagfile_from_data=True)
def retrieve_missing_flagfiles(parent):
"""Copies the missing flagfiles to project for local modification on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
if tag == "calibrate":
ff_base = "calibration.flags"
elif tag in ["bg", "depth", "export"]:
ff_base = "render.flags"
ffs_expected = [[ff_base, parent.flagfile_fn]]
if tag in ["depth", "export"]:
ff_aws_create = os.path.join(
parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_create]
)
ffs_expected.append(["aws_create.flags", ff_aws_create])
for ff_src_rel, ff_dst_abs in ffs_expected:
if not os.path.isfile(ff_dst_abs):
ff_src_abs = os.path.join(dep_flags_dir, ff_src_rel)
os.makedirs(os.path.dirname(ff_dst_abs), exist_ok=True)
shutil.copyfile(ff_src_abs, ff_dst_abs)
update_flagfile(parent, ff_dst_abs)
def add_default_flags(parent):
"""Retrieves the default flags to the local flagfile on the specified tab from
either the source or scripts binaries.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
default_flags = {}
tag = parent.tag
if tag in ["bg", "depth"]:
default_flags.update(
{
os.path.join(depth_est_src, "DerpCLI.cpp"): {
"max_depth_m",
"min_depth_m",
"resolution",
"var_high_thresh",
"var_noise_floor",
}
}
)
if tag == "depth":
default_flags.update(
{
os.path.join(render_scripts, "setup.py"): {"do_temporal_filter"},
os.path.join(depth_est_src, "TemporalBilateralFilter.cpp"): {
"time_radius"
},
os.path.join(render_src, "GenerateForegroundMasks.cpp"): {
"blur_radius",
"morph_closing_size",
"threshold",
},
}
)
elif tag == "export":
default_flags.update(
{
os.path.join(render_src, "SimpleMeshRenderer.cpp"): {"width"},
os.path.join(render_src, "ConvertToBinary.cpp"): {"output_formats"},
}
)
flagfile_fn = os.path.join(parent.path_flags, parent.flagfile_basename)
flags = get_flags_from_flagfile(flagfile_fn)
for source in default_flags:
if os.path.isfile(source):
source_flags = get_flags(source)
else:
source_flags
desired_flags = default_flags[source]
for source_flag in source_flags:
flag_name = source_flag["name"]
# Only add the default flag if not already present in current flags
if flag_name in desired_flags:
if flag_name not in flags or flags[flag_name] == "":
flags[flag_name] = source_flag["default"]
# Add run flags
if tag == "bg":
flags["run_generate_foreground_masks"] = False
flags["run_precompute_resizes"] = True
flags["run_depth_estimation"] = True
flags["run_convert_to_binary"] = False
flags["run_fusion"] = False
flags["run_simple_mesh_renderer"] = False
flags["use_foreground_masks"] = False
elif tag == "depth":
flags["run_depth_estimation"] = True
flags["run_precompute_resizes"] = True
flags["run_precompute_resizes_foreground"] = True
flags["run_convert_to_binary"] = False
flags["run_fusion"] = False
flags["run_simple_mesh_renderer"] = False
elif tag == "export":
flags["run_generate_foreground_masks"] = False
flags["run_precompute_resizes"] = False
flags["run_precompute_resizes_foreground"] = False
flags["run_depth_estimation"] = False
# Overwrite flag file
sorted_flags = collections.OrderedDict(sorted(flags.items()))
dep_util.write_flagfile(flagfile_fn, sorted_flags)
def get_calibrated_rig_json(parent):
"""Finds calibrated rig in the project.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
Returns:
str: Name of the calibrated rig (assumes the rig contains "_calibrated.json").
"""
has_log_reader = "log_reader" in dir(parent)
ps = dep_util.get_files_ext(parent.path_rigs, "json", "calibrated")
if len(ps) == 0:
if has_log_reader:
parent.log_reader.log_warning(f"No rig files found in {parent.path_rigs}")
return ""
if len(ps) > 1:
ps_str = "\n".join(ps)
if has_log_reader:
parent.log_reader.log_warning(
f"Too many rig files found in {parent.path_rigs}:\n{ps_str}"
)
return ""
return ps[0]
def update_run_button_text(parent, btn):
"""Updates the text of the Run button depending on the existance of a process
running on the cloud
"""
text_run_btn = "Run"
if is_cloud_running_process(parent):
text_run_btn = "Re-attach"
btn.setText(text_run_btn)
def update_buttons(parent, gb, ignore=None):
"""Enables buttons and dropdowns according to whether or not data is present on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
ignore (list[QtWidgets.QGroupBox], optional): Buttons to not update.
Returns:
tuple[bool, bool, bool]: Whether or not the UI is currently running a process and if it
has all its dropdowns.
"""
if not ignore:
ignore = []
has_all_dropdowns = True
for dd in gb.findChildren(QtWidgets.QComboBox):
if not dd.currentText() and dd not in ignore:
has_all_dropdowns = False
break
has_all_values = True
for v in gb.findChildren(QtWidgets.QLineEdit):
if v.objectName() and not v.text() and v not in ignore:
has_all_values = False
break
is_running = parent.log_reader.is_running()
for btn in gb.findChildren(QtWidgets.QPushButton):
btn_name = btn.objectName()
if btn in ignore:
continue
if btn_name.endswith("_run"):
btn.setEnabled(not is_running and has_all_dropdowns and has_all_values)
elif btn_name.endswith("_cancel"):
btn.setEnabled(is_running)
elif btn_name.endswith("_threshs"):
btn.setEnabled(not is_running and has_all_dropdowns)
elif btn_name.endswith("_view"):
btn.setEnabled(not is_running)
elif btn_name.endswith("_download_meshes"):
btn.setEnabled(not is_running)
return is_running, has_all_dropdowns, is_running
def on_changed_dropdown(parent, gb, dd):
"""Callback event handler for changed dropdown on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
dd (QtWidgets.QComboBox): Dropdown UI element.
"""
if not parent.is_refreshing_data:
name = dd.objectName()
if not name.endswith(
"_farm_ec2"
): # farm_ec2 dropdowns are not used in flagfile
parent.update_flagfile(parent.flagfile_fn)
# Check if we need to update the threshold image
if name.endswith(("_camera", "_frame_bg", "_first")):
# Check if we are already in a threshold tab, else default to color variance
tag = parent.tag
tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
tab_idx = tab_widget.currentIndex()
if tab_widget.widget(tab_idx).objectName().endswith("_fg_mask"):
type = type_fg_mask
else:
type = type_color_var
if "run_thresholds" in dir(parent):
parent.run_thresholds(type)
def on_changed_line_edit(parent, gb, le):
"""Callback event handler for changed line edit on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
le (_): Ignore
"""
if not parent.is_refreshing_data:
parent.update_buttons(gb)
parent.update_flagfile(parent.flagfile_fn)
def setup_groupbox(gb, callbacks):
"""Sets up callbacks for any groupboxes on the specified tab.
Args:
gb (QtWidgets.QGroupBox): Group box for the tab.
callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
"""
if gb.isCheckable() and gb in callbacks:
gb.toggled.connect(callbacks[gb])
def setup_checkboxes(gb, callbacks):
"""Sets up callbacks for any checkboxes on the specified tab.
Args:
gb (QtWidgets.QGroupBox): Group box for the tab.
callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
"""
for cb in gb.findChildren(QtWidgets.QCheckBox):
if cb in callbacks:
cb.stateChanged.connect(callbacks[cb])
def setup_dropdowns(parent, gb):
"""Sets up callbacks for any dropdowns on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QComboBox): Group box for the tab.
"""
if "on_changed_dropdown" in dir(parent):
for dd in gb.findChildren(QtWidgets.QComboBox):
dd.currentTextChanged.connect(
lambda state, y=gb, z=dd: parent.on_changed_dropdown(y, z)
)
dd.activated.connect(
lambda state, y=gb, z=dd: parent.on_changed_dropdown(y, z)
)
def setup_lineedits(parent, gb):
"""Sets up callbacks for any line edits on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
"""
if "on_changed_line_edit" in dir(parent):
for le in gb.findChildren(QtWidgets.QLineEdit):
le.textChanged.connect(
lambda state, y=gb, z=le: parent.on_changed_line_edit(y, z)
)
def setup_buttons(parent, gb, callbacks):
"""Sets up callbacks for any buttons on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
callbacks (dict[QtWidgets.QPushButton, func : QEvent -> _]): Callbacks for the UI elements.
"""
for btn in gb.findChildren(QtWidgets.QPushButton):
if btn in callbacks:
callback = callbacks[btn]
else:
name = btn.objectName()
callback = None
if name.endswith("_refresh"):
callback = parent.refresh
elif name.endswith("_run"):
callback = parent.run_process
elif name.endswith("_cancel"):
callback = parent.cancel_process
elif name.endswith("_threshs"):
callback = parent.run_thresholds
elif name.endswith("_logs"):
callback = parent.get_logs
else:
parent.log_reader.log_error(f"Cannot setup button {name}")
if callback:
btn.clicked.connect(callback)
def on_changed_preview(parent):
"""Callback event handler for changed image previews on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
tab_idx = tab_widget.currentIndex()
tab_name = tab_widget.widget(tab_idx).objectName()
if "_threshs_" in tab_name:
if tab_name.endswith("_fg_mask"):
type = type_fg_mask
else:
type = type_color_var
if not parent.is_refreshing_data:
parent.run_thresholds(type)
def setup_preview(parent):
"""Creates preview window in the UI and connects a callback on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
dlg = parent.dlg
btn_log_clear = getattr(dlg, f"btn_{tag}_log_clear", None)
text_log = getattr(dlg, f"text_{tag}_log", None)
preview = getattr(dlg, f"w_{tag}_preview", None)
btn_log_clear.clicked.connect(lambda: text_log.clear())
preview.setCurrentIndex(0)
if "on_changed_preview" in dir(parent):
preview.currentChanged.connect(parent.on_changed_preview)
def setup_data(parent, callbacks=None):
"""Sets up callbacks and initial UI element statuses on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
callbacks (dict[QtWidgets.QGroupBox, func : QEvent -> _]): Callbacks for the UI elements.
"""
tag = parent.tag
dlg = parent.dlg
tab = getattr(dlg, f"t_{tag}", None)
if not callbacks:
callbacks = {}
for gb in tab.findChildren(QtWidgets.QGroupBox):
setup_groupbox(gb, callbacks)
setup_checkboxes(gb, callbacks)
setup_dropdowns(parent, gb)
setup_lineedits(parent, gb)
setup_buttons(parent, gb, callbacks)
# Preview tabs
setup_preview(parent)
def update_noise_detail(parent, noise, detail):
"""Updates noise/detail thresholds interaction on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
noise (float): Noise threshold.
detail (float): Detail threshold.
"""
# Modify flagfile
parent.update_data_or_flags(
parent.flagfile_fn, flagfile_from_data=True, switch_to_flag_tab=False
)
# Update flagfile edit window
parent.update_flagfile_edit(parent.flagfile_fn, switch_to_flag_tab=False)
def update_fg_masks_thresholds(parent, blur, closing, thresh):
"""Updates thresholds and display for the foreground masking on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
blur (int, optional): Gaussian blur radius.
closing (int, optional): Closure (for sealing holes).
thresh (int, optional): Threshold applied to segment foreground and background
"""
# Modify flagfile
parent.update_data_or_flags(
parent.flagfile_fn, flagfile_from_data=True, switch_to_flag_tab=False
)
# Update flagfile edit window
parent.update_flagfile_edit(parent.flagfile_fn, switch_to_flag_tab=False)
def log_missing_image(parent, path_color, cam_id, frame):
"""Prints a warning if an image cannot be located.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
path_color (str): Path to the directory with color images.
cam_id (str): Name of the camera.
frame (str): Name of the frame (0-padded, six digits).
"""
parent.log_reader.log_warning(f"Cannot find frame {cam_id}/{frame} in {path_color}")
def update_thresholds_color_variance(parent, path_color, labels=None):
"""Updates the displayed thresholds for color variance on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
path_color (str): Path to the directory with color images.
labels (list[str], optional): Labels used to filter UI elements to update.
"""
labels = labels if labels is not None else ("_frame_bg", "_first")
dlg = parent.dlg
for dd in parent.dlg.findChildren(QtWidgets.QComboBox):
name = dd.objectName()
if name.endswith(labels):
frame = dd.currentText()
elif name.endswith("_camera"):
cam_id = dd.currentText()
image_path = dep_util.get_level_image_path(path_color, cam_id, frame)
if not image_path:
log_missing_image(parent, path_color, cam_id, frame)
return
tag = parent.tag
w_image = getattr(dlg, f"w_{tag}_threshs_image_{type_color_var}", None)
# Foreground masks are generated at the finest level of the pyramid
res = max(config.WIDTHS)
w_image.color_var.set_image(image_path, res)
noise = float(parent.slider_noise.get_label_text())
detail = float(parent.slider_detail.get_label_text())
project = parent.parent.path_project
fn = dep_util.remove_prefix(image_path, f"{project}/")
getattr(dlg, f"label_{tag}_threshs_filename_{type_color_var}", None).setText(fn)
# Force update
w_image.update_thresholds(noise=noise, detail=detail)
def update_thresholds_fg_mask(parent, paths_color):
"""Updates thresholds and display for the foreground masking using values from UI
on the specified tab."
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
paths_color (list[str]): Paths to the directory with color images.
"""
dlg = parent.dlg
frames = [None] * 2
for dd in parent.dlg.findChildren(QtWidgets.QComboBox):
name = dd.objectName()
if name.endswith("_frame_bg"):
frames[0] = dd.currentText()
elif name.endswith("_first"):
frames[1] = dd.currentText()
elif name.endswith("_camera"):
cam_id = dd.currentText()
bg_image_path = dep_util.get_level_image_path(paths_color[0], cam_id, frames[0])
if not bg_image_path:
log_missing_image(parent, paths_color[0], cam_id, frames[0])
return
fg_image_path = dep_util.get_level_image_path(paths_color[1], cam_id, frames[1])
if not fg_image_path:
log_missing_image(parent, paths_color[1], cam_id, frames[1])
return
tag = parent.tag
w_image = getattr(dlg, f"w_{tag}_threshs_image_{type_fg_mask}", None)
# Foreground masks are generated at the finest level of the pyramid
res = max(config.WIDTHS)
w_image.fg_mask.set_images(bg_image_path, fg_image_path, res)
blur = float(parent.slider_blur.get_label_text())
closing = float(parent.slider_closing.get_label_text())
thresh = float(parent.slider_thresh.get_label_text())
project = parent.parent.path_project
fn_bg = dep_util.remove_prefix(bg_image_path, f"{project}/")
fn_fg = dep_util.remove_prefix(fg_image_path, f"{project}/")
getattr(dlg, f"label_{tag}_threshs_filename_{type_fg_mask}", None).setText(
f"{fn_bg} vs {fn_fg}"
)
# Force update
w_image.update_thresholds(blur=blur, closing=closing, thresh=thresh)
def run_thresholds_after_wait(parent, type):
"""Computes the threshold and displays after a delay on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
can be run.
"""
# Apply flag file values in case it had unsaved changes
parent.save_flag_file()
tag = parent.tag
dlg = parent.dlg
label = getattr(dlg, f"label_{tag}_threshs_tooltip_{type}", None)
label.setToolTip(parent.threshs_tooltip)
getattr(dlg, f"w_{tag}_threshs_image_{type}", None).set_zoom_level(0)
if type == type_color_var:
parent.setup_thresholds_color_variance()
parent.update_thresholds_color_variance()
elif type == type_fg_mask:
parent.setup_thresholds_fg_masks()
parent.update_thresholds_fg_mask()
def run_thresholds(parent, type):
"""Runs thresholding based on values in the UI and update UI display on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
type (Union[ColorVariance, ForegroundMask]): Instance where thresholds are run.
"""
tag = parent.tag
tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
dep_util.switch_tab(tab_widget, f"_threshs_{type}")
# HACK: if we try to draw on a widget too quickly after switching tabs the resulting image
# does not span all the way to the width of the widget. We can wait a few milliseconds to
# let the UI "settle"
parent.timer = QtCore.QTimer(parent.parent)
parent.timer.timeout.connect(lambda: parent.run_thresholds_after_wait(type))
parent.timer.setSingleShot(True)
parent.timer.start(10) # 10ms
def output_has_images(output_dirs):
"""Whether or not outputs already have results.
Args:
output_dirs (list[str]): List of directories where outputs will be saved.
Returns:
bool: Whether or not the output directories all have at least one valid file.
"""
for d in output_dirs:
if dep_util.get_first_file_path(d):
return True
return False
def run_process_check_existing_output(parent, gb, app_name, flagfile_fn, p_id):
"""Run terminal process and raise on failure.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
app_name (str): Name of the binary being executed.
flagfile_fn (str): Name of the flagfile.
p_id (str): PID name of the process to be run.
"""
tag = parent.tag
cb_recompute = getattr(parent.dlg, f"cb_{tag}_recompute", None)
if cb_recompute is not None:
needs_rename = cb_recompute.isChecked()
if needs_rename:
# Rename current output directories using timestamp and create new empty ones
ts = dep_util.get_timestamp()
for d in parent.output_dirs:
if not os.path.isdir(d):
continue
d_dst = f"{d}_{ts}"
parent.log_reader.log_notice(
f"Saving copy of {d} to {d_dst} before re-computing"
)
shutil.move(d, d_dst)
os.makedirs(d, exist_ok=True)
run_process(parent, gb, app_name, flagfile_fn, p_id, not needs_rename)
def start_process(parent, cmd, gb, p_id, run_silently=False):
"""Runs a terminal process and disables UI element interaction.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
cmd (str): Command to run in the terminal.
gb (QtWidgets.QGroupBox): Group box for the tab.
p_id (str): PID name of the process being started.
"""
if not run_silently:
parent.log_reader.log(f"CMD: {cmd}")
parent.log_reader.gb = gb
parent.log_reader.setup_process(p_id)
parent.log_reader.start_process(p_id, cmd)
# Switch to log tab
tag = parent.tag
tab_widget = getattr(parent.dlg, f"w_{tag}_preview", None)
dep_util.switch_tab(tab_widget, "_log")
# Disable UI elements
parent.switch_ui_elements_for_processing(False)
def run_process(
parent, gb, app_name=None, flagfile_fn=None, p_id="run", overwrite=False
):
"""Runs an application on the terminal, using the associated flagfile.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
app_name (str, optional): Name of the binary being executed.
flagfile_fn (str, optional): Name of the flagfile to supply to the binary. this
will default to the flagfile associated with the binary if unspecified.
p_id (str, optional): PID name of the process being started.
overwrite (bool, optional): Whether or not to overwrite the local flagfile on disk.
"""
# Apply flag file values in case it had unsaved changes
parent.save_flag_file()
if not app_name:
app_name = parent.app_name
is_py_script = app_name.endswith(".py")
dir = scripts_dir if is_py_script else dep_bin_dir
app_path = os.path.join(dir, app_name)
if not os.path.isfile(app_path):
parent.log_reader.log_warning(f"App doesn't exist: {app_path}")
return
if not flagfile_fn:
flagfile_fn = parent.flagfile_fn
if output_has_images(parent.output_dirs) and not overwrite:
run_process_check_existing_output(parent, gb, app_name, flagfile_fn, p_id)
return
cmd = f'{app_path} --flagfile="{flagfile_fn}"'
if is_py_script:
cmd = f"python3.7 -u {cmd}"
start_process(parent, cmd, gb, p_id)
def update_thresholds(parent, gb, type):
"""Updates the displayed thresholds for either color variance or foreground masks.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
can be run.
"""
if type == type_color_var:
noise = parent.slider_noise.get_label_text()
detail = parent.slider_detail.get_label_text()
parent.update_noise_detail(noise, detail)
elif type == type_fg_mask:
blur = parent.slider_blur.get_label_text()
closing = parent.slider_closing.get_label_text()
thresh = parent.slider_thresh.get_label_text()
parent.update_fg_masks_thresholds(blur, closing, thresh)
# Update buttons
parent.update_buttons(gb)
def on_state_changed_partial_360(parent):
"""Callback event handler for changed "partial coverage" checkbox on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
if not parent.is_refreshing_data:
parent.update_flagfile(parent.flagfile_fn)
def on_state_changed_recompute(parent):
"""Callback event handler for changed "recompute" checkbox on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
if not parent.is_refreshing_data:
parent.update_flagfile(parent.flagfile_fn)
def on_state_changed_use_bg(parent, gb):
"""Callback event handler for changed "use background" checkbox on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
"""
if not parent.is_refreshing_data:
parent.update_buttons(gb)
parent.update_flagfile(parent.flagfile_fn)
def on_state_changed_farm(parent, state):
"""Callback event handler for changed "AWS" checkbox on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
state (str): Identifier of the callback state.
"""
parent.is_farm = state > 0
if not parent.is_refreshing_data:
if "update_frame_range_dropdowns" in dir(parent):
parent.update_frame_range_dropdowns()
if "update_run_button_text" in dir(parent):
parent.update_run_button_text()
parent.update_flagfile(parent.flagfile_fn)
def setup_thresholds(parent, types):
"""Sets necessary thresholds apps on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
type (Union[ColorVariance, ForegroundMask]): Instance where thresholds
can be run.
"""
tag = parent.tag
dlg = parent.dlg
for attr in threshold_sliders:
type, printed, num, max, default = threshold_sliders[attr]
if type in types:
name = getattr(dlg, f"label_{tag}_threshs_{num}_name_{type}", None)
hs = getattr(dlg, f"hs_{tag}_threshs_{num}_{type}", None)
label = getattr(dlg, f"label_{tag}_threshs_{num}_{type}", None)
slider = SliderWidget(type, attr, name, printed, hs, label, max, default)
setattr(parent, f"slider_{attr}", slider)
for type in types:
w_image = getattr(dlg, f"w_{tag}_threshs_image_{type}", None)
w_viewer = getattr(dlg, f"w_{tag}_image_viewer_{type}", None)
w_image.set_image_viewer(w_viewer)
def setup_thresholds_color_variance(parent):
"""Sets color variance thresholds apps on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
for slider in [parent.slider_noise, parent.slider_detail]:
slider.setup(callback=parent.on_changed_slider)
def setup_thresholds_fg_masks(parent):
"""Sets up the default thresholds on foreground masks on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
for slider in [parent.slider_blur, parent.slider_closing, parent.slider_thresh]:
slider.setup(callback=parent.on_changed_slider)
def update_data_from_flags(
parent,
flags,
dropdowns=None,
values=None,
checkboxes=None,
labels=None,
prefix=None,
):
"""Updates UI elements from the flags on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
flags (dict[str, _]): Flags and their corresponding values.
dropdowns (list[QtWidgets.QComboBox], optional): Dropdowns in the tab.
values (dict[QtWidgets.QLineEdit, _], optional): Map from UI elements to values.
checkboxes (list[QtWidgets.QCheckBox], optional): Checkboxes in the tab.
labels (list[QtWidgets.QLabel], optional): Labels in the tab.
prefix (str, optional): Prefix to append to values in the population of tab values.
"""
if not dropdowns:
dropdowns = []
if not values:
values = []
if not checkboxes:
checkboxes = []
if not labels:
labels = []
flagfile = parent.flagfile_basename
if not prefix:
prefix = f"{parent.parent.path_project}/"
for key, dd in dropdowns:
error = dep_util.update_qt_dropdown_from_flags(flags, key, prefix, dd)
if error:
parent.log_reader.log_warning(f"{flagfile}: {error}")
for key, val in values:
dep_util.update_qt_lineedit_from_flags(flags, key, prefix, val)
for key, cb in checkboxes:
error = dep_util.update_qt_checkbox_from_flags(flags, key, prefix, cb)
if error:
parent.log_reader.log_warning(f"{flagfile}: {error}")
for key, label in labels:
dep_util.update_qt_label_from_flags(flags, key, prefix, label)
def get_notation(parent, attr):
"""Gets standard format for attribute on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
attr (str): Name of the attribute.
Returns:
str: Format string corresponding to the display notation.
"""
if attr in ["noise", "detail", "thresh"]:
notation = "{:.3e}"
elif attr in ["blur", "closing"]:
notation = "{:d}"
else:
parent.log_reader.log_error(f"Invalid slider attr: {attr}")
return notation
def on_changed_slider(parent, slider, value):
"""Callback event handler for changes to a slider UI element on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
slider (QtWidgets.QSlider): Slider UI element.
value (int/float): Value of the slider element.
"""
type = slider.type
attr = slider.attr
notation = get_notation(parent, attr)
if notation == "{:d}":
value = int(value)
slider.set_label(value, notation)
tag = parent.tag
w_image = getattr(parent.dlg, f"w_{tag}_threshs_image_{type}", None)
if w_image.update_thresholds(**{attr: value}):
# Update thresholds in flagfile
parent.update_thresholds(type)
def initialize_farm_groupbox(parent):
"""Sets up the farm render box for the project path, i.e. AWS is displayed if
rendering on an S3 project path and LAN if on a SMB drive.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
tag = parent.tag
dlg = parent.dlg
gb_farm = getattr(dlg, f"gb_{tag}_farm", None)
grid_s3 = getattr(dlg, f"w_{tag}_farm_s3", None)
grid_lan = getattr(dlg, f"w_{tag}_farm_lan", None)
parent.is_aws = parent.parent.is_aws
parent.is_lan = parent.parent.is_lan
if not parent.is_aws and not parent.is_lan:
gb_farm.hide()
elif parent.is_aws:
grid_lan.hide()
elif parent.is_lan:
grid_s3.hide()
parent.ec2_instance_types_cpu = []
parent.ec2_instance_types_gpu = []
if parent.is_aws:
# Get list of EC2 instances
client = parent.parent.aws_util.session.client("ec2")
ts = client._service_model.shape_for("InstanceType").enum
ts = [t for t in ts if not t.startswith(config.EC2_UNSUPPORTED_TYPES)]
parent.ec2_instance_types_cpu = [t for t in ts if t.startswith("c")]
parent.ec2_instance_types_gpu = [t for t in ts if t.startswith(("p", "g"))]
# Check if flagfile has farm attributes
flagfile_fn = os.path.join(parent.path_flags, parent.flagfile_basename)
flags = get_flags_from_flagfile(flagfile_fn)
parent.is_farm = False
for farm_attr in ["master", "workers", "cloud"]:
if flags[farm_attr] != "":
parent.is_farm = True
break
call_force_refreshing(parent, gb_farm.setChecked, parent.is_farm)
def show_resources(parent):
"""Displays resources used in the container.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
Returns:
str: Resources (memory and CPU) being used.
"""
return run_command("top -b -n 1")
def show_aws_resources(parent):
"""Displays resources used across the AWS cluster.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
Returns:
src: Resources (memory and CPU) being used in the farm.
"""
return "\n".join(parent.parent.aws_util.ec2_get_running_instances())
def get_aws_workers():
"""Get names of the instances in the AWS farm.
Returns:
list[str]: Instances IDs of EC2 instances in the farm.
"""
with open(config.DOCKER_AWS_WORKERS) as f:
lines = f.readlines()
return lines
def set_aws_workers(workers):
"""Sets names of the instances in the AWS farm.
Args:
workers (list[str]): Instance IDs of EC2 instances in the farm.
"""
with open(config.DOCKER_AWS_WORKERS, "w") as f:
f.writelines([worker.id for worker in workers])
def popup_ec2_dashboard_url(parent):
"""Displays a link to the EC2 dashboard in a popup on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
region = parent.parent.aws_util.region_name
prefix = f"{region}." if region else ""
url = f"https://{prefix}console.aws.amazon.com/ec2#Instances"
dep_util.popup_message(parent.parent, url, "EC2 Dashboard")
def popup_logs_locations(parent):
"""Displays the path to local logs in a popup on the specified tab.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
"""
logs = [parent.log_reader.log_file]
logs_workers = glob.iglob(f"{parent.path_logs}/Worker-*", recursive=False)
for log in logs_workers:
ts_log = datetime.datetime.fromtimestamp(os.path.getmtime(log))
if ts_log > parent.parent.ts_start:
logs.append(log)
project = parent.parent.path_project
logs = [dep_util.remove_prefix(l, f"{project}/") for l in logs]
dep_util.popup_message(parent.parent, "\n".join(logs), "Logs")
def run_process_aws(parent, gb, p_id=None):
"""Runs the process to create a cluster on AWS and perform the render job.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
"""
flags = {}
flags["key_dir"] = os.path.dirname(parent.path_aws_key_fn)
flags["key_name"] = os.path.splitext(os.path.basename(parent.path_aws_key_fn))[0]
flags["csv_path"] = parent.path_aws_credentials
flags["ec2_file"] = parent.path_aws_ip_file
spin_num_workers = getattr(parent.dlg, f"spin_{parent.tag}_farm_num_workers", None)
flags["cluster_size"] = int(spin_num_workers.value())
flags["region"] = parent.parent.aws_util.region_name
dd_ec2 = getattr(parent.dlg, f"dd_{parent.tag}_farm_ec2", None)
flags["instance_type"] = dd_ec2.currentText()
flags["tag"] = parent.tag
# Overwrite flag file
app_name = parent.app_aws_create
flagfile_fn = os.path.join(parent.path_flags, parent.app_name_to_flagfile[app_name])
dep_util.write_flagfile(flagfile_fn, flags)
if not p_id:
p_id = "run_aws_create"
run_process(parent, gb, app_name, flagfile_fn, p_id)
def on_download_meshes(parent, gb):
"""Downloads meshes from S3. This is a no-op if not an S3 project.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
"""
if not parent.parent.is_aws:
return
subdir = image_type_paths["video_bin"]
flags = {}
flags["csv_path"] = parent.path_aws_credentials
flags["local_dir"] = os.path.join(config.DOCKER_INPUT_ROOT, subdir)
flags["s3_dir"] = os.path.join(parent.parent.ui_flags.project_root, subdir)
flags["verbose"] = parent.parent.ui_flags.verbose
flags["watch"] = True # NOTE: watchdog sometimes gets stale file handles in Windows
# Overwrite flag file
app_name = parent.app_aws_download_meshes
flagfile_fn = os.path.join(parent.path_flags, parent.app_name_to_flagfile[app_name])
dep_util.write_flagfile(flagfile_fn, flags)
p_id = "download_meshes"
run_process(parent, gb, app_name, flagfile_fn, p_id)
def on_terminate_cluster(parent, gb):
"""Terminates a running AWS cluster. This is a no-op if no cluster is up.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
gb (QtWidgets.QGroupBox): Group box for the tab.
"""
flags = {}
flags["key_dir"] = os.path.dirname(parent.path_aws_key_fn)
flags["key_name"] = os.path.splitext(os.path.basename(parent.path_aws_key_fn))[0]
flags["csv_path"] = parent.path_aws_credentials
flags["ec2_file"] = parent.path_aws_ip_file
flags["region"] = parent.parent.aws_util.region_name
# Overwrite flag file
flagfile_fn = os.path.join(
parent.path_flags, parent.app_name_to_flagfile[parent.app_aws_clean]
)
dep_util.write_flagfile(flagfile_fn, flags)
app_name = parent.app_aws_clean
p_id = "terminate_cluster"
run_process(parent, gb, app_name, flagfile_fn, p_id)
def get_workers(parent):
"""Finds workers in a LAN farm.
Args:
parent (App(QDialog)): Object corresponding to the parent UI element.
Returns:
list[str]: IPs of workers in the local farm.
"""
if parent.parent.ui_flags.master == config.LOCALHOST:
return []
else:
return parent.lan.scan()
def call_force_refreshing(parent, fun, *args):
already_refreshing = parent.is_refreshing_data
if not already_refreshing:
parent.is_refreshing_data = True
fun(*args)
if not already_refreshing:
parent.is_refreshing_data = False