boris-behav-obs 9.3.1__py2.py3-none-any.whl → 9.3.3__py2.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.
@@ -0,0 +1,59 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ number of occurences of behaviors
5
+ """
6
+
7
+ import pandas as pd
8
+
9
+ __version__ = "0.0.1"
10
+ __version_date__ = "2025-04-10"
11
+ __plugin_name__ = "Behavior latency"
12
+ __author__ = "Olivier Friard - University of Torino - Italy"
13
+
14
+
15
+ import itertools
16
+
17
+
18
+ def run(df: pd.DataFrame):
19
+ """
20
+ Latency of a behavior after another.
21
+ """
22
+
23
+ df["start_time"] = pd.to_datetime(df["Start (s)"])
24
+ df["end_time"] = pd.to_datetime(df["Stop (s)"])
25
+
26
+ latency_by_subject: dict = {}
27
+
28
+ for subject, group in df.groupby("subject"):
29
+ behaviors = group["behavior"].tolist()
30
+ # combinations = []
31
+ # Utiliser itertools pour créer des combinaisons 2 à 2 des comportements
32
+ for comb in itertools.combinations(behaviors, 2):
33
+ # combinations.append(comb)
34
+
35
+ last_A_end_time = None
36
+
37
+ # Liste pour stocker les latences de chaque sujet
38
+ subject_latency = []
39
+
40
+ for index, row in group.iterrows():
41
+ if row["behavior"] == comb[0]:
42
+ # Si on rencontre un comportement A, on réinitialise le temps de fin du comportement A
43
+ last_A_end_time = row["end_time"]
44
+ subject_latency.append(None) # Pas de latence pour A
45
+ elif row["behavior"] == comb[1] and last_A_end_time is not None:
46
+ # Si on rencontre un comportement B et qu'on a déjà vu un A avant
47
+ latency_time = row["start_time"] - last_A_end_time
48
+ subject_latency.append(latency_time)
49
+ else:
50
+ # Si on rencontre un B mais sans A avant
51
+ subject_latency.append(None)
52
+
53
+ # Ajout des latences calculées au DataFrame
54
+ df.loc[group.index, f"latency {comb[1]} after {comb[0]}"] = subject_latency
55
+
56
+ # Calcul de la latence totale ou moyenne par sujet
57
+ latency_by_subject[(subject, comb)] = df.groupby("subject")["latency"].agg(["sum", "mean"])
58
+
59
+ return str(latency_by_subject)
@@ -24,6 +24,7 @@ import binascii
24
24
  import io
25
25
  import json
26
26
  from pathlib import Path
27
+ import gui_utilities
27
28
 
28
29
  from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QLineF, QPoint, Qt, Signal
29
30
  from PySide6.QtGui import QBrush, QColor, QIcon, QMouseEvent, QPen, QPixmap, QPolygonF, QAction
@@ -790,7 +791,6 @@ class BehaviorsMapCreatorWindow(QMainWindow):
790
791
 
791
792
  if not self.fileName:
792
793
  return
793
- """if os.path.splitext(self.fileName)[1] != ".behav_coding_map":"""
794
794
  if Path(self.fileName).suffix != ".behav_coding_map":
795
795
  self.fileName += ".behav_coding_map"
796
796
  self.saveMap()
@@ -1106,10 +1106,6 @@ if __name__ == "__main__":
1106
1106
  app = QApplication(sys.argv)
1107
1107
  window = BehaviorsMapCreatorWindow(["North zone", "East zone", "South zone", "West zone"])
1108
1108
  window.bcm_list = []
1109
- window.resize(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1110
- screen_geometry = app.primaryScreen().geometry()
1111
- center_x = (screen_geometry.width() - window.width()) // 2
1112
- center_y = (screen_geometry.height() - window.height()) // 2
1113
- window.move(center_x, center_y)
1109
+ gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1114
1110
  window.show()
1115
1111
  sys.exit(app.exec())
boris/config.py CHANGED
@@ -155,6 +155,7 @@ CHECK_PROJECT_INTEGRITY = "check_project_integrity"
155
155
  YES = "Yes"
156
156
  NO = "No"
157
157
  CANCEL = "Cancel"
158
+ IGNORE = "Ignore"
158
159
  APPEND = "Append"
159
160
  CLOSE = "Close"
160
161
  REPLACE = "Replace"
@@ -454,7 +455,8 @@ POINT = "POINT"
454
455
  START = "START"
455
456
  STOP = "STOP"
456
457
 
457
- PLAYER1, PLAYER2 = "1", "2"
458
+ PLAYER1 = "1"
459
+ PLAYER2 = "2"
458
460
  ALL_PLAYERS = [str(x + 1) for x in range(N_PLAYER)]
459
461
 
460
462
  VISUALIZE_SPECTROGRAM = "visualize_spectrogram"
@@ -701,6 +703,7 @@ EMPTY_PROJECT = {
701
703
  ETHOGRAM: {},
702
704
  OBSERVATIONS: {},
703
705
  BEHAVIORAL_CATEGORIES: [],
706
+ BEHAVIORAL_CATEGORIES_CONF: {},
704
707
  INDEPENDENT_VARIABLES: {},
705
708
  CODING_MAP: {},
706
709
  BEHAVIORS_CODING_MAP: [],
boris/core.py CHANGED
@@ -114,7 +114,6 @@ from . import cmd_arguments
114
114
 
115
115
  from . import core_qrc
116
116
  from .core_ui import Ui_MainWindow
117
- import exifread
118
117
  from . import config as cfg
119
118
  from . import video_operations
120
119
 
@@ -135,8 +134,9 @@ __version__ = version.__version__
135
134
  __version_date__ = version.__version_date__
136
135
 
137
136
  # check minimal version of python
138
- if util.versiontuple(platform.python_version()) < util.versiontuple("3.8"):
139
- msg = f"BORIS requires Python 3.8+! You are using Python v. {platform.python_version()}\n"
137
+ MIN_PYTHON_VERSION = "3.12"
138
+ if util.versiontuple(platform.python_version()) < util.versiontuple(MIN_PYTHON_VERSION):
139
+ msg = f"BORIS requires Python {MIN_PYTHON_VERSION}+! You are using Python v. {platform.python_version()}\n"
140
140
  logging.critical(msg)
141
141
  sys.exit()
142
142
 
@@ -237,19 +237,6 @@ class TableModel(QAbstractTableModel):
237
237
  return self._data[row][event_idx]
238
238
 
239
239
 
240
- """
241
- class ButtonEventFilter(QObject):
242
- def eventFilter(self, obj, event):
243
- print("event filter")
244
- if isinstance(obj, QPushButton) and event.type() == QEvent.KeyPress:
245
- print("keypress")
246
- if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space):
247
- print("enter sapce")
248
- return False # Block the event
249
- return super().eventFilter(obj, event)
250
- """
251
-
252
-
253
240
  class MainWindow(QMainWindow, Ui_MainWindow):
254
241
  """
255
242
  Main BORIS window
@@ -301,7 +288,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
301
288
  ext_data_timer_list: list = []
302
289
  projectFileName: str = ""
303
290
  mediaTotalLength = None
304
- beep_every = 0
291
+ beep_every: float = 0.0
305
292
 
306
293
  plot_colors = cfg.BEHAVIORS_PLOT_COLORS
307
294
  behav_category_colors = cfg.CATEGORY_COLORS_LIST
@@ -315,19 +302,20 @@ class MainWindow(QMainWindow, Ui_MainWindow):
315
302
  fast = 10
316
303
 
317
304
  currentStates: dict = {}
318
- subject_name_index = {}
305
+ subject_name_index: dict = {}
319
306
  flag_slow = False
320
307
  play_rate: float = 1
321
308
  play_rate_step: float = 0.1
322
309
  currentSubject: str = "" # contains the current subject of observation
323
- coding_map_window_geometry = 0
324
310
 
325
311
  # FFmpeg
