dev: добавлена функция открытия, обработки и отображения трейсов клиента (вообще любых трейсов)

This commit is contained in:
Andrew 2025-01-30 16:08:42 +03:00
parent 3f7959e47c
commit 0afbfb9732
32 changed files with 265 additions and 50 deletions

View File

@ -11,6 +11,35 @@ from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
from OptAlgorithm import OptAlgorithm from OptAlgorithm import OptAlgorithm
from utils.qt_settings import dark_style from utils.qt_settings import dark_style
@dataclass
class KukaTXT:
time: float = 0
endtime: float = 0
#module: str
func: str = ""
type_: str = ""
signal: str = ""
#line: int = 0
#point_name: str = ""
#point_coord: dict = field(default_factory=lambda: {})
#blending: str = ""
#blending_param: float = 0
#velocities: dict = field(default_factory=lambda: {})
#accelerarions: dict = field(default_factory=lambda: {})
#base: dict = field(default_factory=lambda: {})
#tool: dict = field(default_factory=lambda: {})
#ipo_mode: str = ""
#motion_mode: str = ""
#load: dict = field(default_factory=lambda: {})
#load_a3: dict = field(default_factory=lambda: {})
@dataclass
class KukaDataHead:
rob_ID: int
filename: str
channels: dict
@dataclass @dataclass
class PlotItems: class PlotItems:
@ -53,7 +82,8 @@ class BaseMediator:
passport_former: BasePointPassportFormer, passport_former: BasePointPassportFormer,
plot: BasePlotWidget, plot: BasePlotWidget,
controller: BaseController, controller: BaseController,
file_manager: BaseFileManager): file_manager: BaseFileManager,
trace_processor: BaseRawTraceProcessor):
self._converter = converter self._converter = converter
self._converter.mediator = self self._converter.mediator = self
self._passport_former = passport_former self._passport_former = passport_former
@ -64,12 +94,16 @@ class BaseMediator:
self._controller.mediator = self self._controller.mediator = self
self._file_manager = file_manager self._file_manager = file_manager
self._file_manager.mediator = self self._file_manager.mediator = self
self._trace_processor = trace_processor
self._trace_processor.mediator = self
def notify(self, def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget], source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseRawTraceProcessor],
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]): data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget], pd.DataFrame]):
... ...
def prerender(self, data:list[str]) -> None:
...
class BaseDirectoryMonitor: class BaseDirectoryMonitor:
@ -281,6 +315,8 @@ class BasePlotWidget:
def build(self, data: list[pd.DataFrame]) -> list[QWidget]: def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
... ...
def build_raw_trace(self, data:pd.DataFrame) -> None:
...
class BaseController(QObject): class BaseController(QObject):
@ -303,6 +339,9 @@ class BaseController(QObject):
def open_file(self, filepath: str) -> None: def open_file(self, filepath: str) -> None:
... ...
def open_dir(self, dirpath:str) -> None:
...
def update_plots(self) -> None: def update_plots(self) -> None:
... ...
@ -364,6 +403,9 @@ class BaseFileManager:
def open_custom_file(self, path:str) -> None: def open_custom_file(self, path:str) -> None:
... ...
def open_raw_traces_dir(self, path:str) -> None:
...
def set_mode(self, num:int) -> None: def set_mode(self, num:int) -> None:
... ...
@ -501,3 +543,46 @@ class BasePointPassportFormer:
@mediator.setter @mediator.setter
def mediator(self, mediator: BaseMediator) -> None: def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator self._mediator = mediator
class BaseRawTraceProcessor:
def __init__(self,
dataparser:BaseKukaDataParser,
textparser:BaseKukaTextParser,
mediator:Optional[BaseMediator] = None):
self._mediator = mediator
self._dataparser = dataparser
self._textparser = textparser
self._trace_df = None
self._text_data = None
def prerender(self, data:list[str]) -> None:
...
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
class BaseKukaDataParser:
def __init__(self):
self._ch_name = None
def parse(self, head_path: str) -> pd.DataFrame:
...
class BaseKukaTextParser:
def __init__(self):
self._in_msg = None
self._datapacks = None
def parse(self, path:str ) -> list[KukaTXT]:
...

