boris-behav-obs 8.12__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -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 +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  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 +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  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 +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  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 +25 -33
  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 +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  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 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  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 +627 -236
  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 +95 -29
  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.12.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/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.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,31 +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,
41
52
  QSpacerItem,
42
53
  QSizePolicy,
54
+ QDialog,
43
55
  )
44
56
 
57
+ from typing import List
58
+
45
59
  from . import config as cfg
46
60
  from . import dialog, menu_options
47
61
  from . import utilities as util
48
62
 
49
63
 
50
- class wgMeasurement(QWidget):
64
+ class wgMeasurement(QDialog):
51
65
  """
52
66
  widget for geometric measurements
53
67
  """
54
68
 
55
- closeSignal = pyqtSignal()
56
- 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)
57
73
  mark_color: str = cfg.ACTIVE_MEASUREMENTS_COLOR
58
74
  flag_saved = True # store if measurements are saved
59
75
  draw_mem: dict = {}
76
+ mem_points: list = [] # memory of clicked points
77
+ mem_video: list = [] # memory of clicked points
60
78
 
61
79
  def __init__(self):
62
80
  super().__init__()
@@ -65,17 +83,22 @@ class wgMeasurement(QWidget):
65
83
 
66
84
  vbox = QVBoxLayout(self)
67
85
 
68
- self.rbPoint = QRadioButton("Point (left click)")
69
- 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)
70
91
 
71
- self.rbDistance = QRadioButton("Distance (start: left click, end: right click)")
72
- 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)
73
96
 
74
- self.rbArea = QRadioButton("Area (left click for area vertices, right click to close area)")
75
- 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)
76
99
 
77
- self.rbAngle = QRadioButton("Angle (vertex: left click, segments: right click)")
78
- 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)
79
102
 
80
103
  hbox = QHBoxLayout()
81
104
  self.cbPersistentMeasurements = QCheckBox("Measurements are persistent")
@@ -87,33 +110,58 @@ class wgMeasurement(QWidget):
87
110
  self.bt_color_chooser.setStyleSheet(f"QWidget {{background-color:{self.mark_color}}}")
88
111
  hbox.addWidget(self.bt_color_chooser)
89
112
 
113
+ hbox.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
114
+
90
115
  vbox.addLayout(hbox)
91
116
 
92
117
  vbox.addWidget(QLabel("<b>Scale</b>"))
93
118
 
94
119
  hbox1 = QHBoxLayout()
95
-
96
120
  self.lbRef = QLabel("Reference")
97
121
  hbox1.addWidget(self.lbRef)
98
-
99
122
  self.lbPx = QLabel("Pixels")
100
123
  hbox1.addWidget(self.lbPx)
101
-
102
124
  vbox.addLayout(hbox1)
103
125
 
104
126
  hbox2 = QHBoxLayout()
105
-
106
127
  self.leRef = QLineEdit()
107
128
  self.leRef.setText("1")
108
129
  hbox2.addWidget(self.leRef)
109
-
110
130
  self.lePx = QLineEdit()
111
131
  self.lePx.setText("1")
112
132
  hbox2.addWidget(self.lePx)
113
-
114
133
  vbox.addLayout(hbox2)
115
134
 
116
- 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
+
117
165
  vbox.addWidget(self.pte)
118
166
 
119
167
  self.status_lb = QLabel()
@@ -121,20 +169,33 @@ class wgMeasurement(QWidget):
121
169
 
122
170
  hbox3 = QHBoxLayout()
123
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)
124
174
 
125
- self.pbClear = QPushButton("Clear measurements", clicked=self.pbClear_clicked)
126
- 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)
127
177
 
128
- self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
129
- hbox3.addWidget(self.pbSave)
130
-
131
- self.pbClose = QPushButton("Close", clicked=self.pbClose_clicked)
132
- 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)
133
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)
134
186
  vbox.addLayout(hbox3)
135
187
 
136
188
  self.installEventFilter(self)
137
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
+
138
199
  def eventFilter(self, receiver, event):
139
200
  """
140
201
  send event (if keypress) to main window
@@ -145,6 +206,51 @@ class wgMeasurement(QWidget):
145
206
  else:
146
207
  return False
147
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
+
148
254
  def choose_marks_color(self):
149
255
  """
150
256
  show the color chooser dialog
@@ -152,6 +258,7 @@ class wgMeasurement(QWidget):
152
258
  cd = QColorDialog()
153
259
  cd.setWindowFlags(Qt.WindowStaysOnTopHint)