326
- memx, memy, mem_player = -1, -1, -1
312
+ memx = -1
313
+ memy = -1
314
+ mem_player = -1
327
315
 
328
316
  # path for ffmpeg/ffmpeg.exe program
329
- ffmpeg_bin = ""
330
- ffmpeg_cache_dir = ""
317
+ ffmpeg_bin: str = ""
318
+ ffmpeg_cache_dir: str = ""
331
319
 
332
320
  # dictionary for FPS storing
333
321
  fps = 0
@@ -390,9 +378,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
390
378
  super(MainWindow, self).__init__(parent)
391
379
  self.setupUi(self)
392
380
 
393
- # disable trigger with RETURN or SPACE keys
394
- """filter_obj = ButtonEventFilter()
395
- self.pb_live_obs.installEventFilter(filter_obj)"""
396
381
  self.pb_live_obs.setFocusPolicy(Qt.NoFocus)
397
382
 
398
383
  self.ffmpeg_bin = ffmpeg_bin
@@ -418,25 +403,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
418
403
  self.tb_export.setMenu(self.menu)
419
404
  """
420
405
 
421
- gui_utilities.set_icons(self, theme_mode=self.theme_mode())
406
+ gui_utilities.set_icons(self, theme_mode=gui_utilities.theme_mode())
422
407
 
423
408
  self.setWindowTitle(f"{cfg.programName} ({__version__})")
424
409
 
425
- self.w_obs_info.setVisible(False)
426
-
427
410
  self.lbLogoBoris.setPixmap(QPixmap(":/logo"))
428
-
429
411
  self.lbLogoBoris.setScaledContents(False)
430
412
  self.lbLogoBoris.setAlignment(Qt.AlignCenter)
431
413
 
432
- # self.lbLogoUnito.setPixmap(QPixmap(":/dbios_unito"))
433
- # self.lbLogoUnito.setScaledContents(False)
434
- # self.lbLogoUnito.setAlignment(Qt.AlignCenter)
435
-
436
414
  self.toolBar.setEnabled(True)
437
415
 
438
416
  # start with dock widget invisible
439
- for w in [self.dwEvents, self.dwEthogram, self.dwSubjects]:
417
+ for w in (self.w_obs_info, self.dwEvents, self.dwEthogram, self.dwSubjects):
440
418
  w.setVisible(False)
441
419
  w.keyPressEvent = self.keyPressEvent
442
420
 
@@ -475,15 +453,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
475
453
  self.lbTimeOffset.setMinimumWidth(160)
476
454
  self.statusbar.addPermanentWidget(self.lbTimeOffset)
477
455
 
478
- # play rate are now displayed in the main info widget
479
- """
480
- # SPEED
481
- self.lbSpeed = QLabel()
482
- self.lbSpeed.setFrameStyle(QFrame.StyledPanel)
483
- self.lbSpeed.setMinimumWidth(40)
484
- self.statusbar.addPermanentWidget(self.lbSpeed)
485
- """
486
-
487
456
  # set painter for tv_events to highlight current row
488
457
  delegate = self.CustomItemDelegate()
489
458
  self.tv_events.setItemDelegate(delegate)
@@ -499,14 +468,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
499
468
  plugins.load_plugins(self)
500
469
  plugins.add_plugins_to_menu(self)
501
470
 
502
- def theme_mode(self):
503
- """
504
- return the theme mode (dark or light) of the OS
505
- """
506
- palette = QApplication.instance().palette()
507
- color = palette.window().color()
508
- return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
509
-
510
471
  class CustomItemDelegate(QStyledItemDelegate):
511
472
  def paint(self, painter, option, index):
512
473
  # Custom drawing logic here (overriding paint)
@@ -1506,7 +1467,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1506
1467
  # one media
1507
1468
  if self.dw_player[player].player.playlist_count == 1:
1508
1469
  if new_time < self.dw_player[player].player.duration:
1509
- self.dw_player[player].player.seek(new_time, "absolute+exact")
1470
+ new_time_float = round(float(new_time), 3)
1471
+
1472
+ self.dw_player[player].player.seek(new_time_float, "absolute+exact")
1510
1473
 
1511
1474
  if player == 0 and not self.user_move_slider:
1512
1475
  self.video_slider.setValue(
@@ -3835,7 +3798,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3835
3798
  if self.geometric_measurements_mode:
3836
3799
  geometric_measurement.redraw_measurements(self)
3837
3800
 
3838
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3801
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
3839
3802
 
3840
3803
  def previous_frame(self) -> None:
3841
3804
  """
@@ -3859,7 +3822,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3859
3822
  if self.geometric_measurements_mode:
3860
3823
  geometric_measurement.redraw_measurements(self)
3861
3824
 
3862
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
3825
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
3863
3826
 
3864
3827
  def run_event_outside(self):
3865
3828
  """
@@ -4463,7 +4426,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4463
4426
  for data_timer in self.ext_data_timer_list:
4464
4427
  data_timer.stop()
4465
4428
 
4466
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
4429
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
4467
4430
 
4468
4431
  if msg:
4469
4432
  self.lb_current_media_time.setText(msg)
@@ -5634,7 +5597,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5634
5597
  for data_timer in self.ext_data_timer_list:
5635
5598
  data_timer.start()
5636
5599
 
5637
- self.actionPlay.setIcon(QIcon(f":/pause_{self.theme_mode()}"))
5600
+ self.actionPlay.setIcon(QIcon(f":/pause_{gui_utilities.theme_mode()}"))
5638
5601
  self.actionPlay.setText("Pause")
5639
5602
 
5640
5603
  return True
@@ -5668,7 +5631,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5668
5631
  for idx in self.plot_data:
5669
5632
  self.timer_plot_data_out(self.plot_data[idx])
5670
5633
 
5671
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
5634
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
5672
5635
  self.actionPlay.setText("Play")
5673
5636
 
5674
5637
  def play_activated(self):
@@ -5823,8 +5786,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5823
5786
 
5824
5787
 
5825
5788
  def main():
5826
- # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
5827
-
5828
5789
  app = QApplication(sys.argv)
5829
5790
  app.setStyle("Fusion")
5830
5791
 
@@ -5917,7 +5878,7 @@ def main():
5917
5878
  results.show()
5918
5879
 
5919
5880
  window.show()
5920
- window.raise_()
5881
+ window.raise_() # for overlapping widget (?)
5921
5882
 
5922
5883
  if observation_to_open and "error" not in pj:
5923
5884
  r = observation_operations.load_observation(window, obs_id=observation_to_open, mode=cfg.OBS_START)
boris/dialog.py CHANGED
@@ -71,7 +71,12 @@ from . import utilities as util
71
71
 
72
72
  def MessageDialog(title: str, text: str, buttons: tuple) -> str:
73
73
  """
74
- generic message dialog
74
+ show a generic message dialog and returns the text of the clicked button
75
+
76
+ Args:
77
+ title (str): Title of the dialog box
78
+ text (str): text of the dialog box
79
+ buttons (tuple): text for buttons
75
80
 
76
81
  Return
77
82
  str: text of the clicked button
@@ -83,8 +88,8 @@ def MessageDialog(title: str, text: str, buttons: tuple) -> str:
83
88
  for button in buttons:
84
89
  message.addButton(button, QMessageBox.YesRole)
85
90
 
86
- # message.setWindowFlags(Qt.WindowStaysOnTopHint)
87
- message.exec_()
91
+ message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
92
+ message.exec()
88
93
  return message.clickedButton().text()
89
94
 
90
95
 
boris/gui_utilities.py CHANGED
@@ -22,10 +22,19 @@ Copyright 2012-2025 Olivier Friard
22
22
  import pathlib as pl
23
23
  import logging
24
24
  from PySide6.QtCore import QSettings
25
- from PySide6.QtWidgets import QWidget
25
+ from PySide6.QtWidgets import QWidget, QApplication
26
26
  from PySide6.QtGui import QIcon