View File

@ -32,6 +32,9 @@ class Controller(BaseController):
def open_file(self, filepath: str) -> None: def open_file(self, filepath: str) -> None:
self._file_manager.open_custom_file(filepath) self._file_manager.open_custom_file(filepath)
def open_dir(self, dirpath:str) -> None:
self._file_manager.open_raw_traces_dir(dirpath)
def save_file(self, data:list[str, QTabWidget]) -> None: def save_file(self, data:list[str, QTabWidget]) -> None:
filepath, tab = data filepath, tab = data
pixmap = QPixmap(tab.size()) pixmap = QPixmap(tab.size())

View File

@ -21,13 +21,16 @@ class FileManager(BaseFileManager):
self._monitor.stop() self._monitor.stop()
self._paths_library.clear() self._paths_library.clear()
self._paths_library.add('') self._paths_library.add('')
print(self._paths_library)
self._mediator.notify(self, list(self._paths_library)) self._mediator.notify(self, list(self._paths_library))
case 2: # Режим онлайн-мониторинга папки case 2: # Режим онлайн-мониторинга папки
self._monitor.init_state() self._monitor.init_state()
self._monitor.start() self._monitor.start()
case 3: # Режим работы с трейсами клиента
self._monitor.stop()
self._paths_library.clear()
def update_monitor_settings(self, settings:Settings) -> None: def update_monitor_settings(self, settings:Settings) -> None:
directory_path = settings.system['trace_storage_path'][0] directory_path = settings.system['trace_storage_path'][0]
update_time = settings.system['monitor_update_period'][0] update_time = settings.system['monitor_update_period'][0]
@ -44,12 +47,32 @@ class FileManager(BaseFileManager):
self._monitor._update_time = update_time self._monitor._update_time = update_time
if self._monitor.isActive: self._monitor.start() if self._monitor.isActive: self._monitor.start()
def add_new_paths(self, paths): def add_new_paths(self, paths:list) -> None:
paths_set = set(paths) paths_set = set(paths)
new = self._paths_library.difference(paths_set) new = self._paths_library.difference(paths_set)
self._paths_library.update(new) self._paths_library.update(new)
self._mediator.notify(list(new)) self._mediator.notify(list(new))
def open_raw_traces_dir(self, path:str) -> None:
dat_file, txt_file = None, None
for entry in os.listdir(path):
full_path = os.path.join(path, entry)
if os.path.isfile(full_path):
_, ext = os.path.splitext(entry)
ext = ext.lower()
if ext == '.dat' and dat_file is None:
dat_file = full_path
elif ext == '.txt' and txt_file is None:
txt_file = full_path
if dat_file and txt_file:
break
if dat_file and txt_file:
self._mediator.prerender([dat_file, txt_file])
class DirectoryMonitor(BaseDirectoryMonitor): class DirectoryMonitor(BaseDirectoryMonitor):

View File

@ -11,15 +11,16 @@ from base.base import (
BasePointPassportFormer, BasePointPassportFormer,
BaseController, BaseController,
GraphicPassport, GraphicPassport,
Settings Settings,
BaseRawTraceProcessor
) )
class Mediator(BaseMediator): class Mediator(BaseMediator):
def notify(self, def notify(self,
source: Union[BaseFileManager, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseController], source: Union[BaseFileManager, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseController, BaseRawTraceProcessor],
data: Union[list[str], list[pd.DataFrame], list[GraphicPassport], list[QWidget], Settings]): data: Union[list[str], list[pd.DataFrame], list[GraphicPassport], list[QWidget], Settings, pd.DataFrame]):
if issubclass(source.__class__, BaseFileManager): if issubclass(source.__class__, BaseFileManager):
self._controller.update_status("CSV found! Calculating...") self._controller.update_status("CSV found! Calculating...")
@ -41,5 +42,11 @@ class Mediator(BaseMediator):
self._file_manager.update_monitor_settings(data) self._file_manager.update_monitor_settings(data)
self._passport_former.update_settings(data) self._passport_former.update_settings(data)
if issubclass(source.__class__, BaseRawTraceProcessor):
self._plot.build_raw_trace(data)
def prerender(self, data:list[str]) -> None:
self._trace_processor.prerender(data)

