Skip to content

Commit 264795d

Browse files
authored
Merge pull request #78 from DeepLabCut/improvements
Usability improvements
2 parents 2438b79 + c9bc91d commit 264795d

File tree

2 files changed

+136
-27
lines changed

2 files changed

+136
-27
lines changed

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ project_urls =
3333
packages = find:
3434
install_requires =
3535
dask-image
36-
napari==0.4.17
36+
napari==0.4.18
3737
natsort
3838
numpy
3939
opencv-python-headless
@@ -59,7 +59,7 @@ napari.manifest =
5959
[options.extras_require]
6060
testing =
6161
napari
62-
pyside6<6.3.2
62+
pyside6
6363
pytest
6464
pytest-cov
6565
pytest-qt

src/napari_deeplabcut/_widgets.py

Lines changed: 134 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import os
2-
from collections import defaultdict
2+
from collections import defaultdict, namedtuple
33
from copy import deepcopy
44
from datetime import datetime
5-
from functools import partial
5+
from functools import partial, cached_property
66
from math import ceil, log10
77
import pandas as pd
88
from pathlib import Path
@@ -17,12 +17,14 @@
1717
from napari.layers.utils.layer_utils import _features_to_properties
1818
from napari.utils.events import Event
1919
from napari.utils.history import get_save_history, update_save_history
20-
from qtpy.QtCore import Qt, QTimer, Signal, QSize
21-
from qtpy.QtGui import QPainter, QIcon
20+
from qtpy.QtCore import Qt, QTimer, Signal, QSize, QPoint, QSettings
21+
from qtpy.QtGui import QPainter, QIcon, QAction
2222
from qtpy.QtWidgets import (
2323
QButtonGroup,
2424
QCheckBox,
2525
QComboBox,
26+
QDialog,
27+
QDialogButtonBox,
2628
QFileDialog,
2729
QGroupBox,
2830
QHBoxLayout,
@@ -50,6 +52,62 @@
5052
)
5153

5254

