imagebaker 0.0.41__py3-none-any.whl → 0.0.46__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. imagebaker/core/__init__.py +0 -0
  2. imagebaker/core/configs/__init__.py +1 -0
  3. imagebaker/core/configs/configs.py +156 -0
  4. imagebaker/core/defs/__init__.py +1 -0
  5. imagebaker/core/defs/defs.py +258 -0
  6. imagebaker/core/plugins/__init__.py +0 -0
  7. imagebaker/core/plugins/base_plugin.py +39 -0
  8. imagebaker/core/plugins/cosine_plugin.py +39 -0
  9. imagebaker/layers/__init__.py +3 -0
  10. imagebaker/layers/annotable_layer.py +847 -0
  11. imagebaker/layers/base_layer.py +724 -0
  12. imagebaker/layers/canvas_layer.py +1007 -0
  13. imagebaker/list_views/__init__.py +3 -0
  14. imagebaker/list_views/annotation_list.py +203 -0
  15. imagebaker/list_views/canvas_list.py +185 -0
  16. imagebaker/list_views/image_list.py +138 -0
  17. imagebaker/list_views/layer_list.py +390 -0
  18. imagebaker/list_views/layer_settings.py +219 -0
  19. imagebaker/models/__init__.py +0 -0
  20. imagebaker/models/base_model.py +150 -0
  21. imagebaker/tabs/__init__.py +2 -0
  22. imagebaker/tabs/baker_tab.py +496 -0
  23. imagebaker/tabs/layerify_tab.py +837 -0
  24. imagebaker/utils/__init__.py +0 -0
  25. imagebaker/utils/image.py +105 -0
  26. imagebaker/utils/state_utils.py +92 -0
  27. imagebaker/utils/transform_mask.py +107 -0
  28. imagebaker/window/__init__.py +1 -0
  29. imagebaker/window/app.py +136 -0
  30. imagebaker/window/main_window.py +181 -0
  31. imagebaker/workers/__init__.py +3 -0
  32. imagebaker/workers/baker_worker.py +247 -0
  33. imagebaker/workers/layerify_worker.py +91 -0
  34. imagebaker/workers/model_worker.py +54 -0
  35. {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/METADATA +5 -5
  36. imagebaker-0.0.46.dist-info/RECORD +41 -0
  37. {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/WHEEL +1 -1
  38. imagebaker-0.0.41.dist-info/RECORD +0 -7
  39. {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.46.dist-info}/LICENSE +0 -0
  40. {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/entry_points.txt +0 -0
  41. {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,105 @@
1
+ import numpy as np
2
+ from PySide6.QtGui import QPixmap, QImage
3
+ import cv2
4
+
5
+ from imagebaker.core.defs.defs import Annotation
6
+
7
+
8
+ def qpixmap_to_numpy(pixmap: QPixmap | QImage) -> np.ndarray:
9
+ """
10
+ Convert QPixmap to RGBA numpy array.
11
+
12
+ Args:
13
+ pixmap: The QPixmap to convert
14
+
15
+ Returns:
16
+ numpy.ndarray: Array with shape (height, width, 4) containing RGBA values
17
+ """
18
+
19
+ if isinstance(pixmap, QPixmap):
20
+ # Convert QPixmap to QImage first
21
+ image = pixmap.toImage()
22
+ else:
23
+ image = pixmap
24
+ # Convert to Format_RGBA8888 for consistent channel ordering
25
+ if image.format() != QImage.Format_RGBA8888:
26
+ image = image.convertToFormat(QImage.Format_RGBA8888)
27
+
28
+ width = image.width()
29
+ height = image.height()
30
+
31
+ # Get the bytes directly from the QImage
32
+ ptr = image.constBits()
33
+
34
+ # Convert memoryview to bytes and then to numpy array
35
+ bytes_data = bytes(ptr)
36
+ arr = np.frombuffer(bytes_data, dtype=np.uint8).reshape((height, width, 4))
37
+
38
+ return arr
39
+
40
+
41
+ def draw_annotations(image: np.ndarray, annotations: list[Annotation]) -> np.ndarray:
42
+ """
43
+ Draw annotations on an image.
44
+
45
+ Args:
46
+ image (np.ndarray): Image to draw on.
47
+ annotations (list[Annotation]): List of annotations to draw.
48
+
49
+ Returns:
50
+ np.ndarray: Image with annotations drawn.
51
+ """
52
+ for i, ann in enumerate(annotations):
53
+ if ann.rectangle:
54
+ cv2.rectangle(
55
+ image,
56
+ (int(ann.rectangle.x()), int(ann.rectangle.y())),
57
+ (
58
+ int(ann.rectangle.x() + ann.rectangle.width()),
59
+ int(ann.rectangle.y() + ann.rectangle.height()),
60
+ ),
61
+ (0, 255, 0),
62
+ 2,
63
+ )
64
+ rect_center = ann.rectangle.center()
65
+
66
+ cv2.putText(
67
+ image,
68
+ ann.label,
69
+ (int(rect_center.x()), int(rect_center.y())),
70
+ cv2.FONT_HERSHEY_SIMPLEX,
71
+ 1,
72
+ (0, 255, 0),
73
+ 2,
74
+ )
75
+ elif ann.polygon:
76
+ cv2.polylines(
77
+ image,
78
+ [np.array([[int(p.x()), int(p.y())] for p in ann.polygon])],
79
+ True,
80
+ (0, 255, 0),
81
+ 2,
82
+ )
83
+ polygon_center = ann.polygon.boundingRect().center()
84
+ cv2.putText(
85
+ image,
86
+ ann.label,
87
+ (int(polygon_center.x()), int(polygon_center.y())),
88
+ cv2.FONT_HERSHEY_SIMPLEX,
89
+ 1,
90
+ (0, 255, 0),
91
+ 2,
92
+ )
93
+ elif ann.points:
94
+ for p in ann.points:
95
+ cv2.circle(image, (int(p.x()), int(p.y())), 5, (0, 255, 0), -1)
96
+ cv2.putText(
97
+ image,
98
+ ann.label,
99
+ (int(ann.points[0].x()), int(ann.points[0].y())),
100
+ cv2.FONT_HERSHEY_SIMPLEX,
101
+ 1,
102
+ (0, 255, 0),
103
+ 2,
104
+ )
105
+ return image
@@ -0,0 +1,92 @@
1
+ from imagebaker.core.defs import LayerState, DrawingState
2
+ from PySide6.QtCore import QPointF
3
+
4
+
5
+ def calculate_intermediate_states(
6
+ previous_state: LayerState | None, current_state: LayerState | None, steps: int
7
+ ):
8
+ """
9
+ Calculate intermediate states between previous_state and current_state for a layer.
10
+ Append the current_state to the list of states after calculating intermediates.
11
+
12
+ Args:
13
+ previous_state (LayerState): Previous state of the layer.
14
+ current_state (LayerState): Current state of the layer.
15
+ steps (int): Number of intermediate states to calculate.
16
+ """
17
+ if not previous_state or not current_state:
18
+ return [current_state] # If no previous state, return only the current state
19
+
20
+ intermediate_states = []
21
+ for i in range(1, steps + 1):
22
+ # Interpolate attributes between previous_state and current_state
23
+ interpolated_state = LayerState(
24
+ layer_id=current_state.layer_id,
25
+ layer_name=current_state.layer_name,
26
+ opacity=previous_state.opacity
27
+ + (current_state.opacity - previous_state.opacity) * (i / steps),
28
+ position=QPointF(
29
+ previous_state.position.x()
30
+ + (current_state.position.x() - previous_state.position.x())
31
+ * (i / steps),
32
+ previous_state.position.y()
33
+ + (current_state.position.y() - previous_state.position.y())
34
+ * (i / steps),
35
+ ),
36
+ rotation=previous_state.rotation
37
+ + (current_state.rotation - previous_state.rotation) * (i / steps),
38
+ scale=previous_state.scale
39
+ + (current_state.scale - previous_state.scale) * (i / steps),
40
+ scale_x=previous_state.scale_x
41
+ + (current_state.scale_x - previous_state.scale_x) * (i / steps),
42
+ scale_y=previous_state.scale_y
43
+ + (current_state.scale_y - previous_state.scale_y) * (i / steps),
44
+ transform_origin=QPointF(
45
+ previous_state.transform_origin.x()
46
+ + (
47
+ current_state.transform_origin.x()
48
+ - previous_state.transform_origin.x()
49
+ )
50
+ * (i / steps),
51
+ previous_state.transform_origin.y()
52
+ + (
53
+ current_state.transform_origin.y()
54
+ - previous_state.transform_origin.y()
55
+ )
56
+ * (i / steps),
57
+ ),
58
+ order=current_state.order,
59
+ visible=current_state.visible,
60
+ allow_annotation_export=current_state.allow_annotation_export,
61
+ playing=current_state.playing,
62
+ selected=current_state.selected,
63
+ is_annotable=current_state.is_annotable,
64
+ status=current_state.status,
65
+ )
66
+
67
+ # Deep copy the drawing_states from the previous_state
68
+ interpolated_state.drawing_states = [
69
+ DrawingState(
70
+ position=d.position,
71
+ color=d.color,
72
+ size=d.size,
73
+ )
74
+ for d in current_state.drawing_states
75
+ ]
76
+
77
+ intermediate_states.append(interpolated_state)
78
+
79
+ # Append the current state as the final state
80
+ current_state.drawing_states.extend(
81
+ [
82
+ DrawingState(
83
+ position=d.position,
84
+ color=d.color,
85
+ size=d.size,
86
+ )
87
+ for d in current_state.drawing_states
88
+ ]
89
+ )
90
+ intermediate_states.append(current_state)
91
+
92
+ return intermediate_states
@@ -0,0 +1,107 @@
1
+ import cv2
2
+ import numpy as np
3
+ from typing import List, Tuple
4
+
5
+
6
+ def mask_to_polygons(
7
+ mask: np.ndarray,
8
+ min_polygon_area: float = 10,
9
+ merge_polygons: bool = False,
10
+ merge_distance: int = 5, # Max distance between polygons to merge
11
+ ) -> List[List[Tuple[int, int]]]:
12
+ """
13
+ Convert a binary mask to a list of polygons.
14
+ Each polygon is a list of (x, y) coordinates.
15
+
16
+ Args:
17
+ mask (np.ndarray): Binary mask (0 or 255).
18
+ min_polygon_area (float): Minimum area for a polygon to be included.
19
+ merge_polygons (bool): If True, merges nearby/overlapping polygons.
20
+ merge_distance (int): Max distance between polygons to merge (if merge_polygons=True).
21
+
22
+ Returns:
23
+ List[List[Tuple[int, int]]]: List of polygons, each represented as a list of (x, y) points.
24
+ """
25
+ contours, _ = cv2.findContours(
26
+ mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
27
+ )
28
+
29
+ polygons = []
30
+ for contour in contours:
31
+ area = cv2.contourArea(contour)
32
+ if area >= min_polygon_area:
33
+ polygons.append(contour)
34
+
35
+ # Sort polygons by area (descending)
36
+ polygons = sorted(
37
+ polygons, key=lambda p: cv2.contourArea(np.array(p)), reverse=True
38
+ )
39
+
40
+ # Merge polygons if requested
41
+ if merge_polygons and len(polygons) > 1:
42
+ # Use morphological dilation to merge nearby regions
43
+ kernel = np.ones((merge_distance, merge_distance), np.uint8)
44
+ merged_mask = cv2.dilate(mask, kernel, iterations=1)
45
+
46
+ # Re-extract contours after merging
47
+ merged_contours, _ = cv2.findContours(
48
+ merged_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
49
+ )
50
+
51
+ # Filter again by area
52
+ merged_polygons = []
53
+ for contour in merged_contours:
54
+ area = cv2.contourArea(contour)
55
+ if area >= min_polygon_area:
56
+ merged_polygons.append(contour)
57
+
58
+ polygons = merged_polygons
59
+
60
+ # Convert contours to list of points
61
+ result = []
62
+ for poly in polygons:
63
+ points = poly.squeeze().tolist() # Remove extra dimensions
64
+ if len(points) >= 3: # Ensure it's a valid polygon
65
+ result.append([(int(x), int(y)) for x, y in points])
66
+
67
+ return result
68
+
69
+
70
+ def mask_to_rectangles(
71
+ mask: np.ndarray,
72
+ merge_rectangles: bool = False,
73
+ merge_threshold: int = 1,
74
+ merge_epsilon: float = 0.5,
75
+ ) -> List[Tuple[int, int, int, int]]:
76
+ """
77
+ Convert a binary mask to a list of rectangles.
78
+ Each rectangle is a tuple of (x, y, w, h).
79
+
80
+ Args:
81
+ mask (np.ndarray): Binary mask (0 or 255).
82
+ merge_rectangles (bool): If True, merges overlapping or nearby rectangles.
83
+ merge_threshold (int): Min number of rectangles to merge into one.
84
+ merge_epsilon (float): Controls how close rectangles must be to merge (0.0 to 1.0).
85
+
86
+ Returns:
87
+ List[Tuple[int, int, int, int]]: List of rectangles, each as (x, y, w, h).
88
+ """
89
+ contours, _ = cv2.findContours(
90
+ mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
91
+ )
92
+
93
+ rectangles = []
94
+ for contour in contours:
95
+ x, y, w, h = cv2.boundingRect(contour)
96
+ rectangles.append((x, y, w, h))
97
+
98
+ if merge_rectangles and len(rectangles) > 1:
99
+ # Convert rectangles to the format expected by groupRectangles
100
+ rects = np.array(rectangles)
101
+ # groupRectangles requires [x, y, w, h] format
102
+ grouped_rects, _ = cv2.groupRectangles(
103
+ rects.tolist(), merge_threshold, merge_epsilon
104
+ )
105
+ rectangles = [tuple(map(int, rect)) for rect in grouped_rects]
106
+
107
+ return rectangles
@@ -0,0 +1 @@
1
+ from .main_window import MainWindow # noqa
@@ -0,0 +1,136 @@
1
+ from imagebaker.window.main_window import MainWindow
2
+ from imagebaker.core.configs import LayerConfig, CanvasConfig
3
+ from imagebaker import logger
4
+
5
+ import importlib.util
6
+ import runpy
7
+ import ast
8
+ import typer
9
+ from pathlib import Path
10
+ from PySide6.QtWidgets import QApplication
11
+
12
+ app_cli = typer.Typer()
13
+
14
+
15
+ def find_and_import_subclass(file_path: str, base_class_name: str):
16
+ """
17
+ Find and import the first subclass of a given base class in a Python file.
18
+
19
+ Args:
20
+ file_path (str): The path to the Python file to inspect.
21
+ base_class_name (str): The name of the base class to look for subclasses of.
22
+
23
+ Returns:
24
+ type: The first subclass found, or None if no subclass is found.
25
+ """
26
+ with open(file_path, "r") as file:
27
+ tree = ast.parse(file.read(), filename=file_path)
28
+
29
+ for node in ast.walk(tree):
30
+ if isinstance(node, ast.ClassDef):
31
+ for base in node.bases:
32
+ if isinstance(base, ast.Name) and base.id == base_class_name:
33
+ # Dynamically import the file and return the class
34
+ module_name = Path(file_path).stem
35
+ spec = importlib.util.spec_from_file_location(
36
+ module_name, file_path
37
+ )
38
+ module = importlib.util.module_from_spec(spec)
39
+ spec.loader.exec_module(module)
40
+ return getattr(module, node.name)
41
+ return None
42
+
43
+
44
+ def load_models(file_path: str):
45
+ """Dynamically load the LOADED_MODELS object from the specified file."""
46
+ try:
47
+ # Ensure the file path is absolute
48
+ file_path = Path(file_path).resolve()
49
+
50
+ # Execute the file and return its global variables
51
+ loaded_globals = runpy.run_path(str(file_path))
52
+ except Exception as e:
53
+ logger.error(f"Failed to load models from {file_path}: {e}")
54
+ return {}
55
+
56
+ # Ensure LOADED_MODELS exists in the loaded context
57
+ if "LOADED_MODELS" not in loaded_globals:
58
+ logger.warning(f"No LOADED_MODELS object found in {file_path}.")
59
+ return {}
60
+
61
+ return loaded_globals.get("LOADED_MODELS", {})
62
+
63
+
64
+ @app_cli.command()
65
+ def run(
66
+ models_file: str = typer.Option(
67
+ "loaded_models.py", help="Path to the Python file defining LOADED_MODELS."
68
+ ),
69
+ project_dir: str = typer.Option(
70
+ ".", help="The project directory to use for the application."
71
+ ),
72
+ configs_file: str = typer.Option(
73
+ "imagebaker/core/configs.py",
74
+ help="The Python file to search for LayerConfig and CanvasConfig subclasses.",
75
+ ),
76
+ ):
77
+ """
78
+ Run the ImageBaker application.
79
+
80
+ Args:
81
+ models_file (str): Path to the Python file defining LOADED_MODELS.
82
+ project_dir (str): The project directory to use for the application.
83
+ configs_file (str): The Python file to search for LayerConfig and CanvasConfig subclasses.
84
+ """
85
+ models_file_path = Path(models_file)
86
+ if not models_file_path.is_file():
87
+ logger.warning(f"Models file not found: {models_file_path}")
88
+ LOADED_MODELS = {None: None}
89
+ else:
90
+ LOADED_MODELS = load_models(models_file_path)
91
+
92
+ configs_file_path = Path(configs_file)
93
+ if not configs_file_path.is_file():
94
+ logger.warning(f"Configs file not found: {configs_file_path}")
95
+ layer_config_class = None
96
+ canvas_config_class = None
97
+ else:
98
+ # Find and import subclasses of LayerConfig and CanvasConfig
99
+ layer_config_class = find_and_import_subclass(configs_file_path, "LayerConfig")
100
+ canvas_config_class = find_and_import_subclass(
101
+ configs_file_path, "CanvasConfig"
102
+ )
103
+
104
+ # Use the imported subclass if found, or fall back to the default
105
+ if layer_config_class:
106
+ logger.info(f"Using LayerConfig subclass: {layer_config_class.__name__}")
107
+ layer_config = layer_config_class()
108
+ else:
109
+ logger.info("No LayerConfig subclass found. Using default LayerConfig.")
110
+ layer_config = LayerConfig(project_dir=project_dir)
111
+
112
+ if canvas_config_class:
113
+ logger.info(f"Using CanvasConfig subclass: {canvas_config_class.__name__}")
114
+ canvas_config = canvas_config_class()
115
+ else:
116
+ logger.info("No CanvasConfig subclass found. Using default CanvasConfig.")
117
+ canvas_config = CanvasConfig(project_dir=project_dir)
118
+
119
+ main(layer_config, canvas_config, LOADED_MODELS)
120
+
121
+
122
+ def main(layer_config, canvas_config, LOADED_MODELS):
123
+
124
+ # Initialize the application
125
+ app = QApplication([])
126
+ window = MainWindow(
127
+ layerify_config=layer_config,
128
+ canvas_config=canvas_config,
129
+ loaded_models=LOADED_MODELS,
130
+ )
131
+ window.show()
132
+ app.exec()
133
+
134
+
135
+ if __name__ == "__main__":
136
+ app_cli()
@@ -0,0 +1,181 @@
1
+ from imagebaker.core.configs import LayerConfig, CanvasConfig
2
+ from imagebaker import logger
3
+ from imagebaker.tabs import LayerifyTab, BakerTab
4
+
5
+ from PySide6.QtCore import Qt, QTimer
6
+ from PySide6.QtWidgets import (
7
+ QMainWindow,
8
+ QMessageBox,
9
+ QTabWidget,
10
+ )
11
+
12
+
13
+ class MainWindow(QMainWindow):
14
+
15
+ def __init__(
16
+ self,
17
+ layerify_config: LayerConfig = LayerConfig(),
18
+ canvas_config: CanvasConfig = CanvasConfig(),
19
+ loaded_models=None,
20
+ ):
21
+ """
22
+ Main window for Image Baker application.
23
+
24
+ Args:
25
+ layerify_config (LayerConfig): Configuration for Layerify tab.
26
+ canvas_config (CanvasConfig): Configuration for Canvas tab.
27
+ loaded_models (dict): Dictionary of loaded models.
28
+ """
29
+ super().__init__()
30
+ self.layerify_config = layerify_config
31
+ self.canvas_config = canvas_config
32
+ self.loaded_models = loaded_models
33
+
34
+ # Use QTimer to defer UI initialization
35
+ QTimer.singleShot(0, self.init_ui)
36
+
37
+ def init_ui(self):
38
+ """Initialize the main window and set up tabs."""
39
+ try:
40
+ self.setWindowTitle("Image Baker")
41
+ self.setGeometry(100, 100, 1200, 800)
42
+
43
+ self.status_bar = self.statusBar()
44
+ self.status_bar.showMessage("Ready")
45
+
46
+ # Create main tab widget
47
+ self.tab_widget = QTabWidget()
48
+ self.tab_widget.currentChanged.connect(self.handle_tab_change)
49
+ self.setCentralWidget(self.tab_widget)
50
+
51
+ # Initialize tabs
52
+ self.layerify_tab = LayerifyTab(
53
+ self, self.layerify_config, self.canvas_config, self.loaded_models
54
+ )
55
+ self.baker_tab = BakerTab(self, self.canvas_config)
56
+
57
+ self.tab_widget.addTab(self.layerify_tab, "Layerify")
58
+ self.tab_widget.addTab(self.baker_tab, "Baker")
59
+
60
+ # Connect signals
61
+ self.baker_tab.messageSignal.connect(self.update_status)
62
+ self.layerify_tab.layerAdded.connect(self.baker_tab.add_layer)
63
+ self.baker_tab.bakingResult.connect(self.layerify_tab.add_baked_result)
64
+ self.layerify_tab.gotToTab.connect(self.goto_tab)
65
+ # Use QTimer for safe signal connection
66
+ QTimer.singleShot(0, self._connect_final_signals)
67
+
68
+ # Handle initial tab state
69
+ self.handle_tab_change(0)
70
+
71
+ except Exception as e:
72
+ logger.error(f"MainWindow initialization error: {e}")
73
+ import traceback
74
+
75
+ traceback.print_exc()
76
+ QMessageBox.critical(self, "Initialization Error", str(e))
77
+
78
+ def _connect_final_signals(self):
79
+ """Connect signals that might require fully initialized objects"""
80
+ try:
81
+ self.layerify_tab.clearAnnotations.connect(
82
+ lambda: QTimer.singleShot(0, self.clear_annotations),
83
+ Qt.QueuedConnection,
84
+ )
85
+ self.layerify_tab.messageSignal.connect(self.update_status)
86
+ except Exception as e:
87
+ logger.error(f"Final signal connection error: {e}")
88
+
89
+ def goto_tab(self, tab_index):
90
+ """Switch to the specified tab index."""
91
+ self.tab_widget.setCurrentIndex(tab_index)
92
+ self.update_status("Switched to Layerify tab")
93
+
94
+ def clear_annotations(self):
95
+ """Clear all annotations and layers from both tabs."""
96
+ try:
97
+ logger.info("Clearing all annotations")
98
+ # Clear annotations in Layerify tab
99
+ self.layerify_tab.layer.annotations.clear()
100
+ self.layerify_tab.layer.update()
101
+
102
+ # Clear layers in Baker tab
103
+ self.baker_tab.layer_list.clear_layers()
104
+ self.baker_tab.current_canvas.clear_layers()
105
+
106
+ # Update annotation list
107
+ self.layerify_tab.update_annotation_list()
108
+
109
+ # Update status
110
+ self.update_status("All annotations cleared")
111
+ except Exception as e:
112
+ logger.error(f"Error handling clear: {str(e)}")
113
+ self.status_bar.showMessage(f"Error handling clear: {str(e)}")
114
+
115
+ def handle_tab_change(self, index):
116
+ """Control annotation panel visibility based on tab"""
117
+ current_tab = self.tab_widget.tabText(index)
118
+ logger.info(f"Switched to {current_tab} tab.")
119
+
120
+ if current_tab == "Layerify":
121
+ self.layerify_tab.toolbar_dock.setVisible(True)
122
+ self.layerify_tab.toolbar.setVisible(True)
123
+ self.layerify_tab.annotation_list.setVisible(True)
124
+ self.layerify_tab.image_list_panel.setVisible(True)
125
+
126
+ self.baker_tab.layer_settings.setVisible(False)
127
+ self.baker_tab.layer_list.setVisible(True)
128
+ self.baker_tab.toolbar.setVisible(False)
129
+ self.baker_tab.canvas_list.setVisible(False)
130
+
131
+ else:
132
+ self.layerify_tab.annotation_list.setVisible(False)
133
+ self.layerify_tab.toolbar.setVisible(False)
134
+ self.layerify_tab.toolbar_dock.setVisible(False)
135
+ self.layerify_tab.image_list_panel.setVisible(False)
136
+
137
+ self.baker_tab.layer_list.setVisible(True)
138
+ self.baker_tab.layer_settings.setVisible(True)
139
+ self.baker_tab.toolbar.setVisible(True)
140
+ self.baker_tab.toolbar_dock.setVisible(True)
141
+ self.baker_tab.canvas_list.setVisible(True)
142
+ self.baker_tab.canvas_list.update_canvas_list()
143
+ # self.baker_tab.
144
+
145
+ def update_status(self, msg):
146
+ """Update status bar that's visible in all tabs"""
147
+ # if current tab is layerify
148
+ if self.tab_widget.currentIndex() == 0:
149
+ status_text = f"{msg} | Label: {self.layerify_tab.current_label}"
150
+ status_text += (
151
+ f"| Model: {self.layerify_tab.current_model.name} "
152
+ if self.layerify_tab.current_model
153
+ else ""
154
+ )
155
+ status_text += (
156
+ f"| Annotations: {len(self.layerify_tab.layer.annotations)}"
157
+ if self.layerify_tab.layer
158
+ else ""
159
+ )
160
+ status_text += (
161
+ f"| Layers: {len(self.baker_tab.current_canvas.layers)}"
162
+ if self.baker_tab.current_canvas
163
+ else ""
164
+ )
165
+ status_text += f"| Image: {self.layerify_tab.curr_image_idx + 1}"
166
+ status_text += f"/{len(self.layerify_tab.image_entries)}"
167
+ elif self.tab_widget.currentIndex() == 1:
168
+ status_text = (
169
+ f"{msg} | Num Layers: {len(self.baker_tab.current_canvas.layers)}"
170
+ if self.baker_tab.current_canvas
171
+ else ""
172
+ )
173
+ self.status_bar.showMessage(status_text)
174
+
175
+ def closeEvent(self, event):
176
+ # Clean up tabs first
177
+ if hasattr(self, "layerify_tab"):
178
+ self.layerify_tab.deleteLater()
179
+ if hasattr(self, "baker_tab"):
180
+ self.baker_tab.deleteLater()
181
+ super().closeEvent(event)
@@ -0,0 +1,3 @@
1
+ from .layerify_worker import LayerifyWorker # noqa
2
+ from .baker_worker import BakerWorker # noqa
3
+ from .model_worker import ModelPredictionWorker # noqa