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.
Files changed (99) hide show
  1. PyImageLabeling/__init__.py +22 -0
  2. PyImageLabeling/config.json +289 -0
  3. PyImageLabeling/controller/Controller.py +25 -0
  4. PyImageLabeling/controller/Events.py +147 -0
  5. PyImageLabeling/controller/FileEvents.py +69 -0
  6. PyImageLabeling/controller/ImageEvents.py +32 -0
  7. PyImageLabeling/controller/LabelEvents.py +219 -0
  8. PyImageLabeling/controller/LabelingEvents.py +123 -0
  9. PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
  10. PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
  11. PyImageLabeling/controller/settings/EraserSetting.py +73 -0
  12. PyImageLabeling/controller/settings/LabelSetting.py +91 -0
  13. PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
  14. PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
  15. PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
  16. PyImageLabeling/icons/apply.png +0 -0
  17. PyImageLabeling/icons/asterisk-green.png +0 -0
  18. PyImageLabeling/icons/asterisk-red.png +0 -0
  19. PyImageLabeling/icons/back.png +0 -0
  20. PyImageLabeling/icons/border.png +0 -0
  21. PyImageLabeling/icons/cancel.png +0 -0
  22. PyImageLabeling/icons/cleaner.png +0 -0
  23. PyImageLabeling/icons/close.png +0 -0
  24. PyImageLabeling/icons/down.png +0 -0
  25. PyImageLabeling/icons/ellipse.png +0 -0
  26. PyImageLabeling/icons/eraser.png +0 -0
  27. PyImageLabeling/icons/filling.png +0 -0
  28. PyImageLabeling/icons/logoMAIA.png +0 -0
  29. PyImageLabeling/icons/magic.png +0 -0
  30. PyImageLabeling/icons/maia.png +0 -0
  31. PyImageLabeling/icons/maia1.png +0 -0
  32. PyImageLabeling/icons/maia3.ico +0 -0
  33. PyImageLabeling/icons/maia_icon.png +0 -0
  34. PyImageLabeling/icons/move.png +0 -0
  35. PyImageLabeling/icons/opacity.png +0 -0
  36. PyImageLabeling/icons/open_image.png +0 -0
  37. PyImageLabeling/icons/open_layer.png +0 -0
  38. PyImageLabeling/icons/paint.png +0 -0
  39. PyImageLabeling/icons/plus.png +0 -0
  40. PyImageLabeling/icons/polygon.png +0 -0
  41. PyImageLabeling/icons/rectangle.png +0 -0
  42. PyImageLabeling/icons/reset.png +0 -0
  43. PyImageLabeling/icons/save.png +0 -0
  44. PyImageLabeling/icons/setting.png +0 -0
  45. PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
  46. PyImageLabeling/icons/up.png +0 -0
  47. PyImageLabeling/icons/visibility.png +0 -0
  48. PyImageLabeling/icons/zoom_minus.png +0 -0
  49. PyImageLabeling/icons/zoom_plus.png +0 -0
  50. PyImageLabeling/model/Core.py +795 -0
  51. PyImageLabeling/model/File/Files.py +166 -0
  52. PyImageLabeling/model/File/NextImage.py +36 -0
  53. PyImageLabeling/model/File/PreviousImage.py +19 -0
  54. PyImageLabeling/model/Image/MoveImage.py +32 -0
  55. PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
  56. PyImageLabeling/model/Image/ZoomMinus.py +25 -0
  57. PyImageLabeling/model/Image/ZoomPlus.py +16 -0
  58. PyImageLabeling/model/Labeling/ClearAll.py +22 -0
  59. PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
  60. PyImageLabeling/model/Labeling/Ellipse.py +350 -0
  61. PyImageLabeling/model/Labeling/Eraser.py +131 -0
  62. PyImageLabeling/model/Labeling/MagicPen.py +131 -0
  63. PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
  64. PyImageLabeling/model/Labeling/Polygon.py +279 -0
  65. PyImageLabeling/model/Labeling/Rectangle.py +248 -0
  66. PyImageLabeling/model/Labeling/Undo.py +12 -0
  67. PyImageLabeling/model/Model.py +40 -0
  68. PyImageLabeling/model/Utils.py +40 -0
  69. PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
  70. PyImageLabeling/old_version/main.py +2073 -0
  71. PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
  72. PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
  73. PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
  74. PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
  75. PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
  76. PyImageLabeling/old_version/models/PointItem.py +66 -0
  77. PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
  78. PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
  79. PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
  80. PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
  81. PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
  82. PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
  83. PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
  84. PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
  85. PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
  86. PyImageLabeling/parameters.json +1 -0
  87. PyImageLabeling/style.css +611 -0
  88. PyImageLabeling/view/Builder.py +333 -0
  89. PyImageLabeling/view/QBackgroundItem.py +30 -0
  90. PyImageLabeling/view/QWidgets.py +10 -0
  91. PyImageLabeling/view/View.py +226 -0
  92. PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
  93. PyImageLabeling/view/__init__.py +0 -0
  94. pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
  95. pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
  96. pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
  97. pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
  98. pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
  99. 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
+