154
260
  cd.setOptions(QColorDialog.ShowAlphaChannel | QColorDialog.DontUseNativeDialog)
261
+ cd.setCurrentColor(QColor(self.mark_color))
155
262
 
156
263
  if cd.exec_():
157
264
  new_color = cd.currentColor()
@@ -163,20 +270,24 @@ class wgMeasurement(QWidget):
163
270
  Intercept the close event to check if measurements are saved
164
271
  """
165
272
 
273
+ logging.debug("close event")
274
+
166
275
  if not self.flag_saved:
167
276
  response = dialog.MessageDialog(
168
277
  cfg.programName,
169
278
  "The current measurements are not saved. Do you want to save the measurement results before closing?",
170
- [cfg.YES, cfg.NO, cfg.CANCEL],
279
+ (cfg.YES, cfg.NO, cfg.CANCEL),
171
280
  )
172
281
  if response == cfg.YES:
173
- self.pbSave_clicked()
282
+ if self.pb_save_clicked():
283
+ event.ignore()
284
+ return
174
285
  if response == cfg.CANCEL:
175
286
  event.ignore()
176
287
  return
177
288
 
178
- self.flag_saved = True
179
- self.draw_mem = {}
289
+ self.flag_saved: bool = True
290
+ self.draw_mem: dict = {}
180
291
  self.closeSignal.emit()
181
292
 
182
293
  def pbClear_clicked(self):
@@ -188,61 +299,130 @@ class wgMeasurement(QWidget):
188
299
  response = dialog.MessageDialog(
189
300
  cfg.programName,
190
301
  "Confirm clearing",
191
- [cfg.YES, cfg.CANCEL],
302
+ (cfg.YES, cfg.CANCEL),
192
303
  )
193
304
  if response == cfg.CANCEL:
194
305
  return
195
306
 
196
- self.draw_mem = {}
307
+ self.draw_mem: dict = {}
308
+ self.mem_points: list = []
309
+ self.mem_video: list = []
310
+
197
311
  self.pte.clear()
312
+ self.pte.setColumnCount(len(self.measurements_header))
313
+ self.pte.setRowCount(0)
314
+ self.pte.setHorizontalHeaderLabels(self.measurements_header)
198
315
  self.flag_saved = True
199
316
 
317
+ self.reload_image_signal.emit()
318
+
200
319
  def pbClose_clicked(self):
201
320
  """
202
321
  Close button
203
322
  """
323
+ logging.debug("close function")
204
324
  self.close()
205
325
 
206
- def pbSave_clicked(self):
326
+ def pb_save_clicked(self) -> bool:
207
327
  """
208
- Save measurements results in plain text file
328
+ Save measurements results
209
329
  """
210
- if self.pte.toPlainText():
211
- file_name, _ = QFileDialog().getSaveFileName(
212
- self, "Save geometric measurements", "", "Text files (*.txt);;All files (*)"
213
- )
214
- if file_name:
215
- try:
216
- with open(file_name, "w") as f:
217
- f.write(self.pte.toPlainText())
218
- self.flag_saved = True
219
- except Exception:
220
- 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(""))
221
342
  else:
222
- QMessageBox.information(self, cfg.programName, "There are no measurement results to save")
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
223
396
 
224
397
 
225
- def show_widget(self):
398
+ def show_widget(self) -> None:
226
399
  """
227
400
  active the geometric measurement widget
228
401
  """
229
402
 
230
403
  def close_measurement_widget():
404
+ """
405
+ close the geometric measurement widget
406
+ """
231
407
 
232
- self.geometric_measurements_mode = False
233
- for n_player, dw in enumerate(self.dw_player):
234
- dw.frame_viewer.clear()
235
- dw.stack.setCurrentIndex(cfg.VIDEO_VIEWER)
236
- dw.setWindowTitle(f"Player #{n_player + 1}")
237
- self.measurement_w.close()
408
+ logging.debug("close_measurement_widget")
238
409
 
239
- 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)
240
416
 
241
- 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)
242
420
 
243
- if self.playerType == cfg.IMAGES:
244
- QMessageBox.warning(None, cfg.programName, ("Not yet implemented"), QMessageBox.Ok)
245
- 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)
246
426
 
247
427
  self.geometric_measurements_mode = True
248
428
  self.pause_video()
@@ -252,9 +432,13 @@ def show_widget(self):
252
432
  self.actionPlay.setEnabled(False)
253
433
 
254
434
  self.measurement_w = wgMeasurement()
255
- self.measurement_w.setWindowFlags(Qt.WindowStaysOnTopHint)
435
+ # self.measurement_w.setWindowFlags(Qt.WindowStaysOnTopHint)
256
436
  self.measurement_w.closeSignal.connect(close_measurement_widget)
257
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
+
258
442
  self.measurement_w.show()
259
443
 
260
444
  for dw in self.dw_player:
@@ -263,35 +447,62 @@ def show_widget(self):
263
447
  self.extract_frame(dw)
264
448
 
265
449
 
266
- 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:
267
451
  """