View File

@ -4,7 +4,8 @@ from PyQt5.QtCore import Qt, pyqtSignal
from base.base import BaseMainWindow, Settings from base.base import BaseMainWindow, Settings
from gui.start_widget import (CustomMenuBar, CustomStatusBar, from gui.start_widget import (CustomMenuBar, CustomStatusBar,
StartWidget, CustomTabWidget, StartWidget, CustomTabWidget,
RaportWidget, SeekingWidget) RaportWidget, SeekingWidget,
ClientAnalyzerWidget)
from gui.settings_window import SystemSettings, OperatorSettings from gui.settings_window import SystemSettings, OperatorSettings
from gui.report_gui import ReportSettings from gui.report_gui import ReportSettings
@ -17,6 +18,7 @@ class MainWindow(BaseMainWindow):
signal_replot_all = pyqtSignal() signal_replot_all = pyqtSignal()
signal_open_file = pyqtSignal(str) signal_open_file = pyqtSignal(str)
signal_save_file = pyqtSignal(list) signal_save_file = pyqtSignal(list)
signal_open_dir = pyqtSignal(str)
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -53,6 +55,7 @@ class MainWindow(BaseMainWindow):
start_widget = StartWidget() start_widget = StartWidget()
start_widget.seeking_mode_btn.clicked.connect(self._init_seekingUI) start_widget.seeking_mode_btn.clicked.connect(self._init_seekingUI)
start_widget.raport_mode_btn.clicked.connect(self._init_raportUI) start_widget.raport_mode_btn.clicked.connect(self._init_raportUI)
start_widget.client_mode_btn.clicked.connect(self._init_client_UI)
self.setCentralWidget(start_widget.get_widget()) self.setCentralWidget(start_widget.get_widget())
def _init_settings(self) -> None: def _init_settings(self) -> None:
@ -64,6 +67,7 @@ class MainWindow(BaseMainWindow):
self.menu = CustomMenuBar(self.sysSettings, self.repSettings, self.operSettings) self.menu = CustomMenuBar(self.sysSettings, self.repSettings, self.operSettings)
self.menu.seeking_action.triggered.connect(self._init_seekingUI) self.menu.seeking_action.triggered.connect(self._init_seekingUI)
self.menu.raport_action.triggered.connect(self._init_raportUI) self.menu.raport_action.triggered.connect(self._init_raportUI)
self.menu.client_action.triggered.connect(self._init_client_UI)
self.menu.view_settings.triggered.connect(lambda: self._on_tab_changed(0)) self.menu.view_settings.triggered.connect(lambda: self._on_tab_changed(0))
self.menu.setup(self) self.menu.setup(self)
@ -106,6 +110,17 @@ class MainWindow(BaseMainWindow):
seeking_widget = SeekingWidget(self._tab_widget) seeking_widget = SeekingWidget(self._tab_widget)
self.setCentralWidget(seeking_widget.get_widget()) self.setCentralWidget(seeking_widget.get_widget())
def _init_client_UI(self) -> None:
self._clear()
self._set_mode(3)
client_widget = ClientAnalyzerWidget(self._tab_widget)
#TODO: привязать действия к кнопкам
client_widget.open_folder_btn.clicked.connect(self._open_folder)
client_widget.save_screen_btn.clicked.connect(self._save_plots)
self.setCentralWidget(client_widget.get_widget())
#client_widget.build_TCW_btn.clicked.connect()
def _set_mode(self, num:int) -> None: def _set_mode(self, num:int) -> None:
match num: match num:
case 1: case 1:
@ -114,6 +129,9 @@ class MainWindow(BaseMainWindow):
case 2: case 2:
self.status_widget.set_mode("online mode") self.status_widget.set_mode("online mode")
self.signal_mode.emit(2) self.signal_mode.emit(2)
case 3:
self.status_widget.set_mode("client processor mode")
self.signal_mode.emit(3)
def _transfer_settings(self) -> None: def _transfer_settings(self) -> None:
settings = Settings(self.operSettings.getParams(), self.sysSettings.getParams()) settings = Settings(self.operSettings.getParams(), self.sysSettings.getParams())
@ -130,6 +148,12 @@ class MainWindow(BaseMainWindow):
if CSV_path: if CSV_path:
self.signal_open_file.emit(CSV_path) self.signal_open_file.emit(CSV_path)
def _open_folder(self) -> None:
dir_path = QtWidgets.QFileDialog.getExistingDirectory(self,"Select folder with traces","",
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks)
if dir_path:
self.signal_open_dir.emit(dir_path)
def _save_plots(self) -> None: def _save_plots(self) -> None:
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)") filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
tab = self._tab_widget.currentWidget() tab = self._tab_widget.currentWidget()

