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.
- imagebaker/core/__init__.py +0 -0
- imagebaker/core/configs/__init__.py +1 -0
- imagebaker/core/configs/configs.py +156 -0
- imagebaker/core/defs/__init__.py +1 -0
- imagebaker/core/defs/defs.py +258 -0
- imagebaker/core/plugins/__init__.py +0 -0
- imagebaker/core/plugins/base_plugin.py +39 -0
- imagebaker/core/plugins/cosine_plugin.py +39 -0
- imagebaker/layers/__init__.py +3 -0
- imagebaker/layers/annotable_layer.py +847 -0
- imagebaker/layers/base_layer.py +724 -0
- imagebaker/layers/canvas_layer.py +1007 -0
- imagebaker/list_views/__init__.py +3 -0
- imagebaker/list_views/annotation_list.py +203 -0
- imagebaker/list_views/canvas_list.py +185 -0
- imagebaker/list_views/image_list.py +138 -0
- imagebaker/list_views/layer_list.py +390 -0
- imagebaker/list_views/layer_settings.py +219 -0
- imagebaker/models/__init__.py +0 -0
- imagebaker/models/base_model.py +150 -0
- imagebaker/tabs/__init__.py +2 -0
- imagebaker/tabs/baker_tab.py +496 -0
- imagebaker/tabs/layerify_tab.py +837 -0
- imagebaker/utils/__init__.py +0 -0
- imagebaker/utils/image.py +105 -0
- imagebaker/utils/state_utils.py +92 -0
- imagebaker/utils/transform_mask.py +107 -0
- imagebaker/window/__init__.py +1 -0
- imagebaker/window/app.py +136 -0
- imagebaker/window/main_window.py +181 -0
- imagebaker/workers/__init__.py +3 -0
- imagebaker/workers/baker_worker.py +247 -0
- imagebaker/workers/layerify_worker.py +91 -0
- imagebaker/workers/model_worker.py +54 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/METADATA +5 -5
- imagebaker-0.0.46.dist-info/RECORD +41 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/WHEEL +1 -1
- imagebaker-0.0.41.dist-info/RECORD +0 -7
- {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.46.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.46.dist-info}/entry_points.txt +0 -0
- {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
|
imagebaker/window/app.py
ADDED
@@ -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)
|