PyImageLabeling 1.0.0__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.
- PyImageLabeling/__init__.py +22 -0
- PyImageLabeling/config.json +289 -0
- PyImageLabeling/controller/Controller.py +25 -0
- PyImageLabeling/controller/Events.py +147 -0
- PyImageLabeling/controller/FileEvents.py +69 -0
- PyImageLabeling/controller/ImageEvents.py +32 -0
- PyImageLabeling/controller/LabelEvents.py +219 -0
- PyImageLabeling/controller/LabelingEvents.py +123 -0
- PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
- PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
- PyImageLabeling/controller/settings/EraserSetting.py +73 -0
- PyImageLabeling/controller/settings/LabelSetting.py +91 -0
- PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
- PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
- PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
- PyImageLabeling/icons/apply.png +0 -0
- PyImageLabeling/icons/asterisk-green.png +0 -0
- PyImageLabeling/icons/asterisk-red.png +0 -0
- PyImageLabeling/icons/back.png +0 -0
- PyImageLabeling/icons/border.png +0 -0
- PyImageLabeling/icons/cancel.png +0 -0
- PyImageLabeling/icons/cleaner.png +0 -0
- PyImageLabeling/icons/close.png +0 -0
- PyImageLabeling/icons/down.png +0 -0
- PyImageLabeling/icons/ellipse.png +0 -0
- PyImageLabeling/icons/eraser.png +0 -0
- PyImageLabeling/icons/filling.png +0 -0
- PyImageLabeling/icons/logoMAIA.png +0 -0
- PyImageLabeling/icons/magic.png +0 -0
- PyImageLabeling/icons/maia.png +0 -0
- PyImageLabeling/icons/maia1.png +0 -0
- PyImageLabeling/icons/maia3.ico +0 -0
- PyImageLabeling/icons/maia_icon.png +0 -0
- PyImageLabeling/icons/move.png +0 -0
- PyImageLabeling/icons/opacity.png +0 -0
- PyImageLabeling/icons/open_image.png +0 -0
- PyImageLabeling/icons/open_layer.png +0 -0
- PyImageLabeling/icons/paint.png +0 -0
- PyImageLabeling/icons/plus.png +0 -0
- PyImageLabeling/icons/polygon.png +0 -0
- PyImageLabeling/icons/rectangle.png +0 -0
- PyImageLabeling/icons/reset.png +0 -0
- PyImageLabeling/icons/save.png +0 -0
- PyImageLabeling/icons/setting.png +0 -0
- PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
- PyImageLabeling/icons/up.png +0 -0
- PyImageLabeling/icons/visibility.png +0 -0
- PyImageLabeling/icons/zoom_minus.png +0 -0
- PyImageLabeling/icons/zoom_plus.png +0 -0
- PyImageLabeling/model/Core.py +795 -0
- PyImageLabeling/model/File/Files.py +166 -0
- PyImageLabeling/model/File/NextImage.py +36 -0
- PyImageLabeling/model/File/PreviousImage.py +19 -0
- PyImageLabeling/model/Image/MoveImage.py +32 -0
- PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
- PyImageLabeling/model/Image/ZoomMinus.py +25 -0
- PyImageLabeling/model/Image/ZoomPlus.py +16 -0
- PyImageLabeling/model/Labeling/ClearAll.py +22 -0
- PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
- PyImageLabeling/model/Labeling/Ellipse.py +350 -0
- PyImageLabeling/model/Labeling/Eraser.py +131 -0
- PyImageLabeling/model/Labeling/MagicPen.py +131 -0
- PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
- PyImageLabeling/model/Labeling/Polygon.py +279 -0
- PyImageLabeling/model/Labeling/Rectangle.py +248 -0
- PyImageLabeling/model/Labeling/Undo.py +12 -0
- PyImageLabeling/model/Model.py +40 -0
- PyImageLabeling/model/Utils.py +40 -0
- PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
- PyImageLabeling/old_version/main.py +2073 -0
- PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
- PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
- PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
- PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
- PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
- PyImageLabeling/old_version/models/PointItem.py +66 -0
- PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
- PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
- PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
- PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
- PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
- PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
- PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
- PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
- PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
- PyImageLabeling/parameters.json +1 -0
- PyImageLabeling/style.css +611 -0
- PyImageLabeling/view/Builder.py +333 -0
- PyImageLabeling/view/QBackgroundItem.py +30 -0
- PyImageLabeling/view/QWidgets.py +10 -0
- PyImageLabeling/view/View.py +226 -0
- PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
- PyImageLabeling/view/__init__.py +0 -0
- pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
- pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
- pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
- pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
- pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
- pypi/publish_pypi.py +18 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from PyQt6.QtCore import Qt
|
|
4
|
+
from PyQt6.QtWidgets import QFileDialog, QProgressDialog, QMessageBox
|
|
5
|
+
from PyQt6.QtGui import QPixmap, QBitmap, QImage
|
|
6
|
+
|
|
7
|
+
from PyImageLabeling.model.Core import Core, KEYWORD_SAVE_LABEL
|
|
8
|
+
from PyImageLabeling.model.Utils import Utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Files(Core):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
def set_view(self, view):
|
|
19
|
+
super().set_view(view)
|
|
20
|
+
|
|
21
|
+
def select_image(self, path_image):
|
|
22
|
+
#remove all overlays#
|
|
23
|
+
#self.clear_all()
|
|
24
|
+
super().select_image(path_image)
|
|
25
|
+
|
|
26
|
+
def save(self):
|
|
27
|
+
print("save")
|
|
28
|
+
if self.save_directory == "":
|
|
29
|
+
# Open a directory
|
|
30
|
+
default_path = Utils.load_parameters()["save"]["path"]
|
|
31
|
+
|
|
32
|
+
file_dialog = QFileDialog()
|
|
33
|
+
current_file_path = file_dialog.getExistingDirectory(
|
|
34
|
+
parent=self.view,
|
|
35
|
+
caption="Save Folder",
|
|
36
|
+
directory=default_path)
|
|
37
|
+
|
|
38
|
+
if len(current_file_path) == 0: return
|
|
39
|
+
|
|
40
|
+
data = Utils.load_parameters()
|
|
41
|
+
data["save"]["path"] = current_file_path
|
|
42
|
+
Utils.save_parameters(data)
|
|
43
|
+
self.save_directory = current_file_path
|
|
44
|
+
|
|
45
|
+
super().save()
|
|
46
|
+
|
|
47
|
+
def load(self):
|
|
48
|
+
print("load")
|
|
49
|
+
default_path = Utils.load_parameters()["load"]["path"]
|
|
50
|
+
|
|
51
|
+
file_dialog = QFileDialog()
|
|
52
|
+
current_file_path = file_dialog.getExistingDirectory(
|
|
53
|
+
parent=self.view,
|
|
54
|
+
caption="Open Folder",
|
|
55
|
+
directory=default_path)
|
|
56
|
+
|
|
57
|
+
if len(current_file_path) == 0: return
|
|
58
|
+
current_file_path = current_file_path + os.sep
|
|
59
|
+
data = Utils.load_parameters()
|
|
60
|
+
data["load"]["path"] = os.path.dirname(current_file_path)
|
|
61
|
+
Utils.save_parameters(data)
|
|
62
|
+
|
|
63
|
+
# Update the model with the good images
|
|
64
|
+
# The model variables is update in this method: file_paths and image_items
|
|
65
|
+
print("current_file_path:", current_file_path)
|
|
66
|
+
current_files = [current_file_path+os.sep+f for f in os.listdir(current_file_path)]
|
|
67
|
+
current_files_to_add = []
|
|
68
|
+
print("current_files:", current_files)
|
|
69
|
+
labels_json = None
|
|
70
|
+
labels_images = []
|
|
71
|
+
for file in current_files:
|
|
72
|
+
print("file:", file)
|
|
73
|
+
if file in self.file_paths:
|
|
74
|
+
continue
|
|
75
|
+
if file.lower().endswith((".png", ".jpg", ".jpeg", ".gif")):
|
|
76
|
+
if KEYWORD_SAVE_LABEL in file:
|
|
77
|
+
# It is a label file
|
|
78
|
+
labels_images.append(file)
|
|
79
|
+
else:
|
|
80
|
+
# It is a image
|
|
81
|
+
print("file2:", file)
|
|
82
|
+
self.file_paths.append(file)
|
|
83
|
+
self.image_items[file] = None
|
|
84
|
+
current_files_to_add.append(file)
|
|
85
|
+
elif file.endswith("labels.json"):
|
|
86
|
+
labels_json = file # Load it later
|
|
87
|
+
self.view.file_bar_add(current_files_to_add)
|
|
88
|
+
|
|
89
|
+
# Activate previous and next buttons
|
|
90
|
+
for button_name in self.view.buttons_file_bar:
|
|
91
|
+
self.view.buttons_file_bar[button_name].setEnabled(True)
|
|
92
|
+
|
|
93
|
+
# Select the first item in the list if we have some images and no image selected
|
|
94
|
+
if self.view.file_bar_list.count() > 0 and self.view.file_bar_list.currentRow() == -1:
|
|
95
|
+
self.view.file_bar_list.setCurrentRow(0)
|
|
96
|
+
|
|
97
|
+
if (len(labels_images) != 0 and labels_json is None) or \
|
|
98
|
+
(len(labels_images) == 0 and labels_json is not None):
|
|
99
|
+
self.controller.error_message("Load Error", "The labeling image or the `labels.json` file is missing !")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if len(labels_images) == 0 and labels_json is None:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if labels_json is not None and self.get_edited():
|
|
106
|
+
msgBox = QMessageBox(self.view.zoomable_graphics_view)
|
|
107
|
+
msgBox.setWindowTitle("Load")
|
|
108
|
+
msgBox.setText("Are you sure you want to load the new labeling overview without save our previous works ?")
|
|
109
|
+
msgBox.setInformativeText("All previous works not saved will be reset.")
|
|
110
|
+
msgBox.setStandardButtons(QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes)
|
|
111
|
+
msgBox.setDefaultButton(QMessageBox.StandardButton.No)
|
|
112
|
+
msgBox.setModal(True)
|
|
113
|
+
result = msgBox.exec()
|
|
114
|
+
|
|
115
|
+
if result == QMessageBox.StandardButton.No:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Reset all labeling overview in the model
|
|
119
|
+
self.reset()
|
|
120
|
+
self.labeling_overview_was_loaded.clear()
|
|
121
|
+
self.labeling_overview_file_paths.clear()
|
|
122
|
+
|
|
123
|
+
# Reset the view
|
|
124
|
+
to_delete = []
|
|
125
|
+
for label_id in self.view.container_label_bar_temporary:
|
|
126
|
+
widget, separator = self.view.container_label_bar_temporary[label_id]
|
|
127
|
+
|
|
128
|
+
widget.hide()
|
|
129
|
+
self.view.label_bar_layout.removeWidget(widget)
|
|
130
|
+
separator.hide()
|
|
131
|
+
self.view.label_bar_layout.removeWidget(separator)
|
|
132
|
+
|
|
133
|
+
# Clean up the view dictionaries
|
|
134
|
+
to_delete.append(label_id)
|
|
135
|
+
if label_id in self.view.buttons_label_bar_temporary:
|
|
136
|
+
del self.view.buttons_label_bar_temporary[label_id]
|
|
137
|
+
|
|
138
|
+
for label_id in to_delete:
|
|
139
|
+
del self.view.container_label_bar_temporary[label_id]
|
|
140
|
+
|
|
141
|
+
# Clear the labels in the model
|
|
142
|
+
self.label_items.clear()
|
|
143
|
+
|
|
144
|
+
# Reset the icon file
|
|
145
|
+
self.update_icon_file()
|
|
146
|
+
|
|
147
|
+
# We load the overview labelings
|
|
148
|
+
if labels_images is not None:
|
|
149
|
+
for file in labels_images:
|
|
150
|
+
self.load_labels_images(file)
|
|
151
|
+
|
|
152
|
+
# Load the labels and initalize the first one
|
|
153
|
+
if labels_json is not None:
|
|
154
|
+
self.load_labels_json(labels_json)
|
|
155
|
+
first_id = list(self.get_label_items().keys())[0]
|
|
156
|
+
self.controller.select_label(first_id)
|
|
157
|
+
|
|
158
|
+
# Now, we have to save in this directory :)
|
|
159
|
+
self.save_directory = current_file_path
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
from PyImageLabeling.model.Core import Core
|
|
3
|
+
from PyQt6.QtCore import Qt
|
|
4
|
+
from PyQt6.QtWidgets import QFileDialog
|
|
5
|
+
from PyQt6.QtGui import QPixmap
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class NextImage(Core):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
def next_image(self):
|
|
13
|
+
current_row = self.view.file_bar_list.currentRow()
|
|
14
|
+
total_images = len(self.image_items)
|
|
15
|
+
if total_images == 1: return
|
|
16
|
+
|
|
17
|
+
next_row = 0 if current_row == -1 else (current_row + 1) % total_images
|
|
18
|
+
self.view.file_bar_list.setCurrentRow(next_row)
|
|
19
|
+
|
|
20
|
+
# Calculate next row (with wrap-around)
|
|
21
|
+
# if current_row == -1: # No selection
|
|
22
|
+
# next_row = 0
|
|
23
|
+
# else:
|
|
24
|
+
# next_row = (current_row + 1) % total_images
|
|
25
|
+
|
|
26
|
+
# # Select the next item in the list
|
|
27
|
+
#self.view.file_bar_list.setCurrentRow(next_row)
|
|
28
|
+
|
|
29
|
+
# Load the next image
|
|
30
|
+
#file_path = self.loaded_image_paths[next_row]
|
|
31
|
+
#image = QPixmap(file_path)
|
|
32
|
+
#if not image.isNull():
|
|
33
|
+
# self.load_image(image)
|
|
34
|
+
# print(f"Next image loaded: {os.path.basename(file_path)}")
|
|
35
|
+
#else:
|
|
36
|
+
# self.error_message("Load Image", f"Could not load the image: {os.path.basename(file_path)}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
from PyImageLabeling.model.Core import Core
|
|
3
|
+
from PyQt6.QtCore import Qt
|
|
4
|
+
from PyQt6.QtWidgets import QFileDialog
|
|
5
|
+
from PyQt6.QtGui import QPixmap
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class PreviousImage(Core):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
def previous_image(self):
|
|
13
|
+
current_row = self.view.file_bar_list.currentRow()
|
|
14
|
+
total_images = len(self.image_items)
|
|
15
|
+
if total_images == 1: return
|
|
16
|
+
|
|
17
|
+
next_row = total_images - 1 if current_row == -1 else (current_row - 1) % total_images
|
|
18
|
+
self.view.file_bar_list.setCurrentRow(next_row)
|
|
19
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from PyImageLabeling.model.Core import Core
|
|
4
|
+
from PyQt6.QtWidgets import QGraphicsView
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MoveImage(Core):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.move_tool_activation = False
|
|
11
|
+
self.last_cursor = None
|
|
12
|
+
|
|
13
|
+
def move_image(self):
|
|
14
|
+
self.checked_button = self.move_image.__name__
|
|
15
|
+
|
|
16
|
+
def start_move_tool(self, event):
|
|
17
|
+
self.last_cursor = self.view.zoomable_graphics_view.viewport().cursor()
|
|
18
|
+
self.view.zoomable_graphics_view.change_cursor("move")
|
|
19
|
+
self.last_mouse_pos = event.scenePos()
|
|
20
|
+
self.move_tool_activation = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def move_move_tool(self, event):
|
|
24
|
+
if self.move_tool_activation is True:
|
|
25
|
+
self.view.zoomable_graphics_view.horizontalScrollBar().setValue(int(self.view.zoomable_graphics_view.horizontalScrollBar().value() - (event.scenePos().x() - self.last_mouse_pos.x())))
|
|
26
|
+
self.view.zoomable_graphics_view.verticalScrollBar().setValue(int(self.view.zoomable_graphics_view.verticalScrollBar().value() - (event.scenePos().y() - self.last_mouse_pos.y())))
|
|
27
|
+
|
|
28
|
+
def end_move_tool(self):
|
|
29
|
+
self.view.zoomable_graphics_view.change_cursor("move")
|
|
30
|
+
self.view.zoomable_graphics_view.viewport().setCursor(self.last_cursor)
|
|
31
|
+
self.move_tool_activation = False
|
|
32
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
from PyImageLabeling.model.Core import Core
|
|
3
|
+
from PyQt6.QtCore import Qt
|
|
4
|
+
|
|
5
|
+
class ResetMoveZoomImage (Core):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__()
|
|
8
|
+
|
|
9
|
+
def set_view(self, view):
|
|
10
|
+
super().set_view(view)
|
|
11
|
+
|
|
12
|
+
def reset_move_zoom_image(self):
|
|
13
|
+
self.zoomable_graphics_view.resetTransform()
|
|
14
|
+
self.zoomable_graphics_view.setSceneRect(self.image_qrectf)
|
|
15
|
+
self.view.zoom_factor = self.view.initial_zoom_factor
|
|
16
|
+
#self.zoomable_graphics_view.fitInView(self.pixmap_item.boundingRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from PyImageLabeling.model.Core import Core
|
|
4
|
+
from PyQt6.QtCore import QTimer
|
|
5
|
+
|
|
6
|
+
class ZoomMinus(Core):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self.current_zoom_type = None
|
|
10
|
+
|
|
11
|
+
def zoom_minus(self):
|
|
12
|
+
self.checked_button = self.zoom_minus.__name__
|
|
13
|
+
|
|
14
|
+
def start_zoom_minus(self):
|
|
15
|
+
self.view.zoomable_graphics_view.change_cursor("zoom_minus")
|
|
16
|
+
self.view.zoomable_graphics_view.zoom(self.view.minus_zoom_factor-0.2)
|
|
17
|
+
|
|
18
|
+
#self.apply_zoom_minus()
|
|
19
|
+
#self.zoom_timer_plus.start()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
#def end_zoom_minus(self):
|
|
24
|
+
# self.view.zoomable_graphics_view.change_cursor("zoom_minus")
|
|
25
|
+
# self.zoom_timer_plus.stop()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from PyImageLabeling.model.Core import Core
|
|
4
|
+
from PyQt6.QtCore import QTimer
|
|
5
|
+
|
|
6
|
+
class ZoomPlus(Core):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
def zoom_plus(self):
|
|
11
|
+
self.checked_button = self.zoom_plus.__name__
|
|
12
|
+
|
|
13
|
+
def start_zoom_plus(self):
|
|
14
|
+
self.view.zoomable_graphics_view.change_cursor("zoom_plus")
|
|
15
|
+
self.view.zoomable_graphics_view.zoom(self.view.plus_zoom_factor+0.2)
|
|
16
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from PyQt6.QtGui import QPainter, QBitmap, QImage, QPixmap
|
|
2
|
+
from PyImageLabeling.model.Core import Core
|
|
3
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4
|
+
|
|
5
|
+
class ClearAll(Core):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__()
|
|
8
|
+
|
|
9
|
+
def clear_all(self):
|
|
10
|
+
msgBox = QMessageBox(self.view.zoomable_graphics_view)
|
|
11
|
+
msgBox.setWindowTitle("Clear All")
|
|
12
|
+
msgBox.setText("Are you sure you want to delete the selected label ?")
|
|
13
|
+
msgBox.setInformativeText("The `Undo` method will be reset.")
|
|
14
|
+
msgBox.setStandardButtons(QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes)
|
|
15
|
+
msgBox.setDefaultButton(QMessageBox.StandardButton.No)
|
|
16
|
+
msgBox.setModal(True)
|
|
17
|
+
result = msgBox.exec()
|
|
18
|
+
if result == QMessageBox.StandardButton.Yes:
|
|
19
|
+
self.get_current_image_item().get_labeling_overlay().reset()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QMessageBox, QProgressDialog, QGraphicsItem
|
|
2
|
+
from PyQt6.QtCore import Qt, QPointF, QPoint, QLine, QRectF, QRect
|
|
3
|
+
from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen
|
|
4
|
+
from PyImageLabeling.model.Core import Core
|
|
5
|
+
import numpy as np
|
|
6
|
+
import cv2
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from PyImageLabeling.model.Utils import Utils
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
TOLERENCE_PARAMETERS = {
|
|
14
|
+
1: {'canny_low': 100, 'canny_high': 200, 'blur_kernel': 3, 'dilate_iter': 0, 'min_area': 50},
|
|
15
|
+
2: {'canny_low': 80, 'canny_high': 180, 'blur_kernel': 3, 'dilate_iter': 1, 'min_area': 30},
|
|
16
|
+
3: {'canny_low': 70, 'canny_high': 160, 'blur_kernel': 3, 'dilate_iter': 1, 'min_area': 20},
|
|
17
|
+
4: {'canny_low': 60, 'canny_high': 140, 'blur_kernel': 5, 'dilate_iter': 1, 'min_area': 15},
|
|
18
|
+
5: {'canny_low': 50, 'canny_high': 150, 'blur_kernel': 5, 'dilate_iter': 1, 'min_area': 10},
|
|
19
|
+
6: {'canny_low': 40, 'canny_high': 120, 'blur_kernel': 5, 'dilate_iter': 2, 'min_area': 8},
|
|
20
|
+
7: {'canny_low': 30, 'canny_high': 100, 'blur_kernel': 7, 'dilate_iter': 2, 'min_area': 5},
|
|
21
|
+
8: {'canny_low': 25, 'canny_high': 80, 'blur_kernel': 7, 'dilate_iter': 2, 'min_area': 3},
|
|
22
|
+
9: {'canny_low': 20, 'canny_high': 60, 'blur_kernel': 9, 'dilate_iter': 3, 'min_area': 2},
|
|
23
|
+
10: {'canny_low': 15, 'canny_high': 40, 'blur_kernel': 9, 'dilate_iter': 3, 'min_area': 1}
|
|
24
|
+
}
|
|
25
|
+
class ContourItem():
|
|
26
|
+
|
|
27
|
+
def __init__(self, points, color):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.points = points
|
|
30
|
+
self.color = color
|
|
31
|
+
|
|
32
|
+
self.qrectf = QRectF(points[0][0][0], points[0][0][1], 1, 1)
|
|
33
|
+
for point in points:
|
|
34
|
+
self.qrectf = self.qrectf.united(QRectF(point[0][0], point[0][1], 1, 1))
|
|
35
|
+
|
|
36
|
+
self.qrect = self.qrectf.toRect()
|
|
37
|
+
contour_numpy_pixels = np.zeros((self.qrect.height(), self.qrect.width(), 4), dtype=np.uint8)
|
|
38
|
+
|
|
39
|
+
for point in self.points:
|
|
40
|
+
point[0][0], point[0][1] = point[0][0]-self.qrect.x(), point[0][1]-self.qrect.y()
|
|
41
|
+
|
|
42
|
+
cv2.drawContours(contour_numpy_pixels, [self.points], -1, self.color.getRgb(), -1)
|
|
43
|
+
cv2.drawContours(contour_numpy_pixels, [self.points], 0, self.color.getRgb(), 1)
|
|
44
|
+
|
|
45
|
+
self.image_pixmap = QPixmap.fromImage(QImage(contour_numpy_pixels.data, self.qrect.width(), self.qrect.height(), self.qrect.width() * 4, QImage.Format.Format_RGBA8888))
|
|
46
|
+
|
|
47
|
+
def paint_labeling_overlay(self, labeling_overlay_painter):
|
|
48
|
+
labeling_overlay_painter.drawPixmap(self.qrect.x(), self.qrect.y(), self.image_pixmap)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContourFilling(Core):
|
|
53
|
+
def __init__(self):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.contours = [] # It is in list of contours. A contour is a list of points (x, y).
|
|
56
|
+
self.contour_items = [] # QGraphicItem for each contour clicked
|
|
57
|
+
|
|
58
|
+
self.coutour_filling_pixmap = None
|
|
59
|
+
self.coutour_filling_item = None
|
|
60
|
+
|
|
61
|
+
def contour_filling(self):
|
|
62
|
+
self.checked_button = self.contour_filling.__name__
|
|
63
|
+
|
|
64
|
+
def start_contour_filling(self):
|
|
65
|
+
self.view.zoomable_graphics_view.change_cursor("filling")
|
|
66
|
+
self.apply_contour()
|
|
67
|
+
|
|
68
|
+
def end_contour_filling(self):
|
|
69
|
+
# Remove the dislay of all these item
|
|
70
|
+
for item in self.contour_items:
|
|
71
|
+
self.zoomable_graphics_view.scene.removeItem(item)
|
|
72
|
+
self.contour_items.clear()
|
|
73
|
+
|
|
74
|
+
def remove_contour(self):
|
|
75
|
+
if self.coutour_filling_item is not None:
|
|
76
|
+
self.view.zoomable_graphics_view.scene.removeItem(self.coutour_filling_item)
|
|
77
|
+
self.coutour_filling_item = None
|
|
78
|
+
|
|
79
|
+
def get_contours(self):
|
|
80
|
+
# Convert to grayscale (use OpenCV)
|
|
81
|
+
image_numpy_pixels_gray = cv2.cvtColor(self.get_current_image_item().get_image_numpy_pixels_rgb(), cv2.COLOR_RGB2GRAY)
|
|
82
|
+
# Apply Gaussian blur to reduce noise (kernel size based on tolerance)
|
|
83
|
+
image_numpy_pixels_blurred = cv2.GaussianBlur(image_numpy_pixels_gray, (self.tolerance_parameters["blur_kernel"], self.tolerance_parameters["blur_kernel"]), 0)
|
|
84
|
+
# Apply Canny edge detection with tolerance-based parameters
|
|
85
|
+
image_numpy_pixels_canny = cv2.Canny(image_numpy_pixels_blurred, self.tolerance_parameters["canny_low"], self.tolerance_parameters["canny_high"])
|
|
86
|
+
# Apply dilation to connect nearby edges (iterations based on tolerance)
|
|
87
|
+
if self.tolerance_parameters['dilate_iter'] > 0:
|
|
88
|
+
image_numpy_pixels_canny = cv2.dilate(image_numpy_pixels_canny, np.ones((2, 2), np.uint8), iterations=self.tolerance_parameters['dilate_iter'])
|
|
89
|
+
# Find contours with hierarchy to better handle nested shapes
|
|
90
|
+
contours, _ = cv2.findContours(image_numpy_pixels_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)
|
|
91
|
+
# Filter out small contours based on tolerance level
|
|
92
|
+
contours = [cnt for cnt in contours if cv2.contourArea(cnt) > self.tolerance_parameters["min_area"]]
|
|
93
|
+
return contours
|
|
94
|
+
|
|
95
|
+
def apply_contour(self):
|
|
96
|
+
self.width = self.get_current_image_item().get_width()
|
|
97
|
+
self.height = self.get_current_image_item().get_height()
|
|
98
|
+
|
|
99
|
+
self.tolerance = Utils.load_parameters()["contour_filling"]["tolerance"]
|
|
100
|
+
self.tolerance_parameters = TOLERENCE_PARAMETERS[self.tolerance]
|
|
101
|
+
self.contours = self.get_contours() # It is in list of contours. A contour is a list of points (x, y).
|
|
102
|
+
if len(self.contours) == 0:
|
|
103
|
+
raise ValueError("No contours found !")
|
|
104
|
+
|
|
105
|
+
self.color = self.get_current_label_item().get_color()
|
|
106
|
+
# It is more faster to do that because cv2 loop into the points of contours in c++, not python
|
|
107
|
+
contour_numpy_pixels = np.zeros((self.height, self.width, 4), dtype=np.uint8)
|
|
108
|
+
cv2.drawContours(contour_numpy_pixels, self.contours, -1, self.color.darker(200).getRgb(), 1)
|
|
109
|
+
self.coutour_filling_pixmap = QPixmap.fromImage(QImage(contour_numpy_pixels.data, self.width, self.height, self.width * 4, QImage.Format.Format_RGBA8888))
|
|
110
|
+
self.coutour_filling_item = self.view.zoomable_graphics_view.scene.addPixmap(self.coutour_filling_pixmap)
|
|
111
|
+
self.coutour_filling_item.setZValue(1)
|
|
112
|
+
print("end apply_contour")
|
|
113
|
+
|
|
114
|
+
def find_closest_contour(self, position_x, position_y):
|
|
115
|
+
for contour in self.contours:
|
|
116
|
+
if cv2.pointPolygonTest(contour, (position_x, position_y), False) >= 0:
|
|
117
|
+
return contour
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def fill_contour(self, position):
|
|
121
|
+
# Find the good contour
|
|
122
|
+
self.color = self.get_current_label_item().get_color()
|
|
123
|
+
position_x, position_y = int(position.x()), int(position.y())
|
|
124
|
+
closest_contour = self.find_closest_contour(position_x, position_y)
|
|
125
|
+
if closest_contour is None: return # We are not cliked inside of a contour
|
|
126
|
+
|
|
127
|
+
# Create a little QPixmap of this contour with cv2
|
|
128
|
+
coutour_item = ContourItem(closest_contour.copy(), self.color)
|
|
129
|
+
|
|
130
|
+
# Draw the contour QPixmap on the good labeling overlay
|
|
131
|
+
coutour_item.paint_labeling_overlay(self.get_current_image_item().get_labeling_overlay().get_painter())
|
|
132
|
+
|
|
133
|
+
# Update the labeling overlay
|
|
134
|
+
self.get_current_image_item().update_labeling_overlay()
|
|
135
|
+
|