imagebaker 0.0.1__tar.gz → 0.0.3__tar.gz
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-0.0.1 → imagebaker-0.0.3}/PKG-INFO +1 -1
- imagebaker-0.0.3/imagebaker/core/configs/__init__.py +1 -0
- imagebaker-0.0.3/imagebaker/core/configs/configs.py +149 -0
- imagebaker-0.0.3/imagebaker/core/defs/__init__.py +1 -0
- imagebaker-0.0.3/imagebaker/core/defs/defs.py +239 -0
- imagebaker-0.0.3/imagebaker/core/plugins/__init__.py +0 -0
- imagebaker-0.0.3/imagebaker/core/plugins/base_plugin.py +39 -0
- imagebaker-0.0.3/imagebaker/core/plugins/cosine_plugin.py +39 -0
- imagebaker-0.0.3/imagebaker/models/__init__.py +0 -0
- imagebaker-0.0.3/imagebaker/utils/__init__.py +0 -0
- imagebaker-0.0.3/imagebaker/utils/image.py +95 -0
- imagebaker-0.0.3/imagebaker/utils/state_utils.py +64 -0
- imagebaker-0.0.3/imagebaker/utils/transform_mask.py +112 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/PKG-INFO +1 -1
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/SOURCES.txt +12 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/setup.py +1 -1
- {imagebaker-0.0.1 → imagebaker-0.0.3}/LICENSE +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/README.md +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/__init__.py +0 -0
- {imagebaker-0.0.1/imagebaker/models → imagebaker-0.0.3/imagebaker/core}/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/layers/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/layers/annotable_layer.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/layers/base_layer.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/layers/canvas_layer.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/annotation_list.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/canvas_list.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/image_list.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/layer_list.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/list_views/layer_settings.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/models/base_model.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/models/rtdetr_v2.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/models/sam_model.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/models/segmentation.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/tabs/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/tabs/baker_tab.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/tabs/layerify_tab.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/window/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/window/app.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/window/main_window.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/workers/__init__.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/workers/baker_worker.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/workers/layerfy_worker.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker/workers/model_worker.py +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/dependency_links.txt +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/entry_points.txt +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/requires.txt +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/imagebaker.egg-info/top_level.txt +0 -0
- {imagebaker-0.0.1 → imagebaker-0.0.3}/setup.cfg +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
from .configs import * # noqa
|
@@ -0,0 +1,149 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Tuple
|
3
|
+
|
4
|
+
from PySide6.QtCore import Qt
|
5
|
+
from PySide6.QtGui import QColor
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
|
8
|
+
from imagebaker.core.defs import Label, ModelType
|
9
|
+
|
10
|
+
|
11
|
+
class DrawConfig(BaseModel):
|
12
|
+
color: QColor = Field(default_factory=lambda: QColor(255, 255, 255))
|
13
|
+
point_size: int = 5
|
14
|
+
line_width: int = 5
|
15
|
+
control_point_size: int = 1.5
|
16
|
+
ellipse_size: int = 8
|
17
|
+
pen_alpha: int = 150
|
18
|
+
brush_alpha: int = 50
|
19
|
+
brush_fill_pattern: Qt.BrushStyle = Qt.BrushStyle.DiagCrossPattern
|
20
|
+
thumbnail_size: Tuple[int, int] = Field(default_factory=lambda: (50, 50))
|
21
|
+
background_color: QColor = Field(default_factory=lambda: QColor(0, 0, 0, 255))
|
22
|
+
label_font_size: int = 12
|
23
|
+
label_font_background_color: QColor = Field(
|
24
|
+
default_factory=lambda: QColor(0, 0, 0, 150)
|
25
|
+
)
|
26
|
+
handle_color: QColor = Field(default_factory=lambda: QColor(0, 255, 255, 150))
|
27
|
+
handle_width: int = 2
|
28
|
+
handle_point_size: int = 6
|
29
|
+
handle_edge_size: int = 4
|
30
|
+
|
31
|
+
button_width: int = 30
|
32
|
+
|
33
|
+
class Config:
|
34
|
+
arbitrary_types_allowed = True
|
35
|
+
|
36
|
+
|
37
|
+
class BaseConfig(BaseModel):
|
38
|
+
project_name: str = "ImageBaker"
|
39
|
+
version: str = "0.1.0"
|
40
|
+
project_dir: Path = Path(".")
|
41
|
+
|
42
|
+
is_debug: bool = True
|
43
|
+
deque_maxlen: int = 10
|
44
|
+
|
45
|
+
# drawing configs #
|
46
|
+
# ON SELECTION
|
47
|
+
selected_draw_config: DrawConfig = DrawConfig(
|
48
|
+
color=QColor(255, 0, 0),
|
49
|
+
point_size=5,
|
50
|
+
line_width=5,
|
51
|
+
ellipse_size=8,
|
52
|
+
pen_alpha=150,
|
53
|
+
brush_alpha=50,
|
54
|
+
thumbnail_size=(50, 50),
|
55
|
+
brush_fill_pattern=Qt.BrushStyle.CrossPattern,
|
56
|
+
)
|
57
|
+
normal_draw_config: DrawConfig = DrawConfig()
|
58
|
+
zoom_in_factor: float = 1.1
|
59
|
+
zoom_out_factor: float = 0.9
|
60
|
+
|
61
|
+
@property
|
62
|
+
def assets_folder(self):
|
63
|
+
return self.project_dir / "assets"
|
64
|
+
|
65
|
+
class Config:
|
66
|
+
arbitrary_types_allowed = True
|
67
|
+
|
68
|
+
|
69
|
+
class LayerConfig(BaseConfig):
|
70
|
+
show_labels: bool = True
|
71
|
+
show_annotations: bool = True
|
72
|
+
|
73
|
+
default_label: Label = Field(
|
74
|
+
default_factory=lambda: Label("Unlabeled", QColor(255, 255, 255))
|
75
|
+
)
|
76
|
+
predefined_labels: List[Label] = Field(
|
77
|
+
default_factory=lambda: [
|
78
|
+
Label("Unlabeled", QColor(255, 255, 255)),
|
79
|
+
Label("Label 1", QColor(255, 0, 0)),
|
80
|
+
Label("Label 2", QColor(0, 255, 0)),
|
81
|
+
Label("Label 3", QColor(0, 0, 255)),
|
82
|
+
Label("Custom", QColor(128, 128, 128)),
|
83
|
+
]
|
84
|
+
)
|
85
|
+
|
86
|
+
def get_label_color(self, label):
|
87
|
+
for lbl in self.predefined_labels:
|
88
|
+
if lbl.name == label:
|
89
|
+
return lbl.color
|
90
|
+
return self.default_label.color
|
91
|
+
|
92
|
+
|
93
|
+
class CanvasConfig(BaseConfig):
|
94
|
+
save_on_bake: bool = True
|
95
|
+
bake_timeout: float = -1.0
|
96
|
+
filename_format: str = "{project_name}_{timestamp}"
|
97
|
+
export_format: str = "png"
|
98
|
+
max_xpos: int = 1000
|
99
|
+
max_ypos: int = 1000
|
100
|
+
max_scale: int = 10
|
101
|
+
# whether to allow the use of sliders to change layer properties
|
102
|
+
allow_slider_usage: bool = True
|
103
|
+
|
104
|
+
write_annotations: bool = True
|
105
|
+
write_labels: bool = True
|
106
|
+
write_masks: bool = True
|
107
|
+
fps: int = 5
|
108
|
+
|
109
|
+
@property
|
110
|
+
def export_folder(self):
|
111
|
+
folder = self.project_dir / "assets" / "exports"
|
112
|
+
folder.mkdir(parents=True, exist_ok=True)
|
113
|
+
return folder
|
114
|
+
|
115
|
+
|
116
|
+
class CursorDef:
|
117
|
+
POINT_CURSOR: Qt.CursorShape = Qt.CrossCursor
|
118
|
+
POLYGON_CURSOR: Qt.CursorShape = Qt.CrossCursor
|
119
|
+
RECTANGLE_CURSOR: Qt.CursorShape = Qt.CrossCursor
|
120
|
+
IDLE_CURSOR: Qt.CursorShape = Qt.ArrowCursor
|
121
|
+
PAN_CURSOR: Qt.CursorShape = Qt.OpenHandCursor
|
122
|
+
ZOOM_IN_CURSOR: Qt.CursorShape = Qt.SizeFDiagCursor
|
123
|
+
ZOOM_OUT_CURSOR: Qt.CursorShape = Qt.SizeBDiagCursor
|
124
|
+
TRANSFORM_UPDOWN: Qt.CursorShape = Qt.SizeVerCursor
|
125
|
+
TRANSFORM_LEFTRIGHT: Qt.CursorShape = Qt.SizeHorCursor
|
126
|
+
TRANSFORM_ALL: Qt.CursorShape = Qt.SizeAllCursor
|
127
|
+
GRAB_CURSOR: Qt.CursorShape = Qt.OpenHandCursor
|
128
|
+
GRABBING_CURSOR: Qt.CursorShape = Qt.ClosedHandCursor
|
129
|
+
|
130
|
+
|
131
|
+
class DefaultModelConfig(BaseModel):
|
132
|
+
model_type: ModelType = ModelType.DETECTION
|
133
|
+
model_name: str = "Dummy Model"
|
134
|
+
model_description: str = "This is a dummy model"
|
135
|
+
model_version: str = "1.0"
|
136
|
+
model_author: str = "Anonymous"
|
137
|
+
model_license: str = "MIT"
|
138
|
+
input_size: Tuple[int, int] = (224, 224)
|
139
|
+
input_channels: int = 3
|
140
|
+
class_names: List[str] = ["class1", "class2", "class3"]
|
141
|
+
device: str = "cpu"
|
142
|
+
return_annotated_image: bool = False
|
143
|
+
|
144
|
+
@property
|
145
|
+
def num_classes(self):
|
146
|
+
return len(self.class_names)
|
147
|
+
|
148
|
+
class Config:
|
149
|
+
arbitrary_types_allowed = True
|
@@ -0,0 +1 @@
|
|
1
|
+
from .defs import * # noqa
|
@@ -0,0 +1,239 @@
|
|
1
|
+
from PySide6.QtCore import QPointF, QRectF
|
2
|
+
from PySide6.QtGui import QColor, QPolygonF
|
3
|
+
from PySide6.QtGui import QImage
|
4
|
+
|
5
|
+
from enum import Enum
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
import numpy as np
|
8
|
+
from datetime import datetime
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
|
13
|
+
class MouseMode(Enum):
|
14
|
+
IDLE = 0
|
15
|
+
POINT = 1
|
16
|
+
POLYGON = 2
|
17
|
+
RECTANGLE = 3
|
18
|
+
PAN = 4
|
19
|
+
ZOOM_IN = 5
|
20
|
+
ZOOM_OUT = 6
|
21
|
+
RESIZE = 7
|
22
|
+
RESIZE_HEIGHT = 8
|
23
|
+
RESIZE_WIDTH = 9
|
24
|
+
GRAB = 11
|
25
|
+
|
26
|
+
|
27
|
+
class ModelType(str, Enum):
|
28
|
+
DETECTION = "detection"
|
29
|
+
SEGMENTATION = "segmentation"
|
30
|
+
CLASSIFICATION = "classification"
|
31
|
+
PROMPT = "prompt"
|
32
|
+
|
33
|
+
|
34
|
+
class PredictionResult(BaseModel):
|
35
|
+
class_name: str = None
|
36
|
+
class_id: int = None
|
37
|
+
score: float = None
|
38
|
+
rectangle: list[int] | None = None
|
39
|
+
mask: np.ndarray | None = None
|
40
|
+
keypoints: list[list[int, int]] | None = None
|
41
|
+
polygon: np.ndarray | None = None
|
42
|
+
prompt: str | None = None
|
43
|
+
annotated_image: np.ndarray | None = None
|
44
|
+
annotation_time: str | None = None
|
45
|
+
|
46
|
+
class Config:
|
47
|
+
arbitrary_types_allowed = True
|
48
|
+
|
49
|
+
|
50
|
+
@dataclass
|
51
|
+
class LayerState:
|
52
|
+
layer_id: str = ""
|
53
|
+
state_step: int = 0
|
54
|
+
layer_name: str = "Layer"
|
55
|
+
opacity: float = 255.00
|
56
|
+
position: QPointF = field(default_factory=lambda: QPointF(0, 0))
|
57
|
+
rotation: float = 0.00
|
58
|
+
scale: float = 1.00
|
59
|
+
scale_x: float = 1.00
|
60
|
+
scale_y: float = 1.00
|
61
|
+
transform_origin: QPointF = field(default_factory=lambda: QPointF(0.0, 0.0))
|
62
|
+
order: int = 0
|
63
|
+
visible: bool = True
|
64
|
+
allow_annotation_export: bool = True
|
65
|
+
playing: bool = False
|
66
|
+
selected: bool = False
|
67
|
+
is_annotable: bool = True
|
68
|
+
status: str = "Ready"
|
69
|
+
|
70
|
+
def copy(self):
|
71
|
+
return LayerState(
|
72
|
+
layer_id=self.layer_id,
|
73
|
+
layer_name=self.layer_name,
|
74
|
+
opacity=self.opacity,
|
75
|
+
position=QPointF(self.position.x(), self.position.y()),
|
76
|
+
rotation=self.rotation,
|
77
|
+
scale=self.scale,
|
78
|
+
scale_x=self.scale_x,
|
79
|
+
scale_y=self.scale_y,
|
80
|
+
transform_origin=QPointF(
|
81
|
+
self.transform_origin.x(), self.transform_origin.y()
|
82
|
+
),
|
83
|
+
order=self.order,
|
84
|
+
visible=self.visible,
|
85
|
+
allow_annotation_export=self.allow_annotation_export,
|
86
|
+
playing=self.playing,
|
87
|
+
selected=self.selected,
|
88
|
+
is_annotable=self.is_annotable,
|
89
|
+
status=self.status,
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
@dataclass
|
94
|
+
class Label:
|
95
|
+
name: str = "Unlabeled"
|
96
|
+
color: QColor = field(default_factory=lambda: QColor(255, 255, 255))
|
97
|
+
|
98
|
+
|
99
|
+
@dataclass
|
100
|
+
class Annotation:
|
101
|
+
annotation_id: int
|
102
|
+
label: str
|
103
|
+
color: QColor = field(default_factory=lambda: QColor(255, 255, 255))
|
104
|
+
points: list[QPointF] = field(default_factory=list)
|
105
|
+
# [x, y, width, height]
|
106
|
+
rectangle: QRectF = None
|
107
|
+
polygon: QPolygonF = None
|
108
|
+
is_complete: bool = False
|
109
|
+
selected: bool = False
|
110
|
+
score: float = None
|
111
|
+
annotator: str = "User"
|
112
|
+
annotation_time: str = field(
|
113
|
+
default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
114
|
+
)
|
115
|
+
visible: bool = True
|
116
|
+
file_path: Path = field(default_factory=lambda: Path("Runtime"))
|
117
|
+
is_model_generated: bool = False
|
118
|
+
model_name: str = None
|
119
|
+
|
120
|
+
def copy(self):
|
121
|
+
ann = Annotation(
|
122
|
+
annotation_id=self.annotation_id,
|
123
|
+
label=self.label,
|
124
|
+
color=QColor(self.color.red(), self.color.green(), self.color.blue()),
|
125
|
+
points=[QPointF(p) for p in self.points],
|
126
|
+
rectangle=QRectF(self.rectangle) if self.rectangle else None,
|
127
|
+
polygon=QPolygonF(self.polygon) if self.polygon else None,
|
128
|
+
is_complete=self.is_complete,
|
129
|
+
selected=self.selected,
|
130
|
+
score=self.score,
|
131
|
+
annotator=self.annotator,
|
132
|
+
annotation_time=self.annotation_time,
|
133
|
+
visible=self.visible,
|
134
|
+
file_path=self.file_path,
|
135
|
+
is_model_generated=self.is_model_generated,
|
136
|
+
model_name=self.model_name,
|
137
|
+
)
|
138
|
+
ann.is_selected = False
|
139
|
+
return ann
|
140
|
+
|
141
|
+
@property
|
142
|
+
def name(self):
|
143
|
+
return f"{self.annotation_id} {self.label}"
|
144
|
+
|
145
|
+
@staticmethod
|
146
|
+
def save_as_json(annotations: list["Annotation"], path: str):
|
147
|
+
import json
|
148
|
+
|
149
|
+
annotations_dict = []
|
150
|
+
for annotation in annotations:
|
151
|
+
rectangle = None
|
152
|
+
if annotation.rectangle:
|
153
|
+
rectangle = [
|
154
|
+
annotation.rectangle.x(),
|
155
|
+
annotation.rectangle.y(),
|
156
|
+
annotation.rectangle.width(),
|
157
|
+
annotation.rectangle.height(),
|
158
|
+
]
|
159
|
+
polygon = None
|
160
|
+
if annotation.polygon:
|
161
|
+
polygon = [[p.x(), p.y()] for p in annotation.polygon]
|
162
|
+
points = None
|
163
|
+
if annotation.points:
|
164
|
+
points = [[p.x(), p.y()] for p in annotation.points]
|
165
|
+
data = {
|
166
|
+
"annotation_id": annotation.annotation_id,
|
167
|
+
"label": annotation.label,
|
168
|
+
"color": annotation.color.getRgb(),
|
169
|
+
"points": points,
|
170
|
+
"rectangle": rectangle,
|
171
|
+
"polygon": polygon,
|
172
|
+
"is_complete": annotation.is_complete,
|
173
|
+
"selected": annotation.selected,
|
174
|
+
"score": annotation.score,
|
175
|
+
"annotator": annotation.annotator,
|
176
|
+
"annotation_time": annotation.annotation_time,
|
177
|
+
"visible": annotation.visible,
|
178
|
+
"file_path": str(annotation.file_path),
|
179
|
+
"is_model_generated": annotation.is_model_generated,
|
180
|
+
"model_name": annotation.model_name,
|
181
|
+
}
|
182
|
+
annotations_dict.append(data)
|
183
|
+
|
184
|
+
with open(path, "w") as f:
|
185
|
+
json.dump(annotations_dict, f, indent=4)
|
186
|
+
|
187
|
+
@staticmethod
|
188
|
+
def load_from_json(path: str):
|
189
|
+
import json
|
190
|
+
|
191
|
+
with open(path, "r") as f:
|
192
|
+
data = json.load(f)
|
193
|
+
|
194
|
+
annotations = []
|
195
|
+
for d in data:
|
196
|
+
annotation = Annotation(
|
197
|
+
annotation_id=d["annotation_id"],
|
198
|
+
label=d["label"],
|
199
|
+
color=QColor(*d["color"]),
|
200
|
+
is_complete=d.get("is_complete", False),
|
201
|
+
selected=d.get("selected", False),
|
202
|
+
score=d.get("score", None),
|
203
|
+
annotator=d.get("annotator", None),
|
204
|
+
annotation_time=d.get(
|
205
|
+
"annotation_time", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
206
|
+
),
|
207
|
+
visible=d.get("visible", True),
|
208
|
+
file_path=Path(d.get("file_path", "Runtime")),
|
209
|
+
is_model_generated=d.get("is_model_generated", False),
|
210
|
+
model_name=d.get("model_name", None),
|
211
|
+
)
|
212
|
+
|
213
|
+
# Handle points safely
|
214
|
+
points_data = d.get("points")
|
215
|
+
if points_data:
|
216
|
+
annotation.points = [QPointF(*p) for p in points_data]
|
217
|
+
|
218
|
+
# Handle rectangle safely
|
219
|
+
rect_data = d.get("rectangle")
|
220
|
+
if rect_data:
|
221
|
+
annotation.rectangle = QRectF(*rect_data)
|
222
|
+
|
223
|
+
# Handle polygon safely
|
224
|
+
polygon_data = d.get("polygon")
|
225
|
+
if polygon_data:
|
226
|
+
annotation.polygon = QPolygonF([QPointF(*p) for p in polygon_data])
|
227
|
+
|
228
|
+
annotations.append(annotation)
|
229
|
+
|
230
|
+
return annotations
|
231
|
+
|
232
|
+
|
233
|
+
@dataclass
|
234
|
+
class BakingResult:
|
235
|
+
filename: Path
|
236
|
+
image: QImage = field(default=None)
|
237
|
+
masks: list[np.ndarray] = field(default_factory=list)
|
238
|
+
mask_names: list[str] = field(default_factory=list)
|
239
|
+
annotations: list[Annotation] = field(default_factory=list)
|
File without changes
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from imagebaker.core.defs import LayerState
|
3
|
+
from imagebaker import logger
|
4
|
+
|
5
|
+
|
6
|
+
class BasePlugin(ABC):
|
7
|
+
|
8
|
+
def __init__(self, layer_state: LayerState, final_step: int = -1):
|
9
|
+
self.initial_layer_state = layer_state
|
10
|
+
self.final_step = final_step
|
11
|
+
self.current_step = 0
|
12
|
+
|
13
|
+
def __str__(self):
|
14
|
+
return self.__class__.__name
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def compute_step(self, step: int):
|
18
|
+
"""
|
19
|
+
Compute the step based on the given step.
|
20
|
+
|
21
|
+
:param step: The step to compute the step by.
|
22
|
+
"""
|
23
|
+
pass
|
24
|
+
|
25
|
+
def update(self, step: int = 1):
|
26
|
+
"""
|
27
|
+
Returns the updated state after passed step.
|
28
|
+
|
29
|
+
:param step: The step to update the state by.
|
30
|
+
"""
|
31
|
+
if (
|
32
|
+
self.final_step != -1
|
33
|
+
and step > self.final_step
|
34
|
+
or self.current_step >= self.final_step
|
35
|
+
):
|
36
|
+
logger.info(f"Final step reached for {self}. Returning last step.")
|
37
|
+
step = self.final_step
|
38
|
+
self.compute_step(step)
|
39
|
+
self.current_step += step
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from imagebaker.core.plugins.base_plugin import BasePlugin
|
2
|
+
from imagebaker.core.defs import LayerState
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
from PySide6.QtCore import QPointF
|
6
|
+
|
7
|
+
|
8
|
+
class CosinePlugin(BasePlugin):
|
9
|
+
"""
|
10
|
+
Cosine Plugin implementation.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self, layer_state: LayerState, amplitude=50, frequency=0.1):
|
14
|
+
"""
|
15
|
+
Initialize the CosinePlugin.
|
16
|
+
|
17
|
+
:param layer_state: The LayerState to modify.
|
18
|
+
:param amplitude: The amplitude of the cosine wave (how far it moves).
|
19
|
+
:param frequency: The frequency of the cosine wave (how fast it oscillates).
|
20
|
+
"""
|
21
|
+
super().__init__(layer_state)
|
22
|
+
self.amplitude = amplitude
|
23
|
+
self.frequency = frequency
|
24
|
+
|
25
|
+
def compute_step(self, step):
|
26
|
+
"""
|
27
|
+
Update the x and y positions of the LayerState based on a cosine curve.
|
28
|
+
|
29
|
+
:param step: The step to compute the cosine value for.
|
30
|
+
"""
|
31
|
+
# Compute the new x position based on the cosine curve
|
32
|
+
layer_state = LayerState()
|
33
|
+
layer_state.position = QPointF(
|
34
|
+
self.initial_layer_state.position.x()
|
35
|
+
+ self.amplitude * np.cos(self.frequency * step),
|
36
|
+
self.initial_layer_state.position.y(),
|
37
|
+
)
|
38
|
+
|
39
|
+
return layer_state
|
File without changes
|
File without changes
|
@@ -0,0 +1,95 @@
|
|
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]):
|
42
|
+
for i, ann in enumerate(annotations):
|
43
|
+
if ann.rectangle:
|
44
|
+
cv2.rectangle(
|
45
|
+
image,
|
46
|
+
(int(ann.rectangle.x()), int(ann.rectangle.y())),
|
47
|
+
(
|
48
|
+
int(ann.rectangle.x() + ann.rectangle.width()),
|
49
|
+
int(ann.rectangle.y() + ann.rectangle.height()),
|
50
|
+
),
|
51
|
+
(0, 255, 0),
|
52
|
+
2,
|
53
|
+
)
|
54
|
+
rect_center = ann.rectangle.center()
|
55
|
+
|
56
|
+
cv2.putText(
|
57
|
+
image,
|
58
|
+
ann.label,
|
59
|
+
(int(rect_center.x()), int(rect_center.y())),
|
60
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
61
|
+
1,
|
62
|
+
(0, 255, 0),
|
63
|
+
2,
|
64
|
+
)
|
65
|
+
elif ann.polygon:
|
66
|
+
cv2.polylines(
|
67
|
+
image,
|
68
|
+
[np.array([[int(p.x()), int(p.y())] for p in ann.polygon])],
|
69
|
+
True,
|
70
|
+
(0, 255, 0),
|
71
|
+
2,
|
72
|
+
)
|
73
|
+
polygon_center = ann.polygon.boundingRect().center()
|
74
|
+
cv2.putText(
|
75
|
+
image,
|
76
|
+
ann.label,
|
77
|
+
(int(polygon_center.x()), int(polygon_center.y())),
|
78
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
79
|
+
1,
|
80
|
+
(0, 255, 0),
|
81
|
+
2,
|
82
|
+
)
|
83
|
+
elif ann.points:
|
84
|
+
for p in ann.points:
|
85
|
+
cv2.circle(image, (int(p.x()), int(p.y())), 5, (0, 255, 0), -1)
|
86
|
+
cv2.putText(
|
87
|
+
image,
|
88
|
+
ann.label,
|
89
|
+
(int(ann.points[0].x()), int(ann.points[0].y())),
|
90
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
91
|
+
1,
|
92
|
+
(0, 255, 0),
|
93
|
+
2,
|
94
|
+
)
|
95
|
+
return image
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from imagebaker.core.defs import LayerState
|
2
|
+
from PySide6.QtCore import QPointF
|
3
|
+
|
4
|
+
|
5
|
+
def calculate_intermediate_states(previous_state, current_state, steps: int):
|
6
|
+
"""
|
7
|
+
Calculate intermediate states between previous_state and current_state for a layer.
|
8
|
+
Append the current_state to the list of states after calculating intermediates.
|
9
|
+
"""
|
10
|
+
if not previous_state or not current_state:
|
11
|
+
return [current_state] # If no previous state, return only the current state
|
12
|
+
|
13
|
+
intermediate_states = []
|
14
|
+
for i in range(steps):
|
15
|
+
# Interpolate attributes between previous_state and current_state
|
16
|
+
interpolated_state = LayerState(
|
17
|
+
layer_id=current_state.layer_id,
|
18
|
+
layer_name=current_state.layer_name,
|
19
|
+
opacity=previous_state.opacity
|
20
|
+
+ (current_state.opacity - previous_state.opacity) * (i / steps),
|
21
|
+
position=QPointF(
|
22
|
+
previous_state.position.x()
|
23
|
+
+ (current_state.position.x() - previous_state.position.x())
|
24
|
+
* (i / steps),
|
25
|
+
previous_state.position.y()
|
26
|
+
+ (current_state.position.y() - previous_state.position.y())
|
27
|
+
* (i / steps),
|
28
|
+
),
|
29
|
+
rotation=previous_state.rotation
|
30
|
+
+ (current_state.rotation - previous_state.rotation) * (i / steps),
|
31
|
+
scale=previous_state.scale
|
32
|
+
+ (current_state.scale - previous_state.scale) * (i / steps),
|
33
|
+
scale_x=previous_state.scale_x
|
34
|
+
+ (current_state.scale_x - previous_state.scale_x) * (i / steps),
|
35
|
+
scale_y=previous_state.scale_y
|
36
|
+
+ (current_state.scale_y - previous_state.scale_y) * (i / steps),
|
37
|
+
transform_origin=QPointF(
|
38
|
+
previous_state.transform_origin.x()
|
39
|
+
+ (
|
40
|
+
current_state.transform_origin.x()
|
41
|
+
- previous_state.transform_origin.x()
|
42
|
+
)
|
43
|
+
* (i / steps),
|
44
|
+
previous_state.transform_origin.y()
|
45
|
+
+ (
|
46
|
+
current_state.transform_origin.y()
|
47
|
+
- previous_state.transform_origin.y()
|
48
|
+
)
|
49
|
+
* (i / steps),
|
50
|
+
),
|
51
|
+
order=current_state.order,
|
52
|
+
visible=current_state.visible,
|
53
|
+
allow_annotation_export=current_state.allow_annotation_export,
|
54
|
+
playing=current_state.playing,
|
55
|
+
selected=current_state.selected,
|
56
|
+
is_annotable=current_state.is_annotable,
|
57
|
+
status=current_state.status,
|
58
|
+
)
|
59
|
+
intermediate_states.append(interpolated_state)
|
60
|
+
|
61
|
+
# Append the current state as the final state
|
62
|
+
intermediate_states.append(current_state)
|
63
|
+
|
64
|
+
return intermediate_states
|
@@ -0,0 +1,112 @@
|
|
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
|
+
import cv2
|
71
|
+
import numpy as np
|
72
|
+
from typing import List, Tuple
|
73
|
+
|
74
|
+
|
75
|
+
def mask_to_rectangles(
|
76
|
+
mask: np.ndarray,
|
77
|
+
merge_rectangles: bool = False,
|
78
|
+
merge_threshold: int = 1,
|
79
|
+
merge_epsilon: float = 0.5,
|
80
|
+
) -> List[Tuple[int, int, int, int]]:
|
81
|
+
"""
|
82
|
+
Convert a binary mask to a list of rectangles.
|
83
|
+
Each rectangle is a tuple of (x, y, w, h).
|
84
|
+
|
85
|
+
Args:
|
86
|
+
mask (np.ndarray): Binary mask (0 or 255).
|
87
|
+
merge_rectangles (bool): If True, merges overlapping or nearby rectangles.
|
88
|
+
merge_threshold (int): Min number of rectangles to merge into one.
|
89
|
+
merge_epsilon (float): Controls how close rectangles must be to merge (0.0 to 1.0).
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
List[Tuple[int, int, int, int]]: List of rectangles, each as (x, y, w, h).
|
93
|
+
"""
|
94
|
+
contours, _ = cv2.findContours(
|
95
|
+
mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
96
|
+
)
|
97
|
+
|
98
|
+
rectangles = []
|
99
|
+
for contour in contours:
|
100
|
+
x, y, w, h = cv2.boundingRect(contour)
|
101
|
+
rectangles.append((x, y, w, h))
|
102
|
+
|
103
|
+
if merge_rectangles and len(rectangles) > 1:
|
104
|
+
# Convert rectangles to the format expected by groupRectangles
|
105
|
+
rects = np.array(rectangles)
|
106
|
+
# groupRectangles requires [x, y, w, h] format
|
107
|
+
grouped_rects, _ = cv2.groupRectangles(
|
108
|
+
rects.tolist(), merge_threshold, merge_epsilon
|
109
|
+
)
|
110
|
+
rectangles = [tuple(map(int, rect)) for rect in grouped_rects]
|
111
|
+
|
112
|
+
return rectangles
|
@@ -8,6 +8,14 @@ imagebaker.egg-info/dependency_links.txt
|
|
8
8
|
imagebaker.egg-info/entry_points.txt
|
9
9
|
imagebaker.egg-info/requires.txt
|
10
10
|
imagebaker.egg-info/top_level.txt
|
11
|
+
imagebaker/core/__init__.py
|
12
|
+
imagebaker/core/configs/__init__.py
|
13
|
+
imagebaker/core/configs/configs.py
|
14
|
+
imagebaker/core/defs/__init__.py
|
15
|
+
imagebaker/core/defs/defs.py
|
16
|
+
imagebaker/core/plugins/__init__.py
|
17
|
+
imagebaker/core/plugins/base_plugin.py
|
18
|
+
imagebaker/core/plugins/cosine_plugin.py
|
11
19
|
imagebaker/layers/__init__.py
|
12
20
|
imagebaker/layers/annotable_layer.py
|
13
21
|
imagebaker/layers/base_layer.py
|
@@ -26,6 +34,10 @@ imagebaker/models/segmentation.py
|
|
26
34
|
imagebaker/tabs/__init__.py
|
27
35
|
imagebaker/tabs/baker_tab.py
|
28
36
|
imagebaker/tabs/layerify_tab.py
|
37
|
+
imagebaker/utils/__init__.py
|
38
|
+
imagebaker/utils/image.py
|
39
|
+
imagebaker/utils/state_utils.py
|
40
|
+
imagebaker/utils/transform_mask.py
|
29
41
|
imagebaker/window/__init__.py
|
30
42
|
imagebaker/window/app.py
|
31
43
|
imagebaker/window/main_window.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|