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.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
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
- from PyQt5.QtCore import QPoint, Qt, pyqtSignal, QEvent
26
- from PyQt5.QtGui import QColor, QPainter, QPolygon
27
- from PyQt5.QtWidgets import (
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
- QPlainTextEdit,
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(QWidget):
64
+ class wgMeasurement(QDialog):
49
65
  """
50
66
  widget for geometric measurements
51
67
  """
52
68
 
53
- closeSignal = pyqtSignal()
54
- send_event_signal = pyqtSignal(QEvent)
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.rbPoint = QRadioButton("Point (left click)")
67
- vbox.addWidget(self.rbPoint)
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.rbDistance = QRadioButton("Distance (start: left click, end: right click)")
70
- vbox.addWidget(self.rbDistance)
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.rbArea = QRadioButton("Area (left click for area vertices, right click to close area)")
73
- vbox.addWidget(self.rbArea)
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.rbAngle = QRadioButton("Angle (vertex: left click, segments: right click)")
76
- vbox.addWidget(self.rbAngle)
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
- vbox.addWidget(self.cbPersistentMeasurements)
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
- vbox.addWidget(self.bt_color_chooser)
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 = QPlainTextEdit()
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.pbClear = QPushButton("Clear measurements", clicked=self.pbClear_clicked)
121
- hbox3.addWidget(self.pbClear)
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
- self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
124
- hbox3.addWidget(self.pbSave)
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
- [cfg.YES, cfg.NO, cfg.CANCEL],
279
+ (cfg.YES, cfg.NO, cfg.CANCEL),
166
280
  )
167
281
  if response == cfg.YES:
168
- self.pbSave_clicked()
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
- [cfg.YES, cfg.CANCEL],
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 pbSave_clicked(self):
326
+ def pb_save_clicked(self) -> bool:
202
327
  """
203
- Save measurements results in plain text file
328
+ Save measurements results
204
329
  """