268
452
  draw point on frame-by-frame image
269
453
  """
454
+
455
+ logging.debug("draw_point function")
456
+
270
457
  RADIUS = 6
271
- painter = QPainter()
272
- painter.begin(self.dw_player[n_player].frame_viewer.pixmap())
273
- painter.setPen(QColor(color))
274
- painter.drawEllipse(QPoint(x, y), RADIUS, RADIUS)
275
- # cross inside circle
276
- painter.drawLine(x - RADIUS, y, x + RADIUS, y)
277
- painter.drawLine(x, y - RADIUS, x, y + RADIUS)
278
- 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)
279
472
  self.dw_player[n_player].frame_viewer.update()
280
473
 
281
474
 
282
- 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:
283
476
  """
284
477
  draw line on frame-by-frame image
285
478
  """
286
- painter = QPainter()
287
- painter.begin(self.dw_player[n_player].frame_viewer.pixmap())
288
- painter.setPen(QColor(color))
289
- painter.drawLine(x1, y1, x2, y2)
290
- 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)
291
490
  self.dw_player[n_player].frame_viewer.update()
292
491
 
293
492
 
294
- 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:
295
506
  """
296
507
  Geometric measurements on image
297
508
 
@@ -300,7 +511,7 @@ def image_clicked(self, n_player, event):
300
511
  event (Qevent): event (mousepressed)
