imagebaker 0.0.49__tar.gz → 0.0.50__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.49 → imagebaker-0.0.50}/PKG-INFO +2 -2
- {imagebaker-0.0.49 → imagebaker-0.0.50}/README.md +1 -1
- {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/loaded_models.py +20 -5
- {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/rtdetr_v2.py +18 -112
- {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/sam_model.py +14 -172
- imagebaker-0.0.50/examples/segmentation.py +152 -0
- imagebaker-0.0.50/imagebaker/__init__.py +9 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/configs/configs.py +1 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/defs/defs.py +4 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/annotable_layer.py +32 -20
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/base_layer.py +132 -1
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/canvas_layer.py +52 -10
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/layer_settings.py +31 -2
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/models/base_model.py +1 -1
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/baker_tab.py +2 -9
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/layerify_tab.py +1 -0
- imagebaker-0.0.50/imagebaker/utils/__init__.py +3 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/state_utils.py +5 -1
- imagebaker-0.0.50/imagebaker/utils/utils.py +26 -0
- imagebaker-0.0.50/imagebaker/utils/vis.py +174 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/baker_worker.py +13 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/PKG-INFO +2 -2
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/SOURCES.txt +2 -0
- imagebaker-0.0.49/examples/segmentation.py +0 -288
- imagebaker-0.0.49/imagebaker/__init__.py +0 -5
- imagebaker-0.0.49/imagebaker/utils/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/.github/workflows/black-formatter.yml +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/.github/workflows/pypi.yml +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/.gitignore +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/CHANGELOG.md +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/LICENSE +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/TODO.md +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/annotated_veg_smiley.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/annotation_page.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/baker_page.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/drawing.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/options.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo.gif +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/desk.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/README.md +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/android-chrome-192x192.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/android-chrome-512x512.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/apple-touch-icon.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon-16x16.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon-32x32.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon.ico +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/site.webmanifest +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/me.jpg +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/pen.png +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/veg_smiley.jpg +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/docs/api-reference.md +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/docs/index.md +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/app.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/example_config.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/experiments/expt.ipynb +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/configs/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/defs/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/base_plugin.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/cosine_plugin.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/annotation_list.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/canvas_list.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/image_list.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/layer_list.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/models/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/image.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/transform_mask.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/app.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/main_window.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/__init__.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/layerify_worker.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/model_worker.py +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/dependency_links.txt +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/entry_points.txt +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/requires.txt +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/top_level.txt +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/mkdocs.yml +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/requirements.txt +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/setup.cfg +0 -0
- {imagebaker-0.0.49 → imagebaker-0.0.50}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: imagebaker
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.50
|
4
4
|
Summary: A package for baking images.
|
5
5
|
Home-page: https://github.com/q-viper/Image-Baker
|
6
6
|
Author: Ramkrishna Acharya
|
@@ -21,7 +21,7 @@ License-File: LICENSE
|
|
21
21
|

|
22
22
|
<!--  -->
|
23
23
|

|
24
|
-
|
24
|
+
[](https://pypi.org/imagebaker/)
|
25
25
|
|
26
26
|
<p align="center">
|
27
27
|
<img src="assets/demo.gif" alt="Centered Demo" />
|
@@ -5,7 +5,7 @@
|
|
5
5
|

|
6
6
|
<!--  -->
|
7
7
|

|
8
|
-
|
8
|
+
[](https://pypi.org/imagebaker/)
|
9
9
|
|
10
10
|
<p align="center">
|
11
11
|
<img src="assets/demo.gif" alt="Centered Demo" />
|
@@ -19,6 +19,7 @@ from imagebaker import logger
|
|
19
19
|
# YoloSegmentationModel,
|
20
20
|
# YoloSegmentationModelConfig,
|
21
21
|
# )
|
22
|
+
|
22
23
|
# from examples.sam_model import SegmentAnythingModel, SAMModelConfig
|
23
24
|
|
24
25
|
|
@@ -38,14 +39,28 @@ class DetectionModel(BaseDetectionModel):
|
|
38
39
|
return [get_dummy_prediction_result(self.config.model_type)]
|
39
40
|
|
40
41
|
|
41
|
-
|
42
|
+
return_annotated_image = True
|
43
|
+
# detector = RTDetrDetectionModel(
|
44
|
+
# RTDetrModelConfig(return_annotated_image=return_annotated_image)
|
45
|
+
# )
|
42
46
|
|
43
47
|
# classification = ClassificationModel(
|
44
|
-
# DefaultModelConfig(
|
48
|
+
# DefaultModelConfig(
|
49
|
+
# model_type=ModelType.CLASSIFICATION,
|
50
|
+
# return_annotated_image=return_annotated_image,
|
51
|
+
# )
|
52
|
+
# )
|
53
|
+
# segmentation = YoloSegmentationModel(
|
54
|
+
# YoloSegmentationModelConfig(return_annotated_image=return_annotated_image)
|
45
55
|
# )
|
46
|
-
#
|
47
|
-
#
|
48
|
-
|
56
|
+
# prompt = SegmentAnythingModel(
|
57
|
+
# SAMModelConfig(return_annotated_image=return_annotated_image)
|
58
|
+
# )
|
59
|
+
dummy_detector = DetectionModel(
|
60
|
+
DefaultModelConfig(
|
61
|
+
model_type=ModelType.DETECTION, return_annotated_image=return_annotated_image
|
62
|
+
)
|
63
|
+
)
|
49
64
|
|
50
65
|
|
51
66
|
LOADED_MODELS = {
|
@@ -7,6 +7,8 @@ from transformers import RTDetrV2ForObjectDetection, RTDetrImageProcessor
|
|
7
7
|
|
8
8
|
from loguru import logger
|
9
9
|
import time
|
10
|
+
from imagebaker.utils.vis import annotate_detection
|
11
|
+
from imagebaker.utils import generate_color_map
|
10
12
|
|
11
13
|
# Import your base classes
|
12
14
|
from imagebaker.models.base_model import (
|
@@ -15,6 +17,7 @@ from imagebaker.models.base_model import (
|
|
15
17
|
ModelType,
|
16
18
|
PredictionResult,
|
17
19
|
)
|
20
|
+
from imagebaker.utils import generate_color_map
|
18
21
|
|
19
22
|
|
20
23
|
class RTDetrModelConfig(DefaultModelConfig):
|
@@ -61,39 +64,15 @@ class RTDetrDetectionModel(BaseDetectionModel):
|
|
61
64
|
|
62
65
|
# Generate color map for annotations if not provided
|
63
66
|
if not self.config.color_map:
|
64
|
-
self.
|
67
|
+
color_map = generate_color_map(len(self.config.class_names))
|
68
|
+
self.config.color_map = {
|
69
|
+
class_name: color_map[i]
|
70
|
+
for i, class_name in enumerate(self.config.class_names)
|
71
|
+
}
|
65
72
|
|
66
73
|
logger.info(f"Loaded model with {len(self.config.class_names)} classes")
|
67
74
|
logger.info(f"Model running on {self.config.device}")
|
68
75
|
|
69
|
-
def generate_color_map(self):
|
70
|
-
"""Generate a color map for the classes"""
|
71
|
-
num_classes = len(self.config.class_names)
|
72
|
-
np.random.seed(42) # For reproducible colors
|
73
|
-
|
74
|
-
colors = {}
|
75
|
-
for i, class_name in enumerate(self.config.class_names):
|
76
|
-
# Generate distinct colors with good visibility
|
77
|
-
# Using HSV color space for better distribution
|
78
|
-
hue = i / num_classes
|
79
|
-
saturation = 0.8 + np.random.random() * 0.2
|
80
|
-
value = 0.8 + np.random.random() * 0.2
|
81
|
-
|
82
|
-
# Convert HSV to BGR (OpenCV uses BGR)
|
83
|
-
hsv_color = np.array(
|
84
|
-
[[[hue * 180, saturation * 255, value * 255]]], dtype=np.uint8
|
85
|
-
)
|
86
|
-
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
87
|
-
|
88
|
-
# Store as (B, G, R) tuple
|
89
|
-
colors[class_name] = (
|
90
|
-
int(bgr_color[0]),
|
91
|
-
int(bgr_color[1]),
|
92
|
-
int(bgr_color[2]),
|
93
|
-
)
|
94
|
-
|
95
|
-
self.config.color_map = colors
|
96
|
-
|
97
76
|
def preprocess(self, image: np.ndarray):
|
98
77
|
"""Convert numpy array to PIL Image for the RTDetr processor"""
|
99
78
|
self._original_image = (
|
@@ -136,77 +115,6 @@ class RTDetrDetectionModel(BaseDetectionModel):
|
|
136
115
|
|
137
116
|
return results[0] # Return the first (and only) result
|
138
117
|
|
139
|
-
def annotate_image(
|
140
|
-
self, image: np.ndarray, results: List[PredictionResult]
|
141
|
-
) -> np.ndarray:
|
142
|
-
"""
|
143
|
-
Draw bounding boxes and labels on the image
|
144
|
-
|
145
|
-
Args:
|
146
|
-
image: The original image as a numpy array
|
147
|
-
results: List of PredictionResult objects
|
148
|
-
|
149
|
-
Returns:
|
150
|
-
Annotated image as a numpy array
|
151
|
-
"""
|
152
|
-
annotated_image = image.copy()
|
153
|
-
|
154
|
-
for result in results:
|
155
|
-
# Extract data from result
|
156
|
-
box = result.rectangle # [x1, y1, x2, y2]
|
157
|
-
score = result.score
|
158
|
-
class_name = result.class_name
|
159
|
-
|
160
|
-
if not box:
|
161
|
-
continue
|
162
|
-
|
163
|
-
# Get color for this class
|
164
|
-
color = self.config.color_map.get(
|
165
|
-
class_name, (0, 255, 0)
|
166
|
-
) # Default to green if not found
|
167
|
-
|
168
|
-
# Draw bounding box
|
169
|
-
cv2.rectangle(
|
170
|
-
annotated_image,
|
171
|
-
(box[0], box[1]),
|
172
|
-
(box[2], box[3]),
|
173
|
-
color,
|
174
|
-
self.config.box_thickness,
|
175
|
-
)
|
176
|
-
|
177
|
-
# Prepare label text with class name and score
|
178
|
-
label_text = f"{class_name}: {score:.2f}"
|
179
|
-
|
180
|
-
# Calculate text size to create background rectangle
|
181
|
-
(text_width, text_height), baseline = cv2.getTextSize(
|
182
|
-
label_text,
|
183
|
-
self.config.font_face,
|
184
|
-
self.config.text_scale,
|
185
|
-
self.config.text_thickness,
|
186
|
-
)
|
187
|
-
|
188
|
-
# Draw text background
|
189
|
-
cv2.rectangle(
|
190
|
-
annotated_image,
|
191
|
-
(box[0], box[1] - text_height - 5),
|
192
|
-
(box[0] + text_width, box[1]),
|
193
|
-
color,
|
194
|
-
-1, # Fill the rectangle
|
195
|
-
)
|
196
|
-
|
197
|
-
# Draw text
|
198
|
-
cv2.putText(
|
199
|
-
annotated_image,
|
200
|
-
label_text,
|
201
|
-
(box[0], box[1] - 5),
|
202
|
-
self.config.font_face,
|
203
|
-
self.config.text_scale,
|
204
|
-
(255, 255, 255), # White text
|
205
|
-
self.config.text_thickness,
|
206
|
-
)
|
207
|
-
|
208
|
-
return annotated_image
|
209
|
-
|
210
118
|
def postprocess(self, output) -> List[PredictionResult]:
|
211
119
|
"""Convert model output to PredictionResult objects"""
|
212
120
|
results = []
|
@@ -236,19 +144,17 @@ class RTDetrDetectionModel(BaseDetectionModel):
|
|
236
144
|
rectangle=[x, y, w, h],
|
237
145
|
annotation_time=f"{annotation_time:.6f}",
|
238
146
|
)
|
147
|
+
if self.config.return_annotated_image:
|
148
|
+
result.annotated_image = annotate_detection(
|
149
|
+
self._original_image,
|
150
|
+
[result],
|
151
|
+
box_thickness=self.config.box_thickness,
|
152
|
+
text_thickness=self.config.text_thickness,
|
153
|
+
text_scale=self.config.text_scale,
|
154
|
+
font_face=self.config.font_face,
|
155
|
+
color_map=self.config.color_map,
|
156
|
+
)
|
239
157
|
|
240
158
|
results.append(result)
|
241
159
|
|
242
|
-
# If needed, add annotated image
|
243
|
-
if (
|
244
|
-
self.config.return_annotated_image
|
245
|
-
and len(results) > 0
|
246
|
-
and hasattr(self, "_original_image")
|
247
|
-
):
|
248
|
-
annotated_image = self.annotate_image(self._original_image, results)
|
249
|
-
|
250
|
-
# Update all results with the same annotated image
|
251
|
-
for result in results:
|
252
|
-
result.annotated_image = annotated_image
|
253
|
-
|
254
160
|
return results
|
@@ -16,6 +16,7 @@ from imagebaker.models.base_model import (
|
|
16
16
|
ModelType,
|
17
17
|
PredictionResult,
|
18
18
|
)
|
19
|
+
from imagebaker.utils import generate_color_map, mask_to_polygons, annotate_segmentation
|
19
20
|
|
20
21
|
|
21
22
|
class SAMModelConfig(DefaultModelConfig):
|
@@ -30,7 +31,7 @@ class SAMModelConfig(DefaultModelConfig):
|
|
30
31
|
)
|
31
32
|
confidence_threshold: float = 0.5
|
32
33
|
device: str = "cuda" if torch.cuda.is_available() else "cpu"
|
33
|
-
return_annotated_image: bool =
|
34
|
+
return_annotated_image: bool = False
|
34
35
|
|
35
36
|
# Segmentation specific settings
|
36
37
|
points_per_side: int = 32 # Grid size for automatic point generation
|
@@ -78,33 +79,10 @@ class SegmentAnythingModel(BasePromptModel):
|
|
78
79
|
|
79
80
|
# Generate color map for annotations if not provided
|
80
81
|
if not self.config.color_map:
|
81
|
-
self.generate_color_map()
|
82
|
+
self.config.color_map = generate_color_map()
|
82
83
|
|
83
84
|
logger.info(f"Model running on {self.config.device}")
|
84
85
|
|
85
|
-
def generate_color_map(self, num_colors: int = 20):
|
86
|
-
"""Generate a color map for the segmentation masks"""
|
87
|
-
np.random.seed(42) # For reproducible colors
|
88
|
-
|
89
|
-
colors = {}
|
90
|
-
for i in range(num_colors):
|
91
|
-
# Generate distinct colors with good visibility
|
92
|
-
# Using HSV color space for better distribution
|
93
|
-
hue = i / num_colors
|
94
|
-
saturation = 0.8 + np.random.random() * 0.2
|
95
|
-
value = 0.8 + np.random.random() * 0.2
|
96
|
-
|
97
|
-
# Convert HSV to BGR (OpenCV uses BGR)
|
98
|
-
hsv_color = np.array(
|
99
|
-
[[[hue * 180, saturation * 255, value * 255]]], dtype=np.uint8
|
100
|
-
)
|
101
|
-
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
102
|
-
|
103
|
-
# Store as (B, G, R) tuple
|
104
|
-
colors[i] = (int(bgr_color[0]), int(bgr_color[1]), int(bgr_color[2]))
|
105
|
-
|
106
|
-
self.config.color_map = colors
|
107
|
-
|
108
86
|
def preprocess(self, image: np.ndarray):
|
109
87
|
"""Preprocess the image for SAM model"""
|
110
88
|
self._original_image = (
|
@@ -168,142 +146,6 @@ class SegmentAnythingModel(BasePromptModel):
|
|
168
146
|
"scores": scores,
|
169
147
|
}
|
170
148
|
|
171
|
-
def mask_to_polygons(self, mask: np.ndarray) -> List[List[List[int]]]:
|
172
|
-
"""
|
173
|
-
Convert a binary mask to a list of polygons.
|
174
|
-
Each polygon is a list of [x, y] coordinates.
|
175
|
-
|
176
|
-
Args:
|
177
|
-
mask: Binary mask as numpy array
|
178
|
-
|
179
|
-
Returns:
|
180
|
-
List of polygons, where each polygon is a list of [x, y] coordinates
|
181
|
-
"""
|
182
|
-
# Find contours in the mask
|
183
|
-
contours = measure.find_contours(mask, 0.5)
|
184
|
-
|
185
|
-
# Convert to polygon format and simplify
|
186
|
-
polygons = []
|
187
|
-
for contour in contours:
|
188
|
-
# Skimage find_contours returns points in (row, col) format, convert to (x, y)
|
189
|
-
contour = np.fliplr(contour)
|
190
|
-
|
191
|
-
# Convert to integer coordinates
|
192
|
-
contour = contour.astype(np.int32)
|
193
|
-
|
194
|
-
# Simplify polygon with Douglas-Peucker algorithm
|
195
|
-
epsilon = self.config.polygon_epsilon
|
196
|
-
approx = cv2.approxPolyDP(contour.reshape(-1, 1, 2), epsilon, True)
|
197
|
-
approx = approx.reshape(-1, 2)
|
198
|
-
|
199
|
-
# Calculate polygon area
|
200
|
-
area = cv2.contourArea(approx.reshape(-1, 1, 2))
|
201
|
-
|
202
|
-
# Filter out small polygons
|
203
|
-
if area >= self.config.min_polygon_area:
|
204
|
-
# Convert to list format
|
205
|
-
poly = approx.tolist()
|
206
|
-
polygons.append(poly)
|
207
|
-
|
208
|
-
# Limit number of polygons
|
209
|
-
polygons = sorted(
|
210
|
-
polygons,
|
211
|
-
key=lambda p: cv2.contourArea(np.array(p).reshape(-1, 1, 2)),
|
212
|
-
reverse=True,
|
213
|
-
)
|
214
|
-
return polygons[: self.config.max_polygons_per_mask]
|
215
|
-
|
216
|
-
def annotate_image(
|
217
|
-
self, image: np.ndarray, results: List[PredictionResult]
|
218
|
-
) -> np.ndarray:
|
219
|
-
"""
|
220
|
-
Draw segmentation masks and contours on the image
|
221
|
-
|
222
|
-
Args:
|
223
|
-
image: The original image as a numpy array
|
224
|
-
results: List of PredictionResult objects
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
Annotated image as a numpy array
|
228
|
-
"""
|
229
|
-
annotated_image = image.copy()
|
230
|
-
mask_overlay = np.zeros_like(image)
|
231
|
-
|
232
|
-
for i, result in enumerate(results):
|
233
|
-
if (result.polygon is not None) or not result.mask:
|
234
|
-
continue
|
235
|
-
|
236
|
-
# Get color for this mask
|
237
|
-
color_idx = i % len(self.config.color_map)
|
238
|
-
color = self.config.color_map[color_idx]
|
239
|
-
|
240
|
-
# Create mask from polygons
|
241
|
-
mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)
|
242
|
-
for poly in result.polygon:
|
243
|
-
# Convert polygon to numpy array
|
244
|
-
poly_np = np.array(poly, dtype=np.int32).reshape((-1, 1, 2))
|
245
|
-
# Fill polygon
|
246
|
-
cv2.fillPoly(mask, [poly_np], 1)
|
247
|
-
|
248
|
-
# Apply color to mask overlay
|
249
|
-
color_mask = np.zeros_like(image)
|
250
|
-
color_mask[mask == 1] = color
|
251
|
-
mask_overlay = cv2.addWeighted(mask_overlay, 1.0, color_mask, 1.0, 0)
|
252
|
-
|
253
|
-
# Draw contours
|
254
|
-
for poly in result.polygon:
|
255
|
-
poly_np = np.array(poly, dtype=np.int32).reshape((-1, 1, 2))
|
256
|
-
cv2.polylines(
|
257
|
-
annotated_image,
|
258
|
-
[poly_np],
|
259
|
-
True,
|
260
|
-
color,
|
261
|
-
self.config.contour_thickness,
|
262
|
-
)
|
263
|
-
|
264
|
-
# Add label text
|
265
|
-
label_position = (
|
266
|
-
result.polygon[0][0]
|
267
|
-
if result.polygon and result.polygon[0]
|
268
|
-
else [10, 10]
|
269
|
-
)
|
270
|
-
label_text = f"{result.class_id}: {result.score:.2f}"
|
271
|
-
|
272
|
-
# Draw text background
|
273
|
-
(text_width, text_height), baseline = cv2.getTextSize(
|
274
|
-
label_text,
|
275
|
-
self.config.font_face,
|
276
|
-
self.config.text_scale,
|
277
|
-
self.config.text_thickness,
|
278
|
-
)
|
279
|
-
|
280
|
-
# Draw text background
|
281
|
-
cv2.rectangle(
|
282
|
-
annotated_image,
|
283
|
-
(label_position[0], label_position[1] - text_height - 5),
|
284
|
-
(label_position[0] + text_width, label_position[1]),
|
285
|
-
color,
|
286
|
-
-1, # Fill the rectangle
|
287
|
-
)
|
288
|
-
|
289
|
-
# Draw text
|
290
|
-
cv2.putText(
|
291
|
-
annotated_image,
|
292
|
-
label_text,
|
293
|
-
(label_position[0], label_position[1] - 5),
|
294
|
-
self.config.font_face,
|
295
|
-
self.config.text_scale,
|
296
|
-
(255, 255, 255), # White text
|
297
|
-
self.config.text_thickness,
|
298
|
-
)
|
299
|
-
|
300
|
-
# Blend mask overlay with original image
|
301
|
-
annotated_image = cv2.addWeighted(
|
302
|
-
annotated_image, 1.0, mask_overlay, self.config.mask_opacity, 0
|
303
|
-
)
|
304
|
-
|
305
|
-
return annotated_image
|
306
|
-
|
307
149
|
def postprocess(self, outputs) -> List[PredictionResult]:
|
308
150
|
"""Convert model outputs to PredictionResult objects"""
|
309
151
|
results = []
|
@@ -328,12 +170,21 @@ class SegmentAnythingModel(BasePromptModel):
|
|
328
170
|
|
329
171
|
# Convert mask to polygons
|
330
172
|
mask_np = mask.cpu().numpy()
|
331
|
-
polygons =
|
173
|
+
polygons = mask_to_polygons(mask_np)
|
332
174
|
# polygons = np.array(polygons)
|
333
175
|
|
334
176
|
if not polygons:
|
335
177
|
continue
|
336
178
|
for p, polygon in enumerate(polygons):
|
179
|
+
annotated_image = (
|
180
|
+
annotate_segmentation(
|
181
|
+
self._original_image,
|
182
|
+
results,
|
183
|
+
self.config.color_map,
|
184
|
+
)
|
185
|
+
if self.config.return_annotated_image
|
186
|
+
else None
|
187
|
+
)
|
337
188
|
|
338
189
|
# Create result
|
339
190
|
results.append(
|
@@ -344,17 +195,8 @@ class SegmentAnythingModel(BasePromptModel):
|
|
344
195
|
mask=np.argwhere(mask_np > 0.5),
|
345
196
|
polygon=np.array(polygon).astype(np.int32),
|
346
197
|
annotation_time=f"{annotation_time:.6f}",
|
198
|
+
annotated_image=annotated_image,
|
347
199
|
)
|
348
200
|
)
|
349
201
|
|
350
|
-
# Add annotated image if requested
|
351
|
-
if (
|
352
|
-
self.config.return_annotated_image
|
353
|
-
and results
|
354
|
-
and hasattr(self, "_original_image")
|
355
|
-
):
|
356
|
-
annotated = self.annotate_image(self._original_image, results)
|
357
|
-
for r in results:
|
358
|
-
r.annotated_image = annotated
|
359
|
-
|
360
202
|
return results
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# based on https://docs.ultralytics.com/tasks/segment/#how-do-i-load-and-validate-a-pretrained-yolo-segmentation-model
|
2
|
+
|
3
|
+
from typing import List, Tuple
|
4
|
+
import numpy as np
|
5
|
+
import cv2
|
6
|
+
from loguru import logger
|
7
|
+
import time
|
8
|
+
from ultralytics import YOLO
|
9
|
+
import torch
|
10
|
+
|
11
|
+
|
12
|
+
from imagebaker.models.base_model import (
|
13
|
+
BaseSegmentationModel,
|
14
|
+
DefaultModelConfig,
|
15
|
+
ModelType,
|
16
|
+
PredictionResult,
|
17
|
+
)
|
18
|
+
from imagebaker.utils import mask_to_polygons, annotate_segmentation, generate_color_map
|
19
|
+
|
20
|
+
|
21
|
+
class YoloSegmentationModelConfig(DefaultModelConfig):
|
22
|
+
model_type: ModelType = ModelType.SEGMENTATION
|
23
|
+
model_name: str = "YOLOv8-Segmentation"
|
24
|
+
model_description: str = "YOLOv8 model for instance segmentation"
|
25
|
+
model_version: str = "yolov8n-seg"
|
26
|
+
model_author: str = "Ultralytics"
|
27
|
+
model_license: str = "AGPL-3.0"
|
28
|
+
pretrained_model_name: str = "yolo11n-seg.pt"
|
29
|
+
confidence_threshold: float = 0.5
|
30
|
+
device: str = "cuda" if torch.cuda.is_available() else "cpu"
|
31
|
+
return_annotated_image: bool = False
|
32
|
+
|
33
|
+
# Segmentation specific settings
|
34
|
+
polygon_epsilon: float = 1.0 # Douglas-Peucker algorithm epsilon
|
35
|
+
min_polygon_area: int = 100 # Minimum area for a polygon to be considered
|
36
|
+
max_polygons_per_mask: int = 5 # Maximum number of polygons per mask
|
37
|
+
|
38
|
+
# Annotation parameters
|
39
|
+
mask_opacity: float = 0.5 # Opacity of mask overlay
|
40
|
+
contour_thickness: int = 2 # Thickness of contour lines
|
41
|
+
text_thickness: int = 1 # Thickness of text
|
42
|
+
text_scale: float = 0.5 # Text scale
|
43
|
+
font_face: int = cv2.FONT_HERSHEY_SIMPLEX
|
44
|
+
color_map: dict = {} # Will be auto-generated
|
45
|
+
|
46
|
+
|
47
|
+
class YoloSegmentationModel(BaseSegmentationModel):
|
48
|
+
def __init__(
|
49
|
+
self, config: YoloSegmentationModelConfig = YoloSegmentationModelConfig()
|
50
|
+
):
|
51
|
+
super().__init__(config)
|
52
|
+
|
53
|
+
def setup(self):
|
54
|
+
"""Initialize the YOLO model"""
|
55
|
+
logger.info(f"Loading YOLO model from {self.config.pretrained_model_name}")
|
56
|
+
|
57
|
+
# Load the YOLO model
|
58
|
+
self.model = YOLO(self.config.pretrained_model_name)
|
59
|
+
|
60
|
+
# Generate color map for annotations if not provided
|
61
|
+
if not self.config.color_map:
|
62
|
+
self.config.color_map = generate_color_map()
|
63
|
+
|
64
|
+
logger.info(f"Model running on {self.config.device}")
|
65
|
+
|
66
|
+
def preprocess(self, image: np.ndarray):
|
67
|
+
"""Preprocess the image for the model"""
|
68
|
+
self._original_image = (
|
69
|
+
image.copy() if isinstance(image, np.ndarray) else np.array(image)
|
70
|
+
)
|
71
|
+
return image
|
72
|
+
|
73
|
+
def predict_mask(self, image):
|
74
|
+
"""Run segmentation on the input image using YOLO"""
|
75
|
+
# Run inference
|
76
|
+
results = self.model(image)
|
77
|
+
|
78
|
+
# Extract masks, polygons, and scores
|
79
|
+
masks = []
|
80
|
+
polygons = []
|
81
|
+
scores = []
|
82
|
+
class_ids = []
|
83
|
+
for result in results:
|
84
|
+
if result.masks is not None:
|
85
|
+
for mask in result.masks.data:
|
86
|
+
masks.append(mask.cpu().numpy())
|
87
|
+
for polygon in result.masks.xy:
|
88
|
+
polygons.append(polygon.tolist())
|
89
|
+
scores.extend(result.boxes.conf.cpu().numpy().tolist())
|
90
|
+
|
91
|
+
class_ids.extend(result.boxes.cls.cpu().numpy().tolist())
|
92
|
+
|
93
|
+
return {
|
94
|
+
"masks": masks,
|
95
|
+
"polygons": polygons,
|
96
|
+
"scores": scores,
|
97
|
+
"class_ids": class_ids,
|
98
|
+
}
|
99
|
+
|
100
|
+
def postprocess(self, outputs) -> List[PredictionResult]:
|
101
|
+
"""Convert model outputs to PredictionResult objects with polygons"""
|
102
|
+
results: list[PredictionResult] = []
|
103
|
+
masks = outputs["masks"]
|
104
|
+
polygons = outputs["polygons"]
|
105
|
+
scores = outputs["scores"]
|
106
|
+
class_ids = outputs["class_ids"]
|
107
|
+
|
108
|
+
annotation_time = time.time()
|
109
|
+
|
110
|
+
for i, (mask, polygon, score) in enumerate(zip(masks, polygons, scores)):
|
111
|
+
# Skip masks with low scores
|
112
|
+
if score < self.config.confidence_threshold:
|
113
|
+
continue
|
114
|
+
|
115
|
+
# Convert mask to polygons (if not already provided)
|
116
|
+
if not polygon:
|
117
|
+
polygon = mask_to_polygons(mask)
|
118
|
+
|
119
|
+
if not polygon: # Skip if no valid polygons found
|
120
|
+
continue
|
121
|
+
|
122
|
+
# Create a flattened mask for the result
|
123
|
+
mask_coords = np.argwhere(mask > 0.5)
|
124
|
+
mask_coords = mask_coords.tolist() if len(mask_coords) > 0 else None
|
125
|
+
polygon = np.array(polygon).astype(np.int32)
|
126
|
+
|
127
|
+
# Create a PredictionResult
|
128
|
+
result = PredictionResult(
|
129
|
+
class_name=self.model.names[class_ids[i]],
|
130
|
+
class_id=i,
|
131
|
+
score=float(score),
|
132
|
+
polygon=polygon,
|
133
|
+
annotation_time=f"{annotation_time:.6f}",
|
134
|
+
)
|
135
|
+
|
136
|
+
results.append(result)
|
137
|
+
|
138
|
+
# If needed, add annotated image
|
139
|
+
if (
|
140
|
+
self.config.return_annotated_image
|
141
|
+
and len(results) > 0
|
142
|
+
and hasattr(self, "_original_image")
|
143
|
+
):
|
144
|
+
annotated_image = annotate_segmentation(
|
145
|
+
self._original_image, results, self.config.color_map
|
146
|
+
)
|
147
|
+
|
148
|
+
# Update all results with the same annotated image
|
149
|
+
for result in results:
|
150
|
+
result.annotated_image = annotated_image
|
151
|
+
|
152
|
+
return results
|
@@ -76,6 +76,8 @@ class LayerState:
|
|
76
76
|
is_annotable: bool = True
|
77
77
|
status: str = "Ready"
|
78
78
|
drawing_states: list[DrawingState] = field(default_factory=list)
|
79
|
+
edge_opacity: int = 100
|
80
|
+
edge_width: int = 10
|
79
81
|
|
80
82
|
def copy(self):
|
81
83
|
return LayerState(
|
@@ -105,6 +107,8 @@ class LayerState:
|
|
105
107
|
)
|
106
108
|
for d in self.drawing_states
|
107
109
|
],
|
110
|
+
edge_opacity=self.edge_opacity,
|
111
|
+
edge_width=self.edge_width,
|
108
112
|
)
|
109
113
|
|
110
114
|
|