vars-localize 0.1.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.
- vars_localize/__init__.py +0 -0
- vars_localize/__main__.py +34 -0
- vars_localize/assets/__init__.py +28 -0
- vars_localize/assets/images/arrow_left.png +0 -0
- vars_localize/assets/images/arrow_right.png +0 -0
- vars_localize/ui/AppWindow.py +276 -0
- vars_localize/ui/BoundingBox.py +243 -0
- vars_localize/ui/ConceptEntry.py +33 -0
- vars_localize/ui/ConceptSearchbar.py +29 -0
- vars_localize/ui/DisplayPanel.py +28 -0
- vars_localize/ui/EntryTree.py +504 -0
- vars_localize/ui/ImageView.py +743 -0
- vars_localize/ui/JSONTree.py +32 -0
- vars_localize/ui/LoginDialog.py +67 -0
- vars_localize/ui/Paginator.py +95 -0
- vars_localize/ui/PropertiesDialog.py +43 -0
- vars_localize/ui/PropertiesForm.py +66 -0
- vars_localize/ui/SearchPanel.py +232 -0
- vars_localize/ui/__init__.py +0 -0
- vars_localize/util/__init__.py +0 -0
- vars_localize/util/endpoints.py +115 -0
- vars_localize/util/m3.py +481 -0
- vars_localize/util/utils.py +90 -0
- vars_localize-0.1.0.dist-info/METADATA +48 -0
- vars_localize-0.1.0.dist-info/RECORD +27 -0
- vars_localize-0.1.0.dist-info/WHEEL +4 -0
- vars_localize-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for the VARS Localize application.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtWidgets import QApplication
|
|
9
|
+
|
|
10
|
+
from vars_localize.ui.AppWindow import AppWindow
|
|
11
|
+
from vars_localize.util.endpoints import DEFAULT_M3_URL
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
"""
|
|
16
|
+
Main entry point for the VARS Localize application.
|
|
17
|
+
"""
|
|
18
|
+
parser = argparse.ArgumentParser(description="VARS Localize")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-u", "--url", type=str, default=DEFAULT_M3_URL, help="URL of M3 server"
|
|
21
|
+
)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
app = QApplication(sys.argv)
|
|
25
|
+
|
|
26
|
+
window = AppWindow(args.url)
|
|
27
|
+
window.show()
|
|
28
|
+
|
|
29
|
+
exit_code = app.exec()
|
|
30
|
+
sys.exit(exit_code)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helpers for dealing with assets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ASSETS_DIR = Path(__file__).parent.resolve()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_asset_path(rel_path: Union[str, Path]) -> Path:
|
|
13
|
+
"""
|
|
14
|
+
Get the absolute path to an asset.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
rel_path (Union[str, Path]): The relative path to the asset.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Path: The absolute path to the asset.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If rel_path is an absolute path.
|
|
24
|
+
"""
|
|
25
|
+
rel_path = Path(rel_path)
|
|
26
|
+
if rel_path.is_absolute():
|
|
27
|
+
raise ValueError("rel_path must be a relative path.")
|
|
28
|
+
return ASSETS_DIR / rel_path
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main application window.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt
|
|
8
|
+
from PyQt6.QtGui import QCloseEvent, QIcon, QAction
|
|
9
|
+
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox, QInputDialog
|
|
10
|
+
|
|
11
|
+
from vars_localize.ui.EntryTree import EntryTreeItem
|
|
12
|
+
from vars_localize.ui.LoginDialog import LoginDialog
|
|
13
|
+
from vars_localize.ui.DisplayPanel import DisplayPanel
|
|
14
|
+
from vars_localize.ui.SearchPanel import SearchPanel
|
|
15
|
+
from vars_localize.util.m3 import (
|
|
16
|
+
check_connection,
|
|
17
|
+
get_all_users,
|
|
18
|
+
get_annotations_by_video_refernce,
|
|
19
|
+
get_imaged_moments_by_image_reference,
|
|
20
|
+
)
|
|
21
|
+
from vars_localize.util.utils import log, split_comma_list
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AppWindow(QMainWindow):
|
|
25
|
+
def __init__(self, m3_url: str, parent=None):
|
|
26
|
+
super(AppWindow, self).__init__(parent)
|
|
27
|
+
self._m3_url = m3_url.rstrip("/")
|
|
28
|
+
|
|
29
|
+
self.setWindowTitle("VARS Localize")
|
|
30
|
+
|
|
31
|
+
log(f"Checking connection to M3 at {self._m3_url}...")
|
|
32
|
+
if not check_connection(self._m3_url):
|
|
33
|
+
log(
|
|
34
|
+
"You are not connected to M3. Check your internet connection and/or VPN.",
|
|
35
|
+
level=2,
|
|
36
|
+
)
|
|
37
|
+
QMessageBox.critical(
|
|
38
|
+
self,
|
|
39
|
+
"No connection to M3",
|
|
40
|
+
"You are not connected to M3. Check your internet connection and/or VPN.",
|
|
41
|
+
)
|
|
42
|
+
exit(1)
|
|
43
|
+
log("Connected.")
|
|
44
|
+
|
|
45
|
+
self.observer = None
|
|
46
|
+
self.observer_role = None
|
|
47
|
+
self.admin_mode = False
|
|
48
|
+
|
|
49
|
+
login_ok = self.login()
|
|
50
|
+
if not login_ok:
|
|
51
|
+
log("You must log in to use this tool.", level=2)
|
|
52
|
+
exit(1)
|
|
53
|
+
|
|
54
|
+
self.central_container = QWidget()
|
|
55
|
+
self.central_container.setLayout(QHBoxLayout())
|
|
56
|
+
|
|
57
|
+
self.search_panel = SearchPanel(parent=self)
|
|
58
|
+
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.search_panel)
|
|
59
|
+
|
|
60
|
+
self.display_panel = DisplayPanel(parent=self)
|
|
61
|
+
self.central_container.layout().addWidget(self.display_panel)
|
|
62
|
+
|
|
63
|
+
self.setCentralWidget(self.central_container)
|
|
64
|
+
|
|
65
|
+
# Add admin menu if available to user
|
|
66
|
+
if self.observer_role in ("Maint", "Admin"):
|
|
67
|
+
self.add_admin_menu()
|
|
68
|
+
|
|
69
|
+
self.add_search_menu()
|
|
70
|
+
self.add_video_menu()
|
|
71
|
+
|
|
72
|
+
self.display_panel.image_view.observer = self.observer
|
|
73
|
+
self.display_panel.image_view.select_next = self.search_panel.select_next
|
|
74
|
+
self.display_panel.image_view.select_prev = self.search_panel.select_prev
|
|
75
|
+
|
|
76
|
+
self.search_panel.observer = self.observer
|
|
77
|
+
|
|
78
|
+
def load_entry(self, current: EntryTreeItem, previous: EntryTreeItem):
|
|
79
|
+
"""
|
|
80
|
+
Load the current entry into the display panel
|
|
81
|
+
:param current: Current selected entry
|
|
82
|
+
:param previous: Previously selected entry
|
|
83
|
+
:return: None
|
|
84
|
+
"""
|
|
85
|
+
if current and current.metadata:
|
|
86
|
+
self.display_panel.load_entry(current)
|
|
87
|
+
|
|
88
|
+
def login(self):
|
|
89
|
+
"""
|
|
90
|
+
Prompt for observer login
|
|
91
|
+
:return: None
|
|
92
|
+
"""
|
|
93
|
+
login_dialog = LoginDialog(parent=self)
|
|
94
|
+
login_dialog._login_form._username_line_edit.setFocus()
|
|
95
|
+
ok = login_dialog.exec()
|
|
96
|
+
|
|
97
|
+
if ok:
|
|
98
|
+
# Get the username/password from the dialog
|
|
99
|
+
username, password = login_dialog.credentials
|
|
100
|
+
|
|
101
|
+
# Set up the M3 configuration, returning False if login fails
|
|
102
|
+
if not self.configure_m3(username, password):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
all_valid_users = get_all_users()
|
|
106
|
+
users_dict = {
|
|
107
|
+
user_data["username"]: user_data for user_data in all_valid_users
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Set the observer and role
|
|
111
|
+
self.observer = username
|
|
112
|
+
self.observer_role = users_dict[username]["role"]
|
|
113
|
+
else: # Login cancel, return failure
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
return True # Return success
|
|
117
|
+
|
|
118
|
+
def configure_m3(self, username, password) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Configure endpoints and set up Annosaurus auth
|
|
121
|
+
"""
|
|
122
|
+
from vars_localize.util.endpoints import configure as configure_endpoints
|
|
123
|
+
from vars_localize.util.m3 import configure_anno_session
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
configure_endpoints(self._m3_url, username, password)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
log("Login failed.", level=2)
|
|
129
|
+
log(e, level=2)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
configure_anno_session()
|
|
133
|
+
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def add_admin_menu(self):
|
|
137
|
+
"""
|
|
138
|
+
Add the admin menu for observation modification/deletion
|
|
139
|
+
"""
|
|
140
|
+
main_menu = self.menuBar()
|
|
141
|
+
options_menu = main_menu.addMenu("&Options")
|
|
142
|
+
|
|
143
|
+
admin_mode_action = QAction("Admin Mode", options_menu, checkable=True)
|
|
144
|
+
|
|
145
|
+
def set_admin_mode(val):
|
|
146
|
+
if val:
|
|
147
|
+
QMessageBox.warning(
|
|
148
|
+
self,
|
|
149
|
+
"Entering Admin Mode",
|
|
150
|
+
"WARNING: You are now entering administrator mode. This mode allows modification and deletion of observations within VARS.",
|
|
151
|
+
)
|
|
152
|
+
self.admin_mode = val
|
|
153
|
+
|
|
154
|
+
admin_mode_action.toggled.connect(set_admin_mode)
|
|
155
|
+
options_menu.addAction(admin_mode_action)
|
|
156
|
+
|
|
157
|
+
def add_search_menu(self):
|
|
158
|
+
"""
|
|
159
|
+
Add the Go menu for non-concept searches
|
|
160
|
+
"""
|
|
161
|
+
main_menu = self.menuBar()
|
|
162
|
+
search_menu = main_menu.addMenu("&Search")
|
|
163
|
+
|
|
164
|
+
def search_imaged_moment():
|
|
165
|
+
imaged_moment_uuid_list, ok = QInputDialog.getText(
|
|
166
|
+
self,
|
|
167
|
+
"Imaged Moment UUID Search",
|
|
168
|
+
"Imaged Moment UUID (or comma-separated list)",
|
|
169
|
+
)
|
|
170
|
+
if ok:
|
|
171
|
+
imaged_moment_uuids = split_comma_list(imaged_moment_uuid_list)
|
|
172
|
+
imaged_moment_uuids = list(
|
|
173
|
+
set(imaged_moment_uuids)
|
|
174
|
+
) # Ensure no duplicates
|
|
175
|
+
|
|
176
|
+
# Set the UUIDs and load the first page
|
|
177
|
+
self.search_panel.set_uuids(imaged_moment_uuids)
|
|
178
|
+
self.search_panel.load_page()
|
|
179
|
+
|
|
180
|
+
search_imaged_moment_action = QAction("Imaged Moment UUID", search_menu)
|
|
181
|
+
search_imaged_moment_action.triggered.connect(search_imaged_moment)
|
|
182
|
+
search_menu.addAction(search_imaged_moment_action)
|
|
183
|
+
|
|
184
|
+
def search_image_reference():
|
|
185
|
+
image_reference_uuid_list, ok = QInputDialog.getText(
|
|
186
|
+
self,
|
|
187
|
+
"Image Reference UUID Search",
|
|
188
|
+
"Image Reference UUID (or comma-separated list)",
|
|
189
|
+
)
|
|
190
|
+
if ok:
|
|
191
|
+
all_image_reference_uuids = split_comma_list(image_reference_uuid_list)
|
|
192
|
+
imaged_moment_uuids = []
|
|
193
|
+
for image_reference_uuid in all_image_reference_uuids:
|
|
194
|
+
res = get_imaged_moments_by_image_reference(image_reference_uuid)
|
|
195
|
+
if res:
|
|
196
|
+
imaged_moment_uuids.extend(
|
|
197
|
+
[item["imaged_moment_uuid"] for item in res]
|
|
198
|
+
)
|
|
199
|
+
imaged_moment_uuids = list(
|
|
200
|
+
set(imaged_moment_uuids)
|
|
201
|
+
) # Ensure no duplicates
|
|
202
|
+
|
|
203
|
+
# Set the UUIDs and load the first page
|
|
204
|
+
self.search_panel.set_uuids(imaged_moment_uuids)
|
|
205
|
+
self.search_panel.load_page()
|
|
206
|
+
|
|
207
|
+
search_image_reference_action = QAction("Image Reference UUID", search_menu)
|
|
208
|
+
search_image_reference_action.triggered.connect(search_image_reference)
|
|
209
|
+
search_menu.addAction(search_image_reference_action)
|
|
210
|
+
|
|
211
|
+
def search_video_reference():
|
|
212
|
+
video_reference_uuid, ok = QInputDialog.getText(
|
|
213
|
+
self, "Video Reference UUID Search", "Video Reference UUID"
|
|
214
|
+
)
|
|
215
|
+
if ok:
|
|
216
|
+
res = get_annotations_by_video_refernce(video_reference_uuid)
|
|
217
|
+
if res:
|
|
218
|
+
timestamp_uuid_tuples = set()
|
|
219
|
+
for item in res:
|
|
220
|
+
timestamp = datetime.now()
|
|
221
|
+
if "recorded_timestamp" in item:
|
|
222
|
+
try:
|
|
223
|
+
timestamp = datetime.strptime(
|
|
224
|
+
item["recorded_timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
225
|
+
)
|
|
226
|
+
except ValueError:
|
|
227
|
+
timestamp = datetime.strptime(
|
|
228
|
+
item["recorded_timestamp"], "%Y-%m-%dT%H:%M:%SZ"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
timestamp_uuid_tuples.add(
|
|
232
|
+
(timestamp, item["imaged_moment_uuid"])
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Sort by timestamp, then UUID
|
|
236
|
+
timestamp_uuid_tuples = sorted(timestamp_uuid_tuples)
|
|
237
|
+
imaged_moment_uuids = [item[1] for item in timestamp_uuid_tuples]
|
|
238
|
+
|
|
239
|
+
# Set the UUIDs and load the first page
|
|
240
|
+
self.search_panel.set_uuids(imaged_moment_uuids)
|
|
241
|
+
self.search_panel.load_page()
|
|
242
|
+
else:
|
|
243
|
+
# No results, warning dialog
|
|
244
|
+
QMessageBox.warning(
|
|
245
|
+
self,
|
|
246
|
+
"No Results",
|
|
247
|
+
"No results found for video reference UUID: {}".format(
|
|
248
|
+
video_reference_uuid
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
search_video_reference_action = QAction("Video Reference UUID", search_menu)
|
|
253
|
+
search_video_reference_action.triggered.connect(search_video_reference)
|
|
254
|
+
search_menu.addAction(search_video_reference_action)
|
|
255
|
+
|
|
256
|
+
def add_video_menu(self):
|
|
257
|
+
"""
|
|
258
|
+
Add the Video menu for video-level operations
|
|
259
|
+
"""
|
|
260
|
+
main_menu = self.menuBar()
|
|
261
|
+
video_menu = main_menu.addMenu("&Video")
|
|
262
|
+
|
|
263
|
+
def open_video():
|
|
264
|
+
self.search_panel.open_video()
|
|
265
|
+
|
|
266
|
+
open_video_action = QAction("Open Video", video_menu)
|
|
267
|
+
open_video_action.triggered.connect(open_video)
|
|
268
|
+
video_menu.addAction(open_video_action)
|
|
269
|
+
|
|
270
|
+
def closeEvent(self, a0: QCloseEvent) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Detect window close and tear down components
|
|
273
|
+
:param a0: Close event
|
|
274
|
+
:return: None
|
|
275
|
+
"""
|
|
276
|
+
self.deleteLater()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bounding box data structure and manager helper class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, QRectF, QPoint, QSizeF, QRect, QPointF
|
|
8
|
+
from PyQt6.QtGui import QColor, QPainter, QPen, QFont
|
|
9
|
+
from PyQt6.QtWidgets import QGraphicsItem, QStyleOptionGraphicsItem, QWidget
|
|
10
|
+
|
|
11
|
+
from vars_localize.util import m3, utils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SourceBoundingBox(QRect):
|
|
15
|
+
"""Bounding box VARS source data structure"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
box_json,
|
|
20
|
+
label,
|
|
21
|
+
observer=None,
|
|
22
|
+
observation_uuid=None,
|
|
23
|
+
association_uuid=None,
|
|
24
|
+
part=None,
|
|
25
|
+
):
|
|
26
|
+
super(SourceBoundingBox, self).__init__(
|
|
27
|
+
box_json["x"], box_json["y"], box_json["width"], box_json["height"]
|
|
28
|
+
)
|
|
29
|
+
self.image_reference_uuid = box_json.get("image_reference_uuid", None)
|
|
30
|
+
self.observation_uuid = observation_uuid
|
|
31
|
+
self.association_uuid = association_uuid
|
|
32
|
+
self.part = part
|
|
33
|
+
self.label = label
|
|
34
|
+
self.observer = observer
|
|
35
|
+
|
|
36
|
+
def set_label(self, label):
|
|
37
|
+
if label in m3.get_all_concepts():
|
|
38
|
+
self.label = label
|
|
39
|
+
|
|
40
|
+
def get_json(self):
|
|
41
|
+
d = {
|
|
42
|
+
"x": self.x(),
|
|
43
|
+
"y": self.y(),
|
|
44
|
+
"width": self.width(),
|
|
45
|
+
"height": self.height(),
|
|
46
|
+
"generator": "vars-localize",
|
|
47
|
+
"image_reference_uuid": self.image_reference_uuid,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if self.observer is not None:
|
|
51
|
+
d["observer"] = self.observer
|
|
52
|
+
|
|
53
|
+
return d
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GraphicsBoundingBox(QGraphicsItem):
|
|
57
|
+
"""Graphical bounding box representation"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, source: SourceBoundingBox, editable: bool = True):
|
|
60
|
+
super(GraphicsBoundingBox, self).__init__()
|
|
61
|
+
|
|
62
|
+
self.source = source
|
|
63
|
+
self.editable = editable
|
|
64
|
+
|
|
65
|
+
self.width = 0
|
|
66
|
+
self.height = 0
|
|
67
|
+
self.label = None
|
|
68
|
+
self.highlighted = False
|
|
69
|
+
self.color = QColor(0, 0, 0)
|
|
70
|
+
|
|
71
|
+
def set_box(self, x, y, w, h):
|
|
72
|
+
"""
|
|
73
|
+
Update box position and dimensions
|
|
74
|
+
:param x: x position
|
|
75
|
+
:param y: y position
|
|
76
|
+
:param w: Width
|
|
77
|
+
:param h: Height
|
|
78
|
+
:return: None
|
|
79
|
+
"""
|
|
80
|
+
self.prepareGeometryChange()
|
|
81
|
+
self.setPos(x, y)
|
|
82
|
+
self.width = w
|
|
83
|
+
self.height = h
|
|
84
|
+
|
|
85
|
+
def set_label(self, label):
|
|
86
|
+
"""
|
|
87
|
+
Set the label of the bounding box
|
|
88
|
+
:param label: Bounding box label
|
|
89
|
+
:return: None
|
|
90
|
+
"""
|
|
91
|
+
self.label = label
|
|
92
|
+
if self.editable:
|
|
93
|
+
self.color.setHsv(*utils.n_split_hash(label, 1), 255, 255)
|
|
94
|
+
else:
|
|
95
|
+
# If not editable, set color to gray with 50% opacity
|
|
96
|
+
self.color.setHsv(0, 0, 128, alpha=128)
|
|
97
|
+
|
|
98
|
+
def set_highlighted(self, highlighted: bool):
|
|
99
|
+
"""
|
|
100
|
+
Set the highlight status of the bounding box
|
|
101
|
+
:param highlighted: Highlight on or off
|
|
102
|
+
:return: None
|
|
103
|
+
"""
|
|
104
|
+
self.highlighted = highlighted
|
|
105
|
+
|
|
106
|
+
def area(self):
|
|
107
|
+
"""
|
|
108
|
+
Compute the area of the box
|
|
109
|
+
:return: Box area
|
|
110
|
+
"""
|
|
111
|
+
return self.width * self.height
|
|
112
|
+
|
|
113
|
+
def boundingRect(self) -> QRectF:
|
|
114
|
+
"""
|
|
115
|
+
Give the bounding rectangle of the graphics item
|
|
116
|
+
:return: Bounding rectangle of box (not including label)
|
|
117
|
+
"""
|
|
118
|
+
return QRectF(QPointF(0, 0), QSizeF(self.width, self.height))
|
|
119
|
+
|
|
120
|
+
def contains(self, pt: QPoint):
|
|
121
|
+
"""
|
|
122
|
+
Check if the given point lies within the bounding box
|
|
123
|
+
:param pt: Point to check
|
|
124
|
+
:return: True if point within box, else False
|
|
125
|
+
"""
|
|
126
|
+
return (
|
|
127
|
+
self.x() <= pt.x() <= self.x() + self.width
|
|
128
|
+
and self.y() <= pt.y() <= self.y() + self.height
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def paint(
|
|
132
|
+
self,
|
|
133
|
+
painter: QPainter,
|
|
134
|
+
option: QStyleOptionGraphicsItem,
|
|
135
|
+
widget: typing.Optional[QWidget] = ...,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Paint the item within the scene
|
|
139
|
+
:param painter: Painter object
|
|
140
|
+
:param option: Style object
|
|
141
|
+
:param widget: Optional widget
|
|
142
|
+
:return:
|
|
143
|
+
"""
|
|
144
|
+
pen = QPen(self.color.lighter(), 4 if self.highlighted else 2)
|
|
145
|
+
painter.setPen(pen)
|
|
146
|
+
painter.drawRect(0, 0, int(self.width), int(self.height))
|
|
147
|
+
|
|
148
|
+
painter.setFont(QFont("Helvetica", 12, QFont.Weight.Bold))
|
|
149
|
+
draw_text = self.label if self.label else "No label"
|
|
150
|
+
if self.source.part is not None and self.source.part != "self":
|
|
151
|
+
draw_text += " " + self.source.part
|
|
152
|
+
painter.drawText(
|
|
153
|
+
0,
|
|
154
|
+
int(self.height),
|
|
155
|
+
int(self.width),
|
|
156
|
+
20,
|
|
157
|
+
Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextDontClip,
|
|
158
|
+
draw_text,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class BoundingBoxManager:
|
|
163
|
+
"""Manages a list of graphical bounding box objects"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, bounding_boxes: list = None):
|
|
166
|
+
if bounding_boxes:
|
|
167
|
+
self.bounding_boxes = bounding_boxes
|
|
168
|
+
else:
|
|
169
|
+
self.bounding_boxes = []
|
|
170
|
+
|
|
171
|
+
self.box_click_callback = None
|
|
172
|
+
self.box_right_click_callback = None
|
|
173
|
+
|
|
174
|
+
def make_box(self, x, y, w, h, label, src, editable: bool = True):
|
|
175
|
+
"""
|
|
176
|
+
Create a box and add it to the manager
|
|
177
|
+
:param x: x position
|
|
178
|
+
:param y: y position
|
|
179
|
+
:param w: Width
|
|
180
|
+
:param h: Height
|
|
181
|
+
:param label: Bounding box label
|
|
182
|
+
:param src: Source bounding box
|
|
183
|
+
:param editable: Whether the box is editable
|
|
184
|
+
:return: Graphical bounding box item
|
|
185
|
+
"""
|
|
186
|
+
box = GraphicsBoundingBox(src, editable=editable)
|
|
187
|
+
box.set_box(x, y, w, h)
|
|
188
|
+
box.set_label(label)
|
|
189
|
+
self.bounding_boxes.append(box)
|
|
190
|
+
return box
|
|
191
|
+
|
|
192
|
+
def set_box_click_callback(self, func):
|
|
193
|
+
"""
|
|
194
|
+
Set the callback function for when the box is clicked
|
|
195
|
+
:param func: Callback function
|
|
196
|
+
:return: None
|
|
197
|
+
"""
|
|
198
|
+
self.box_click_callback = func
|
|
199
|
+
|
|
200
|
+
def set_box_right_click_callback(self, func):
|
|
201
|
+
"""
|
|
202
|
+
Set the callback function for when the box is clicked
|
|
203
|
+
:param func: Callback function
|
|
204
|
+
:return: None
|
|
205
|
+
"""
|
|
206
|
+
self.box_right_click_callback = func
|
|
207
|
+
|
|
208
|
+
def check_box_click(self, pt: QPoint, right_click: bool):
|
|
209
|
+
"""
|
|
210
|
+
Check managed boxes for point containment, process callbacks
|
|
211
|
+
:param pt: Point to process
|
|
212
|
+
:return: None
|
|
213
|
+
"""
|
|
214
|
+
selected_box = None
|
|
215
|
+
for box in self.bounding_boxes:
|
|
216
|
+
if box.contains(pt) and box.editable:
|
|
217
|
+
if not selected_box or box.area() < selected_box.area():
|
|
218
|
+
selected_box = box
|
|
219
|
+
if self.box_click_callback:
|
|
220
|
+
if selected_box:
|
|
221
|
+
if right_click:
|
|
222
|
+
self.box_right_click_callback(selected_box)
|
|
223
|
+
else:
|
|
224
|
+
self.box_click_callback(selected_box)
|
|
225
|
+
|
|
226
|
+
def get_box_hovered(self, pt: QPoint):
|
|
227
|
+
"""
|
|
228
|
+
Check managed boxes for point containment, return hovered box if any
|
|
229
|
+
:param pt: Point to process
|
|
230
|
+
:return: Hovered box, if any
|
|
231
|
+
"""
|
|
232
|
+
hovered_box = None
|
|
233
|
+
for box in self.bounding_boxes:
|
|
234
|
+
if box.contains(pt) and box.editable:
|
|
235
|
+
if not hovered_box or box.area() < hovered_box.area():
|
|
236
|
+
hovered_box = box
|
|
237
|
+
return hovered_box
|
|
238
|
+
|
|
239
|
+
def boxes(self):
|
|
240
|
+
return self.bounding_boxes
|
|
241
|
+
|
|
242
|
+
def clear(self):
|
|
243
|
+
self.bounding_boxes.clear()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom QListWidgetItem for displaying concept information.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import Qt
|
|
6
|
+
from PyQt6.QtGui import QFont
|
|
7
|
+
from PyQt6.QtWidgets import QListWidgetItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConceptEntry(QListWidgetItem):
|
|
11
|
+
def __init__(self, data=None, parent=None):
|
|
12
|
+
super(ConceptEntry, self).__init__(parent)
|
|
13
|
+
|
|
14
|
+
self.setFont(QFont("Courier New"))
|
|
15
|
+
self.setTextAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
16
|
+
|
|
17
|
+
self.ann_data = data
|
|
18
|
+
if data:
|
|
19
|
+
self.update_message()
|
|
20
|
+
|
|
21
|
+
def set_ann_data(self, data):
|
|
22
|
+
self.ann_data = data
|
|
23
|
+
|
|
24
|
+
def get_data(self):
|
|
25
|
+
return self.ann_data
|
|
26
|
+
|
|
27
|
+
def update_message(self):
|
|
28
|
+
"""
|
|
29
|
+
Set message based on annotation data
|
|
30
|
+
:return: None
|
|
31
|
+
"""
|
|
32
|
+
str_rep = "{0}: {1}".format(self.ann_data["concept"], self.ann_data["timecode"])
|
|
33
|
+
self.setText(str_rep)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom QLineEdit widget for searching concepts.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
6
|
+
from PyQt6.QtWidgets import QLineEdit, QCompleter
|
|
7
|
+
|
|
8
|
+
from vars_localize.util import m3
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConceptSearchbar(QLineEdit):
|
|
12
|
+
conceptSelected = pyqtSignal()
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None):
|
|
15
|
+
super(ConceptSearchbar, self).__init__(parent)
|
|
16
|
+
|
|
17
|
+
self.setPlaceholderText("Search for concept")
|
|
18
|
+
|
|
19
|
+
self.concept_completer = QCompleter(m3.get_all_concepts())
|
|
20
|
+
self.concept_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
|
21
|
+
self.setCompleter(self.concept_completer)
|
|
22
|
+
|
|
23
|
+
def set_callback(self, func):
|
|
24
|
+
"""
|
|
25
|
+
Set callback on completer activation (concept selected)
|
|
26
|
+
:param func: Activation callback
|
|
27
|
+
:return: None
|
|
28
|
+
"""
|
|
29
|
+
self.concept_completer.activated.connect(func)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Container widget used do display images + localizations and process input.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtWidgets import QWidget, QVBoxLayout
|
|
6
|
+
|
|
7
|
+
from vars_localize.ui.ImageView import ImageView
|
|
8
|
+
from vars_localize.ui.EntryTree import EntryTreeItem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DisplayPanel(QWidget):
|
|
12
|
+
def __init__(self, parent=None):
|
|
13
|
+
super(DisplayPanel, self).__init__(parent)
|
|
14
|
+
|
|
15
|
+
self.setLayout(QVBoxLayout())
|
|
16
|
+
|
|
17
|
+
self.image_view = ImageView(parent=self)
|
|
18
|
+
|
|
19
|
+
self.layout().addWidget(self.image_view, stretch=1)
|
|
20
|
+
|
|
21
|
+
def load_entry(self, entry: EntryTreeItem):
|
|
22
|
+
"""
|
|
23
|
+
Load an entry into the image view, redraw
|
|
24
|
+
:param entry: Concept entry
|
|
25
|
+
:return: None
|
|
26
|
+
"""
|
|
27
|
+
self.image_view.set_entry(entry)
|
|
28
|
+
self.image_view.redraw()
|