Skip to content

Commit 9735fe8

Browse files
committed
refactor: upgrade to pyqt6
1 parent a3d192d commit 9735fe8

39 files changed

Lines changed: 421 additions & 468 deletions

.cursorrules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
- Use PyQt5 for GUI.
1+
- Use PyQt6 for GUI.
22
- Split code into files when possible.
33
- Make code clean and understandable.
44
- Optimize code for performance and memory usage.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ conda create -n anylabeling python=3.12
6161
conda activate anylabeling
6262
```
6363

64-
- **(For macOS only)** Install PyQt5 using Conda:
64+
- **(For macOS only)** Install PyQt6 using Conda:
6565

6666
```bash
67-
conda install -c conda-forge pyqt==5.15.9
67+
conda install -c conda-forge pyqt=6
6868
```
6969

7070
- Install anylabeling:

anylabeling/app.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sys
1414

1515
import yaml
16-
from PyQt5 import QtCore, QtWidgets
16+
from PyQt6 import QtCore, QtWidgets
1717

1818
from anylabeling.app_info import __appname__
1919
from anylabeling.config import get_config
@@ -174,13 +174,8 @@ def main():
174174
loaded_language = translator.load(":/languages/translations/" + language + ".qm")
175175

176176
# Enable scaling for high dpi screens
177-
QtWidgets.QApplication.setAttribute(
178-
QtCore.Qt.AA_EnableHighDpiScaling, True
179-
) # enable highdpi scaling
180-
QtWidgets.QApplication.setAttribute(
181-
QtCore.Qt.AA_UseHighDpiPixmaps, True
182-
) # use highdpi icons
183-
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
177+
# High DPI scaling is enabled by default in Qt 6
178+
QtCore.QCoreApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
184179

185180
app = QtWidgets.QApplication(sys.argv)
186181
app.processEvents()

anylabeling/resources/resources.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
# Resource object code
44
#
5-
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
5+
# Created by: The Resource Compiler for PyQt6 (Qt v5.15.2)
66
#
77
# WARNING! All changes made in this file will be lost!
88

9-
from PyQt5 import QtCore
9+
from PyQt6 import QtCore
1010

1111
qt_resource_data = b"\
1212
\x00\x00\x68\xc2\

anylabeling/services/auto_labeling/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import ssl
66
from abc import abstractmethod
77

8-
from PyQt5.QtCore import QCoreApplication, QFile, QObject
9-
from PyQt5.QtGui import QImage
8+
from PyQt6.QtCore import QCoreApplication, QFile, QObject
9+
from PyQt6.QtGui import QImage
1010

1111
from .types import AutoLabelingResult
1212
from anylabeling.views.labeling.label_file import LabelFile, LabelFileError

anylabeling/services/auto_labeling/model_manager.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import urllib.request
1212

1313
import yaml
14-
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
15-
from PyQt5.QtCore import QCoreApplication
14+
from PyQt6.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
15+
from PyQt6.QtCore import QCoreApplication
1616

1717
from anylabeling.configs import auto_labeling as auto_labeling_configs
1818
from anylabeling.services.auto_labeling.types import AutoLabelingResult
@@ -144,6 +144,16 @@ def set_output_mode(self, mode):
144144
@pyqtSlot()
145145
def on_model_download_finished(self):
146146
"""Handle model download thread finished"""
147+
if self.model_download_thread:
148+
try:
149+
self.model_download_thread.quit()
150+
if not self.model_download_thread.wait(1000):
151+
logging.warning("Model download thread did not stop in time")
152+
except RuntimeError:
153+
pass
154+
self.model_download_thread = None
155+
self.model_download_worker = None
156+
147157
if self.loaded_model_config and self.loaded_model_config["model"]:
148158
self.new_model_status.emit(self.tr("Model loaded. Ready for labeling."))
149159
self.model_loaded.emit(self.loaded_model_config)
@@ -269,6 +279,12 @@ def load_model(self, config_file):
269279
self.model_download_worker = GenericWorker(self._load_model, model_id)
270280
self.model_download_worker.finished.connect(self.on_model_download_finished)
271281
self.model_download_worker.finished.connect(self.model_download_thread.quit)
282+
self.model_download_worker.finished.connect(
283+
self.model_download_worker.deleteLater
284+
)
285+
self.model_download_thread.finished.connect(
286+
self.model_download_thread.deleteLater
287+
)
272288
self.model_download_worker.moveToThread(self.model_download_thread)
273289
self.model_download_thread.started.connect(self.model_download_worker.run)
274290
self.model_download_thread.start()
@@ -382,6 +398,7 @@ def _load_model(self, model_id):
382398
"""Load and return model info"""
383399
if self.loaded_model_config is not None:
384400
self.loaded_model_config["model"].unload()
401+
# If the model has a thread, it should have been joined in unload()
385402
self.loaded_model_config = None
386403
self.auto_segmentation_model_unselected.emit()
387404

@@ -504,22 +521,25 @@ def predict_shapes_threading(self, image, filename=None):
504521
self.tr("Model is not loaded. Choose a mode to continue.")
505522
)
506523
return
507-
self.new_model_status.emit(self.tr("Inferencing AI model. Please wait..."))
508-
self.prediction_started.emit()
509-
QCoreApplication.processEvents()
510524

511525
with self.model_execution_thread_lock:
526+
# If a model is already running, try to stop it first
512527
if (
513528
self.model_execution_thread is not None
514529
and self.model_execution_thread.isRunning()
515530
):
516-
self.new_model_status.emit(
517-
self.tr(
518-
"Another model is being executed. Please wait for it to finish."
519-
)
520-
)
521-
self.prediction_finished.emit()
522-
return
531+
if hasattr(self.loaded_model_config["model"], "unload"):
532+
self.loaded_model_config["model"].unload()
533+
534+
# Wait for the thread to finish
535+
self.model_execution_thread.quit()
536+
if not self.model_execution_thread.wait(1000):
537+
# If still running, we skip this request to avoid over-queuing
538+
self.prediction_finished.emit()
539+
return
540+
541+
self.new_model_status.emit(self.tr("Inferencing AI model. Please wait..."))
542+
self.prediction_started.emit()
523543

524544
self.model_execution_thread = QThread()
525545
self.model_execution_worker = GenericWorker(
@@ -528,6 +548,15 @@ def predict_shapes_threading(self, image, filename=None):
528548
self.model_execution_worker.finished.connect(
529549
self.model_execution_thread.quit
530550
)
551+
self.model_execution_worker.finished.connect(
552+
self.model_execution_worker.deleteLater
553+
)
554+
self.model_execution_thread.finished.connect(
555+
lambda: setattr(self, "model_execution_thread", None)
556+
)
557+
self.model_execution_thread.finished.connect(
558+
self.model_execution_thread.deleteLater
559+
)
531560
self.model_execution_worker.moveToThread(self.model_execution_thread)
532561
self.model_execution_thread.started.connect(self.model_execution_worker.run)
533562
self.model_execution_thread.start()

anylabeling/services/auto_labeling/segment_anything.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import cv2
66
import onnx
77
import numpy as np
8-
from PyQt5 import QtCore
9-
from PyQt5.QtCore import QThread
10-
from PyQt5.QtCore import QCoreApplication
8+
from PyQt6 import QtCore
9+
from PyQt6.QtCore import QThread
10+
from PyQt6.QtCore import QCoreApplication
1111

1212
from anylabeling.utils import GenericWorker
1313
from anylabeling.views.labeling.shape import Shape
@@ -179,6 +179,10 @@ def post_process(self, masks, label="AUTOLABEL_OBJECT"):
179179
masks:
180180
2-D array of shape ``(H, W)``. May be bool, float, or uint8.
181181
"""
182+
# Ensure the mask is 2D
183+
while len(masks.shape) > 2:
184+
masks = masks[0]
185+
182186
# Ensure the mask is a float/uint8 array so that assignment of the
183187
# value 255 works correctly (bool arrays raise an error in NumPy ≥ 2).
184188
masks = masks.astype(np.float32)
@@ -199,7 +203,7 @@ def post_process(self, masks, label="AUTOLABEL_OBJECT"):
199203
if len(approx_contours) > 1:
200204
image_size = masks.shape[0] * masks.shape[1]
201205
areas = [cv2.contourArea(contour) for contour in approx_contours]
202-
filtered_approx_contours = [
206+
approx_contours = [
203207
contour
204208
for contour, area in zip(approx_contours, areas)
205209
if area < image_size * 0.9
@@ -210,12 +214,11 @@ def post_process(self, masks, label="AUTOLABEL_OBJECT"):
210214
areas = [cv2.contourArea(contour) for contour in approx_contours]
211215
avg_area = np.mean(areas)
212216

213-
filtered_approx_contours = [
217+
approx_contours = [
214218
contour
215219
for contour, area in zip(approx_contours, areas)
216220
if area > avg_area * 0.2
217221
]
218-
approx_contours = filtered_approx_contours
219222

220223
# Contours to shapes
221224
shapes = []
@@ -284,6 +287,7 @@ def predict_shapes(self, image, filename=None) -> AutoLabelingResult:
284287
"""
285288
Predict shapes from image
286289
"""
290+
self.stop_inference = False
287291
if image is None or (not self.marks and self.prompt_mode != "text"):
288292
return AutoLabelingResult([], replace=False)
289293

@@ -361,7 +365,9 @@ def predict_shapes(self, image, filename=None) -> AutoLabelingResult:
361365
if masks is None or len(masks) == 0:
362366
return AutoLabelingResult([], replace=False)
363367

364-
mask_2d = masks[0] # (H, W)
368+
mask_2d = masks
369+
while len(mask_2d.shape) > 2:
370+
mask_2d = mask_2d[0]
365371
shapes = self.post_process(mask_2d, label="AUTOLABEL_OBJECT")
366372
except Exception as e: # noqa
367373
logging.warning("Could not inference model")
@@ -375,7 +381,15 @@ def predict_shapes(self, image, filename=None) -> AutoLabelingResult:
375381
def unload(self):
376382
self.stop_inference = True
377383
if self.pre_inference_thread:
378-
self.pre_inference_thread.quit()
384+
try:
385+
self.pre_inference_thread.quit()
386+
# Wait for the thread to actually finish (increased to 3s)
387+
if not self.pre_inference_thread.wait(3000):
388+
logging.warning("Pre-inference thread did not stop in time")
389+
else:
390+
self.pre_inference_thread = None
391+
except RuntimeError:
392+
self.pre_inference_thread = None
379393

380394
def preload_worker(self, files):
381395
"""
@@ -409,6 +423,16 @@ def on_next_files_changed(self, next_files):
409423
self.pre_inference_thread = QThread()
410424
self.pre_inference_worker = GenericWorker(self.preload_worker, next_files)
411425
self.pre_inference_worker.finished.connect(self.pre_inference_thread.quit)
426+
self.pre_inference_worker.finished.connect(
427+
self.pre_inference_worker.deleteLater
428+
)
429+
# Reset reference when the thread actually finishes
430+
self.pre_inference_thread.finished.connect(
431+
lambda: setattr(self, "pre_inference_thread", None)
432+
)
433+
self.pre_inference_thread.finished.connect(
434+
self.pre_inference_thread.deleteLater
435+
)
412436
self.pre_inference_worker.moveToThread(self.pre_inference_thread)
413437
self.pre_inference_thread.started.connect(self.pre_inference_worker.run)
414438
self.pre_inference_thread.start()

anylabeling/services/auto_labeling/yolov5.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import cv2
55
import numpy as np
6-
from PyQt5 import QtCore
7-
from PyQt5.QtCore import QCoreApplication
6+
from PyQt6 import QtCore
7+
from PyQt6.QtCore import QCoreApplication
88

99
from anylabeling.app_info import __preferred_device__
1010
from anylabeling.views.labeling.shape import Shape

anylabeling/services/auto_labeling/yolov8.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import cv2
55
import numpy as np
6-
from PyQt5 import QtCore
7-
from PyQt5.QtCore import QCoreApplication
6+
from PyQt6 import QtCore
7+
from PyQt6.QtCore import QCoreApplication
88

99
from anylabeling.app_info import __preferred_device__
1010
from anylabeling.views.labeling.shape import Shape

anylabeling/styles/theme.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import darkdetect
2-
from PyQt5.QtGui import QPalette, QColor
2+
from PyQt6.QtGui import QPalette, QColor
33
import os
44

55

@@ -100,21 +100,21 @@ def apply_theme(app):
100100

101101
# Create and apply palette
102102
palette = QPalette()
103-
palette.setColor(QPalette.Window, QColor(colors["window"]))
104-
palette.setColor(QPalette.WindowText, QColor(colors["window_text"]))
105-
palette.setColor(QPalette.Base, QColor(colors["base"]))
106-
palette.setColor(QPalette.AlternateBase, QColor(colors["alternate_base"]))
107-
palette.setColor(QPalette.Text, QColor(colors["text"]))
108-
palette.setColor(QPalette.Button, QColor(colors["button"]))
109-
palette.setColor(QPalette.ButtonText, QColor(colors["button_text"]))
110-
palette.setColor(QPalette.BrightText, QColor(colors["bright_text"]))
111-
palette.setColor(QPalette.Highlight, QColor(colors["highlight"]))
112-
palette.setColor(QPalette.HighlightedText, QColor(colors["highlighted_text"]))
113-
palette.setColor(QPalette.Link, QColor(colors["link"]))
114-
palette.setColor(QPalette.Dark, QColor(colors["dark"]))
115-
palette.setColor(QPalette.Mid, QColor(colors["mid"]))
116-
palette.setColor(QPalette.Midlight, QColor(colors["midlight"]))
117-
palette.setColor(QPalette.Light, QColor(colors["light"]))
103+
palette.setColor(QPalette.ColorRole.Window, QColor(colors["window"]))
104+
palette.setColor(QPalette.ColorRole.WindowText, QColor(colors["window_text"]))
105+
palette.setColor(QPalette.ColorRole.Base, QColor(colors["base"]))
106+
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(colors["alternate_base"]))
107+
palette.setColor(QPalette.ColorRole.Text, QColor(colors["text"]))
108+
palette.setColor(QPalette.ColorRole.Button, QColor(colors["button"]))
109+
palette.setColor(QPalette.ColorRole.ButtonText, QColor(colors["button_text"]))
110+
palette.setColor(QPalette.ColorRole.BrightText, QColor(colors["bright_text"]))
111+
palette.setColor(QPalette.ColorRole.Highlight, QColor(colors["highlight"]))
112+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor(colors["highlighted_text"]))
113+
palette.setColor(QPalette.ColorRole.Link, QColor(colors["link"]))
114+
palette.setColor(QPalette.ColorRole.Dark, QColor(colors["dark"]))
115+
palette.setColor(QPalette.ColorRole.Mid, QColor(colors["mid"]))
116+
palette.setColor(QPalette.ColorRole.Midlight, QColor(colors["midlight"]))
117+
palette.setColor(QPalette.ColorRole.Light, QColor(colors["light"]))
118118

119119
app.setPalette(palette)
120120

0 commit comments

Comments
 (0)