View File

@ -19,6 +19,7 @@ import pyqtgraph as pg
import pandas as pd import pandas as pd
from base.base import BasePlotWidget, GraphicPassport, PlotItems, PointPassport, UsefulGraphData from base.base import BasePlotWidget, GraphicPassport, PlotItems, PointPassport, UsefulGraphData
from utils import qt_settings as qts
@dataclass @dataclass
@ -54,6 +55,31 @@ class PlotWidget(BasePlotWidget):
finally: finally:
self._mediator.notify(self, widgets_datapack) self._mediator.notify(self, widgets_datapack)
def build_raw_trace(self, data:pd.DataFrame) -> None:
"""
Создаёт один виджет с одним графиком, где представлены все данные
"""
container_widget, container_layout, pyqt_container = self._generate_widget_container()
plot = self._build_raw_plotitem(data, pyqt_container)
container_layout.addWidget(plot)
container_widget.setProperty("pyqt_container", pyqt_container)
self._mediator.notify(self, [container_widget])
def _build_raw_plotitem(self,
dataframe:pd.DataFrame,
pyqt_container:PlotItems) -> pg.GraphicsLayoutWidget:
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
channels = dataframe.columns.to_list()
for i, channel in enumerate(channels):
plot = plot_item.plot(dataframe.index.values, dataframe[channel], pen = qts.colors[i], fast = True)
legend.addItem(plot, channel)
pyqt_container.curves["real"].setdefault(channel, {})
pyqt_container.curves["real"][channel] = plot
plot_layout = pg.GraphicsLayoutWidget()
plot_layout.addItem(plot_item)
return plot_layout
def _build_performance_label(self, def _build_performance_label(self,
timings: ChannelTimings, timings: ChannelTimings,
qt_items: dict) -> QWidget: qt_items: dict) -> QWidget:
@ -132,7 +158,8 @@ class CustomPlotLayout(pg.GraphicsLayoutWidget):
def __init__(self, def __init__(self,
graphic_passport: GraphicPassport, graphic_passport: GraphicPassport,
widget_steps: int, widget_steps: int,
colors: dict, parent: PlotWidget = None) -> None: colors: dict,
parent: PlotWidget = None) -> None:
super().__init__() super().__init__()
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent) self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
self.setProperty("performance", None) self.setProperty("performance", None)
@ -155,7 +182,6 @@ class CustomPlotLayout(pg.GraphicsLayoutWidget):
if navigator is not None: if navigator is not None:
self.addItem(navigator, widget_num+1, 0) self.addItem(navigator, widget_num+1, 0)
class PlotItemGenerator: class PlotItemGenerator:
def __init__(self, def __init__(self,

View File

@ -27,14 +27,17 @@ class StartWidget(QWidget):
def _build_main_layout(self) -> None: def _build_main_layout(self) -> None:
self.resize(800,800) self.resize(800,800)
self.seeking_mode_btn = QPushButton("Real time folder scanning") self.seeking_mode_btn = QPushButton("Real time folder scanning")
self.seeking_mode_btn.setFixedWidth(300) self.seeking_mode_btn.setFixedWidth(350)
self.raport_mode_btn = QPushButton("Raport editor") self.raport_mode_btn = QPushButton("Raport editor")
self.raport_mode_btn.setFixedWidth(300) self.raport_mode_btn.setFixedWidth(350)
self.client_mode_btn = QPushButton("Client trace processor")
self.client_mode_btn.setFixedWidth(350)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.setSpacing(2) button_layout.setSpacing(2)
button_layout.addWidget(self.seeking_mode_btn) button_layout.addWidget(self.seeking_mode_btn)
button_layout.addWidget(self.raport_mode_btn) button_layout.addWidget(self.raport_mode_btn)
button_layout.addWidget(self.client_mode_btn)
button_widget = QWidget() button_widget = QWidget()
button_widget.setLayout(button_layout) button_widget.setLayout(button_layout)
@ -201,6 +204,7 @@ class CustomMenuBar(QMenuBar):
# Создаем действия для меню # Создаем действия для меню
self.seeking_action = QAction("Real time folder scanning", self) self.seeking_action = QAction("Real time folder scanning", self)
self.raport_action = QAction("Raport editor", self) self.raport_action = QAction("Raport editor", self)
self.client_action = QAction("Client trace processor", self)
system_settings = QAction("System settings", self) system_settings = QAction("System settings", self)
system_settings.setIcon(QIcon('resources/system_ico.png')) system_settings.setIcon(QIcon('resources/system_ico.png'))
@ -217,6 +221,7 @@ class CustomMenuBar(QMenuBar):
# Добавляем действия в меню "Режимы" # Добавляем действия в меню "Режимы"
modes_menu.addAction(self.seeking_action) modes_menu.addAction(self.seeking_action)
modes_menu.addAction(self.raport_action) modes_menu.addAction(self.raport_action)
modes_menu.addAction(self.client_action)
settings_menu.addAction(system_settings) settings_menu.addAction(system_settings)
settings_menu.addAction(operator_settings) settings_menu.addAction(operator_settings)
@ -294,3 +299,34 @@ class SeekingWidget(QWidget):
self.setLayout(main_layout) self.setLayout(main_layout)
class ClientAnalyzerWidget(QWidget):
def __init__(self, tabWidget:CustomTabWidget):
super().__init__()
self._tabWidget = tabWidget
self._build_widget()
def get_widget(self) -> QWidget:
return self
def _build_widget(self):
main_layout = QVBoxLayout()
self.save_screen_btn = QPushButton("Save state")
self.save_screen_btn.setFixedWidth(185)
self.open_folder_btn = QPushButton("Open folder")
self.open_folder_btn.setFixedWidth(185)
self.build_TCW_btn = QPushButton("Calculate TCW")
self.build_TCW_btn.setFixedWidth(185)
button_layout = QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(self.save_screen_btn)
button_layout.addWidget(self.open_folder_btn)
button_layout.addWidget(self.build_TCW_btn)
button_widget = QWidget()
button_widget.setLayout(button_layout)
main_layout.addWidget(self._tabWidget)
main_layout.addWidget(button_widget)
self.setLayout(main_layout)

View File

@ -10,6 +10,7 @@ from controller.converter import DataConverter
from gui.plotter import PlotWidget from gui.plotter import PlotWidget
from controller.controller import Controller from controller.controller import Controller
from controller.passport_former import PassportFormer from controller.passport_former import PassportFormer
from performance.roboter import TraceProcessor
def main(): def main():
@ -22,13 +23,15 @@ def main():
plot_widget_builder = PlotWidget(controller=controller) plot_widget_builder = PlotWidget(controller=controller)
passport_former = PassportFormer() passport_former = PassportFormer()
window = MainWindow() window = MainWindow()
mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager) trace_processor = TraceProcessor()
mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager, trace_processor)
window.show() window.show()
window.signal_mode.connect(controller.set_working_mode) window.signal_mode.connect(controller.set_working_mode)
window.signal_settings.connect(controller.update_settings) window.signal_settings.connect(controller.update_settings)
window.signal_replot_all.connect(controller.update_plots) window.signal_replot_all.connect(controller.update_plots)
window.signal_open_file.connect(controller.open_file) window.signal_open_file.connect(controller.open_file)
window.signal_open_dir.connect(controller.open_dir)
window.signal_save_file.connect(controller.save_file) window.signal_save_file.connect(controller.save_file)
controller.signal_widgets.connect(window.show_plot_tabs) controller.signal_widgets.connect(window.show_plot_tabs)