55+
Tip = namedtuple('Tip', ['msg', 'pos'])
56+
57+
58+
class Tutorial(QDialog):
59+
def __init__(self, parent):
60+
super().__init__(parent=parent)
61+
self.setParent(parent)
62+
self.setWindowTitle("Tutorial")
63+
self.setModal(True)
64+
self.setWindowOpacity(0.85)
65+
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
66+
67+
self._current_tip = 0
68+
self._tips = [
69+
Tip('Load a folder of annotated data\n(and optionally a config file if labeling from scratch).\nAlternatively, files and folders of images can be dragged\nand dropped onto the main window.', (0.45, 0.05)),
70+
Tip('Data layers will be listed at the bottom left;\ntheir visibility can be toggled by clicking on the small eye icon.', (0.1, 0.65)),
71+
Tip('Corresponding layer controls can be found at the top left.\nSwitch between labeling and selection mode using the numeric keys 2 and 3,\nor clicking on the + or -> icons.', (0.1, 0.2)),
72+
Tip('There are three keypoint labeling modes:\nthe key M can be used to cycle between them.', (0.65, 0.05)),
73+
Tip('When done labeling, save your data by selecting the Points layer\nand hitting Ctrl+S (or File > Save Selected Layer(s)...).', (0.1, 0.65)),
74+
Tip('''Read more at <a href='https://github.com/DeepLabCut/napari-deeplabcut#usage'>napari-deeplabcut</a>''', (0.4, 0.4)),
75+
]
76+
77+
buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Abort
78+
self.button_box = QDialogButtonBox(buttons)
79+
self.button_box.accepted.connect(self.accept)
80+
self.button_box.rejected.connect(self.reject)
81+
82+
vlayout = QVBoxLayout()
83+
self.message = QLabel("Let's get started with a quick walkthrough!")
84+
self.message.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
85+
self.message.setOpenExternalLinks(True)
86+
vlayout.addWidget(self.message)
87+
hlayout = QHBoxLayout()
88+
self.count = QLabel("")
89+
hlayout.addWidget(self.count)
90+
hlayout.addWidget(self.button_box)
91+
vlayout.addLayout(hlayout)
92+
self.setLayout(vlayout)
93+
94+
def accept(self):
95+
if self._current_tip == 0 and "walkthrough" not in self.message.text():
96+
self.reject()
97+
tip = self._tips[self._current_tip]
98+
self.message.setText(tip.msg)
99+
self.count.setText(f"Tip {self._current_tip + 1}|{len(self._tips)}")
100+
self.adjustSize()
101+
xrel, yrel = tip.pos
102+
geom = self.parent().geometry()
103+
p = QPoint(
104+
int(geom.left() + geom.width() * xrel),
105+
int(geom.top() + geom.height() * yrel),
106+
)
107+
self.move(p)
108+
self._current_tip = (self._current_tip + 1) % len(self._tips)
109+
110+
53111
def _get_and_try_preferred_reader(
54112
self,
55113
dialog,
@@ -163,9 +221,7 @@ def _save_layers_dialog(self, selected=False):
163221
if not len(self.viewer.layers):
164222
msg = "There are no layers in the viewer to save."
165223
elif selected and not len(selected_layers):
166-
msg = (
167-
"Please select one or more layers to save," '\nor use "Save all layers..."'
168-
)
224+
msg = "Please select a Points layer to save."
169225
if msg:
170226
QMessageBox.warning(self, "Nothing to save", msg, QMessageBox.Ok)
171227
return
@@ -227,7 +283,7 @@ def __init__(self, napari_viewer):
227283
status_bar.addPermanentWidget(self.last_saved_label)
228284

229285
# Hack napari's Welcome overlay to show more relevant instructions
230-
overlay = self.viewer.window._qt_viewer._canvas_overlay
286+
overlay = self.viewer.window._qt_viewer._welcome_widget
231287
welcome_widget = overlay.layout().itemAt(1).widget()
232288
welcome_widget.deleteLater()
233289
w = QtWelcomeWidget(None)
@@ -256,8 +312,23 @@ def __init__(self, napari_viewer):
256312
self._menus = []
257313

258314
self._video_group = self._form_video_action_menu()
315+
self.video_widget = self.viewer.window.add_dock_widget(
316+
self._video_group, name="video", area="right"
317+
)
318+
self.video_widget.setVisible(False)
319+
320+
# Add some buttons to load files and data (as a complement to drag/drop)
321+
hlayout = QHBoxLayout()
322+
load_config_button = QPushButton("Load config file")
323+
load_config_button.clicked.connect(self._load_config)
324+
hlayout.addWidget(load_config_button)
325+
326+
self.load_data_button = QPushButton("Load data folder")
327+
self.load_data_button.clicked.connect(self._load_data_folder)
328+
hlayout.addWidget(self.load_data_button)
329+
self._layout.addLayout(hlayout)
259330

260-
vlayout = QHBoxLayout()
331+
hlayout = QHBoxLayout()
261332
trail_label = QLabel("Show trails")
262333
self._trail_cb = QCheckBox()
263334
self._trail_cb.setToolTip("toggle trails visibility")
@@ -268,11 +339,11 @@ def __init__(self, napari_viewer):
268339

269340
self._view_scheme_cb = QCheckBox("Show color scheme", parent=self)
270341

271-
vlayout.addWidget(trail_label)
272-
vlayout.addWidget(self._trail_cb)
273-
vlayout.addWidget(self._view_scheme_cb)
342+
hlayout.addWidget(trail_label)
343+
hlayout.addWidget(self._trail_cb)
344+
hlayout.addWidget(self._view_scheme_cb)
274345

275-
self._layout.addLayout(vlayout)
346+
self._layout.addLayout(hlayout)
276347

277348
self._radio_group = self._form_mode_radio_buttons()
278349

@@ -282,16 +353,54 @@ def __init__(self, napari_viewer):
282353
self._view_scheme_cb.toggle()
283354

284355
# Substitute default menu action with custom one
285-
for action in self.viewer.window.file_menu.actions():
286-
if "save selected layer" in action.text().lower():
356+
for action in self.viewer.window.file_menu.actions()[::-1]:
357+
action_name = action.text().lower()
358+
if "save selected layer" in action_name:
287359
action.triggered.disconnect()
288360
action.triggered.connect(
289361
lambda: _save_layers_dialog(
290362
self,
291363
selected=True,
292364
)
293365
)
294-
break
366+
elif "save all layers" in action_name:
367+
self.viewer.window.file_menu.removeAction(action)
368+
369+
# Add action to show the walkthrough again
370+
launch_tutorial = QAction("&Launch Tutorial", self)
371+
launch_tutorial.triggered.connect(self.start_tutorial)
372+
self.viewer.window.view_menu.addAction(launch_tutorial)
373+
374+
if self.settings.value("first_launch", True):
375+
QTimer.singleShot(10, self.start_tutorial)
376+
self.settings.setValue("first_launch", False)
377+
378+
@cached_property
379+
def settings(self):
380+
return QSettings()
381+
382+
def start_tutorial(self):
383+
Tutorial(self.viewer.window._qt_window.__wrapped__).show()
384+
385+
def _load_config(self):
386+
config = QFileDialog.getOpenFileName(
387+
self, "Select a configuration file", "", "Config files (*.yaml)"
388+
)
389+
if not config:
390+
return
391+
392+
try: # Needed to silence a late ValueError caused by the layer having no data
393+
self.viewer.open(config, plugin="napari-deeplabcut", stack=False)
394+
except ValueError:
395+
pass
396+
397+
def _load_data_folder(self):
398+
dialog = QFileDialog(self)
399+
dialog.setFileMode(QFileDialog.Directory)
400+
dialog.setViewMode(QFileDialog.Detail)
401+
if dialog.exec_():
402+
folder = dialog.selectedFiles()[0]
403+
self.viewer.open(folder, plugin="napari-deeplabcut")
295404

296405
def _move_image_layer_to_bottom(self, index):
297406
if (ind := index) != 0:
@@ -317,23 +426,20 @@ def _show_trails(self, state):
317426
colormap="viridis",
318427
)
319428
self._trails.visible = True
320-
else:
429+
elif self._trails is not None:
321430
self._trails.visible = False
322431

