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.
Files changed (85) hide show
  1. {imagebaker-0.0.49 → imagebaker-0.0.50}/PKG-INFO +2 -2
  2. {imagebaker-0.0.49 → imagebaker-0.0.50}/README.md +1 -1
  3. {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/loaded_models.py +20 -5
  4. {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/rtdetr_v2.py +18 -112
  5. {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/sam_model.py +14 -172
  6. imagebaker-0.0.50/examples/segmentation.py +152 -0
  7. imagebaker-0.0.50/imagebaker/__init__.py +9 -0
  8. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/configs/configs.py +1 -0
  9. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/defs/defs.py +4 -0
  10. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/annotable_layer.py +32 -20
  11. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/base_layer.py +132 -1
  12. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/canvas_layer.py +52 -10
  13. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/layer_settings.py +31 -2
  14. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/models/base_model.py +1 -1
  15. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/baker_tab.py +2 -9
  16. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/layerify_tab.py +1 -0
  17. imagebaker-0.0.50/imagebaker/utils/__init__.py +3 -0
  18. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/state_utils.py +5 -1
  19. imagebaker-0.0.50/imagebaker/utils/utils.py +26 -0
  20. imagebaker-0.0.50/imagebaker/utils/vis.py +174 -0
  21. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/baker_worker.py +13 -0
  22. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/PKG-INFO +2 -2
  23. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/SOURCES.txt +2 -0
  24. imagebaker-0.0.49/examples/segmentation.py +0 -288
  25. imagebaker-0.0.49/imagebaker/__init__.py +0 -5
  26. imagebaker-0.0.49/imagebaker/utils/__init__.py +0 -0
  27. {imagebaker-0.0.49 → imagebaker-0.0.50}/.github/workflows/black-formatter.yml +0 -0
  28. {imagebaker-0.0.49 → imagebaker-0.0.50}/.github/workflows/pypi.yml +0 -0
  29. {imagebaker-0.0.49 → imagebaker-0.0.50}/.gitignore +0 -0
  30. {imagebaker-0.0.49 → imagebaker-0.0.50}/CHANGELOG.md +0 -0
  31. {imagebaker-0.0.49 → imagebaker-0.0.50}/LICENSE +0 -0
  32. {imagebaker-0.0.49 → imagebaker-0.0.50}/TODO.md +0 -0
  33. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/annotated_veg_smiley.png +0 -0
  34. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/annotation_page.png +0 -0
  35. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/baker_page.png +0 -0
  36. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/drawing.png +0 -0
  37. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo/options.png +0 -0
  38. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/demo.gif +0 -0
  39. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/desk.png +0 -0
  40. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/README.md +0 -0
  41. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/android-chrome-192x192.png +0 -0
  42. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/android-chrome-512x512.png +0 -0
  43. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/apple-touch-icon.png +0 -0
  44. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon-16x16.png +0 -0
  45. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon-32x32.png +0 -0
  46. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/favicon.ico +0 -0
  47. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/favicon_io/site.webmanifest +0 -0
  48. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/me.jpg +0 -0
  49. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/pen.png +0 -0
  50. {imagebaker-0.0.49 → imagebaker-0.0.50}/assets/veg_smiley.jpg +0 -0
  51. {imagebaker-0.0.49 → imagebaker-0.0.50}/docs/api-reference.md +0 -0
  52. {imagebaker-0.0.49 → imagebaker-0.0.50}/docs/index.md +0 -0
  53. {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/app.py +0 -0
  54. {imagebaker-0.0.49 → imagebaker-0.0.50}/examples/example_config.py +0 -0
  55. {imagebaker-0.0.49 → imagebaker-0.0.50}/experiments/expt.ipynb +0 -0
  56. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/__init__.py +0 -0
  57. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/configs/__init__.py +0 -0
  58. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/defs/__init__.py +0 -0
  59. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/__init__.py +0 -0
  60. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/base_plugin.py +0 -0
  61. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/core/plugins/cosine_plugin.py +0 -0
  62. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/layers/__init__.py +0 -0
  63. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/__init__.py +0 -0
  64. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/annotation_list.py +0 -0
  65. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/canvas_list.py +0 -0
  66. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/image_list.py +0 -0
  67. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/list_views/layer_list.py +0 -0
  68. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/models/__init__.py +0 -0
  69. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/tabs/__init__.py +0 -0
  70. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/image.py +0 -0
  71. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/utils/transform_mask.py +0 -0
  72. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/__init__.py +0 -0
  73. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/app.py +0 -0
  74. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/window/main_window.py +0 -0
  75. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/__init__.py +0 -0
  76. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/layerify_worker.py +0 -0
  77. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker/workers/model_worker.py +0 -0
  78. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/dependency_links.txt +0 -0
  79. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/entry_points.txt +0 -0
  80. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/requires.txt +0 -0
  81. {imagebaker-0.0.49 → imagebaker-0.0.50}/imagebaker.egg-info/top_level.txt +0 -0
  82. {imagebaker-0.0.49 → imagebaker-0.0.50}/mkdocs.yml +0 -0
  83. {imagebaker-0.0.49 → imagebaker-0.0.50}/requirements.txt +0 -0
  84. {imagebaker-0.0.49 → imagebaker-0.0.50}/setup.cfg +0 -0
  85. {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.49
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
  ![code size in bytes](https://img.shields.io/github/languages/code-size/q-viper/image-baker)
22
22
  <!-- ![Tests](https://github.com/q-viper/SmokeSim/actions/workflows/test-on-push.yml/badge.svg) -->
23
23
  ![Code Formatting](https://github.com/q-viper/image-baker/actions/workflows/black-formatter.yml/badge.svg)
24
-
24
+ [![PyPI version](https://img.shields.io/pypi/v/imagebaker.svg)](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
  ![code size in bytes](https://img.shields.io/github/languages/code-size/q-viper/image-baker)
6
6
  <!-- ![Tests](https://github.com/q-viper/SmokeSim/actions/workflows/test-on-push.yml/badge.svg) -->
7
7
  ![Code Formatting](https://github.com/q-viper/image-baker/actions/workflows/black-formatter.yml/badge.svg)
8
-
8
+ [![PyPI version](https://img.shields.io/pypi/v/imagebaker.svg)](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
- # detector = RTDetrDetectionModel(RTDetrModelConfig())
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(model_type=ModelType.CLASSIFICATION)
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
- # segmentation = YoloSegmentationModel(YoloSegmentationModelConfig())
47
- # prompt = SegmentAnythingModel(SAMModelConfig())
48
- dummy_detector = DetectionModel(DefaultModelConfig(model_type=ModelType.DETECTION))
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.generate_color_map()
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 = True
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 = self.mask_to_polygons(mask_np)
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
@@ -0,0 +1,9 @@
1
+ from loguru import logger # noqa
2
+ from importlib.metadata import version, PackageNotFoundError
3
+
4
+ logger.info("imagebaker package loaded with loguru logger.")
5
+
6
+ try:
7
+ __version__ = version("imagebaker")
8
+ except PackageNotFoundError:
9
+ __version__ = "0.0.0"
@@ -119,6 +119,7 @@ class CanvasConfig(BaseConfig):
119
119
  write_labels: bool = True
120
120
  write_masks: bool = True
121
121
  fps: int = 5
122
+ max_edge_width: int = 100
122
123
 
123
124
  @property
124
125
  def export_folder(self):
@@ -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