Files
test/gripper_control_02/visualizer.py
T
2026-06-29 17:14:42 +08:00

794 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import multiprocessing as mp
import sys
import threading
import time
import cv2
import numpy as np
def _fix_qt_plugin_path():
try:
from PyQt5.QtCore import QLibraryInfo
import os
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location(
QLibraryInfo.PluginsPath
)
except Exception:
pass
_fix_qt_plugin_path()
_QT_IMPORT_ERROR = None
try:
from PyQt5.QtCore import QEasingCurve, QPropertyAnimation, Qt, QTimer
from PyQt5.QtGui import QFont, QImage, QPixmap
from PyQt5.QtWidgets import (
QApplication,
QFrame,
QGraphicsOpacityEffect,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QSizePolicy,
QSplitter,
QVBoxLayout,
QWidget,
)
except Exception as exc:
_QT_IMPORT_ERROR = exc
Qt = QTimer = QPropertyAnimation = QEasingCurve = QFont = QImage = QPixmap = QApplication = None
QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = QSplitter = None
QVBoxLayout = QWidget = None
QFrame = QGraphicsOpacityEffect = QMainWindow = object
try:
import pyqtgraph as pg
except Exception as exc:
pg = None
_PYQTGRAPH_IMPORT_ERROR = exc
else:
_PYQTGRAPH_IMPORT_ERROR = None
from .main import build_controller
PLOT_POINTS = 180
PLOT_FORCE_Y_MIN_N = 0.0
PLOT_FORCE_Y_MAX_N = 3.0
PLOT_SHEAR_Y_MIN_N = -3.0
PLOT_SHEAR_Y_MAX_N = 3.0
FORCE_DISPLAY_ZERO_THRESHOLD_N = 0.05
CURVE_COLOR = "#00E5FF"
def cv2_to_pixmap(img_bgr):
if img_bgr is None:
return QPixmap()
if len(img_bgr.shape) == 2:
img_bgr = cv2.cvtColor(img_bgr, cv2.COLOR_GRAY2BGR)
rgb = cv2.cvtColor(np.ascontiguousarray(img_bgr), cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
return QPixmap.fromImage(qimg)
def force_display_value(value):
value = float(value)
if abs(value) < FORCE_DISPLAY_ZERO_THRESHOLD_N:
return 0.0
return value
def fmt(value, unit="", width=0, precision=3):
if value is None:
text = "--"
else:
value = force_display_value(value)
text = "0" if value == 0.0 else f"{value:.{precision}f}"
if width:
text = text.rjust(width)
return f"{text}{unit}"
STATE_TEXT = {
"open_wait": "等待触发",
"closing": "闭合中",
"gripping": "夹持加力",
"hold_check": "夹持检测",
"open_recover": "张开恢复",
"manual_close": "手动闭合",
"manual_hold": "手动保持",
"error": "错误",
}
ACTION_TEXT = {
"wait": "等待",
"waiting-for-sensor-samples": "等待传感器数据",
"open-wait-stabilizing": "等待稳定基准",
"open-wait-armed": "已就绪",
"side-trigger-close": "侧边触发闭合",
"closing-continue": "继续闭合",
"closing-to-grip": "闭合寻找物体",
"object-detected-low-force": "检测到物体",
"close-timeout-open": "闭合超时张开",
"grip-ramp-up": "夹持加力",
"grip-hold": "夹持保持",
"hold-reference-armed": "记录夹持基准",
"adaptive-grip-ramp": "自适应加力",
"adaptive-grip-wait": "等待自适应加力",
"release-arm-delay": "释放检测延时",
"release-reference-armed": "释放检测就绪",
"hold-check": "保持检测",
"force-change-release-open": "力变化张开",
"open-recover-position": "张开位置恢复",
"open-recover-position-stabilizing": "张开位置稳定中",
"open-ready-position": "张开到位",
"open-recover-quiet": "力稳定恢复",
"open-recover-stabilizing": "恢复稳定中",
"open-recover-wait-force-clear": "等待力恢复",
"open-ready": "恢复完成",
"manual-open": "手动张开",
"manual-close": "手动闭合",
"manual-hold": "手动保持",
"manual-release": "手动释放",
}
def label_with_cn(value, mapping):
value = str(value or "--")
cn = mapping.get(value)
if cn is None:
for prefix, text in mapping.items():
if value.startswith(f"{prefix} "):
cn = text
break
if cn is None:
return value
return f"{value}{cn}"
STYLE = """
QMainWindow {
background-color: #000000;
}
QWidget {
background-color: #000000;
color: #9a9a9a;
font-family: "Microsoft YaHei", "SimHei";
}
QLabel#Title {
color: #d7d7d7;
font-size: 18px;
font-weight: 600;
}
QLabel#Subtle {
color: #777777;
font-size: 12px;
}
QLabel#MetricLabel {
color: #888888;
font-size: 12px;
}
QLabel#MetricValue {
color: #f2f2f2;
font-family: "Cascadia Code", "Consolas";
font-size: 22px;
font-weight: 600;
}
QLabel#SmallValue {
color: #d8d8d8;
font-family: "Cascadia Code", "Consolas";
font-size: 15px;
}
QLabel#StatusChip {
background-color: #101010;
border: 1px solid #303030;
border-radius: 4px;
color: #00e5ff;
font-family: "Cascadia Code", "Consolas";
font-size: 18px;
font-weight: 600;
padding: 8px 12px;
}
QFrame#StatusPanel {
background-color: #101010;
border: 1px solid #303030;
border-radius: 4px;
}
QLabel#StatusMain {
color: #00e5ff;
font-family: "Cascadia Code", "Consolas";
font-size: 17px;
font-weight: 600;
}
QLabel#StatusSub {
color: #9a9a9a;
font-family: "Cascadia Code", "Consolas";
font-size: 12px;
}
QLabel#ActionChip {
background-color: #0b0b0b;
border: 1px solid #262626;
border-radius: 4px;
color: #c8c8c8;
font-family: "Cascadia Code", "Consolas";
font-size: 13px;
padding: 7px 10px;
}
QLabel#ImagePanel {
background-color: #0b0b0b;
border: 1px solid #262626;
border-radius: 4px;
}
QFrame#Panel {
background-color: #050505;
border: 1px solid #222222;
border-radius: 4px;
}
QPushButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
border-radius: 4px;
padding: 7px 16px;
font-size: 13px;
}
QPushButton:hover {
background-color: #242424;
color: #ffffff;
border: 1px solid #5a5a5a;
}
QPushButton:pressed {
background-color: #0f0f0f;
border: 1px solid #8a8a8a;
padding-top: 8px;
padding-bottom: 6px;
}
QPushButton:disabled {
background-color: #101010;
color: #606060;
border: 1px solid #242424;
}
QPushButton#StartButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
}
QPushButton#StopButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
}
QPushButton#RestartButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
}
QPushButton#ManualButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
}
QPushButton#HoldButton {
background-color: #171717;
color: #d8d8d8;
border: 1px solid #363636;
}
QSplitter::handle {
background-color: #151515;
}
QSplitter::handle:hover {
background-color: #2a2a2a;
}
QSplitter::handle:horizontal {
width: 8px;
}
QSplitter::handle:vertical {
height: 8px;
}
"""
if QPushButton is not None:
class AnimatedButton(QPushButton):
def __init__(self, text=""):
super().__init__(text)
self._opacity = QGraphicsOpacityEffect(self)
self._opacity.setOpacity(1.0)
self.setGraphicsEffect(self._opacity)
self._animation = QPropertyAnimation(self._opacity, b"opacity", self)
self._animation.setEasingCurve(QEasingCurve.OutCubic)
def _animate_opacity(self, target, duration):
self._animation.stop()
self._animation.setDuration(duration)
self._animation.setStartValue(self._opacity.opacity())
self._animation.setEndValue(target)
self._animation.start()
def mousePressEvent(self, event):
if self.isEnabled():
self._animate_opacity(0.72, 70)
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
if self.isEnabled():
self._animate_opacity(1.0, 140)
super().mouseReleaseEvent(event)
else:
AnimatedButton = None
class MetricBox(QFrame):
def __init__(self, title, value="--"):
super().__init__()
self.setObjectName("Panel")
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(4)
self.title_label = QLabel(title)
self.title_label.setObjectName("MetricLabel")
self.value_label = QLabel(value)
self.value_label.setObjectName("MetricValue")
layout.addWidget(self.title_label)
layout.addWidget(self.value_label)
def set_value(self, value, sub=None):
self.value_label.setText(value)
class ImageBox(QFrame):
def __init__(self, title):
super().__init__()
self.setObjectName("Panel")
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(6)
self.title_label = QLabel(title)
self.title_label.setObjectName("MetricLabel")
self.image_label = QLabel()
self.image_label.setObjectName("ImagePanel")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setMinimumSize(180, 180)
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.image_label.setScaledContents(True)
layout.addWidget(self.title_label)
layout.addWidget(self.image_label, 1)
def set_image(self, img):
self.image_label.setPixmap(cv2_to_pixmap(img))
class GripperDemo02Viewer(QMainWindow):
def __init__(self, argv=None):
super().__init__()
if _QT_IMPORT_ERROR is not None:
raise RuntimeError(f"PyQt5 import failed: {_QT_IMPORT_ERROR}")
if pg is None:
raise RuntimeError(f"pyqtgraph import failed: {_PYQTGRAPH_IMPORT_ERROR}")
self.setWindowTitle("Gripper Demo 02")
self.setStyleSheet(STYLE)
self.controller = None
self.control_thread = None
self.latest_status = None
self.status_lock = threading.Lock()
self.controller_finished = False
self.restart_pending = False
self.button_state = "idle"
self.start_time = time.time()
self.force_history = []
self.argv = argv
self._build_ui()
self._configure_plot()
self.timer = QTimer(self)
self.timer.timeout.connect(self._refresh)
self.timer.start(33)
def _build_ui(self):
root = QWidget()
self.setCentralWidget(root)
outer = QVBoxLayout(root)
outer.setContentsMargins(8, 8, 8, 8)
outer.setSpacing(8)
top = QHBoxLayout()
top.setSpacing(8)
title = QLabel("Orisys 夹爪")
title.setObjectName("Title")
self.status_panel = QFrame()
self.status_panel.setObjectName("StatusPanel")
status_layout = QVBoxLayout(self.status_panel)
status_layout.setContentsMargins(10, 6, 10, 6)
status_layout.setSpacing(1)
self.status_chip = QLabel("state=--")
self.status_chip.setObjectName("StatusMain")
self.action_label = QLabel("action=--")
self.action_label.setObjectName("StatusSub")
status_layout.addWidget(self.status_chip)
status_layout.addWidget(self.action_label)
self.start_button = AnimatedButton("启动")
self.start_button.setObjectName("StartButton")
self.start_button.clicked.connect(self.start_control)
self.stop_button = AnimatedButton("停止")
self.stop_button.setObjectName("StopButton")
self.stop_button.clicked.connect(self.stop_control)
self.stop_button.setEnabled(False)
self.restart_button = AnimatedButton("重启")
self.restart_button.setObjectName("RestartButton")
self.restart_button.clicked.connect(self.restart_control)
self.manual_open_button = AnimatedButton("夹爪张开")
self.manual_open_button.setObjectName("ManualButton")
self.manual_open_button.clicked.connect(lambda: self.send_manual_command("open"))
self.manual_close_button = AnimatedButton("夹爪闭合")
self.manual_close_button.setObjectName("ManualButton")
self.manual_close_button.clicked.connect(lambda: self.send_manual_command("close"))
self.manual_hold_button = AnimatedButton("保持")
self.manual_hold_button.setObjectName("HoldButton")
self.manual_hold_button.clicked.connect(self.toggle_manual_hold)
self._set_button_state("idle")
top.addWidget(title)
top.addWidget(self.status_panel)
top.addStretch(1)
top.addWidget(self.start_button)
top.addWidget(self.stop_button)
top.addWidget(self.restart_button)
top.addWidget(self.manual_open_button)
top.addWidget(self.manual_close_button)
top.addWidget(self.manual_hold_button)
outer.addLayout(top)
main_splitter = QSplitter(Qt.Horizontal)
main_splitter.setChildrenCollapsible(False)
outer.addWidget(main_splitter, 1)
flow_splitter = QSplitter(Qt.Vertical)
flow_splitter.setChildrenCollapsible(False)
self.left_flow = ImageBox("左侧光流")
self.right_flow = ImageBox("右侧光流")
flow_splitter.addWidget(self.left_flow)
flow_splitter.addWidget(self.right_flow)
flow_splitter.setStretchFactor(0, 1)
flow_splitter.setStretchFactor(1, 1)
flow_splitter.setSizes([360, 360])
main_splitter.addWidget(flow_splitter)
side_widget = QWidget()
side = QVBoxLayout(side_widget)
side.setContentsMargins(0, 0, 0, 0)
side.setSpacing(8)
main_splitter.addWidget(side_widget)
main_splitter.setStretchFactor(0, 2)
main_splitter.setStretchFactor(1, 4)
main_splitter.setSizes([520, 980])
metrics_grid = QGridLayout()
metrics_grid.setSpacing(8)
side.addLayout(metrics_grid)
self.left_force = MetricBox("左法向力", "--")
self.right_force = MetricBox("右法向力", "--")
self.left_shear_box = MetricBox("左切向力", "--")
self.right_shear_box = MetricBox("右切向力", "--")
self.force_pct_box = MetricBox("夹爪当前力比例(%", "--")
self.target_force_box = MetricBox("夹爪目标力比例(%", "--")
self.speed_box = MetricBox("速度", "--")
self.trigger_box = MetricBox("是否触发/系统是否允许触发", "--")
self.contact_box = MetricBox("接触", "--")
self.release_box = MetricBox("释放检测", "--")
boxes = [
self.left_force,
self.right_force,
self.left_shear_box,
self.right_shear_box,
self.force_pct_box,
self.target_force_box,
self.speed_box,
self.trigger_box,
self.contact_box,
self.release_box,
]
for idx, box in enumerate(boxes):
metrics_grid.addWidget(box, idx // 2, idx % 2)
pg.setConfigOption("background", "#000000")
pg.setConfigOption("foreground", "#777777")
pg.setConfigOptions(antialias=True)
self.plot_widget = pg.GraphicsLayoutWidget()
self.plot_widget.setMinimumHeight(240)
side.addWidget(self.plot_widget, 1)
def _configure_plot(self):
self.plots = []
self.curves = {}
plot_specs = [
("left_normal", "左法向力", "#00E5FF", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N),
("right_normal", "右法向力", "#ffb000", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N),
("left_shear_x", "左切向 X", "#61d394", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
("left_shear_y", "左切向 Y", "#f07178", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
("right_shear_x", "右切向 X", "#8ab4f8", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
("right_shear_y", "右切向 Y", "#d19a66", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
]
for idx, (key, title, color, y_min, y_max) in enumerate(plot_specs):
row = idx // 2
col = idx % 2
plot = self.plot_widget.addPlot(row=row, col=col)
plot.setTitle(title, color="#999999", size="11pt")
plot.titleLabel.setFont(QFont("SimHei", 11))
plot.showAxis("top", False)
plot.showAxis("right", False)
plot.showGrid(x=False, y=True, alpha=0.12)
plot.setLabel("bottom", "时间", units="s")
plot.setLabel("left", "力", units="N")
plot.getAxis("bottom").setTickSpacing(major=1.0, minor=1.0)
plot.setYRange(y_min, y_max, padding=0)
plot.setLimits(yMin=y_min, yMax=y_max)
plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False)
self.plots.append(plot)
self.curves[key] = plot.plot(pen=pg.mkPen(color, width=2))
def _set_button_state(self, state):
labels = {
"idle": ("启动", "停止", "重启"),
"starting": ("启动中", "停止", "重启"),
"running": ("启动", "停止", "重启"),
"stopping": ("启动", "停止中", "重启"),
"restarting": ("启动", "停止", "重启中"),
}
enabled = {
"idle": (True, False, True),
"starting": (False, True, False),
"running": (False, True, True),
"stopping": (False, False, False),
"restarting": (False, False, False),
}
self.button_state = state
start_text, stop_text, restart_text = labels[state]
start_enabled, stop_enabled, restart_enabled = enabled[state]
self.start_button.setText(start_text)
self.stop_button.setText(stop_text)
self.restart_button.setText(restart_text)
self.start_button.setEnabled(start_enabled)
self.stop_button.setEnabled(stop_enabled)
self.restart_button.setEnabled(restart_enabled)
manual_enabled = state == "running"
self.manual_open_button.setEnabled(manual_enabled)
self.manual_close_button.setEnabled(manual_enabled)
self.manual_hold_button.setEnabled(manual_enabled)
def send_manual_command(self, command):
if self.controller is None or self.control_thread is None or not self.control_thread.is_alive():
return
try:
self.controller.request_manual_command(command)
except Exception as exc:
print(f"manual command failed: {exc}")
def toggle_manual_hold(self):
command = "release" if self.manual_hold_button.text() == "释放" else "hold"
self.send_manual_command(command)
def start_control(self, from_restart=False):
if self.control_thread is not None and self.control_thread.is_alive():
return
self.force_history.clear()
self.start_time = time.time()
self._set_button_state("restarting" if from_restart else "starting")
self.controller_finished = False
self.controller = build_controller(
self.argv,
status_callback=self._on_status,
include_visuals=True,
visual_size=320,
)
self.control_thread = threading.Thread(
target=self._run_controller,
daemon=True,
)
self.control_thread.start()
def stop_control(self):
if self.controller is not None:
self.controller.stop()
self._set_button_state("stopping")
def restart_control(self):
if self.control_thread is not None and self.control_thread.is_alive():
self.restart_pending = True
self._set_button_state("restarting")
if self.controller is not None:
self.controller.stop()
return
self.restart_pending = False
self.start_control(from_restart=True)
def _run_controller(self):
try:
self.controller.run()
except Exception as exc:
self._on_status({"state": "error", "action": str(exc), "timestamp": time.time()})
finally:
self.controller_finished = True
def _on_status(self, status):
with self.status_lock:
self.latest_status = status
def _refresh(self):
with self.status_lock:
status = self.latest_status
if not status:
self._update_thread_buttons()
return
self._update_thread_buttons()
if (
self.control_thread is not None
and self.control_thread.is_alive()
and not self.restart_pending
and self.button_state in ("starting", "restarting")
):
self._set_button_state("running")
state = status.get("state", "--")
action = status.get("action", "--")
unit = status.get("normal_unit", "N")
shear_unit = status.get("shear_unit", "N")
self.status_chip.setText(f"state={label_with_cn(state, STATE_TEXT)}")
self.action_label.setText(f"action={label_with_cn(action, ACTION_TEXT)}")
self.manual_hold_button.setText(
"释放" if bool(status.get("manual_hold_active")) else "保持"
)
self.left_force.set_value(
fmt(status.get("left_normal"), unit),
)
self.right_force.set_value(
fmt(status.get("right_normal"), unit),
)
self.left_shear_box.set_value(
fmt(status.get("left_shear"), shear_unit),
)
self.right_shear_box.set_value(
fmt(status.get("right_shear"), shear_unit),
)
self.force_pct_box.set_value(
str(status.get("force_pct", "--")),
)
self.target_force_box.set_value(
str(status.get("adaptive_target_force", "--")),
)
self.speed_box.set_value(
str(status.get("speed_pct", "--")),
)
self.trigger_box.set_value(
f"{int(bool(status.get('trigger')))} / {int(bool(status.get('trigger_armed')))}",
)
self.contact_box.set_value(
str(int(bool(status.get("grip_contact")))),
)
self.release_box.set_value(
str(int(bool(status.get("release_armed")))),
)
left_sample = status.get("left_sample") or {}
right_sample = status.get("right_sample") or {}
self.left_flow.set_image(left_sample.get("flow_view"))
self.right_flow.set_image(right_sample.get("flow_view"))
elapsed = time.time() - self.start_time
self.force_history.append(
(
elapsed,
force_display_value(status.get("left_normal", 0.0) or 0.0),
force_display_value(status.get("right_normal", 0.0) or 0.0),
force_display_value(status.get("left_shear_x", 0.0) or 0.0),
force_display_value(status.get("left_shear_y", 0.0) or 0.0),
force_display_value(status.get("right_shear_x", 0.0) or 0.0),
force_display_value(status.get("right_shear_y", 0.0) or 0.0),
)
)
if len(self.force_history) > PLOT_POINTS:
self.force_history = self.force_history[-PLOT_POINTS:]
self._update_plot()
def _update_plot(self):
if not self.force_history:
return
data = np.asarray(self.force_history, dtype=float)
x = data[:, 0]
keys = [
"left_normal",
"right_normal",
"left_shear_x",
"left_shear_y",
"right_shear_x",
"right_shear_y",
]
for idx, key in enumerate(keys, start=1):
self.curves[key].setData(x, data[:, idx])
if len(x) >= 2:
x_max = max(6.0, float(np.ceil(x[-1])))
x_min = max(0.0, x_max - 6.0)
for plot in self.plots:
plot.setXRange(x_min, x_max, padding=0)
def _update_thread_buttons(self):
if not self.controller_finished:
return
self.controller_finished = False
if self.control_thread is not None:
self.control_thread.join(timeout=0.1)
self.control_thread = None
if self.restart_pending:
self.restart_pending = False
self.start_control(from_restart=True)
return
self._set_button_state("idle")
def keyPressEvent(self, event):
if event.key() == Qt.Key_Q:
self.close()
return
if event.key() == Qt.Key_Escape:
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
return
super().keyPressEvent(event)
def closeEvent(self, event):
self.timer.stop()
self.restart_pending = False
self.stop_control()
if self.control_thread is not None:
self.control_thread.join(timeout=3.0)
event.accept()
def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument("--windowed", action="store_true")
ui_args, controller_argv = parser.parse_known_args(argv)
if _QT_IMPORT_ERROR is not None:
print(
"PyQt5 is required for the visualizer. "
"Install it in the active environment, for example: pip install PyQt5 pyqtgraph"
)
print(f"Import error: {_QT_IMPORT_ERROR}")
return 1
if pg is None:
print(
"pyqtgraph is required for the visualizer. "
"Install it in the active environment, for example: pip install pyqtgraph"
)
print(f"Import error: {_PYQTGRAPH_IMPORT_ERROR}")
return 1
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv[:1])
window = GripperDemo02Viewer(controller_argv)
if ui_args.windowed:
window.resize(1500, 900)
window.show()
else:
window.showFullScreen()
window.start_control()
return app.exec_()
if __name__ == "__main__":
mp.freeze_support()
raise SystemExit(main(sys.argv[1:]))