imagebaker 0.0.48__py3-none-any.whl → 0.0.50__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/__init__.py +5 -1
- imagebaker/core/configs/configs.py +11 -1
- imagebaker/core/defs/defs.py +4 -0
- imagebaker/layers/annotable_layer.py +125 -35
- imagebaker/layers/base_layer.py +132 -1
- imagebaker/layers/canvas_layer.py +52 -10
- imagebaker/list_views/annotation_list.py +3 -2
- imagebaker/list_views/image_list.py +3 -1
- imagebaker/list_views/layer_settings.py +31 -2
- imagebaker/models/base_model.py +1 -1
- imagebaker/tabs/baker_tab.py +2 -9
- imagebaker/tabs/layerify_tab.py +27 -25
- imagebaker/utils/__init__.py +3 -0
- imagebaker/utils/state_utils.py +5 -1
- imagebaker/utils/utils.py +26 -0
- imagebaker/utils/vis.py +174 -0
- imagebaker/window/main_window.py +3 -1
- imagebaker/workers/baker_worker.py +13 -0
- {imagebaker-0.0.48.dist-info → imagebaker-0.0.50.dist-info}/METADATA +2 -2
- imagebaker-0.0.50.dist-info/RECORD +43 -0
- imagebaker-0.0.48.dist-info/RECORD +0 -41
- {imagebaker-0.0.48.dist-info → imagebaker-0.0.50.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.48.dist-info → imagebaker-0.0.50.dist-info}/WHEEL +0 -0
- {imagebaker-0.0.48.dist-info → imagebaker-0.0.50.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.48.dist-info → imagebaker-0.0.50.dist-info}/top_level.txt +0 -0
imagebaker/__init__.py
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
from loguru import logger # noqa
|
2
|
+
from importlib.metadata import version, PackageNotFoundError
|
2
3
|
|
3
4
|
logger.info("imagebaker package loaded with loguru logger.")
|
4
5
|
|
5
|
-
|
6
|
+
try:
|
7
|
+
__version__ = version("imagebaker")
|
8
|
+
except PackageNotFoundError:
|
9
|
+
__version__ = "0.0.0"
|
@@ -8,6 +8,11 @@ from pydantic import BaseModel, Field
|
|
8
8
|
from imagebaker.core.defs import Label, ModelType
|
9
9
|
from imagebaker import logger
|
10
10
|
|
11
|
+
try:
|
12
|
+
from imagebaker import __version__
|
13
|
+
except ImportError:
|
14
|
+
__version__ = "unknown"
|
15
|
+
|
11
16
|
|
12
17
|
class DrawConfig(BaseModel):
|
13
18
|
color: QColor = Field(default_factory=lambda: QColor(255, 255, 255))
|
@@ -37,11 +42,13 @@ class DrawConfig(BaseModel):
|
|
37
42
|
|
38
43
|
class BaseConfig(BaseModel):
|
39
44
|
project_name: str = "ImageBaker"
|
40
|
-
version: str =
|
45
|
+
version: str = __version__
|
41
46
|
project_dir: Path = Path(".")
|
42
47
|
|
43
48
|
is_debug: bool = True
|
44
49
|
deque_maxlen: int = 10
|
50
|
+
# max num_characters to show in name
|
51
|
+
max_name_length: int = 15
|
45
52
|
|
46
53
|
# drawing configs #
|
47
54
|
# ON SELECTION
|
@@ -87,6 +94,8 @@ class LayerConfig(BaseConfig):
|
|
87
94
|
Label("Custom", QColor(128, 128, 128)),
|
88
95
|
]
|
89
96
|
)
|
97
|
+
# whether to search image in subfolders as well
|
98
|
+
full_search: bool = False
|
90
99
|
|
91
100
|
def get_label_color(self, label):
|
92
101
|
for lbl in self.predefined_labels:
|
@@ -110,6 +119,7 @@ class CanvasConfig(BaseConfig):
|
|
110
119
|
write_labels: bool = True
|
111
120
|
write_masks: bool = True
|
112
121
|
fps: int = 5
|
122
|
+
max_edge_width: int = 100
|
113
123
|
|
114
124
|
@property
|
115
125
|
def export_folder(self):
|
imagebaker/core/defs/defs.py
CHANGED
@@ -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
|
|
@@ -54,6 +54,7 @@ class AnnotableLayer(BaseLayer):
|
|
54
54
|
self.file_path: Path = Path("Runtime")
|
55
55
|
self.layers: list[BaseLayer] = []
|
56
56
|
self.is_annotable = True
|
57
|
+
self.handle_zoom: float = 1
|
57
58
|
|
58
59
|
def init_ui(self):
|
59
60
|
logger.info(f"Initializing Layer UI of {self.layer_name}")
|
@@ -157,14 +158,14 @@ class AnnotableLayer(BaseLayer):
|
|
157
158
|
self.config.normal_draw_config.brush_alpha,
|
158
159
|
)
|
159
160
|
|
160
|
-
pen = QPen(pen_color, self.config.normal_draw_config.line_width)
|
161
|
+
pen = QPen(pen_color, self.config.normal_draw_config.line_width / self.scale)
|
161
162
|
brush = QBrush(brush_color, Qt.DiagCrossPattern)
|
162
163
|
|
163
164
|
if annotation.selected:
|
164
165
|
painter.setPen(
|
165
166
|
QPen(
|
166
167
|
self.config.selected_draw_config.color,
|
167
|
-
self.config.selected_draw_config.line_width,
|
168
|
+
self.config.selected_draw_config.line_width / self.scale,
|
168
169
|
)
|
169
170
|
)
|
170
171
|
painter.setBrush(
|
@@ -184,8 +185,8 @@ class AnnotableLayer(BaseLayer):
|
|
184
185
|
elif annotation.points:
|
185
186
|
painter.drawEllipse(
|
186
187
|
annotation.points[0],
|
187
|
-
self.config.selected_draw_config.ellipse_size,
|
188
|
-
self.config.selected_draw_config.ellipse_size,
|
188
|
+
self.config.selected_draw_config.ellipse_size / self.scale,
|
189
|
+
self.config.selected_draw_config.ellipse_size / self.scale,
|
189
190
|
)
|
190
191
|
|
191
192
|
if is_temp:
|
@@ -200,8 +201,8 @@ class AnnotableLayer(BaseLayer):
|
|
200
201
|
for point in annotation.points:
|
201
202
|
painter.drawEllipse(
|
202
203
|
point,
|
203
|
-
self.config.normal_draw_config.point_size,
|
204
|
-
self.config.normal_draw_config.point_size,
|
204
|
+
self.config.normal_draw_config.point_size / self.scale,
|
205
|
+
self.config.normal_draw_config.point_size / self.scale,
|
205
206
|
)
|
206
207
|
elif annotation.rectangle:
|
207
208
|
painter.drawRect(annotation.rectangle)
|
@@ -223,28 +224,34 @@ class AnnotableLayer(BaseLayer):
|
|
223
224
|
]
|
224
225
|
painter.save()
|
225
226
|
painter.setPen(
|
226
|
-
QPen(
|
227
|
+
QPen(
|
228
|
+
Qt.black,
|
229
|
+
self.config.normal_draw_config.control_point_size / self.scale,
|
230
|
+
)
|
227
231
|
)
|
228
232
|
painter.setBrush(QBrush(Qt.white))
|
229
233
|
for corner in corners:
|
230
234
|
painter.drawEllipse(
|
231
235
|
corner,
|
232
|
-
self.config.normal_draw_config.point_size,
|
233
|
-
self.config.normal_draw_config.point_size,
|
236
|
+
self.config.normal_draw_config.point_size / self.scale,
|
237
|
+
self.config.normal_draw_config.point_size / self.scale,
|
234
238
|
)
|
235
239
|
painter.restore()
|
236
240
|
|
237
241
|
if annotation.polygon and len(annotation.polygon) > 0:
|
238
242
|
painter.save()
|
239
243
|
painter.setPen(
|
240
|
-
QPen(
|
244
|
+
QPen(
|
245
|
+
Qt.white,
|
246
|
+
self.config.normal_draw_config.control_point_size / self.scale,
|
247
|
+
)
|
241
248
|
)
|
242
249
|
painter.setBrush(QBrush(Qt.darkGray))
|
243
250
|
for point in annotation.polygon:
|
244
251
|
painter.drawEllipse(
|
245
252
|
point,
|
246
|
-
self.config.normal_draw_config.point_size,
|
247
|
-
self.config.normal_draw_config.point_size,
|
253
|
+
self.config.normal_draw_config.point_size / self.scale,
|
254
|
+
self.config.normal_draw_config.point_size / self.scale,
|
248
255
|
)
|
249
256
|
painter.restore()
|
250
257
|
|
@@ -266,7 +273,7 @@ class AnnotableLayer(BaseLayer):
|
|
266
273
|
# Set up font
|
267
274
|
font = painter.font()
|
268
275
|
font.setPixelSize(
|
269
|
-
self.config.normal_draw_config.label_font_size
|
276
|
+
self.config.normal_draw_config.label_font_size * self.scale
|
270
277
|
) # Fixed screen size
|
271
278
|
painter.setFont(font)
|
272
279
|
|
@@ -300,7 +307,10 @@ class AnnotableLayer(BaseLayer):
|
|
300
307
|
painter.save()
|
301
308
|
handle_color = self.config.selected_draw_config.handle_color
|
302
309
|
painter.setPen(
|
303
|
-
QPen(
|
310
|
+
QPen(
|
311
|
+
handle_color,
|
312
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
313
|
+
)
|
304
314
|
)
|
305
315
|
painter.setBrush(QBrush(handle_color))
|
306
316
|
|
@@ -315,8 +325,8 @@ class AnnotableLayer(BaseLayer):
|
|
315
325
|
]:
|
316
326
|
painter.drawEllipse(
|
317
327
|
corner,
|
318
|
-
self.config.selected_draw_config.handle_point_size,
|
319
|
-
self.config.selected_draw_config.handle_point_size,
|
328
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
329
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
320
330
|
)
|
321
331
|
# Draw edge handles
|
322
332
|
for edge in [
|
@@ -327,8 +337,8 @@ class AnnotableLayer(BaseLayer):
|
|
327
337
|
]:
|
328
338
|
painter.drawEllipse(
|
329
339
|
edge,
|
330
|
-
self.config.selected_draw_config.handle_edge_size,
|
331
|
-
self.config.selected_draw_config.handle_edge_size,
|
340
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
341
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
332
342
|
)
|
333
343
|
|
334
344
|
elif annotation.polygon:
|
@@ -336,8 +346,8 @@ class AnnotableLayer(BaseLayer):
|
|
336
346
|
for point in annotation.polygon:
|
337
347
|
painter.drawEllipse(
|
338
348
|
point,
|
339
|
-
self.config.selected_draw_config.handle_point_size,
|
340
|
-
self.config.selected_draw_config.handle_point_size,
|
349
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
350
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
341
351
|
)
|
342
352
|
|
343
353
|
painter.restore()
|
@@ -472,10 +482,13 @@ class AnnotableLayer(BaseLayer):
|
|
472
482
|
self.selected_annotation.polygon[self.active_point_index] = (
|
473
483
|
clamped_pos
|
474
484
|
)
|
485
|
+
elif self.selected_annotation.points:
|
486
|
+
self.selected_annotation.points[0] = clamped_pos
|
475
487
|
self.annotationMoved.emit()
|
476
488
|
self.annotationUpdated.emit(self.selected_annotation)
|
477
489
|
self.update()
|
478
490
|
return
|
491
|
+
|
479
492
|
if self.mouse_mode == MouseMode.PAN and event.buttons() & Qt.LeftButton:
|
480
493
|
if self.pan_start:
|
481
494
|
delta = event.position() - self.pan_start
|
@@ -534,6 +547,7 @@ class AnnotableLayer(BaseLayer):
|
|
534
547
|
self.mouse_mode = MouseMode.IDLE
|
535
548
|
for ann in self.annotations:
|
536
549
|
ann.selected = False
|
550
|
+
self.annotationUpdated.emit(ann)
|
537
551
|
self.update()
|
538
552
|
|
539
553
|
# If left-clicked
|
@@ -559,7 +573,9 @@ class AnnotableLayer(BaseLayer):
|
|
559
573
|
elif self.selected_annotation.polygon:
|
560
574
|
self.initial_polygon = QPolygonF(self.selected_annotation.polygon)
|
561
575
|
if "point_" in self.active_handle:
|
562
|
-
self.
|
576
|
+
self.active_point_index = int(self.active_handle.split("_")[1])
|
577
|
+
elif self.selected_annotation.points:
|
578
|
+
self.active_point_index = 0
|
563
579
|
|
564
580
|
# If pan mode
|
565
581
|
if self.mouse_mode == MouseMode.PAN:
|
@@ -646,6 +662,11 @@ class AnnotableLayer(BaseLayer):
|
|
646
662
|
if annotation.polygon.containsPoint(pos, Qt.OddEvenFill):
|
647
663
|
return annotation, "move"
|
648
664
|
|
665
|
+
# Check points
|
666
|
+
elif annotation.points:
|
667
|
+
if (annotation.points[0] - pos).manhattanLength() < margin:
|
668
|
+
return annotation, "point_0"
|
669
|
+
|
649
670
|
return None, None
|
650
671
|
|
651
672
|
def handle_mouse_double_click(self, event: QMouseEvent, pos: QPoint):
|
@@ -656,6 +677,73 @@ class AnnotableLayer(BaseLayer):
|
|
656
677
|
break
|
657
678
|
# if left double click
|
658
679
|
if event.button() == Qt.LeftButton:
|
680
|
+
img_pos = self.widget_to_image_pos(event.position())
|
681
|
+
|
682
|
+
self.selected_annotation, self.active_handle = (
|
683
|
+
self.find_annotation_and_handle_at(img_pos)
|
684
|
+
)
|
685
|
+
|
686
|
+
if self.selected_annotation:
|
687
|
+
if self.selected_annotation.polygon and self.active_handle:
|
688
|
+
if "point_" in self.active_handle:
|
689
|
+
index = int(self.active_handle.split("_")[1])
|
690
|
+
# Remove the point at the clicked index
|
691
|
+
polygon = self.selected_annotation.polygon
|
692
|
+
polygon = QPolygonF(
|
693
|
+
[p for i, p in enumerate(polygon) if i != index]
|
694
|
+
)
|
695
|
+
|
696
|
+
self.selected_annotation.polygon = polygon
|
697
|
+
self.annotationUpdated.emit(self.selected_annotation)
|
698
|
+
self.update()
|
699
|
+
logger.info(f"Removed point at index {index}")
|
700
|
+
return
|
701
|
+
|
702
|
+
# Check if an edge was double-clicked
|
703
|
+
polygon = self.selected_annotation.polygon
|
704
|
+
if polygon:
|
705
|
+
for i in range(len(polygon)):
|
706
|
+
start_point = polygon[i]
|
707
|
+
end_point = polygon[
|
708
|
+
(i + 1) % len(polygon)
|
709
|
+
] # Wrap around to the first point
|
710
|
+
|
711
|
+
# Calculate the vector along the edge and the vector from the start point to the clicked position
|
712
|
+
line_vector = end_point - start_point
|
713
|
+
point_vector = img_pos - start_point
|
714
|
+
|
715
|
+
# Calculate the length of the edge
|
716
|
+
line_length_squared = (
|
717
|
+
line_vector.x() ** 2 + line_vector.y() ** 2
|
718
|
+
)
|
719
|
+
if line_length_squared == 0:
|
720
|
+
continue # Avoid division by zero for degenerate edges
|
721
|
+
|
722
|
+
# Project the point onto the line (normalized)
|
723
|
+
projection = (
|
724
|
+
point_vector.x() * line_vector.x()
|
725
|
+
+ point_vector.y() * line_vector.y()
|
726
|
+
) / line_length_squared
|
727
|
+
|
728
|
+
# Clamp the projection to the range [0, 1] to ensure it lies on the segment
|
729
|
+
projection = max(0, min(1, projection))
|
730
|
+
|
731
|
+
# Calculate the projection point on the edge
|
732
|
+
projection_point = start_point + projection * line_vector
|
733
|
+
|
734
|
+
# Calculate the perpendicular distance from the clicked position to the edge
|
735
|
+
perpendicular_distance = (
|
736
|
+
img_pos - projection_point
|
737
|
+
).manhattanLength()
|
738
|
+
|
739
|
+
# Check if the perpendicular distance is within the margin
|
740
|
+
if perpendicular_distance < 10: # Margin of 10
|
741
|
+
# Insert a new point at the projection point
|
742
|
+
polygon.insert(i + 1, projection_point)
|
743
|
+
self.annotationUpdated.emit(self.selected_annotation)
|
744
|
+
self.update()
|
745
|
+
return
|
746
|
+
|
659
747
|
# if drawing a polygon, close the polygon
|
660
748
|
if (
|
661
749
|
self.current_annotation
|
@@ -670,20 +758,20 @@ class AnnotableLayer(BaseLayer):
|
|
670
758
|
return
|
671
759
|
|
672
760
|
# did we click on an annotation?
|
673
|
-
annotation = self.find_annotation_at(self.widget_to_image_pos(pos))
|
674
|
-
if annotation:
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
else:
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
761
|
+
# annotation = self.find_annotation_at(self.widget_to_image_pos(pos))
|
762
|
+
# if annotation:
|
763
|
+
# # toggle selection
|
764
|
+
# annotation.selected = not annotation.selected
|
765
|
+
|
766
|
+
# # make all other annotations unselected
|
767
|
+
# for ann in self.annotations:
|
768
|
+
# if ann != annotation:
|
769
|
+
# ann.selected = False
|
770
|
+
# else:
|
771
|
+
# # we clicked on the background
|
772
|
+
# # make all annotations unselected
|
773
|
+
# for ann in self.annotations:
|
774
|
+
# ann.selected = False
|
687
775
|
# update the view
|
688
776
|
for ann in self.annotations:
|
689
777
|
self.annotationUpdated.emit(ann)
|
@@ -828,6 +916,8 @@ class AnnotableLayer(BaseLayer):
|
|
828
916
|
f"{annotation.label} {annotation.annotation_id} {annotation.annotator}"
|
829
917
|
)
|
830
918
|
|
919
|
+
new_layer._apply_edge_opacity()
|
920
|
+
new_layer.update()
|
831
921
|
self.messageSignal.emit(f"Layerified: {new_layer.layer_name}")
|
832
922
|
logger.info(f"Num annotations: {len(self.annotations)}")
|
833
923
|
|
imagebaker/layers/base_layer.py
CHANGED
@@ -18,6 +18,8 @@ from PySide6.QtWidgets import QWidget
|
|
18
18
|
|
19
19
|
from typing import Optional
|
20
20
|
from pathlib import Path
|
21
|
+
import cv2
|
22
|
+
import numpy as np
|
21
23
|
|
22
24
|
|
23
25
|
class BaseLayer(QWidget):
|
@@ -157,7 +159,8 @@ class BaseLayer(QWidget):
|
|
157
159
|
self.offset: QPointF = QPointF(0, 0)
|
158
160
|
self.pan_start: QPointF = None
|
159
161
|
self.pan_offset: QPointF = None
|
160
|
-
self.
|
162
|
+
self._image = QPixmap()
|
163
|
+
self._original_image = QPixmap()
|
161
164
|
self.annotations: list[Annotation] = []
|
162
165
|
self.current_annotation: Optional[Annotation] = None
|
163
166
|
self.copied_annotation: Optional[Annotation] = None
|
@@ -365,8 +368,120 @@ class BaseLayer(QWidget):
|
|
365
368
|
self.reset_view()
|
366
369
|
self.update()
|
367
370
|
|
371
|
+
self._original_image = self.image.copy() # Store a copy of the original image
|
368
372
|
self.original_size = QSizeF(self.image.size()) # Store original size
|
369
373
|
|
374
|
+
@property
|
375
|
+
def image(self):
|
376
|
+
"""
|
377
|
+
Get the current image of the canvas layer.
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
QPixmap: The current image of the canvas layer.
|
381
|
+
"""
|
382
|
+
return self._image
|
383
|
+
|
384
|
+
@image.setter
|
385
|
+
def image(self, value: QPixmap):
|
386
|
+
"""
|
387
|
+
Set the image of the canvas layer.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
value (QPixmap): The new image for the canvas layer.
|
391
|
+
"""
|
392
|
+
self._image = value
|
393
|
+
|
394
|
+
def _apply_edge_opacity(self):
|
395
|
+
"""
|
396
|
+
Apply edge opacity to the image. This function modifies the edges of the image
|
397
|
+
to have reduced opacity based on the configuration.
|
398
|
+
"""
|
399
|
+
logger.debug("Applying edge opacity to the image.")
|
400
|
+
edge_width = self.edge_width
|
401
|
+
edge_opacity = self.edge_opacity
|
402
|
+
|
403
|
+
# Convert QPixmap to QImage for pixel manipulation
|
404
|
+
image = self._original_image.toImage()
|
405
|
+
image = image.convertToFormat(
|
406
|
+
QImage.Format_ARGB32
|
407
|
+
) # Ensure format supports alpha
|
408
|
+
|
409
|
+
width = image.width()
|
410
|
+
height = image.height()
|
411
|
+
annotation = self.annotations[0] if self.annotations else None
|
412
|
+
if annotation is None:
|
413
|
+
return
|
414
|
+
|
415
|
+
if annotation.rectangle:
|
416
|
+
for x in range(width):
|
417
|
+
for y in range(height):
|
418
|
+
color = image.pixelColor(x, y)
|
419
|
+
if color.alpha() != 0: # If the pixel is not fully transparent
|
420
|
+
# Calculate horizontal and vertical distances to the edges
|
421
|
+
horizontal_distance = min(x, width - x - 1)
|
422
|
+
vertical_distance = min(y, height - y - 1)
|
423
|
+
|
424
|
+
# If the pixel is within the edge region
|
425
|
+
if (
|
426
|
+
horizontal_distance < edge_width
|
427
|
+
or vertical_distance < edge_width
|
428
|
+
):
|
429
|
+
distance_to_edge = min(
|
430
|
+
horizontal_distance, vertical_distance
|
431
|
+
)
|
432
|
+
# Calculate the new alpha based on the distance to the edge
|
433
|
+
factor = (edge_width - distance_to_edge) / edge_width
|
434
|
+
new_alpha = int(
|
435
|
+
color.alpha()
|
436
|
+
* ((1 - factor) + (factor * (edge_opacity / 255.0)))
|
437
|
+
)
|
438
|
+
color.setAlpha(new_alpha)
|
439
|
+
image.setPixelColor(x, y, color)
|
440
|
+
|
441
|
+
elif annotation.polygon:
|
442
|
+
# Extract alpha channel and find contours
|
443
|
+
alpha_image = image.convertToFormat(QImage.Format_Alpha8)
|
444
|
+
bytes_per_line = (
|
445
|
+
alpha_image.bytesPerLine()
|
446
|
+
) # Get the stride (bytes per line)
|
447
|
+
alpha_data = alpha_image.bits().tobytes()
|
448
|
+
|
449
|
+
# Extract only the valid data (remove padding)
|
450
|
+
alpha_array = np.frombuffer(alpha_data, dtype=np.uint8).reshape(
|
451
|
+
(alpha_image.height(), bytes_per_line)
|
452
|
+
)[
|
453
|
+
:, : alpha_image.width()
|
454
|
+
] # Remove padding to match the actual width
|
455
|
+
|
456
|
+
# Use OpenCV to find contours
|
457
|
+
contours, _ = cv2.findContours(
|
458
|
+
alpha_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
459
|
+
)
|
460
|
+
|
461
|
+
# Iterate over each pixel and apply edge opacity
|
462
|
+
for x in range(width):
|
463
|
+
for y in range(height):
|
464
|
+
color = image.pixelColor(x, y)
|
465
|
+
if color.alpha() != 0: # If the pixel is not fully transparent
|
466
|
+
# Calculate distance to the nearest contour
|
467
|
+
distance_to_edge = cv2.pointPolygonTest(
|
468
|
+
contours[0], (x, y), True
|
469
|
+
) # True for distance calculation
|
470
|
+
|
471
|
+
# If the pixel is within the edge region
|
472
|
+
if 0 <= distance_to_edge < edge_width:
|
473
|
+
# Calculate the new alpha based on the distance to the edge
|
474
|
+
factor = (edge_width - distance_to_edge) / edge_width
|
475
|
+
new_alpha = int(
|
476
|
+
color.alpha()
|
477
|
+
* ((1 - factor) + (factor * (edge_opacity / 255.0)))
|
478
|
+
)
|
479
|
+
color.setAlpha(new_alpha)
|
480
|
+
image.setPixelColor(x, y, color)
|
481
|
+
|
482
|
+
# Convert the modified QImage back to QPixmap
|
483
|
+
self.image = QPixmap.fromImage(image)
|
484
|
+
|
370
485
|
def get_thumbnail(self, annotation: Annotation = None):
|
371
486
|
"""
|
372
487
|
Generate a thumbnail for the layer or a specific annotation.
|
@@ -722,3 +837,19 @@ class BaseLayer(QWidget):
|
|
722
837
|
@drawing_states.setter
|
723
838
|
def drawing_states(self, value: list[DrawingState]):
|
724
839
|
self.layer_state.drawing_states = value
|
840
|
+
|
841
|
+
@property
|
842
|
+
def edge_opacity(self) -> int:
|
843
|
+
return self.layer_state.edge_opacity
|
844
|
+
|
845
|
+
@edge_opacity.setter
|
846
|
+
def edge_opacity(self, value: int):
|
847
|
+
self.layer_state.edge_opacity = value
|
848
|
+
|
849
|
+
@property
|
850
|
+
def edge_width(self) -> int:
|
851
|
+
return self.layer_state.edge_width
|
852
|
+
|
853
|
+
@edge_width.setter
|
854
|
+
def edge_width(self, value: int):
|
855
|
+
self.layer_state.edge_width = value
|
@@ -27,6 +27,7 @@ from PySide6.QtGui import (
|
|
27
27
|
QMouseEvent,
|
28
28
|
QKeyEvent,
|
29
29
|
QTransform,
|
30
|
+
QImage,
|
30
31
|
)
|
31
32
|
from PySide6.QtWidgets import (
|
32
33
|
QApplication,
|
@@ -57,7 +58,6 @@ class CanvasLayer(BaseLayer):
|
|
57
58
|
config (CanvasConfig): Configuration settings for the canvas layer.
|
58
59
|
"""
|
59
60
|
super().__init__(parent, config)
|
60
|
-
self.image = QPixmap()
|
61
61
|
self.is_annotable = False
|
62
62
|
self.last_pan_point = None
|
63
63
|
self.state_thumbnail = dict()
|
@@ -178,6 +178,7 @@ class CanvasLayer(BaseLayer):
|
|
178
178
|
opacity = layer.opacity / 255.0
|
179
179
|
temp_painter.setOpacity(opacity) # Scale opacity to 0.0-1.0
|
180
180
|
temp_painter.drawPixmap(0, 0, layer.image)
|
181
|
+
|
181
182
|
temp_painter.end()
|
182
183
|
|
183
184
|
# Draw the modified pixmap
|
@@ -287,20 +288,20 @@ class CanvasLayer(BaseLayer):
|
|
287
288
|
painter.setPen(
|
288
289
|
QPen(
|
289
290
|
self.config.selected_draw_config.handle_color,
|
290
|
-
self.config.selected_draw_config.handle_width,
|
291
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
291
292
|
)
|
292
293
|
)
|
293
294
|
painter.setBrush(self.config.selected_draw_config.handle_color)
|
294
295
|
painter.drawEllipse(
|
295
296
|
rotation_pos,
|
296
|
-
self.config.selected_draw_config.handle_point_size *
|
297
|
-
self.config.selected_draw_config.handle_point_size *
|
297
|
+
self.config.selected_draw_config.handle_point_size * 1.1 / self.scale,
|
298
|
+
self.config.selected_draw_config.handle_point_size * 1.1 / self.scale,
|
298
299
|
)
|
299
300
|
# now draw rotation symbol
|
300
301
|
painter.setPen(
|
301
302
|
QPen(
|
302
303
|
self.config.selected_draw_config.handle_color,
|
303
|
-
self.config.selected_draw_config.handle_width,
|
304
|
+
self.config.selected_draw_config.handle_width / self.scale,
|
304
305
|
)
|
305
306
|
)
|
306
307
|
painter.drawLine(
|
@@ -323,21 +324,23 @@ class CanvasLayer(BaseLayer):
|
|
323
324
|
# Draw scale handles
|
324
325
|
handle_color = self.config.selected_draw_config.handle_color
|
325
326
|
painter.setPen(
|
326
|
-
QPen(
|
327
|
+
QPen(
|
328
|
+
handle_color, self.config.selected_draw_config.handle_width / self.scale
|
329
|
+
)
|
327
330
|
)
|
328
331
|
painter.setBrush(self.config.selected_draw_config.handle_color)
|
329
332
|
for corner in corners:
|
330
333
|
painter.drawEllipse(
|
331
334
|
corner,
|
332
|
-
self.config.selected_draw_config.handle_point_size,
|
333
|
-
self.config.selected_draw_config.handle_point_size,
|
335
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
336
|
+
self.config.selected_draw_config.handle_point_size / self.scale,
|
334
337
|
)
|
335
338
|
for edge in edges:
|
336
339
|
# draw small circles on the edges
|
337
340
|
painter.drawEllipse(
|
338
341
|
edge,
|
339
|
-
self.config.selected_draw_config.handle_edge_size,
|
340
|
-
self.config.selected_draw_config.handle_edge_size,
|
342
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
343
|
+
self.config.selected_draw_config.handle_edge_size / self.scale,
|
341
344
|
)
|
342
345
|
# draw sides
|
343
346
|
painter.drawLine(
|
@@ -874,6 +877,33 @@ class CanvasLayer(BaseLayer):
|
|
874
877
|
"""
|
875
878
|
self.export_current_state(export_to_annotation_tab=True)
|
876
879
|
|
880
|
+
def seek_state(self, step):
|
881
|
+
"""Seek to a specific state using the timeline slider."""
|
882
|
+
self.messageSignal.emit(f"Seeking to step {step}")
|
883
|
+
logger.info(f"Seeking to step {step}")
|
884
|
+
|
885
|
+
# Get the states for the selected step
|
886
|
+
if step in self.states:
|
887
|
+
states = self.states[step]
|
888
|
+
for state in states:
|
889
|
+
layer = self.get_layer(state.layer_id)
|
890
|
+
if layer:
|
891
|
+
# Update the layer's state
|
892
|
+
update_opacities = False
|
893
|
+
logger.debug(
|
894
|
+
f"Updating layer {layer.layer_name} with state: {state}"
|
895
|
+
)
|
896
|
+
|
897
|
+
if (
|
898
|
+
layer.edge_width != state.edge_width
|
899
|
+
or layer.edge_opacity != state.edge_opacity
|
900
|
+
):
|
901
|
+
update_opacities = True
|
902
|
+
layer.layer_state = state
|
903
|
+
if update_opacities:
|
904
|
+
layer._apply_edge_opacity()
|
905
|
+
layer.update()
|
906
|
+
|
877
907
|
def play_states(self):
|
878
908
|
"""Play all the states stored in self.states."""
|
879
909
|
if len(self.states) == 0:
|
@@ -896,7 +926,19 @@ class CanvasLayer(BaseLayer):
|
|
896
926
|
layer = self.get_layer(state.layer_id)
|
897
927
|
if layer:
|
898
928
|
# Update the layer's state
|
929
|
+
update_opacities = False
|
930
|
+
logger.debug(
|
931
|
+
f"Updating layer {layer.layer_name} with state: {state}"
|
932
|
+
)
|
933
|
+
|
934
|
+
if (
|
935
|
+
layer.edge_width != state.edge_width
|
936
|
+
or layer.edge_opacity != state.edge_opacity
|
937
|
+
):
|
938
|
+
update_opacities = True
|
899
939
|
layer.layer_state = state
|
940
|
+
if update_opacities:
|
941
|
+
layer._apply_edge_opacity()
|
900
942
|
layer.update()
|
901
943
|
|
902
944
|
# Update the UI to reflect the changes
|
@@ -23,9 +23,10 @@ from imagebaker import logger
|
|
23
23
|
class AnnotationList(QDockWidget):
|
24
24
|
messageSignal = Signal(str)
|
25
25
|
|
26
|
-
def __init__(self, layer: AnnotableLayer, parent=None):
|
26
|
+
def __init__(self, layer: AnnotableLayer, parent=None, max_name_length=15):
|
27
27
|
super().__init__("Annotations", parent)
|
28
28
|
self.layer = layer
|
29
|
+
self.max_name_length = max_name_length
|
29
30
|
self.init_ui()
|
30
31
|
|
31
32
|
def init_ui(self):
|
@@ -100,7 +101,7 @@ class AnnotationList(QDockWidget):
|
|
100
101
|
else ann.annotator
|
101
102
|
)
|
102
103
|
secondary_text.append(score_text)
|
103
|
-
short_path = ann.file_path.stem
|
104
|
+
short_path = ann.file_path.stem[: self.max_name_length]
|
104
105
|
secondary_text.append(f"<span style='color:#666;'>{short_path}</span>")
|
105
106
|
|
106
107
|
if secondary_text:
|
@@ -25,6 +25,7 @@ class ImageListPanel(QDockWidget):
|
|
25
25
|
image_entries: list["ImageEntry"],
|
26
26
|
processed_images: set[Path],
|
27
27
|
parent=None,
|
28
|
+
max_name_length=15,
|
28
29
|
):
|
29
30
|
"""
|
30
31
|
:param image_entries: List of image paths to display.
|
@@ -35,6 +36,7 @@ class ImageListPanel(QDockWidget):
|
|
35
36
|
self.processed_images = processed_images
|
36
37
|
self.current_page = 0
|
37
38
|
self.images_per_page = 10
|
39
|
+
self.max_name_length = max_name_length
|
38
40
|
self.init_ui()
|
39
41
|
|
40
42
|
def init_ui(self):
|
@@ -104,7 +106,7 @@ class ImageListPanel(QDockWidget):
|
|
104
106
|
thumbnail_pixmap = QPixmap(str(image_entry.data)).scaled(
|
105
107
|
50, 50, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
106
108
|
)
|
107
|
-
name_label_text = Path(image_entry.data).name
|
109
|
+
name_label_text = Path(image_entry.data).name[: self.max_name_length]
|
108
110
|
|
109
111
|
thumbnail_label.setPixmap(thumbnail_pixmap)
|
110
112
|
item_layout.addWidget(thumbnail_label)
|
@@ -20,7 +20,14 @@ class LayerSettings(QDockWidget):
|
|
20
20
|
layerState = Signal(LayerState)
|
21
21
|
messageSignal = Signal(str)
|
22
22
|
|
23
|
-
def __init__(
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
parent=None,
|
26
|
+
max_xpos=1000,
|
27
|
+
max_ypos=1000,
|
28
|
+
max_scale=100,
|
29
|
+
max_edge_width=10,
|
30
|
+
):
|
24
31
|
super().__init__("BaseLayer Settings", parent)
|
25
32
|
self.selected_layer: BaseLayer = None
|
26
33
|
|
@@ -29,6 +36,7 @@ class LayerSettings(QDockWidget):
|
|
29
36
|
self.max_xpos = max_xpos
|
30
37
|
self.max_ypos = max_ypos
|
31
38
|
self.max_scale = max_scale
|
39
|
+
self.max_edge_width = max_edge_width
|
32
40
|
self.init_ui()
|
33
41
|
self.setFeatures(
|
34
42
|
QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
|
@@ -67,6 +75,12 @@ class LayerSettings(QDockWidget):
|
|
67
75
|
self.main_layout.addWidget(self.scale_y_slider["widget"])
|
68
76
|
self.rotation_slider = self.create_slider("Rotation:", 0, 360, 0, 1)
|
69
77
|
self.main_layout.addWidget(self.rotation_slider["widget"])
|
78
|
+
self.edge_opacity_slider = self.create_slider("Edge Opacity:", 0, 255, 255, 1)
|
79
|
+
self.main_layout.addWidget(self.edge_opacity_slider["widget"])
|
80
|
+
self.edge_width_slider = self.create_slider(
|
81
|
+
"Edge Width:", 0, self.max_edge_width, 5, 1
|
82
|
+
)
|
83
|
+
self.main_layout.addWidget(self.edge_width_slider["widget"])
|
70
84
|
|
71
85
|
# Add stretch to push content to the top
|
72
86
|
self.main_layout.addStretch()
|
@@ -125,7 +139,6 @@ class LayerSettings(QDockWidget):
|
|
125
139
|
|
126
140
|
try:
|
127
141
|
self._disable_updates = True
|
128
|
-
|
129
142
|
if sender == self.opacity_slider["slider"]:
|
130
143
|
self.selected_layer.opacity = value
|
131
144
|
elif sender == self.x_slider["slider"]:
|
@@ -138,6 +151,12 @@ class LayerSettings(QDockWidget):
|
|
138
151
|
self.selected_layer.scale_y = value / 100.0
|
139
152
|
elif sender == self.rotation_slider["slider"]:
|
140
153
|
self.selected_layer.rotation = value
|
154
|
+
elif sender == self.edge_opacity_slider["slider"]:
|
155
|
+
self.selected_layer.edge_opacity = value
|
156
|
+
self.selected_layer._apply_edge_opacity()
|
157
|
+
elif sender == self.edge_width_slider["slider"]:
|
158
|
+
self.selected_layer.edge_width = value
|
159
|
+
self.selected_layer._apply_edge_opacity()
|
141
160
|
|
142
161
|
self.selected_layer.update() # Trigger a repaint
|
143
162
|
|
@@ -154,6 +173,10 @@ class LayerSettings(QDockWidget):
|
|
154
173
|
rotation=self.selected_layer.rotation,
|
155
174
|
scale_x=self.selected_layer.scale_x,
|
156
175
|
scale_y=self.selected_layer.scale_y,
|
176
|
+
opacity=self.selected_layer.opacity,
|
177
|
+
edge_opacity=self.selected_layer.edge_opacity,
|
178
|
+
edge_width=self.selected_layer.edge_width,
|
179
|
+
visible=self.selected_layer.visible,
|
157
180
|
)
|
158
181
|
logger.info(f"Storing state {bake_settings}")
|
159
182
|
self.messageSignal.emit(f"Stored state for {bake_settings.layer_name}")
|
@@ -211,6 +234,12 @@ class LayerSettings(QDockWidget):
|
|
211
234
|
self.rotation_slider["slider"].setValue(
|
212
235
|
int(self.selected_layer.rotation)
|
213
236
|
)
|
237
|
+
self.edge_opacity_slider["slider"].setValue(
|
238
|
+
int(self.selected_layer.edge_opacity)
|
239
|
+
)
|
240
|
+
self.edge_width_slider["slider"].setValue(
|
241
|
+
int(self.selected_layer.edge_width)
|
242
|
+
)
|
214
243
|
else:
|
215
244
|
self.widget.setEnabled(False)
|
216
245
|
self.layer_name_label.setText("No BaseLayer")
|
imagebaker/models/base_model.py
CHANGED
imagebaker/tabs/baker_tab.py
CHANGED
@@ -66,6 +66,7 @@ class BakerTab(QWidget):
|
|
66
66
|
max_xpos=self.config.max_xpos,
|
67
67
|
max_ypos=self.config.max_ypos,
|
68
68
|
max_scale=self.config.max_scale,
|
69
|
+
max_edge_width=self.config.max_edge_width,
|
69
70
|
)
|
70
71
|
self.layer_list = LayerList(
|
71
72
|
canvas=self.current_canvas,
|
@@ -415,15 +416,7 @@ class BakerTab(QWidget):
|
|
415
416
|
"""Seek to a specific state using the timeline slider."""
|
416
417
|
self.messageSignal.emit(f"Seeking to step {step}")
|
417
418
|
logger.info(f"Seeking to step {step}")
|
418
|
-
|
419
|
-
# Get the states for the selected step
|
420
|
-
if step in self.current_canvas.states:
|
421
|
-
states = self.current_canvas.states[step]
|
422
|
-
for state in states:
|
423
|
-
layer = self.current_canvas.get_layer(state.layer_id)
|
424
|
-
if layer:
|
425
|
-
layer.layer_state = state
|
426
|
-
layer.update()
|
419
|
+
self.current_canvas.seek_state(step)
|
427
420
|
|
428
421
|
# Update the canvas
|
429
422
|
self.current_canvas.update()
|
imagebaker/tabs/layerify_tab.py
CHANGED
@@ -114,7 +114,9 @@ class LayerifyTab(QWidget):
|
|
114
114
|
def init_ui(self):
|
115
115
|
"""Initialize the UI components"""
|
116
116
|
# Create annotation list and image list panel
|
117
|
-
self.annotation_list = AnnotationList(
|
117
|
+
self.annotation_list = AnnotationList(
|
118
|
+
None, parent=self.main_window, max_name_length=self.config.max_name_length
|
119
|
+
)
|
118
120
|
self.image_list_panel = ImageListPanel(
|
119
121
|
self.image_entries, self.processed_images
|
120
122
|
)
|
@@ -197,18 +199,7 @@ class LayerifyTab(QWidget):
|
|
197
199
|
if not self.image_entries:
|
198
200
|
assets_folder = self.config.assets_folder
|
199
201
|
if assets_folder.exists() and assets_folder.is_dir():
|
200
|
-
|
201
|
-
if img_path.suffix.lower() in [
|
202
|
-
".jpg",
|
203
|
-
".jpeg",
|
204
|
-
".png",
|
205
|
-
".bmp",
|
206
|
-
".tiff",
|
207
|
-
]:
|
208
|
-
# Add regular images as dictionaries with type and data
|
209
|
-
self.image_entries.append(
|
210
|
-
ImageEntry(is_baked_result=False, data=img_path)
|
211
|
-
)
|
202
|
+
self._load_images_from_folder(assets_folder)
|
212
203
|
|
213
204
|
# Load images into layers if any are found
|
214
205
|
if self.image_entries:
|
@@ -472,6 +463,7 @@ class LayerifyTab(QWidget):
|
|
472
463
|
def handle_model_error(self, error):
|
473
464
|
logger.error(f"Model error: {error}")
|
474
465
|
QMessageBox.critical(self, "Error", f"Model error: {error}")
|
466
|
+
self.loading_dialog.close()
|
475
467
|
|
476
468
|
def save_annotations(self):
|
477
469
|
"""Save annotations to a JSON file."""
|
@@ -690,6 +682,27 @@ class LayerifyTab(QWidget):
|
|
690
682
|
self.model_combo.currentIndexChanged.connect(self.handle_model_change)
|
691
683
|
toolbar_layout.addWidget(self.model_combo)
|
692
684
|
|
685
|
+
def _load_images_from_folder(self, folder_path: Path):
|
686
|
+
"""Load images from a folder and update the image list."""
|
687
|
+
self.image_entries = [] # Clear the existing image paths
|
688
|
+
|
689
|
+
if self.config.full_search:
|
690
|
+
image_paths = list(folder_path.rglob("*.*"))
|
691
|
+
else:
|
692
|
+
image_paths = list(folder_path.glob("*.*"))
|
693
|
+
|
694
|
+
for img_path in image_paths:
|
695
|
+
if img_path.suffix.lower() in [
|
696
|
+
".jpg",
|
697
|
+
".jpeg",
|
698
|
+
".png",
|
699
|
+
".bmp",
|
700
|
+
".tiff",
|
701
|
+
]:
|
702
|
+
self.image_entries.append(
|
703
|
+
ImageEntry(is_baked_result=False, data=img_path)
|
704
|
+
)
|
705
|
+
|
693
706
|
def select_folder(self):
|
694
707
|
"""Allow the user to select a folder and load images from it."""
|
695
708
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
@@ -697,18 +710,7 @@ class LayerifyTab(QWidget):
|
|
697
710
|
self.image_entries = [] # Clear the existing image paths
|
698
711
|
folder_path = Path(folder_path)
|
699
712
|
|
700
|
-
|
701
|
-
for img_path in folder_path.rglob("*.*"):
|
702
|
-
if img_path.suffix.lower() in [
|
703
|
-
".jpg",
|
704
|
-
".jpeg",
|
705
|
-
".png",
|
706
|
-
".bmp",
|
707
|
-
".tiff",
|
708
|
-
]:
|
709
|
-
self.image_entries.append(
|
710
|
-
ImageEntry(is_baked_result=False, data=img_path)
|
711
|
-
)
|
713
|
+
self._load_images_from_folder(folder_path)
|
712
714
|
|
713
715
|
self.curr_image_idx = 0 # Reset the current image index
|
714
716
|
|
imagebaker/utils/__init__.py
CHANGED
imagebaker/utils/state_utils.py
CHANGED
@@ -59,9 +59,13 @@ def calculate_intermediate_states(
|
|
59
59
|
visible=current_state.visible,
|
60
60
|
allow_annotation_export=current_state.allow_annotation_export,
|
61
61
|
playing=current_state.playing,
|
62
|
-
selected=
|
62
|
+
selected=False,
|
63
63
|
is_annotable=current_state.is_annotable,
|
64
64
|
status=current_state.status,
|
65
|
+
edge_opacity=previous_state.edge_opacity
|
66
|
+
+ (current_state.edge_opacity - previous_state.edge_opacity) * (i / steps),
|
67
|
+
edge_width=previous_state.edge_width
|
68
|
+
+ (current_state.edge_width - previous_state.edge_width) * (i / steps),
|
65
69
|
)
|
66
70
|
|
67
71
|
# Deep copy the drawing_states from the previous_state
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import cv2
|
3
|
+
|
4
|
+
|
5
|
+
def generate_color_map(num_colors: int = 20):
|
6
|
+
"""Generate a color map for the segmentation masks"""
|
7
|
+
np.random.seed(42) # For reproducible colors
|
8
|
+
|
9
|
+
colors = {}
|
10
|
+
for i in range(num_colors):
|
11
|
+
# Generate distinct colors with good visibility
|
12
|
+
# Using HSV color space for better distribution
|
13
|
+
hue = i / num_colors
|
14
|
+
saturation = 0.8 + np.random.random() * 0.2
|
15
|
+
value = 0.8 + np.random.random() * 0.2
|
16
|
+
|
17
|
+
# Convert HSV to BGR (OpenCV uses BGR)
|
18
|
+
hsv_color = np.array(
|
19
|
+
[[[hue * 180, saturation * 255, value * 255]]], dtype=np.uint8
|
20
|
+
)
|
21
|
+
bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)[0][0]
|
22
|
+
|
23
|
+
# Store as (B, G, R) tuple
|
24
|
+
colors[i] = (int(bgr_color[0]), int(bgr_color[1]), int(bgr_color[2]))
|
25
|
+
|
26
|
+
return colors
|
imagebaker/utils/vis.py
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
from imagebaker.core.defs import PredictionResult
|
2
|
+
|
3
|
+
import cv2
|
4
|
+
import numpy as np
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
|
8
|
+
def annotate_detection(
|
9
|
+
image: np.ndarray,
|
10
|
+
results: List[PredictionResult],
|
11
|
+
color_map: dict[str, tuple[int, int, int]],
|
12
|
+
box_thickness: int = 2,
|
13
|
+
font_face: int = cv2.FONT_HERSHEY_SIMPLEX,
|
14
|
+
text_scale: float = 0.5,
|
15
|
+
text_thickness: int = 1,
|
16
|
+
) -> np.ndarray:
|
17
|
+
"""
|
18
|
+
Draw bounding boxes and labels on the image
|
19
|
+
|
20
|
+
Args:
|
21
|
+
image: The original image as a numpy array
|
22
|
+
results: List of PredictionResult objects
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Annotated image as a numpy array
|
26
|
+
"""
|
27
|
+
annotated_image = image.copy()
|
28
|
+
|
29
|
+
for result in results:
|
30
|
+
# Extract data from result
|
31
|
+
box = result.rectangle # [x1, y1, x2, y2]
|
32
|
+
score = result.score
|
33
|
+
class_name = result.class_name
|
34
|
+
|
35
|
+
if not box:
|
36
|
+
continue
|
37
|
+
|
38
|
+
# Get color for this class
|
39
|
+
color = color_map.get(
|
40
|
+
result.class_name, (0, 255, 0)
|
41
|
+
) # Default to green if not found
|
42
|
+
|
43
|
+
# Draw bounding box
|
44
|
+
cv2.rectangle(
|
45
|
+
annotated_image,
|
46
|
+
(box[0], box[1]),
|
47
|
+
(box[2], box[3]),
|
48
|
+
color,
|
49
|
+
box_thickness,
|
50
|
+
)
|
51
|
+
|
52
|
+
# Prepare label text with class name and score
|
53
|
+
label_text = f"{class_name}: {score:.2f}"
|
54
|
+
|
55
|
+
# Calculate text size to create background rectangle
|
56
|
+
(text_width, text_height), baseline = cv2.getTextSize(
|
57
|
+
label_text,
|
58
|
+
font_face,
|
59
|
+
text_scale,
|
60
|
+
text_thickness,
|
61
|
+
)
|
62
|
+
|
63
|
+
# Draw text background
|
64
|
+
cv2.rectangle(
|
65
|
+
annotated_image,
|
66
|
+
(box[0], box[1] - text_height - 5),
|
67
|
+
(box[0] + text_width, box[1]),
|
68
|
+
color,
|
69
|
+
-1, # Fill the rectangle
|
70
|
+
)
|
71
|
+
|
72
|
+
# Draw text
|
73
|
+
cv2.putText(
|
74
|
+
annotated_image,
|
75
|
+
label_text,
|
76
|
+
(box[0], box[1] - 5),
|
77
|
+
font_face,
|
78
|
+
text_scale,
|
79
|
+
(255, 255, 255), # White text
|
80
|
+
text_thickness,
|
81
|
+
)
|
82
|
+
|
83
|
+
return annotated_image
|
84
|
+
|
85
|
+
|
86
|
+
def annotate_segmentation(
|
87
|
+
image: np.ndarray,
|
88
|
+
results: List[PredictionResult],
|
89
|
+
color_map: dict[int, tuple[int, int, int]],
|
90
|
+
contour_thickness: int = 2,
|
91
|
+
mask_opacity: float = 0.5,
|
92
|
+
font_face: int = cv2.FONT_HERSHEY_SIMPLEX,
|
93
|
+
text_scale: float = 0.5,
|
94
|
+
text_thickness: int = 1,
|
95
|
+
) -> np.ndarray:
|
96
|
+
"""
|
97
|
+
Draw segmentation masks and contours on the image
|
98
|
+
"""
|
99
|
+
annotated_image = image.copy()
|
100
|
+
mask_overlay = np.zeros_like(image)
|
101
|
+
|
102
|
+
for i, result in enumerate(results):
|
103
|
+
if (result.polygon is not None) or not result.mask:
|
104
|
+
continue
|
105
|
+
|
106
|
+
# Get color for this mask
|
107
|
+
color_idx = i % len(color_map)
|
108
|
+
color = color_map[color_idx]
|
109
|
+
|
110
|
+
# Create mask from polygons
|
111
|
+
mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)
|
112
|
+
for poly in result.polygon:
|
113
|
+
poly_np = np.array(poly, dtype=np.int32).reshape((-1, 1, 2))
|
114
|
+
cv2.fillPoly(mask, [poly_np], 1)
|
115
|
+
|
116
|
+
# Apply color to mask overlay
|
117
|
+
color_mask = np.zeros_like(image)
|
118
|
+
color_mask[mask == 1] = color
|
119
|
+
mask_overlay = cv2.addWeighted(mask_overlay, 1.0, color_mask, 1.0, 0)
|
120
|
+
|
121
|
+
# Draw contours
|
122
|
+
for poly in result.polygon:
|
123
|
+
poly_np = np.array(poly, dtype=np.int32).reshape((-1, 1, 2))
|
124
|
+
cv2.polylines(
|
125
|
+
annotated_image,
|
126
|
+
[poly_np],
|
127
|
+
True,
|
128
|
+
color,
|
129
|
+
contour_thickness,
|
130
|
+
)
|
131
|
+
|
132
|
+
# Add label text
|
133
|
+
label_position = (
|
134
|
+
result.polygon[0][0] if result.polygon and result.polygon[0] else [10, 10]
|
135
|
+
)
|
136
|
+
label_text = f"{result.class_id}: {result.score:.2f}"
|
137
|
+
|
138
|
+
# Draw text background
|
139
|
+
(text_width, text_height), baseline = cv2.getTextSize(
|
140
|
+
label_text,
|
141
|
+
font_face,
|
142
|
+
text_scale,
|
143
|
+
text_thickness,
|
144
|
+
)
|
145
|
+
|
146
|
+
cv2.rectangle(
|
147
|
+
annotated_image,
|
148
|
+
(label_position[0], label_position[1] - text_height - 5),
|
149
|
+
(label_position[0] + text_width, label_position[1]),
|
150
|
+
color,
|
151
|
+
-1, # Fill the rectangle
|
152
|
+
)
|
153
|
+
|
154
|
+
# Draw text
|
155
|
+
cv2.putText(
|
156
|
+
annotated_image,
|
157
|
+
label_text,
|
158
|
+
(label_position[0], label_position[1] - 5),
|
159
|
+
font_face,
|
160
|
+
text_scale,
|
161
|
+
(255, 255, 255), # White text
|
162
|
+
text_thickness,
|
163
|
+
)
|
164
|
+
|
165
|
+
# Blend mask overlay with original image
|
166
|
+
annotated_image = cv2.addWeighted(
|
167
|
+
annotated_image,
|
168
|
+
1.0,
|
169
|
+
mask_overlay,
|
170
|
+
mask_opacity,
|
171
|
+
0,
|
172
|
+
)
|
173
|
+
|
174
|
+
return annotated_image
|
imagebaker/window/main_window.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
from imagebaker.core.configs import LayerConfig, CanvasConfig
|
2
2
|
from imagebaker import logger
|
3
3
|
from imagebaker.tabs import LayerifyTab, BakerTab
|
4
|
+
from imagebaker import __version__
|
5
|
+
|
4
6
|
|
5
7
|
from PySide6.QtCore import Qt, QTimer
|
6
8
|
from PySide6.QtWidgets import (
|
@@ -37,7 +39,7 @@ class MainWindow(QMainWindow):
|
|
37
39
|
def init_ui(self):
|
38
40
|
"""Initialize the main window and set up tabs."""
|
39
41
|
try:
|
40
|
-
self.setWindowTitle("Image Baker")
|
42
|
+
self.setWindowTitle(f"Image Baker v{__version__}")
|
41
43
|
self.setGeometry(100, 100, 1200, 800)
|
42
44
|
|
43
45
|
self.status_bar = self.statusBar()
|
@@ -49,10 +49,23 @@ class BakerWorker(QObject):
|
|
49
49
|
top_left = QPointF(sys.maxsize, sys.maxsize)
|
50
50
|
bottom_right = QPointF(-sys.maxsize, -sys.maxsize)
|
51
51
|
|
52
|
+
# contains all states in currenct step
|
52
53
|
for state in states:
|
53
54
|
layer = self._get_layer(state.layer_id)
|
54
55
|
if layer and layer.visible and not layer.image.isNull():
|
56
|
+
update_opacities = False
|
57
|
+
logger.debug(
|
58
|
+
f"Updating layer {layer.layer_name} with state: {state}"
|
59
|
+
)
|
60
|
+
|
61
|
+
if (
|
62
|
+
layer.edge_width != state.edge_width
|
63
|
+
or layer.edge_opacity != state.edge_opacity
|
64
|
+
):
|
65
|
+
update_opacities = True
|
55
66
|
layer.layer_state = state
|
67
|
+
if update_opacities:
|
68
|
+
layer._apply_edge_opacity()
|
56
69
|
layer.update()
|
57
70
|
|
58
71
|
transform = QTransform()
|
@@ -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
|
@@ -35,7 +35,7 @@ Requires-Dist: mkdocs-awesome-pages-plugin; extra == "docs"
|
|
35
35
|

|
36
36
|
<!--  -->
|
37
37
|

|
38
|
-
|
38
|
+
[](https://pypi.org/imagebaker/)
|
39
39
|
|
40
40
|
<p align="center">
|
41
41
|
<img src="assets/demo.gif" alt="Centered Demo" />
|
@@ -0,0 +1,43 @@
|
|
1
|
+
imagebaker/__init__.py,sha256=zrrxwyzuqVNeIu3rVPrOGYf6SCd5kWsoGcdqqUfsYX4,258
|
2
|
+
imagebaker/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
imagebaker/core/configs/__init__.py,sha256=iyR_GOVMFw3XJSm7293YfyTnaLZa7pLQMfn5tGxVofI,31
|
4
|
+
imagebaker/core/configs/configs.py,sha256=-qy7vmYaaUk3bh49pwsBig8de_3Y2JWjTDeyWcGQods,5130
|
5
|
+
imagebaker/core/defs/__init__.py,sha256=NqV7gYIlRkaS7nx_UTNPSNZbdPrx4w-VurKOKyRLbKY,28
|
6
|
+
imagebaker/core/defs/defs.py,sha256=-ZItfJdWaK9yFSuFn2LmQ4ncqukAZ_hvYFgE44HUdIo,8394
|
7
|
+
imagebaker/core/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
imagebaker/core/plugins/base_plugin.py,sha256=ROa1HTwV5LgGL-40CHKk_5MZYI5QAT1MzpYO7Fx-9P0,1084
|
9
|
+
imagebaker/core/plugins/cosine_plugin.py,sha256=IXBfvaoxrvf-hytg_dT1zFmOfDbcWXBZ7NvIFPJ2tWQ,1251
|
10
|
+
imagebaker/layers/__init__.py,sha256=q1kUDHhUXEGBOdu6CHDfqCnE2mraLHRqh0DFHYTbnRY,158
|
11
|
+
imagebaker/layers/annotable_layer.py,sha256=ejBp6nooLtHs8c_G6lOsTJmSwBT404F_dmPsSkjvdEQ,37167
|
12
|
+
imagebaker/layers/base_layer.py,sha256=1K7Nt6OPITrILj-p4I6Jf0eaesCpdeecOXGj_8oAQb8,30650
|
13
|
+
imagebaker/layers/canvas_layer.py,sha256=7eDo0UHXRRZ5BX3BciPX88JsumzmwVemtS6cBjU9qwM,42115
|
14
|
+
imagebaker/list_views/__init__.py,sha256=Aa9slE6do8eYgZp77wrofpd_mlBDwxgF3adMyHYFanE,144
|
15
|
+
imagebaker/list_views/annotation_list.py,sha256=HGV6lGlkFjvJvvGnCcLuX1kkfXA0GL8wKo8jOXSBXec,7527
|
16
|
+
imagebaker/list_views/canvas_list.py,sha256=JYSYR0peGyJFJ6amL1894KsUHETPUkR3qAWdGL50Lbc,6717
|
17
|
+
imagebaker/list_views/image_list.py,sha256=NInkc893FGU7L6oSxy8KrWql-i6RB0BqvkroPAASjVw,4912
|
18
|
+
imagebaker/list_views/layer_list.py,sha256=fLx3Ry72fas1W5y_V84hSp41ARneogQN3qjfYTOcpxY,14476
|
19
|
+
imagebaker/list_views/layer_settings.py,sha256=0WVSCm_RSBKo4pCkYU5c2OYjb_sW8x0UUfFC4So26jQ,9752
|
20
|
+
imagebaker/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
+
imagebaker/models/base_model.py,sha256=4RyS4vShqWFHhdQDwYluwTPnRPEXjpZl9UjYY_w8NL0,4203
|
22
|
+
imagebaker/tabs/__init__.py,sha256=ijg7MA17RvcHA2AuZE4OgRJXWxjecaUAlfASKAoCQ6Q,86
|
23
|
+
imagebaker/tabs/baker_tab.py,sha256=yFQRiNmLIua5BvgW7Ysj5FrNbTLhGxqGRTRCABxeTtw,20493
|
24
|
+
imagebaker/tabs/layerify_tab.py,sha256=Qsf9w81-43sB0LZy_vIB4wCzUhHHhoIiarZJvKRxeZ0,32340
|
25
|
+
imagebaker/utils/__init__.py,sha256=I1z5VVEf6QPOMvVkgVHDauQ9ew7tcTVguV4Kdi3Lk4Y,130
|
26
|
+
imagebaker/utils/image.py,sha256=fq7g3DqSdjF9okxZ3fe5kF4Hxn32rqhvVqxy8yI5bnI,3067
|
27
|
+
imagebaker/utils/state_utils.py,sha256=Y2JVRGVfsoffwfA2lsCcqHwIxH_jOrEJAdH-oWfe2XE,3841
|
28
|
+
imagebaker/utils/transform_mask.py,sha256=k8MfTgM5-_U2TvDHQHRelz-leGFX6OcsllV6-J4BKfw,3651
|
29
|
+
imagebaker/utils/utils.py,sha256=MnJ4flxxwZbjROWJ5iKHnJxPSSMbfWRbF9GKfVcKutA,840
|
30
|
+
imagebaker/utils/vis.py,sha256=f7c44gm6g9ja5hgVeXKfOhHzxHdzXcIUwKiA1RZU_F8,4736
|
31
|
+
imagebaker/window/__init__.py,sha256=FIxtUR1qnbQMYzppQv7tEfv1-ueHhpu0Z7xuWZR794w,44
|
32
|
+
imagebaker/window/app.py,sha256=e6FGO_BnvkiQC9JN3AmqkgbF72zzZS0hc7PFc43QiVc,4725
|
33
|
+
imagebaker/window/main_window.py,sha256=gpJ7DDuPmxhHh_6Rv3YH2J_1AqG7-NM8R3tKNYhFT3E,7030
|
34
|
+
imagebaker/workers/__init__.py,sha256=XfXENwAYyNg9q_zR-gOsYJGjzwg_iIb_gING8ydnp9c,154
|
35
|
+
imagebaker/workers/baker_worker.py,sha256=EJTL4ln09NuntFpu0o-Hfk0vCtDxpKqJxJcmtgTnMwo,11297
|
36
|
+
imagebaker/workers/layerify_worker.py,sha256=EOqKvhdACtf3y5Ljy6M7MvddAjlZW5DNfBFMtNPD-us,3223
|
37
|
+
imagebaker/workers/model_worker.py,sha256=Tlg6_D977iK-kuGCNdQY4OnGiP8QqWY7adpRNXZw4rA,1636
|
38
|
+
imagebaker-0.0.50.dist-info/LICENSE,sha256=1vkysFPOnT7y4LsoFTv9YsopIrQvBc2l6vUOfv4KKLc,1082
|
39
|
+
imagebaker-0.0.50.dist-info/METADATA,sha256=Zsl37tIGiV9wrQ2zfiaubhmPbhOMucos2V_fF5mtbXU,6829
|
40
|
+
imagebaker-0.0.50.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
41
|
+
imagebaker-0.0.50.dist-info/entry_points.txt,sha256=IDjZHJCiiHpH5IUTByT2en0nMbnnnlrJZ5FPFehUvQM,61
|
42
|
+
imagebaker-0.0.50.dist-info/top_level.txt,sha256=Gg-eILTlqJXwVQr0saSwsx3-H4SPdZ2agBZaufe194s,11
|
43
|
+
imagebaker-0.0.50.dist-info/RECORD,,
|
@@ -1,41 +0,0 @@
|
|
1
|
-
imagebaker/__init__.py,sha256=6so9hrBqCwTtCvUvaNB-hBAtmag8RbMJz5irhPANmD4,128
|
2
|
-
imagebaker/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
imagebaker/core/configs/__init__.py,sha256=iyR_GOVMFw3XJSm7293YfyTnaLZa7pLQMfn5tGxVofI,31
|
4
|
-
imagebaker/core/configs/configs.py,sha256=5KRZfLShu4JqV459n5dX4AlbIlkySyAmJr8OzYG4X7Q,4850
|
5
|
-
imagebaker/core/defs/__init__.py,sha256=NqV7gYIlRkaS7nx_UTNPSNZbdPrx4w-VurKOKyRLbKY,28
|
6
|
-
imagebaker/core/defs/defs.py,sha256=nIg2ZQADbpcyC0ZOl54L14yLZ38-SUnfMkklCDfhN3E,8257
|
7
|
-
imagebaker/core/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
imagebaker/core/plugins/base_plugin.py,sha256=ROa1HTwV5LgGL-40CHKk_5MZYI5QAT1MzpYO7Fx-9P0,1084
|
9
|
-
imagebaker/core/plugins/cosine_plugin.py,sha256=IXBfvaoxrvf-hytg_dT1zFmOfDbcWXBZ7NvIFPJ2tWQ,1251
|
10
|
-
imagebaker/layers/__init__.py,sha256=q1kUDHhUXEGBOdu6CHDfqCnE2mraLHRqh0DFHYTbnRY,158
|
11
|
-
imagebaker/layers/annotable_layer.py,sha256=8Y35JvgSQMBTkvjG8VlD1xQgTsskyMwjU6ETU_xalAw,32819
|
12
|
-
imagebaker/layers/base_layer.py,sha256=uncseSxTbLKnV84hAPbJLDbBMV0nu1gQ4N-SLHoGB6s,25486
|
13
|
-
imagebaker/layers/canvas_layer.py,sha256=47R-g3NAzAQ720tWrIZUYIAupxImdORVYK5BFIRopqo,40414
|
14
|
-
imagebaker/list_views/__init__.py,sha256=Aa9slE6do8eYgZp77wrofpd_mlBDwxgF3adMyHYFanE,144
|
15
|
-
imagebaker/list_views/annotation_list.py,sha256=Wx2MbDGxcGeqss9TccFWVVYvlDo9hsefBMQBi4s72is,7436
|
16
|
-
imagebaker/list_views/canvas_list.py,sha256=JYSYR0peGyJFJ6amL1894KsUHETPUkR3qAWdGL50Lbc,6717
|
17
|
-
imagebaker/list_views/image_list.py,sha256=o4yGNXRffPJOZxd_c9JIK9MVzJkeWtXbZUF7pAVmfzw,4813
|
18
|
-
imagebaker/list_views/layer_list.py,sha256=fLx3Ry72fas1W5y_V84hSp41ARneogQN3qjfYTOcpxY,14476
|
19
|
-
imagebaker/list_views/layer_settings.py,sha256=38E39z-rEdl0YSe2C_k4wd5CgHPETQeJE5VvJLfFQ-k,8454
|
20
|
-
imagebaker/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
-
imagebaker/models/base_model.py,sha256=ZfZ_VgP-jzfmEAdOycVtOYkr03m8EyXWdmK0-50MxIk,4197
|
22
|
-
imagebaker/tabs/__init__.py,sha256=ijg7MA17RvcHA2AuZE4OgRJXWxjecaUAlfASKAoCQ6Q,86
|
23
|
-
imagebaker/tabs/baker_tab.py,sha256=kIVoF4w_Ch1YNdHtHsNZKfr46tZWBDSj5xrrEkF1piM,20752
|
24
|
-
imagebaker/tabs/layerify_tab.py,sha256=L8FqZf5JHHYc8BCRKwLWIRdgUSq2YvS7XiJ5Zd5kkTo,32427
|
25
|
-
imagebaker/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
-
imagebaker/utils/image.py,sha256=fq7g3DqSdjF9okxZ3fe5kF4Hxn32rqhvVqxy8yI5bnI,3067
|
27
|
-
imagebaker/utils/state_utils.py,sha256=sI0R3ht4W_kQGwOpzUdBjy6M4PaKaAQlj-_MM6pidfM,3584
|
28
|
-
imagebaker/utils/transform_mask.py,sha256=k8MfTgM5-_U2TvDHQHRelz-leGFX6OcsllV6-J4BKfw,3651
|
29
|
-
imagebaker/window/__init__.py,sha256=FIxtUR1qnbQMYzppQv7tEfv1-ueHhpu0Z7xuWZR794w,44
|
30
|
-
imagebaker/window/app.py,sha256=e6FGO_BnvkiQC9JN3AmqkgbF72zzZS0hc7PFc43QiVc,4725
|
31
|
-
imagebaker/window/main_window.py,sha256=GM5Pf7wpR8u99FarL7eqyc09Khwi6TWEgka6MvrjP6Y,6978
|
32
|
-
imagebaker/workers/__init__.py,sha256=XfXENwAYyNg9q_zR-gOsYJGjzwg_iIb_gING8ydnp9c,154
|
33
|
-
imagebaker/workers/baker_worker.py,sha256=JyV1Hu4mzbYhmogc7K3U24adklmT3x3V0ZNMxe7iT-w,10697
|
34
|
-
imagebaker/workers/layerify_worker.py,sha256=EOqKvhdACtf3y5Ljy6M7MvddAjlZW5DNfBFMtNPD-us,3223
|
35
|
-
imagebaker/workers/model_worker.py,sha256=Tlg6_D977iK-kuGCNdQY4OnGiP8QqWY7adpRNXZw4rA,1636
|
36
|
-
imagebaker-0.0.48.dist-info/LICENSE,sha256=1vkysFPOnT7y4LsoFTv9YsopIrQvBc2l6vUOfv4KKLc,1082
|
37
|
-
imagebaker-0.0.48.dist-info/METADATA,sha256=RpP88IOsQrk0ju2vBDfwcUpC9D6swAk-FDCC4srIg1U,6736
|
38
|
-
imagebaker-0.0.48.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
39
|
-
imagebaker-0.0.48.dist-info/entry_points.txt,sha256=IDjZHJCiiHpH5IUTByT2en0nMbnnnlrJZ5FPFehUvQM,61
|
40
|
-
imagebaker-0.0.48.dist-info/top_level.txt,sha256=Gg-eILTlqJXwVQr0saSwsx3-H4SPdZ2agBZaufe194s,11
|
41
|
-
imagebaker-0.0.48.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|