View File

@ -1,43 +1,15 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import Tuple, Union from typing import Tuple, Union, Optional
from dataclasses import dataclass, field from dataclasses import dataclass, field
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from base.base import BaseKukaDataParser, BaseKukaTextParser, BaseRawTraceProcessor, KukaDataHead, KukaTXT
@dataclass
class KukaDataHead:
rob_ID: int
filename: str
channels: dict
@dataclass class KukaDataParser(BaseKukaDataParser):
class KukaTXT:
time: float = 0
endtime: float = 0
#module: str
func: str = ""
type_: str = ""
signal: str = ""
#line: int = 0
#point_name: str = ""
#point_coord: dict = field(default_factory=lambda: {})
#blending: str = ""
#blending_param: float = 0
#velocities: dict = field(default_factory=lambda: {})
#accelerarions: dict = field(default_factory=lambda: {})
#base: dict = field(default_factory=lambda: {})
#tool: dict = field(default_factory=lambda: {})
#ipo_mode: str = ""
#motion_mode: str = ""
#load: dict = field(default_factory=lambda: {})
#load_a3: dict = field(default_factory=lambda: {})
class KukaDataParser:
def parse(self, head_path: str) -> pd.DataFrame: def parse(self, head_path: str) -> pd.DataFrame:
head = self._parse_dat_file(head_path) head = self._parse_dat_file(head_path)
@ -120,8 +92,9 @@ class KukaDataParser:
return floats return floats
class TextParser: class KukaTextParser(BaseKukaTextParser):
def __init__(self): def __init__(self):
super().__init__()
self._in_msg = False self._in_msg = False
self._datapacks = [] self._datapacks = []
@ -290,6 +263,41 @@ class TextStageDetector:
return stages return stages
class TraceProcessor(BaseRawTraceProcessor):
def __init__(self):
dataparser = KukaDataParser()
textparser = KukaTextParser()
super().__init__(dataparser, textparser)
def prerender(self, data:list[str]) -> None:
if data:
dat_filepath = data[0]
txt_filepath = None
elif len(data) == 2:
dat_filepath = data[0]
txt_filepath = data[1]
else:
dat_filepath = None
txt_filepath = None
self._trace_df = self._unpack_trace(dat_filepath)
self._text_data = self._unpack_text(txt_filepath)
self._mediator.notify(self, self._trace_df)
def _unpack_trace(self, dat_filepath:str = None) -> Optional[pd.DataFrame]:
if dat_filepath:
return self._dataparser.parse(dat_filepath)
return None
def _unpack_text(self, txt_filepath:str = None) -> Optional[list[KukaTXT]]:
if txt_filepath:
return self._textparser.parse(txt_filepath)
return None
"Перемещение" "Перемещение"
@ -330,7 +338,7 @@ class TextStageDetector:
if __name__ == '__main__': if __name__ == '__main__':
roboreader = KukaDataParser() roboreader = KukaDataParser()
txt_reader = TextParser() txt_reader = KukaTextParser()
detector_traces = TraceStageDetector(region_of_focus=[7.7, 42]) detector_traces = TraceStageDetector(region_of_focus=[7.7, 42])
detector_weldings = TextStageDetector() detector_weldings = TextStageDetector()