27
27
 
28
28
 
29
+ def theme_mode() -> str:
30
+ """
31
+ return the theme mode (dark or light) of the OS
32
+ """
33
+ palette = QApplication.instance().palette()
34
+ color = palette.window().color()
35
+ return "dark" if color.value() < 128 else "light" # Dark mode if the color value is less than 128
36
+
37
+
29
38
  def save_geometry(widget: QWidget, widget_name: str):
30
39
  """
31
40
  save window geometry in ini file
@@ -44,6 +53,7 @@ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
44
53
  """
45
54
  restore window geometry in ini file
46
55
  """
56
+
47
57
  def default_resize(widget, default_width_height):
48
58
  if default_width_height != (0, 0):
49
59
  try:
@@ -51,15 +61,14 @@ def restore_geometry(widget: QWidget, widget_name: str, default_width_height):
51
61
  except Exception:
52
62
  logging.warning("Error during restoring default")
53
63
 
54
-
55
- logging.debug(f'restore geometry function for {widget_name}')
64
+ logging.debug(f"restore geometry function for {widget_name}")
56
65
  try:
57
66
  ini_file_path = pl.Path.home() / pl.Path(".boris")
58
67
  if ini_file_path.is_file():
59
68
  settings = QSettings(str(ini_file_path), QSettings.IniFormat)
60
69
  print(settings.value(f"{widget_name} geometry"))
61
70
  widget.restoreGeometry(settings.value(f"{widget_name} geometry"))
62
- logging.debug(f'geometry restored for {widget_name} {settings.value(f"{widget_name} geometry")}')
71
+ logging.debug(f"geometry restored for {widget_name} {settings.value(f'{widget_name} geometry')}")
63
72
  else:
64
73
  default_resize(widget, default_width_height)
65
74
  except Exception:
@@ -108,3 +117,20 @@ def set_icons(self, theme_mode: str) -> None:
108
117
  self.action_geometric_measurements.setIcon(QIcon(f":/measurement_{theme_mode}"))
109
118
  self.actionFind_in_current_obs.setIcon(QIcon(f":/find_{theme_mode}"))
110
119
  self.actionExplore_project.setIcon(QIcon(f":/explore_{theme_mode}"))
120
+
121
+
122
+ def resize_center(app, window, width: int, height: int) -> None:
123
+ """
124
+ resize and center window
125
+ """
126
+ window.resize(width, height)
127
+ screen_geometry = app.primaryScreen().geometry()
128
+ if window.height() > screen_geometry.height():
129
+ window.resize(window.width(), int(screen_geometry.height() * 0.8))
130
+ if window.width() > screen_geometry.width():
131
+ window.resize(screen_geometry.width(), window.height())
132
+ # center
133
+ center_x = (screen_geometry.width() - window.width()) // 2
134
+ center_y = (screen_geometry.height() - window.height()) // 2
135
+
136
+ window.move(center_x, center_y)
boris/media_file.py CHANGED
@@ -20,11 +20,13 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
+ from PySide6.QtWidgets import QFileDialog
24
+
23
25
  from . import config as cfg
24
26
  from . import utilities as util
25
27
  from . import dialog
26
28
  from . import project_functions
27
- from PySide6.QtWidgets import QFileDialog
29
+ from . import utilities as util
28
30
 
29
31
 
30
32
  def get_info(self) -> None:
@@ -38,17 +40,17 @@ def get_info(self) -> None:
38
40
  if "error" in r:
39
41
  ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
40
42
  else:
41
- ffmpeg_output = f"<br><b>{r['analysis_program'] } analysis</b><br>"
43
+ ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
42
44
 
43
45
  ffmpeg_output += (
44
46
  f"File path: <b>{media_full_path}</b><br><br>"
45
47
  f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
48
+ f"FPS: {r['fps']}<br>"
49
+ f"Resolution: {r['resolution']} pixels<br>"
46
50
  f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
47
51
  f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
48
- f"Resolution: {r['resolution']}<br>"
49
52
  f"Number of frames: {r['frames_number']}<br>"
50
53
  f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
51
- f"FPS: {r['fps']}<br>"
52
54
  f"Has video: {r['has_video']}<br>"
53
55
  f"Has audio: {r['has_audio']}<br>"
54
56
  f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
@@ -70,6 +72,12 @@ def get_info(self) -> None:
70
72
 
71
73
  mpv_output = (
72
74
  "<b>MPV information</b><br>"
75
+ f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
76
+ # "Position: {} %<br>"
77
+ f"FPS: {dw.player.container_fps}<br>"
78
+ # "Rate: {}<br>"
79
+ f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
80
+ # "Scale: {}<br>"
73
81
  f"Video format: {dw.player.video_format}<br>"
74
82
  # "State: {}<br>"
75
83
  # "Media Resource Location: {}<br>"
@@ -77,12 +85,6 @@ def get_info(self) -> None:
77
85
  # "Track: {}/{}<br>"
78
86
  f"Number of media in media list: {dw.player.playlist_count}<br>"
79
87
  f"Current time position: {dw.player.time_pos}<br>"
80
- f"Duration: {dw.player.duration}<br>"
81
- # "Position: {} %<br>"
82
- f"FPS: {dw.player.container_fps}<br>"
83
- # "Rate: {}<br>"
84
- f"Video size: {dw.player.width}x{dw.player.height}<br>"
85
- # "Scale: {}<br>"
86
88
  f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
87
89
  # "is seekable? {}<br>"
88
90
  # "has_vout? {}<br>"
@@ -26,8 +26,9 @@ This file is part of BORIS.
26
26
  import binascii
27
27
  import io
28
28
  import json
29
- import os
29
+ from pathlib import Path
30
30
  import re
31
+ import gui_utilities
31
32
 
32
33
  from PySide6.QtCore import (
33
34
  Qt,
@@ -727,7 +728,8 @@ class ModifiersMapCreatorWindow(QMainWindow):
727
728
  self.fileName = fn
728
729
 
729
730
  if self.fileName:
730
- if os.path.splitext(self.fileName)[1] != ".boris_map":
731
+ # if os.path.splitext(self.fileName)[1] != ".boris_map":
732
+ if Path(self.fileName).suffix != ".boris_map":
731
733
  self.fileName += ".boris_map"
732
734
  self.saveMap()
733
735
 
@@ -744,7 +746,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
744
746
  else:
745
747
  self.fileName = fn
746
748
 
747
- if self.fileName and os.path.splitext(self.fileName)[1] != ".boris_map":
749
+ if self.fileName and Path(self.fileName).suffix() != ".boris_map":
748
750
  self.fileName += ".boris_map"
749
751
 
750
752
  if self.fileName:
@@ -1006,25 +1008,8 @@ if __name__ == "__main__":
1006
1008
 
1007
1009
  app = QApplication(sys.argv)
1008
1010
  window = ModifiersMapCreatorWindow()
1009
- window.resize(800, 700)
1010
1011
 
1011
- print(f"{window.width()=}")
1012
- print(f"{window.height()=}")
1013
-
1014
- # Get the screen geometry (screen size and position)
1015
- screen_geometry = app.primaryScreen().geometry()
1016
-
1017
- print(f"{screen_geometry=}")
1018
-
1019
- # Calculate the center of the screen
1020
- center_x = (screen_geometry.width() - window.width()) // 2
1021
- center_y = (screen_geometry.height() - window.height()) // 2
1022
-
1023
- print(f"{center_x=}")
1024
- print(f"{center_y=}")
1025
-
1026
- # Move the widget to the center of the screen
1027
- window.move(center_x, center_y)
1012
+ gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
1028
1013
 
1029
1014
  window.show()
1030
1015
  sys.exit(app.exec())
boris/observation.py CHANGED
@@ -1008,27 +1008,30 @@ class Observation(QDialog, Ui_Form):
1008
1008
 
1009
1009
  # check if observation id not empty
1010
1010
  if not self.leObservationId.text():
1011
- self.qm = QMessageBox()
1012
- self.qm.setIcon(QMessageBox.Critical)
1013
- self.qm.setText("The <b>observation id</b> is mandatory and must be unique.")
1014
- self.qm.exec_()
1011
+ QMessageBox.critical(
1012
+ self,
1013
+ cfg.programName,
1014
+ "The <b>observation id</b> is mandatory and must be unique.",
1015
+ )
1015
1016
  return False
1016
1017
 
1017
1018
  # check if observation_type
1018
1019
  if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
1019
- self.qm = QMessageBox()
1020
- self.qm.setIcon(QMessageBox.Critical)
1021
- self.qm.setText("Choose an observation type.")
1022
- self.qm.exec_()
1020
+ QMessageBox.critical(
1021
+ self,
1022
+ cfg.programName,
1023
+ "Choose an observation type.",
1024
+ )
1023
1025
  return False
1024
1026
 
1025
1027
  # check if offset is correct
1026
1028
  if self.cb_time_offset.isChecked():
1027
1029
  if self.obs_time_offset.get_time() is None:
1028
- self.qm = QMessageBox()
1029
- self.qm.setIcon(QMessageBox.Critical)
1030
- self.qm.setText("Check the time offset value.")
1031
- self.qm.exec_()
1030
+ QMessageBox.critical(
1031
+ self,
1032
+ cfg.programName,
1033
+ "Check the time offset value.",
1034
+ )
1032
1035
  return False
1033
1036
 
1034
1037
  if self.rb_media_files.isChecked(): # observation based on media file(s)
@@ -1051,18 +1054,20 @@ class Observation(QDialog, Ui_Form):
1051
1054
 
1052
1055
  # check if player #1 is used
1053
1056
  if not players_list or min(players_list) > 1:
1054
- self.qm = QMessageBox()
1055
- self.qm.setIcon(QMessageBox.Critical)
1056
- self.qm.setText("A media file must be loaded in player #1")
1057
- self.qm.exec_()
1057
+ QMessageBox.critical(
1058
+ self,
1059
+ cfg.programName,
1060
+ "A media file must be loaded in player #1",
1061
+ )
1058
1062
  return False
1059
1063
 
1060
1064
  # check if players are used in crescent order
1061
1065
  if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
1062
- self.qm = QMessageBox()
1063
- self.qm.setIcon(QMessageBox.Critical)
1064
- self.qm.setText("Some player are not used. Please reorganize your media files")
1065
- self.qm.exec_()
1066
+ QMessageBox.critical(
1067
+ self,
1068
+ cfg.programName,
1069
+ "Some player are not used. Please reorganize your media files",
1070
+ )
1066
1071
  return False
1067
1072
 
1068
1073
  # check if more media in player #1 and media in other players
@@ -888,7 +888,7 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
888
888
  self.pj[cfg.OBSERVATIONS][obsId][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS]
889
889
  )
890
890
 
891
- rv = observationWindow.exec_()
891
+ rv = observationWindow.exec()
892
892
 
893
893
  # save geometry
894
894
  gui_utilities.save_geometry(observationWindow, "new observation")
@@ -2012,7 +2012,7 @@ def initialize_new_media_observation(self) -> bool:
2012
2012
  self.mpv_eof_reached_signal.connect(self.mpv_eof_reached)
2013
2013
  self.video_click_signal.connect(self.player_clicked)
2014
2014
 
2015
- self.actionPlay.setIcon(QIcon(f":/play_{self.theme_mode()}"))
2015
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
2016
2016
 
2017
2017
  self.display_statusbar_info(self.observationId)
2018
2018
 
@@ -2406,15 +2406,15 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2406
2406
  """
