From 7be34dd161970b603bb60347e9d2a18a01856cd1 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 24 Feb 2026 12:23:36 +0000 Subject: [PATCH] feat(main): implement class QtScanSlicer --- scanslicer/main.py | 270 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/scanslicer/main.py b/scanslicer/main.py index f9c3543..39a6e43 100644 --- a/scanslicer/main.py +++ b/scanslicer/main.py @@ -173,3 +173,273 @@ def resolve_app_icon(): return candidate_icon return QIcon() + + +# QtScanSlicer: class definition for ScanSlicer behavior. +class QtScanSlicer(QMainWindow, ScanSlicerUiBuilderMixin, ScanSlicerControllerMixin): + CONTROL_PANEL_WIDTH = 370 + STEPPER_WIDTH = 75 + PANEL_CONTENT_MARGIN = 10 + VERTICAL_SPACER_HEIGHT = 8 + SECTION_TITLE_FONT_PX = 18 + SECTION_TITLE_VPAD = 5 + CHOICE_PARAM_OPTIONS = { + "detect_pipeline": [("Threshold", 0), ("Canny", 1)], + "bw_method": [("Simple", 0), ("Gauss", 1), ("Otsu", 2)], + "invert_mode": [("Auto", 0), ("Off", 1), ("On", 2)], + "morph_op": [("None", 0), ("Dilate", 1), ("Erode", 2), ("Open", 3), ("Close", 4)], + } + ADVANCED_PARAMS = { + "detect_pipeline", + "clahe_clip", + "gaussian", + "bw_adaptive_c", + "invert_mode", + "contour_approx_pct", + "bbox_max_size", + "aspect_ratio_min", + "aspect_ratio_max", + "border_ignore", + "iou_merge_thresh", + } + LOCK_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWxvY2staWNvbiBsdWNpZGUtbG9jayI+PHJlY3Qgd2lkdGg9IjE4IiBoZWlnaHQ9IjExIiB4" + "PSIzIiB5PSIxMSIgcng9IjIiIHJ5PSIyIi8+PHBhdGggZD0iTTcgMTFWN2E1IDUgMCAwIDEgMTAgMHY0Ii8+PC" + "9zdmc+" + ) + SQUARIZE_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIx" + "OCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4=" + ) + ROTATE_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXJlZnJlc2gtY2N3LWRvdC1pY29uIGx1Y2lkZS1yZWZyZXNoLWNjdy1kb3QiPjxwYXRoIGQ9" + "Ik0yMSAxMmE5IDkgMCAwIDAtOS05IDkuNzUgOS43NSAwIDAgMC02Ljc0IDIuNzRMMyA4Ii8+PHBhdGggZD0i" + "TTMgM3Y1aDUiLz48cGF0aCBkPSJNMyAxMmE5IDkgMCAwIDAgOSA5IDkuNzUgOS43NSAwIDAgMCA2Ljc0LTIu" + "NzRMMjEgMTYiLz48cGF0aCBkPSJNMTYgMTZoNXY1Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMSIv" + "Pjwvc3ZnPg==" + ) + RESET_ROTATION_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXJvdGF0ZS1jdy1zcXVhcmUtaWNvbiBsdWNpZGUtcm90YXRlLWN3LXNxdWFyZSI+PHBhdGgg" + "ZD0iTTEyIDVINmEyIDIgMCAwIDAtMiAydjMiLz48cGF0aCBkPSJtOSA4IDMtMy0zLTMiLz48cGF0aCBkPSJNNC" + "AxNHY0YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY3YTIgMiAwIDAgMC0yLTJoLTIiLz48L3N2Zz4=" + ) + REMOVE_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXNxdWFyZS14LWljb24gbHVjaWRlLXNxdWFyZS14Ij48cmVjdCB3aWR0aD0iMTgiIGhlaWdo" + "dD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxwYXRoIGQ9Im0xNSA5LTYgNiIvPjxwYXRoIGQ9" + "Im05IDkgNiA2Ii8+PC9zdmc+" + ) + LOCK_ALL_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWxvY2sta2V5aG9sZS1pY29uIGx1Y2lkZS1sb2NrLWtleWhvbGUiPjxjaXJjbGUgY3g9IjEy" + "IiBjeT0iMTYiIHI9IjEiLz48cmVjdCB4PSIzIiB5PSIxMCIgd2lkdGg9IjE4IiBoZWlnaHQ9IjEyIiByeD0i" + "MiIvPjxwYXRoIGQ9Ik03IDEwVjdhNSA1IDAgMCAxIDEwIDB2MyIvPjwvc3ZnPg==" + ) + DETECT_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAx" + "IDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1ID" + "AgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg==" + ) + PREVIEW_FILTER_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmEx" + "MC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMC" + "AwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIv" + "PjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwID" + "EgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwv" + "c3ZnPg==" + ) + ADD_BOX_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXNxdWFyZS1wbHVzLWljb24gbHVjaWRlLXNxdWFyZS1wbHVzIj48cmVjdCB3aWR0aD0iMTgi" + "IGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0x" + "MiA4djgiLz48L3N2Zz4=" + ) + RESET_VIEW_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLXZpZXctaWNvbiBsdWNpZGUtdmlldyI+PHBhdGggZD0iTTIxIDE3djJhMiAyIDAgMCAxLTIg" + "Mkg1YTIgMiAwIDAgMS0yLTJ2LTIiLz48cGF0aCBkPSJNMjEgN1Y1YTIgMiAwIDAgMC0yLTJINWEyIDIgMCAwID" + "AtMiAydjIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxIi8+PHBhdGggZD0iTTE4Ljk0NCAxMi4zM2Ex" + "IDEgMCAwIDAgMC0uNjYgNy41IDcuNSAwIDAgMC0xMy44ODggMCAxIDEgMCAwIDAgMCAuNjYgNy41IDcuNSAwID" + "AgMCAxMy44ODggMCIvPjwvc3ZnPg==" + ) + PREV_IMAGE_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0LWljb24gbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0Ij48Y2ly" + "Y2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Im0xMiA4LTQgNCA0IDQiLz48cGF0aCBkPSJN" + "MTYgMTJIOCIvPjwvc3ZnPg==" + ) + NEXT_IMAGE_ICON_DATA_URL = ( + "data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" + "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" + "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" + "NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1yaWdodC1pY29uIGx1Y2lkZS1jaXJjbGUtYXJyb3ctcmlnaHQiPjxj" + "aXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0ibTEyIDE2IDQtNC00LTQiLz48cGF0aCBk" + "PSJNOCAxMmg4Ii8+PC9zdmc+" + ) + + # __init__: function definition for ScanSlicer behavior. + def __init__(self, startup_dir=None): + super().__init__() + + self.setWindowTitle("ScanSlicer") + app_icon = resolve_app_icon() + if not app_icon.isNull(): + self.setWindowIcon(app_icon) + self.setStyleSheet( + "QPushButton { padding: 5px; }" + "QSpinBox { padding: 3px; }" + ) + self.source_images = [] + self.source_index = None + self.input_dir = None + self.output_dir = None + self.save_format = "png" + self.jpg_quality = 95 + self.png_compression = 3 + self.sort_mode = "name" + self.preview_filter_enabled = False + self.last_detected_boxes = None + self.last_preview_normal_image = None + self.last_preview_filter_image = None + + self.params = AutoslicerParams() + self.autoslicer = Autoslicer(self.params) + self.autotune_profile = self.autoslicer.default_autotune_profile_name() + self.param_controls = {} + self.param_blocks = {} + self.show_advanced_params = False + self.updating_param_controls = False + self.processing_busy = False + self.controls_scroll = None + self.preview_toolbar = None + self.status_log_path = None + self.status_logging_enabled = True + self._status_last_event_pump_ts = 0.0 + self._is_closing = False + self._pending_detection_new_image = False + self._worker_pool = None + self._detection_task = None + self._autotune_task = None + self._detection_running = False + self._autotune_running = False + self._detection_task_id = 0 + self._autotune_task_id = 0 + self._active_detection_task_id = None + self._active_autotune_task_id = None + + root = QWidget(self) + self.setCentralWidget(root) + layout = QGridLayout(root) + + header = self._build_header_controls(root) + layout.addWidget(header, 0, 0, 1, 2) + + self.controls_scroll = self._build_controls_panel(root) + preview_panel = self._build_preview_panel(root) + + layout.addWidget(self.controls_scroll, 1, 0) + layout.addWidget(preview_panel, 1, 1) + layout.setColumnStretch(1, 1) + layout.setRowStretch(1, 1) + + self._build_status_bar() + + self.initialize_status_log() + self.update_statusbar("Ready.") + self.update_quality_controls_state() + self.update_parameter_visibility() + self.update_output_dir_label() + self.refresh_active_box_toolbar() + + if startup_dir: + self.open_directory(startup_dir) + + # _build_header_controls: function definition for ScanSlicer behavior. + def _build_header_controls(self, parent): + header = QWidget(parent) + header_layout = QHBoxLayout(header) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(6) + + title_label = QLabel("ScanSlicer") + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + + header_layout.addStretch(1) + header_layout.addWidget(title_label) + header_layout.addStretch(1) + + self.button_minimize = QPushButton("−", header) + self.button_minimize.setToolTip("Minimize") + self.button_minimize.clicked.connect(self.showMinimized) + header_layout.addWidget(self.button_minimize) + + self.button_maximize = QPushButton("□", header) + self.button_maximize.setToolTip("Maximize/Restore") + self.button_maximize.clicked.connect(self.toggle_maximize_restore) + header_layout.addWidget(self.button_maximize) + + self.button_quit = QPushButton("✕", header) + self.button_quit.setToolTip("Quit") + self.button_quit.clicked.connect(self.close) + header_layout.addWidget(self.button_quit) + return header + + # toggle_maximize_restore: function definition for ScanSlicer behavior. + def toggle_maximize_restore(self): + if self.isMaximized(): + self.showNormal() + return + self.showMaximized() + + # closeEvent: function definition for ScanSlicer behavior. + def closeEvent(self, event): + self._is_closing = True + if hasattr(self, "autoslicer"): + self.autoslicer.abort_operation() + if hasattr(self, "_close_status_log"): + self._close_status_log() + super().closeEvent(event)