boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +36 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +161 -77
- boris/config_file.py +63 -83
- boris/connections.py +112 -57
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2511 -1824
- boris/core_qrc.py +15895 -10185
- boris/core_ui.py +946 -792
- boris/db_functions.py +21 -41
- boris/dev.py +134 -0
- boris/dialog.py +505 -244
- boris/duration_widget.py +15 -20
- boris/edit_event.py +84 -28
- boris/edit_event_ui.py +214 -78
- boris/event_operations.py +517 -415
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +213 -583
- boris/export_observation.py +98 -611
- boris/external_processes.py +156 -97
- boris/geometric_measurement.py +652 -287
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +9 -9
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +26 -63
- boris/latency.py +34 -25
- boris/measurement_widget.py +14 -18
- boris/media_file.py +52 -84
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +655 -310
- boris/observation_operations.py +1036 -404
- boris/observation_ui.py +584 -356
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -80
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +43 -46
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +685 -228
- boris/project.py +448 -293
- boris/project_functions.py +689 -254
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -199
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +53 -37
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +766 -266
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +125 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/boris_ui.py +0 -886
- boris/converters.ui +0 -289
- boris/core.qrc +0 -35
- boris/core.ui +0 -1543
- boris/edit_event.ui +0 -175
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -773
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
- boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
- boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/geometric_measurement.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -21,10 +21,21 @@ This file is part of BORIS.
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import logging
|
|
24
|
+
import io
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import pathlib as pl
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
try:
|
|
29
|
+
import pyreadr
|
|
30
|
+
|
|
31
|
+
flag_pyreadr_loaded = True
|
|
32
|
+
except ModuleNotFoundError:
|
|
33
|
+
flag_pyreadr_loaded = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
from PySide6.QtCore import QPoint, Qt, Signal, QEvent
|
|
37
|
+
from PySide6.QtGui import QColor, QPainter, QPolygon, QPixmap, QAction, QPen
|
|
38
|
+
from PySide6.QtWidgets import (
|
|
28
39
|
QApplication,
|
|
29
40
|
QCheckBox,
|
|
30
41
|
QFileDialog,
|
|
@@ -32,29 +43,38 @@ from PyQt5.QtWidgets import (
|
|
|
32
43
|
QLabel,
|
|
33
44
|
QLineEdit,
|
|
34
45
|
QMessageBox,
|
|
35
|
-
|
|
46
|
+
QTableWidget,
|
|
47
|
+
QTableWidgetItem,
|
|
36
48
|
QPushButton,
|
|
37
49
|
QRadioButton,
|
|
38
50
|
QVBoxLayout,
|
|
39
|
-
QWidget,
|
|
40
51
|
QColorDialog,
|
|
52
|
+
QSpacerItem,
|
|
53
|
+
QSizePolicy,
|
|
54
|
+
QDialog,
|
|
41
55
|
)
|
|
42
56
|
|
|
57
|
+
from typing import List
|
|
58
|
+
|
|
43
59
|
from . import config as cfg
|
|
44
60
|
from . import dialog, menu_options
|
|
45
61
|
from . import utilities as util
|
|
46
62
|
|
|
47
63
|
|
|
48
|
-
class wgMeasurement(
|
|
64
|
+
class wgMeasurement(QDialog):
|
|
49
65
|
"""
|
|
50
66
|
widget for geometric measurements
|
|
51
67
|
"""
|
|
52
68
|
|
|
53
|
-
closeSignal =
|
|
54
|
-
send_event_signal =
|
|
69
|
+
closeSignal = Signal()
|
|
70
|
+
send_event_signal = Signal(QEvent)
|
|
71
|
+
reload_image_signal = Signal()
|
|
72
|
+
save_picture_signal = Signal(str)
|
|
55
73
|
mark_color: str = cfg.ACTIVE_MEASUREMENTS_COLOR
|
|
56
74
|
flag_saved = True # store if measurements are saved
|
|
57
75
|
draw_mem: dict = {}
|
|
76
|
+
mem_points: list = [] # memory of clicked points
|
|
77
|
+
mem_video: list = [] # memory of clicked points
|
|
58
78
|
|
|
59
79
|
def __init__(self):
|
|
60
80
|
super().__init__()
|
|
@@ -63,73 +83,119 @@ class wgMeasurement(QWidget):
|
|
|
63
83
|
|
|
64
84
|
vbox = QVBoxLayout(self)
|
|
65
85
|
|
|
66
|
-
self.
|
|
67
|
-
vbox.addWidget(self.
|
|
86
|
+
self.rb_point = QRadioButton("Point (left click)", clicked=self.rb_clicked)
|
|
87
|
+
vbox.addWidget(self.rb_point)
|
|
88
|
+
|
|
89
|
+
self.rb_polyline = QRadioButton("Polyline (left click for vertices, right click to finish)", clicked=self.rb_clicked)
|
|
90
|
+
vbox.addWidget(self.rb_polyline)
|
|
68
91
|
|
|
69
|
-
self.
|
|
70
|
-
|
|
92
|
+
self.rb_polygon = QRadioButton(
|
|
93
|
+
"Polygon (left click for Polygon vertices, right click to close the polygon)", clicked=self.rb_clicked
|
|
94
|
+
)
|
|
95
|
+
vbox.addWidget(self.rb_polygon)
|
|
71
96
|
|
|
72
|
-
self.
|
|
73
|
-
vbox.addWidget(self.
|
|
97
|
+
self.rb_angle = QRadioButton("Angle (vertex: left click, segments: right click)", clicked=self.rb_clicked)
|
|
98
|
+
vbox.addWidget(self.rb_angle)
|
|
74
99
|
|
|
75
|
-
self.
|
|
76
|
-
vbox.addWidget(self.
|
|
100
|
+
self.rb_oriented_angle = QRadioButton("Oriented angle (vertex: left click, segments: right click)", clicked=self.rb_clicked)
|
|
101
|
+
vbox.addWidget(self.rb_oriented_angle)
|
|
77
102
|
|
|
103
|
+
hbox = QHBoxLayout()
|
|
78
104
|
self.cbPersistentMeasurements = QCheckBox("Measurements are persistent")
|
|
79
105
|
self.cbPersistentMeasurements.setChecked(True)
|
|
80
|
-
|
|
106
|
+
hbox.addWidget(self.cbPersistentMeasurements)
|
|
81
107
|
|
|
82
108
|
# color chooser
|
|
83
|
-
|
|
84
109
|
self.bt_color_chooser = QPushButton("Choose color of marks", clicked=self.choose_marks_color)
|
|
85
110
|
self.bt_color_chooser.setStyleSheet(f"QWidget {{background-color:{self.mark_color}}}")
|
|
86
|
-
|
|
111
|
+
hbox.addWidget(self.bt_color_chooser)
|
|
112
|
+
|
|
113
|
+
hbox.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
114
|
+
|
|
115
|
+
vbox.addLayout(hbox)
|
|
87
116
|
|
|
88
117
|
vbox.addWidget(QLabel("<b>Scale</b>"))
|
|
89
118
|
|
|
90
119
|
hbox1 = QHBoxLayout()
|
|
91
|
-
|
|
92
120
|
self.lbRef = QLabel("Reference")
|
|
93
121
|
hbox1.addWidget(self.lbRef)
|
|
94
|
-
|
|
95
122
|
self.lbPx = QLabel("Pixels")
|
|
96
123
|
hbox1.addWidget(self.lbPx)
|
|
97
|
-
|
|
98
124
|
vbox.addLayout(hbox1)
|
|
99
125
|
|
|
100
126
|
hbox2 = QHBoxLayout()
|
|
101
|
-
|
|
102
127
|
self.leRef = QLineEdit()
|
|
103
128
|
self.leRef.setText("1")
|
|
104
129
|
hbox2.addWidget(self.leRef)
|
|
105
|
-
|
|
106
130
|
self.lePx = QLineEdit()
|
|
107
131
|
self.lePx.setText("1")
|
|
108
132
|
hbox2.addWidget(self.lePx)
|
|
109
|
-
|
|
110
133
|
vbox.addLayout(hbox2)
|
|
111
134
|
|
|
112
|
-
self.pte =
|
|
135
|
+
self.pte = QTableWidget()
|
|
136
|
+
self.pte.verticalHeader().hide()
|
|
137
|
+
|
|
138
|
+
# header
|
|
139
|
+
self.measurements_header = [
|
|
140
|
+
"Player",
|
|
141
|
+
"media file name",
|
|
142
|
+
"Time",
|
|
143
|
+
"Frame index",
|
|
144
|
+
"Type of measurement",
|
|
145
|
+
"x",
|
|
146
|
+
"y",
|
|
147
|
+
"Distance",
|
|
148
|
+
"Area",
|
|
149
|
+
"Angle",
|
|
150
|
+
"Coordinates",
|
|
151
|
+
]
|
|
152
|
+
self.pte.setColumnCount(len(self.measurements_header))
|
|
153
|
+
self.pte.setHorizontalHeaderLabels(self.measurements_header)
|
|
154
|
+
|
|
155
|
+
self.pte.setSelectionBehavior(QTableWidget.SelectRows)
|
|
156
|
+
self.pte.setSelectionMode(QTableWidget.MultiSelection)
|
|
157
|
+
|
|
158
|
+
self.pte.setContextMenuPolicy(Qt.ActionsContextMenu)
|
|
159
|
+
|
|
160
|
+
self.action = QAction("Delete measurement")
|
|
161
|
+
self.action.triggered.connect(self.delete_measurement)
|
|
162
|
+
|
|
163
|
+
self.pte.addAction(self.action)
|
|
164
|
+
|
|
113
165
|
vbox.addWidget(self.pte)
|
|
114
166
|
|
|
115
167
|
self.status_lb = QLabel()
|
|
116
168
|
vbox.addWidget(self.status_lb)
|
|
117
169
|
|
|
118
170
|
hbox3 = QHBoxLayout()
|
|
171
|
+
hbox3.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
172
|
+
self.pb_clear = QPushButton("Clear measurements", clicked=self.pbClear_clicked)
|
|
173
|
+
hbox3.addWidget(self.pb_clear)
|
|
119
174
|
|
|
120
|
-
self.
|
|
121
|
-
hbox3.addWidget(self.
|
|
175
|
+
self.pb_save_picture = QPushButton("Save current picture", clicked=self.pb_save_picture_clicked)
|
|
176
|
+
hbox3.addWidget(self.pb_save_picture)
|
|
122
177
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
self.pbClose = QPushButton("Close", clicked=self.pbClose_clicked)
|
|
127
|
-
hbox3.addWidget(self.pbClose)
|
|
178
|
+
# disabled for now
|
|
179
|
+
self.pb_save_all_pictures = QPushButton("Save all pictures", clicked=self.pb_save_all_pictures_clicked)
|
|
180
|
+
hbox3.addWidget(self.pb_save_all_pictures)
|
|
128
181
|
|
|
182
|
+
self.pb_save = QPushButton("Save results", clicked=self.pb_save_clicked)
|
|
183
|
+
hbox3.addWidget(self.pb_save)
|
|
184
|
+
self.pb_close = QPushButton(cfg.CLOSE, clicked=self.pbClose_clicked)
|
|
185
|
+
hbox3.addWidget(self.pb_close)
|
|
129
186
|
vbox.addLayout(hbox3)
|
|
130
187
|
|
|
131
188
|
self.installEventFilter(self)
|
|
132
189
|
|
|
190
|
+
def rb_clicked(self):
|
|
191
|
+
"""
|
|
192
|
+
radiobutton clicked, all points in memory are cleared
|
|
193
|
+
"""
|
|
194
|
+
self.mem_points = []
|
|
195
|
+
self.mem_video = []
|
|
196
|
+
|
|
197
|
+
self.reload_image_signal.emit()
|
|
198
|
+
|
|
133
199
|
def eventFilter(self, receiver, event):
|
|
134
200
|
"""
|
|
135
201
|
send event (if keypress) to main window
|
|
@@ -140,6 +206,51 @@ class wgMeasurement(QWidget):
|
|
|
140
206
|
else:
|
|
141
207
|
return False
|
|
142
208
|
|
|
209
|
+
def pb_save_picture_clicked(self):
|
|
210
|
+
"""
|
|
211
|
+
ask to save the current frame
|
|
212
|
+
"""
|
|
213
|
+
self.save_picture_signal.emit("current")
|
|
214
|
+
|
|
215
|
+
def pb_save_all_pictures_clicked(self):
|
|
216
|
+
"""
|
|
217
|
+
ask to save all frames with measurements
|
|
218
|
+
"""
|
|
219
|
+
self.save_picture_signal.emit("all")
|
|
220
|
+
|
|
221
|
+
def delete_measurement(self):
|
|
222
|
+
"""
|
|
223
|
+
delete the selected measurement(s)
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
if not self.pte.selectedItems():
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
rows_to_delete: list = []
|
|
230
|
+
for item in self.pte.selectedItems():
|
|
231
|
+
if item.row() not in rows_to_delete:
|
|
232
|
+
rows_to_delete.append(item.row())
|
|
233
|
+
|
|
234
|
+
elements_to_delete = []
|
|
235
|
+
for row in sorted(rows_to_delete, reverse=True):
|
|
236
|
+
player = int(self.pte.item(row, 0).text())
|
|
237
|
+
frame_idx = int(self.pte.item(row, 2).text())
|
|
238
|
+
obj_type = self.pte.item(row, 3).text()
|
|
239
|
+
coord = eval(self.pte.item(row, 9).text())
|
|
240
|
+
|
|
241
|
+
if frame_idx in self.draw_mem:
|
|
242
|
+
for idx, element in enumerate(self.draw_mem[frame_idx]):
|
|
243
|
+
if (element["player"] == player - 1) and (element["object_type"] == obj_type) and (element["coordinates"] == coord):
|
|
244
|
+
elements_to_delete.append((frame_idx, idx))
|
|
245
|
+
|
|
246
|
+
self.pte.removeRow(row)
|
|
247
|
+
self.pte.flag_saved = False
|
|
248
|
+
|
|
249
|
+
for frame_idx, idx in sorted(elements_to_delete, reverse=True):
|
|
250
|
+
self.draw_mem[frame_idx].pop(idx)
|
|
251
|
+
|
|
252
|
+
self.reload_image_signal.emit()
|
|
253
|
+
|
|
143
254
|
def choose_marks_color(self):
|
|
144
255
|
"""
|
|
145
256
|
show the color chooser dialog
|
|
@@ -147,6 +258,7 @@ class wgMeasurement(QWidget):
|
|
|
147
258
|
cd = QColorDialog()
|
|
148
259
|
cd.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
149
260
|
cd.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
|
|
261
|
+
cd.setCurrentColor(QColor(self.mark_color))
|
|
150
262
|
|
|
151
263
|
if cd.exec_():
|
|
152
264
|
new_color = cd.currentColor()
|
|
@@ -158,20 +270,24 @@ class wgMeasurement(QWidget):
|
|
|
158
270
|
Intercept the close event to check if measurements are saved
|
|
159
271
|
"""
|
|
160
272
|
|
|
273
|
+
logging.debug("close event")
|
|
274
|
+
|
|
161
275
|
if not self.flag_saved:
|
|
162
276
|
response = dialog.MessageDialog(
|
|
163
277
|
cfg.programName,
|
|
164
278
|
"The current measurements are not saved. Do you want to save the measurement results before closing?",
|
|
165
|
-
|
|
279
|
+
(cfg.YES, cfg.NO, cfg.CANCEL),
|
|
166
280
|
)
|
|
167
281
|
if response == cfg.YES:
|
|
168
|
-
self.
|
|
282
|
+
if self.pb_save_clicked():
|
|
283
|
+
event.ignore()
|
|
284
|
+
return
|
|
169
285
|
if response == cfg.CANCEL:
|
|
170
286
|
event.ignore()
|
|
171
287
|
return
|
|
172
288
|
|
|
173
|
-
self.flag_saved = True
|
|
174
|
-
self.draw_mem = {}
|
|
289
|
+
self.flag_saved: bool = True
|
|
290
|
+
self.draw_mem: dict = {}
|
|
175
291
|
self.closeSignal.emit()
|
|
176
292
|
|
|
177
293
|
def pbClear_clicked(self):
|
|
@@ -183,61 +299,130 @@ class wgMeasurement(QWidget):
|
|
|
183
299
|
response = dialog.MessageDialog(
|
|
184
300
|
cfg.programName,
|
|
185
301
|
"Confirm clearing",
|
|
186
|
-
|
|
302
|
+
(cfg.YES, cfg.CANCEL),
|
|
187
303
|
)
|
|
188
304
|
if response == cfg.CANCEL:
|
|
189
305
|
return
|
|
190
306
|
|
|
191
|
-
self.draw_mem = {}
|
|
307
|
+
self.draw_mem: dict = {}
|
|
308
|
+
self.mem_points: list = []
|
|
309
|
+
self.mem_video: list = []
|
|
310
|
+
|
|
192
311
|
self.pte.clear()
|
|
312
|
+
self.pte.setColumnCount(len(self.measurements_header))
|
|
313
|
+
self.pte.setRowCount(0)
|
|
314
|
+
self.pte.setHorizontalHeaderLabels(self.measurements_header)
|
|
193
315
|
self.flag_saved = True
|
|
194
316
|
|
|
317
|
+
self.reload_image_signal.emit()
|
|
318
|
+
|
|
195
319
|
def pbClose_clicked(self):
|
|
196
320
|
"""
|
|
197
321
|
Close button
|
|
198
322
|
"""
|
|
323
|
+
logging.debug("close function")
|
|
199
324
|
self.close()
|
|
200
325
|
|
|
201
|
-
def
|
|
326
|
+
def pb_save_clicked(self) -> bool:
|
|
202
327
|
"""
|
|
203
|
-
Save measurements results
|
|
328
|
+
Save measurements results
|
|
204
329
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
330
|
+
|
|
331
|
+
file_formats = [cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.HTML, cfg.PANDAS_DF]
|
|
332
|
+
if flag_pyreadr_loaded:
|
|
333
|
+
file_formats.append(cfg.RDS)
|
|
334
|
+
|
|
335
|
+
# default file name
|
|
336
|
+
media_file_list: list = []
|
|
337
|
+
for row in range(self.pte.rowCount()):
|
|
338
|
+
media_file_list.append(self.pte.item(row, 1).text())
|
|
339
|
+
|
|
340
|
+
if len(set(media_file_list)) == 1:
|
|
341
|
+
default_file_name = str(pl.Path(media_file_list[0]).with_suffix(""))
|
|
216
342
|
else:
|
|
217
|
-
|
|
343
|
+
default_file_name = ""
|
|
218
344
|
|
|
345
|
+
file_name, filter_ = QFileDialog().getSaveFileName(self, "Save geometric measurements", default_file_name, ";;".join(file_formats))
|
|
346
|
+
if not file_name:
|
|
347
|
+
return True
|
|
219
348
|
|
|
220
|
-
|
|
349
|
+
# add correct file extension if not present
|
|
350
|
+
if pl.Path(file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
|
|
351
|
+
file_name = str(pl.Path(file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
|
|
352
|
+
# check if file with new extension already exists
|
|
353
|
+
if pl.Path(file_name).is_file():
|
|
354
|
+
if (
|
|
355
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", (cfg.CANCEL, cfg.OVERWRITE))
|
|
356
|
+
== cfg.CANCEL
|
|
357
|
+
):
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
plain_text: str = "\t".join(self.measurements_header) + "\n"
|
|
361
|
+
for row in range(self.pte.rowCount()):
|
|
362
|
+
row_content: list = []
|
|
363
|
+
for col in range(self.pte.columnCount()):
|
|
364
|
+
row_content.append(self.pte.item(row, col).text())
|
|
365
|
+
plain_text += "\t".join(row_content) + "\n"
|
|
366
|
+
|
|
367
|
+
plain_text = plain_text[:-1]
|
|
368
|
+
|
|
369
|
+
df = pd.read_csv(io.StringIO(plain_text), sep="\t")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
if filter_ == cfg.ODS:
|
|
373
|
+
df.to_excel(file_name, engine="odf", sheet_name="Geometric measurements", index=False, na_rep="NA")
|
|
374
|
+
self.flag_saved = True
|
|
375
|
+
if filter_ == cfg.XLSX:
|
|
376
|
+
df.to_excel(file_name, sheet_name="Geometric measurements", index=False, na_rep="NA")
|
|
377
|
+
self.flag_saved = True
|
|
378
|
+
if filter_ == cfg.HTML:
|
|
379
|
+
df.to_html(file_name, index=False, na_rep="NA")
|
|
380
|
+
self.flag_saved = True
|
|
381
|
+
if filter_ == cfg.CSV:
|
|
382
|
+
df.to_csv(file_name, index=False, sep=",", na_rep="NA")
|
|
383
|
+
self.flag_saved = True
|
|
384
|
+
if filter_ == cfg.TSV:
|
|
385
|
+
df.to_csv(file_name, index=False, sep="\t", na_rep="NA")
|
|
386
|
+
self.flag_saved = True
|
|
387
|
+
if filter_ == cfg.PANDAS_DF:
|
|
388
|
+
df.to_pickle(file_name)
|
|
389
|
+
if filter_ == cfg.RDS:
|
|
390
|
+
pyreadr.write_rds(file_name, df)
|
|
391
|
+
|
|
392
|
+
except Exception:
|
|
393
|
+
QMessageBox.warning(self, cfg.programName, "An error occured during saving the measurement results")
|
|
394
|
+
return True
|
|
395
|
+
return False # everything OK
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def show_widget(self) -> None:
|
|
221
399
|
"""
|
|
222
400
|
active the geometric measurement widget
|
|
223
401
|
"""
|
|
224
402
|
|
|
225
403
|
def close_measurement_widget():
|
|
404
|
+
"""
|
|
405
|
+
close the geometric measurement widget
|
|
406
|
+
"""
|
|
226
407
|
|
|
227
|
-
|
|
228
|
-
for n_player, dw in enumerate(self.dw_player):
|
|
229
|
-
dw.frame_viewer.clear()
|
|
230
|
-
dw.stack.setCurrentIndex(cfg.VIDEO_VIEWER)
|
|
231
|
-
dw.setWindowTitle(f"Player #{n_player + 1}")
|
|
232
|
-
self.measurement_w.close()
|
|
408
|
+
logging.debug("close_measurement_widget")
|
|
233
409
|
|
|
234
|
-
|
|
410
|
+
if self.observationId and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
411
|
+
for n_player, dw in enumerate(self.dw_player):
|
|
412
|
+
dw.frame_viewer.clear()
|
|
413
|
+
dw.stack.setCurrentIndex(cfg.VIDEO_VIEWER)
|
|
414
|
+
dw.setWindowTitle(f"Player #{n_player + 1}")
|
|
415
|
+
self.actionPlay.setEnabled(True)
|
|
235
416
|
|
|
236
|
-
self.
|
|
417
|
+
if self.observationId and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
418
|
+
for dw in self.dw_player:
|
|
419
|
+
self.extract_frame(dw)
|
|
237
420
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
421
|
+
self.geometric_measurements_mode = False
|
|
422
|
+
self.measurement_w.draw_mem = {}
|
|
423
|
+
|
|
424
|
+
self.measurement_w.close()
|
|
425
|
+
menu_options.update_menu(self)
|
|
241
426
|
|
|
242
427
|
self.geometric_measurements_mode = True
|
|
243
428
|
self.pause_video()
|
|
@@ -247,9 +432,13 @@ def show_widget(self):
|
|
|
247
432
|
self.actionPlay.setEnabled(False)
|
|
248
433
|
|
|
249
434
|
self.measurement_w = wgMeasurement()
|
|
250
|
-
self.measurement_w.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
435
|
+
# self.measurement_w.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
251
436
|
self.measurement_w.closeSignal.connect(close_measurement_widget)
|
|
252
437
|
self.measurement_w.send_event_signal.connect(self.signal_from_widget)
|
|
438
|
+
self.measurement_w.reload_image_signal.connect(self.reload_frame)
|
|
439
|
+
self.measurement_w.save_picture_signal.connect(self.save_picture_with_measurements)
|
|
440
|
+
self.measurement_w.draw_mem = {}
|
|
441
|
+
|
|
253
442
|
self.measurement_w.show()
|
|
254
443
|
|
|
255
444
|
for dw in self.dw_player:
|
|
@@ -258,35 +447,62 @@ def show_widget(self):
|
|
|
258
447
|
self.extract_frame(dw)
|
|
259
448
|
|
|
260
449
|
|
|
261
|
-
def draw_point(self, x, y, color: str, n_player: int = 0):
|
|
450
|
+
def draw_point(self, x: int, y: int, color: str, n_player: int = 0) -> None:
|
|
262
451
|
"""
|
|
263
452
|
draw point on frame-by-frame image
|
|
264
453
|
"""
|
|
454
|
+
|
|
455
|
+
logging.debug("draw_point function")
|
|
456
|
+
|
|
265
457
|
RADIUS = 6
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
painter
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
458
|
+
|
|
459
|
+
pixmap_copy = self.dw_player[n_player].frame_viewer.pixmap().copy()
|
|
460
|
+
|
|
461
|
+
painter = QPainter(pixmap_copy)
|
|
462
|
+
try:
|
|
463
|
+
painter.setPen(QPen(QColor(color), 1))
|
|
464
|
+
painter.drawEllipse(QPoint(x, y), RADIUS, RADIUS)
|
|
465
|
+
# cross inside circle
|
|
466
|
+
painter.drawLine(x - RADIUS, y, x + RADIUS, y)
|
|
467
|
+
painter.drawLine(x, y - RADIUS, x, y + RADIUS)
|
|
468
|
+
finally:
|
|
469
|
+
painter.end()
|
|
470
|
+
|
|
471
|
+
self.dw_player[n_player].frame_viewer.setPixmap(pixmap_copy)
|
|
274
472
|
self.dw_player[n_player].frame_viewer.update()
|
|
275
473
|
|
|
276
474
|
|
|
277
|
-
def draw_line(self, x1, y1, x2, y2, color: str, n_player=0):
|
|
475
|
+
def draw_line(self, x1: int, y1: int, x2: int, y2: int, color: str, n_player: int = 0) -> None:
|
|
278
476
|
"""
|
|
279
477
|
draw line on frame-by-frame image
|
|
280
478
|
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
painter
|
|
284
|
-
|
|
285
|
-
|
|
479
|
+
|
|
480
|
+
pixmap_copy = self.dw_player[n_player].frame_viewer.pixmap().copy()
|
|
481
|
+
painter = QPainter(pixmap_copy)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
painter.setPen(QColor(color))
|
|
485
|
+
painter.drawLine(x1, y1, x2, y2)
|
|
486
|
+
finally:
|
|
487
|
+
painter.end()
|
|
488
|
+
|
|
489
|
+
self.dw_player[n_player].frame_viewer.setPixmap(pixmap_copy)
|
|
286
490
|
self.dw_player[n_player].frame_viewer.update()
|
|
287
491
|
|
|
288
492
|
|
|
289
|
-
def
|
|
493
|
+
def append_results(self, results: list) -> None:
|
|
494
|
+
"""
|
|
495
|
+
append results to measurements table
|
|
496
|
+
"""
|
|
497
|
+
self.measurement_w.pte.setRowCount(self.measurement_w.pte.rowCount() + 1)
|
|
498
|
+
for idx, x in enumerate(results):
|
|
499
|
+
item = QTableWidgetItem()
|
|
500
|
+
item.setText(str(x))
|
|
501
|
+
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
|
502
|
+
self.measurement_w.pte.setItem(self.measurement_w.pte.rowCount() - 1, idx, item)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def image_clicked(self, n_player: int, event) -> None:
|
|
290
506
|
"""
|
|
291
507
|
Geometric measurements on image
|
|
292
508
|
|
|
@@ -295,7 +511,7 @@ def image_clicked(self, n_player, event):
|
|
|
295
511
|
event (Qevent): event (mousepressed)
|
|
296
512
|
"""
|
|
297
513
|
|
|
298
|
-
logging.debug(
|
|
514
|
+
logging.debug("function image_clicked")
|
|
299
515
|
|
|
300
516
|
if not self.geometric_measurements_mode:
|
|
301
517
|
return
|
|
@@ -305,211 +521,341 @@ def image_clicked(self, n_player, event):
|
|
|
305
521
|
return
|
|
306
522
|
|
|
307
523
|
self.mem_player = n_player
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
- (self.dw_player[n_player].frame_viewer.width() - self.dw_player[n_player].frame_viewer.pixmap().width())
|
|
316
|
-
/ 2
|
|
317
|
-
)
|
|
318
|
-
y = int(
|
|
319
|
-
y
|
|
320
|
-
- (self.dw_player[n_player].frame_viewer.height() - self.dw_player[n_player].frame_viewer.pixmap().height())
|
|
321
|
-
/ 2
|
|
322
|
-
)
|
|
524
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
525
|
+
if self.dw_player[n_player].player.estimated_frame_number is not None:
|
|
526
|
+
current_frame = self.dw_player[n_player].player.estimated_frame_number
|
|
527
|
+
else:
|
|
528
|
+
current_frame = cfg.NA
|
|
529
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
530
|
+
current_frame = self.image_idx
|
|
323
531
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
(x / self.dw_player[n_player].frame_viewer.pixmap().width()) * self.dw_player[n_player].player.width
|
|
327
|
-
)
|
|
328
|
-
y_video = round(
|
|
329
|
-
(y / self.dw_player[n_player].frame_viewer.pixmap().height()) * self.dw_player[n_player].player.height
|
|
330
|
-
)
|
|
532
|
+
if not (hasattr(self, "measurement_w") and (self.measurement_w is not None) and (self.measurement_w.isVisible())):
|
|
533
|
+
return
|
|
331
534
|
|
|
332
|
-
|
|
333
|
-
0 <= x <= self.dw_player[n_player].frame_viewer.pixmap().width()
|
|
334
|
-
and 0 <= y <= self.dw_player[n_player].frame_viewer.pixmap().height()
|
|
335
|
-
):
|
|
336
|
-
self.measurement_w.status_lb.setText("<b>The click is outside the video area</b>")
|
|
337
|
-
return
|
|
535
|
+
x, y = event.pos().x(), event.pos().y()
|
|
338
536
|
|
|
339
|
-
|
|
537
|
+
logging.debug(f"clicked on {x} {y}")
|
|
340
538
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
draw_point(self, x, y, self.measurement_w.mark_color, n_player)
|
|
345
|
-
if current_frame not in self.measurement_w.draw_mem:
|
|
346
|
-
self.measurement_w.draw_mem[current_frame] = []
|
|
539
|
+
# convert label coordinates in pixmap coordinates
|
|
540
|
+
pixmap_x = int(x - (self.dw_player[n_player].frame_viewer.width() - self.dw_player[n_player].frame_viewer.pixmap().width()) / 2)
|
|
541
|
+
pixmap_y = int(y - (self.dw_player[n_player].frame_viewer.height() - self.dw_player[n_player].frame_viewer.pixmap().height()) / 2)
|
|
347
542
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
543
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
544
|
+
# convert pixmap coordinates in video coordinates
|
|
545
|
+
x_video = round((pixmap_x / self.dw_player[n_player].frame_viewer.pixmap().width()) * self.dw_player[n_player].player.width)
|
|
546
|
+
y_video = round((pixmap_y / self.dw_player[n_player].frame_viewer.pixmap().height()) * self.dw_player[n_player].player.height)
|
|
547
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
548
|
+
original_width = QPixmap(self.images_list[self.image_idx]).size().width()
|
|
549
|
+
original_height = QPixmap(self.images_list[self.image_idx]).size().height()
|
|
550
|
+
x_video = round((pixmap_x / self.dw_player[n_player].frame_viewer.pixmap().width()) * original_width)
|
|
551
|
+
y_video = round((pixmap_y / self.dw_player[n_player].frame_viewer.pixmap().height()) * original_height)
|
|
552
|
+
|
|
553
|
+
if not (
|
|
554
|
+
0 <= pixmap_x <= self.dw_player[n_player].frame_viewer.pixmap().width()
|
|
555
|
+
and 0 <= pixmap_y <= self.dw_player[n_player].frame_viewer.pixmap().height()
|
|
556
|
+
):
|
|
557
|
+
self.measurement_w.status_lb.setText("<b>The click is outside the video area</b>")
|
|
558
|
+
return
|
|
351
559
|
|
|
352
|
-
|
|
353
|
-
(
|
|
354
|
-
f"Time: {self.getLaps():.3f}\tPlayer: {n_player + 1}\t"
|
|
355
|
-
f"Frame: {current_frame}\tPoint: {x_video},{y_video}"
|
|
356
|
-
)
|
|
357
|
-
)
|
|
358
|
-
self.measurement_w.flag_saved = False
|
|
560
|
+
self.measurement_w.status_lb.clear()
|
|
359
561
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
draw_point(self, x, y, self.measurement_w.mark_color, n_player)
|
|
364
|
-
self.memx, self.memy = x, y
|
|
365
|
-
self.memx_video, self.memy_video = x_video, y_video
|
|
562
|
+
# media file name
|
|
563
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
564
|
+
fn = self.dw_player[n_player - 1].player.playlist[self.dw_player[n_player - 1].player.playlist_pos]["filename"]
|
|
366
565
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
566
|
+
# check if media file path contained in media list
|
|
567
|
+
if [True for x in self.pj[cfg.OBSERVATIONS][self.observationId]["file"].values() if fn in x]:
|
|
568
|
+
media_file_name = self.dw_player[n_player - 1].player.playlist[self.dw_player[n_player - 1].player.playlist_pos]["filename"]
|
|
569
|
+
else:
|
|
570
|
+
media_file_name = str(pl.Path(fn).relative_to(pl.Path(self.projectFileName).parent))
|
|
370
571
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
572
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
573
|
+
# check if pictures dir path is relative
|
|
574
|
+
if str(pl.Path(self.images_list[current_frame]).parent) not in self.pj[cfg.OBSERVATIONS][self.observationId].get(
|
|
575
|
+
cfg.DIRECTORIES_LIST, []
|
|
576
|
+
):
|
|
577
|
+
media_file_name = str(pl.Path(self.images_list[current_frame]).relative_to(pl.Path(self.projectFileName).parent))
|
|
578
|
+
else:
|
|
579
|
+
media_file_name = self.images_list[current_frame]
|
|
580
|
+
|
|
581
|
+
# point
|
|
582
|
+
if self.measurement_w.rb_point.isChecked():
|
|
583
|
+
if event.button() == Qt.LeftButton:
|
|
584
|
+
draw_point(self, pixmap_x, pixmap_y, self.measurement_w.mark_color, n_player)
|
|
585
|
+
if current_frame not in self.measurement_w.draw_mem:
|
|
586
|
+
self.measurement_w.draw_mem[current_frame] = []
|
|
587
|
+
|
|
588
|
+
self.measurement_w.draw_mem[current_frame].append(
|
|
589
|
+
{
|
|
590
|
+
"player": n_player,
|
|
591
|
+
"object_type": cfg.POINT_OBJECT,
|
|
592
|
+
"color": self.measurement_w.mark_color,
|
|
593
|
+
"coordinates": [(x_video, y_video)],
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
append_results(
|
|
598
|
+
self,
|
|
599
|
+
(
|
|
600
|
+
n_player + 1,
|
|
601
|
+
media_file_name,
|
|
602
|
+
f"{self.getLaps():.03f}",
|
|
603
|
+
current_frame,
|
|
604
|
+
cfg.POINT_OBJECT,
|
|
605
|
+
x_video,
|
|
606
|
+
y_video,
|
|
607
|
+
cfg.NA,
|
|
608
|
+
cfg.NA,
|
|
609
|
+
cfg.NA,
|
|
610
|
+
str([(x_video, y_video)]),
|
|
611
|
+
),
|
|
612
|
+
)
|
|
376
613
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
614
|
+
self.measurement_w.flag_saved = False
|
|
615
|
+
|
|
616
|
+
# angle
|
|
617
|
+
elif self.measurement_w.rb_angle.isChecked() or self.measurement_w.rb_oriented_angle.isChecked():
|
|
618
|
+
if event.button() == Qt.LeftButton:
|
|
619
|
+
draw_point(self, pixmap_x, pixmap_y, self.measurement_w.mark_color, n_player)
|
|
620
|
+
self.measurement_w.mem_points = [(pixmap_x, pixmap_y)]
|
|
621
|
+
self.measurement_w.mem_video = [(x_video, y_video)]
|
|
622
|
+
|
|
623
|
+
if event.button() == Qt.RightButton and len(self.measurement_w.mem_points):
|
|
624
|
+
draw_point(self, pixmap_x, pixmap_y, self.measurement_w.mark_color, n_player)
|
|
625
|
+
draw_line(
|
|
626
|
+
self,
|
|
627
|
+
self.measurement_w.mem_points[0][0],
|
|
628
|
+
self.measurement_w.mem_points[0][1],
|
|
629
|
+
pixmap_x,
|
|
630
|
+
pixmap_y,
|
|
631
|
+
self.measurement_w.mark_color,
|
|
632
|
+
n_player,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
self.measurement_w.mem_points.append((pixmap_x, pixmap_y))
|
|
636
|
+
self.measurement_w.mem_video.append((x_video, y_video))
|
|
637
|
+
|
|
638
|
+
if len(self.measurement_w.mem_points) == 3:
|
|
639
|
+
if self.measurement_w.rb_angle.isChecked():
|
|
640
|
+
angle = util.angle(self.measurement_w.mem_points[0], self.measurement_w.mem_points[1], self.measurement_w.mem_points[2])
|
|
641
|
+
object_type = cfg.ANGLE_OBJECT
|
|
642
|
+
else: # oriented angle
|
|
643
|
+
angle = util.oriented_angle(
|
|
644
|
+
self.measurement_w.mem_points[0], self.measurement_w.mem_points[1], self.measurement_w.mem_points[2]
|
|
387
645
|
)
|
|
646
|
+
object_type = cfg.ORIENTED_ANGLE_OBJECT
|
|
388
647
|
|
|
389
|
-
|
|
648
|
+
append_results(
|
|
649
|
+
self,
|
|
390
650
|
(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
651
|
+
n_player + 1,
|
|
652
|
+
media_file_name,
|
|
653
|
+
f"{self.getLaps():.03f}",
|
|
654
|
+
current_frame,
|
|
655
|
+
object_type,
|
|
656
|
+
cfg.NA,
|
|
657
|
+
cfg.NA,
|
|
658
|
+
cfg.NA,
|
|
659
|
+
cfg.NA,
|
|
660
|
+
round(
|
|
661
|
+
angle,
|
|
662
|
+
1,
|
|
663
|
+
),
|
|
664
|
+
str(self.measurement_w.mem_video),
|
|
665
|
+
),
|
|
394
666
|
)
|
|
667
|
+
|
|
395
668
|
self.measurement_w.flag_saved = False
|
|
396
|
-
|
|
669
|
+
if current_frame not in self.measurement_w.draw_mem:
|
|
670
|
+
self.measurement_w.draw_mem[current_frame] = []
|
|
671
|
+
|
|
672
|
+
self.measurement_w.draw_mem[current_frame].append(
|
|
673
|
+
{
|
|
674
|
+
"player": n_player,
|
|
675
|
+
"object_type": object_type,
|
|
676
|
+
"color": self.measurement_w.mark_color,
|
|
677
|
+
"coordinates": self.measurement_w.mem_video,
|
|
678
|
+
}
|
|
679
|
+
)
|
|
397
680
|
|
|
398
|
-
|
|
399
|
-
elif self.measurement_w.rbAngle.isChecked():
|
|
400
|
-
if event.button() == 1: # left for vertex
|
|
401
|
-
draw_point(self, x, y, self.measurement_w.mark_color, n_player)
|
|
402
|
-
self.memPoints = [(x, y)]
|
|
681
|
+
self.measurement_w.mem_points, self.measurement_w.mem_video = [], []
|
|
403
682
|
|
|
404
|
-
|
|
405
|
-
|
|
683
|
+
# polygon
|
|
684
|
+
elif self.measurement_w.rb_polygon.isChecked():
|
|
685
|
+
if event.button() == Qt.LeftButton:
|
|
686
|
+
draw_point(self, pixmap_x, pixmap_y, self.measurement_w.mark_color, n_player)
|
|
687
|
+
if len(self.measurement_w.mem_points):
|
|
406
688
|
draw_line(
|
|
407
|
-
self,
|
|
689
|
+
self,
|
|
690
|
+
self.measurement_w.mem_points[-1][0],
|
|
691
|
+
self.measurement_w.mem_points[-1][1],
|
|
692
|
+
pixmap_x,
|
|
693
|
+
pixmap_y,
|
|
694
|
+
self.measurement_w.mark_color,
|
|
695
|
+
n_player,
|
|
408
696
|
)
|
|
697
|
+
self.measurement_w.mem_points.append((pixmap_x, pixmap_y))
|
|
698
|
+
self.measurement_w.mem_video.append((x_video, y_video))
|
|
699
|
+
|
|
700
|
+
if event.button() == Qt.RightButton and len(self.measurement_w.mem_points) >= 2:
|
|
701
|
+
# close polygon
|
|
702
|
+
draw_line(
|
|
703
|
+
self,
|
|
704
|
+
self.measurement_w.mem_points[-1][0],
|
|
705
|
+
self.measurement_w.mem_points[-1][1],
|
|
706
|
+
self.measurement_w.mem_points[0][0],
|
|
707
|
+
self.measurement_w.mem_points[0][1],
|
|
708
|
+
self.measurement_w.mark_color,
|
|
709
|
+
n_player,
|
|
710
|
+
)
|
|
711
|
+
if current_frame not in self.measurement_w.draw_mem:
|
|
712
|
+
self.measurement_w.draw_mem[current_frame] = []
|
|
713
|
+
|
|
714
|
+
self.measurement_w.draw_mem[current_frame].append(
|
|
715
|
+
{
|
|
716
|
+
"player": n_player,
|
|
717
|
+
"object_type": cfg.POLYGON_OBJECT,
|
|
718
|
+
"color": self.measurement_w.mark_color,
|
|
719
|
+
"coordinates": self.measurement_w.mem_video,
|
|
720
|
+
}
|
|
721
|
+
)
|
|
409
722
|
|
|
410
|
-
|
|
723
|
+
area = util.polygon_area(self.measurement_w.mem_video)
|
|
724
|
+
try:
|
|
725
|
+
area = area / (float(self.measurement_w.lePx.text()) ** 2) * float(self.measurement_w.leRef.text()) ** 2
|
|
726
|
+
except Exception:
|
|
727
|
+
QMessageBox.critical(
|
|
728
|
+
None,
|
|
729
|
+
cfg.programName,
|
|
730
|
+
"Check reference and pixel values! Values must be numeric.",
|
|
731
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
732
|
+
QMessageBox.NoButton,
|
|
733
|
+
)
|
|
411
734
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
self.measurement_w.draw_mem[current_frame].append(
|
|
424
|
-
[n_player, "angle", self.measurement_w.mark_color, self.memPoints]
|
|
425
|
-
)
|
|
735
|
+
length = util.polyline_length(self.measurement_w.mem_video)
|
|
736
|
+
try:
|
|
737
|
+
length = length / float(self.measurement_w.lePx.text()) * float(self.measurement_w.leRef.text())
|
|
738
|
+
except Exception:
|
|
739
|
+
QMessageBox.critical(
|
|
740
|
+
None,
|
|
741
|
+
cfg.programName,
|
|
742
|
+
"Check reference and pixel values! Values must be numeric.",
|
|
743
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
744
|
+
QMessageBox.NoButton,
|
|
745
|
+
)
|
|
426
746
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
self.memPoints.append((x, y))
|
|
444
|
-
self.memPoints_video.append((x_video, y_video))
|
|
747
|
+
append_results(
|
|
748
|
+
self,
|
|
749
|
+
(
|
|
750
|
+
n_player + 1,
|
|
751
|
+
media_file_name,
|
|
752
|
+
f"{self.getLaps():.03f}",
|
|
753
|
+
current_frame,
|
|
754
|
+
cfg.POLYGON_OBJECT,
|
|
755
|
+
cfg.NA,
|
|
756
|
+
cfg.NA,
|
|
757
|
+
round(length, 1),
|
|
758
|
+
round(area, 1),
|
|
759
|
+
cfg.NA,
|
|
760
|
+
str(self.measurement_w.mem_video),
|
|
761
|
+
),
|
|
762
|
+
)
|
|
445
763
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
draw_line(
|
|
449
|
-
self, self.memPoints[-1][0], self.memPoints[-1][1], x, y, self.measurement_w.mark_color, n_player
|
|
450
|
-
)
|
|
451
|
-
self.memPoints.append((x, y))
|
|
452
|
-
self.memPoints_video.append((x_video, y_video))
|
|
764
|
+
self.measurement_w.flag_saved = False
|
|
765
|
+
self.measurement_w.mem_points, self.measurement_w.mem_video = [], []
|
|
453
766
|
|
|
454
|
-
|
|
767
|
+
# polyline
|
|
768
|
+
elif self.measurement_w.rb_polyline.isChecked():
|
|
769
|
+
if event.button() == Qt.LeftButton:
|
|
770
|
+
draw_point(self, pixmap_x, pixmap_y, self.measurement_w.mark_color, n_player)
|
|
771
|
+
if len(self.measurement_w.mem_points):
|
|
455
772
|
draw_line(
|
|
456
773
|
self,
|
|
457
|
-
self.
|
|
458
|
-
self.
|
|
459
|
-
|
|
460
|
-
|
|
774
|
+
self.measurement_w.mem_points[-1][0],
|
|
775
|
+
self.measurement_w.mem_points[-1][1],
|
|
776
|
+
pixmap_x,
|
|
777
|
+
pixmap_y,
|
|
461
778
|
self.measurement_w.mark_color,
|
|
462
779
|
n_player,
|
|
463
780
|
)
|
|
464
|
-
|
|
781
|
+
self.measurement_w.mem_points.append((pixmap_x, pixmap_y))
|
|
782
|
+
self.measurement_w.mem_video.append((x_video, y_video))
|
|
783
|
+
|
|
784
|
+
if event.button() == Qt.RightButton:
|
|
785
|
+
if current_frame not in self.measurement_w.draw_mem:
|
|
786
|
+
self.measurement_w.draw_mem[current_frame] = []
|
|
787
|
+
|
|
788
|
+
self.measurement_w.draw_mem[current_frame].append(
|
|
789
|
+
{
|
|
790
|
+
"player": n_player,
|
|
791
|
+
"object_type": cfg.POLYLINE_OBJECT,
|
|
792
|
+
"color": self.measurement_w.mark_color,
|
|
793
|
+
"coordinates": self.measurement_w.mem_video,
|
|
794
|
+
}
|
|
795
|
+
)
|
|
465
796
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
self.measurement_w.
|
|
469
|
-
|
|
797
|
+
length = util.polyline_length(self.measurement_w.mem_video)
|
|
798
|
+
try:
|
|
799
|
+
length = length / float(self.measurement_w.lePx.text()) * float(self.measurement_w.leRef.text())
|
|
800
|
+
except Exception:
|
|
801
|
+
QMessageBox.critical(
|
|
802
|
+
None,
|
|
803
|
+
cfg.programName,
|
|
804
|
+
"Check reference and pixel values! Values must be numeric.",
|
|
805
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
806
|
+
QMessageBox.NoButton,
|
|
470
807
|
)
|
|
471
808
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
f"Frame: {current_frame}\tArea: {round(area, 1)}"
|
|
491
|
-
)
|
|
492
|
-
)
|
|
493
|
-
self.measurement_w.flag_saved = False
|
|
494
|
-
self.memPoints, self.memPoints_video = [], []
|
|
809
|
+
append_results(
|
|
810
|
+
self,
|
|
811
|
+
(
|
|
812
|
+
n_player + 1,
|
|
813
|
+
media_file_name,
|
|
814
|
+
f"{self.getLaps():.03f}",
|
|
815
|
+
current_frame,
|
|
816
|
+
cfg.POLYLINE_OBJECT,
|
|
817
|
+
cfg.NA,
|
|
818
|
+
cfg.NA,
|
|
819
|
+
round(length, 1),
|
|
820
|
+
cfg.NA,
|
|
821
|
+
cfg.NA,
|
|
822
|
+
str(self.measurement_w.mem_video),
|
|
823
|
+
),
|
|
824
|
+
)
|
|
825
|
+
self.measurement_w.mem_points, self.measurement_w.mem_video = [], []
|
|
826
|
+
self.measurement_w.flag_saved = False
|
|
495
827
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
else: # no measurements
|
|
500
|
-
QMessageBox.warning(
|
|
501
|
-
self,
|
|
502
|
-
cfg.programName,
|
|
503
|
-
"The Focus area function is not yet available in frame-by-frame mode.",
|
|
504
|
-
QMessageBox.Ok | QMessageBox.Default,
|
|
505
|
-
QMessageBox.NoButton,
|
|
506
|
-
)
|
|
828
|
+
else:
|
|
829
|
+
self.measurement_w.status_lb.setText("<b>Choose a measurement type!</b>")
|
|
507
830
|
|
|
508
831
|
|
|
509
832
|
def redraw_measurements(self):
|
|
510
833
|
"""
|
|
511
834
|
redraw measurements from previous frames
|
|
512
835
|
"""
|
|
836
|
+
|
|
837
|
+
def scale_coord(coord_list: list) -> List[int]:
|
|
838
|
+
"""
|
|
839
|
+
scale coordinates from original media resolution to pixmap
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
843
|
+
# pixmap_size = QPixmap(self.images_list[self.image_idx]).size()
|
|
844
|
+
original_width, original_height = self.current_image_size
|
|
845
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
846
|
+
original_width = dw.player.width
|
|
847
|
+
original_height = dw.player.height
|
|
848
|
+
|
|
849
|
+
pixmap_coord_list: List[int] = []
|
|
850
|
+
for idx, coord in enumerate(coord_list):
|
|
851
|
+
if idx % 2 == 0:
|
|
852
|
+
coord_pixmap = round(coord / original_width * dw.frame_viewer.pixmap().width())
|
|
853
|
+
else:
|
|
854
|
+
coord_pixmap = round(coord / original_height * dw.frame_viewer.pixmap().height())
|
|
855
|
+
pixmap_coord_list.append(coord_pixmap)
|
|
856
|
+
|
|
857
|
+
return pixmap_coord_list
|
|
858
|
+
|
|
513
859
|
logging.debug("Redraw measurement marks")
|
|
514
860
|
|
|
515
861
|
if not (hasattr(self, "measurement_w") and self.measurement_w is not None and self.measurement_w.isVisible()):
|
|
@@ -520,53 +866,72 @@ def redraw_measurements(self):
|
|
|
520
866
|
return
|
|
521
867
|
|
|
522
868
|
for idx, dw in enumerate(self.dw_player):
|
|
523
|
-
|
|
524
|
-
|
|
869
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
870
|
+
if dw.player.estimated_frame_number is not None:
|
|
871
|
+
current_frame = dw.player.estimated_frame_number
|
|
872
|
+
else:
|
|
873
|
+
current_frame = cfg.NA
|
|
874
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
875
|
+
current_frame = self.image_idx
|
|
525
876
|
|
|
526
877
|
for frame in self.measurement_w.draw_mem:
|
|
527
|
-
|
|
528
878
|
for element in self.measurement_w.draw_mem[frame]:
|
|
529
|
-
|
|
530
879
|
if frame == current_frame:
|
|
531
|
-
|
|
880
|
+
elements_color = element["color"]
|
|
532
881
|
else:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if element[
|
|
536
|
-
if element[
|
|
537
|
-
x, y = element[
|
|
538
|
-
draw_point(self, x, y,
|
|
539
|
-
|
|
540
|
-
if element[
|
|
541
|
-
x1, y1
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
draw_point(self,
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
draw_line(self, x1, y1,
|
|
552
|
-
|
|
553
|
-
draw_point(self,
|
|
554
|
-
draw_point(self,
|
|
555
|
-
|
|
556
|
-
|
|
882
|
+
elements_color = cfg.PASSIVE_MEASUREMENTS_COLOR
|
|
883
|
+
|
|
884
|
+
if element["player"] == idx:
|
|
885
|
+
if element["object_type"] == cfg.POINT_OBJECT:
|
|
886
|
+
x, y = scale_coord(element["coordinates"][0])
|
|
887
|
+
draw_point(self, int(x), int(y), elements_color, n_player=idx)
|
|
888
|
+
|
|
889
|
+
if element["object_type"] == cfg.SEGMENT_OBJECT:
|
|
890
|
+
x1, y1 = scale_coord(element["coordinates"][0])
|
|
891
|
+
x2, y2 = scale_coord(element["coordinates"][1])
|
|
892
|
+
draw_line(self, x1, y1, x2, y2, elements_color, n_player=idx)
|
|
893
|
+
draw_point(self, x1, y1, elements_color, n_player=idx)
|
|
894
|
+
draw_point(self, x2, y2, elements_color, n_player=idx)
|
|
895
|
+
|
|
896
|
+
if element["object_type"] in (cfg.ANGLE_OBJECT, cfg.ORIENTED_ANGLE_OBJECT):
|
|
897
|
+
x1, y1 = scale_coord(element["coordinates"][0])
|
|
898
|
+
x2, y2 = scale_coord(element["coordinates"][1])
|
|
899
|
+
x3, y3 = scale_coord(element["coordinates"][2])
|
|
900
|
+
draw_line(self, x1, y1, x2, y2, elements_color, n_player=idx)
|
|
901
|
+
draw_line(self, x1, y1, x3, y3, elements_color, n_player=idx)
|
|
902
|
+
draw_point(self, x1, y1, elements_color, n_player=idx)
|
|
903
|
+
draw_point(self, x2, y2, elements_color, n_player=idx)
|
|
904
|
+
draw_point(self, x3, y3, elements_color, n_player=idx)
|
|
905
|
+
|
|
906
|
+
if element["object_type"] == cfg.POLYGON_OBJECT:
|
|
557
907
|
polygon = QPolygon()
|
|
558
|
-
for
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
painter
|
|
564
|
-
|
|
908
|
+
for x, y in element["coordinates"]:
|
|
909
|
+
x, y = scale_coord([x, y])
|
|
910
|
+
polygon.append(QPoint(x, y))
|
|
911
|
+
|
|
912
|
+
pixmap_copy = self.dw_player[idx].frame_viewer.pixmap().copy()
|
|
913
|
+
painter = QPainter(pixmap_copy)
|
|
914
|
+
|
|
915
|
+
try:
|
|
916
|
+
painter.setPen(QColor(elements_color))
|
|
917
|
+
painter.drawPolygon(polygon)
|
|
918
|
+
finally:
|
|
919
|
+
painter.end()
|
|
920
|
+
|
|
921
|
+
self.dw_player[idx].frame_viewer.setPixmap(pixmap_copy)
|
|
922
|
+
|
|
565
923
|
dw.frame_viewer.update()
|
|
566
924
|
|
|
925
|
+
if element["object_type"] == cfg.POLYLINE_OBJECT:
|
|
926
|
+
for idx1, p1 in enumerate(element["coordinates"][:-1]):
|
|
927
|
+
x1, y1 = scale_coord(p1)
|
|
928
|
+
p2 = element["coordinates"][idx1 + 1]
|
|
929
|
+
x2, y2 = scale_coord(p2)
|
|
567
930
|
|
|
568
|
-
|
|
931
|
+
draw_line(self, x1, y1, x2, y2, elements_color, n_player=idx)
|
|
569
932
|
|
|
933
|
+
|
|
934
|
+
if __name__ == "__main__":
|
|
570
935
|
import sys
|
|
571
936
|
|
|
572
937
|
app = QApplication(sys.argv)
|