205
- if self.pte.toPlainText():
206
- file_name, _ = QFileDialog().getSaveFileName(
207
- self, "Save geometric measurements", "", "Text files (*.txt);;All files (*)"
208
- )
209
- if file_name:
210
- try:
211
- with open(file_name, "w") as f:
212
- f.write(self.pte.toPlainText())
213
- self.flag_saved = True
214
- except Exception:
215
- QMessageBox.warning(self, cfg.programName, "An error occured during saving the measurement results")
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
- QMessageBox.information(self, cfg.programName, "There are no measurement results to save")
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
- def show_widget(self):
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
- self.geometric_measurements_mode = False
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
- menu_options.update_menu(self)
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.actionPlay.setEnabled(True)
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
- if self.playerType == cfg.IMAGES:
239
- QMessageBox.warning(None, cfg.programName, ("Not yet implemented"), QMessageBox.Ok)
240
- return
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
- painter = QPainter()
267
- painter.begin(self.dw_player[n_player].frame_viewer.pixmap())
268
- painter.setPen(QColor(color))
269
- painter.drawEllipse(QPoint(x, y), RADIUS, RADIUS)
270
- # cross inside circle
271
- painter.drawLine(x - RADIUS, y, x + RADIUS, y)
272
- painter.drawLine(x, y - RADIUS, x, y + RADIUS)
273
- painter.end()
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
- painter = QPainter()
282
- painter.begin(self.dw_player[n_player].frame_viewer.pixmap())
283
- painter.setPen(QColor(color))
284
- painter.drawLine(x1, y1, x2, y2)
285
- painter.end()
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 image_clicked(self, n_player, event):
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(f"function image_clicked")
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
- current_frame = self.dw_player[n_player].player.estimated_frame_number + 1
309
- if hasattr(self, "measurement_w") and self.measurement_w is not None and self.measurement_w.isVisible():
310
- x, y = event.pos().x(), event.pos().y()
311
-
312
- # convert label coordinates in pixmap coordinates
313
- x = int(
314
- x
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
- # convert pixmap coordinates in video coordinates
325
- x_video = round(
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
- if not (
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
- self.measurement_w.status_lb.clear()
537
+ logging.debug(f"clicked on {x} {y}")
340
538
 
341
- # point
342
- if self.measurement_w.rbPoint.isChecked():
343
- if event.button() == 1: # left click
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
- self.measurement_w.draw_mem[current_frame].append(
349
- [n_player, "point", self.measurement_w.mark_color, x, y]
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
- self.measurement_w.pte.appendPlainText(
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
- # distance
361
- elif self.measurement_w.rbDistance.isChecked():
362
- if event.button() == 1: # left
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
- if event.button() == 2 and self.memx != -1 and self.memy != -1:
368
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
369
- draw_line(self, self.memx, self.memy, x, y, self.measurement_w.mark_color, n_player)
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
- if current_frame not in self.measurement_w.draw_mem:
372
- self.measurement_w.draw_mem[current_frame] = []
373
- self.measurement_w.draw_mem[current_frame].append(
374
- [n_player, "line", self.measurement_w.mark_color, self.memx, self.memy, x, y]
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
- distance = ((x_video - self.memx_video) ** 2 + (y_video - self.memy_video) ** 2) ** 0.5
378
- try:
379
- distance = distance / float(self.measurement_w.lePx.text()) * float(self.measurement_w.leRef.text())
380
- except Exception:
381
- QMessageBox.critical(
382
- None,
383
- cfg.programName,
384
- "Check reference and pixel values! Values must be numeric.",
385
- QMessageBox.Ok | QMessageBox.Default,
386
- QMessageBox.NoButton,
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
- self.measurement_w.pte.appendPlainText(
648
+ append_results(
649
+ self,
390
650
  (
391
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
392
- f"Frame: {current_frame}\tDistance: {round(distance, 1)}"
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
- self.memx, self.memy = -1, -1
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
- # angle 1st clic -> vertex
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
- if event.button() == 2 and len(self.memPoints):
405
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
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, self.memPoints[0][0], self.memPoints[0][1], x, y, self.measurement_w.mark_color, n_player
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
- self.memPoints.append((x, y))
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
- if len(self.memPoints) == 3:
413
- self.measurement_w.pte.appendPlainText(
414
- (
415
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
416
- f"Frame: {current_frame}\t"
417
- f"Angle: {round(util.angle(self.memPoints[0], self.memPoints[1], self.memPoints[2]), 1)}"
418
- )
419
- )
420
- self.measurement_w.flag_saved = False
421
- if current_frame not in self.measurement_w.draw_mem:
422
- self.measurement_w.draw_mem[current_frame] = []
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
- self.memPoints = []
428
-
429
- # Area
430
- elif self.measurement_w.rbArea.isChecked():
431
- if event.button() == 1: # left
432
- draw_point(self, x, y, self.measurement_w.mark_color)
433
- if len(self.memPoints):
434
- draw_line(
435
- self,
436
- self.memPoints[-1][0],
437
- self.memPoints[-1][1],
438
- x,
439
- y,
440
- self.measurement_w.mark_color,
441
- n_player,
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
- if event.button() == 2 and len(self.memPoints) >= 2:
447
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
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
- # close polygon
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.memPoints[-1][0],
458
- self.memPoints[-1][1],
459
- self.memPoints[0][0],
460
- self.memPoints[0][1],
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
- area = util.polygon_area(self.memPoints_video)
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
- if current_frame not in self.measurement_w.draw_mem:
467
- self.measurement_w.draw_mem[current_frame] = []
468
- self.measurement_w.draw_mem[current_frame].append(
469
- [n_player, "polygon", self.measurement_w.mark_color, self.memPoints]
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
- try:
473
- area = (
474
- area
475
- / (float(self.measurement_w.lePx.text()) ** 2)
476
- * float(self.measurement_w.leRef.text()) ** 2
477
- )
478
- except Exception:
479
- QMessageBox.critical(
480
- None,
481
- cfg.programName,
482
- "Check reference and pixel values! Values must be numeric.",
483
- QMessageBox.Ok | QMessageBox.Default,
484
- QMessageBox.NoButton,
485
- )
486
-
487
- self.measurement_w.pte.appendPlainText(
488
- (
489
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
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
- else:
497
- self.measurement_w.status_lb.setText("<b>Choose a measurement type!</b>")
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
- current_frame = dw.player.estimated_frame_number + 1
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
- elementsColor = element[2] # color
880
+ elements_color = element["color"]
532
881
  else:
533
- elementsColor = cfg.PASSIVE_MEASUREMENTS_COLOR
534
-
535
- if element[0] == idx:
536
- if element[1] == "point":
537
- x, y = element[3:]
538
- draw_point(self, x, y, elementsColor, n_player=idx)
539
-
540
- if element[1] == "line":
541
- x1, y1, x2, y2 = element[3:]
542
- draw_line(self, x1, y1, x2, y2, elementsColor, n_player=idx)
543
- draw_point(self, x1, y1, elementsColor, n_player=idx)
544
- draw_point(self, x2, y2, elementsColor, n_player=idx)
545
-
546
- if element[1] == "angle":
547
- x1, y1 = element[3][0]
548
- x2, y2 = element[3][1]
549
- x3, y3 = element[3][2]
550
- draw_line(self, x1, y1, x2, y2, elementsColor, n_player=idx)
551
- draw_line(self, x1, y1, x3, y3, elementsColor, n_player=idx)
552
- draw_point(self, x1, y1, elementsColor, n_player=idx)
553
- draw_point(self, x2, y2, elementsColor, n_player=idx)
554
- draw_point(self, x3, y3, elementsColor, n_player=idx)
555
-
556
- if element[1] == "polygon":
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 point in element[3]:
559
- polygon.append(QPoint(point[0], point[1]))
560
- painter = QPainter()
561
- painter.begin(self.dw_player[idx].frame_viewer.pixmap())
562
- painter.setPen(QColor(elementsColor))
563
- painter.drawPolygon(polygon)
564
- painter.end()
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
- if __name__ == "__main__":
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)