2407
2407
 
2408
2408
  cumul_media_durations: list = [dec(0)]
2409
- for media_file in observation[cfg.FILE]["1"]:
2409
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2410
2410
  media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2411
- cumul_media_durations.append(cumul_media_durations[-1] + media_duration)
2411
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2412
2412
 
2413
2413
  cumul_media_durations.remove(dec(0))
2414
2414
 
2415
2415
  # test if timestamp is at end of last media
2416
2416
  if timestamp == cumul_media_durations[-1]:
2417
- player_idx = len(observation[cfg.FILE]["1"]) - 1
2417
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2418
2418
  else:
2419
2419
  player_idx = -1
2420
2420
  for idx, value in enumerate(cumul_media_durations):
@@ -2424,7 +2424,7 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2424
2424
  break
2425
2425
 
2426
2426
  if player_idx != -1:
2427
- video_file_name = observation[cfg.FILE]["1"][player_idx]
2427
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx]
2428
2428
  else:
2429
2429
  video_file_name = None
2430
2430
 
@@ -23,8 +23,12 @@ This file is part of BORIS.
23
23
  import sys
24
24
  import logging
25
25
  import functools
26
+ from . import mpv2 as mpv
27
+ import config as cfg
28
+ import gui_utilities
29
+
30
+
26
31
  from PySide6.QtWidgets import (
27
- QApplication,
28
32
  QLabel,
29
33
  QDockWidget,
30
34
  QWidget,
@@ -38,8 +42,6 @@ from PySide6.QtWidgets import (
38
42
  from PySide6.QtCore import Signal, QEvent, Qt
39
43
  from PySide6.QtGui import QIcon, QAction
40
44
 
41
- from . import mpv2 as mpv
42
- import config as cfg
43
45
 
44
46
  """
45
47
  try:
@@ -153,7 +155,7 @@ class DW_player(QDockWidget):
153
155
  self.mute_button.setAutoRaise(True)
154
156
  self.mute_action = QAction()
155
157
 
156
- theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
158
+ theme_mode = gui_utilities.theme_mode()
157
159
 
158
160
  self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
159
161
  self.mute_action.triggered.connect(self.mute_action_triggered)
@@ -194,7 +196,7 @@ class DW_player(QDockWidget):
194
196
  """
195
197
  emit signal when mute action is triggered
196
198
  """
197
- theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
199
+ theme_mode = gui_utilities.theme_mode()
198
200
  if self.player.mute:
199
201
  self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
200
202
  else:
boris/plugins.py CHANGED
@@ -82,6 +82,8 @@ def load_plugins(self):
82
82
  for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
83
83
  if file_.name == "__init__.py":
84
84
  continue
85
+ if file_.name.startswith("_"):
86
+ continue
85
87
  plugin_name = get_plugin_name(file_)
86
88
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
87
89
  # check if plugin with same name already loaded
@@ -96,6 +98,8 @@ def load_plugins(self):
96
98
  for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
97
99
  if file_.name == "__init__.py":
98
100
  continue
101
+ if file_.name.startswith("_"):
102
+ continue
99
103
  plugin_name = get_plugin_name(file_)
100
104
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
101
105
  # check if plugin with same name already loaded
boris/project.py CHANGED
@@ -82,12 +82,12 @@ class BehavioralCategories(QDialog):
82
82
  # add categories
83
83
  self.lw.setColumnCount(2)
84
84
  self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
85
- # self.lw.verticalHeader().hide()
86
85
  self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
87
86
 
88
- # self.lw.setSelectionBehavior(QAbstractItemView.SelectRows)
89
87
  self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
90
88
 
89
+ behavioral_categories: list = []
90
+
91
91
  if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
92
92
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
93
93
  behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
@@ -95,7 +95,7 @@ class BehavioralCategories(QDialog):
95
95
  # name
96
96
  item = QTableWidgetItem()
97
97
  item.setText(behav_cat[key]["name"])
98
- # item.setFlags(Qt.ItemIsEnabled)
98
+ behavioral_categories.append(behav_cat[key]["name"])
99
99
  self.lw.setItem(idx, 0, item)
100
100
  # color
101
101
  item = QTableWidgetItem()
@@ -103,24 +103,21 @@ class BehavioralCategories(QDialog):
103
103
  if behav_cat[key].get(cfg.COLOR, ""):
104
104
  item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
105
105
  else:
106
- # item.setBackground(QColor(230, 230, 230))
107
106
  item.setBackground(self.not_editable_column_color())
108
- # item.setFlags(Qt.ItemIsEnabled)
109
107
  self.lw.setItem(idx, 1, item)
110
108
  else:
111
109
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
112
110
  for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
111
+ # name
113
112
  item = QTableWidgetItem()
114
113
  item.setText(category)
115
- # item.setFlags(Qt.ItemIsEnabled)
114
+ behavioral_categories.append(category)
116
115
  self.lw.setItem(idx, 0, item)
117
-
116
+ # color
118
117
  item = QTableWidgetItem()
119
118
  item.setText("")
120
- # item.setFlags(Qt.ItemIsEnabled)
121
- self.lw.setItem(idx, 1, item)
122
119
 
123
- # self.lw.addItem(QListWidgetItem(category))
120
+ self.lw.setItem(idx, 1, item)
124
121
 
125
122
  self.vbox.addWidget(self.lw)
126
123
 
@@ -148,6 +145,41 @@ class BehavioralCategories(QDialog):
148
145
 
149
146
  self.setLayout(self.vbox)
150
147
 
148
+ # check if behavioral categories are present in events
149
+ behavioral_categories_in_ethogram = set(
150
+ sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
151
+ )
152
+
153
+ if behavioral_categories_in_ethogram.difference(set(behavioral_categories)):
154
+ if (
155
+ dialog.MessageDialog(
156
+ cfg.programName,
157
+ (
158
+ "They are behavioral categories that are present in ethogram but not defined.<br>"
159
+ f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
160
+ "<br>"
161
+ "Do you want to add them in the behavioral categories list?"
162
+ ),
163
+ [cfg.YES, cfg.NO],
164
+ )
165
+ == cfg.YES
166
+ ):
167
+ # add behavioral categories present in ethogram in behavioal categories list
168
+ rc = self.lw.rowCount()
169
+ self.lw.setRowCount(rc + len(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))
170
+ for idx, category in enumerate(sorted(list(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))):
171
+ print(category)
172
+ # name
173
+ item = QTableWidgetItem()
174
+ item.setText(category)
175
+ # behavioral_categories.append(category)
176
+ self.lw.setItem(rc + idx, 0, item)
177
+ # color
178
+ item = QTableWidgetItem()
179
+ item.setText("")
180
+
181
+ self.lw.setItem(rc + idx, 1, item)
182
+
151
183
  def not_editable_column_color(self):
152
184
  """
153
185
  return a color for the not editable column
@@ -663,7 +695,7 @@ class projectDialog(QDialog, Ui_dlgProject):
663
695
  QMessageBox.warning(
664
696
  self,
665
697
  cfg.programName,
666
- ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
698
+ ("The following behavior{} are not defined in the ethogram:<br>{}").format(
667
699
  "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
668
700
  ),
669
701
  )
@@ -733,7 +765,7 @@ class projectDialog(QDialog, Ui_dlgProject):
733
765
  behavioral categories manager
734
766
  """
735
767
 
736
- bc = BehavioralCategories(self.pj) # self.config_param.get(cfg.DARK_MODE, cfg.DEFAULT_FRAME_MODE)
768
+ bc = BehavioralCategories(self.pj)
737
769
 
738
770
  if bc.exec_():
739
771
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
@@ -1377,7 +1409,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1377
1409
  # let user select a coding maop
1378
1410
  file_name, _ = QFileDialog().getOpenFileName(
1379
1411
  self,
1380
- "Select a modifier coding map for " f"{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1412
+ f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1381
1413
  "",
1382
1414
  "BORIS map files (*.boris_map);;All files (*)",
1383
1415
  )
@@ -1759,24 +1791,40 @@ class projectDialog(QDialog, Ui_dlgProject):
1759
1791
  return {cfg.CANCEL: True}
1760
1792
 
1761
1793
  # check if behavior belong to category that is not in categories list
1762
- behavior_category: list = []
1794
+ missing_behavior_category: list = []
1763
1795
  for idx in checked_ethogram:
1764
1796
  if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
1765
1797
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
1766
1798
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
1767
- behavior_category.append((checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]))
1768
- if behavior_category:
1799
+ missing_behavior_category.append(
1800
+ (checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
1801
+ )
1802
+ if missing_behavior_category:
1769
1803
  response = dialog.MessageDialog(
1770
1804
  f"{cfg.programName} - Behavioral categories",
1771
1805
  (
1772
- "The behavioral categorie(s) "
1773
- f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in behavior_category]))} "
1774
- "are no more defined in behavioral categories list"
1806
+ "The behavioral category/ies<br> "
1807
+ f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)<br>' for x in missing_behavior_category]))} "
1808
+ "are not defined in behavioral categories list.<br>"
1775
1809
  ),
1776
- ["Add behavioral category/ies", "Ignore", cfg.CANCEL],
1810
+ ["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
1777
1811
  )
1778
1812
  if response == "Add behavioral category/ies":
1779
- [self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1) for x1 in set(x[1] for x in behavior_category)]
1813
+ if cfg.BEHAVIORAL_CATEGORIES_CONF not in self.pj:
1814
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
1815
+ for x1 in set(x[1] for x in missing_behavior_category):
1816
+ self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1)
1817
+
1818
+ if self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]:
1819
+ index = str(max([int(k) for k in self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]]) + 1)
1820
+ else:
1821
+ index = "0"
1822
+
1823
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][index] = {
1824
+ "name": x1,
1825
+ cfg.COLOR: "",
1826
+ }
1827
+
1780
1828
  if response == cfg.CANCEL:
1781
1829
  return {cfg.CANCEL: True}
1782
1830
 
@@ -89,9 +89,10 @@ def export_ethogram(self) -> None:
89
89
  return
90
90
  pj = dict(cfg.EMPTY_PROJECT)
91
91
  pj[cfg.ETHOGRAM] = dict(r)
92
- # behavioral categories
93
92
 
93
+ # behavioral categories
94
94
  pj[cfg.BEHAVIORAL_CATEGORIES] = list(self.pj[cfg.BEHAVIORAL_CATEGORIES])
95
+ pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict(self.pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
95
96
 
96
97
  # project file indentation
97
98
  file_indentation = self.config_param.get(cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
@@ -309,6 +310,7 @@ def import_ethogram_from_dict(self, project: dict):
309
310
  """
310
311
  # import behavioral_categories
311
312
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = list(project.get(cfg.BEHAVIORAL_CATEGORIES, []))
313
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = list(project.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
312
314
 
313
315
  # configuration of behaviours
314
316
  if not (cfg.ETHOGRAM in project and project[cfg.ETHOGRAM]):
boris/utilities.py CHANGED
@@ -21,14 +21,12 @@ Copyright 2012-2025 Olivier Friard
21
21
 
22
22
  import csv
23
23
  import datetime as dt
24
- import hashlib
25
24
  import json
26
25
  import logging
27
26
  import math
28
27
  import os
29
28
  import pathlib as pl
30
29
  import re
31
- import socket
32
30
  import subprocess
33
31
  import sys
34
32
  import urllib.parse
@@ -292,27 +290,6 @@ def return_file_header_footer(file_name: str, file_row_number: int = 0, row_numb
292
290
  return header, footer
293
291
 
294
292
 
295
- def bytes_to_str(b: bytes) -> str:
296
- """
297
- Translate bytes to string.
298
-
299
- Args:
300
- b (bytes): byte to convert
301
-
302
- Returns:
303
- str: converted byte
304
- """
305
-
306
- if isinstance(b, bytes):
307
- fileSystemEncoding = sys.getfilesystemencoding()
308
- # hack for PyInstaller
309
- if fileSystemEncoding is None:
310
- fileSystemEncoding = "UTF-8"
311
- return b.decode(fileSystemEncoding)
312
- else:
313
- return b
314
-
315
-
316
293
  def convertTime(time_format: str, sec: Union[float, dec]) -> Union[str, None]:
317
294
  """
318
295
  convert time in base at the current format (S or HHMMSS)
@@ -381,20 +358,6 @@ def count_media_file(media_files: dict) -> int:
381
358
  return sum([len(media_files[idx]) for idx in media_files])
382
359
 
383
360
 
384
- def file_content_md5(file_name: str) -> str:
385
- """
386
- returns the MD5 sum of file content
387
- """
388
- hash_md5 = hashlib.md5()
389
- try:
390
- with open(file_name, "rb") as f:
391
- for chunk in iter(lambda: f.read(4096), b""):
392
- hash_md5.update(chunk)
393
- return hash_md5.hexdigest()
394
- except FileNotFoundError:
395
- return ""
396
-
397
-
398
361
  def txt2np_array(
399
362
  file_name: str, columns_str: str, substract_first_value: str, converters=None, column_converter=None
400
363
  ) -> Tuple[bool, str, np.array]:
@@ -760,19 +723,6 @@ def get_current_points_by_subject(
760
723
  return current_points
761
724
 
762
725
 
763
- def get_ip_address():
764
- """Get current IP address
765
-
766
- Args:
767
-
768
- Returns:
769
- str: IP address
770
- """
771
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
772
- s.connect(("8.8.8.8", 80))
773
- return s.getsockname()[0]
774
-
775
-
776
726
  def check_txt_file(file_name: str) -> dict:
777
727
  """
778
728
  Extract parameters of txt file (test for tsv csv)
@@ -950,7 +900,7 @@ def intfloatstr(s: str) -> int:
950
900
  return s
951
901
 
952
902
 
953
- def distance(p1, p2):
903
+ def distance(p1: tuple, p2: tuple) -> float:
954
904
  """
955
905
  euclidean distance between 2 points
956
906
  """
@@ -987,12 +937,12 @@ def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
987
937
  Calculate the oriented angle between two segments.
988
938
 
989
939
  Args:
990
- P1: Coordinates of the vertex
991
- P2: Coordinates of the first point
992
- P3: Coordinates of the second point
940
+ P1 (tuple): Coordinates of the vertex
941
+ P2 (tuple): Coordinates of the first point
942
+ P3 (tuple): Coordinates of the second point
993
943
 
994
944
  Returns:
995
- The oriented angle between the two segments in degrees.
945
+ float: The oriented angle between the two segments in degrees.
996
946
  """
997
947
 
998
948
  x1, y1 = P1
@@ -1008,6 +958,36 @@ def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
1008
958
  return oriented_angle
1009
959
 
1010
960
 
961
+ def oriented_angle_trigo(B: Tuple[float, float], A: Tuple[float, float], C: Tuple[float, float]) -> float:
962
+ """
963
+ Calculates the oriented angle between vectors BA and BC, in degrees.
964
+ The angle is positive in the counter-clockwise (trigonometric) direction.
965
+
966
+ Parameters:
967
+ B: The pivot point (the origin of the vectors BA and BC).
968
+ A, C: Points that define the vectors.
969
+
970
+ Returns:
971
+ Angle in degrees, between 0 and 360.
972
+ """
973
+ # Vectors BA and BC
974
+ v1 = (A[0] - B[0], A[1] - B[1])
975
+ v2 = (C[0] - B[0], C[1] - B[1])
976
+
977
+ # Dot product and 2D cross product (determinant)
978
+ dot = v1[0] * v2[0] + v1[1] * v2[1]
979
+ det = v1[0] * v2[1] - v1[1] * v2[0]
980
+
981
+ # Signed angle in radians, then converted to degrees
982
+ angle_rad = math.atan2(det, dot)
983
+ angle_deg = math.degrees(angle_rad)
984
+
985
+ if angle_deg < 0:
986
+ angle_deg += 360
987
+
988
+ return angle_deg
989
+
990
+
1011
991
  def mem_info():
1012
992
  """
1013
993
  get info about total mem, used mem and available mem using:
boris/version.py CHANGED
@@ -20,5 +20,5 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- __version__ = "9.3.1"
24
- __version_date__ = "2025-04-08"
23
+ __version__ = "9.3.3"
24
+ __version_date__ = "2025-05-07"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.3.1
3
+ Version: 9.3.3
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License-Expression: GPL-3.0-only
@@ -1,28 +1,27 @@
1
- boris/1.py,sha256=rb6Nstw1vIHlBwMnzExVOlgU2F75Tuf1VYRXBTrZUFg,1082
2
1
  boris/__init__.py,sha256=iAtmVMy22TJpMmxVTMSK_6-wXnCbx1ogvWgfYEcbHzU,773
3
2
  boris/__main__.py,sha256=ANjTbXgXDoz2nB1tCtOIllfIVotCa602iebACX7rXaE,764
4
3
  boris/about.py,sha256=KEUz6nryrg8FceVyFsf8sMz-xWd2cGwIUfuVydHxqC4,5366
5
4
  boris/add_modifier.py,sha256=DWqxkKDBm21QH_kPvhpnltwLtFvPxne0VmZ1SY26hj8,26340
6
5
  boris/add_modifier_ui.py,sha256=Y7TLO5uS6zW7zpjXmjA4V_VIp_bFDNtjOTbJ9Q6m-mQ,11601
7
6
  boris/advanced_event_filtering.py,sha256=VlvU12mL6xYacZOvJAi5uLpHMcmAw5Pvuvmka-PN29c,15469
8
- boris/behav_coding_map_creator.py,sha256=gzBmfMmG-BsvREdkmiCtu73OuvnHkWb_aVLsMKxTZzU,39071
7
+ boris/behav_coding_map_creator.py,sha256=_WmfWTYkKh_a7pZa49h2GtORCi6h8joZTWihud6YDBE,38826
9
8
  boris/behavior_binary_table.py,sha256=bpmRDpEjq0rw3YOCoN_He3kfUe8A_R6E48kQR7KnkH8,12453
10
9
  boris/behaviors_coding_map.py,sha256=xIGJxp2eghrpiGDmYH73eJPERuyc4A_54uT-Got3zTs,7302
11
10
  boris/boris_cli.py,sha256=n0OiVvZM1gM6E7yKaff9wlgmpAGK4TK052VRi8AabJo,13196
12
11
  boris/cmd_arguments.py,sha256=oWb-FvhKLbKJhATlTHy9muWu8XnnUfOZ-3Fmz2M8Yzc,1848
13
12
  boris/coding_pad.py,sha256=fBKdp7DDyupySJosIYtqNd8s2E-GruzCgVhDFsoVWKE,10986
14
- boris/config.py,sha256=cy4MSVFMHYaEHpHdncljuiaMSTpV6rFNm1OyVYF28Bc,17295
13
+ boris/config.py,sha256=rPfhmdE5XilliNjioC1KuDL_LNze4MZBDy_p0zK4tt8,17349
15
14
  boris/config_file.py,sha256=1-2ZmTvKET57rwrLR1dXblt0AxMpGB1LAiHxu-Sy8es,13543
16
15
  boris/connections.py,sha256=rVI18AuXh8cEnnoCKJk0RMWAaiNOpiaS554Okgk3SBY,19383
17
16
  boris/converters.py,sha256=c1Jps-URoglY5ILQHz-pCCf6-4DFUHZLtqr_ofsrFg0,11722
18
17
  boris/converters_ui.py,sha256=uu7LOBV_fKv2DBdOqsqPwjGsjgONr5ODBoscAA-EP48,9900
19
18
  boris/cooccurence.py,sha256=tVERC-V8MWjWHlGEfDuu08iS94qjt4do-38jwI62QaY,10367
20
- boris/core.py,sha256=f3sEuCVf9zYzyuPuLviYI9IddrZgNWhz5EqueQmNJtc,234758
19
+ boris/core.py,sha256=EK19Xn0Z_sxu9TaqeRimMzzpn9ThZdTqUtlJJXz6pVY,233512
21
20
  boris/core_qrc.py,sha256=T3ki5e2Pj0I0QBGz63MPUgZzl8F_VHZwSq074mRNBDU,650669
22
21
  boris/core_ui.py,sha256=SeC26uveDCjrCBLsRPuQ6FaapKfON_HIRcQStEDLhl4,76384
23
22
  boris/db_functions.py,sha256=Uw9wWH_Pe-qNzpV1k21YG_jKsoOmfY_iiK_7ARZHGDc,13352
24
23
  boris/dev.py,sha256=9pUElbjl9g17rFUJXX5aVSu55_iIKIuDxNdrB0DI_d0,3671
25
- boris/dialog.py,sha256=IAX5_CtTfEUGm5lhzxajjx0oVTs8HNEAw2twTW8AmkE,32859
24
+ boris/dialog.py,sha256=6MayZdOsLZCXP1ns02EH9bygYbWPNoEKrXNQrOaw_aw,33071
26
25
  boris/duration_widget.py,sha256=GjZgCAMGOcsNjoPiRImEVe6yMkH2vuNoh44ulpd5nlg,6924
27
26
  boris/edit_event.py,sha256=2hpxn9DYuJW1CK02hF4iU0--J_0C_KTiN9gGZ1frJBc,7678
28
27
  boris/edit_event_ui.py,sha256=vhglQrhkF9tt0HVlkXnkG7symW0eOFA7nhbk6l_qn3k,7772
@@ -34,38 +33,38 @@ boris/export_events.py,sha256=3B336WEA0g_8oW3VDo_kfq5D0ISu-e7z2f-_ROUvU9c,39756
34
33
  boris/export_observation.py,sha256=SvKhuGa-Ag_kK3igL9DFdJ0TKoQLDneu54R_uiSHUyo,50813
35
34
  boris/external_processes.py,sha256=vpmhA4Lj2GblBIrDD0YjesB8HPOgx4K9gSWVhTop4Cg,11927
36
35
  boris/geometric_measurement.py,sha256=4pI-AYpBSFlJBqS-f8dnkgLtj_Z2E5kwwAdh6WwZ4kk,35049
37
- boris/gui_utilities.py,sha256=5vjIWbUOHFbqKSti-kT0GoteBBEQ5fUYdNGdMxcg_0A,4607
36
+ boris/gui_utilities.py,sha256=2HdWFxo2y0oxC29VJAA3R-TOMxVbOy3FuVwspjrTD6A,5519
38
37
  boris/image_overlay.py,sha256=zZAL8MTt2i2s58CuX81Nym3rJ5pKiTeP4AO8WbIUonM,2527
39
38
  boris/import_observations.py,sha256=hwEPIag741AXTFIuxDdZLDvLrsmvaqTkjyTjQu5M_RA,8798
40
39
  boris/irr.py,sha256=o5QN3B2b-02AUkrklMJCitFGsfiUDtmI0MxUbPv2cBg,22472
41
40
  boris/latency.py,sha256=48z9L_A582-wKCfD0M3h0uyYkeL2ezjlQAS_GzeoOe0,9739
42
41
  boris/measurement_widget.py,sha256=lZV62KtK6TjdoNbKxj3uyNAuL5dfnQnn7mYwzMo-dOM,4480
43
- boris/media_file.py,sha256=QzUC0mT905SzlONvcXUJB2OCxhj8kJ0h0W6PN1ssSIY,4722
42
+ boris/media_file.py,sha256=Wnw-PCyAz6CA00zhjrx0UTgXZ0wmHuNlnElV_TzJ_2M,4818
44
43
  boris/menu_options.py,sha256=UEB3GxRh6YKNCg67qbhOVhJW1ZOznuPe15bADc_CNTI,7062
45
- boris/modifier_coding_map_creator.py,sha256=AHGi1s-T0EB1p9qazQerQH1IlsrF8ND9v0OvLC-fWW4,33643
44
+ boris/modifier_coding_map_creator.py,sha256=NQHy_txgxKZnGByXiro_Oy_cq4DrFaFiAYwVp1CWrTs,33281
46
45
  boris/modifiers_coding_map.py,sha256=oT56ZY_PXhEJsMoblEsyNMAPbDpv7ZMOCnvmt7Ibx_Y,4554
47
46
  boris/mpv-1.0.3.py,sha256=EXRtzQqFjOn4wMC6482Ilq3fNQ9N1GRP1VxwLzdeaBY,88077
48
47
  boris/mpv.py,sha256=EfzIHjPbgewG4w3smEtqEUPZoVwYmMQkL4Q8ZyW-a58,76410
49
48
  boris/mpv2.py,sha256=IUI4t4r9GYX7G5OXTjd3RhMMOkDdfal_15buBgksLsk,92152
50
- boris/observation.py,sha256=oop08nflDLZAgDbIB8GOiVdTgLhppJ_ODH0Z24cyqvE,57176
51
- boris/observation_operations.py,sha256=j7TSGzWSRmAdmD7vdrfvEBFtCfM0HXbo9VeIHNew2PY,105748
49
+ boris/observation.py,sha256=d-7q-RkMHuLDV87nF4yahvDFPYhlXp6GmE80vckn5zU,57073
50
+ boris/observation_operations.py,sha256=Eh-D4ApnuHaXX6fhrFwSsAvEFjMHu7eaI_9vci-drgQ,105790
52
51
  boris/observation_ui.py,sha256=DAnU94QBNvkLuHT6AxTwqSk_D_n6VUhSl8PexZv_dUk,33309
53
52
  boris/observations_list.py,sha256=NqwECGHtHYmKhSe-qCfqPmJ24SSfzlXvIXS2i3op_zE,10591
54
53
  boris/otx_parser.py,sha256=70QvilzFHXbjAHR88YH0aEXJ3xxheLS5fZGgHFHGpNE,16367
55
54
  boris/param_panel.py,sha256=G0XzNmJIX89-n2OQTDccuY_wWMhr3p7GB4ZorbU6EWc,8786
56
55
  boris/param_panel_ui.py,sha256=4emQDFmuL4_R7bKxosLjdUb-VSPWkDm7suy38F5EKcA,13260
57
- boris/player_dock_widget.py,sha256=aDC6a7JEEY64TWMDU_jBBaavN-Z-fd9JQo9BnnNI4_M,6303
56
+ boris/player_dock_widget.py,sha256=cJ2UB6qfdxrk22nLWgOzs5EomCfR7OcznJq67UjF8dg,6186
58
57
  boris/plot_data_module.py,sha256=6QbLKfyGp4TYRyHnB9G45y5XrpeXLytcorltEAWfYak,16562
59
58
  boris/plot_events.py,sha256=CF6gnsTeaPG-P1USwh4An2s31NoMJ1roHDImcQrQj3c,24060
60
59
  boris/plot_events_rt.py,sha256=xig__Uea3mQqO5raMBVB3pm3vuQkjAbJpwSS7AwIob8,8327
61
60
  boris/plot_spectrogram_rt.py,sha256=JV8N7T8133wGVhlPxmgOb426u1g1p21-LbTqgaeddkk,8361
62
61
  boris/plot_waveform_rt.py,sha256=05JN_6HCq674ROore_6PNw93GQNZJQDlDxp2ODAFkkA,7474
63
- boris/plugins.py,sha256=FSeZdbGFPGfi5TDM3RQG6mEVwsp1cGKhJIZs-H0BMy4,9901
62
+ boris/plugins.py,sha256=CCS1I44OMkGZqcfLGKNPGfEQXPgngocy5YhWveXQPKM,10029
64
63
  boris/preferences.py,sha256=qPfd9Tyg7u30kXwVqMOgkdy2RXri9bItRa5U2-ZVQmg,16847
65
64
  boris/preferences_ui.py,sha256=D2bFLb3E0m6IwSeqKoItRDiqvPmJGoeXLHF2K02n1Zo,26293
66
- boris/project.py,sha256=hAeAb5pD0u_l0bezU9ePvbTOYQKfxrFGvYB2NAqSDHg,84377
65
+ boris/project.py,sha256=Yrpoqr5kF8wlvbeYiwm_RK2A9-dLfnxfnhp8TU5ZQbM,86452
67
66
  boris/project_functions.py,sha256=mPaKTjcegsC6n-J8ZsOhWh_4TepJ3Y-vwXiIgl9nij0,79702
68
- boris/project_import_export.py,sha256=1FdsYFzZ_jrhPRaH7xEkcPnh-hQXN4HFz1PhsIsSoL8,38361
67
+ boris/project_import_export.py,sha256=oBG1CSXfKISsb3TLNT-8BH8kZPAzxIYSNemlLVH1Lh8,38560
69
68
  boris/project_ui.py,sha256=yB-ewhHt8S8DTTRIk-dNK2tPMNU24lNji9fDW_Xazu8,38805
70
69
  boris/qrc_boris.py,sha256=aH-qUirYY1CGxmTK1SFCPvuZfazIHX4DdUKF1gxZeYM,675008
71
70
  boris/qrc_boris5.py,sha256=prnOw7VGXWXRuVCYp_yIrmWhrlG1F9rx-3BQvkPenjY,161608
@@ -78,8 +77,8 @@ boris/synthetic_time_budget.py,sha256=3Eb9onMLmgqCLd50CuxV9L8RV2ESzfaMWvPK_bXUMM
78
77
  boris/time_budget_functions.py,sha256=y5He8crz0xsTxVfz0jATwFFQVnPAIrNHja_0sF6NtRE,52551
79
78
  boris/time_budget_widget.py,sha256=z-tyITBtIz-KH1H2OdMB5a8x9QQLK7Wu96-zkC6NVDA,43213
80
79
  boris/transitions.py,sha256=_aZJfJWv3EBrtmQ7qsdTCayQo6uWU7BXqtQQgflEhr4,12250
81
- boris/utilities.py,sha256=H69vrYGkQuQJVE_ywcYC6DNYcS8VTC-GoClmVEUXacA,52748
82
- boris/version.py,sha256=8YtWciv9CU7BQCzJyVkJgpiFVxZsHgPZWue0GIrwFJU,787
80
+ boris/utilities.py,sha256=7iZ6XQ8G1by9j-xVjTCBX6dp9FQg_9MbC5Ym1sZJYp0,52639
81
+ boris/version.py,sha256=wqhoE3kKrpzgOgOUWT2QNHWqh24C1Kp05JOwqZKmU8k,787
83
82
  boris/video_equalizer.py,sha256=FartoGghFK-T53zklP70rPKYqTuzL8qdvfGlsOF2wwc,5854
84
83
  boris/video_equalizer_ui.py,sha256=1CG3s79eM4JAbaCx3i1ILZXLceb41_gGXlOLNfpBgnw,10142
85
84
  boris/video_operations.py,sha256=mh3iR__Sm2KnV44L_sW2pOo3AgLwlM7wiTnnqQiAVs4,9381
@@ -87,6 +86,7 @@ boris/view_df.py,sha256=AKScLASX2Uatw7rqPbsnio83eVT4GZYCFhL091eMvlY,3370
87
86
  boris/view_df_ui.py,sha256=CaMeRH_vQ00CTDDFQn73ZZaS-r8BSTWpL-dMCFqzJ_Q,2775
88
87
  boris/write_event.py,sha256=Fsy_apFl7RLnRsBAwXqACr_URnE_QoAFiPMh0o95ANg,23852
89
88
  boris/analysis_plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
+ boris/analysis_plugins/_latency.py,sha256=vLWCPh0cPpAEpdMboCkVIuYO1e3pJlP5tEICe27Sqms,2027
90
90
  boris/analysis_plugins/number_of_occurences.py,sha256=IDyDrdezqvSKT3BlD8QWpSYk8X9nnBBLI80OUnFJ3bY,509
91
91
  boris/analysis_plugins/number_of_occurences_by_independent_variable.py,sha256=t39bmmmZIDCSbcDvVeiKAhKNNP2SdpHp417JczHEnP4,793
92
92
  boris/analysis_plugins/time_budget.py,sha256=C1wNYwd5Jugr8h5z2aXRUBY8dF8pD4n953dPwNHY5VY,2244
@@ -96,9 +96,9 @@ boris/portion/dict.py,sha256=SyHxc7PfDw2ufNLFQycwJtzmRfL48rDp4UrM2KN7IWc,11282
96
96
  boris/portion/func.py,sha256=3TkQtFKLfsqntwd27HSGHceFhnXHmT-EbNMqktElC5Q,2174
97
97
  boris/portion/interval.py,sha256=bAdUiJjGeUAPgsBAImwNeviiwfQq5odfhFZccAWzOTA,20299
98
98
  boris/portion/io.py,sha256=ppNeRpiLNrocF1yzGeuEUIhYMf2LfsR-cji3d0nmvUs,6371
99
- boris_behav_obs-9.3.1.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
100
- boris_behav_obs-9.3.1.dist-info/METADATA,sha256=6OEa47oUYBrON6hG2Rg7PkIqMb5Z6Ty9upy1O7vwmuA,4518
101
- boris_behav_obs-9.3.1.dist-info/WHEEL,sha256=MAQBAzGbXNI3bUmkDsiV_duv8i-gcdnLzw7cfUFwqhU,109
102
- boris_behav_obs-9.3.1.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
103
- boris_behav_obs-9.3.1.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
104
- boris_behav_obs-9.3.1.dist-info/RECORD,,
99
+ boris_behav_obs-9.3.3.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
100
+ boris_behav_obs-9.3.3.dist-info/METADATA,sha256=oiGWIVEJKIfM9EOznjvVeDr15l2rtecxzculq_IsJAg,4518
101
+ boris_behav_obs-9.3.3.dist-info/WHEEL,sha256=oSJJyWjO7Z2XSScFQUpXG1HL-N0sFMqqeKVVbZTPkWc,109
102
+ boris_behav_obs-9.3.3.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
103
+ boris_behav_obs-9.3.3.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
104
+ boris_behav_obs-9.3.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
boris/1.py DELETED
@@ -1,45 +0,0 @@
1
- import time
2
- from PIL import Image, ImageDraw, ImageFont
3
- import mpv
4
-
5
- player = mpv.MPV()
6
-
7
- player.loop = True
8
- player.play('/home/olivier/gdrive_sync/src/python/generate_video_test/video1.mp4')
9
- player.wait_until_playing()
10
-
11
- font = ImageFont.truetype('DejaVuSans.ttf', 40)
12
-
13
- overlay = player.create_image_overlay()
14
-
15
- img = Image.new('RGBA', (400, 150), (255, 255, 255, 0))
16
- d = ImageDraw.Draw(img)
17
- d.text((10, 10), 'Hello World', font=font, fill=(0, 255, 255, 128))
18
- #d.text((10, 60), f't={ts:.3f}', font=font, fill=(255, 0, 255, 255))
19
-
20
- pos = 100
21
-
22
- overlay.update(img, pos=(2*pos, pos))
23
-
24
-
25
- while not player.core_idle:
26
- pass
27
-
28
-
29
- '''
30
- for pos in range(0, 500, 5):
31
- ts = player.time_pos
32
- if ts is None:
33
- break
34
-
35
- img = Image.new('RGBA', (400, 150), (255, 255, 255, 0))
36
- d = ImageDraw.Draw(img)
37
- d.text((10, 10), 'Hello World', font=font, fill=(0, 255, 255, 128))
38
- d.text((10, 60), f't={ts:.3f}', font=font, fill=(255, 0, 255, 255))
39
-
40
- overlay.update(img, pos=(2*pos, pos))
41
- time.sleep(0.05)
42
-
43
-
44
- overlay.remove()
45
- '''