Skip to content

Commit eabe0a5

Browse files
authored
Merge pull request #53 from DeepLabCut/lock_kpt
Allow users to lock keypoint selection during LOOP labeling mode
2 parents 5a5709d + a093498 commit eabe0a5

File tree

8 files changed

+58
-26
lines changed

8 files changed

+58
-26
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
include LICENSE
22
include README.md
3+
include src/napari_deeplabcut/assets/*.svg
34

45
recursive-exclude * __pycache__
56
recursive-exclude * *.py[co]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The easiest way to get started is to drop a folder (typically a folder from with
4545

4646
- `2` and `3`, to easily switch between labeling and selection mode
4747
- `4`, to enable pan & zoom (which is achieved using the mouse wheel or finger scrolling on the Trackpad)
48-
- `M`, to cycle through regular (sequential), quick, and cycle annotation mode (see the description [here](https://github.com/DeepLabCut/DeepLabCut-label/blob/ee71b0e15018228c98db3b88769e8a8f4e2c0454/dlclabel/layers.py#L9-L19))
48+
- `M`, to cycle through regular (sequential), quick, and cycle annotation mode (see the description [here](https://github.com/DeepLabCut/napari-deeplabcut/blob/5a5709dd38868341568d66eab548ae8abf37cd63/src/napari_deeplabcut/keypoints.py#L25-L34))
4949
- `E`, to enable edge coloring (by default, if using this in refinement GUI mode, points with a confidence lower than 0.6 are marked
5050
in red)
5151
- `F`, to toggle between animal and body part color scheme.

src/napari_deeplabcut/_tests/test_keypoints.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ def test_store(store, fake_keypoints):
2626
assert store.current_keypoint == kpt
2727
store.next_keypoint()
2828

29-
store.smart_reset(event=None)
30-
assert store.current_keypoint == kpt
31-
assert store.current_label == "kpt_0"
32-
assert store.current_id == "animal_0"
33-
store.current_id = "animal_1"
34-
assert store.current_id == "animal_1"
35-
3629
store._find_first_unlabeled_frame(event=None)
3730
assert store.current_step == store.n_steps - 1
3831
# Remove a frame to test whether it is correctly found

src/napari_deeplabcut/_tests/test_widgets.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ def test_keypoints_dropdown_menu(store):
9090
assert label_menu.count() == 0
9191

9292

93+
def test_keypoints_dropdown_menu_smart_reset(store):
94+
widget = _widgets.KeypointsDropdownMenu(store)
95+
label_menu = widget.menus['label']
96+
label_menu.update_to("kpt_2")
97+
widget._locked = True
98+
widget.smart_reset(event=None)
99+
assert label_menu.currentText() == "kpt_2"
100+
widget._locked = False
101+
widget.smart_reset(event=None)
102+
assert label_menu.currentText() == "kpt_0"
103+
104+
93105
def test_color_pair():
94106
pair = _widgets.LabelPair(color="pink", name="kpt", parent=None)
95107
assert pair.part_name == "kpt"

src/napari_deeplabcut/_widgets.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from napari.layers.utils import color_manager
1616
from napari.utils.events import Event
1717
from napari.utils.history import get_save_history, update_save_history
18-
from qtpy.QtCore import Qt, QTimer, Signal
19-
from qtpy.QtGui import QPainter
18+
from qtpy.QtCore import Qt, QTimer, Signal, QSize
19+
from qtpy.QtGui import QPainter, QIcon
2020
from qtpy.QtWidgets import (
2121
QButtonGroup,
2222
QCheckBox,
@@ -324,6 +324,11 @@ def _store_crop_coordinates(self, *args):
324324

325325
def _form_dropdown_menus(self, store):
326326
menu = KeypointsDropdownMenu(store)
327+
self.viewer.dims.events.current_step.connect(
328+
menu.smart_reset,
329+
position="last",
330+
)
331+
menu.smart_reset(event=None)
327332
self._menus.append(menu)
328333
layout = QVBoxLayout()
329334
layout.addWidget(menu)
@@ -447,11 +452,7 @@ def on_insert(self, event):
447452
layer.events.query_next_frame.connect(store._advance_step)
448453
layer.bind_key("Shift-Right", store._find_first_unlabeled_frame)
449454
layer.bind_key("Shift-Left", store._find_first_unlabeled_frame)
450-
self.viewer.dims.events.current_step.connect(
451-
store.smart_reset,
452-
position="last",
453-
)
454-
store.smart_reset(event=None)
455+
455456
layer.bind_key("Down", store.next_keypoint, overwrite=True)
456457
layer.bind_key("Up", store.prev_keypoint, overwrite=True)
457458
layer.face_color_mode = "cycle"
@@ -548,6 +549,7 @@ def __init__(
548549
super().__init__(parent)
549550
self.store = store
550551
self.store.layer.events.current_properties.connect(self.update_menus)
552+
self._locked = False
551553

552554
# Map individuals to their respective bodyparts
553555
self.id2label = defaultdict(list)
@@ -571,10 +573,24 @@ def __init__(
571573
layout2 = QVBoxLayout()
572574
for menu in self.menus.values():
573575
layout2.addWidget(menu)
576+
self.lock_button = QPushButton("Lock selection")
577+
self.lock_button.setIcon(QIcon('src/napari_deeplabcut/assets/unlock.svg'))
578+
self.lock_button.setIconSize(QSize(24, 24))
579+
self.lock_button.clicked.connect(self._lock_current_keypoint)
580+
layout2.addWidget(self.lock_button)
574581
group_box.setLayout(layout2)
575582
layout1.addWidget(group_box)
576583
self.setLayout(layout1)
577584

585+
def _lock_current_keypoint(self):
586+
self._locked = not self._locked
587+
if self._locked:
588+
self.lock_button.setText("Unlock selection")
589+
self.lock_button.setIcon(QIcon('src/napari_deeplabcut/assets/lock.svg'))
590+
else:
591+
self.lock_button.setText("Lock selection")
592+
self.lock_button.setIcon(QIcon('src/napari_deeplabcut/assets/unlock.svg'))
593+
578594
def update_menus(self, event):
579595
keypoint = self.store.current_keypoint
580596
for attr, menu in self.menus.items():
@@ -589,6 +605,18 @@ def refresh_label_menu(self, text: str):
589605
menu.blockSignals(False)
590606
menu.addItems(self.id2label[text])
591607

608+
def smart_reset(self, event):
609+
"""Set current keypoint to the first unlabeled one."""
610+
if self._locked:
611+
return
612+
unannotated = ""
613+
already_annotated = self.store.annotated_keypoints
614+
for keypoint in self.store._keypoints:
615+
if keypoint not in already_annotated:
616+
unannotated = keypoint
617+
break
618+
self.store.current_keypoint = unannotated if unannotated else self.store._keypoints[0]
619+
592620

593621
def create_dropdown_menu(store, items, attr):
594622
menu = DropdownMenu(items)
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

src/napari_deeplabcut/keypoints.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class LabelMode(CycleEnum):
3131
annotated point actually moves it to the cursor location.
3232
LOOP: the first point is placed frame by frame, then it wraps
3333
to the next label at the end and restart from frame 1, etc.
34+
Unless the keypoint selection is locked, the dropdown menu is
35+
automatically set to the first unlabeled keypoint of
36+
the current frame.
3437
"""
3538

3639
SEQUENTIAL = auto()
@@ -49,7 +52,10 @@ def default(cls):
4952
"QUICK": "Similar to SEQUENTIAL, but trying to add an already\n"
5053
"annotated point actually moves it to the cursor location.",
5154
"LOOP": "The first point is placed frame by frame, then it wraps\n"
52-
"to the next label at the end and restart from frame 1, etc.",
55+
"to the next label at the end and restart from frame 1, etc.\n"
56+
"Unless the keypoint selection is locked, the dropdown menu is\n"
57+
"automatically set to the first unlabeled keypoint of\n"
58+
"the current frame.",
5359
}
5460

5561

@@ -99,16 +105,6 @@ def current_keypoint(self, keypoint: Keypoint):
99105
current_properties["id"] = np.asarray([keypoint.id])
100106
self.layer.current_properties = current_properties
101107

102-
def smart_reset(self, event):
103-
"""Set current keypoint to the first unlabeled one."""
104-
unannotated = ""
105-
already_annotated = self.annotated_keypoints
106-
for keypoint in self._keypoints:
107-
if keypoint not in already_annotated:
108-
unannotated = keypoint
109-
break
110-
self.current_keypoint = unannotated if unannotated else self._keypoints[0]
111-
112108
def next_keypoint(self, *args):
113109
ind = self._keypoints.index(self.current_keypoint) + 1
114110
if ind <= len(self._keypoints) - 1:

0 commit comments

Comments
 (0)