301
512
  """
302
513
 
303
- logging.debug(f"function image_clicked")
514
+ logging.debug("function image_clicked")
304
515
 
305
516
  if not self.geometric_measurements_mode:
306
517
  return
@@ -310,214 +521,341 @@ def image_clicked(self, n_player, event):
310
521
  return
311
522
 
312
523
  self.mem_player = n_player
313
- if self.dw_player[n_player].player.estimated_frame_number is not None:
314
- current_frame = self.dw_player[n_player].player.estimated_frame_number + 1
315
- else:
316
- current_frame = cfg.NA
317
- if hasattr(self, "measurement_w") and self.measurement_w is not None and self.measurement_w.isVisible():
318
- x, y = event.pos().x(), event.pos().y()
319
-
320
- # convert label coordinates in pixmap coordinates
321
- x = int(
322
- x
323
- - (self.dw_player[n_player].frame_viewer.width() - self.dw_player[n_player].frame_viewer.pixmap().width())
324
- / 2
325
- )
326
- y = int(
327
- y
328
- - (self.dw_player[n_player].frame_viewer.height() - self.dw_player[n_player].frame_viewer.pixmap().height())
329
- / 2
330
- )
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
331
531
 
332
- # convert pixmap coordinates in video coordinates
333
- x_video = round(
334
- (x / self.dw_player[n_player].frame_viewer.pixmap().width()) * self.dw_player[n_player].player.width
335
- )
336
- y_video = round(
337
- (y / self.dw_player[n_player].frame_viewer.pixmap().height()) * self.dw_player[n_player].player.height
338
- )
532
+ if not (hasattr(self, "measurement_w") and (self.measurement_w is not None) and (self.measurement_w.isVisible())):
533
+ return
339
534
 
340
- if not (
341
- 0 <= x <= self.dw_player[n_player].frame_viewer.pixmap().width()
342
- and 0 <= y <= self.dw_player[n_player].frame_viewer.pixmap().height()
343
- ):
344
- self.measurement_w.status_lb.setText("<b>The click is outside the video area</b>")
345
- return
535
+ x, y = event.pos().x(), event.pos().y()
346
536
 
347
- self.measurement_w.status_lb.clear()
537
+ logging.debug(f"clicked on {x} {y}")
348
538
 
349
- # point
350
- if self.measurement_w.rbPoint.isChecked():
351
- if event.button() == 1: # left click
352
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
353
- if current_frame not in self.measurement_w.draw_mem:
354
- 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)
355
542
 
356
- self.measurement_w.draw_mem[current_frame].append(
357
- [n_player, "point", self.measurement_w.mark_color, x, y]
358
- )
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
359
559
 
360
- self.measurement_w.pte.appendPlainText(
361
- (
362
- f"Time: {self.getLaps():.3f}\tPlayer: {n_player + 1}\t"
363
- f"Frame: {current_frame}\tPoint: {x_video},{y_video}"
364
- )
365
- )
366
- self.measurement_w.flag_saved = False
560
+ self.measurement_w.status_lb.clear()
367
561
 
368
- # distance
369
- elif self.measurement_w.rbDistance.isChecked():
370
- if event.button() == 1: # left
371
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
372
- self.memx, self.memy = x, y
373
- 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"]
374
565
 
375
- if event.button() == 2 and self.memx != -1 and self.memy != -1:
376
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
377
- 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))
378
571
 
379
- if current_frame not in self.measurement_w.draw_mem:
380
- self.measurement_w.draw_mem[current_frame] = []
381
- self.measurement_w.draw_mem[current_frame].append(
382
- [n_player, "line", self.measurement_w.mark_color, self.memx, self.memy, x, y]
383
- )
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
+ )
384
634
 
385
- distance = ((x_video - self.memx_video) ** 2 + (y_video - self.memy_video) ** 2) ** 0.5
386
- try:
387
- distance = distance / float(self.measurement_w.lePx.text()) * float(self.measurement_w.leRef.text())
388
- except Exception:
389
- QMessageBox.critical(
390
- None,
391
- cfg.programName,
392
- "Check reference and pixel values! Values must be numeric.",
393
- QMessageBox.Ok | QMessageBox.Default,
394
- QMessageBox.NoButton,
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]
395
645
  )
646
+ object_type = cfg.ORIENTED_ANGLE_OBJECT
396
647
 
397
- self.measurement_w.pte.appendPlainText(
648
+ append_results(
649
+ self,
398
650
  (
399
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
400
- f"Frame: {current_frame}\tDistance: {round(distance, 1)}"
401
- )
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
+ ),
402
666
  )
667
+
403
668
  self.measurement_w.flag_saved = False
404
- 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] = []
405
671
 
406
- # angle 1st clic -> vertex
407
- elif self.measurement_w.rbAngle.isChecked():
408
- if event.button() == 1: # left for vertex
409
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
410
- self.memPoints = [(x, y)]
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
+ )
411
680
 
412
- if event.button() == 2 and len(self.memPoints):
413
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
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):
414
688
  draw_line(
415
- 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,
416
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
+ )
417
722
 
418
- 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
+ )
419
734
 
420
- if len(self.memPoints) == 3:
421
- self.measurement_w.pte.appendPlainText(
422
- (
423
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
424
- f"Frame: {current_frame}\t"
425
- f"Angle: {round(util.angle(self.memPoints[0], self.memPoints[1], self.memPoints[2]), 1)}"
426
- )
427
- )
428
- self.measurement_w.flag_saved = False
429
- if current_frame not in self.measurement_w.draw_mem:
430
- self.measurement_w.draw_mem[current_frame] = []
431
- self.measurement_w.draw_mem[current_frame].append(
432
- [n_player, "angle", self.measurement_w.mark_color, self.memPoints]
433
- )
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
+ )
434
746
 
435
- self.memPoints = []
436
-
437
- # Area
438
- elif self.measurement_w.rbArea.isChecked():
439
- if event.button() == 1: # left
440
- draw_point(self, x, y, self.measurement_w.mark_color)
441
- if len(self.memPoints):
442
- draw_line(
443
- self,
444
- self.memPoints[-1][0],
445
- self.memPoints[-1][1],
446
- x,
447
- y,
448
- self.measurement_w.mark_color,
449
- n_player,
450
- )
451
- self.memPoints.append((x, y))
452
- 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
+ )
453
763
 
454
- if event.button() == 2 and len(self.memPoints) >= 2:
455
- draw_point(self, x, y, self.measurement_w.mark_color, n_player)
456
- draw_line(
457
- self, self.memPoints[-1][0], self.memPoints[-1][1], x, y, self.measurement_w.mark_color, n_player
458
- )
459
- self.memPoints.append((x, y))
460
- 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 = [], []
461
766
 
462
- # 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):
463
772
  draw_line(
464
773
  self,
465
- self.memPoints[-1][0],
466
- self.memPoints[-1][1],
467
- self.memPoints[0][0],
468
- 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,
469
778
  self.measurement_w.mark_color,
470
779
  n_player,
471
780
  )
472
- 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
+ )
473
796
 
474
- if current_frame not in self.measurement_w.draw_mem:
475
- self.measurement_w.draw_mem[current_frame] = []
476
- self.measurement_w.draw_mem[current_frame].append(
477
- [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,
478
807
  )
479
808
 
480
- try:
481
- area = (
482
- area
483
- / (float(self.measurement_w.lePx.text()) ** 2)
484
- * float(self.measurement_w.leRef.text()) ** 2
485
- )
486
- except Exception:
487
- QMessageBox.critical(
488
- None,
489
- cfg.programName,
490
- "Check reference and pixel values! Values must be numeric.",
491
- QMessageBox.Ok | QMessageBox.Default,
492
- QMessageBox.NoButton,
493
- )
494
-
495
- self.measurement_w.pte.appendPlainText(
496
- (
497
- f"Time: {self.getLaps()}\tPlayer: {n_player + 1}\t"
498
- f"Frame: {current_frame}\tArea: {round(area, 1)}"
499
- )
500
- )
501
- self.measurement_w.flag_saved = False
502
- 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
503
827
 
504
- else:
505
- self.measurement_w.status_lb.setText("<b>Choose a measurement type!</b>")
506
-
507
- else: # no measurements
508
- QMessageBox.warning(
509
- self,
510
- cfg.programName,
511
- "The Focus area function is not yet available in frame-by-frame mode.",
512
- QMessageBox.Ok | QMessageBox.Default,
513
- QMessageBox.NoButton,
514
- )
828
+ else:
829
+ self.measurement_w.status_lb.setText("<b>Choose a measurement type!</b>")
515
830
 
516
831
 
517
832
  def redraw_measurements(self):
518
833
  """
