boris-behav-obs 9.7.7__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.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- 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 +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import io
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import pathlib as pl
|
|
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 (
|
|
39
|
+
QApplication,
|
|
40
|
+
QCheckBox,
|
|
41
|
+
QFileDialog,
|
|
42
|
+
QHBoxLayout,
|
|
43
|
+
QLabel,
|
|
44
|
+
QLineEdit,
|
|
45
|
+
QMessageBox,
|
|
46
|
+
QTableWidget,
|
|
47
|
+
QTableWidgetItem,
|
|
48
|
+
QPushButton,
|
|
49
|
+
QRadioButton,
|
|
50
|
+
QVBoxLayout,
|
|
51
|
+
QColorDialog,
|
|
52
|
+
QSpacerItem,
|
|
53
|
+
QSizePolicy,
|
|
54
|
+
QDialog,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from typing import List
|
|
58
|
+
|
|
59
|
+
from . import config as cfg
|
|
60
|
+
from . import dialog, menu_options
|
|
61
|
+
from . import utilities as util
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class wgMeasurement(QDialog):
|
|
65
|
+
"""
|
|
66
|
+
widget for geometric measurements
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
closeSignal = Signal()
|
|
70
|
+
send_event_signal = Signal(QEvent)
|
|
71
|
+
reload_image_signal = Signal()
|
|
72
|
+
save_picture_signal = Signal(str)
|
|
73
|
+
mark_color: str = cfg.ACTIVE_MEASUREMENTS_COLOR
|
|
74
|
+
flag_saved = True # store if measurements are saved
|
|
75
|
+
draw_mem: dict = {}
|
|
76
|
+
mem_points: list = [] # memory of clicked points
|
|
77
|
+
mem_video: list = [] # memory of clicked points
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
self.setWindowTitle("Geometric measurements")
|
|
83
|
+
|
|
84
|
+
vbox = QVBoxLayout(self)
|
|
85
|
+
|
|
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)
|
|
91
|
+
|
|
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)
|
|
96
|
+
|
|
97
|
+
self.rb_angle = QRadioButton("Angle (vertex: left click, segments: right click)", clicked=self.rb_clicked)
|
|
98
|
+
vbox.addWidget(self.rb_angle)
|
|
99
|
+
|
|
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)
|
|
102
|
+
|
|
103
|
+
hbox = QHBoxLayout()
|
|
104
|
+
self.cbPersistentMeasurements = QCheckBox("Measurements are persistent")
|
|
105
|
+
self.cbPersistentMeasurements.setChecked(True)
|
|
106
|
+
hbox.addWidget(self.cbPersistentMeasurements)
|
|
107
|
+
|
|
108
|
+
# color chooser
|
|
109
|
+
self.bt_color_chooser = QPushButton("Choose color of marks", clicked=self.choose_marks_color)
|
|
110
|
+
self.bt_color_chooser.setStyleSheet(f"QWidget {{background-color:{self.mark_color}}}")
|
|
111
|
+
hbox.addWidget(self.bt_color_chooser)
|
|
112
|
+
|
|
113
|
+
hbox.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
114
|
+
|
|
115
|
+
vbox.addLayout(hbox)
|
|
116
|
+
|
|
117
|
+
vbox.addWidget(QLabel("<b>Scale</b>"))
|
|
118
|
+
|
|
119
|
+
hbox1 = QHBoxLayout()
|
|
120
|
+
self.lbRef = QLabel("Reference")
|
|
121
|
+
hbox1.addWidget(self.lbRef)
|
|
122
|
+
self.lbPx = QLabel("Pixels")
|
|
123
|
+
hbox1.addWidget(self.lbPx)
|
|
124
|
+
vbox.addLayout(hbox1)
|
|
125
|
+
|
|
126
|
+
hbox2 = QHBoxLayout()
|
|
127
|
+
self.leRef = QLineEdit()
|
|
128
|
+
self.leRef.setText("1")
|
|
129
|
+
hbox2.addWidget(self.leRef)
|
|
130
|
+
self.lePx = QLineEdit()
|
|
131
|
+
self.lePx.setText("1")
|
|
132
|
+
hbox2.addWidget(self.lePx)
|
|
133
|
+
vbox.addLayout(hbox2)
|
|
134
|
+
|
|
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
|
+
|
|
165
|
+
vbox.addWidget(self.pte)
|
|
166
|
+
|
|
167
|
+
self.status_lb = QLabel()
|
|
168
|
+
vbox.addWidget(self.status_lb)
|
|
169
|
+
|
|
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)
|
|
174
|
+
|
|
175
|
+
self.pb_save_picture = QPushButton("Save current picture", clicked=self.pb_save_picture_clicked)
|
|
176
|
+
hbox3.addWidget(self.pb_save_picture)
|
|
177
|
+
|
|
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)
|
|
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)
|
|
186
|
+
vbox.addLayout(hbox3)
|
|
187
|
+
|
|
188
|
+
self.installEventFilter(self)
|
|
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
|
+
|
|
199
|
+
def eventFilter(self, receiver, event):
|
|
200
|
+
"""
|
|
201
|
+
send event (if keypress) to main window
|
|
202
|
+
"""
|
|
203
|
+
if event.type() == QEvent.KeyPress:
|
|
204
|
+
self.send_event_signal.emit(event)
|
|
205
|
+
return True
|
|
206
|
+
else:
|
|
207
|
+
return False
|
|
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
|
+
|
|
254
|
+
def choose_marks_color(self):
|
|
255
|
+
"""
|
|
256
|
+
show the color chooser dialog
|
|
257
|
+
"""
|
|
258
|
+
cd = QColorDialog()
|
|
259
|
+
cd.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
260
|
+
cd.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
|
|
261
|
+
cd.setCurrentColor(QColor(self.mark_color))
|
|
262
|
+
|
|
263
|
+
if cd.exec_():
|
|
264
|
+
new_color = cd.currentColor()
|
|
265
|
+
self.bt_color_chooser.setStyleSheet(f"QWidget {{background-color:{new_color.name()}}}")
|
|
266
|
+
self.mark_color = new_color.name()
|
|
267
|
+
|
|
268
|
+
def closeEvent(self, event):
|
|
269
|
+
"""
|
|
270
|
+
Intercept the close event to check if measurements are saved
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
logging.debug("close event")
|
|
274
|
+
|
|
275
|
+
if not self.flag_saved:
|
|
276
|
+
response = dialog.MessageDialog(
|
|
277
|
+
cfg.programName,
|
|
278
|
+
"The current measurements are not saved. Do you want to save the measurement results before closing?",
|
|
279
|
+
(cfg.YES, cfg.NO, cfg.CANCEL),
|
|
280
|
+
)
|
|
281
|
+
if response == cfg.YES:
|
|
282
|
+
if self.pb_save_clicked():
|
|
283
|
+
event.ignore()
|
|
284
|
+
return
|
|
285
|
+
if response == cfg.CANCEL:
|
|
286
|
+
event.ignore()
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
self.flag_saved: bool = True
|
|
290
|
+
self.draw_mem: dict = {}
|
|
291
|
+
self.closeSignal.emit()
|
|
292
|
+
|
|
293
|
+
def pbClear_clicked(self):
|
|
294
|
+
"""
|
|
295
|
+
clear measurements draw and results
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
if not self.flag_saved:
|
|
299
|
+
response = dialog.MessageDialog(
|
|
300
|
+
cfg.programName,
|
|
301
|
+
"Confirm clearing",
|
|
302
|
+
(cfg.YES, cfg.CANCEL),
|
|
303
|
+
)
|
|
304
|
+
if response == cfg.CANCEL:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
self.draw_mem: dict = {}
|
|
308
|
+
self.mem_points: list = []
|
|
309
|
+
self.mem_video: list = []
|
|
310
|
+
|
|
311
|
+
self.pte.clear()
|
|
312
|
+
self.pte.setColumnCount(len(self.measurements_header))
|
|
313
|
+
self.pte.setRowCount(0)
|
|
314
|
+
self.pte.setHorizontalHeaderLabels(self.measurements_header)
|
|
315
|
+
self.flag_saved = True
|
|
316
|
+
|
|
317
|
+
self.reload_image_signal.emit()
|
|
318
|
+
|
|
319
|
+
def pbClose_clicked(self):
|
|
320
|
+
"""
|
|
321
|
+
Close button
|
|
322
|
+
"""
|
|
323
|
+
logging.debug("close function")
|
|
324
|
+
self.close()
|
|
325
|
+
|
|
326
|
+
def pb_save_clicked(self) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Save measurements results
|
|
329
|
+
"""
|
|
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(""))
|
|
342
|
+
else:
|
|
343
|
+
default_file_name = ""
|
|
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
|
|
348
|
+
|
|
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:
|
|
399
|
+
"""
|
|
400
|
+
active the geometric measurement widget
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
def close_measurement_widget():
|
|
404
|
+
"""
|
|
405
|
+
close the geometric measurement widget
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
logging.debug("close_measurement_widget")
|
|
409
|
+
|
|
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)
|
|
416
|
+
|
|
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)
|
|
420
|
+
|
|
421
|
+
self.geometric_measurements_mode = False
|
|
422
|
+
self.measurement_w.draw_mem = {}
|
|
423
|
+
|
|
424
|
+
self.measurement_w.close()
|
|
425
|
+
menu_options.update_menu(self)
|
|
426
|
+
|
|
427
|
+
self.geometric_measurements_mode = True
|
|
428
|
+
self.pause_video()
|
|
429
|
+
|
|
430
|
+
menu_options.update_menu(self)
|
|
431
|
+
|
|
432
|
+
self.actionPlay.setEnabled(False)
|
|
433
|
+
|
|
434
|
+
self.measurement_w = wgMeasurement()
|
|
435
|
+
# self.measurement_w.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
436
|
+
self.measurement_w.closeSignal.connect(close_measurement_widget)
|
|
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
|
+
|
|
442
|
+
self.measurement_w.show()
|
|
443
|
+
|
|
444
|
+
for dw in self.dw_player:
|
|
445
|
+
dw.setWindowTitle("Geometric measurements")
|
|
446
|
+
dw.stack.setCurrentIndex(cfg.PICTURE_VIEWER)
|
|
447
|
+
self.extract_frame(dw)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def draw_point(self, x: int, y: int, color: str, n_player: int = 0) -> None:
|
|
451
|
+
"""
|
|
452
|
+
draw point on frame-by-frame image
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
logging.debug("draw_point function")
|
|
456
|
+
|
|
457
|
+
RADIUS = 6
|
|
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)
|
|
472
|
+
self.dw_player[n_player].frame_viewer.update()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def draw_line(self, x1: int, y1: int, x2: int, y2: int, color: str, n_player: int = 0) -> None:
|
|
476
|
+
"""
|
|
477
|
+
draw line on frame-by-frame image
|
|
478
|
+
"""
|
|
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)
|
|
490
|
+
self.dw_player[n_player].frame_viewer.update()
|
|
491
|
+
|
|
492
|
+
|
|
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:
|
|
506
|
+
"""
|
|
507
|
+
Geometric measurements on image
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
n_player (int): id of clicked player
|
|
511
|
+
event (Qevent): event (mousepressed)
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
logging.debug("function image_clicked")
|
|
515
|
+
|
|
516
|
+
if not self.geometric_measurements_mode:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
if self.mem_player != -1 and n_player != self.mem_player:
|
|
520
|
+
self.mem_player = n_player
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
self.mem_player = n_player
|
|
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
|
|
531
|
+
|
|
532
|
+
if not (hasattr(self, "measurement_w") and (self.measurement_w is not None) and (self.measurement_w.isVisible())):
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
x, y = event.pos().x(), event.pos().y()
|
|
536
|
+
|
|
537
|
+
logging.debug(f"clicked on {x} {y}")
|
|
538
|
+
|
|
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)
|
|
542
|
+
|
|
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
|
|
559
|
+
|
|
560
|
+
self.measurement_w.status_lb.clear()
|
|
561
|
+
|
|
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"]
|
|
565
|
+
|
|
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))
|
|
571
|
+
|
|
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
|
+
)
|
|
613
|
+
|
|
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]
|
|
645
|
+
)
|
|
646
|
+
object_type = cfg.ORIENTED_ANGLE_OBJECT
|
|
647
|
+
|
|
648
|
+
append_results(
|
|
649
|
+
self,
|
|
650
|
+
(
|
|
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
|
+
),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
self.measurement_w.flag_saved = False
|
|
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
|
+
)
|
|
680
|
+
|
|
681
|
+
self.measurement_w.mem_points, self.measurement_w.mem_video = [], []
|
|
682
|
+
|
|
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):
|
|
688
|
+
draw_line(
|
|
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,
|
|
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
|
+
)
|
|
722
|
+
|
|
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
|
+
)
|
|
734
|
+
|
|
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
|
+
)
|
|
746
|
+
|
|
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
|
+
)
|
|
763
|
+
|
|
764
|
+
self.measurement_w.flag_saved = False
|
|
765
|
+
self.measurement_w.mem_points, self.measurement_w.mem_video = [], []
|
|
766
|
+
|
|
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):
|
|
772
|
+
draw_line(
|
|
773
|
+
self,
|
|
774
|
+
self.measurement_w.mem_points[-1][0],
|
|
775
|
+
self.measurement_w.mem_points[-1][1],
|
|
776
|
+
pixmap_x,
|
|
777
|
+
pixmap_y,
|
|
778
|
+
self.measurement_w.mark_color,
|
|
779
|
+
n_player,
|
|
780
|
+
)
|
|
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
|
+
)
|
|
796
|
+
|
|
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,
|
|
807
|
+
)
|
|
808
|
+
|
|
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
|
|
827
|
+
|
|
828
|
+
else:
|
|
829
|
+
self.measurement_w.status_lb.setText("<b>Choose a measurement type!</b>")
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def redraw_measurements(self):
|
|
833
|
+
"""
|
|
834
|
+
redraw measurements from previous frames
|
|
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
|
+
|
|
859
|
+
logging.debug("Redraw measurement marks")
|
|
860
|
+
|
|
861
|
+
if not (hasattr(self, "measurement_w") and self.measurement_w is not None and self.measurement_w.isVisible()):
|
|
862
|
+
return
|
|
863
|
+
|
|
864
|
+
if not self.measurement_w.cbPersistentMeasurements.isChecked():
|
|
865
|
+
self.measurement_w.draw_mem = {}
|
|
866
|
+
return
|
|
867
|
+
|
|
868
|
+
for idx, dw in enumerate(self.dw_player):
|
|
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
|
|
876
|
+
|
|
877
|
+
for frame in self.measurement_w.draw_mem:
|
|
878
|
+
for element in self.measurement_w.draw_mem[frame]:
|
|
879
|
+
if frame == current_frame:
|
|
880
|
+
elements_color = element["color"]
|
|
881
|
+
else:
|
|
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:
|
|
907
|
+
polygon = QPolygon()
|
|
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
|
+
|
|
923
|
+
dw.frame_viewer.update()
|
|
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)
|
|
930
|
+
|
|
931
|
+
draw_line(self, x1, y1, x2, y2, elements_color, n_player=idx)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
if __name__ == "__main__":
|
|
935
|
+
import sys
|
|
936
|
+
|
|
937
|
+
app = QApplication(sys.argv)
|
|
938
|
+
w = wgMeasurement(logging.getLogger().getEffectiveLevel())
|
|
939
|
+
w.show()
|
|
940
|
+
|
|
941
|
+
sys.exit(app.exec_())
|