323432
def _form_video_action_menu(self):
324433
group_box = QGroupBox("Video")
325434
layout = QVBoxLayout()
326435
extract_button = QPushButton("Extract frame")
327436
extract_button.clicked.connect(self._extract_single_frame)
328-
extract_button.setEnabled(False)
329437
layout.addWidget(extract_button)
330438
crop_button = QPushButton("Store crop coordinates")
331439
crop_button.clicked.connect(self._store_crop_coordinates)
332-
crop_button.setEnabled(False)
333440
layout.addWidget(crop_button)
334441
group_box.setLayout(layout)
335-
self._layout.addWidget(group_box)
336-
return extract_button, crop_button
442+
return group_box
337443

338444
def _extract_single_frame(self, *args):
339445
image_layer = None
@@ -490,8 +596,7 @@ def on_insert(self, event):
490596
if isinstance(layer, Image):
491597
paths = layer.metadata.get("paths")
492598
if paths is None: # Then it's a video file
493-
for widget in self._video_group:
494-
widget.setEnabled(True)
599+
self.video_widget.setVisible(True)
495600
# Store the metadata and pass them on to the other layers
496601
self._images_meta.update(
497602
{
@@ -570,6 +675,7 @@ def on_insert(self, event):
570675
}
571676
)
572677
self._trail_cb.setEnabled(True)
678+
self.load_data_button.setDisabled(True)
573679

574680
# Hide the color pickers, as colormaps are strictly defined by users
575681
controls = self.viewer.window.qt_viewer.dockLayerControls
@@ -578,6 +684,9 @@ def on_insert(self, event):
578684
point_controls.edgeColorEdit.hide()
579685
point_controls.layout().itemAt(9).widget().hide()
580686
point_controls.layout().itemAt(11).widget().hide()
687+
# Hide out of slice checkbox
688+
point_controls.outOfSliceCheckBox.hide()
689+
point_controls.layout().itemAt(15).widget().hide()
581690

582691
for layer_ in self.viewer.layers:
583692
if not isinstance(layer_, Image):
@@ -596,13 +705,13 @@ def on_remove(self, event):
596705
menu.deleteLater()
597706
menu.destroy()
598707
self._trail_cb.setEnabled(False)
708+
self.load_data_button.setDisabled(False)
599709
self.last_saved_label.hide()
600710
elif isinstance(layer, Image):
601711
self._images_meta = dict()
602712
paths = layer.metadata.get("paths")
603713
if paths is None:
604-
for widget in self._video_group:
605-
widget.setEnabled(False)
714+
self.video_widget.setVisible(False)
606715
elif isinstance(layer, Tracks):
607716
self._trail_cb.setChecked(False)
608717
self._trails = None

0 commit comments

Comments
 (0)