519
834
  redraw measurements from previous frames
520
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
+
521
859
  logging.debug("Redraw measurement marks")
522
860
 
523
861
  if not (hasattr(self, "measurement_w") and self.measurement_w is not None and self.measurement_w.isVisible()):
@@ -528,56 +866,72 @@ def redraw_measurements(self):
528
866
  return
529
867
 
530
868
  for idx, dw in enumerate(self.dw_player):
531
-
532
- if dw.player.estimated_frame_number is not None:
533
- current_frame = dw.player.estimated_frame_number + 1
534
- else:
535
- current_frame = cfg.NA
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
536
876
 
537
877
  for frame in self.measurement_w.draw_mem:
538
-
539
878
  for element in self.measurement_w.draw_mem[frame]:
540
-
541
879
  if frame == current_frame:
542
- elementsColor = element[2] # color
880
+ elements_color = element["color"]
543
881
  else:
544
- elementsColor = cfg.PASSIVE_MEASUREMENTS_COLOR
545
-
546
- if element[0] == idx:
547
- if element[1] == "point":
548
- x, y = element[3:]
549
- draw_point(self, x, y, elementsColor, n_player=idx)
550
-
551
- if element[1] == "line":
552
- x1, y1, x2, y2 = element[3:]
553
- draw_line(self, x1, y1, x2, y2, elementsColor, n_player=idx)
554
- draw_point(self, x1, y1, elementsColor, n_player=idx)
555
- draw_point(self, x2, y2, elementsColor, n_player=idx)
556
-
557
- if element[1] == "angle":
558
- x1, y1 = element[3][0]
559
- x2, y2 = element[3][1]
560
- x3, y3 = element[3][2]
561
- draw_line(self, x1, y1, x2, y2, elementsColor, n_player=idx)
562
- draw_line(self, x1, y1, x3, y3, elementsColor, n_player=idx)
563
- draw_point(self, x1, y1, elementsColor, n_player=idx)
564
- draw_point(self, x2, y2, elementsColor, n_player=idx)
565
- draw_point(self, x3, y3, elementsColor, n_player=idx)
566
-
567
- 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:
568
907
  polygon = QPolygon()
569
- for point in element[3]:
570
- polygon.append(QPoint(point[0], point[1]))
571
- painter = QPainter()
572
- painter.begin(self.dw_player[idx].frame_viewer.pixmap())
573
- painter.setPen(QColor(elementsColor))
574
- painter.drawPolygon(polygon)
575
- 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
+
576
923
  dw.frame_viewer.update()
577
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)
578
930
 
579
- if __name__ == "__main__":
931
+ draw_line(self, x1, y1, x2, y2, elements_color, n_player=idx)
580
932
 
933
+
934
+ if __name__ == "__main__":
581
935
  import sys
582
936
 
583
937
  app = QApplication(sys.argv)